Repository navigation
SPA revamp: implement the chosen direction — field-first phone (B), ledger desktop (A), one attention line (C) #674
Description
Activity
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 indocs/decisions/,web/README.mdandspecs/: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 insrc/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.jsoncarries 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.tsis 1,261 hand-written linesNone 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 fouruseDialog*hooks~640 usePagedList381 DayStrip+StockBar(charts)143 styles.css3,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:
- 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.
- Floating UI for positioning, which most of the above already depend on.
- 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.
- 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.tsguards 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.tsenforces 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
tlruns 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.
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, andstyles.test.tsmust 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
- The four farm palettes are a product feature, not a styling detail. They are per-account and swap at runtime. A theme system that assumes one brand per build is a downgrade regardless of how good it is otherwise; a set with runtime-swappable multi-theme support is not.
- The grade hues must stay outside the farm-palette system.
styles.grades.test.ts(Dashboard: Recent sales rows do not form columns, and the trend and stock charts are hard to read #777) holds eight hues per mode, distinct by CIE76 ΔE ≥ 20 from each other, from the semantic colours, and from every palette's accent, at ≥ 3:1 against both panel surfaces, identical under all four palettes. That guard survived a mutation round that caught a real hole. Whatever the theming layer becomes, this property is worth re-expressing rather than dropping — two farms' screenshots must stay comparable. - Something must still be checkable. Ten
styles.*.test.tsguards parsestyles.csswith postcss today. If the colours move into JS theme objects those guards do not port as-is, and "we'll check it by eye" is not a replacement. Budget the replacement guards as part of the adoption, in the same PR, or the adoption quietly removes ten tests. - The elevation, caps and
EmptyStateguards encode decisions from 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 that were argued and shipped. A component set arrives with its own opinions on shadows and capitalisation; where they disagree, the repo's decision wins or gets re-argued in writing — not silently overwritten by a default.
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.
- addedepicPhase-level tracking issuePhase-level tracking issueepic-674SPA revamp — field-first phone, ledger desktop (#674)SPA revamp — field-first phone, ledger desktop (#674)priority:tier2High value, low riskHigh value, low risk
on Sep 13, 2026 Reviewed in the 2026-09-13 issue cleanup. Formalised as an epic and labelled
priority:tier2.What changed today:
epic+epic-674labels added — the body already said "slices, each filed on this epic", but without the label nothing was navigable from here.- SPA: visual identity — brand-derived link colour, display optical size, two-colour mark (owner decisions) #656 closed and folded in. Its three visual-identity questions are now in the body above, including the one already answered:
@fontsource-variable/interdoes carry theopszaxis, so that risk does not apply. SPA: visual identity — brand-derived link colour, display optical size, two-colour mark (owner decisions) #656 stays the record of how the direction was chosen. - Offline data capture (PWA) — tech spec KD-4/§6 #50 (offline PWA data capture) attached as a slice, sequenced after the visual work — it captures from screens this epic rewrites, and only pays off under the chosen field-first phone direction.
- Its parent epic EPIC: Phase 1.5 — Egg product hardening #15 (Phase 1.5) was closed, so this epic now stands on its own rather than nested.
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:
- Brand-derived link colour, verified at 4.5:1 across every palette × theme.
- Display optical size.
- The two-colour mark.
- The IA — 18-link flat sidebar in 5 groups vs bottom tabs plus a "More" sheet on phone.
- The layout system — today everything is a white card; direction A has none, B has cards for houses only.
- 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.- added a commit that references this issue
on Sep 13, 2026 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(branchfeat/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.tsxresolves the live CSS
custom properties and hands MUI concrete colours, followingdata-brand/data-themethrough a
MutationObserver.styles.cssstays the single source of truth — add a fifth palette there and
MUI picks it up with no code change. Proven bysrc/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:
- The CSS-custom-property constraint was not decisive. It was named "the single most likely
thing to disqualify a candidate". The style guards parsestyles.csswith postcss — they walk
the stylesheet, never the rendered DOM — so a library's own markup is invisible to them. - "Tailwind + shadcn/ui replaces the token system rather than sitting on it" is wrong.
shadcn's default theming is CSS custom properties. Tailwind would displacestyles.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, Dialogactually ported1365.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 atAutocomplete/data-grid.Two findings that became slices
- web: the postcss style guards go blind as screens convert to MUI #824 — the postcss style guards go blind as screens convert. MUI styles through Emotion at
runtime, so its rules never reachstyles.css. Ten guards keep passing while covering less and
less of the app. Nothing fails; that is the problem. - web: convert the Dashboard to MUI — ledger desktop, field-first phone #829 — wrapping markup in MUI changes nothing on screen. A light Dashboard pass (panels ->
Paper, headings ->Typography, badge ->Chip) passed all 39 tests, added 12.1 KiB gzip, and
looked essentially identical, because.panelalready sets background, border, radius and shadow.
MUI cannot improve a screen whilestyles.cssis still driving it. That shallow diff is not
committed. Caught only by comparing rendered screenshots — the AGENTS.md: three conventions earned while shipping #651 + #652 #662 rule earning its place.
Sequencing
#822->#823,#824,#825-> then#826,#827,#828in parallel with#829-> the rest.
#829early on purpose: the Dashboard is where it first becomes visible whether MUI delivers the
look this epic is for.- MUI is adopted. It was chosen on the owner's stated criteria — longest track record, most
- added sub-issues
on Sep 13, 2026 6 remaining items
- added sub-issues
on Sep 13, 2026 - added 4 commits that reference this issue
on Sep 14, 2026 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,OrderIteminweb/src/api/cluckwork.ts~L328) carries each line'squantityandquantityBasebut only aneggGradeId, never a grade name; the screen would need a secondlistEggGrades()fetch, or the API would need to include the grade name on the order line. Not filed; recorded here until the owner decides.All slices shipped; checklist complete.

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.
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:
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
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 Interopszaxis, 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).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.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:
#4a154bprimary,#1264a3link blue,#611f69press (web/DESIGN.md, adopted in #52). Blue links are a second huethat 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-accentin night, hover a step lighter/darker. Verify4.5:1 on
--surfacefor 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" 32onh1,h2,.stat-valuebuys tighterdisplay cuts at zero download cost.
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.
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
epiclabel was missing, so nothing was navigablefrom here. Label:
epic-674.Phase 0 — design ·
phase:0-design· gates everything belowdocs/designs/822-mui-revamp.md)Phase 1 — groundwork ·
phase:1-groundwork· all three run in parallel, after #822CssBaseline, type, spacing, elevation)style-src 'self'(found by feat(web): whole-app MUI baseline, theme policy guard and the #740 phone action rule (#823) #871: no MUI style reaches the screen until this lands; owner chose the nonce over'unsafe-inline', 2026-09-14)Phase 2 — controls ·
phase:2-controls· parallel with each other AND with phase 3NamedEntityPicker→ MUIAutocomplete(1,147 lines + 1,852 of test)Dialog+useConfirm+ the fouruseDialog*hooks → MUIDialog(~640 lines)NumberFieldand the hand-rolled tooltip positioningPhase 3 — screens ·
phase:3-screens· #829 first; it sets the conventions the rest followor these five screens get converted twice
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-offPhase 4 — identity ·
phase:4-identity· near-independent; #835 needs #823's type decisionopszaxisweb: a two-colour brand mark that survives favicon and PWA icon sizes #836 — a two-colour brand mark that survives favicon and PWA icon sizes(not planned, owner 2026-09-24)Unphased — start any time, blocked by nothing
Offline data capture (PWA) — tech spec KD-4/§6 #50 — offline data capture (PWA), KD-4. Not revamp work; independent of every phase above.(moved to its own milestone "Offline data capture (PWA)", owner 2026-09-26)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 buttonisflex: 1at phone width, which squeezes the buttonnarrow enough that its label wraps to three lines; the box then becomes taller than it is wide and
border-radius: 999pxresolves to an ellipse the text falls outside of. Broken in Englishtoo —
enat 420px is the worst aspect ratio of the three, so this is width-dependent, not alocale 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)
web/src/styles.caps.test.ts).EmptyState(13 list screens, two variants,emptyStates.guard.test.ts) and.toolbarare reused, never rebuilt;--shadow-cardis retired; the elevation guard (styles.elevation.test.ts) allow-lists exactly which selectors may cast a shadow — extend deliberately.grep -rn "<class>" web/src --include='*.tsx'); zero is a legitimate, stated answer.web/change ships Vitest tests in the same PR; style facts jsdom cannot see go in astyles.*.test.tsparsingstyles.csswith postcss.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.