@micha.bigler/ui-core-micha 3.6.0 → 3.8.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 (54) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/DESIGN.md +9 -1
  3. package/README.md +2 -0
  4. package/WORK_ORDERS.md +8 -0
  5. package/dist/auth/UserMenu.js +23 -1
  6. package/dist/components/EmptyState.js +17 -0
  7. package/dist/components/UserListComponent.js +2 -1
  8. package/dist/i18n/authTranslations.js +196 -0
  9. package/dist/i18n/uiCoreTranslations.js +19 -0
  10. package/dist/index.js +2 -1
  11. package/dist/messaging/Composer.js +4 -2
  12. package/dist/messaging/MessageBubble.js +2 -1
  13. package/dist/messaging/ReadTicks.js +5 -3
  14. package/dist/messaging/linkifyText.js +39 -0
  15. package/dist/pages/PasswordInvitePage.js +23 -8
  16. package/dist/pages/SignUpPage.js +4 -4
  17. package/dist/theme/createAppTheme.js +62 -9
  18. package/dist/theme/kitSxRegistry.js +5 -1
  19. package/dist/theme/tokens.js +1 -1
  20. package/package.json +1 -1
  21. package/src/auth/UserMenu.jsx +26 -1
  22. package/src/components/EmptyState.jsx +38 -0
  23. package/src/components/UserListComponent.jsx +2 -0
  24. package/src/i18n/authTranslations.ts +197 -0
  25. package/src/i18n/uiCoreTranslations.js +10 -0
  26. package/src/index.js +2 -1
  27. package/src/messaging/Composer.jsx +7 -3
  28. package/src/messaging/MessageBubble.jsx +2 -1
  29. package/src/messaging/ReadTicks.jsx +4 -3
  30. package/src/messaging/linkifyText.jsx +37 -0
  31. package/src/pages/PasswordInvitePage.jsx +36 -7
  32. package/src/pages/SignUpPage.jsx +79 -68
  33. package/src/theme/createAppTheme.js +68 -4
  34. package/src/theme/kitSxRegistry.js +9 -1
  35. package/src/theme/tokens.js +1 -1
  36. package/tests/EmptyState.test.jsx +62 -0
  37. package/tests/PasswordInvitePage.test.jsx +135 -0
  38. package/tests/SignUpPage.test.jsx +15 -2
  39. package/tests/UserListComponent.test.jsx +85 -0
  40. package/tests/UserMenu.test.jsx +40 -4
  41. package/tests/createAppThemePageContrast.test.js +108 -0
  42. package/tests/i18nGermanVariant.test.js +65 -0
  43. package/tests/messagingComposer.test.jsx +25 -0
  44. package/tests/messagingLinkify.test.jsx +51 -0
  45. package/tests/messagingReadTicksTooltip.test.jsx +36 -0
  46. package/tests/userScreenTranslations.test.jsx +165 -0
  47. package/work-orders/UCM-AUTH-9.md +207 -0
  48. package/work-orders/UCM-I18N-4.md +226 -0
  49. package/work-orders/UCM-MSG-20.md +192 -0
  50. package/work-orders/UCM-PRIM-2.md +114 -0
  51. package/work-orders/UCM-SHELL-7.md +134 -0
  52. package/work-orders/UCM-THEME-13.md +69 -0
  53. package/work-orders/UCM-THEME-14.md +142 -0
  54. package/work-orders/UCM-THEME-15.md +356 -0
