@micha.bigler/ui-core-micha 2.30.0 → 2.31.1

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.
@@ -0,0 +1,339 @@
1
+ # THEME-2 — Two defects from the THEME-1/CHART-6 landings: `warning.main` contrast, and `grid` replacing instead of merging
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` **and** `ui_reviewer` — spawned concurrently, both mandatory before commit
6
+ **Version target:** `2.31.1` (patch — one palette value corrected, one merge bug fixed, one assertion rule added; no API change)
7
+ **Decision record:** `webapp-management/DESIGN_SYSTEM_PROGRAM.md` → "Open follow-ups from the two landings"
8
+
9
+ Short-form WO: both defects are fully diagnosed and measured. There is no design question in either.
10
+
11
+ ---
12
+
13
+ ## A. Envelope
14
+
15
+ ### Goal
16
+
17
+ Fix the two defects found after `THEME-1` (`96992cf`) and `CHART-6` (`6dedc62`) landed, and in each
18
+ case close the **mechanism** rather than only the instance — both are the same failure class as
19
+ something already fixed, which is why they recurred.
20
+
21
+ ### Defect 1 — `warning.main` fails AA as a foreground
22
+
23
+ THEME-1's R1 fix gave every status channel an explicit `main`/`light`/`dark`/`contrastText` (correct
24
+ — MUI was otherwise filling its stock hues for `color="success"` and friends). It put the **fill**
25
+ tone in `main`. Measured against `#FFFFFF`:
26
+
27
+ | Channel | `main` | as foreground | |
28
+ |---|---|---|---|
29
+ | success | `#1B8038` | 5.010 | passes |
30
+ | error | `#BF3227` | 5.674 | passes |
31
+ | **warning** | `#C08A2C` | **3.038** | **fails AA (4.5)** |
32
+
33
+ `main`/`contrastText` pairs are all fine (warning 5.078 with dark ink). The failure is only when
34
+ `main` is used as a **foreground on a light surface** — `<Typography color="warning.main">`,
35
+ `color="warning"` on an icon, and an outlined warning chip, whose border and label both come from
36
+ `main`.
37
+
38
+ **Why warning specifically, and why this is not carelessness:** green and red can serve both roles
39
+ with one value. Amber cannot. A legible amber **fill** needs dark text on it and must therefore stay
40
+ light; a legible amber **text** must be dark. The two requirements pull in opposite directions.
41
+ Warning is the one channel where "one hex for both roles" is impossible in principle — so the
42
+ pattern that works for success and error had to break here.
43
+
44
+ **Fix:** `warning.main` = `#976100` (the text tone already in the same token block, 5.216 on white),
45
+ `contrastText` = `#FFFFFF`. The light amber stays available as `warning.fill` for deliberate fill
46
+ use, unchanged.
47
+
48
+ **Accepted visual consequence, stated so nobody treats it as a regression:** a filled
49
+ `color="warning"` Button/Chip becomes a darker brown with white text instead of a lighter amber with
50
+ dark text. That is the price of one token serving MUI's `main` contract, and it is the operator's
51
+ call already recorded in the programme doc.
52
+
53
+ **Close the mechanism, not just the value:** add an assertion rule that checks **every** status
54
+ channel's `main` against `#FFFFFF` and `background.default` at 4.5:1. Without it the next channel
55
+ added repeats this exactly — the existing contrast checks were designed before R1 introduced `main`,
56
+ which is why they did not catch it.
57
+
58
+ ### Defect 2 — `grid` is replaced, not merged
59
+
60
+ `LineChart` and `BarChart` declare `grid = { horizontal: true }` as a plain default parameter and
61
+ pass it straight through. A caller writing `grid={{ vertical: true }}` — intending to *add* vertical
62
+ lines — silently loses the horizontal ones.
63
+
64
+ This is the same class as the `scaleType` bug the rendered gate caught during CHART-6: **a partial
65
+ caller object erases a default.** `xAxis`, `yAxis`, `margin` and `slotProps` all go through merge
66
+ helpers; `grid` alone does not.
67
+
68
+ **Fix:** merge `grid` with the default the way the sibling props already do, so a caller adds to it
69
+ rather than replacing it.
70
+
71
+ **Close the mechanism:** audit the remaining object-valued props the wrappers default or forward
72
+ (`slots`, `tickLabelStyle` and anything CHART-6 introduced) for the same replace-instead-of-merge
73
+ shape, and state the result — either "all others already merge" or a named list. **Audit and report;
74
+ do not refactor beyond `grid`** unless the audit finds another live instance, in which case fix that
75
+ too and say so.
76
+
77
+ ### Non-goals / do not touch
78
+
79
+ - The other status channels' `main` values — success and error measure fine and stay.
80
+ - `warning.fill` / `warning.text` / `warning.bg` — unchanged.
81
+ - The reference sheet's committed HTML artifact. It is the frozen spec of the accepted baseline;
82
+ `warning.main` is a channel the sheet never depicted (see Verification).
83
+ - Anything in `CHART-6`'s scope beyond the `grid` merge and the audit.
84
+ - No behaviour, permission or data-contract change.
85
+
86
+ ### Risks
87
+
88
+ - **A palette value change is visible.** Any app already using `color="warning"` sees a darker
89
+ filled control. No app has an adoption WO yet, so the blast radius today is ucm's own harness — but
90
+ say it in the release note rather than let someone find it.
91
+ - **Adding an assertion rule can turn a green suite red.** Safe today by the ratchet: the
92
+ completeness check is a hard failure only for an app whose adoption WO has landed, and none has.
93
+ It must stay that way — the new rule must not become hard for non-adopters.
94
+ - **No staging net.** A push to `main` touching `src/**` publishes to npm at once; the independent
95
+ review is the only gate and is not back-fillable.
96
+ - **The `grid` merge changes behaviour for any caller who passes a partial `grid` today.** That is
97
+ the point, but it means the rendered check should look at a chart that passes `grid` explicitly if
98
+ one exists.
99
+
100
+ ### Required tests to WRITE
101
+
102
+ Narrow. Extend the existing files rather than adding new ones.
103
+
104
+ **`tests/themeCompleteness.test.js`**
105
+
106
+ 1. Every status channel's `main` clears 4.5:1 against `#FFFFFF` **and** against
107
+ `background.default`. **Prove non-vacuity** by restoring `warning.main` to `#C08A2C` and
108
+ confirming this test fails — that is the whole point of the rule.
109
+ 2. The new rule reports, and does not hard-fail, for a theme belonging to an app without a landed
110
+ adoption WO (the ratchet stays intact).
111
+
112
+ **`tests/chartDefaults.test.js`**
113
+
114
+ 3. A caller passing `grid={{ vertical: true }}` receives **both** `vertical: true` and the default
115
+ `horizontal: true`. **Prove non-vacuity** by reverting to the plain default parameter and
116
+ confirming the test fails.
117
+ 4. A caller passing `grid={false}` (or the documented way to switch the grid off entirely) still
118
+ gets no grid — merging must not make the default un-removable.
119
+
120
+ Plus `tsc -p tsconfig.build.json --noEmit` clean.
121
+
122
+ **No full-suite run.** Affected-areas set is the two test files above.
123
+
124
+ ### Verification
125
+
126
+ - The rendered two-width side-by-side per DS-1, **and this time it must cover what changed**: the
127
+ harness needs a specimen row rendering `color="success|warning|error|info"` on **Button, Chip and
128
+ Alert** — the components where `main` actually resolves. It has none today, which is precisely why
129
+ the gate could not catch defect 1 the first time. Adding that row is part of this WO.
130
+ - A chart specimen that passes a partial `grid`, to show the merge.
131
+
132
+ ### Preconditions
133
+
134
+ - `THEME-1` (`2.30.0`) and `CHART-6` (`2.31.0`) landed and published.
135
+
136
+ ### Parity guardrail
137
+
138
+ No behaviour, permission or data-contract change. One palette value moves deliberately and visibly;
139
+ everything else must render identically.
140
+
141
+ ---
142
+
143
+ ## B. Implementation map
144
+
145
+ *Filled by the Orchestrator on `git pull` — see AGENTS.md → "Work Order".*
146
+
147
+ ### Execution directive (read this first)
148
+
149
+ > **If you are the implementer reading this work order as your own specification: this section is
150
+ > NOT addressed to you.** It tells the Orchestrator how to invoke you. **You ARE that invocation —
151
+ > do NOT shell out to `codex exec`.**
152
+ >
153
+ > Implement through `codex exec` in the background — invoked directly via Bash (never the
154
+ > `debugger`/`*_coder` Agent wrappers) with BOTH flags `--skip-git-repo-check` and
155
+ > `--dangerously-bypass-approvals-and-sandbox`. **Pass the WO via stdin, not as a positional
156
+ > argument** — `cat work-orders/THEME-2.md | codex exec --skip-git-repo-check
157
+ > --dangerously-bypass-approvals-and-sandbox -` (a positional arg hits Windows' command-line length
158
+ > limit on a WO this size — proven on THEME-1/CHART-6). Fallback to direct Claude implementation
159
+ > only on Codex quota/rate-limit/non-zero exit.
160
+
161
+ ### Context package
162
+
163
+ **Preconditions verified 2026-08-10:** `THEME-1` (`96992cf`) and `CHART-6` (`6dedc62`) are both
164
+ landed on `main` and published (`2.30.0`, `2.31.0`).
165
+
166
+ **Defect 1 — exact current state, verified in `src/theme/tokens.js:81-87`:**
167
+ ```js
168
+ warning: {
169
+ ...withMainShades('#C08A2C', '#212529'),
170
+ text: '#976100',
171
+ fill: '#C08A2C',
172
+ fillText: '#212529',
173
+ bg: '#FBF0DC',
174
+ },
175
+ ```
176
+ `withMainShades(main, contrastText)` (defined near the top of `tokens.js`) sets `main`/`light`/`dark`/
177
+ `contrastText` via MUI's own `lighten`/`darken` at 0.2/0.3 tonal offsets. **Fix:** change the call to
178
+ `withMainShades('#976100', '#FFFFFF')` — reusing the existing `text` value as the new `main`, and
179
+ `'#FFFFFF'` as `contrastText` (verify this actually clears 4.5:1 — the WO states 5.216 on white
180
+ already for `#976100`, and `contrastText` white-on-`#976100` is the same ratio by symmetry). Leave
181
+ `text`/`fill`/`fillText`/`bg` untouched — `fill` stays the lighter amber for deliberate fill use
182
+ (THEME-1's own `MuiAlert` `standardWarning` styleOverride already reads `palette.warning.text`/`.bg`,
183
+ NOT `.main`, so that specimen is unaffected by this change — see the harness note below for why a
184
+ NEW specimen is needed to actually exercise `.main`).
185
+
186
+ **Defect 1's assertion rule — add to `src/theme/themeCompleteness.js`'s `contrastFindings(theme)`
187
+ function (~line 249).** Alongside the existing per-status `text`-vs-`bg` and `fillText`-vs-`fill`
188
+ loops, add a third: for each `status` in `STATUS_KEYS` (`['success','warning','error','info',
189
+ 'stale']`, from `tokens.js`) that HAS a `.main` property (**`stale` does not** — it's a custom
190
+ augmented namespace, never passed through MUI's `createPalette`/`augmentColor`, so
191
+ `theme.palette.stale.main` is `undefined` — skip it, don't push a spurious finding for a channel
192
+ that was never supposed to have one), check `calculateContrastRatio(tokens.main, '#FFFFFF')` and
193
+ `calculateContrastRatio(tokens.main, theme.palette.background.default)`, both `>= 4.5`, pushing a
194
+ `contrast.<status>.main-on-<white|page>` finding (matching the existing naming convention,
195
+ `contrast.controlBorder.<state>-on-<surface>`) on failure. This is a REPORT via the existing
196
+ `findings` array — `assertThemeComplete` never throws or hard-fails on its own (confirm this by
197
+ reading the function; the "ratchet stays a report, not a hard fail for non-adopters" is structural
198
+ here, not a flag to add — a consuming app's OWN test suite decides whether to assert
199
+ `findings.length === 0`, ucm's export just returns data either way).
200
+
201
+ **Defect 2 — exact current state, verified in `src/components/charts/BarChart.jsx:26` and
202
+ `LineChart.jsx:43`:** `grid = { horizontal: true }` as a bare JS default parameter, then
203
+ `grid={grid}` passed straight through (`BarChart.jsx:65`, `LineChart.jsx:88`) — a caller-supplied
204
+ `grid` object REPLACES the default wholesale rather than merging per-key, unlike every sibling prop
205
+ in this file (compare `withAxisDefaults`'s `tickLabelStyle` merge, `withChartSlotDefaults`'s
206
+ `tooltip`/`legend` merge — both spread the default first, caller second, so the caller only
207
+ overrides the keys it actually sets). **Fix:** add a `withGridDefaults(grid)` helper to
208
+ `src/components/charts/chartDefaults.js` following the SAME pattern (`{ horizontal: true, ...grid }`
209
+ — MUI's `grid` prop type is `Pick<ChartsGridProps, 'vertical'|'horizontal'>`, plain optional
210
+ booleans, no special `false`/`true` shorthand at the type level, so spreading a caller's object
211
+ after the default is sufficient: a caller wanting no grid at all passes
212
+ `grid={{ horizontal: false, vertical: false }}` explicitly, which the spread correctly honours since
213
+ it's a later key overriding an earlier one — do not add special-casing for a literal `grid={false}`
214
+ unless you find MUI's actual runtime prop handling treats a boolean differently from the declared
215
+ type, which the WO's own phrasing ("or the documented way to switch the grid off entirely") already
216
+ anticipates might not be a literal `false`). Wire it into both `BarChart.jsx` and `LineChart.jsx`
217
+ the same way the other `chartDefaults.js` helpers are used.
218
+
219
+ **Defect 2's audit — read `src/components/charts/chartDefaults.js` in full and check each
220
+ object-valued prop the wrappers default or forward against the same replace-vs-merge question:**
221
+ - `withAxisDefaults`'s `tickLabelStyle: { fontSize: tickFontSize, ...axis.tickLabelStyle }` —
222
+ ALREADY merges correctly (default first, caller's own `tickLabelStyle` fields win per-key).
223
+ - `withChartSlotDefaults`'s `{ ...slotProps, tooltip: {...}, legend: {...} }` — the outer
224
+ `...slotProps` spread preserves any OTHER slot key a caller sets (e.g. `slotProps.axisHighlight`),
225
+ and the `tooltip`/`legend` sub-objects are themselves merged default-first. ALREADY correct.
226
+ - `LineChart.jsx`'s `slots={{ mark: FilledMarkElement, ...slots }}` — default first, caller's own
227
+ `slots` spread after, so a caller's own `slots.mark` wins and other slot keys survive. ALREADY
228
+ correct.
229
+ - `spaceForRotatedTicks`'s `margin` handling is NOT a "default object caller can partially override"
230
+ shape at all (it only computes `margin.bottom` when rotation is detected AND the caller hasn't set
231
+ one) — not the same bug class, nothing to fix there, but confirm this reading is right by re-reading
232
+ the function rather than trusting this summary.
233
+ **Verify each of these four independently rather than trusting this list — this map is a starting
234
+ point for the audit, not a substitute for it.** If the audit finds anything actually broken beyond
235
+ `grid`, fix it and say so explicitly in the PR/commit, per the WO's "fix that too and say so." If the
236
+ audit confirms all four are already correct, state that explicitly too — "audited and confirmed
237
+ correct" is a valid, expected outcome per the WO ("either... or a named list").
238
+
239
+ **Harness — why a new specimen row is required, not optional (verified 2026-08-10):** the current
240
+ `ThemeBaselineEntry` (`dev/entries.jsx`) has TWO status-colour specimens today, and NEITHER exercises
241
+ `palette.<status>.main`:
242
+ 1. `StatusChip` (`dev/entries.jsx:71-78`) sets `sx={{ bgcolor: '${tone}.fill', color:
243
+ '${tone}.fillText' }}` directly — bypasses `.main` entirely by design.
244
+ 2. `<Alert severity="warning">` etc. (`dev/entries.jsx:136-140`) — THEME-1's `MuiAlert`
245
+ `styleOverrides.standardWarning` (`src/theme/createAppTheme.js`'s `createPaletteAwareComponents`)
246
+ already overrides the STANDARD variant's `color`/`backgroundColor` to `palette.warning.text`/
247
+ `.bg` directly, NOT `.main` — so this existing Alert row was already immune to defect 1 and would
248
+ not have caught it even now.
249
+ Add: a row of `<Button color="success|warning|error|info" variant="contained">` (MUI's contained
250
+ Button reads `palette[color].main` for its background and `.contrastText` for its label — this is
251
+ the most direct `.main` exposure in the kit), a row of native `<Chip color="warning">` (not
252
+ `StatusChip` — MUI's own `color` prop on `Chip`, filled variant, also reads `.main`/
253
+ `.contrastText` directly), and an `<Alert severity="warning" variant="filled">` (MUI's `filled`
254
+ Alert variant is NOT covered by THEME-1's `standardWarning`-only override, so it still resolves
255
+ `.main` — confirm this by checking `createPaletteAwareComponents`'s `alertStyles` only sets
256
+ `standard${Status}` keys, never `filled${Status}` or `outlined${Status}`). These three together
257
+ are what the WO's Verification section means by "Button, Chip and Alert — the components where
258
+ `main` actually resolves."
259
+
260
+ **Chart specimen for the `grid` merge:** the existing "Caller overrides" `LineChart` specimen
261
+ (`dev/entries.jsx`, added in CHART-6) already sets `grid={{ horizontal: true, vertical: true }}` — a
262
+ COMPLETE object, which happens to already include `horizontal: true`, so it would not visibly
263
+ demonstrate the bug even pre-fix. Change it to a genuinely PARTIAL grid, e.g. `grid={{ vertical:
264
+ true }}` alone — pre-fix this would have shown ONLY vertical lines (losing the horizontal default);
265
+ post-fix it must show both. This is the specimen the WO's Verification section calls for ("A chart
266
+ specimen that passes a partial `grid`, to show the merge").
267
+
268
+ ### Named files to change
269
+
270
+ - `src/theme/tokens.js` — the `warning` entry's `withMainShades(...)` call (line 82).
271
+ - `src/theme/themeCompleteness.js` — `contrastFindings` (~line 249), add the `main`-vs-white/page
272
+ loop, skipping any `STATUS_KEYS` entry without a `.main`.
273
+ - `src/components/charts/chartDefaults.js` — new `withGridDefaults` export.
274
+ - `src/components/charts/BarChart.jsx:26,65`, `src/components/charts/LineChart.jsx:43,88` — wire in
275
+ `withGridDefaults`.
276
+ - `dev/entries.jsx` — new Button/Chip/Alert `.main`-exercising specimen row; fix the existing
277
+ "Caller overrides" LineChart's `grid` prop to a genuinely partial object.
278
+ - `package.json` — version `2.31.0` → `2.31.1` (patch).
279
+ - `tests/themeCompleteness.test.js` — extend with the 2 required tests (main contrast + non-vacuity
280
+ proof by temporarily reverting `warning.main`; ratchet/report-not-hard-fail confirmation).
281
+ - `tests/chartDefaults.test.js` — extend with the 2 required tests (partial-grid merge + non-vacuity
282
+ proof by temporarily reverting to the bare default parameter; grid-off-via-explicit-false-object
283
+ still works).
284
+
285
+ ### Do-not-touch / invariants
286
+
287
+ - `success`/`error`/`info`/`stale` palette values — untouched, they measure fine.
288
+ - `warning.fill`/`.text`/`.bg` — untouched.
289
+ - The committed reference-sheet HTML artifact (`work-orders/assets/THEME-1-baseline-reference-sheet.html`)
290
+ — frozen spec, never depicted `warning.main` as a foreground, not touched by this WO.
291
+ - Anything in CHART-6's scope beyond the `grid` merge and the audit — no new chart-chrome feature
292
+ work here.
293
+ - No behaviour, permission, or data-contract change.
294
+
295
+ ### Pitfalls (verified against landed code 2026-08-10)
296
+
297
+ - `withMainShades` is defined once near the top of `tokens.js` and used for
298
+ `success`/`warning`/`error`/`info` — changing ONLY the `warning` call site, not the helper itself
299
+ (the helper's `lighten`/`darken` derivation logic is correct and unrelated to this bug).
300
+ - `STATUS_KEYS` includes `'stale'`, which has no MUI-augmented `.main` — a naive loop over all
301
+ `STATUS_KEYS` reading `tokens.main` will hit `undefined` for `stale`; guard it (`if (!tokens?.main)
302
+ continue`, matching the existing loop's `if (!tokens) continue` pattern one function up).
303
+ - Don't confuse this WO's `contrastFindings` addition with `THEME_COMPLETENESS_SURFACES`'s existing
304
+ `STATUS_SURFACES` (`text`/`fill`/`fillText`/`bg` presence-vs-default check) — the new rule is a
305
+ CONTRAST check (numeric ratio), same family as the existing `contrast.<status>.text-on-bg` /
306
+ `contrast.controlBorder.*` findings, not a "surface is deliberately defined" presence check.
307
+
308
+ ### Target repo working directory (absolute)
309
+
310
+ `C:\Users\biglmi\Documents\webapps\ui-core-micha`
311
+
312
+ ### Required tests to WRITE (Codex writes them; the Orchestrator runs them)
313
+
314
+ Exactly the 4 tests enumerated in Envelope § "Required tests to WRITE" above, extending
315
+ `tests/themeCompleteness.test.js` and `tests/chartDefaults.test.js` (no new files), plus
316
+ `tsc -p tsconfig.build.json --noEmit` clean. Do not add more, do not run the full suite. Affected-areas
317
+ set is exactly those two files.
318
+
319
+ ### Preamble (append verbatim)
320
+
321
+ > The text above is the COMPLETE spec — the committed WO file's content, not a plan to refine; there
322
+ > is no separate plan file. Read the nearest `AGENTS.md`, the relevant `.codex/skills/<role>/SKILL.md`, and the
323
+ > app `MEMORY.md` ONLY for conventions. Stay in scope; do not touch auth/permissions/deps/schema/CI
324
+ > unless the spec says so; do not update `MEMORY.md`. Do NOT `git add`/`commit`/`push` — leave every
325
+ > change uncommitted in the working tree for the orchestrator's independent review. WRITE the tests
326
+ > the `Required tests` section calls for AND **RUN the tests you just wrote** to confirm they execute
327
+ > and pass — that is the ONLY test run you do (NOT the app's affected/full suite, NOT any review).
328
+ > The orchestrator re-runs the authoritative set + does the independent review after you finish —
329
+ > those are the gate; your own run does not count as the gate.
330
+ >
331
+ > Narrate continuously: a `PLAN: <step1> | <step2> | …` line up front, then a single-line
332
+ > `PROGRESS: [<n>/<total>] <present-tense action>` before every relevant action (and `… done` on
333
+ > completion), spaced so no gap exceeds ~2 min, stdout unbuffered, plus exactly one final
334
+ > `RESULT: DONE|BLOCKED <reason>`.
335
+
336
+ ### Mini-handover
337
+
338
+ Repo: `ui-core-micha` (`C:\Users\biglmi\Documents\webapps\ui-core-micha`), branch `main`. WO:
339
+ `work-orders/THEME-2.md`. Follow `orchestrate-codex`.