Skip to content

SPA revamp: implement the chosen direction — field-first phone (B), ledger desktop (A), one attention line (C) #674

Description

@mforce

Amendment (2026-09-13). Sequence step 1's open question — "do we adopt external UI
components?"
— is answered: MUI. Tailwind + shadcn/ui is declined, not deferred. The farm
palettes are kept and proven. See
#674 (comment) for what
changed, which claims in the evidence comment were corrected, and the measured bundle cost; the
reasoning lives in docs/decisions/674-ui-component-library.md.

The body below is left as written, for history. The checklist at the end is current.

Tracking issue for the SPA revamp. Owner decision recorded on #656 (2026-09-02): a combination — B's field-first phone, A's ledger desktop, C's attention strip reduced to one line on both. This issue carries the work; #656 stays the home of the three visual-identity questions, which the design doc answers.

Every slice follows docs/designs/822-mui-revamp.md.
It is the component plan, not background reading — it maps each existing custom element to the
MUI component that replaces it, and states the rule: "reused, never rebuilt", and for the
shared picker, "the engine stays whole". Composing MUI primitives into a screen-specific
wrapper is expected (FilterBar, AuthShell and FieldConsole all do). Reimplementing
behaviour a shared component already has is not. #918 built a 216-line flock picker duplicating
NamedEntityPicker's paging, debounce and keyboard handling — with the same constants, 50
and 250 — and its review found three defects in mechanics the shared picker had already
solved. That plan even documents the slots.paper technique for rendering content around an
Autocomplete listbox, which is the problem the bespoke component was built to work around.
Cite this document in every slice brief. It appears as a ticked item below, which reads as
finished work; it is a standing reference for every slice that follows.

What was decided, and why

Three rendered directions were built from the app's own seeded data (four houses, 327/298/341 collected with House C not recorded, 63,122 eggs on hand by grade, the 14-day trend, four real orders), differing in point of view rather than palette:

  • A — the ledger. The screen is the working record. Ruled rows, numerals first, no cards.
  • B — field-first. The hen-house phone is primary; one task per screen, glove-sized targets; desktop is the same parts wider.
  • C — operations console. Status before records; a persistent attention strip; built for the night check.

Chosen: phone = B, desktop = A, both = one attention line. The app has exactly two real contexts — a phone in a shed and a desk in an office — and the renders designed for each were B-phone and A-desktop. (The A-phone and C-phone renders reused desktop tables at 390px and overflowed; they were not fair renders of those directions.)

Sequence

  1. Design doc in docs/designs/ — must answer SPA: visual identity — brand-derived link colour, display optical size, two-colour mark (owner decisions) #656's three questions (link colour derived from the brand, display optical size via the already-installed Inter opsz axis, the two-colour mark) and the two questions nobody has asked: the IA (18-link flat sidebar in 5 groups, bottom tabs + "More" sheet on phone) and the layout system (today: everything is a white card; A has none, B has cards for houses only).
    • Also answer: do we adopt external UI components? There is no rule against them and specs/technical/tech_spec.md §8.1 (KD-6) prescribes them; the hand-built approach is a convention nobody decided. Evidence, candidates and the constraints a candidate must satisfy: #674 (comment). Ends as a decision record either way.
  2. Grill the design doc (contrarian pass) before signoff.
  3. Slices, each filed on this epic, sized so each can be rendered and looked at; one PR per slice; every PR attaches a 1:1 before/after comparison captured from a stack rebuilt at the head under review (the only check in this repo that reads the rendered result; it found the sole real defect in the punch-list).

