The frontend is rebuilt screen by screen from the handoff: warm cream ground, terracotta + sage accents, Caprasimo over Figtree, every control a pill. Same React/Vite/TanStack stack and the same lib/ data layer; the presentation is new. - Tokens: web/src/styles/index.css declares the handoff's styles.css variables through Tailwind's @theme under the same names; dark mode is those variables overridden on <html> by the handoff's pansy-theme.js, inlined in index.html so it runs before first paint. Lucide glyphs at stroke 2.75; a small pill kit (Button, Dialog, Field, Seg, Toggle, Tag, toast). - Login / Register: the centered column over soft accent circles; OIDC button and signup footer still follow /auth/providers. - Gardens: cards with a real SVG plot thumbnail (objects + plant-colored dots from /full), a `plan` tag for "<name> — <year>" copies, shares line, Open + share/copy/edit/delete; New garden / Share / Plan-a-season dialogs. - Plants: monogram markers derived from the name (collision-resolved across the catalog — replaces emoji icons), category chips, expandable lot cards, the scan-packet flow as a two-step dialog that never auto-creates. - Settings: Appearance (theme seg), Who gets in (read-only sign-in config), Garden assistant (self-saving toggle + chat/vision model fields), You. - Editor: a new canvas with the prototype's pointer model (wheel-to-cursor, pinch about the centroid, 3″ snap, one PATCH per drop, semantic-zoom monograms/labels), plus corner resize handles; desktop three-card workspace (toolkit | plan | rail with Plot/Journal/History/Assistant) and, below 760px of container width, the phone chrome (header, peek panel, tool strip, mode bar). Seasons as a segmented control over the years with data plus plan copies; Undo re-reads history before reverting the newest step. - Public read-only view and the register page restyled to match. - GET /settings gains a read-only `auth` view (registration mode, local auth, OIDC issuer) so the Settings page can show what's in force. - README / DESIGN.md / CLAUDE.md updated; @use-gesture/react dropped. Co-Authored-By: Claude Fable 5 <[email protected]>
464 lines
12 KiB
CSS
464 lines
12 KiB
CSS
@import 'tailwindcss';
|
|
|
|
/* ─────────────────────────────────────────────────────────────────────────────
|
|
"Organic" — the design tokens from docs/design_handoff_pansy_ui (its
|
|
_ds/…/styles.css is the source of truth; these are the same values under the
|
|
same names). Tailwind utilities reference these variables by name, and dark
|
|
mode is done by overriding the SAME variables on <html> from the theme script
|
|
in index.html — so there is exactly one set of names, no second stylesheet,
|
|
and no class swapping. Every namespace is reset first so the only colors,
|
|
fonts and radii that exist in this app are the design's.
|
|
───────────────────────────────────────────────────────────────────────────── */
|
|
@theme {
|
|
--color-*: initial;
|
|
--font-*: initial;
|
|
--shadow-*: initial;
|
|
--radius-*: initial;
|
|
|
|
--color-bg: #f5ead8;
|
|
--color-surface: #ebddc5;
|
|
--color-text: #201e1d;
|
|
--color-accent: #c67139;
|
|
--color-accent-2: #7a8a5e;
|
|
--color-divider: color-mix(in srgb, #201e1d 16%, transparent);
|
|
/* The monogram ink on plant markers; does not change between modes. */
|
|
--color-paper: #fffaf1;
|
|
|
|
--color-neutral-100: #f9f4ed;
|
|
--color-neutral-200: #eee7db;
|
|
--color-neutral-300: #dcd3c4;
|
|
--color-neutral-400: #c0b6a5;
|
|
--color-neutral-500: #a19786;
|
|
--color-neutral-600: #82796a;
|
|
--color-neutral-700: #645c50;
|
|
--color-neutral-800: #474238;
|
|
--color-neutral-900: #2e2b25;
|
|
|
|
--color-accent-100: #fff2eb;
|
|
--color-accent-200: #ffe1d0;
|
|
--color-accent-300: #ffc6a5;
|
|
--color-accent-400: #f6a06b;
|
|
--color-accent-500: #d67f48;
|
|
--color-accent-600: #b2622d;
|
|
--color-accent-700: #8c491a;
|
|
--color-accent-800: #643312;
|
|
--color-accent-900: #402310;
|
|
|
|
--color-accent-2-100: #f0fae1;
|
|
--color-accent-2-200: #e1eecc;
|
|
--color-accent-2-300: #ccdbb2;
|
|
--color-accent-2-400: #aebf92;
|
|
--color-accent-2-500: #8fa073;
|
|
--color-accent-2-600: #728157;
|
|
--color-accent-2-700: #56633f;
|
|
--color-accent-2-800: #3d472b;
|
|
--color-accent-2-900: #272e1b;
|
|
|
|
/* Ink + canvas roles alias the --p-* properties declared on :root below (the
|
|
names the theme script overrides), so text-ink-mute etc. flip with the
|
|
theme as well. */
|
|
--color-ink-strong: var(--p-ink-strong);
|
|
--color-ink-soft: var(--p-ink-soft);
|
|
--color-ink-mute: var(--p-ink-mute);
|
|
--color-field: var(--p-field);
|
|
|
|
--font-heading: 'Caprasimo', system-ui, sans-serif;
|
|
--font-body: 'Figtree', system-ui, sans-serif;
|
|
|
|
--radius-sm: 8px;
|
|
--radius-md: 16px;
|
|
--radius-xl: 22px;
|
|
--radius-lg: 28px;
|
|
}
|
|
|
|
@layer base {
|
|
:root {
|
|
/* Canvas + ink tokens (light values). Dark values live in index.html. */
|
|
--p-field: #efe3c9;
|
|
--p-grid-ink: #201e1d;
|
|
--p-ink-strong: #474238;
|
|
--p-ink-soft: #645c50;
|
|
--p-ink-mute: #82796a;
|
|
--p-bed-fill: #e3d0ac;
|
|
--p-bed-stroke: #b5a37f;
|
|
--p-ing-fill: #d6bf98;
|
|
--p-ing-stroke: #b1a07c;
|
|
--p-path-fill: #ece1cb;
|
|
--p-path-stroke: #cabfa6;
|
|
--p-bag-fill: #d8c5a2;
|
|
--p-bag-stroke: #a99677;
|
|
--p-bkt-fill: #cfc3ad;
|
|
--p-bkt-stroke: #9a8d76;
|
|
--p-tree-fill: #dce9c6;
|
|
--p-tree-stroke: #8fa073;
|
|
--p-str-fill: #d9cfbc;
|
|
--p-str-stroke: #a3947c;
|
|
|
|
/* Elevation — ink-tinted on the light ground (the theme script swaps in
|
|
ambient darkness). Plain custom properties rather than Tailwind's shadow
|
|
namespace, which inlines its values and so could not follow the override:
|
|
use the .elev-* classes below. */
|
|
--shadow-sm: 0 1px 2px color-mix(in srgb, #2e2b25 14%, transparent);
|
|
--shadow-md: 0 3px 10px color-mix(in srgb, #2e2b25 16%, transparent);
|
|
--shadow-lg: 0 12px 32px color-mix(in srgb, #2e2b25 22%, transparent);
|
|
}
|
|
|
|
html,
|
|
body,
|
|
#root {
|
|
height: 100%;
|
|
}
|
|
body {
|
|
margin: 0;
|
|
background: var(--color-bg);
|
|
color: var(--color-text);
|
|
font-family: var(--font-body);
|
|
font-size: 15px;
|
|
line-height: 1.55;
|
|
font-weight: 400;
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
h1,
|
|
h2,
|
|
h3,
|
|
h4,
|
|
h5,
|
|
h6 {
|
|
font-family: var(--font-heading);
|
|
font-weight: 400;
|
|
line-height: 1.12;
|
|
letter-spacing: -0.015em;
|
|
margin: 0;
|
|
}
|
|
h1 {
|
|
font-size: 42px;
|
|
}
|
|
h2 {
|
|
font-size: 32px;
|
|
}
|
|
h3 {
|
|
font-size: 25px;
|
|
}
|
|
h4 {
|
|
font-size: 20px;
|
|
}
|
|
h5 {
|
|
font-size: 16px;
|
|
}
|
|
h6 {
|
|
font-size: 13px;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
a {
|
|
color: var(--color-accent);
|
|
text-underline-offset: 3px;
|
|
}
|
|
a:hover {
|
|
color: var(--color-accent-600);
|
|
}
|
|
button {
|
|
cursor: pointer;
|
|
font: inherit;
|
|
color: inherit;
|
|
}
|
|
button:disabled {
|
|
cursor: not-allowed;
|
|
}
|
|
:focus {
|
|
outline: none;
|
|
}
|
|
:focus-visible {
|
|
outline: 2px solid var(--color-accent);
|
|
outline-offset: 2px;
|
|
}
|
|
::selection {
|
|
background: color-mix(in srgb, var(--color-accent) 30%, transparent);
|
|
}
|
|
::-webkit-scrollbar {
|
|
height: 6px;
|
|
width: 6px;
|
|
}
|
|
::-webkit-scrollbar-thumb {
|
|
background: var(--color-neutral-400);
|
|
border-radius: 99px;
|
|
}
|
|
::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
}
|
|
|
|
/* ── Components — the handful of classes the prototypes lean on ─────────── */
|
|
@layer components {
|
|
.elev-sm {
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
.elev-md {
|
|
box-shadow: var(--shadow-md);
|
|
}
|
|
.elev-lg {
|
|
box-shadow: var(--shadow-lg);
|
|
}
|
|
|
|
/* Buttons: every control is a pill; labels are set in the display face. */
|
|
.btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 6px;
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
font-family: var(--font-heading);
|
|
font-weight: 400;
|
|
font-size: 14px;
|
|
line-height: 1.2;
|
|
color: var(--color-text);
|
|
background: transparent;
|
|
border: 1px solid transparent;
|
|
padding: 8.8px 15.84px;
|
|
border-radius: 999px;
|
|
white-space: nowrap;
|
|
}
|
|
.btn svg {
|
|
display: block;
|
|
flex: none;
|
|
}
|
|
.btn:disabled {
|
|
opacity: 0.45;
|
|
cursor: not-allowed;
|
|
}
|
|
.btn-primary {
|
|
background: var(--color-accent);
|
|
color: var(--color-bg);
|
|
}
|
|
.btn-primary:hover:not(:disabled) {
|
|
background: var(--color-accent-600);
|
|
}
|
|
.btn-primary:active:not(:disabled) {
|
|
background: var(--color-accent-700);
|
|
}
|
|
.btn-secondary {
|
|
border-color: var(--color-divider);
|
|
}
|
|
.btn-secondary:hover:not(:disabled) {
|
|
background: color-mix(in srgb, var(--color-text) 7%, transparent);
|
|
}
|
|
.btn-secondary:active:not(:disabled) {
|
|
background: color-mix(in srgb, var(--color-text) 14%, transparent);
|
|
}
|
|
.btn-ghost {
|
|
color: var(--color-accent);
|
|
padding-inline: 4.4px;
|
|
}
|
|
.btn-ghost:hover:not(:disabled) {
|
|
background: color-mix(in srgb, var(--color-accent) 10%, transparent);
|
|
}
|
|
.btn-ghost:active:not(:disabled) {
|
|
background: color-mix(in srgb, var(--color-accent) 18%, transparent);
|
|
}
|
|
.btn-icon {
|
|
width: 36px;
|
|
height: 36px;
|
|
padding: 0;
|
|
flex: none;
|
|
}
|
|
.btn-block {
|
|
width: 100%;
|
|
}
|
|
/* A bare icon button on a surface (zoom pill, peek close): no border, a soft
|
|
accent wash on hover. */
|
|
.btn-soft:hover:not(:disabled) {
|
|
background: var(--color-accent-100);
|
|
}
|
|
|
|
/* Forms */
|
|
.field > label {
|
|
display: block;
|
|
font-size: 12px;
|
|
margin-bottom: 5px;
|
|
color: color-mix(in srgb, var(--color-text) 70%, transparent);
|
|
}
|
|
.input {
|
|
width: 100%;
|
|
min-height: 36px;
|
|
padding: 6px 14px;
|
|
font: inherit;
|
|
font-size: 14px;
|
|
color: var(--color-text);
|
|
caret-color: var(--color-accent);
|
|
background: var(--color-surface);
|
|
border: 1px solid var(--color-divider);
|
|
border-radius: 999px;
|
|
}
|
|
.input::placeholder {
|
|
color: var(--p-ink-mute);
|
|
opacity: 1;
|
|
}
|
|
.input:hover {
|
|
border-color: color-mix(in srgb, var(--color-text) 45%, transparent);
|
|
}
|
|
.input:focus-visible {
|
|
border-color: var(--color-accent);
|
|
outline-offset: 0;
|
|
}
|
|
.input:disabled,
|
|
.input[readonly] {
|
|
cursor: default;
|
|
}
|
|
.input:disabled {
|
|
opacity: 0.7;
|
|
}
|
|
textarea.input {
|
|
min-height: 90px;
|
|
resize: vertical;
|
|
border-radius: var(--radius-md);
|
|
}
|
|
/* Phone: 16px stops iOS Safari zooming the page on focus. */
|
|
.input-lg {
|
|
font-size: 16px;
|
|
min-height: 44px;
|
|
}
|
|
|
|
/* Tags */
|
|
.tag {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
font-size: 11px;
|
|
letter-spacing: 0.02em;
|
|
padding: 3px 10px;
|
|
border-radius: 999px;
|
|
white-space: nowrap;
|
|
font-weight: 400;
|
|
}
|
|
.tag-accent {
|
|
background: var(--color-accent-100);
|
|
color: var(--color-accent-800);
|
|
}
|
|
.tag-accent-2 {
|
|
background: var(--color-accent-2-100);
|
|
color: var(--color-accent-2-800);
|
|
}
|
|
.tag-neutral {
|
|
background: var(--color-neutral-100);
|
|
color: var(--color-neutral-800);
|
|
}
|
|
.tag-outline {
|
|
border: 1px solid var(--color-accent);
|
|
color: var(--color-accent);
|
|
}
|
|
|
|
/* The raised panel every page builds from: cards, the editor's three regions. */
|
|
.panel {
|
|
background: var(--color-neutral-100);
|
|
border: 1px solid var(--color-divider);
|
|
border-radius: var(--radius-lg);
|
|
}
|
|
|
|
/* Segmented pill control: a neutral-200 track, the active option lifts. */
|
|
.seg {
|
|
display: inline-flex;
|
|
background: var(--color-neutral-200);
|
|
border-radius: 999px;
|
|
padding: 3px;
|
|
}
|
|
.seg-opt {
|
|
padding: 5px 16px;
|
|
cursor: pointer;
|
|
border: none;
|
|
border-radius: 999px;
|
|
white-space: nowrap;
|
|
font-family: var(--font-body);
|
|
font-size: 12.5px;
|
|
font-weight: 700;
|
|
line-height: 1.55;
|
|
background: transparent;
|
|
color: var(--p-ink-soft);
|
|
}
|
|
.seg-opt[aria-pressed='true'] {
|
|
background: var(--color-neutral-100);
|
|
color: var(--color-text);
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
.seg-opt:disabled {
|
|
cursor: default;
|
|
}
|
|
|
|
/* On/Off pill */
|
|
.toggle {
|
|
cursor: pointer;
|
|
border: none;
|
|
border-radius: 999px;
|
|
padding: 6px 18px;
|
|
font-family: var(--font-body);
|
|
font-size: 12.5px;
|
|
font-weight: 700;
|
|
line-height: 1.55;
|
|
background: var(--color-neutral-300);
|
|
color: var(--p-ink-soft);
|
|
}
|
|
.toggle[aria-pressed='true'] {
|
|
background: var(--color-accent-2-300);
|
|
color: var(--color-accent-2-800);
|
|
}
|
|
.toggle:disabled {
|
|
cursor: default;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
/* Chips: category filters, the phone tool strip, rail tabs share this shape. */
|
|
.chip {
|
|
cursor: pointer;
|
|
border: 1px solid var(--color-divider);
|
|
border-radius: 999px;
|
|
padding: 7px 16px;
|
|
font-family: var(--font-body);
|
|
font-size: 13px;
|
|
font-weight: 700;
|
|
line-height: 1.55;
|
|
background: transparent;
|
|
color: var(--p-ink-soft);
|
|
white-space: nowrap;
|
|
}
|
|
.chip[aria-pressed='true'],
|
|
.chip-active {
|
|
background: var(--color-accent-200);
|
|
border-color: var(--color-accent-400);
|
|
color: var(--color-accent-800);
|
|
}
|
|
|
|
/* Dialogs */
|
|
.dialog-backdrop {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: color-mix(in srgb, #201e1d 40%, transparent);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 20px;
|
|
z-index: 50;
|
|
}
|
|
.dialog {
|
|
background: var(--color-neutral-100);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow-lg);
|
|
padding: 26px;
|
|
width: min(420px, 100%);
|
|
max-height: calc(100dvh - 40px);
|
|
overflow-y: auto;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 14px;
|
|
}
|
|
|
|
.hr {
|
|
height: 1px;
|
|
border: 0;
|
|
background: var(--color-divider);
|
|
}
|
|
|
|
/* The editor's camera move: fit/focus animate, drags and zooms don't. */
|
|
.camera-anim {
|
|
transition: transform 0.48s cubic-bezier(0.22, 0.85, 0.3, 1);
|
|
}
|
|
}
|