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]>
64 lines
2.6 KiB
TypeScript
64 lines
2.6 KiB
TypeScript
// Plant markers are a solid circle in the plant's color with a 1–2 letter
|
||
// monogram — the design's replacement for emoji icons. The letters are derived
|
||
// from the name, never stored, so a renamed plant re-letters itself.
|
||
//
|
||
// Rule: the initial of the species (the part before a " — Variety" suffix).
|
||
// Within one catalog, plants that share an initial are told apart by a second,
|
||
// lowercase letter — Marigold → Ma, Mint → Mi. The bare initial goes to the
|
||
// plant that entered the catalog first (lowest id): the built-ins are seeded
|
||
// roughly commonest-first, so Tomato keeps T over Thyme, and a variety you add
|
||
// later ("Tomato — Cherokee Purple") gets To rather than displacing it. The
|
||
// collision set is the whole catalog, so a plant's letters are the same on
|
||
// every screen.
|
||
|
||
/** "Tomato — Cherokee Purple" → "Tomato"; "Melon (Hale's Best)" → "Melon". */
|
||
export function speciesName(name: string): string {
|
||
const cut = name.split(/\s[—–-]\s|\s\(/)[0] ?? name
|
||
return cut.trim() || name.trim()
|
||
}
|
||
|
||
function letters(name: string): string[] {
|
||
// Letters only; a leading digit or punctuation would make an unreadable mark.
|
||
return speciesName(name)
|
||
.normalize('NFD')
|
||
.replace(/[̀-ͯ]/g, '')
|
||
.replace(/[^A-Za-z]/g, '')
|
||
.split('')
|
||
}
|
||
|
||
/** The monogram for every plant in the catalog, keyed by id. */
|
||
export function monogramMap<P extends { id: number; name: string }>(plants: readonly P[]): Map<number, string> {
|
||
const out = new Map<number, string>()
|
||
const taken = new Set<string>()
|
||
const sorted = [...plants].sort((a, b) => a.id - b.id || a.name.localeCompare(b.name))
|
||
for (const p of sorted) {
|
||
const ls = letters(p.name)
|
||
if (ls.length === 0) {
|
||
out.set(p.id, '?')
|
||
continue
|
||
}
|
||
const initial = ls[0].toUpperCase()
|
||
// Prefer the bare initial, then initial + each following letter in turn —
|
||
// skipping a repeat of the initial ("Pp" is not a mark anyone can read) —
|
||
// then give up on uniqueness rather than grow past two letters.
|
||
const candidates = [
|
||
initial,
|
||
...ls
|
||
.slice(1)
|
||
.filter((l) => l.toUpperCase() !== initial)
|
||
.map((l) => initial + l.toLowerCase()),
|
||
]
|
||
const pick = candidates.find((c) => !taken.has(c)) ?? candidates[candidates.length - 1]
|
||
taken.add(pick)
|
||
out.set(p.id, pick)
|
||
}
|
||
return out
|
||
}
|
||
|
||
/** A single plant's monogram when the catalog isn't at hand (a fallback, not the
|
||
* collision-aware mark — prefer monogramMap where a list exists). */
|
||
export function monogramFor(name: string): string {
|
||
const ls = letters(name)
|
||
return ls.length ? ls[0].toUpperCase() : '?'
|
||
}
|