@@ -0,0 +1,114 @@
1
+ # UCM-PRIM-2 — A shared empty-state primitive
2
+
3
+ # A. Envelope — authored by the Expertenchat
4
+
5
+ ## Goal & expected outcome
6
+
7
+ - Goal: every "nothing here yet" in a consuming app looks and reads the same.
8
+ - Expected outcome: `EmptyState` in the kit — an optional icon, one line of text, an optional secondary
9
+ line and an optional action — built from theme tokens, accessible (text reachable by screen
10
+ readers, action a real button).
11
+
12
+ ## Context — measured 2026-09-29 in jg-ferien's local browser test (ucm 3.5.0 installed there)
13
+
14
+ - jg-ferien's 15 event sections show "empty" in **seven** ways (plain text, card, blue info alert,
15
+ card with title, filled green card, alert inside a green card, nothing at all). No shared component
16
+ exists in jg or ucm; jg has only a song-search special case.
17
+ - **Operator decision 2026-09-29: generic goes to ucm.** jg adopts it in `JG-UX-9`.
18
+ - `PRIM-1` promoted `StatTile`/`SoftChip` along the same lines.
19
+
20
+ ## Scope + non-goals
21
+
22
+ - In scope: the component, its export, a story or docs entry, tests.
23
+ - Explicit non-goals / do-not-touch: no adoption in consumers here; no change to `Alert` styling.
24
+
25
+ ## Tier · gates
26
+
27
+ - **Tier 3** — new public export in shared-core (additive).
28
+ - Reviewer: `reviewer` (all lenses) · `ui_reviewer`.
29
+ - **Done means released; jg adoption is `JG-UX-9`.**
30
+
31
+ ## Risks
32
+
33
+ - A too-flexible API reintroduces the seven variants; keep the props minimal.
34
+
35
+ ## Required tests to WRITE (you write them and run YOUR OWN new ones; the orchestrator's run is the gate)
36
+
37
+ 1. Renders text only; with secondary line; with action (button with accessible name).
38
+ 2. Uses theme tokens (no literal colours; the kit's literal guard passes).
39
+
40
+ ---
41
+
42
+ # B. Implementation map — filled by the Orchestrator — ADDRESSED TO THE IMPLEMENTER
43
+
44
+ ## Context package
45
+
46
+ - New file `src/components/EmptyState.jsx`. Model it on the existing primitives
47
+ `src/components/StatTile.jsx` and `src/components/SoftChip.jsx` — a small functional component,
48
+ default export, built from `Box`/`Typography`/`Button` (MUI), theme tokens only (no literal
49
+ colours — `reportOffPaletteColours` in `src/theme/themeCompleteness.js` scans every source file
50
+ for off-palette hex/rgb/named colours and is part of the kit's literal guard the Required Tests
51
+ reference).
52
+ - Props (keep minimal per the Risk note — do not over-generalise):
53
+ `icon` (optional, a rendered MUI icon element, e.g. `<InboxOutlinedIcon />`), `title` (string,
54
+ required — the one line of text), `description` (optional secondary line), `action` (optional
55
+ `{ label, onClick }` — rendered as a real `<Button variant="outlined">`, not a link, so it is
56
+ reachable and operable like `StatTile`'s children slot), `sx` (optional passthrough, MUI
57
+ convention used throughout the kit, e.g. `Composer.jsx`'s `COMPOSER_EMOJI_BUTTON_SX` pattern).
58
+ - Export it from `src/index.js` next to the other components section (`--- 5. Components ---`,
59
+ after `SoftChip` at line 65): `export { default as EmptyState } from './components/EmptyState';`.
60
+ - Accessibility: `title` must be real text content (not just an `aria-label`), so a screen reader
61
+ reads it as part of document flow; if `icon` is passed, it must not need its own label (decorative
62
+ — rely on the adjacent text, matching how `StatTile` treats its icon-less design).
63
+ - Test file: `tests/EmptyState.test.jsx`, following `tests/StatTile.test.jsx`'s pattern exactly —
64
+ `@vitest-environment jsdom`, wrap in `<ThemeProvider theme={createAppTheme(...)}>`, `cleanup()`
65
+ in `afterEach`.
66
+ - Render text-only, with `description`, with `action` (assert the button has an accessible name
67
+ matching the label and that clicking it calls the passed `onClick`).
68
+ - The "theme tokens only" requirement: either call `reportOffPaletteColours([{ path: 'src/components/EmptyState.jsx', source: fs.readFileSync(...) }])` (see how `themeCompleteness.js` self-tests, or grep existing callers of `reportOffPaletteColours` in `tests/` for the established pattern) and assert `findings` is empty, or (simpler, sufficient) just don't use any hex/rgb/named-colour literal in the component at all and note in a comment that the guard is structural, not a runtime assertion in this test. Prefer calling `reportOffPaletteColours` directly if an existing test already does — grep `tests/*.test.*` for `reportOffPaletteColours` to find the precedent and match its call shape exactly.
69
+
70
+ ## Do-not-touch / invariants
71
+
72
+ - Do not add `EmptyState` to `kitSxRegistry.js` unless you export a top-level `sx` object constant
73
+ from the component (only needed if the root element gets kit-owned baked-in styling beyond plain
74
+ `sx` passthrough — for this minimal component you likely don't need one; only add a registry entry
75
+ if you do add such a constant).
76
+ - No adoption anywhere in this repo's own consumers (`AccountPage`, etc.) — out of scope per the
77
+ Envelope's non-goals.
78
+ - Do not touch `Alert` styling anywhere.
79
+
80
+ ## Target repo working directory (absolute)
81
+
82
+ `C:\Users\biglmi\Documents\webapps\ui-core-micha` (branch `main`)
83
+
84
+ ## Preamble — a REQUIRED block IN this file, not something appended at invocation
85
+
86
+ > The text above is the COMPLETE spec — the committed WO file's content, not a plan to refine; there
87
+ > is no separate plan file. Read the nearest `AGENTS.md`, the relevant `.codex/skills/<role>/SKILL.md`, and the
88
+ > app `MEMORY.md` ONLY for conventions. Stay in scope; do not touch auth/permissions/deps/schema/CI
89
+ > unless the spec says so; do not update `MEMORY.md`. **Do NOT edit `WORK_ORDERS.md` — the register
90
+ > row and the review verdicts are the orchestrator's alone.** **Your tools are for editing source
91
+ > and test files and for running the tests you wrote — nothing else.** Do NOT install dependencies,
92
+ > touch a lockfile, run a package manager, or tidy up stray files; if something in the repo state
93
+ > blocks you, stop and report it as `RESULT: BLOCKED <reason>` instead of fixing it. Do NOT
94
+ > `git add`/`commit`/`push` — leave every
95
+ > change uncommitted in the working tree for the orchestrator's independent review. WRITE the tests
96
+ > the `Required tests` section calls for AND **RUN the tests you just wrote** to confirm they execute
97
+ > and pass — that is the ONLY test run you do (NOT the app's affected/full suite, NOT any review).
98
+ > The orchestrator re-runs the authoritative set + does the independent review after you finish —
99
+ > those are the gate; your own run does not count as the gate.
100
+ >
101
+ > Narrate continuously: a `PLAN: <step1> | <step2> | …` line up front, then a single-line
102
+ > `PROGRESS: [<n>/<total>] <present-tense action>` before every relevant action (and `… done` on
103
+ > completion), spaced so no gap exceeds ~2 min, stdout unbuffered, plus exactly one final
104
+ > `RESULT: DONE|BLOCKED <reason>`.
105
+
106
+ ---
107
+
108
+ # C. Orchestrator only — NOT ADDRESSED TO THE IMPLEMENTER
109
+
110
+ > **If you are the implementer reading this work order as your own specification: STOP at this line.
111
+ > Everything below describes what the Orchestrator does AFTER you finish. You do none of it — no
112
+ > reviewers, no verification run, no register edit, no commit.**
113
+
114
+ *(to be filled by the Orchestrator: execution directive; `reviewer` (all lenses) + `ui_reviewer`; release; register row here and cross-reference in jg (`JG-UX-9`).)*
@@ -0,0 +1,134 @@
1
+ # UCM-SHELL-7 — User-menu avatar initials fail contrast
2
+
3
+ # A. Envelope — authored by the Expertenchat
4
+
5
+ ## Goal & expected outcome
6
+
7
+ - Goal: the initials in the header avatar are readable.
8
+ - Expected outcome: initials reach at least 4.5:1 against the avatar background, using the consuming
9
+ app's theme tokens rather than MUI's default grey.
10
+
11
+ ## Context — measured 2026-09-29 in jg-ferien's local browser test (ucm 3.5.0 installed there)
12
+
13
+ - `UserMenu` (imported in jg-ferien's `Header.jsx`) renders the MUI default avatar: white initials on
14
+ light grey, **1.83:1**, on every page of the app.
15
+
16
+ ## Scope + non-goals
17
+
18
+ - In scope: avatar colours in `UserMenu` from theme tokens (e.g. primary surface with its
19
+ contrastText), with a fallback that passes when a consumer defines nothing.
20
+ - Explicit non-goals / do-not-touch: no layout change of the user menu.
21
+
22
+ ## Tier · gates
23
+
24
+ - **Tier 3** — shared-core component (small).
25
+ - Reviewer: `ui_reviewer` (no new logic beyond the colour choice).
26
+ - **Done means released and jg-ferien's pin bumped.**
27
+
28
+ ## Risks
29
+
30
+ - `createAppTheme` baseline: if the chosen token is optional, `assertThemeComplete` must require it or
31
+ the fallback must pass on its own.
32
+
33
+ ## Required tests to WRITE (you write them and run YOUR OWN new ones; the orchestrator's run is the gate)
34
+
35
+ 1. With jg-ferien's theme values the avatar pair is at least 4.5:1.
36
+ 2. With a theme defining nothing extra the fallback pair is at least 4.5:1.
37
+
38
+ ---
39
+
40
+ # B. Implementation map — filled by the Orchestrator — ADDRESSED TO THE IMPLEMENTER
41
+
42
+ ## Context package
43
+
44
+ - File: `src/auth/UserMenu.jsx`. The avatar is rendered at line 60:
45
+ `<Avatar src={avatarSrc} sx={{ width: 32, height: 32, fontSize: '0.8rem' }}>` — no `bgcolor`/`color`
46
+ is set today, so it falls through to MUI's default grey Avatar styling (1.83:1, the measured
47
+ defect). Only this one JSX element changes; nothing else in the file.
48
+ - **Pitfall — do not just reach for `theme.palette.primary.contrastText`.** MUI's own
49
+ `createTheme`/`augmentColor` computes `contrastText` with `contrastThreshold: 3` (tuned for large
50
+ text/graphics, AA), not the 4.5:1 this WO requires for normal-size initials text. A consumer's
51
+ `primary.main` that legitimately passes MUI's own 3:1 threshold can still fail 4.5:1, and nothing
52
+ here re-validates it — so `contrastText` alone is not a fallback that "passes when a consumer
53
+ defines nothing", it is a fallback that USUALLY passes.
54
+ - **Design to implement instead** (uses the kit's own exported contrast helper, satisfies both
55
+ required tests meaningfully since both actually depend on the consumer's `primary.main`):
56
+ 1. Import `{ calculateContrastRatio }` from `'../theme'` (already exported from
57
+ `src/theme/index.js` → re-exported at `src/index.js`) and `useTheme` from
58
+ `'@mui/material/styles'`.
59
+ 2. Inside `UserMenu`, read `const theme = useTheme();`.
60
+ 3. Background: `theme.palette.primary.main` (the "primary surface" the Envelope names).
61
+ 4. Text colour: compute
62
+ `calculateContrastRatio(theme.palette.background.paper, theme.palette.primary.main) >= 4.5
63
+ ? theme.palette.background.paper : theme.palette.ink.primary`.
64
+ This is not an arbitrary heuristic: for any background colour, the higher of
65
+ (contrast against white) and (contrast against near-black) is mathematically always ≥ √21 ≈
66
+ 4.58 — i.e. picking whichever of the kit's own light (`background.paper`, white) or dark
67
+ (`ink.primary`, near-black) ink token has the higher measured contrast against
68
+ `primary.main` is GUARANTEED to clear 4.5:1 for every possible `primary.main`, including one
69
+ a consumer hasn't customised at all (the untouched baseline `primary.main` MUI ships). That is
70
+ what makes this "a fallback that passes when a consumer defines nothing" instead of "usually
71
+ passes".
72
+ 5. Apply as `sx={{ ..., bgcolor: theme.palette.primary.main, color: <computed> }}` on the same
73
+ `Avatar` element — keep the existing `width`/`height`/`fontSize` untouched.
74
+ 6. Do NOT introduce a new literal hex/rgb anywhere — `theme.palette.background.paper` and
75
+ `theme.palette.ink.primary` are both resolved theme values, and referencing them via
76
+ `theme.palette.<path>` (not writing their hex value as a string literal) keeps
77
+ `reportOffPaletteColours` clean, same as every other component in the kit.
78
+ - Test file: new `tests/UserMenu.test.jsx` (none exists yet — check with `Glob` before assuming;
79
+ if a `UserMenu` test already exists under a different name, extend it instead of creating a
80
+ duplicate). Pattern after `tests/StatTile.test.jsx`: `@vitest-environment jsdom`,
81
+ `createAppTheme(...)`, wrap `UserMenu` in the theme provider AND `AuthContext.Provider` (it reads
82
+ `user`/`logout` via `useContext(AuthContext)` at line 32 — a minimal `{ user: { username: 'ab' },
83
+ logout: () => {} }` value is enough to render the avatar).
84
+ 1. With a theme built from jg-ferien's actual `primary.main` — `#367964` (measured directly from
85
+ `jg-ferien/frontend/src/theme.js:25`, a mid-brightness teal-green), assert
86
+ `calculateContrastRatio(<resolved avatar text colour>, <resolved avatar bg colour>) >= 4.5`
87
+ by reading the rendered `Avatar`'s computed `backgroundColor`/`color` via
88
+ `window.getComputedStyle` (see `tests/StatTile.test.jsx`'s `accent` border test for the
89
+ `getComputedStyle` pattern) and feeding both into `calculateContrastRatio` directly — do not
90
+ hardcode an expected hex, assert the ratio.
91
+ 2. With `createAppTheme({})` (no consumer overrides — MUI's own baseline `primary.main`), assert
92
+ the same ≥4.5 computed-style contrast check.
93
+
94
+ ## Do-not-touch / invariants
95
+
96
+ - No layout change to the user menu (Envelope non-goal) — only the two `sx` colour values change.
97
+ - Do not touch `assertThemeComplete`/`THEME_COMPLETENESS_SURFACES` — this fix reads existing
98
+ required surfaces (`primary.main`, `background.paper`, `ink.primary`), it does not need a new one.
99
+
100
+ ## Target repo working directory (absolute)
101
+
102
+ `C:\Users\biglmi\Documents\webapps\ui-core-micha` (branch `main`)
103
+
104
+ ## Preamble — a REQUIRED block IN this file, not something appended at invocation
105
+
106
+ > The text above is the COMPLETE spec — the committed WO file's content, not a plan to refine; there
107
+ > is no separate plan file. Read the nearest `AGENTS.md`, the relevant `.codex/skills/<role>/SKILL.md`, and the
108
+ > app `MEMORY.md` ONLY for conventions. Stay in scope; do not touch auth/permissions/deps/schema/CI
109
+ > unless the spec says so; do not update `MEMORY.md`. **Do NOT edit `WORK_ORDERS.md` — the register
110
+ > row and the review verdicts are the orchestrator's alone.** **Your tools are for editing source
111
+ > and test files and for running the tests you wrote — nothing else.** Do NOT install dependencies,
112
+ > touch a lockfile, run a package manager, or tidy up stray files; if something in the repo state
113
+ > blocks you, stop and report it as `RESULT: BLOCKED <reason>` instead of fixing it. Do NOT
114
+ > `git add`/`commit`/`push` — leave every
115
+ > change uncommitted in the working tree for the orchestrator's independent review. WRITE the tests
116
+ > the `Required tests` section calls for AND **RUN the tests you just wrote** to confirm they execute
117
+ > and pass — that is the ONLY test run you do (NOT the app's affected/full suite, NOT any review).
118
+ > The orchestrator re-runs the authoritative set + does the independent review after you finish —
119
+ > those are the gate; your own run does not count as the gate.
120
+ >
121
+ > Narrate continuously: a `PLAN: <step1> | <step2> | …` line up front, then a single-line
122
+ > `PROGRESS: [<n>/<total>] <present-tense action>` before every relevant action (and `… done` on
123
+ > completion), spaced so no gap exceeds ~2 min, stdout unbuffered, plus exactly one final
124
+ > `RESULT: DONE|BLOCKED <reason>`.
125
+
126
+ ---
127
+
128
+ # C. Orchestrator only — NOT ADDRESSED TO THE IMPLEMENTER
129
+
130
+ > **If you are the implementer reading this work order as your own specification: STOP at this line.
131
+ > Everything below describes what the Orchestrator does AFTER you finish. You do none of it — no
132
+ > reviewers, no verification run, no register edit, no commit.**
133
+
134
+ *(to be filled by the Orchestrator: execution directive; `ui_reviewer`; release; jg-ferien pin bump; register rows here and in jg (`JG-DL-12`).)*
@@ -0,0 +1,69 @@
1
+ # UCM-THEME-13: Freeze a dark-mode baseline token set
2
+
3
+ # A. Envelope, authored by the Expertenchat
4
+
5
+ ## Goal & expected outcome
6
+
7
+ The central token file `src/theme/tokens.js` has one light baseline palette and no dark one. This order
8
+ produces the **decided** dark token set as a committed reference sheet, `docs/DARK-TOKENS.md`. The
9
+ operator freezes it before `UCM-THEME-14` puts those values into code. The output is a document. No
10
+ source file changes here.
11
+
12
+ Operator decision (2026-09-29): dark mode gets its own work order in the kit, rather than one app keeping
13
+ a raw `createTheme` for it.
14
+
15
+ ## Context the operator established
16
+
17
+ - **One consumer today: `webshop-guenter`.** It has a light/dark switch (`frontend/src/App.jsx:96`)
18
+ and a hand-built `darkTheme` from its operator design handoff (background `#0E0B08`, paper
19
+ `#171209`, primary `#D49040`, secondary `#A88868`). Its adoption, `WSG-THEME-1`, is blocked on this
20
+ order and on `UCM-THEME-14`. No other app in the estate renders a dark theme (measured 2026-09-29).
21
+ - The light baseline covers: the palette (primary is app identity; secondary, the status family
22
+ `success/warning/error/info/stale` with `main/light/dark/contrastText` plus `text/fill/bg`,
23
+ `background.default/subtle/paper`, the text inks, `divider`, `controlBorder`), `dataSeries` (six
24
+ categorical colours) and `shadow.rest/overlay`. Typography, radii, spacing, density and motion do not
25
+ depend on the mode.
26
+
27
+ ## Scope + non-goals
28
+
29
+ In scope: `docs/DARK-TOKENS.md` with a value for every **mode-dependent** light token class listed
30
+ above, each with a one-line reason, and a contrast table: every text/fill pair and every
31
+ `main`-on-surface pair at 3:1 or better against the dark `background.default` and `background.paper`.
32
+ Follow the `design-language` skill's process: inventory first, then a structural round, then values
33
+ decided in an instrument, then the freeze.
34
+
35
+ Non-goals:
36
+ - No change to any light value.
37
+ - No code, no tests. `UCM-THEME-14` implements.
38
+ - No per-app dark identity. An app's dark primary stays that app's own, just as its light one does.
39
+ - No automatic switching (`prefers-color-scheme`) and no toggle component. Apps own their switch.
40
+
41
+ ## Tier · precondition / gate
42
+
43
+ - **Tier 2 · tests: none (document only).** It decides the future content of shared-core, but it does
44
+ not change shared-core. The code change is `UCM-THEME-14`, which is Tier 3.
45
+ - Precondition: none.
46
+ - Gate: **the operator freezes the sheet**, and the sheet itself says so with a dated section. Until
47
+ then `UCM-THEME-14` does not start.
48
+
49
+ ## Risks
50
+
51
+ - **A dark palette drawn from a single app's handoff becomes that app's palette for everyone.**
52
+ webshop-guenter's values are one input to the inventory, not the answer. It keeps its own values as
53
+ identity in any case (`WSG-THEME-1`).
54
+ - **Status tints (`bg`) are the class that breaks first on dark.** A light tint over a dark page fails
55
+ contrast in the other direction.
56
+
57
+ ---
58
+
59
+ # B. Implementation map
60
+
61
+ *Not applicable. This is an Expertenchat document order, authored in-session with the operator.*
62
+
63
+ ---
64
+
65
+ # C. Orchestrator only, NOT ADDRESSED TO THE IMPLEMENTER
66
+
67
+ > **If you are the implementer reading this: STOP.** There is no implementer for this order.
68
+
69
+ Register: row -> `done` with the commit of the frozen sheet. Downstream: `UCM-THEME-14` unblocks.
@@ -0,0 +1,142 @@
1
+ # UCM-THEME-14: Dark mode in `createAppTheme`, from the central token file
2
+
3
+ # A. Envelope, authored by the Expertenchat
4
+
5
+ ## Goal & expected outcome
6
+
7
+ `createAppTheme({ palette: { mode: 'dark', primary }, ... })` builds a complete dark theme whose
8
+ mode-dependent values come from a dark baseline palette in `src/theme/tokens.js`, next to the light
9
+ one. `assertThemeComplete` judges a dark theme against its own surfaces. **The light output does not
10
+ change by a single value.**
11
+
12
+ Afterwards an app with a light/dark switch (today only `webshop-guenter`, `WSG-THEME-1`) can compose
13
+ both of its themes through the factory.
14
+
15
+ ## Value source
16
+
17
+ **`docs/DARK-TOKENS.md`, frozen by the operator in `UCM-THEME-13`.** Every dark value in code comes
18
+ from that sheet. A value the implementation needs that the sheet does not carry is a stop-and-report.
19
+ Do not improvise it.
20
+
21
+ ## Context the operator established
22
+
23
+ - `createAppTheme` (`src/theme/createAppTheme.js`) resolves the palette in three layers: the app's
24
+ palette, then `BASELINE_PALETTE`, then the app's palette again. It then derives
25
+ `controlBorder.focus`, builds the palette-aware component slots, and merges `BASELINE_STATIC`, those
26
+ components and the app config. `palette.mode` is not read anywhere today.
27
+ - **Two places assume a white page:**
28
+ - `deriveFocusColour` checks 3:1 against `'#FFFFFF'` and `background.default`, and only ever
29
+ **darkens** the primary. On a dark page it would have to lighten.
30
+ - `contrastFindings` in `src/theme/themeCompleteness.js` checks against a fixed
31
+ `white: '#FFFFFF'` (line 298) plus `page: background.default`.
32
+ - `BASELINE_INTENTIONAL_DEFAULT_EXEMPTIONS` includes `palette.background.paper` with the reason "The
33
+ canonical surface is deliberately MUI white". That reason is false for a dark theme.
34
+ - The palette-aware slots use palette values (for example the autofill inset uses `background.paper`),
35
+ so they follow the resolved palette once it is right.
36
+ - The kit has components registered against the baseline (`src/theme/kitSxRegistry.js`, `THEME-4`/
37
+ `THEME-5`). A kit component that hard-codes a light value would show that value only in dark mode.
38
+
39
+ ## Scope + non-goals
40
+
41
+ In scope:
42
+ 1. `tokens.js`: a dark baseline palette with the sheet's values, and whatever exemption entries a
43
+ dark theme needs, each with a true reason.
44
+ 2. `createAppTheme`: select the baseline palette by `palette.mode`. Make the focus derivation work in
45
+ both directions against the mode's surfaces.
46
+ 3. `themeCompleteness.js`: contrast checks against the theme's own surfaces, not a fixed white. Mode-
47
+ specific exemptions.
48
+ 4. Kit components that resolve a hard-coded light value instead of a token: **list** every one.
49
+ Switching one to an existing token is in scope. Anything that needs a new token is a finding.
50
+ 5. `DESIGN.md` (a dark-mode section), `CHANGELOG.md`, and a **minor** version bump. The change is
51
+ additive: light output is unchanged, and dark is new.
52
+
53
+ Non-goals:
54
+ - No change to any light value or light behaviour.
55
+ - No automatic `prefers-color-scheme` switching and no toggle component.
56
+ - No consumer adoption. `WSG-THEME-1` does that in its own repo, after its own pin bump.
57
+ - No change to chart components beyond reading palette tokens they already read.
58
+
59
+ ## Tier · precondition / gate
60
+
61
+ - **Tier 3 · tests: light-output regression guard, dark completeness, focus contrast in both modes,
62
+ and the kit's existing theme tests.** A change inside shared-core (`AGENTS.md` -> Tiering).
63
+ - Precondition: **`UCM-THEME-13` frozen**, and **`UCM-THEME-15` landed**. 15 generalises the focus
64
+ derivation into page-derived status and border colours on the same surface pair; build the dark
65
+ branch on top of it, not beside it.
66
+ - Downstream: `webshop-guenter`/`WSG-THEME-1` waits for this release and for its own pin bump.
67
+
68
+ ## Risks
69
+
70
+ - **Ten adopted apps consume the light path.** A light regression reaches all of them on their next
71
+ bump. That is why the regression guard is a required test, not a nice-to-have.
72
+ - **The three-layer palette merge interacts with augmentation** (the comment at the top of `tokens.js`:
73
+ only the first `createTheme` argument is augmented). A dark status entry missing
74
+ `main`/`contrastText` would silently keep MUI's light hue.
75
+ - **Contrast in the other direction.** Light status tints (`bg`) over a dark page, and a focus colour
76
+ derived by darkening, both fail on dark.
77
+ - **The `background.paper` exemption reason is currently mode-blind.** Leaving it would record a false
78
+ statement as the reason in every dark theme.
79
+
80
+ ## Required tests to WRITE (you write them and run YOUR OWN new ones; the Orchestrator's run is the gate)
81
+
82
+ Kit test suite, narrow, next to the existing theme tests:
83
+
84
+ 1. **Light regression guard.** For a fixed light app config, the fully resolved theme (palette,
85
+ typography, shape, components, exemptions) equals the output from before this change. Show that the
86
+ test fails when one light value is altered by hand. A structural test that cannot fail is
87
+ decoration.
88
+ 2. **Dark completeness.** `assertThemeComplete(createAppTheme({ palette: { mode: 'dark', primary } }))`
89
+ has zero findings, or only exemptions with a true reason. The same check with a primary that has to be
90
+ **lightened** to clear contrast.
91
+ 3. **Focus contrast.** `controlBorder.focus` clears 3:1 against both page surfaces in each mode.
92
+ 4. **Status family on dark.** Each status key resolves `main/light/dark/contrastText` from the dark
93
+ baseline, not MUI's defaults.
94
+
95
+ Run, do not write: the kit's existing theme and `kitSxRegistry` tests.
96
+
97
+ ## Parity guardrail
98
+
99
+ No light-mode change of any kind. No behaviour, API or data-contract change beyond the new `mode`
100
+ branch.
101
+
102
+ ---
103
+
104
+ # B. Implementation map, filled by the Orchestrator and ADDRESSED TO THE IMPLEMENTER
105
+
106
+ *Placeholder. The Orchestrator fills the context package, the absolute working directory, the
107
+ progress contract and the preamble block on `git pull`, per `AGENTS.md` -> "Work Order". Do not
108
+ dispatch while this placeholder stands, and not before `UCM-THEME-13` is frozen.*
109
+
110
+ ---
111
+
112
+ # C. Orchestrator only, NOT ADDRESSED TO THE IMPLEMENTER
113
+
114
+ > **If you are the implementer reading this work order as your own specification: STOP at this line.**
115
+ > Everything below describes what the Orchestrator does AFTER you finish. You do none of it: no
116
+ > reviewers, no verification run, no register edit, no `git add`/`commit`/`push`.
117
+
118
+ ### Execution directive
119
+
120
+ Check `.claude/codex-status.md` first (newest-first; read it with `head`). No line for today means use
121
+ Codex.
122
+
123
+ ### Review routing
124
+
125
+ Tier 3: independent `reviewer` (all configured lenses) and `ui_reviewer`, concurrent, one batch,
126
+ before the commit. No `sec_reviewer`.
127
+
128
+ ### Verification
129
+
130
+ Render a kit demo surface in both modes at 375 px and 1280 px (`dev/`), and name the screenshots in
131
+ the register Notiz. No prototype is in scope.
132
+
133
+ ### Register + commit + release
134
+
135
+ Row -> `done` with the review Notiz in the `AGENTS.md` shape. The publish runs on push to `main` when
136
+ `package.json`/`src/**` change (`.github/workflows/publish.yml`). Then add a planned row in
137
+ `webshop-guenter/WORK_ORDERS.md` for the pin bump to this version, and unblock `WSG-THEME-1`.
138
+
139
+ ### Mini-handover
140
+
141
+ `Orchestrator: implement work-orders/UCM-THEME-14.md in ui-core-micha (main). git pull first, confirm
142
+ UCM-THEME-13 is frozen, read the WO, then follow orchestrate-codex.`