Files

Handoff: Pansy UI redesign

Overview

A full UI/UX replacement for pansy, the self-hosted garden planner (Go backend + JSON API, see repo DESIGN.md). This package covers every screen: the garden editor (desktop workspace + phone layout), gardens list, plants catalog with seed lots and packet scanning, instance settings, and login — plus a light/dark theme system defaulting to the OS preference.

About the design files

The .dc.html files in this bundle are design references created in HTML — working prototypes showing intended look and behavior, not production code to copy. The task is to recreate these designs in the pansy codebase. The existing frontend (React 19 + TS + Vite + Tailwind 4) is explicitly up for replacement: keep it, or choose anything that renders SVG and talks JSON. Everything here was deliberately built with framework-agnostic primitives (plain SVG canvas, pointer events, CSS custom properties) so it ports anywhere. The Go backend and its API are the fixed contract — this design maps 1:1 onto the existing endpoints.

The prototypes carry in-memory demo data (the real 9-bed garden: five 3'×6' beds, four 2'×8' beds, bags, buckets). Wherever demo state exists, the matching API call is named below.

Fidelity

High-fidelity. Colors, typography, spacing, radii, and interactions are final. Recreate pixel-perfectly. The visual system is "Organic": warm cream ground, terracotta + sage accents, Caprasimo display over Figtree body, everything over-rounded (pills, 1628px radii). Fonts are Google Fonts: Caprasimo:wght@400 and Figtree:wght@400;600;700.

Files

File What it is
Pansy Editor.dc.html The editor — BOTH desktop and phone layouts in one file (breakpoint 760px on container width)
Pansy Gardens.dc.html Gardens list + new-garden and share dialogs
Pansy Plants.dc.html Plant catalog + seed lots + scan-packet and add-plant dialogs
Pansy Settings.dc.html Instance settings (admin) + appearance
Pansy Login.dc.html Login
Pansy Phone Preview.dc.html The editor mounted in an iPhone frame (ios-frame.jsx) — preview aid only, do not implement
pansy-theme.js The theme mechanism: dark-mode token overrides + system-pref logic. Port this pattern directly
_ds/organic-…/styles.css The design-token stylesheet (CSS custom properties + component classes). The single source of visual truth

Each .dc.html is template + logic in one file; read the markup for exact inline styles and the class Component script for behavior math.

Screenshots

screenshots/ holds visual ground truth for every screen in both modes: editor-desktop, editor-phone (in the device frame), gardens, plants, settings, login — each as -light.png and -dark.png. When a spec and a screenshot disagree, flag it rather than guessing; the live HTML file is the tiebreaker.

Design tokens