The three visual-identity questions (folded in from #656, closed 2026-09-13)

#656 was closed during the issue cleanup and its questions moved here, so one decision has one
home. Its comment thread stays the record of how the direction was chosen; the substance is below.
The design doc in sequence step 1 answers all three.

1 — Links derive from the brand. The palette is Slack's, token for token: #4a154b primary,
#1264a3 link blue, #611f69 press (web/DESIGN.md, adopted in #52). Blue links are a second hue
that fights the "chromatic monotheism" the stylesheet header claims, and they ignore the farm accent
palettes (#149) — a forest or terracotta farm still gets Slack-blue links. Proposal:
--link: var(--brand) in light, --stat-accent in night, hover a step lighter/darker. Verify
4.5:1 on --surface for every palette × theme in the styles test
, not by eye.

2 — Optical size for display text. Inter everywhere at one optical size gives headings and stat
figures no voice. font-variation-settings: "opsz" 32 on h1, h2, .stat-value buys tighter
display cuts at zero download cost.

Amended 2026-09-16 (#822 D7.2, #864): the premise below was wrong in the way that matters. The files are in the package, but the app loads @fontsource-variable/inter's default entry, whose index.css carries wght faces only; the opsz faces are a separate entry (opsz.css), and adopting them costs about +119 KiB of precache (+24 KiB on the latin subset a phone fetches). "Zero download cost" above does not hold; #835 owns the swap and is charged against #825's ceiling. Left as written below for history.

Already answered, incidentally (2026-09-02): the installed @fontsource-variable/inter does
carry the opsz axis — web/node_modules/@fontsource-variable/inter/files/ contains
inter-*-opsz-normal.woff2. The issue flagged a risk that the default entry ships wght only.
It does not apply. Do not re-investigate this.

3 — The brand mark. A generic single-stroke egg outline that will not survive favicon and PWA
icon sizes. Wants a two-colour mark that holds up small.

Also inherited from #656: it was deliberately sequenced last among the seven look-and-feel
slices, because "the other six are defects; this one is a point of view." The other six shipped
(#653, #655, #660, #662, #663, #664). That ordering rationale still applies to the design doc —
answer the point-of-view questions deliberately, not as a side effect of a defect fix.

Current owner-approved screen directions

These descriptions supersede earlier visual targets for the named slices only. Implementation remains open.

Slice Approved composition
#906 — Dashboard Operations desk: morning brief, collection progress, grade/count/share stock ledger, aligned recent orders and a 14-day bar chart.
#907 — Daily Entry Count workbench: separate collection counts and sellable-egg grading, explicit reconciliation, mortality as a separate flock event, reachable draft/submit actions.
#908 — CRUD Complete comparison tables above full-width bottom inspectors. Lists scroll independently; Products and Packed units are separate tabs on the same page.
#831 — Ledgers Workflow-specific order desk, grade stock board, inventory movement ledger, production correction sheet, expense daybook, feed ticket, direct/meter water log and raw-detail reports.
#833 — Settings and support Expandable Settings/Account sections and Audit rows; persistent Help contents with active-section highlighting; split Login/Set Password panels; full ZIP backup plus single-dataset CSV selector.

Detailed approved compositions and captures: Dashboard, Daily Entry, CRUD, ledgers, Settings and support.

Login banner placement is visually approved; pre-login access to authenticated farm images remains an explicit implementation decision. Production states, localization, accessibility, tests and rebuilt-stack evidence remain acceptance work. #836's brand-mark design is separate and still open.

Slices on this epic

Formalised as an epic on 2026-09-13 during the issue cleanup — the body already said
"slices, each filed on this epic", but the epic label was missing, so nothing was navigable
from here. Label: epic-674.

Phase 0 — design · phase:0-design · gates everything below

Phase 1 — groundwork · phase:1-groundwork · all three run in parallel, after #822

Phase 2 — controls · phase:2-controls · parallel with each other AND with phase 3

Phase 3 — screens · phase:3-screens · #829 first; it sets the conventions the rest follow

Phase 3 follow-up — owner-directed redesign · phase:3-screens · preserve the completed conversion issues as history; each follow-up designs first, then implements after owner sign-off

Phase 4 — identity · phase:4-identity · near-independent; #835 needs #823's type decision

Unphased — start any time, blocked by nothing

Defects

  • web: long tl/es strings overflow fixed-width controls on narrow screens #740 — the phone action bar cannot hold its own button labels. Added 2026-09-13 (owner)
    after a live reproduction. .actions button is flex: 1 at phone width, which squeezes the button
    narrow enough that its label wraps to three lines; the box then becomes taller than it is wide and
    border-radius: 999px resolves to an ellipse the text falls outside of. Broken in English
    too
    — en at 420px is the worst aspect ratio of the three, so this is width-dependent, not a
    locale bug. It belongs here because the fix is a decision about how action buttons lay out on a
    phone, which is sequence step 1's layout-system question. Full measurements, screenshots at three
    viewports and two candidate fixes are on the issue.

  • Offline data capture (PWA) — tech spec KD-4/§6 #50 — Offline data capture (PWA). (Moved to its own milestone "Offline data capture (PWA)", owner 2026-09-26.) Added to this epic 2026-09-13 (owner). The Installable PWA baseline — manifest, icons, service worker app-shell cache (split from #50) #142 PWA
    baseline shipped; this is the queued-writes, conflict-resolution and sync half. It sits here
    because it captures from the screens this revamp rewrites — built before the revamp lands it
    would be built twice — and because the field-first phone direction (B) is the context that
    makes offline capture worth having at all. Sequence it after the visual slices, not
    alongside them. Tier4 until the design doc lands.

Constraints the slices inherit (from #650–#657, all shipped)

  • Sentence-case labels everywhere; caps survive only on nav group dividers (web/src/styles.caps.test.ts).
  • EmptyState (13 list screens, two variants, emptyStates.guard.test.ts) and .toolbar are reused, never rebuilt; --shadow-card is retired; the elevation guard (styles.elevation.test.ts) allow-lists exactly which selectors may cast a shadow — extend deliberately.
  • Count a selector's call sites before styling it (grep -rn "<class>" web/src --include='*.tsx'); zero is a legitimate, stated answer.
  • Removing a presentational transform makes every string it transformed a caller — read the strings in all three locales.
  • Every web/ change ships Vitest tests in the same PR; style facts jsdom cannot see go in a styles.*.test.ts parsing styles.css with postcss.
  • Coverage is a ratchet with <1 pt headroom on functions — never re-baseline to get green.
  • i18n is not English-first: every new string ships en/es/tl inline, machine-drafted and flagged for native review; Spanish and Tagalog run longer.
  • Docs in sync: GLOSSARY + Help page in the same PR when a concept appears or changes; a pure restyle owes neither and says so.

Touchpoints

Materials

Rendered mockups (desktop 1440×1000 and phone 390×844 for A/B/C), the handoff notes and the decision log live in the driver's records (~/.claude/driver-records/cluckwork-revamp/), not in the repo; the design doc brings whatever the repo needs to keep.

Activity

  1. mforce commented on Sep 12, 2026

    @mforce
    OwnerAuthor

    Suggestion for the design doc: stop hand-building every control

    Raised while shipping #780, where the "Last 14 days" panel needed a tooltip and a highlight. I answered a question about reusing an existing component with "there's nothing in-repo, and I'd keep it that way", then went looking for the rule that says so. There isn't one, and the only written document on the subject says the opposite.

    What is actually written down

    Searched AGENTS.md, CONTRIBUTING.md, all 36 records in docs/decisions/, web/README.md and specs/:

    • AGENTS.md — nothing about UI libraries. Its dependency rules are the CI: dependency-vulnerability and SAST gates #146 vulnerability gate, NuGet central package management (chore: adopt Central Package Management so a version bump is one file, not eight #684), lock files, and SHA-pinning Actions. All gates; none a prohibition. The "no MediatR" style of rule exists for the backend and has no frontend equivalent.
    • CONTRIBUTING.md §Dependencies — the same three, and nothing at all about adding an npm package.
    • docs/decisions/ — no record on the frontend stack.
    • web/README.md:11 — "No CSS framework yet — plain CSS in src/styles.css". A status line containing the word yet, not a rule.

    And specs/technical/tech_spec.md §8.1, under KD-6, prescribes:

    UI:      Tailwind CSS + shadcn/ui
    Charts:  Recharts (or visx for custom dashboard viz)
    

    KD-6's own rationale for choosing React over Blazor is "best offline/PWA + forms + charting ecosystem". That spec is live — status "initial technical design for Phase 1 build", last synced in #601 — and nothing supersedes it.

    So the hand-built approach is a convention that drifted, not a decision anyone made. It has never been argued in writing, for or against.

    The drift is wider than charts

    web/package.json carries seven runtime dependencies: react, react-dom, react-router, i18next, react-i18next, lucide-react, @fontsource-variable/inter. Against §8.1:

    §8.1 prescribes Installed
    React + TS + Vite yes
    React Router (spec allowed it beside TanStack Router) yes
    PWA via Workbox yes (vite-plugin-pwa)
    TanStack Query no
    React Hook Form + Zod no
    Dexie (IndexedDB) no
    Tailwind + shadcn/ui no
    Recharts / visx no
    OpenAPI-generated typed client no — web/src/api/cluckwork.ts is 1,261 hand-written lines

    None of that divergence is recorded in an issue or a decision record. Dexie and TanStack Query are arguably #50's business (offline capture, KD-4) rather than this issue's, but the UI half is squarely here.

    What it has cost, measured

    The controls this app hand-built, where a mature library exists:

    Hand-built Lines
    NamedEntityPicker (combobox, + CustomerPicker/FlockPicker) 1,147, with 1,852 lines of test across four files
    Dialog + useConfirm + the four useDialog* hooks ~640
    usePagedList 381
    DayStrip + StockBar (charts) 143
    styles.css 3,628

    The combobox is the clearest case: 1,147 lines and four test files, for a control whose keyboard, focus and announcement behaviour is a solved problem with a spec. #501 is the tell — the accessibility work there needed CDP because Playwright does not model inert, which is the kind of problem you inherit by owning the primitive.

    #780 is a smaller, fresher example. The tooltip needed a measured clamp against its container, written as a useLayoutEffect; Floating UI exists for exactly that and is ~10 kB. Ten lines is not a crisis, but it is the third time this app has re-derived positioning.

    The suggestion

    The design doc should take an explicit position on external UI components, and say it either way. Right now every slice re-litigates it by default, and the default is "build it", chosen by nobody.

    Worth weighing, in rough order of value for this app:

    1. A headless primitive set (Radix, Ark, React Aria) for combobox, dialog, popover, tooltip, tabs, menu. Headless matters because the visual layer here is the token system and should stay so — what is wanted is the behaviour and the ARIA, not the look. This would retire the largest hand-built surfaces.
    2. Floating UI for positioning, which most of the above already depend on.
    3. A chart library (Recharts or visx, as §8.1 says). Lowest urgency: there are two charts and both now work. Reconsider when the revamp adds a third.
    4. Tailwind + shadcn/ui, as §8.1 also says, is a much larger swing — it replaces the token system rather than sitting on it, and SPA: typeset numbers as numbers — right-aligned tabular columns, digit grouping, locale currency and date formatting #650–SPA: Help page + glossary refresh — grouped, searchable, deep-linkable definitions; guide reordered around the tasks people come for #657 plus ten styles.*.test.ts guards are built on that system. This one needs its own argument; do not let it ride in on the others.

    What a candidate has to satisfy — constraints, not vetoes

    • Colours resolve through CSS custom properties. styles.test.ts enforces it, and it is what carries the four farm palettes and both themes. A library that takes colours as JS props needs wiring that does not bypass the guard. This is the single most likely thing to disqualify a candidate, so test it first.
    • Bundle. This is a PWA for phones in sheds; the current precache is 1.3 MB. Budget it explicitly rather than discovering it.
    • CI: dependency-vulnerability and SAST gates #146 audit gate. Each production dependency is a new advisory surface with a fail-closed gate and only a dated exception as a mute.
    • The existing guards stay. The elevation allow-list, the caps guard, EmptyState, the coverage ratchet with under a point of headroom on functions. A library's own markup has to pass them, or the guard gets amended deliberately and in writing.
    • i18n. Every string still ships en/es/tl, and tl runs roughly 30% longer (i18n: catalogParity compares key SETS, so a translation that drifts in MEANING passes every gate #688). A component with baked-in English is not adoptable as-is.

    Outcome this asks for

    Whichever way it goes, it ends as a decision record — either amending §8.1 to match reality, or recording the divergence with its reasons. Today the spec and the code disagree and neither says so, which is how the next person gets the same wrong answer I did.

  2. mforce commented on Sep 12, 2026

    @mforce
    OwnerAuthor

    Owner input: the token system is not a hard constraint

    Correcting the weight I gave one item above. I listed "colours resolve through CSS custom properties" as the single most likely thing to disqualify a candidate. The owner's position (2026-09-12): the current themes are giving-up-able if a component set brings good theming with proven support.

    That changes the shape of the evaluation rather than one line of it.

    What it moves

    The constraint was never really "use var(--x)" — it was "one farm's four brand palettes and both themes must keep working, and styles.test.ts must keep being able to check it". A component set with a real theming layer can own that job instead; what it may not do is own it badly, or own half of it and leave the other half to the token system with no guard across the seam.

    So the design doc should evaluate theming as a capability of the candidate, weighted like any other, not as a gate the candidate has to pass to be considered. Concretely, the question becomes: does this set's theming carry the four farm palettes and light/dark at least as well as 3,628 lines of hand-written CSS, and can a guard still assert it?

    What does not move

    Practical note for the doc

    "Proven support" is the operative phrase and worth making a scored criterion rather than a vibe: release cadence, breaking-change history, whether theming is a first-class documented API or a wall of overrides, and whether multi-theme-at-runtime is supported or bolted on. A set whose theming is good but whose upgrade path breaks every two majors costs more than the CSS it replaces.

    Suggested order for the doc: pick the theming model first, because it decides whether the answer is headless-plus-our-tokens or a full styled set. Everything else in the list above follows from that choice rather than constraining it.

  3. added
    epicPhase-level tracking issue
    epic-674SPA revamp — field-first phone, ledger desktop (#674)
    on Sep 13, 2026
  4. mforce commented on Sep 13, 2026

    @mforce
    OwnerAuthor

    Reviewed in the 2026-09-13 issue cleanup. Formalised as an epic and labelled priority:tier2.

    What changed today:

    Tier2 — "high value, low risk" — is about the next step, not the whole epic. The design doc is cheap, fully specified, and unblocks everything after it. It must answer five questions, and the last one is the one that changes the most downstream:

    1. Brand-derived link colour, verified at 4.5:1 across every palette × theme.
    2. Display optical size.
    3. The two-colour mark.
    4. The IA — 18-link flat sidebar in 5 groups vs bottom tabs plus a "More" sheet on phone.
    5. The layout system — today everything is a white card; direction A has none, B has cards for houses only.
    6. Do we adopt an external UI component set? Nothing in the repo forbids it, specs/technical/tech_spec.md §8.1 (KD-6) prescribes it, and the hand-built approach is a convention nobody decided. The owner has since said the current token system is giving-up-able for a component set with good theming and proven support. Ends as a decision record either way.

    Next task: write the design doc in docs/designs/, then grill it before signoff. No slices should be filed until it exists.

  5. mforce commented on Sep 13, 2026

    @mforce
    OwnerAuthor

    Direction update (2026-09-13): the library question is answered — MUI

    Sequence step 1 above asked "do we adopt external UI components?", pointing at the evidence
    comment. That question is now closed, and the epic's slices are filed against the answer.

    Decision record: docs/decisions/674-ui-component-library.md (branch feat/674-mui).

    What was decided

    • MUI is adopted. It was chosen on the owner's stated criteria — longest track record, most
      trusted, fully featured. MUI shipped 2014-10 and is the only candidate that is both the
      longest-lived and a complete styled set.
    • Tailwind + shadcn/ui is declined, not deferred (owner, 2026-09-13). It was the live
      alternative while the choice was open; re-opening it needs a new decision record, not a ticket.
    • The four farm palettes are kept. src/theme/FarmThemeProvider.tsx resolves the live CSS
      custom properties and hands MUI concrete colours, following data-brand/data-theme through a
      MutationObserver. styles.css stays the single source of truth — add a fifth palette there and
      MUI picks it up with no code change. Proven by src/theme/farmTokens.test.ts: all four palettes
      x both modes, distinct accents, derived states generated.

    Corrections to the evidence comment above

    Two of its claims did not survive being spiked, and both matter:

    1. The CSS-custom-property constraint was not decisive. It was named "the single most likely
      thing to disqualify a candidate". The style guards parse styles.css with postcss — they walk
      the stylesheet, never the rendered DOM — so a library's own markup is invisible to them.
    2. "Tailwind + shadcn/ui replaces the token system rather than sitting on it" is wrong.
      shadcn's default theming is CSS custom properties. Tailwind would displace styles.css's
      layout rules, not its colours.

    The comment also did not mention Base UI, which is what shadcn's current components actually
    sit on (the Radix set is behind a "Legacy Docs" link) and which is built by MUI's own team.

    Measured, not predicted

    precache JS gzip
    baseline, hand-rolled 1312.45 KiB 85.27
    Base UI, Dialog actually ported 1365.15 KiB 103.37
    MUI provider only, zero components 1397.79 KiB 115.45
    MUI + a realistic component kit 1632.68 KiB 186.98
    Radix Themes + the same kit 2130.79 KiB 132.08 (+92.27 CSS)

    MUI costs +320 KiB precache (+24%); Radix Themes would cost +818 KiB — the gap is CSS, because
    Radix Themes ships every accent colour in both modes whether used or not. That inverts the usual
    assumption and is why it was measured.

    Runtime cost looks fine at this scale: at 6x CPU throttling, a converted Dashboard rendered in
    1168 ms median vs 1213 ms hand-rolled — inside the noise. Re-measure at Autocomplete/data-grid.

    Two findings that became slices

    Sequencing

    #822 -> #823, #824, #825 -> then #826, #827, #828 in parallel with #829 -> the rest.
    #829 early on purpose: the Dashboard is where it first becomes visible whether MUI delivers the
    look this epic is for.

  6. added this to the SPA revamp milestone on Sep 13, 2026
  7. 6 remaining items

  8. mforce commented on Sep 16, 2026

    @mforce
    OwnerAuthor

    Finding, for the owner's read (2026-09-16): the Dashboard's Recent sales rows cannot show eggs and grade. The confirmed mockup's row is customer with the order number under it, eggs and grade ("3,600 Large"), amount, status, action. #883 renders every column but eggs and grade, because the orders list the Dashboard reads (listOrders, OrderItem in web/src/api/cluckwork.ts ~L328) carries each line's quantity and quantityBase but only an eggGradeId, never a grade name; the screen would need a second listEggGrades() fetch, or the API would need to include the grade name on the order line. Not filed; recorded here until the owner decides.

    Recent sales row, mockup vs app

  9. mforce commented on Sep 26, 2026

    @mforce
    OwnerAuthor

    All slices shipped; checklist complete.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:frontendReact/Vite web clientepicPhase-level tracking issueepic-674SPA revamp — field-first phone, ledger desktop (#674)priority:tier2High value, low risk

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions