@micha.bigler/ui-core-micha 2.29.2 → 2.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/DESIGN.md +5 -5
  2. package/WORK_ORDERS.md +6 -0
  3. package/dev/entries.jsx +167 -1
  4. package/dev/main.jsx +10 -3
  5. package/dist/components/charts/BarChart.js +8 -6
  6. package/dist/components/charts/LineChart.js +23 -6
  7. package/dist/components/charts/TimeSeriesChart.js +12 -3
  8. package/dist/components/charts/chartDefaults.js +80 -0
  9. package/dist/components/charts/chartLabels.js +31 -0
  10. package/dist/components/charts/palette.js +7 -3
  11. package/dist/components/charts/yearTickInterval.js +7 -0
  12. package/dist/index.js +4 -0
  13. package/dist/theme/createAppTheme.js +168 -0
  14. package/dist/theme/index.js +4 -0
  15. package/dist/theme/themeCompleteness.js +320 -0
  16. package/dist/theme/tokens.js +325 -0
  17. package/package.json +4 -1
  18. package/src/components/charts/BarChart.jsx +37 -9
  19. package/src/components/charts/LineChart.jsx +61 -9
  20. package/src/components/charts/TimeSeriesChart.jsx +21 -0
  21. package/src/components/charts/chartDefaults.js +111 -0
  22. package/src/components/charts/chartLabels.js +36 -0
  23. package/src/components/charts/palette.js +6 -3
  24. package/src/components/charts/yearTickInterval.js +7 -0
  25. package/src/index.js +16 -0
  26. package/src/theme/createAppTheme.js +212 -0
  27. package/src/theme/index.js +14 -0
  28. package/src/theme/themeCompleteness.js +355 -0
  29. package/src/theme/tokens.js +376 -0
  30. package/tests/BarChart.test.jsx +4 -1
  31. package/tests/LineChart.test.jsx +5 -2
  32. package/tests/chartDefaults.test.js +177 -0
  33. package/tests/chartLabels.test.js +50 -0
  34. package/tests/chartsPalette.test.js +40 -0
  35. package/tests/createAppTheme.test.js +103 -0
  36. package/tests/themeCompleteness.test.js +63 -0
  37. package/tests/timeSeriesLegend.test.js +99 -0
  38. package/work-orders/CHART-6.md +428 -0
  39. package/work-orders/SHELL-2.md +197 -0
  40. package/work-orders/THEME-1.md +587 -0
  41. package/work-orders/assets/SHELL-2-account-navigation.html +1159 -0
  42. package/work-orders/assets/THEME-1-baseline-reference-sheet.html +1352 -0