Core (light) — from styles.css

  • Ground --color-bg: #f5ead8 · surface #ebddc5 · text #201e1d
  • Accent (terracotta) #c67139 with 100900 ramp (#fff2eb → #402310); accent-2 (sage) #7a8a5e with ramp (#f0fae1 → #272e1b)
  • Neutral ramp #f9f4ed → #2e2b25
  • Divider: color-mix(in srgb, #201e1d 16%, transparent)
  • Radii: sm 8 / md 16 / lg 28 / pills 999px (all controls are pills)
  • Shadows: --shadow-sm/md/lg (ink-tinted; see styles.css)
  • Type: h-font Caprasimo 400; body Figtree; base 15px/1.55. Page titles h2 32px, card titles ~1619px Caprasimo, UI labels 12.513.5px Figtree 600700, metadata 11.512.5px at --p-ink-mute

Pansy canvas + ink tokens (light values; declared per-page in :root)

--p-field:#efe3c9        garden field fill        --p-grid-ink:#201e1d (grid lines @ .06/.12 opacity)
--p-ink-strong:#474238   primary secondary text   --p-ink-soft:#645c50   --p-ink-mute:#82796a
--p-bed-fill:#e3d0ac / --p-bed-stroke:#b5a37f     raised bed
--p-ing-fill:#d6bf98 / #b1a07c (dash 10 7)        in-ground plot
--p-path-fill:#ece1cb / #cabfa6 (dash 3 8)        path
--p-bag-fill:#d8c5a2 / #a99677                    grow bag
--p-bkt-fill:#cfc3ad / #9a8d76                    container/bucket
--p-tree-fill:#dce9c6 / #8fa073 (dash 12 8)       tree canopy
--p-str-fill:#d9cfbc / #a3947c                    structure

Dark mode

pansy-theme.js holds the full override map — dark is implemented only by overriding these custom properties on <html> (plus color-scheme). Key values: bg #252220, surface #33302a, text #f1e9da, cards #2e2b25, field #2b2823, grid ink flips to #f5ead8, accent lifts to #d67f48, tag/tint pairs flip (e.g. accent-2-100 → #2d3520 with accent-2-800 → #e1eecc). Plant marker colors do NOT change between modes.

  • Preference: 'system' | 'light' | 'dark', persisted (localStorage['pansy-theme'] in the prototype; per-user setting in production). Default system, live-updates on prefers-color-scheme change.
  • UI: icon button in every nav cycling system→light→dark (monitor/sun/moon, Lucide, stroke 2.75) + a System/Light/Dark segmented control in Settings → Appearance.

Plant markers (demo palette)

Solid circle in the plant's color + 12 letter monogram in Caprasimo #fffaf1: Garlic #97a97c G · Tomato #c8553d T · Cucumber #6f8f4f C · Watermelon #46683c W · Basil #5f8f45 B · Pepper #b2622d P · Marigold #d9912f Ma · Melon #c2913a Me. In production the color comes from plants.color; derive the monogram from the name. This replaces the old emoji icons.

Screens

1. Editor — desktop (≥760px container)

Three-region workspace under a nav bar; every region is a --color-neutral-100 card, --radius-lg, 1px divider border, in a 14px-gap grid: 216px | minmax(0,1fr) | 336px.

  • Nav: brand (sprout icon, sage, stroke 2.75 + "pansy" Caprasimo 18), links Gardens/Plants (active = accent), right cluster: theme toggle, settings gear, avatar circle (32px, sage-300 bg).
  • Left card — Toolkit: 7 draggable object kinds (Bed 3'×6', Grow bag 1'4", Container 2', In-ground 6'7", Tree 9'10", Path 3'3"×9'10", Structure 6'7"), each a pill row: mini shape swatch (SVG, true proportions, kind's fill/stroke) + bold 13px label + 11px size in ink-mute. Click arms (accent-200 bg + accent-400 border, crosshair cursor, click canvas to place); drag-drop onto canvas also places. When a bed is focused the card swaps to: back chevron + bed name (Caprasimo 16), search input, plant list rows (14px color dot + name + spacing), same arm/drag behavior.
  • Center card — canvas: header row (18px padding): "Home Garden" Caprasimo 19 + size 40 × 28 ink-mute + focus crumb (/ Bed 2 accent-700) + right cluster (wraps at narrow widths): season segmented pill control [2025 | 2026 | 2027 plan] on neutral-200 track (active = neutral-100 pill + shadow-sm) and an Undo pill button (disabled at 45% opacity when stack empty). A status banner strip (accent-2-200 bg / accent-2-800 text, 13px) appears under the header when season ≠ 2026: 2025 = read-only; 2027 plan = "separate copy" note.
  • Canvas (SVG): field = rounded rect (rx 18) --p-field, stroke accent-2-500 3px/scale; 1ft grid minor / 5ft major lines. Objects positioned by center + rotation (translate(x y) rotate(deg)), stroke width 2.5/scale (3.5 accent when selected). Bed names in 13px/scale above plantable objects; centered inside non-plantable ones. Plops (planting patches) = circles in plant color, opacity .94, monogram shown when r·scale ≥ 9px, name below when ≥ 34px (semantic zoom). Selection = dashed accent outline offset 7/scale + size label (3 × 6) under the object. Ghost preview follows cursor when armed, 55% opacity dashed.
  • Zoom pill floating bottom-right of canvas: / fit / +.
  • Right card — rail with 4 pill tabs: Plot (garden summary: counts line + full plant roster with dots and "64 in Long bed A"-style locations; or, with a selection, the inspector: name input, kind tag + size, "Growing:" roster, actions [Plant this · rotate 90° · delete]; plop selection: plant swatch + count + patch size + "Pull it out"), Journal (entry cards: object tag accent-2-700 + date, 13px body; input + add pill, Enter submits, attaches to selection/focus/garden), History ("Every change — yours or the assistant's — is one undoable step." + newest-first op pills with timestamps + "Undo the last step"), Assistant (chat bubbles: user right-aligned accent-200/accent-900 radius 18/18/4/18, assistant left bg/divider 18/18/18/4; input + send).

2. Editor — phone (<760px container)

Same file, same state, different chrome (matches repo DESIGN.md #99/#101):

  • Header 10px padding: back circle button (38px; exits focus first, else → Gardens), garden/bed name Caprasimo 17 (ellipsis), right: theme button, season chip (tap cycles seasons, shows "26"/"27 plan"), undo circle button.
  • Canvas fills the middle; floating fit button bottom-right. Pinch to zoom (two-pointer), one-finger pan/drag, tap = select, touch drag threshold 7px (mouse 3px).
  • Peek panel (inspector on selection, or Journal/Assistant modes): docked between canvas and mode bar, max-height 45%, radius 22px top corners, shadow-lg, title + close X; canvas stays visible above it. Inputs inside are 16px font (prevents iOS zoom). All hit targets ≥ 44px.
  • Tool strip (Build or Plants mode, hidden while peek is open): horizontal scroll row of pill chips (min-height 44) above the mode bar. In focus + Plants mode the first chip is a primary "Done".
  • Mode bar, always visible, surface bg, 4 items (icon 19px + 11px label): Build, Plants, Journal, Assistant; active = accent-200 pill. Bottom padding calc(6px + env(safe-area-inset-bottom)).
  • Focusing a bed on phone auto-switches to Plants mode; plants can also be tapped straight into any bed without focusing.

3. Gardens list

Nav + max-width 1080px page. Title row: "Gardens" h2 + tagline ink-mute + "New garden" primary pill. Card grid repeat(auto-fill, minmax(300px,1fr)), 18px gap. Each card: SVG plot thumbnail (150px band, --p-field bg, real object layout + plant-colored dots/pills), name Caprasimo 18 (+ optional plan accent tag), size right-aligned, 13px meta line, optional "Shared with lauren@ · editor" in accent-2-700, footer: Open (primary pill, flex 1) + share / copy / delete icon buttons. Dialogs: New garden (name, width/height in ft — note: API stores cm; "Break ground" primary) and Share (invite by email + role chips toggling viewer/editor on tap, read-only-link toggle + dashed link pill). Deep-copy card behavior mirrors POST /gardens/:id/copy.

4. Plants catalog

Title row + two actions: "Scan a packet" (secondary, camera icon) and "Add a plant" (primary). Filter row: search input (260px) + category pill chips (All/Vegetables/Herbs/Flowers/Fruit), live filtering. Card grid minmax(240px,1fr): 40px color swatch with Caprasimo monogram, name Caprasimo 16.5 (ellipsis), sub line Category · spacing · days, built-in neutral tag for seeded plants, seed-lot summary line; clicking expands the card (accent border) to show lot cards (vendor bold + "packed for YYYY" + detail line with derived remaining) — remaining is derived server-side, never stored. Scan-packet dialog (the #81 flow): step 1 = dashed drop zone + camera icon + copy "The vision model reads it into fields. It only reads; nothing is saved until you confirm." Step 2 = extracted fields grid (Variety/Vendor/Packed for/Quantity) + ranked match list as radio pills ("Garlic (built-in)" / "Garlic — Music (yours) · best match" / "Create a new plant") + "Add the lot" primary. Never auto-create — the user confirms the match. Add-plant dialog: name, category select, spacing (in), marker color swatch row (6 curated swatches, accent ring on selection).

5. Settings (admin)

max-width 720px column of cards: Appearance (theme seg), Who gets in (signup Open/Closed seg, Local passwords toggle pill with "pure-Authentik" note, read-only OIDC issuer input, first-user-is-admin note), Garden assistant (live status tag e.g. "key present · glm-5.2 live", on/off toggle, chat model + vision model inputs with inherit-from-env placeholders, key-stays-in-env note — precedence Settings → env → default), You (display name, email read-only, Sign out ghost). Maps to GET/PATCH /settings + /capabilities.

6. Login

Centered 400px column over two soft blurred accent circles (decoration, 5055% opacity): sprout 44px + "pansy" Caprasimo 40 + tagline "Plan the plot. Keep the notes. Grow the thing." Card: email + password fields (16px font), "Into the garden" primary block pill, "or" divider, "Sign in with Authentik" secondary block (lock icon; label from PANSY_OIDC_BUTTON_LABEL; hide per GET /auth/providers), footer "New here? Create an account — the first one becomes admin."

Interactions & behavior (editor core)

Viewport: world = garden cm, screen = translate(tx,ty) scale(s) on one SVG group (s = px/cm, clamp 0.128). Wheel zoom to cursor (s · e^(deltaY·0.0016), non-passive listener). Pinch: scale by finger-distance ratio, keep world point under the centroid fixed. Fit: s = min((w2p)/GW, (h2p)/GH), pad 40 desktop / 20 phone, centered. Camera animates on fit/focus with transform .48s cubic-bezier(.22,.85,.3,1); drags/zooms are transition-free. Refit when container size changes >60px (ResizeObserver) or mobile/desktop flips.

Placement & drag: object drags snap center to a 3in (7.62cm) grid; drag commits ONE change (PATCH on drop, not per frame). Plops live in the parent object's local frame (rotate/move the bed moves its plants); drag clamps so a patch may overhang the bed edge by up to r/2 (spacing rule from DESIGN.md). Placing a plop: radius = spacing/2, count derived max(1, round(πr²/spacing²)). Click vs drag disambiguated by movement threshold. Escape ladder: disarm → deselect → unfocus. Delete/Backspace deletes selection (never while typing in inputs).

Focus (plant a bed): double-click (desktop) or inspector "Plant this" — camera zooms to the bed (≈2.1× margin desktop, 1.35× phone, cap s=6), siblings dim to 30% opacity (their plops 22%), palette/strip swaps to plants. Same canvas, no separate view; mirror to ?focus=objectId.

Undo/History: every operation (add, move, rotate, delete, plant, pull, clear) is one undoable step with a human label ("Planted garlic in Long bed A"). Maps directly to the change-set API (GET /gardens/:id/history, POST /change-sets/:id/revert); the prototype's snapshot stack is a stand-in.

Seasons: the seg switches ?year= on GET /gardens/:id/full. Past seasons render read-only (edit handlers guard + banner). "2027 plan" opens the copied plan garden (from /copy) — visually identical editing with a persistent banner.

Read-only viewers (share role viewer / public token): same guard path as the 2025 season — canvas renders, all mutation entry points disabled.

State management (suggested shape)

  • Server state: GET /gardens/:id/full (garden + objects + plantings + plants) keyed per garden+year; optimistic mutations with version guard — on 409 roll back and refetch.
  • Ephemeral UI state: {tx, ty, s}, selection {type: 'object'|'plop', id} | null, focusId | null, armedKind | null, armedPlant | null, season/year, rail tab, phone mode ('build'|'plants'|'journal'|'chat'), theme pref.
  • Journal (/gardens/:id/journal) and assistant thread (/gardens/:id/agent/history, POST /agent/chat SSE) load per tab/peek.

Assets

No binary assets. Icons are Lucide (https://lucide.dev) inlined at stroke-width 2.75, round caps/joins: sprout (brand + Plants mode), shovel (Build), notebook (Journal), message-circle (Assistant), settings, undo-2, rotate-cw, trash-2, plus, minus, maximize (fit), chevron-left/right, x, camera, share-2, copy, lock, monitor/sun/moon (theme), search, send. Object-kind "icons" are not glyphs — they're mini SVG swatches of the kind's actual shape, fill, and stroke.

Implementation notes for pansy specifically

  • Keep rendering as plain SVG — the prototypes prove tens of objects + low-hundreds of plops need nothing heavier; native hit-testing does all picking.
  • The editor's desktop/phone split is container-width-driven (760px), one component tree, two chromes — don't build two apps.
  • Semantic zoom thresholds that felt right: monogram at r·s ≥ 9px, plop name at ≥ 34px, object labels when max(w,h)·s > 54px.
  • Imperial display is presentation-only: cm → nearest inch, shown as 3 / 16″ / 8″; API stays metric.
  • Theme: implement exactly as pansy-theme.js does — token overrides on the root element, color-scheme set, system watcher. No second stylesheet, no class swapping on every node.