Files
pansy/web/src/lib/monogram.ts
T
steveandClaude Fable 5 52b2c09a9e
Build image / build-and-push (push) Successful in 31s
Gadfly review (reusable) / review (pull_request) Failing after 1s
Adversarial Review (Gadfly) / review (pull_request) Failing after 1s
Replace the UI with the Organic design handoff (docs/design_handoff_pansy_ui)
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]>
2026-08-22 19:12:29 -04:00

64 lines
2.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Plant markers are a solid circle in the plant's color with a 12 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() : '?'
}