From 5d29e2b37003acb9d377a860300bc9c749efee10 Mon Sep 17 00:00:00 2001 From: fireshaper Date: Wed, 28 Oct 2026 11:05:35 -0400 Subject: [PATCH] Rework Add Rabbit flow for mobile and clarity - Pedigree renders as stacked Sire/Dam sections below 1280px instead of a sideways-scrolling tree; larger touch targets in the ancestor popup - Step lives in ?step= so the back gesture moves between steps; sticky Back/Next/Save bar and a clickable stepper; beforeunload guard - Steps stay mounted, fixing pedigree entries lost when going back - Review shows formatted values, hides empty rows, per-section Edit links - Failed photo upload after create now lands on the new rabbit with a notice instead of allowing a duplicate save - Details form: primary fields first, secondary fields collapsible on Add, name error only after a Next attempt; "Use as color name" button Co-Authored-By: Claude Opus 5.5 --- memory-bank/architecture.md | 10 +- mykindlebox/client/src/components/Badge.jsx | 11 +- .../client/src/components/GeneticsForm.jsx | 53 ++- .../client/src/components/PedigreeTree.jsx | 117 ++++-- .../src/components/RabbitDetailsForm.jsx | 388 ++++++++++-------- .../client/src/components/ReviewStep.jsx | 152 ++++--- .../client/src/components/StickyActionBar.jsx | 13 + mykindlebox/client/src/hooks/useMediaQuery.js | 14 + mykindlebox/client/src/lib/format.js | 9 + mykindlebox/client/src/pages/AddRabbit.jsx | 265 +++++++++--- mykindlebox/client/src/pages/EditRabbit.jsx | 1 + mykindlebox/client/src/pages/RabbitDetail.jsx | 10 +- 12 files changed, 695 insertions(+), 348 deletions(-) create mode 100644 mykindlebox/client/src/components/StickyActionBar.jsx create mode 100644 mykindlebox/client/src/hooks/useMediaQuery.js diff --git a/memory-bank/architecture.md b/memory-bank/architecture.md index 07aeeca..8db4ea3 100644 --- a/memory-bank/architecture.md +++ b/memory-bank/architecture.md @@ -62,10 +62,12 @@ docker-compose.yml — orchestrates both services for self-hosted deployment | `src/lib/ocrPreprocess.js` | OCR Pedigree Scanning, Phase 2. Canvas-only (no `jimp`) image normalization for OCR. Exports `preprocessImage(file)` — decode → downscale to `MAX_WIDTH` (1600px, never upscaling) → grayscale (luminance) → binarize to pure black/white in one pixel pass → return a PNG `Blob`. Also exports the tunable `MAX_WIDTH` and `BINARIZE_THRESHOLD` (binarization cutoff, tuned to **103** during Step 2.2 — see progress.md; a fixed global threshold, deliberately not Otsu). This processed blob is the canonical image for the whole OCR pipeline: the scanner OCRs it and the zone picker's bounding boxes live in its coordinate space, so the original file is discarded after preprocessing. | | `src/lib/pedigree.js` | Pedigree slot definitions (`GEN1`/`GEN2`/`GEN3`, 14 ancestor slots mapping 1:1 to the schema's `_name_text` columns) and the client-side chain-authority resolver. `resolveEffectiveNode(path, state, cascadeCache)` mirrors `server/routes/rabbits.ts`'s `resolveAncestorNode` — see the pedigree convention note below. Also exports `seedPedigreeStateFromRabbit`/`cascadeFromAncestors` (used by `EditRabbit.jsx` to pre-populate from an existing record) and `buildPedigreePayload`/`buildPedigreeSummary` (flat API-ready fields vs. a display-only summary array, both derived from the same resolver). `serializeManualAncestor`/`parseManualAncestor`/`formatManualAncestorDisplay` encode/decode a manually-entered ancestor's Farm/Name/Ear Number/Weight/DOB as a JSON string in that same `_name_text` column — see the manual-ancestor convention note below. | | `src/lib/genetics.js` | `LOCI` (option config for all seven loci pill-groups) and `generateColorString(loci)`, which combines A/B/D/E/C/En into a single description string (`vv` and `cc` short-circuit to "BEW"/"REW"). Also `hasAnyLocusSet` (drives `GeneticsForm`'s default-expanded state on Edit). | -| `src/components/PedigreeTree.jsx` | Self-contained pedigree input (Phase 7 Step 7.1) — manages its own `state`/`cascadeCache` and reports out via `onChange(fields, summary)` rather than being a fully controlled component, since its internal shape (per-slot mode/id/name plus an async ancestor cache) isn't something a parent should need to reconstruct. Accepts optional `initialState`/`initialCascade` for `EditRabbit.jsx`. A locked (cascade-derived) node renders non-interactively with a 🔒 and is not clickable — see the pedigree convention note below. | -| `src/components/RabbitDetailsForm.jsx` | Controlled form (Step 7.2) over the same snake_case field names the API uses. Weight toggles between one decimal input and an lbs+oz pair based on a `weightUnit` prop; photo selection is reported via a separate `onPhotoChange(file)` callback since it's always a second request (`uploadRabbitPhoto`) after the rabbit row exists. | -| `src/components/GeneticsForm.jsx` | Collapsible pill-group genetics section (Step 7.3) over `lib/genetics.js`'s `LOCI` config, with a read-only generated color string field. | -| `src/components/ReviewStep.jsx` | Read-only summary (Step 7.4) of pedigree/details/genetics before save, with a shared `dash()` helper for empty values. | +| `src/components/PedigreeTree.jsx` | Self-contained pedigree input (Phase 7 Step 7.1) — manages its own `state`/`cascadeCache` and reports out via `onChange(fields, summary)` rather than being a fully controlled component, since its internal shape (per-slot mode/id/name plus an async ancestor cache) isn't something a parent should need to reconstruct. Accepts optional `initialState`/`initialCascade` for `EditRabbit.jsx`. A locked (cascade-derived) node renders non-interactively with a 🔒 and is not clickable — see the pedigree convention note below. **Responsive layout:** at `≥1280px` (`useMediaQuery`, `hooks/useMediaQuery.js`) it renders the 3-column tree; below that the same `AncestorSlot`s render as a stacked, indented list (`StackedBranch`) in "Sire's side"/"Dam's side" cards, so phones never scroll sideways. Popup drop-direction accounts for the bottom chrome (action bar + mobile nav). | +| `src/components/RabbitDetailsForm.jsx` | Controlled form (Step 7.2) over the same snake_case field names the API uses. Weight toggles between one decimal input and an lbs+oz pair based on a `weightUnit` prop; photo selection is reported via a separate `onPhotoChange(file)` callback since it's always a second request (`uploadRabbitPhoto`) after the rabbit row exists. Primary fields (Name, Farm, Ear #, Sex, DOB, Breed, Color, Weight, Photo) come first; Status/Location/Acquired/Death sit in a "More details" section that `collapseSecondary` (Add flow only) starts closed. The name error only shows when `showValidation` is set (Add sets it after a failed Next). | +| `src/components/GeneticsForm.jsx` | Collapsible pill-group genetics section (Step 7.3) over `lib/genetics.js`'s `LOCI` config, with a read-only generated color string field. Optional `onUseColor(color)` adds a "Use as color name" button (Add + Edit wire it to `details.color_name`). | +| `src/components/ReviewStep.jsx` | Read-only summary (Step 7.4) of pedigree/details/genetics before save. Formats values for display (sex/status labels, `formatDate`, `formatWeight` in the user's unit), hides empty rows (Sire/Dam always shown; deeper ancestors only when set) and shows genetics as "Not set" when empty. Each section has an Edit button calling `onEdit(step)`. Action buttons live in `AddRabbit`'s `StickyActionBar`, not here. | +| `src/components/StickyActionBar.jsx` | Bottom-pinned form action row (sticky above the mobile bottom nav, `bottom-0` on desktop). Used by the Add Rabbit flow for Back/Next/Save. | +| `src/pages/AddRabbit.jsx` (flow notes) | The current step is a `?step=` URL param (pushed, so the Back gesture moves between steps; step 3 falls back to 2 without a name). All steps stay mounted and are hidden with CSS so `PedigreeTree`'s internal state survives going back / Review "Edit". Step 1 has an optional name field feeding the tree's root label. A `beforeunload` guard warns on reload/close with unsaved input (in-app link navigation is not guarded — `BrowserRouter` has no `useBlocker`). On save, a failed photo upload still navigates to the created rabbit with a `location.state.notice` (shown by `RabbitDetail`) instead of leaving the user on Review, where re-saving would create a duplicate. | | `src/lib/theme.js` | Theme registry (post-Phase-11 feature) — the single source of truth for the *list* of selectable themes (`classic`/`midnight`/`ocean`/`berry`/`sunset`/`carnival`), their labels/descriptions, the Settings preview swatches, and each theme's browser-chrome `theme-color`. The actual palettes are CSS variables in `index.css` (one `[data-theme='…']` block per theme); this module only knows ids + preview metadata. `applyTheme(id)` sets `document.documentElement.dataset.theme` (which swaps the variables), updates the `theme-color` meta, and mirrors the id to `localStorage` (key `mykindlebox-theme`). `normalizeTheme` falls back to `classic` for unknown ids so a stray DB/cache value never leaves the app unstyled. See the theming convention note below. | | `src/lib/breeds.js` | The ARBA-recognized rabbit breed list (`ARBA_BREEDS`) powering the breed picker, plus `isCustomBreed(breed)` (true for a non-empty value not on the list). Suggestion list only — `BreedSelect` keeps a free-text fallback, so a breed not on this list still saves. Add new ARBA breeds here. | | `src/components/BreedSelect.jsx` | The breed picker (combobox): a text input bound **directly** to `value` (so anything typed is a valid free-text breed) with a filtered dropdown of `ARBA_BREEDS` underneath for guided selection. Reuses `lib/keyboardNav.js` for the same arrow-key roving focus as `RabbitAutocomplete`/`PedigreeTree`. Shows a "Custom breed" hint when the value isn't an ARBA breed. Used by `RabbitDetailsForm.jsx` (the rabbit Breed field) and `Settings.jsx` (the Default Breed setting). | diff --git a/mykindlebox/client/src/components/Badge.jsx b/mykindlebox/client/src/components/Badge.jsx index 59c8003..6728f78 100644 --- a/mykindlebox/client/src/components/Badge.jsx +++ b/mykindlebox/client/src/components/Badge.jsx @@ -1,3 +1,5 @@ +import { STATUS_LABELS } from '../lib/format' + const STATUS_CLASSES = { active: 'bg-accent text-surface', for_sale: 'bg-accent-warm text-surface', @@ -7,15 +9,6 @@ const STATUS_CLASSES = { other: 'bg-border text-text-primary', } -const STATUS_LABELS = { - active: 'Active', - for_sale: 'For Sale', - on_hold: 'On Hold', - sold: 'Sold', - deceased: 'Deceased', - other: 'Other', -} - function Badge({ status }) { return (