@@ -0,0 +1,587 @@
1
+ # THEME-1 — Shared MUI theme baseline: `createAppTheme` factory + completeness assertion
2
+
3
+ **Target repo:** `ui-core-micha` (branch `main` — this repo has no `develop`)
4
+ **Tier:** 2 (shared core — `ui-core-micha` is named in AGENTS.md's Tier-2 forcing list)
5
+ **Review:** independent `reviewer` (Sonnet, full) **and** `ui_reviewer` — spawned concurrently, both mandatory before commit
6
+ **Value source (accepted spec):** [`work-orders/assets/THEME-1-baseline-reference-sheet.html`](assets/THEME-1-baseline-reference-sheet.html) — its **canonical token table** carries every decided value
7
+ **Decision record:** `webapp-management/DESIGN_SYSTEM_PROGRAM.md`, decisions 1–23 (DS-2)
8
+ **Version target:** `2.30.0` (currently 2.29.2)
9
+
10
+ ---
11
+
12
+ ## A. Envelope
13
+
14
+ ### Goal
15
+
16
+ Ship `createAppTheme()` from `ui-core-micha`: a shared MUI theme baseline that closes **every**
17
+ visual default surface, an exported **completeness assertion** each app calls from its own test
18
+ suite, and the reference sheet as a **living harness page**.
19
+
20
+ Expected outcome: a consuming app's `theme.js` shrinks to the two things that are genuinely its
21
+ own — its palette and its font — and everything else comes from the baseline instead of from
22
+ MUI's defaults.
23
+
24
+ ```js
25
+ // what an adopting app's theme.js becomes
26
+ import { createAppTheme } from '@micha.bigler/ui-core-micha'
27
+ export default createAppTheme({
28
+ palette: { primary: { main: '#0F62FE' } },
29
+ typography: { fontFamily: "'DM Sans', sans-serif" },
30
+ })
31
+ ```
32
+
33
+ ### Why
34
+
35
+ MUI's `createTheme` has no "unset" state: whatever an app does not specify, MUI fills with its
36
+ own default, and those defaults are Material Design 2014 — 16px body, 20px `h6`, elevation
37
+ shadows, 4px radius, uppercase buttons. Every unspecified property therefore pulls the design
38
+ backwards, systematically rather than randomly.
39
+
40
+ The second-order effect is the one that matters: the estate's standing rule *"derive from the
41
+ theme, don't hardcode"* is correct **only when the token was set deliberately**. hram's `SoftChip`
42
+ derives from `theme.palette` and looks right; fitness-monitor followed the identical rule and got
43
+ MUI's untouched `#2e7d32`. The rule does not need abolishing — its precondition needs
44
+ establishing. That is this WO.
45
+
46
+ ### Measured evidence (2026-08-08/09, five verification passes)
47
+
48
+ - **All apps run MUI `7.3.11`** — the default baseline being overridden is identical everywhere.
49
+ - **Not one app sets a typography `fontSize`.** All set weights and letter-spacing only;
50
+ `subtitle1`, `subtitle2`, `caption` and `overline` are untouched in every app surveyed. The
51
+ whole type scale is MUI's.
52
+ - **No app overrides `shadows`, `spacing` or `transitions`.** Only spesix sets
53
+ `shape.borderRadius`.
54
+ - **`MuiButton` is overridden in 7 of 7 app themes, `MuiTableCell` in 6 of 7** — with identical
55
+ values (`textTransform: 'none'`, `borderRadius: 3`, `padding: '3px 8px'`, no contained
56
+ elevation). A house style enforced by copy-paste across independent files.
57
+ - **The leak surface is measurable.** Heavily rendered components with *no* override anywhere in
58
+ the estate: `Box` (494 files), `Typography` (449), `Stack` (236), `CircularProgress` (151),
59
+ `IconButton` (101), `Divider` (93), `FormControlLabel` (74), `Checkbox` (72), `Tooltip` (56),
60
+ `Select` (54), `Container` (49).
61
+ - **14 consumers, all pinning exact versions**, 7 still on 2.4.5, spread up to 2.29.2. All 14
62
+ already have a runnable `vitest` script, so the exported assertion runs everywhere with zero
63
+ consumer-side setup.
64
+
65
+ ### Scope
66
+
67
+ #### A. The factory
68
+
69
+ - `createAppTheme(appConfig)` returning `createTheme(BASELINE, appConfig)` — MUI's own
70
+ multi-argument deep merge does the combining; **no custom merge logic**.
71
+ - **Palette-first ordering invariant (decision 20).** The factory merges the **palette first**
72
+ (baseline + app input) and computes component values from the *final* palette afterwards.
73
+ Without that order, pre-computed values are silently wrong for every app that overrides
74
+ `primary` or `success` — which is the entire purpose of the factory. This is the most breakable
75
+ thing in the WO.
76
+ - **Throws without `palette.primary` (decision 21)**, with a message naming what is missing. The
77
+ baseline defines no accent; an app without a deliberately chosen one is an error, not a default.
78
+ Same contract the chart wrappers already use for required axis labels.
79
+ - **Objects only in overridable `styleOverrides` slots (decision 20)** — never functions. MUI's
80
+ deep merge treats functions as leaf values, so a function in the baseline would be silently
81
+ displaced by any app that touches the same slot.
82
+ - **Throws on a function in the app's `styleOverrides` (decision 20)**, naming the slot and
83
+ pointing at the object/`variants` idiom. The trap has two directions and this closes the second.
84
+ Verified 2026-08-10: **zero** function overrides across all seven app themes *and* survey_app's
85
+ runtime builder, so this costs nothing today. MUI 7's `variants` syntax expresses
86
+ prop-/state-dependent styles as data objects — jg-ferien already uses it (`theme.js:120`, `:168`).
87
+ - **Runtime-callable.** survey_app builds a theme **per site at runtime**
88
+ (`survey_app/frontend/src/site-runtime/buildThemeFromSiteConfig.js`). The factory must accept
89
+ arbitrary palette input at call time, not only as a module-level constant, and two calls with
90
+ different palettes in one process must yield two independent themes.
91
+ - **Mode-capable API, light tokens only in v1 (decision 17).** Dark mode is DS-12 and is not built
92
+ here; the API must not have to change when it arrives.
93
+
94
+ #### B. The baseline token surfaces — this list *is* the definition of "complete"
95
+
96
+ Values come from the reference sheet's canonical token table. This Envelope fixes the
97
+ **surfaces**; the sheet fixes the **numbers**. Where the two disagree, the sheet is the one to fix.
98
+
99
+ | Group | Surfaces the baseline must set |
100
+ |---|---|
101
+ | Palette — status | `success` · `warning` · `error`/critical · `info`, each as a **text tone** *and* a **fill tone with its own `contrastText`** |
102
+ | Palette — ink | three opaque tiers: primary, secondary, muted |
103
+ | Palette — surfaces | `background.default` (page) · `background.paper` |
104
+ | Palette — borders | `divider` (decorative, 1.4.11-exempt) **and** `controlBorder` with rest / hover / focus / error |
105
+ | Palette — required from the app | `primary` (throws if absent) |
106
+ | Augmented namespaces | `stale` freshness semantic · **two-tier radius** (control + card) · **data-series ramp** |
107
+ | Typography | `fontFamily` plus, for h1–h6, subtitle1/2, body1/2, button, caption, overline: `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing` |
108
+ | Shape / spacing / breakpoints | `shape.borderRadius` · `spacing` unit · `breakpoints` |
109
+ | Shadows | rest = **none** (`MuiPaper` `elevation: 0`, `variant: 'outlined'` as default); weight only on Dialog / Drawer / Menu / Tooltip |
110
+ | Transitions | four durations (`fast` / `base` / `overlay` in+out / `chart`), the easing assignment (enter `easeOut`, exit `easeIn`, state `easeInOut`), and the reduced-motion collapse to **`0.01ms`, not `0`** |
111
+ | Components | `MuiButton` · `MuiTableCell` (the 7/7 and 6/7 consensus) plus the measured leak surface: `MuiIconButton` · `MuiDivider` · `MuiTooltip` · `MuiSelect` · `MuiCheckbox` · `MuiFormControlLabel` · `MuiContainer` · `MuiPaper` · `MuiCard` · `MuiChip` · `MuiTextField`/`MuiOutlinedInput` · `MuiDialog` · `MuiAlert` |
112
+ | Autofill | `:-webkit-autofill` **and** the standard `:autofill` — MUI styles neither, and the same field rendered yellow in Chromium, dark grey in Opera and dark blue in Firefox |
113
+ | Font loading | DM Sans 400/500/600, self-hosted via `@fontsource` — never a CDN |
114
+ | Touch | 44px is an **internal principle**, not WCAG AA (that floor is 24×24, 2.5.8; 44 is 2.5.5 AAA). Visual size may stay smaller; the **hit area** must reach 44px, gated by `@media (any-pointer: coarse)` — `any-pointer`, so hybrid devices are covered |
115
+
116
+ The enumerated list is **versioned in ucm** and is what the assertion checks. Adding a surface
117
+ later is a deliberate, reviewed change to that list.
118
+
119
+ #### C. The completeness assertion
120
+
121
+ - An **exported check** each app calls from its own `vitest` suite — the pattern this estate
122
+ already uses for S112 (`assert_all_consumers_secure`). Fails in CI, blocks nothing locally.
123
+ - **The exemption arm is required (decision 5).** An app that deliberately wants a value equal to
124
+ MUI's default declares it **with a reason**. Without the arm the check is gamed by
125
+ pseudo-deviations (radius 4 → 5) and measures nothing. An exemption *without* a reason is itself
126
+ a finding.
127
+ - **Contrast is an assertion surface.** Each status **text** tone against **its own tint** — not
128
+ against white. Checking the wrong surface is exactly what hid a real AA failure during review:
129
+ warning measured 4.10:1 on `#FBF0DC` while passing 4.63 on white. `controlBorder` against both
130
+ white and the page background.
131
+ - **Adoption is reported as a number, never gated** — inline-hex count, theme-bypass paths.
132
+ "Complete" is definable; "adopted" is not, so it is measured and printed, not enforced.
133
+ - **Hardness is a ratchet, not a retrofit.** The completeness check is a **hard failure only for
134
+ an app whose adoption WO has landed**; for every other app it reports numbers. No consumer goes
135
+ red because its maintainer has not had capacity yet.
136
+ - **Baseline invariant test (decision 20):** assert that no function appears anywhere in the
137
+ baseline's `components` tree, so a later edit cannot reintroduce the trap.
138
+
139
+ #### D. Series-colour transition
140
+
141
+ `useNeutralChartPalette` today derives the categorical palette from
142
+ `theme.palette.{primary,secondary,info,success,warning}` — which means **`success` and `warning`
143
+ are used as series colours**, the thing `DESIGN.md` #2 explicitly forbids. The baseline's ramp
144
+ fixes an existing violation rather than creating one.
145
+
146
+ - The hook **reads the ramp from the theme when present** and **falls back to today's derivation**
147
+ when it is not (decision 22). An adopter gets the ramp; a non-adopter sees **no change on a pin
148
+ bump**. Without the fallback this would recolour every chart in the estate unasked.
149
+ - The public exports `getNeutralChartPalette` / `useNeutralChartPalette` stay — no break, minor
150
+ version. Accepted cost: the name becomes historical, since the ramp is no longer a neutral
151
+ derivation.
152
+
153
+ #### E. The reference sheet as a harness page
154
+
155
+ Decision 18: one page in ucm's existing dev harness rendering every surface — buttons, cards,
156
+ tables, chips, form controls, charts. It is how the baseline itself gets a **rendered acceptance**
157
+ instead of being a value list, and it is the object decision 11's theme-bump pass runs against.
158
+ The committed HTML artifact remains the frozen spec; the harness page is the living version.
159
+
160
+ #### F. Version
161
+
162
+ `2.30.0` — minor, because `createAppTheme` and the assertion are new public exports and nothing
163
+ is removed. DS-4 (chart prop defaults) follows separately as `2.31.0`; see Risks for why they are
164
+ not bundled.
165
+
166
+ ### Non-goals / do not touch
167
+
168
+ - **Dark mode** — DS-12. The API stays mode-capable; no dark tokens ship here.
169
+ - **Chart prop defaults, the legend default, the fm→ucm hoists** — DS-4, `2.31.0`.
170
+ - **The section-navigation shell** — DS-11.
171
+ - **Migrating any consuming app.** Adoption is one WO per app. **innoservice stays untouched**
172
+ by operator decision.
173
+ - **spesix's Google-Fonts CDN link** — DS-14 — and its parallel `DESIGN` token object, which is
174
+ cross-file refactoring and its own WO.
175
+ - **hram's `dataViz` / `results` / `facilityTiers`** — the app-level data-colour layer stays
176
+ exactly where it is, governed by hram's own `theme/COLOR-CONCEPT.md`.
177
+ - **The six hand-drawn hram plots** — DS-7/8/9.
178
+ - **Demonstrating motion.** The tokens are declared; timing cannot be judged from a static sheet,
179
+ and the harness page is where it first becomes visible.
180
+ - **No behaviour, permission or data-contract change** anywhere in this WO.
181
+
182
+ ### Replaces / removes
183
+
184
+ 1. **`useNeutralChartPalette`'s status-derived palette as the effective series source for
185
+ adopters.** The `DESIGN.md` #2 violation (status hues used as series colours) goes away for any
186
+ app that adopts. The exports and the non-adopter behaviour stay — see scope D.
187
+ 2. **`ui-core-micha/DESIGN.md` #1 must be rewritten as part of this WO.** It currently states that
188
+ every app owns its own tokens and that ucm deliberately ships no theme. This WO contradicts
189
+ that, and the operator accepted the policy change (decision 1). It must be rewritten, **not
190
+ silently contradicted** — a stale principle next to a shipped factory is exactly the kind of
191
+ contradiction this programme exists to remove.
192
+ 3. **`DESIGN.md` #2's "the concrete data palette is per-app"** becomes "the data palette is a
193
+ separate layer **with a shared default** that apps override". The separation stays; it becomes
194
+ structural instead of conventional.
195
+
196
+ ### Deliberately keeps
197
+
198
+ - **Per-app `palette.primary` and `fontFamily` as overrides.** App identity is a legitimate reason
199
+ to deviate, and the baseline says so.
200
+ - **hram's `dataViz`/`results`/`facilityTiers` modules**, unchanged and in place.
201
+ - **The public exports** `getNeutralChartPalette` and `useNeutralChartPalette`.
202
+ - **`ChartFrame`'s `p: 2` padding and its foot row** — 16 panels depend on them byte-for-byte.
203
+ - **jg-ferien's `variants:` usage** — it is the sanctioned replacement idiom, not a deviation.
204
+ - **Every consumer's current rendering until it chooses to bump.** All 14 pin exact versions.
205
+
206
+ ### Token delta list
207
+
208
+ **Not applicable here, stated explicitly rather than omitted.** The delta list exists to resolve a
209
+ prototype's values against a *host app's* theme. `ui-core-micha` has no theme of its own, and the
210
+ reference sheet's canonical token table **is** the value source. Per-app deltas arise in the
211
+ adoption WOs, one per app, where an app's existing values meet the baseline.
212
+
213
+ ### Risks
214
+
215
+ - **No staging net.** A push to `main` touching `src/**`, `package.json`, `pnpm-lock.yaml` or
216
+ `tsconfig.build.json` publishes to npm immediately via `.github/workflows/publish.yml`. The
217
+ independent review is the **only** gate and is not back-fillable.
218
+ - **The palette-first ordering is the single most breakable invariant.** Get it wrong and every app
219
+ that overrides `primary` or `success` receives silently wrong component values — a failure of
220
+ exactly the class this programme exists to eliminate, one level down. Test 4 is its guard and
221
+ must be proven non-vacuous.
222
+ - **`shadows` is an array, not an object.** MUI's deep merge does not combine arrays
223
+ element-wise the way it combines objects. The baseline must supply the full 25-entry array or
224
+ deliberately accept MUI's — decide and state which, do not discover it.
225
+ - **14 consumers inherit the baseline on their next pin bump**, and a theme-touching bump is a
226
+ **visual verification event** (decision 11), not a dependency line. The harness page is what
227
+ that pass runs against.
228
+ - **DS-2 and DS-4 are deliberately not bundled** (decision 23). If a consumer sees something odd
229
+ after a combined bump, it is unclear which of two independent surfaces caused it.
230
+ - **Host-merged translation bundles.** New ucm translation bundles are host-merged (the SHELL-1
231
+ pattern, no inline defaults). The factory is expected to ship **no** user-facing strings; if it
232
+ ships any — an assertion message is developer-facing and does not count — that bundle belongs on
233
+ the bump checklist.
234
+
235
+ ### Required tests to WRITE
236
+
237
+ Narrow per AGENTS.md → "Test scope". Written as part of implementation; **run only by the
238
+ Orchestrator**. ucm's existing `vitest` setup has 51 spec files, and
239
+ `tests/chartsPalette.test.js` already synthesises light/dark themes, so there is a precedent to
240
+ extend rather than scaffold.
241
+
242
+ **`tests/createAppTheme.test.js`** (new)
243
+
244
+ 1. A minimal call (`palette.primary` only) returns a theme in which every enumerated surface is
245
+ non-default — the positive case of the assertion.
246
+ 2. Throws without `palette.primary`, with a message naming what is missing.
247
+ 3. Throws on a function in `components.*.styleOverrides`, with a message naming the slot.
248
+ 4. **Palette-first ordering:** an app overriding `palette.success` receives component values
249
+ computed from **its** success, not the baseline's. **Prove non-vacuity** by reversing the
250
+ computation order and confirming this test fails.
251
+ 5. Deep merge: an app's object override merges property-wise with the baseline object, and
252
+ baseline properties the app did not mention survive.
253
+ 6. Two calls with different palettes in one process return two independent themes — survey_app's
254
+ per-site runtime case.
255
+ 7. No function appears anywhere in the baseline's `components` tree (the baseline invariant).
256
+
257
+ **`tests/themeCompleteness.test.js`** (new)
258
+
259
+ 8. The exported assertion returns findings for a theme built with a bare `createTheme()` and none
260
+ for one built with `createAppTheme`.
261
+ 9. An exemption **with** a reason suppresses exactly that surface and no other.
262
+ 10. An exemption **without** a reason is itself a finding.
263
+ 11. Contrast: every status **text** tone clears 4.5:1 against **its own tint**, and
264
+ `controlBorder` clears 3:1 against both white and the page background.
265
+
266
+ **`tests/chartsPalette.test.js`** (extend the existing file)
267
+
268
+ 12. `useNeutralChartPalette` returns the theme's ramp when the theme carries one.
269
+ 13. It falls back to today's derivation when the theme carries no ramp — **a non-adopter sees no
270
+ change**. **Prove non-vacuity** by removing the fallback and confirming this test fails.
271
+
272
+ Plus `tsc -p tsconfig.build.json --noEmit` clean, per the SHELL-1 precedent.
273
+
274
+ **No full-suite run.** The affected-areas set is the three files above; the promotion gate is not
275
+ in play because this repo has no `develop`.
276
+
277
+ ### Verification
278
+
279
+ - The **harness page** renders every surface — the baseline's rendered acceptance.
280
+ - The Orchestrator runs the **two-width rendered side-by-side** against the committed reference
281
+ sheet before commit; it is a hard gate per DS-1, and the two screenshots are named in the
282
+ register Notiz. Evidence-based, not independence-based, so it holds even though the Orchestrator
283
+ may be the author in a Codex fallback.
284
+ - The assertion runs green in ucm's own suite.
285
+ - **Known limitation to state at `done`:** motion is declared but not demonstrated.
286
+
287
+ ### Preconditions
288
+
289
+ - **DS-1 landed** — `webapps@8bd4553` (AGENTS.md, CLAUDE.md, `ui_reviewer`, `frontend-design`,
290
+ `preview-running-app`, `.codex/frontend-engineering`) and `ui-core-micha@e7cb320` (`DESIGN.md`
291
+ #10).
292
+ - **Reference sheet accepted by the operator, 2026-08-10.**
293
+ - No dependency on DS-4 or DS-11. DS-4 follows as `2.31.0`.
294
+
295
+ ### Parity guardrail
296
+
297
+ The reference sheet is the visual spec. **No behaviour, no permission, no data-contract change.**
298
+ The hard condition specific to this WO: **ucm's existing components must render identically for a
299
+ consumer that has not adopted the factory.** Test 13 is that guard for charts; the exact-version
300
+ pins are the guard everywhere else.
301
+
302
+ ### Decisions binding this WO
303
+
304
+ `webapp-management/DESIGN_SYSTEM_PROGRAM.md`: **1** (factory + assertion, policy change accepted) ·
305
+ **5** (assertion via each app's own suite) · **11** (theme bump is a visual verification event) ·
306
+ **17** (mode-capable, light-only v1) · **18** (reference sheet is a deliverable) · **20**
307
+ (objects-only, palette-first, function throw, invariant test) · **21** (throws without palette) ·
308
+ **22** (series-colour transition with fallback) · **23** (`2.30.0`, DS-2 before DS-4). Plus the
309
+ operator decisions of 2026-08-09 on the assertion ratchet, same-file wart cleanup, and innoservice
310
+ staying untouched.
311
+
312
+ ---
313
+
314
+ ## B. Implementation map
315
+
316
+ *Filled by the Orchestrator on `git pull` — see AGENTS.md → "Work Order".*
317
+
318
+ ### Execution directive (read this first)
319
+
320
+ > **If you are the implementer reading this work order as your own specification: this section is
321
+ > NOT addressed to you.** It tells the Orchestrator how to invoke you. **You ARE that invocation —
322
+ > do NOT shell out to `codex exec`.**
323
+ >
324
+ > Implement through `codex exec` in the background — invoked directly via Bash (never the
325
+ > `debugger`/`*_coder` Agent wrappers) with BOTH flags `--skip-git-repo-check` and
326
+ > `--dangerously-bypass-approvals-and-sandbox`, prompt passed as a positional argument from a file.
327
+ > (Fallback to direct Claude implementation only on Codex quota / rate-limit / non-zero exit.)
328
+
329
+ ### Context package
330
+
331
+ **Repo layout relevant here:** `ui-core-micha` is a flat package — no `frontend/` subdir. `src/`
332
+ ships to npm (`tsconfig.build.json` → `include: ["src"]`, `rootDir: "./src"`); `dev/` is the vite
333
+ dev harness (not shipped); `tests/` is vitest, not shipped. Current version `2.29.2` in
334
+ `package.json:3`.
335
+
336
+ **Value source — read this file's canonical token table before writing any token:**
337
+ `work-orders/assets/THEME-1-baseline-reference-sheet.html`, section `#canonical-baseline-tokens`
338
+ (~line 1166–1285). Every numeric value below is transcribed from there; if this map and that table
339
+ ever disagree, the table wins (per the Envelope). Key transcribed values, so you don't have to
340
+ re-derive them:
341
+
342
+ - **Typography** (fontSize/fontWeight/lineHeight; letterSpacing only where the sheet sets one —
343
+ everywhere else use `0` explicitly, don't leave it unset, since MUI's own defaults are non-zero
344
+ and unset would silently inherit them):
345
+ `h1` 32px/600/1.2 · `h2` 28px/600/1.22 · `h3` 24px/600/1.25 · `h4` 20px/600/1.3 ·
346
+ `h5` 18px/600/1.3 · `h6` 16px/600/1.35 · `subtitle1` 15px/600/1.4 · `subtitle2` 13px/500/1.4 ·
347
+ `body1` 14px/400/1.55 · `body2` 13px/400/1.55 · `button` 14px/500, `textTransform:'none'`,
348
+ `letterSpacing:0` · `caption` 12px/400/1.4 · `overline` 11px/600, `textTransform:'uppercase'`,
349
+ `letterSpacing:'0.4px'`.
350
+ `fontFamily: "'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"` — this is the
351
+ baseline's OWN fontFamily default (an app's `typography.fontFamily` override replaces it, per the
352
+ factory example in the Envelope Goal).
353
+ - **Palette — ink:** `primary #212529` · `secondary #5B6670` · `muted #6A7178` (4.6:1 target, not
354
+ the app's own colours — these are baseline-owned, not overridable-by-convention).
355
+ - **Palette — surfaces:** `background.default #FAFAFA` (MUI `grey[50]`) · `background.paper #FFFFFF`.
356
+ - **Palette — borders:** `divider: 'rgba(33,37,41,.10)'` (decorative, exempt from 1.4.11).
357
+ `controlBorder` is NOT a MUI palette key — it is an **augmented namespace** (`palette.controlBorder`)
358
+ with `{ main: 'rgba(33,37,41,.50)', hover: 'rgba(33,37,41,.65)', error: '#BF3227' }` plus **`focus`
359
+ computed from the final `primary.main`**, darkened via MUI's `darken()`/`getContrastRatio()` until
360
+ it clears 3:1 against `background.paper` — this is the one baseline value that is a *function of
361
+ app input*, so it must be computed inside `createAppTheme` after the palette merge (see the
362
+ palette-first ordering invariant below), not stored as a static token.
363
+ - **Status tokens** (each is `{ text, fill, fillText, bg }`, all under an augmented
364
+ `palette.<status>` — `success`/`warning`/`error`/`info` reuse MUI's palette slots for `main`/
365
+ `contrastText` but the baseline ALSO needs the four-part shape the sheet defines; decide whether
366
+ `text`/`fill`/`bg` live as extra keys on the same palette entries (`palette.success.text` etc.) or
367
+ a parallel `palette.status.success.*` — the sheet's token names (`success.text`, `success.fill`,
368
+ `success.fillText`, `success.bg`) suggest keying directly on `palette.success.*` so
369
+ `theme.palette.success.text` / `.fill` / `.fillText` / `.bg` all resolve, alongside MUI's own
370
+ `.main`/`.light`/`.dark`/`.contrastText` which stay untouched for compatibility):
371
+ - success: text `#35794A` · fill `#1B8038` / fillText `#FFFFFF` · bg `#E5F4E9`
372
+ - warning: text `#976100` · fill `#C08A2C` / fillText `#212529` · bg `#FBF0DC`
373
+ - critical (maps to MUI's `error` palette key): text `#BF3227` · fill `#BF3227` / fillText
374
+ `#FFFFFF` · bg `#FBEAE8`
375
+ - info: not itemized with new values in the table — MUI's own `info` stays as-is per the "no new
376
+ hue" pattern used for `stale`; only add `text`/`fill`/`fillText`/`bg` shape for consistency if
377
+ cheap, otherwise state the omission in the completeness assertion's exemption list with a reason.
378
+ - `stale` (new augmented namespace, `palette.stale`, no MUI precedent): text/fill `#5B6B7D`,
379
+ fillText `#FFFFFF`, bg `#EAEDF1`.
380
+ - **Radius:** `shape.borderRadius: 3` (MUI's own single knob covers `radius.control`) — but
381
+ `radius.card: 8` is a SECOND radius MUI has no slot for, so it must be an augmented token
382
+ (`theme.radius.card` or similar) consumed by the `MuiPaper`/`MuiCard`/`MuiDialog`
383
+ `styleOverrides` rather than by `shape`.
384
+ - **Shadows:** `shadows` is the 25-entry MUI array — per the Risk section, either supply the full
385
+ array or explicitly keep MUI's; the sheet's rule is **rest = none** (`elevation:0`,
386
+ `variant:'outlined'` default on `MuiPaper`) and only Dialog/Drawer/Menu/Tooltip get
387
+ `shadow.overlay = '0 8px 24px rgba(20,26,31,.16), 0 2px 8px rgba(20,26,31,.08)'` — the simplest
388
+ correct reading is: don't touch the `shadows` array at all (keep MUI's default 25 entries, since
389
+ nothing here reads `theme.shadows[n]` by index other than via the two named component overrides),
390
+ and apply `shadow.overlay` directly as a literal boxShadow string in the four component
391
+ `styleOverrides`, with `MuiPaper` defaulting to `elevation: 0`/`variant: 'outlined'`. State this
392
+ choice explicitly in the PR/commit — the Risk section calls it out as something to decide, not
393
+ discover.
394
+ - **Density:** `control.height: 40` (44 under `@media (any-pointer: coarse)`) ·
395
+ `table.cellPadding: '10px 16px'` · `chip: { height: 32, radius: 16, fontSize: 13 }`.
396
+ - **Transitions:** `motion.fast 120ms` (hover/focus/border/colour) · `motion.base 180ms`
397
+ (expand/collapse) · `motion.overlay` 220ms in / 180ms out (Dialog/Drawer/Menu/Tooltip) ·
398
+ `motion.chart 300ms` (kept as MUI's `standard`) · easing: enter `easeOut`, exit `easeIn`, state
399
+ `easeInOut` (MUI's own curve constants, only the assignment is new) · reduced-motion: ALL
400
+ durations collapse to `'0.01ms'` (a string, not `0`) under `prefers-reduced-motion: reduce` —
401
+ implement via `transitions.create`'s duration overrides is not enough since that's static; this
402
+ needs either a CSS `@media` global override in a `MuiCssBaseline` styleOverride, or a documented
403
+ runtime hook — read the sheet's motion section (`grep -n "prefers-reduced-motion"` in the HTML)
404
+ for how it demonstrates this, and match that mechanism.
405
+ - **Spacing/breakpoints:** `spacing: 8` (MUI's default unit — confirm it's actually 8 today, i.e.
406
+ this may be a no-op) · `container.gutter: 24px, 32px ≥1200px` via `MuiContainer` styleOverrides.
407
+ - **Components — the 7/7 and 6/7 consensus plus the measured leak surface** (exact values from the
408
+ Density/Radius/Shadow tokens above, applied via `styleOverrides`, OBJECTS ONLY per decision 20 —
409
+ never a function):
410
+ `MuiButton` (height 40, radius 3, textTransform none, no contained elevation) ·
411
+ `MuiTableCell` (padding `10px 16px`) · `MuiIconButton` · `MuiDivider` (uses `divider` token) ·
412
+ `MuiTooltip` (overlay shadow, motion.overlay) · `MuiSelect` (height 40, controlBorder) ·
413
+ `MuiCheckbox` (controlBorder box) · `MuiFormControlLabel` · `MuiContainer` (gutter) ·
414
+ `MuiPaper` (elevation 0, outlined variant default, radius.card) · `MuiCard` (radius.card) ·
415
+ `MuiChip` (height 32, radius 16, fontSize 13) ·
416
+ `MuiTextField`/`MuiOutlinedInput` (controlBorder rest/hover/focus/error, height 40, autofill) ·
417
+ `MuiDialog` (radius.card, overlay shadow) · `MuiAlert` (status bg/text tones).
418
+ - **Autofill:** target BOTH `&:-webkit-autofill` and the standard `&:autofill` inside
419
+ `MuiOutlinedInput`/`MuiFilledInput`/`MuiInput` `styleOverrides.input`, using a `box-shadow` inset
420
+ trick (`box-shadow: 0 0 0 100px <bg.surface> inset`) plus `-webkit-text-fill-color: <ink.primary>`
421
+ — the standard estate pattern for suppressing browser-native autofill colouring; there is no app
422
+ precedent to copy from since this is a measured gap (sheet's `control.autofill` row), so implement
423
+ from the description in the table plus standard MUI/CSS practice.
424
+ - **Data-series ramp:** `series.1`…`series.6` =
425
+ `['#3D5A99', '#3E80B8', '#2E8F8A', '#7A5FA8', '#9C4F86', '#8A7355']`. Marker/dasharray/pattern
426
+ variants are DS-13, **opt-in and out of scope** — do not implement them, the ramp colours only.
427
+ Expose this as e.g. `theme.chartPalette` or `theme.palette.dataSeries` (pick one, document it,
428
+ and use the SAME key in the `useNeutralChartPalette` fallback wiring below).
429
+ - **Font loading:** `@fontsource/dm-sans` weights 400/500/600, self-hosted, never a CDN. All 14
430
+ consumer apps already depend on `@fontsource/dm-sans` in their own `package.json` (verified
431
+ 2026-08-10) — so the safest approach is for `createAppTheme`'s module (or a sibling module it
432
+ imports) to `import '@fontsource/dm-sans/400.css'` etc. as a side effect, AND for `ui-core-micha`
433
+ to gain its OWN `dependencies` entry `"@fontsource/dm-sans": "^5.2.8"` (matching the version every
434
+ consumer already pins) in `package.json` — this is a new dependency, but it is explicitly named in
435
+ this WO's scope (Envelope § B, "Font loading" row), so it carries the WO's approval per AGENTS.md
436
+ and does not need a separate approval round. A duplicate import in an app that also imports the
437
+ font itself is harmless (same CSS, same `@font-face`, browser dedups by URL).
438
+ - **Touch target:** 44px hit area gated by `@media (any-pointer: coarse)` on `MuiButton`/
439
+ `MuiIconButton`/`MuiSelect`/`MuiCheckbox` etc. — visual `control.height` stays 40, only the
440
+ clickable/touch area grows under the coarse-pointer query (commonly via `minHeight`/padding, not
441
+ a visual box-size change at 40px).
442
+
443
+ ### Named files to create / change
444
+
445
+ **New:**
446
+ - `src/theme/createAppTheme.js` — the factory. `export function createAppTheme(appConfig = {})`.
447
+ Steps inside: (1) throw if `!appConfig?.palette?.primary` with a message naming what's missing;
448
+ (2) throw if any function is found in `appConfig.components.*.styleOverrides` (walk the object,
449
+ name the offending slot in the message); (3) build a first-pass theme merging
450
+ `BASELINE_PALETTE` with `appConfig.palette` ONLY (via `createTheme({ palette: {...} })` or a plain
451
+ object merge — palette-first) to get the FINAL resolved palette (primary, success, etc. as the app
452
+ set them); (4) compute palette-derived values from that final palette — `controlBorder.focus`
453
+ (darken `primary.main` until ≥3:1), and anything else in the components tree that depends on
454
+ `success`/`warning`/`error` colours; (5) call MUI's `createTheme(BASELINE_STATIC, appConfig,
455
+ { palette: computed additions })` or equivalent multi-arg merge, being careful that the
456
+ computed/palette-derived pieces are merged AFTER the palette but BEFORE/alongside `appConfig`'s
457
+ own component overrides — re-read the palette-first invariant (Envelope § A) and design the merge
458
+ order so Test 4 (reversing the order must fail) is provable. This is the single most important
459
+ piece of code in the WO — do not guess at MUI's deep-merge semantics without checking `mui/material`
460
+ `createTheme`'s documented multi-arg behavior (later args win, plain-object deep merge, arrays
461
+ replace not concatenate).
462
+ Import `@fontsource/dm-sans/400.css`, `/500.css`, `/600.css` here as side-effect imports.
463
+ - `src/theme/tokens.js` (or inline in `createAppTheme.js` if smaller) — `BASELINE_PALETTE` /
464
+ `BASELINE_STATIC` constants holding the values transcribed above, one place, referenced by both
465
+ the factory and (for baseline-invariant testing) the completeness assertion.
466
+ - `src/theme/themeCompleteness.js` — the exported assertion. Shape (design freely, but must satisfy
467
+ Envelope § C and the required tests): a function like
468
+ `assertThemeComplete(theme, { exemptions = [] } = {})` returning `{ findings: [...] }` (array of
469
+ `{ surface, reason? }`), where a `findings.length === 0` result is "complete". Compares the given
470
+ theme's enumerated surfaces (typography variants, palette tokens, component overrides, shadow
471
+ rule, etc.) against MUI's OWN untouched `createTheme()` output for the same surfaces — a surface
472
+ equal to the default is a finding UNLESS present in `exemptions` with a `reason` (no reason on an
473
+ exemption is itself a finding — decision 5). Also export a **contrast-checking** helper (or reuse
474
+ one if MUI/`polished`/similar is already a devDependency — check `package.json` first; if nothing
475
+ suitable exists, implement WCAG relative-luminance contrast inline, it's ~15 lines) used both by
476
+ the assertion (status text-vs-own-tint, `controlBorder` vs white/page) and by test 11.
477
+ Also export an **adoption-reporting** helper (inline-hex count, theme-bypass paths) — Envelope
478
+ says "reported as a number, never gated"; keep this simple (e.g. count of raw hex literals in a
479
+ given source-string set, or just document the intended API surface if a full static-analysis pass
480
+ is out of proportion for this WO — narrow test scope, don't over-build).
481
+ - `src/theme/index.js` — re-exports `createAppTheme`, `assertThemeComplete` (+ whatever helper
482
+ names you settle on) for `src/index.js` to pull from.
483
+ - `tests/createAppTheme.test.js`, `tests/themeCompleteness.test.js` — per Required tests below.
484
+
485
+ **Change:**
486
+ - `src/index.js` — add exports for `createAppTheme` and the completeness assertion near the top
487
+ (a new numbered section, e.g. "0. Theme", or alongside an existing section — match the file's
488
+ existing numbered-comment convention).
489
+ - `src/components/charts/palette.js:7-19` (`getNeutralChartPalette`) — add the ramp-first branch:
490
+ if `theme.palette?.dataSeries` (or whatever key `createAppTheme` settles on above — MUST match)
491
+ is present and has both `categorical`-shaped data, return it (or derive `categorical` from it,
492
+ `sequential` can keep deriving from ink/primary as today unless the sheet specifies a sequential
493
+ ramp too — it doesn't, only categorical `series.1..6` is enumerated); ELSE fall back to the
494
+ existing derivation unchanged (decision 22 — a non-adopter theme has no `dataSeries` key and must
495
+ see byte-identical output to today).
496
+ - `tests/chartsPalette.test.js` — extend with tests 12–13 (ramp present → used; ramp absent →
497
+ today's derivation, proven non-vacuous by temporarily removing the fallback branch and observing
498
+ the test fail, then restoring it — this is a proof step during development, not something to leave
499
+ removed).
500
+ - `package.json` — version `2.29.2` → `2.30.0`; add a `"dependencies"` block (currently absent —
501
+ only `peerDependencies`/`devDependencies` exist) with `"@fontsource/dm-sans": "^5.2.8"`.
502
+ - `DESIGN.md:9` (principle 1) — rewrite to reflect the policy change (decision 1): ucm now ships a
503
+ shared theme baseline via `createAppTheme`; apps still own their identity (`palette.primary`,
504
+ `fontFamily`) as deliberate overrides, but the baseline (not each app from scratch) is the source
505
+ of everything else. Don't delete the "match first, then elevate" sentiment — it still governs
506
+ what an app's OWN overrides should look like — but the "every app owns its own tokens... ucm ships
507
+ no theme" framing is now false and must go.
508
+ - `DESIGN.md:22` (principle 8, the "concrete data palette is per-app" clause) — reword per Envelope
509
+ § "Replaces/removes" item 3: the data-colour layer is now "a separate layer **with a shared
510
+ default** that apps override" rather than purely per-app.
511
+ - `dev/entries.jsx` — add a new harness entry rendering the baseline's surfaces (buttons, cards,
512
+ tables, chips, form controls; a chart via the existing `BarChart`/`ChartFrame` to show the ramp).
513
+ This is decision 18's "living harness page" — it does not need to reproduce the full 1352-line
514
+ reference sheet, just render enough real MUI components under `createAppTheme` for the Orchestrator
515
+ to do the two-width visual comparison against the reference sheet. Reasonable scope: one entry,
516
+ e.g. `id: 'theme-baseline'`, composing `Button` (contained/outlined/text), a `Paper`/`Card` pair,
517
+ a `TableCell`-based table, `Chip` (default + status colours), `TextField`/`Select`/`Checkbox`, an
518
+ `Alert` per status (including `stale` if you added it to the palette), and the existing `BarChart`
519
+ to show the series ramp.
520
+ - `dev/main.jsx:32` — the harness currently does `createTheme({ palette: { mode } })` directly;
521
+ add a way to switch the harness's OWN theme to `createAppTheme({ palette: { primary: {...} } })`
522
+ for the new entry (or just always use `createAppTheme` if it stays backward compatible with a
523
+ bare mode toggle — check whether dark mode is requested elsewhere in the harness; the WO is
524
+ light-only v1, so if switching the whole harness to `createAppTheme` breaks the dark-mode toggle
525
+ for OTHER entries, keep the two paths separate instead of forcing one factory globally).
526
+
527
+ ### Do-not-touch / invariants
528
+
529
+ - **No behaviour, permission, or data-contract change** anywhere (parity guardrail).
530
+ - **`ChartFrame`'s `p: 2` padding and its foot row** stay untouched — 16 panels depend on them.
531
+ - **`getNeutralChartPalette`/`useNeutralChartPalette` exports** stay, same signature, same names.
532
+ - **jg-ferien's `variants:` usage** is not in this repo — irrelevant here, just don't introduce a
533
+ function anywhere in `BASELINE_STATIC.components.*.styleOverrides`.
534
+ - **innoservice, and every other consumer** — do not touch any other repo. All 14 consumers pin
535
+ exact versions and are unaffected until they bump.
536
+ - Dark mode tokens: do not add any — the API must stay mode-capable (i.e. don't hard-code
537
+ `mode: 'light'` inside the factory in a way that would need a breaking signature change later;
538
+ simplest: don't set `palette.mode` at all in `BASELINE_PALETTE`, let `appConfig`/MUI's default
539
+ handle it, and only ship light-token values).
540
+
541
+ ### Pitfalls (verified against landed code 2026-08-10)
542
+
543
+ - `getNeutralChartPalette` is called in existing tests with a **plain object**, not a real MUI
544
+ theme (`tests/chartsPalette.test.js:11-23`) — so the new ramp-lookup branch must not call any
545
+ MUI-only API (e.g. `useTheme()`) inside `getNeutralChartPalette` itself; keep it a pure function
546
+ of the `theme` object passed in, exactly as today.
547
+ - `src/index.js` has a duplicate `// --- 9. ... ---` comment (two "9"s, messaging and onboarding) —
548
+ pre-existing, not yours to fix; just don't compound it, pick a header that doesn't collide.
549
+ - The dev harness (`dev/main.jsx`) is NOT part of the shipped package (`tsconfig.build.json` only
550
+ includes `src`) — safe to add whatever's needed there without touching the npm build surface.
551
+ - MUI's `shadows` theme key is a 25-element ARRAY; passing a partial array in a `createTheme()` arg
552
+ REPLACES the whole array (arrays don't deep-merge index-wise) — this is why the map above says
553
+ "don't touch `shadows` at all" rather than trying to override two entries.
554
+
555
+ ### Target repo working directory (absolute)
556
+
557
+ `C:\Users\biglmi\Documents\webapps\ui-core-micha`
558
+
559
+ ### Required tests to WRITE (Codex writes them; the Orchestrator runs them)
560
+
561
+ Exactly the 13 tests plus `tsc` check enumerated in Envelope § "Required tests to WRITE" above —
562
+ `tests/createAppTheme.test.js` (7 tests), `tests/themeCompleteness.test.js` (4 tests),
563
+ `tests/chartsPalette.test.js` extension (2 tests), plus `tsc -p tsconfig.build.json --noEmit`
564
+ clean. Do not add more, do not run the full suite.
565
+
566
+ ### Preamble (append verbatim)
567
+
568
+ > The text above is the COMPLETE spec — the committed WO file's content, not a plan to refine; there
569
+ > is no separate plan file. Read the nearest `AGENTS.md`, the relevant `.codex/skills/<role>/SKILL.md`, and the
570
+ > app `MEMORY.md` ONLY for conventions. Stay in scope; do not touch auth/permissions/deps/schema/CI
571
+ > unless the spec says so (the ONE new dependency, `@fontsource/dm-sans`, IS in scope — see "Font
572
+ > loading" above); do not update `MEMORY.md`. Do NOT `git add`/`commit`/`push` — leave every change
573
+ > uncommitted in the working tree for the orchestrator's independent review. WRITE the tests the
574
+ > `Required tests` section calls for AND **RUN the tests you just wrote** to confirm they execute
575
+ > and pass — that is the ONLY test run you do (NOT the app's affected/full suite, NOT any review).
576
+ > The orchestrator re-runs the authoritative set + does the independent review after you finish —
577
+ > those are the gate; your own run does not count as the gate.
578
+ >
579
+ > Narrate continuously: a `PLAN: <step1> | <step2> | …` line up front, then a single-line
580
+ > `PROGRESS: [<n>/<total>] <present-tense action>` before every relevant action (and `… done` on
581
+ > completion), spaced so no gap exceeds ~2 min, stdout unbuffered, plus exactly one final
582
+ > `RESULT: DONE|BLOCKED <reason>`.
583
+
584
+ ### Mini-handover
585
+
586
+ Repo: `ui-core-micha` (`C:\Users\biglmi\Documents\webapps\ui-core-micha`), branch `main`. WO:
587
+ `work-orders/THEME-1.md`. Follow `orchestrate-codex`.