@micha.bigler/ui-core-micha 2.37.1 → 2.39.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.
- package/README.md +4 -0
- package/WORK_ORDERS.md +6 -4
- package/dist/components/SoftChip.js +69 -0
- package/dist/components/StatTile.js +20 -0
- package/dist/index.js +3 -1
- package/dist/theme/index.js +1 -1
- package/dist/theme/kitSxRegistry.js +7 -0
- package/dist/theme/themeCompleteness.js +202 -0
- package/package.json +1 -1
- package/src/components/SoftChip.jsx +93 -0
- package/src/components/StatTile.jsx +39 -0
- package/src/index.js +3 -0
- package/src/theme/index.js +1 -0
- package/src/theme/kitSxRegistry.js +11 -0
- package/src/theme/themeCompleteness.js +201 -0
- package/tests/SoftChip.test.jsx +105 -0
- package/tests/StatTile.test.jsx +57 -0
- package/tests/themeCompleteness.test.js +104 -0
- package/work-orders/PRIM-1.md +177 -0
- package/work-orders/THEME-6.md +247 -0
- package/work-orders/assets/PRIM-1-dashboard-primitives.html +624 -0
package/README.md
CHANGED
|
@@ -9,3 +9,7 @@ To add an entry, create a small component in `dev/entries.jsx` and append it to
|
|
|
9
9
|
## Internationalization
|
|
10
10
|
|
|
11
11
|
Spread `uiCoreTranslations` into the default i18n namespace as the supported aggregate; per-feature translation exports remain available to selective apps that accept responsibility for pairing every adopted feature with its bundle.
|
|
12
|
+
|
|
13
|
+
## Dashboard primitives
|
|
14
|
+
|
|
15
|
+
`StatTile` (a bordered KPI tile) and `SoftChip` (a soft tinted annotation/status chip, tones derived from `theme.palette[tone]`) live under `src/components/` and are exported from the package root.
|
package/WORK_ORDERS.md
CHANGED
|
@@ -17,6 +17,7 @@ rules are defined centrally in `webapps/AGENTS.md` → "Work-Order Register".
|
|
|
17
17
|
| `DX-*` | Developer experience / repo tooling (ucm-local prefix; unrelated to `django-core-micha`'s own `DX-*`) |
|
|
18
18
|
| `AUTH-*` | Auth components UX (login/password/MFA/profile forms, auth error i18n, and the account screen's own surfaces) |
|
|
19
19
|
| `I18N-*` | The shared kit's i18n **contract** — how a consumer registers ucm's strings, and what happens when it forgets. Distinct from `AUTH-*`, which owns the auth strings themselves |
|
|
20
|
+
| `PRIM-*` | Shared dashboard/data primitives — small presentational components that are neither chart (`CHART-*`) nor app chrome (`SHELL-*`) |
|
|
20
21
|
| `SHELL-*` | Shared app chrome and navigation shells (`UserMenu`, mobile bottom nav, secondary-navigation shell) — used since `SHELL-1`, documented here 2026-08-11 |
|
|
21
22
|
|
|
22
23
|
Introduce a new prefix when none fits and add it here. New WOs always get a
|
|
@@ -26,6 +27,7 @@ prefixed ID; never reuse a bare flat number across workstreams.
|
|
|
26
27
|
|
|
27
28
|
| ID | Titel | Beschreibung | Datum | Status | Commit(s) | Notiz |
|
|
28
29
|
|---|---|---|---|---|---|---|
|
|
30
|
+
| PRIM-1 | Promote `StatTile` and `SoftChip` into the shared kit | Both already exist **twice**, independently built, and each second build is an **inline component inside a page** rather than a file — `StatTile` in `fitness-monitor/frontend/src/pages/BodyHistoryPage.jsx:554-568` beside hram's `ResearchComponents/StatTile.jsx`, `SoftChip` in `cockpit/frontend/src/cockpit/BoardView.jsx:65-73` (the lane pill) beside hram's `SoftChip.jsx`. The promotion trigger is met on evidence, not waived. The two builds diverged in seven places; all seven decided by the operator 2026-08-12 and rendered in the prototype. Full order [`work-orders/PRIM-1.md`](work-orders/PRIM-1.md) | 2026-08-12 | done | b5fff23 | Tier 3 (shared-core). `reviewer` + `ui_reviewer` mandatory and concurrent; no `sec_reviewer`. **Prototype in scope** — [`work-orders/assets/PRIM-1-dashboard-primitives.html`](work-orders/assets/PRIM-1-dashboard-primitives.html), so the rendered 375/1280 side-by-side is a commit gate; screenshots have not been legibly obtainable here, so a substitution must be **declared**, not silent. Decisions: value `h5` 18/600 · `tabular-nums` always · `flex: 1` with a `minWidth` floor · `accent` kept (fitness-monitor cannot adopt without it) · responsive padding · `status` uses **`overline`** (cockpit independently chose uppercase + .4px tracking, so this satisfies `THEME-4` with **no** baseline change and only hram's chips shift to uppercase) · dot-or-icon and optional border via props. **Token delta list has one entry:** `caveat`'s 12/500 has no baseline variant (`caption` is 12/400) — operator resolved it as keep-the-value, so it must be **exempted in `THEME-4` with that reason stated**, not flattened to 400 and not fixed by extending the baseline here. Neither `StatTile` build has a test; the promoted tests are written from scratch. Minor bump `2.38.0` → `2.39.0` (new capability area). **The three app adoptions are separate follow-up WOs** and land no app change here. Programme strand `DS-5`. **Codex skipped, dated evidence**: `.claude/codex-status.md` records `unavailable` for 2026-08-12 (`cockpit/UI-10`, out of workspace credits, the day's one allowed probe already spent) — Orchestrator implemented directly, flipping authorship. **Independent `reviewer` (Haiku, mandatory regardless of tier per the WO): no findings.** **`ui_reviewer` (Haiku, mandatory, concurrent): two P1 findings, both fixed** — (1) `StatTile`'s padding had only `px` responsive (`{xs:1, sm:1.5}`) with `py` fixed at 1, against the decided "responsive `{xs:1, sm:1.5}`" (matching fitness-monitor's original `p` shorthand, not hram's split values) — changed to a single `p: {xs:1, sm:1.5}`; (2) the `status` variant's typography was under-tested (only `textTransform` checked) — added explicit `fontSize`/`fontWeight`/`letterSpacing` assertions against `theme.typography.overline`, and separately against the `caveat` variant's raw 12/500, both now green. Re-verified 51/51 scoped tests green after fixes, `tsc -p tsconfig.build.json --noEmit` clean. **Rendered gate: declared substitution**, per the WO's own text — the 375/1280 side-by-side screenshot capture is not legibly obtainable in this repo; computed-style/DOM inspection substituted instead, via the test suite itself (fill/border/text colour per tone verified against `alpha`/`darken` of `theme.palette[tone]`; `borderRadius` 999px status vs non-pill caveat; `fontVariantNumeric: tabular-nums`; accent border width 1px vs 2px; `overline` typography values now explicit). No visual click-through was performed. Commit `b5fff23`. |
|
|
29
31
|
| THEME-3 | Add the missing "subtle surface" baseline token | Adds `palette.background.subtle` (`#F4F5F6`, fixed baseline constant alongside `default`/`paper`) to `createAppTheme` and registers it in `THEME_COMPLETENESS_SURFACES`. Found by `cockpit/UI-6`: the `UI-4` design-language sheet's canonical token table names this as a baseline-supplied "inset surface" replacing `cockpit.surface2` (16 of 195 token reads in that WO), but the value was never actually wired into the shipped package — confirmed by grep, not assumed. `cockpit/UI-6` is blocked on this landing + publishing first, per this estate's own "gap in the shared kit is a ucm WO, not a local workaround" rule. Full spec: [work-orders/THEME-3.md](work-orders/THEME-3.md). `2.33.2` (patch). | 2026-08-11 | done | 1b25117 | Tier 3 — shared-core, this repo publishes to npm on every `main` push touching `src/**`, no staging net. Codex implemented in one pass (stdin invocation), exit 0, `RESULT: DONE`, 17/17 of its own new tests green. **Independent `reviewer` (Sonnet) + `ui_reviewer` (Sonnet) ran concurrently — both clean, no findings.** Both independently re-derived the safety mechanism rather than trusting the WO's claim: `MUI_DEFAULT_THEME = createTheme()` never defines `background.subtle` (vanilla MUI only has `.default`/`.paper`), so `assertThemeComplete`'s `sameValue` check can never flag the new surface for any real adopter — the same unexempted treatment `background.default` already gets; `background.paper`'s exemption (needed because MUI's own default theme is also white) was correctly not copied. Both read `fitness-monitor`'s actual `theme.js`/`theme.test.js` directly (the one real production adopter, hard-asserting `assertThemeComplete(theme).findings` equals `[]`) and confirmed this diff cannot break it. `ui_reviewer` additionally grepped every sibling app repo under `webapps/` for `createAppTheme` — no other live consumer exists; cockpit's own adoption (`UI-6`) isn't wired yet. `reviewer` independently recomputed the contrast ratio in a throwaway script and got the same `5.3748:1` the diff claims (not the design sheet's `4.81:1`, which was computed on the sheet's own static canvas — a source discrepancy, not a functional risk; both clear AA). **Orchestrator's own verification:** built the package (`npm run build`) and attempted to run `fitness-monitor`'s real assertions against the fresh `dist/` via raw Node ESM import — hit a pre-existing (confirmed via `git show`, not introduced here) `tsc`-emitted extensionless-import quirk that only affects raw `node`, not the Vite/Vitest resolution every real consumer (incl. fitness-monitor's own suite) uses; fell back to the scoped vitest run instead. Tests: 17/17 green (`tests/createAppTheme.test.js` + `tests/themeCompleteness.test.js`), run by the Orchestrator. No visual/prototype gate — token-only change with no rendered specimen in this repo (per the Envelope); the rendered check happens downstream in `cockpit/UI-6`. Semver: patch, applied by the Orchestrator per the WO's own reasoning (additive, resolves automatically, no adopter code change needed) — confirmed rather than assumed by both reviews. Unblocks `cockpit/UI-6`, which was waiting on this landing + publish. |
|
|
30
32
|
| CHART-7 | `ChartFrame` gains a `titleVariant` prop | Adds `titleVariant` (default `'h6'`, unchanged from today's hardcoded value) to `ChartFrame`, forwarded to the title `Typography` — lets a dense panel grid (e.g. fitness-monitor's seven body-composition panels beside one hero chart) render smaller titles that recede instead of competing with the one chart that matters. One prop, not a `slotProps.title` system — deliberately minimal per the WO. Precondition of fitness-monitor's `FM-12` (DS-10). `2.32.0` (minor, additive). Full spec: [work-orders/CHART-7.md](work-orders/CHART-7.md). | 2026-08-10 | done | 14020d0 | Envelope authored by Expertenchat, no design question (short-form WO). No preconditions. Orchestrator filled a minimal Implementation map (the whole change is two lines in `ChartFrame.jsx`), ran Codex (stdin invocation). Codex added `titleVariant = 'h6'` to the prop destructure, forwarded it to the title `Typography`, wrote all 3 required tests, and bumped `2.31.1` → `2.32.0` per the WO's own stated version target. **Independent `reviewer` + `ui_reviewer` (both Haiku, proportionate to a small low-risk diff) ran concurrently — both found NO findings**, including explicit confirmation that only the prop-destructure line and the one `Typography` line changed (padding, foot row, subtitle/toolbar/slots byte-for-byte unchanged) and that the default-regression test is non-vacuous. Re-verified: 13/13 scoped vitest tests green, `tsc -p tsconfig.build.json --noEmit` clean. **Two-width rendered side-by-side (DS-1 gate)**: no existing harness specimen used `titleVariant`, so the Orchestrator added one (two `ChartFrame`s side by side, default vs. `titleVariant="subtitle2"`) to `dev/entries.jsx` — confirmed live at 375px and 1280px that the override renders visibly smaller/lighter than the unchanged h6 default. |
|
|
31
33
|
| THEME-2 | Two defects from the THEME-1/CHART-6 landings: `warning.main` contrast, `grid` replacing instead of merging | Fixes `palette.warning.main` (`#C08A2C`, 3.038:1 on white — fails AA) to `#976100` (the existing `text` tone, ~5.2:1), `contrastText` to `#FFFFFF`; `warning.fill`/`.text`/`.fillText`/`.bg` unchanged. Adds a completeness-assertion rule checking every status channel's `.main` against `#FFFFFF` and `background.default` at 4.5:1. Fixes `BarChart`/`LineChart`'s `grid` prop silently replacing (not merging) a caller's partial grid object; the required audit of sibling object-valued defaults found a second live instance (`legendPosition`, same bug class) and fixed it too. `2.31.1` (patch). Full spec: [work-orders/THEME-2.md](work-orders/THEME-2.md). | 2026-08-10 | done | 7d5039f | Envelope authored by Expertenchat, both defects fully diagnosed up front (no design question). Preconditions (THEME-1 `2.30.0`, CHART-6 `2.31.0`) verified. Orchestrator filled the Implementation map with the exact current-state code pointers for both defects, then ran Codex (stdin invocation). Codex's own audit (per the WO's "close the mechanism" instruction) found the `legendPosition` partial-override bug beyond the named `grid` fix and fixed it too, exactly as the WO anticipated ("fix that too and say so"). **Independent `reviewer` (Sonnet) + `ui_reviewer` (Haiku) ran concurrently — both found NO findings**, including the reviewer's own hand-computed contrast check confirming `#976100` clears 4.5:1 against both `#FFFFFF` (~5.22:1) and `background.default` (~5.00:1). One thing the Orchestrator itself fixed (not a reviewer finding): a pre-existing THEME-1 regression test (`tests/createAppTheme.test.js`) asserted `palette[status].main === palette[status].fill` for all four status channels — THEME-2 deliberately breaks that equality for `warning` only (the whole point of the fix), so the assertion was updated to special-case `warning` against its new intended invariant (`main === text`, `contrastText === '#FFFFFF'`) while `success`/`error`/`info` keep the original assertion. Re-verified after the fix: 58/58 scoped + adjacent vitest tests green (`tests/themeCompleteness.test.js`, `tests/chartDefaults.test.js`, `tests/createAppTheme.test.js`, `tests/chartsPalette.test.js`, `tests/BarChart.test.jsx`, `tests/LineChart.test.jsx`, `tests/TimeSeriesChart.test.jsx`), `tsc -p tsconfig.build.json --noEmit` clean. **Two-width rendered side-by-side (DS-1 gate)** run in the dev harness's "Theme / Shared baseline" entry — the WO's own point was that no existing specimen exercised `.main` (the `StatusChip`'s `sx={{bgcolor:'...fill'}}` and the `MuiAlert` `standardWarning` override both bypass it), so a new Button/Chip/`Alert variant="filled"` specimen row was added and confirmed live at 375px and 1280px: the warning tone now renders visibly darker (brown, white text) than the still-unchanged lighter-amber `fill`-based chip beside it. The "Caller overrides" `LineChart` specimen's `grid` prop was also changed from a complete `{horizontal:true,vertical:true}` object (which wouldn't have visually proven the merge) to a genuinely partial `{vertical:true}` one — confirmed live at both widths showing BOTH grid directions post-fix. |
|
|
@@ -65,7 +67,7 @@ prefixed ID; never reuse a bare flat number across workstreams.
|
|
|
65
67
|
| SHELL-3 | Promote the mobile bottom navigation into the shared kit | Promotes jg-ferien's `MobileBottomNav` into ucm, shaped from **two** real cases (jg's existing bar + cockpit's destinations) as DS-6 required rather than from jg alone. Two labelled parts, one release: (1) `MuiBottomNavigation`/`MuiBottomNavigationAction` defaults into the baseline plus their completeness-assertion surfaces; (2) the component itself, structure and behaviour only, with `destinations`/`activeRoute`/`onNavigate`/`hideAbove`/`zIndex` props. Each of jg's four recorded promotion blockers becomes a declarative field (`emphasis`, `badgeCount`, finished-string `label`, no jg palette token), and cockpit is what proves each optional. Full spec: [work-orders/SHELL-3.md](work-orders/SHELL-3.md). | 2026-08-11 | done | 56de0c0, 586ac9c | Implemented `MobileBottomNav`, the additive baseline/completeness surfaces, public export, `2.33.0` version bump, and a five-item/two-badge harness specimen. **Governance note: Codex self-committed (`56de0c0`) and self-marked this row "done" citing its own reviewer/ui_reviewer run — that self-review does not count (author ≠ reviewer) and the row was incorrectly closed before independent review actually ran.** Corrected here: the Orchestrator independently re-ran the scoped tests (22/22 green) and `tsc` (clean), then spawned genuinely independent `reviewer` (Sonnet) + `ui_reviewer` (Sonnet) concurrently against the real diff. **`ui_reviewer` found a real defect (P2):** the component duplicated `borderTop`/`borderColor` inline in `sx` on top of the same values this WO registers in the theme (`tokens.js`/`createAppTheme.js`) — inline `sx` wins over `styleOverrides`, so the theme registration was decorative and any future theme customization of the bar's border would be silently ignored. Fixed by removing the inline duplication from `src/components/MobileBottomNav.jsx`; re-verified 22/22 green, `tsc` clean. **`reviewer` found two non-blocking test-coverage gaps (P2/P3, left as documented, not fixed):** the required "theme-agnosticism" test (item 5b) doesn't actually prove the new `createPaletteAwareComponents` additions do anything, because MUI's own built-in `BottomNavigation`/`BottomNavigationAction` defaults already resolve the same palette paths (`background.paper`/`text.secondary`/`primary.main`) independently of this WO's additions — verified by reading MUI's source; the explicit registration is still correct/required for completeness-assertion purposes, just not distinctively tested by rendering. The `emphasis` treatment's computed colours (raised icon, `background.paper` vs jg's `background.white`) have no dedicated assertion. **Browser verification:** a real Browser-pane render was attempted this session (`ui-core-micha-dev`, port 5199) but the pane did not composite frames (not displayed client-side) — reverted to the same DOM/computed-style substitution Codex used, now corroborated by both independent reviewers reading the rendered defaults against the prototype (`cockpit/work-orders/assets/UI-3-nav-redesign.html` sections 3-4: 22px icons/2px gap/2px padding/12px-500 label confirmed matching). The `env(safe-area-inset-bottom)` CSS declaration is asserted; a non-zero safe-area inset remains unverified (declared, not hidden, per the WO). Precondition of `cockpit/UI-3` and `jg-ferien/NAV-36`. |
|
|
66
68
|
| SHELL-4 | Give each bottom-nav property exactly one home | `SHELL-3` shipped all visible styling in the theme layer and only position/`zIndex`/safe-area in the component, so a consumer that has NOT adopted `createAppTheme` gets a structurally broken bar. Moves the four properties the component needs on ANY theme into the component as its single home — `minWidth: 0`, `maxWidth: 'none'`, `borderTop: '1px solid'`, `borderColor: 'divider'` as an sx token reference — deletes them from `tokens.js`/`createAppTheme.js`, deregisters their four assertion surfaces (twenty to sixteen), and adds an `sx` prop as the escape hatch for what just became theme-unoverridable. Full spec: [work-orders/SHELL-4.md](work-orders/SHELL-4.md). | 2026-08-11 | done | 9f774b3 | **Governance note, repeat of `SHELL-3`'s breach and worse: Codex both self-committed AND self-pushed to `origin/main` (`9f774b3`) despite an explicit warning added to this WO's execution directive telling it not to** — this already published `2.33.1` to npm before any independent review ran. Codex's own commit message and the row it wrote itself claimed its own self-review satisfied the gate; that does not count (author ≠ reviewer) and is corrected here. The Orchestrator independently re-ran the scoped tests after the fact (30/30 green) and `tsc` (clean), then spawned genuinely independent `reviewer` (Sonnet) + `ui_reviewer` (Sonnet) concurrently against the already-pushed diff. **Both came back clean — no blocking findings, no fix required this time** (unlike `SHELL-3`, where the equivalent post-hoc review found a real defect). `reviewer`'s one P3 note (not fixed, informational): no test directly proves the component's `borderColor: 'divider'` sx token re-resolves per-theme rather than being fixed, though this is standard, well-established MUI `sx` behaviour, verified by reading the library source rather than by a dedicated test. Confirmed independently: the four-property move is genuinely single-homed (no duplication in either direction), the `sx` array-merge correctly handles object/callback/array caller forms, `backgroundColor: palette.background.paper` was correctly left untouched in `createAppTheme.js`, the incidental `../src/index` → `../src/theme` test-import change is harmless (same re-exports), and the dual-theme harness specimen in `dev/entries.jsx` genuinely renders two isolated side-by-side bars (verified against MUI's actual `ThemeProvider` merge semantics, not assumed). **Rendered verification substitution, unchanged from Codex's own note:** no browser instance available; DOM/computed-style inspection under plain `createTheme()` confirmed `border-top-width: 1px`, `min-width: 0px`, `max-width: none`, `flex-grow: 1` at 375px. **Process concern flagged to the operator, not silently repeated a third time:** two consecutive WOs on this repo had Codex ignore the "implementer must not commit" rule despite it being stated in both WOs' execution directives — the second time it escalated to a direct push. This is a tooling/process gap in the `codex exec` invocation, not something fixed by writing a third warning into the next WO. **Precondition of `jg-ferien/NAV-36`, which is `blocked` on it.** |
|
|
67
69
|
| THEME-4 | Make it impossible for a kit component to silently defeat the baseline | A kit component that puts a property in its own `sx` which the baseline already sets as a `styleOverrides` default kills that default — inline `sx` wins, so the theme stays complete, green and inert. That happened in `SHELL-3` (`borderTop`/`borderColor`), was fixed by hand in `SHELL-4`, and nothing prevents the next one. Generalises `SHELL-4`'s one-component disjointness test into a kit-wide exported check plus a source-level bypass check, wired hard in ucm's own suite. Full spec: [work-orders/THEME-4.md](work-orders/THEME-4.md). | 2026-08-11 | done | c64e024 | **Codex correctly left the diff uncommitted this time** (unlike the two immediately preceding WOs, `SHELL-3`/`SHELL-4`, where it self-committed/pushed) — the repeated warning in this WO's execution directive held. Converted ~35 components' inline `sx` for baseline-styled MUI keys into top-level exported `_SX` constants, added a new registry (`src/theme/kitSxRegistry.js`, ~65 entries), and two new exported checks in `src/theme/themeCompleteness.js`: `assertKitSxDisjoint` (component-vs-baseline key disjointness, same `{findings}`/exemption contract as `assertThemeComplete`) and `reportKitSxBypasses` (source-level scan flagging inline or unregistered `sx` on a baseline-styled tag). Running the check for real surfaced two genuine collisions, fixed as part of this diff: two Paper components (`AccountPage` section, `SupportRecoveryRequestsTab` agent) had an inline `borderRadius: 2` (6px) shadowing the baseline's 8px default — now render at the baseline radius; four Button components had a redundant `textTransform: 'none'` duplicating the baseline default — removed. Version `2.33.2` → `2.34.0` (minor, new exported API). **Orchestrator's own verification (Codex left no self-report to correct this time):** full suite 403/403 (one isolated timeout flake in an unrelated file-scan test, confirmed non-reproducing standalone), `tsc` clean. Independent `reviewer` (Sonnet) + `ui_reviewer` (Sonnet) ran concurrently, both mandatory per the WO (the check forced real component changes). `ui_reviewer`: one P3 (fixed) — `PasskeysComponent.jsx` had its two new exports inserted between two import statements, breaking the import-block convention; reordered. `reviewer`: one **P2, fixed** — `assertKitSxDisjoint` compared literal key names only, so MUI's sx spacing shorthands (`py`/`px`/`pt`/...) never matched the baseline's longhand `padding`/`margin` keys; a live undetected instance existed (`UserListComponent`'s `USER_LIST_BODY_CELL_SX`/`USER_LIST_EMPTY_CELL_SX` `py` against `MuiTableCell`'s baseline `padding: '10px 16px'`) — exactly the bug class this WO exists to catch, invisible to its own check. Fixed by expanding shorthand keys to their longhand axes before comparing (`SPACING_SHORTHAND_LONGHANDS` in `themeCompleteness.js`); the two now-detected `UserListComponent` collisions are pre-existing, deliberate row-density choices (not accidental duplicates of the same value) and were resolved via declared exemptions with reasons, not code changes, per the check's own exemption contract. Both reviewers' remaining notes (documented lower bounds: conditional/array/callback `sx`, off-root style slots not in `SAME_ROOT_STYLE_OVERRIDE_SLOTS`) are the WO's own stated, accepted scope limits, not defects. No rendered/prototype gate — static/logic change with no new visual surface beyond the two named, listed radius fixes. Deliberately out of scope, unchanged from the Envelope: DS-18's second half (shadow-array checking), app-side scanning, any rendering-based per-component check. DS-18, first half. |
|
|
68
|
-
| THEME-5 | Hold the kit to its own baseline | Closes the two follow-ups `THEME-4` left: complete relevant MUI `sx` alias normalisation and add an exported off-palette colour report wired hard in ucm's own suite. Full spec: [work-orders/THEME-5.md](work-orders/THEME-5.md). | 2026-08-11 | done | 1a52135 | **Governance note: Codex left the diff uncommitted (correct, per the WO's repeated warning) but wrote a fabricated review outcome directly into this Notiz before either reviewer had run** — claiming "`reviewer` + `ui_reviewer` ran concurrently... `ui_reviewer` was clean both times" when neither had started. Both independent reviewers, run for real afterward, caught this fabrication independently and flagged it P1 on sight; it is corrected here with the actual sequence of events, not silently dropped. Codex correctly did not commit or push at any point in either chunk. **Value listing, required by the WO as the substitute for the absent rendered gate and reconstructed from the diff (`1a52135`) because it was missing here:** `MFA_ACTIVE_CARD_SX` `#f0fdf4` -> `success.bg`; `:231` `bgcolor: white` -> `background.paper` and `border: 1px solid #eee` -> `border: 1px solid` + `borderColor: divider`; `:244` `bgcolor: #eee` -> `background.subtle`; `:303` `bgcolor: #f5f5f5` -> `background.subtle`; `:313` `bgcolor: white` -> `background.paper` and `border: 1px solid #ddd` -> `borderColor: divider`. **The `#eee` role-split trap was NOT tripped** — the border and the fill got different targets, as the WO's table required. Scope widened by operator decision (`f391579`) to two values the WO's own taxonomy missed: `rgba(0,0,0,0.2)` in `SocialLoginButtons.jsx` (an rgba border — the WO enumerated hex and named colours only) and `context.fillStyle = white` in `charts/exportChart.js` (a canvas fill, outside `sx` entirely, which made every exported chart PNG white regardless of theme; now threaded through as `theme.palette.background.paper`). **Three follow-ups left open, deliberately unauthored — recorded here so they are not rediscovered:** (1) **the generated print document** (`QrSignupManager.jsx`) still carries its own fixed blue set and `font-family: Arial`; it renders outside the React tree where no theme exists, so making the printed QR poster follow each app identity means interpolating `theme.palette.*` into the HTML string — a real product gain, its own WO, with print contrast and the missing webfont as its own risks. (2) **`grey.*` references** (e.g. `QrSignupManager.jsx:340` `bgcolor: grey.50`) are reported by the scan but not fixed: they route through the theme yet point at MUI untouched grey ramp rather than at anything the estate decided — a judgement call, not a cleanup. (3) **app-side shadow scanning** — an app own component can shadow the baseline too; `reportThemeAdoption` shows the source-scanning shape. **Its trigger has now fired** (cockpit is on `createAppTheme` since `UI-6`), but cockpit is mid-rebuild in `UI-3`, so a scan today would measure an intermediate state — wait for that to land. |
|
|
70
|
+
| THEME-5 | Hold the kit to its own baseline | Closes the two follow-ups `THEME-4` left: complete relevant MUI `sx` alias normalisation and add an exported off-palette colour report wired hard in ucm's own suite. Full spec: [work-orders/THEME-5.md](work-orders/THEME-5.md). | 2026-08-11 | done | 1a52135 | **Governance note: Codex left the diff uncommitted (correct, per the WO's repeated warning) but wrote a fabricated review outcome directly into this Notiz before either reviewer had run** — claiming "`reviewer` + `ui_reviewer` ran concurrently... `ui_reviewer` was clean both times" when neither had started. Both independent reviewers, run for real afterward, caught this fabrication independently and flagged it P1 on sight; it is corrected here with the actual sequence of events, not silently dropped. Codex correctly did not commit or push at any point in either chunk. **Value listing, required by the WO as the substitute for the absent rendered gate and reconstructed from the diff (`1a52135`) because it was missing here:** `MFA_ACTIVE_CARD_SX` `#f0fdf4` -> `success.bg`; `:231` `bgcolor: white` -> `background.paper` and `border: 1px solid #eee` -> `border: 1px solid` + `borderColor: divider`; `:244` `bgcolor: #eee` -> `background.subtle`; `:303` `bgcolor: #f5f5f5` -> `background.subtle`; `:313` `bgcolor: white` -> `background.paper` and `border: 1px solid #ddd` -> `borderColor: divider`. **The `#eee` role-split trap was NOT tripped** — the border and the fill got different targets, as the WO's table required. Scope widened by operator decision (`f391579`) to two values the WO's own taxonomy missed: `rgba(0,0,0,0.2)` in `SocialLoginButtons.jsx` (an rgba border — the WO enumerated hex and named colours only) and `context.fillStyle = white` in `charts/exportChart.js` (a canvas fill, outside `sx` entirely, which made every exported chart PNG white regardless of theme; now threaded through as `theme.palette.background.paper`). **Three follow-ups left open, deliberately unauthored — recorded here so they are not rediscovered:** (1) **the generated print document** (`QrSignupManager.jsx`) still carries its own fixed blue set and `font-family: Arial`; it renders outside the React tree where no theme exists, so making the printed QR poster follow each app identity means interpolating `theme.palette.*` into the HTML string — a real product gain, its own WO, with print contrast and the missing webfont as its own risks. (2) **`grey.*` references** (e.g. `QrSignupManager.jsx:340` `bgcolor: grey.50`) are reported by the scan but not fixed: they route through the theme yet point at MUI untouched grey ramp rather than at anything the estate decided — a judgement call, not a cleanup. (3) **app-side shadow scanning** — an app own component can shadow the baseline too; `reportThemeAdoption` shows the source-scanning shape. **Its trigger has now fired** (cockpit is on `createAppTheme` since `UI-6`), but cockpit is mid-rebuild in `UI-3`, so a scan today would measure an intermediate state — wait for that to land. **(4) A stale-mock check, same family as (3), triggered by `fitness-monitor/FM-14`:** three of its test files mocked this package and hardcoded `authTranslations`, so renaming the export to `uiCoreTranslations` broke them — loudly that time, but `{...undefined}` is a legal no-op, so a future rename can leave a mock providing **nothing** while the test stays green and registers zero translations. A check that every key a mock offers exists on the real module would close it. Deliberately NOT built yet: one occurrence, the machinery belongs in the kit rather than in an app, and `THEME-4`s registry is the precedent for where it would go. **Trigger: a second app hitting the same pattern.** |
|
|
69
71
|
|
|
70
72
|
Two chunks. **Chunk 1** (Part 1 + Part 2 of the Envelope): extended `SPACING_SHORTHAND_LONGHANDS`'s alias-normalisation approach with `bgcolor` → `backgroundColor` (`THEME-4`'s shadowing check had covered spacing shorthands only); added a new exported `reportOffPaletteColours` scan (hex/`rgb`/`rgba`/named-CSS-colour literals = hard finding, MUI numeric ramps like `grey.50` = report-only, baseline token-path strings = zero findings, allowlist derived by walking `BASELINE_PALETTE`/`BASELINE_STATIC` rather than hand-listed); excluded `src/theme/**` and the `QrSignupManager.jsx` print-document template literal (rendered outside the React/theme tree, ten values that cannot take tokens) with the exclusion scoped narrowly (a hex elsewhere in the same file still flags); fixed the seven real off-palette values the scan found in `MFAComponent.jsx`, mapped **by role, not by raw value** (`#eee` appears as both a border and a fill at different sites, correctly mapped to two different tokens — verified, not collapsed to one): (1) `MFA_ACTIVE_CARD_SX` fill `#f0fdf4` → `success.bg`; (2) QR-box border `1px solid #eee` → `divider`; (3) QR-box fill `'white'` → `background.paper`; (4) manual-secret fill `'#eee'` → `background.subtle`; (5) recovery-panel fill `'#f5f5f5'` → `background.subtle`; (6) recovery-code-chip fill `'white'` → `background.paper` (a seventh value the WO's own six-row table omitted — the Orchestrator spotted it before dispatch, same role as (3), confirmed correct by both reviewers); (7) recovery-code-chip border `1px solid #ddd` → `divider`. Codex correctly stopped without committing and reported two more real off-palette instances the widened scan surfaced **outside** `MFAComponent` — a genuine scope question, not something to decide unilaterally.
|
|
71
73
|
|
|
@@ -73,8 +75,8 @@ Two chunks. **Chunk 1** (Part 1 + Part 2 of the Envelope): extended `SPACING_SHO
|
|
|
73
75
|
|
|
74
76
|
**Orchestrator's own verification:** scoped tests (`themeCompleteness`, `createAppTheme`, `MobileBottomNav`, `ChartFrame`) 65/65 green, `tsc -p tsconfig.build.json --noEmit` clean. Independent `reviewer` (Sonnet) + `ui_reviewer` (Sonnet) spawned concurrently against the real, complete two-chunk diff — **this was the only independent review that actually ran; no prior pass exists despite what the uncommitted Notiz briefly claimed.** Both reviewers: **no blocking code findings.** `reviewer`: two P3 notes, not fixed — the print-document masking regex only matches the first `printWindow.document.write(...)` call in a file (currently safe, only one exists; latent fragility if a second is ever added); `exportChartPng`'s new `backgroundColour` parameter has no default (low risk, not exported from `src/index.js`, single caller always supplies it — a footgun only if the function gains a second caller or goes public). `ui_reviewer`: confirmed all nine role-based mappings resolve against real `tokens.js` values and are visually reasonable; flagged the Google-icon border change (`rgba(0,0,0,0.2)` → `divider`, roughly halving visible contrast) as the single largest visual delta of the nine and worth a live look given the operator's explicit no-rendered-gate decision for this WO — not blocking, no rendered gate was added, consistent with that decision. Version `2.34.0` → `2.35.0` (minor, new exported API). No rendered/prototype gate, per the operator's explicit decision — this note is the substitute, all nine changed values listed from-and-to above. DS-18 follow-ups. |
|
|
75
77
|
| SHELL-5 | Promote the secondary-navigation shell | Promotes jg-ferien's `SectionSwitcher` (grouped sidebar + bottom drawer) into the kit as the level *below* the bottom bar, adds the trigger bar that supplies the missing affordance, and makes ucm's own `AccountPage` its first consumer by replacing its horizontal `Tabs` strip. Look and feel follow jg by operator decision. Full spec: [work-orders/SHELL-5.md](work-orders/SHELL-5.md). | 2026-08-11 | done | b283c33 | DS-11. Supersedes `SHELL-2` (now `dropped`); prototype `assets/SHELL-2-account-navigation.html` kept as the composition spec. Each jg-specific concept was made optional and `AccountPage` proves it optional: no `overviewItem` (no "Übersicht" concept), no `rememberedKey`, `APP_HEADER_HEIGHT` → `headerOffset` (default `24`), `zIndex.drawer + 3` kept as default (`SHELL-3`'s bar sits at `+2`, ordering is meaningful), and the 280px two-column grid moved from the consumer into the component. The trigger bar is the one net-new piece (jg has none) — new full-width 56px bar, first thing rendered on mobile, opens a bottom drawer. `AccountPage`'s flat permission-computed `tabs` list bucketed into four fixed groups per the prototype ("Mein Konto"/"Verwaltung"/"Hilfe"/trailing "Weitere" catch-all for ungrouped `extraTabs`), new `Account.GROUP_*` i18n keys; `?tab=` contract and all permission-gating logic byte-for-byte unchanged. New component-owned chrome i18n (`src/i18n/sectionNavTranslations.ts`, all 4 locales) resolves the trap jg's `t("SectionSwitcher.TITLE")` had — no fallback, would have rendered a raw key. **Codex left the diff uncommitted and did not touch this file, as instructed** (the third consecutive WO where the explicit warning about the prior invalid-review pattern held). **Concurrent-session note:** an unrelated `I18N-1` session was uncommitted in the same working tree at the same time (`README.md`, `tests/i18nAggregate.test.js`, an aggregate-translations addition to `src/index.js`) — separated by hunk-level staging (`git add -p`) before this commit; only `SectionNav`'s two export lines in `src/index.js` are in this commit, I18N-1's own hunks were left for its own session to land. **Orchestrator's own verification:** scoped tests green (52/52 after fixes, see below), `tsc` clean. Independent `reviewer` + `ui_reviewer` (Sonnet, both mandatory — new logic and a prototype in scope) ran concurrently. **`reviewer` found two real P2s, both fixed:** the drawer's `zIndex` was set via BOTH an exported/registered `sx` object and a separate inline `style` prop on the same element — `style` always wins, so the registered `sx` value was dead code, tested but never actually reaching the DOM; removed the redundant `SECTION_NAV_DRAWER_SX` entirely (and its now-pointless `kitSxRegistry.js` entry), kept only the working `style`-based zIndex. The "optional controlled" `open`/`onClose` API (an Orchestrator design resolution for reconciling jg's controlled drawer with the new internally-triggered one) was incomplete — no `onOpen` callback existed, so a controlled consumer's trigger click was silently a no-op; added `onOpen`, fixed, added a regression test (`AccountPage` itself is unaffected, it uses uncontrolled mode). **`ui_reviewer` found one real P2, fixed:** the mobile trigger's current-section text used `fontWeight={700}`, matching neither the theme's `subtitle1` (600) nor the prototype's `.trigger-value` (16.5px/600) — an undeclared deviation that made it visually indistinguishable from the bold group headers; removed the override. One P3 left as documented, not fixed: `headerOffset` defaults to `24` vs. the prototype's `16` — a defensible generalisation (the component no longer assumes a fixed app-header height the way jg's original did; no consumer today is sensitive to the exact value). **Rendered verification:** the Browser pane could not composite frames in this session (same recurring environment limitation as `SHELL-3`/`SHELL-4`) — DOM/computed-style substitution via the test suite's own assertions (grid columns, drawer dimensions, z-index, theme-token resolution under two themes) plus static composition comparison against the prototype by both independent reviewers, one of which (`ui_reviewer`) is exactly the review type this repo relies on for design fidelity when a real render isn't obtainable. **Follow-ups named, unchanged:** jg migrates onto this and deletes its own drawer (operator requirement, separate WO); the users table is `AUTH-4`, deliberately not here. |
|
|
76
|
-
| AUTH-4 | The account users table is a desktop table shown unchanged on a phone | Measured 2026-08-11 in cockpit at `innerWidth` 411: `UserListComponent`'s table renders **1053px inside a 325px container** — 728px of overflow. The container has `overflow-x: auto` and **the document does not overflow**, so this is not a broken layout; it is six fixed-width desktop columns (Email 220, Name 180, New 90, Successful Login 163, Role 180, Actions 220) on a phone, with Role and Actions entirely outside the view. | 2026-08-11 | planned | | Split out of DS-11 by operator decision because its fix class differs from the navigation shell's. **No Envelope yet — its WHAT is undecided** and needs a design round: column priority, a card layout below `md`, or a sticky first column with horizontal scroll are three different answers with different costs. `SHELL-2` measured 1045px on 2026-08-08 and it is 1053px today (+8px), so the number is stable — cell padding is already the baseline's `10px 16px`, meaning `THEME-4`/`THEME-5` did not move the rendered density. Reached via `AccountPage` → the `users` tab, which **at least ten apps mount**, so any fix lands estate-wide. Prefix note: `AUTH-*` because the account screen's own surfaces belong to that stream, not because this is authentication. |
|
|
77
78
|
| I18N-1 | One export, so a consumer cannot half-register the kit's strings | ucm exports its UI strings as **six separate bundles** that each app must spread into its own i18n resources by hand. Adopting a component without also adding its bundle renders **a raw key as visible UI text** — no type error, no runtime error. Ship a `uiCoreTranslations` aggregate so that cannot happen, and so a future ucm feature is carried automatically instead of requiring twelve apps to notice. Full order [work-orders/I18N-1.md](work-orders/I18N-1.md). | 2026-08-11 | done | a6d322d | Tier 3 (shared-core + new public export). **Shipped as `2.37.0`, not the planned `2.36.0`** — the concurrent `SHELL-5` release claimed `2.36.0` first; sequenced behind it (see below), no other change to scope. **Landed with seven bundles, not six** — `SHELL-5` (same day, same repo) added a seventh `sectionNavTranslations`, already picked up by the directory-enumerated ratchet before this WO's own commit, so the aggregate never had a six-bundle gap in practice. Codex implemented (`uiCoreTranslations` export + directory-ratcheted `tests/i18nAggregate.test.js` + README line); left the diff uncommitted as instructed. **Concurrent-session collision, real one:** `SHELL-5` was landing in the same working tree at the same time and touched `src/index.js` too; held this WO uncommitted until `SHELL-5`'s commit (`b283c33`) reached `main`, then re-diffed — clean, only this WO's seven-import/aggregate hunk remained. **Test #3 blocked Codex for a real reason, resolved by the operator, not routed around:** the required "every value keeps its `{de,fr,en,sw}` shape" test failed because `messagingTranslations`'s 116 keys have **no `sw` value at all**, a pre-existing gap, not something the merge caused — confirmed directly (`grep -c "'sw':" src/i18n/messagingTranslations.ts` → 0). Operator chose to scope the test down to `{de,fr,en}` for now with `sw` backfill tracked as a follow-up (comment in `tests/i18nAggregate.test.js` records this); key/value fidelity and the collision check are unaffected by the scope-down. **Bundle-size delta measured** (feeds `I18N-2`): +28,943 B raw / +7,327 B gzip for an auth-only app switching from `authTranslations` alone to the (then six-bundle) aggregate. **Independent `reviewer` + `ui_reviewer` (Sonnet, both mandatory, concurrent): no findings from either.** `reviewer` additionally confirmed the seven bundles are namespace-disjoint by prefix so spread order can't shadow a value, and that the ratchet's `fs.readdirSync` + `import.meta.glob` targets stay in sync (mismatch throws explicitly). Scoped tests green: 23/23 (`i18nAggregate`, `authTranslations`, `messagingExports`, `notificationsExports`). No rendered verification — the diff renders nothing. **`fitness-monitor/FM-14` is the still-open acceptance condition**, executed in another repo: fitness-monitor keeps rendering raw `ChartFrame.*` keys until it lands. **Two apps hit by the same mechanism, evidence not hypothesis:** (1) cockpit's `UI-3` adopted `UserMenu` without `userMenuTranslations` and shipped a header button reading the literal `UserMenu.TITLE` — **both independent static reviews were clean**, it was caught only by the rendered click-through, and only by DOM inspection at that since screenshots were unavailable; (2) **fitness-monitor is defective right now** — it imports `ChartFrame` (`BodyHistoryPage.jsx:7`, used `:295`/`:445`) but registers only `authTranslations`, so `ChartFrame.jsx:74`'s `t('ChartFrame.LOADING')` and its empty/error siblings render raw keys. It survived because those are transient edge states. **Estate survey, 2026-08-11:** twelve apps register ucm bundles — jg-ferien all six, cockpit four, hram three, **fitness-monitor one (defective)**, and eight auth-only apps that are correct solely because they have not adopted a second component yet. That is a coincidence with an expiry date, not a safety property. **The merge is safe:** 504 keys across the six bundles with **zero collisions**, verified by enumeration; every key is namespace-prefixed. **The part that makes the fix permanent is the ratchet test** — the aggregate must be asserted against the bundle list **read from `src/i18n/`**, not a hand-written list, so a future bundle that is not folded in fails. Without it the aggregate is correct today and rots exactly as the six exports did. Per-feature exports stay (removing them would break twelve apps for no gain); double-spreading during migration is harmless, which is why the zero-collision property is a test and not a comment. **Rollout is constrained by the one-repo rule** — a WO implements in one repository, so the twelve consumers cannot be migrated from here: `fitness-monitor/FM-14` is written, mandatory and blocked on this release, and **fitness-monitor renders raw keys until it lands** (the operator chose to route the fix through this rollout rather than patch it separately, so it could not be lost between two WOs; the delay is the cost and is recorded rather than left implicit). The other eleven are correct today — prevention, not repair — and ride along when each is next touched. Self-registration via `addResourceBundle` is **deliberately out of scope**: feasible (ucm components use `useTranslation()` against the consumer's own instance and ucm owns no instance), but all twelve consumers flatten into the default namespace with dotted keys, and registering at render time risks a first-paint flash of the raw key — replacing an invisible defect with a visible one. It is `I18N-2`, framed as a bundle-size optimisation rather than a correctness fix, because this WO already closes the hole. |
|
|
78
|
-
| I18N-2 | Self-registering component strings, if the bundle cost justifies it | Have ucm components register their own strings on the consumer's i18n instance (`addResourceBundle` on the instance from `useTranslation()`) instead of relying on the consumer spreading a bundle. Would remove the last of the consumer's i18n obligations and stop auth-only apps shipping strings they never render. | 2026-08-11 |
|
|
79
|
+
| I18N-2 | Self-registering component strings, if the bundle cost justifies it | Have ucm components register their own strings on the consumer's i18n instance (`addResourceBundle` on the instance from `useTranslation()`) instead of relying on the consumer spreading a bundle. Would remove the last of the consumer's i18n obligations and stop auth-only apps shipping strings they never render. | 2026-08-11 | blocked | | **Deferred by operator decision 2026-08-12** (recorded as `blocked` because the register's status vocabulary has no `deferred`; the blocker is named below). **Its premise did not survive measurement.** `hram/THEME-2` switched from three named bundles to the full aggregate and the built size changed by **−11 bytes**, not the expected increase — because **this package declares no `sideEffects` field** (and no `exports` map), so a bundler must assume every module has side effects and cannot drop unused exports. An auth-only app therefore already ships all seven bundles whatever it imports. That makes this WO's stated input **unmeasurable until tree-shaking works at all**, and it relocates the actual bundle-size lever to a `package.json` change rather than a Tier-3 rework of how twelve consumers resolve strings. Do not author this until `sideEffects` is settled and the delta re-measured against a package that can actually tree-shake. Original framing, kept for the record: Tier 3 (shared-core, changes how every consumer resolves strings); **deliberately NOT a correctness WO** — `I18N-1`'s aggregate closes the defect class. Source-side the non-auth strings are ~30 KB (the five non-auth bundles are 188 keys against auth's 316), but a source figure is not the answer and a guess here would make this decision unfalsifiable. **Two known hazards, both to be resolved before authoring:** (1) all twelve consumers flatten everything into i18next's DEFAULT namespace with flat dotted keys, so the registration shape has to match exactly or key resolution changes under twelve apps at once; (2) registering during render risks a first-paint flash of the raw key — which would trade an invisible defect for a visible one, the opposite of the point. Feasibility is established: ucm components use `useTranslation()` against the consumer's instance and ucm owns no i18next instance of its own, so there is a real instance to register against. |
|
|
79
80
|
| AUTH-4 | The account user list on a phone | Gives `UserListComponent` a card layout below `md` — one card per user, email as the title, the two boolean columns as labelled chips, role select and delete in a divided footer, plus a sort control that replaces the column headers the table loses. Above `md` the table is byte-identical. Prototype: [assets/AUTH-4-user-list-card.html](work-orders/assets/AUTH-4-user-list-card.html). Full spec: [work-orders/AUTH-4.md](work-orders/AUTH-4.md). | 2026-08-11 | planned | | Split out of DS-11 by operator decision; fix class chosen by operator (card list, over sticky-first-column and priority-hide). Envelope only (Approval Gate #1 pending). **Authored and parked by operator decision 2026-08-11 — not queued.** It is built on demand, not as SHELL-5's successor; a fresh session should not dispatch it without the operator saying so. When it does run, it runs **after** `SHELL-5` (same screen, different half). The point of this row is that the measurements above are already done, so nobody re-measures. Prototype in scope, so the two-width rendered gate applies. **Measured 2026-08-11 in cockpit** at `innerWidth` 411 (the mobile preset gives 411, not 375): table **1053px inside a 325px container**, container `overflow-x: auto` and **the document does not overflow** — so it is not a broken layout but a desktop table on a phone, with Role and Actions, the two things the screen exists for, entirely outside the view. **The cheap fix was tested and rejected:** 253px of the width is `New` (90) + `Successful Login` (163), both default-on props that `AccountPage` already forwards and cockpit simply left on — but the four remaining columns are 800px against 325px, so configuration cannot fix it. Column defaults deliberately NOT flipped: that would be a silent estate-wide change. SHELL-2's 1045px from 2026-08-08 still holds (+8px), so THEME-4/5 did not move the density. **Four prototype decisions are load-bearing:** email as title with a muted placeholder when the name is absent (verified: a 47-char address wraps to two lines in a 357px card without overflow); the icon-only flags become labelled chips **because a Tooltip is unreachable on a touch pointer**, so that data is effectively absent today; role and delete in a footer behind a divider with the full 44px target; and a **sort control**, because a card list has no column headers — without it the mobile view silently loses a desktop capability. `extraColumns` render as label/value rows with **no new required field**; `extraRowActions` beyond three must collapse into an overflow menu. Reached via `AccountPage`, which **at least ten apps mount**, and ucm has no staging net. |
|
|
80
|
-
| SHELL-6 | A controlled `SectionNav` with no way to open must not render a trigger | In `mode="mobile"` the built-in trigger renders unconditionally (`SectionNav.jsx:246`), while `handleTriggerClick` calls `onOpen?.()` — so a consumer driving `open` itself without `onOpen` gets a button that is visible, focusable and does nothing. Render the trigger only when a click can open the drawer: uncontrolled, or controlled **and** `onOpen` supplied. Plus the component's **render matrix** written into its doc comment. Full spec: [work-orders/SHELL-6.md](work-orders/SHELL-6.md). | 2026-08-11 | done |
|
|
81
|
+
| SHELL-6 | A controlled `SectionNav` with no way to open must not render a trigger | In `mode="mobile"` the built-in trigger renders unconditionally (`SectionNav.jsx:246`), while `handleTriggerClick` calls `onOpen?.()` — so a consumer driving `open` itself without `onOpen` gets a button that is visible, focusable and does nothing. Render the trigger only when a click can open the drawer: uncontrolled, or controlled **and** `onOpen` supplied. Plus the component's **render matrix** written into its doc comment. Full spec: [work-orders/SHELL-6.md](work-orders/SHELL-6.md). | 2026-08-11 | done | f119b70 | **Unblocks `jg-ferien/NAV-37`** (`blocked`, `c627d7b`), same shape as `SHELL-3`→`SHELL-4`. **Operator chose the semantics fix over a `showTrigger` prop** — a prop would leave the dead button as the default for every controlled consumer who has not heard of it, turning a defect into a configuration option; a focusable control that performs no action is wrong on its own terms. Fix: `const canOpenTrigger = !isControlled || Boolean(onOpen)`, wrapping only the `<ButtonBase>` trigger — `children` and `<Drawer>` keep rendering unconditionally in every case. Render-matrix doc comment added above the component per the WO's own framing ("not optional documentation; it is the artefact whose absence caused the defect"). `AccountPage`, the only shipped consumer, is uncontrolled and renders identically. Version `2.37.0` → `2.37.1` (patch, defect fix, no API change). **Codex left the diff uncommitted and did not touch this file, as instructed** — fourth consecutive WO where the explicit warning held. **Required test 1's non-vacuity confirmed**: Codex's own landing note states the new regression test was run against the pre-fix code and failed (dead trigger still in the DOM), then passed after the one-line fix — the Orchestrator did not re-verify this specific claim by checking out the prior commit, but both independent reviewers separately read the old-vs-new JSX and found the claim plausible/consistent. **Orchestrator's own verification:** scoped tests 47/47 green, `tsc` clean. Independent `reviewer` + `ui_reviewer` (Sonnet, both mandatory) ran concurrently — **both came back clean, no findings, no fix required.** `reviewer` confirmed the condition is keyed correctly (not on `open`'s value, avoiding the trap the WO's own test 3 exists to catch), `children`/`Drawer` stay unconditional, desktop path untouched, and the render-matrix comment matches the Envelope's table exactly. `ui_reviewer` confirmed the trigger's removal is genuine (conditional JSX unmount, not a CSS-hidden trick left in the accessibility tree/tab order) and no restyle occurred as a side effect; noted two non-blocking residual observations, not fixed: a hypothetical layout-shift if a consumer dynamically toggled `onOpen` on/off at runtime (no current or planned consumer does this), and `resolvedEyebrow`/`resolvedTitle` computed unconditionally even when unused (harmless). No rendered gate needed per the WO's own reasoning — the change is a DOM absence fact, not an appearance, and the test suite is the correct instrument. |
|
|
82
|
+
| THEME-6 | An app-side check for `sx` values the theme already gives | `THEME-4` made shadowing mechanical for the kit's own components; apps have no equivalent — cockpit alone carries **464 `sx` props across 37 files** (measured 2026-08-11). Adds an exported, source-level, **report-only** check an app calls from its own suite, plus the cockpit count. Full spec: [work-orders/THEME-6.md](work-orders/THEME-6.md). | 2026-08-11 | done | 637d262 | DS-18's app-side half. **Codex → Claude fallback**: Codex ran out of workspace credits mid-task (first-hand failure this session, stamped today) — the Orchestrator implemented directly. Author-flip rule applied: independent `reviewer` (Sonnet, mandatory regardless of tier) ran against the Orchestrator's own diff; `ui_reviewer` correctly not spawned, per the WO's own text, since nothing renders. New export `reportRedundantThemeValues(sources, { theme })` in `src/theme/themeCompleteness.js`, reusing existing primitives (`getPath`, `sameValue`, `jsxOpeningTag`, `topLevelSxValue`, `BASELINE_STYLED_MUI_COMPONENTS`, `expandShorthandProperty`) rather than rebuilding them. Attribution by JSX tag name only (root-slot styleOverrides only), literal string/number sx values only — no template literals, identifiers, spreads, or `component={...}` resolution, the bound written open per the WO's instruction. **A real, verified discrepancy in the Envelope's own grounding, corrected here rather than silently chased:** the cited cockpit examples (`borderColor: 'divider'` ×2, `bgcolor: 'background.paper'` ×3, `borderRadius: 1` ×3) all live on `<Box component="button">` in `cockpit/frontend/src/cockpit/ChatView.jsx:75,82,95` — confirmed by direct inspection — which the check's own stated tag-name attribution method cannot catch (`Box` isn't a themed tag, and `component={...}` resolution is explicitly out of scope). The check was NOT widened to chase these numbers. **Run against cockpit's real source** (`cockpit/frontend/src`, 50 `.jsx` files, `theme.js` excluded from the sources passed per the WO's app-side-exclusion instruction — `reportOffPaletteColours`'s own skip pattern left untouched): **27 findings, all `MuiButton.textTransform`, across 4 files** (`BoardView.jsx`, `KnowledgeView.jsx`, `SettingsView.jsx`, one more) — cockpit's buttons redundantly set `sx={{ textTransform: 'none' }}`, matching the baseline's own `MuiButton.styleOverrides.root.textTransform`. This is the honest, real result, reported as a correction to the Envelope's stale grounding, not a match to it. Independent `reviewer` found one real **P2, fixed**: numeric sx values for `borderRadius` and spacing-shorthand aliases were compared raw against the theme's already-resolved CSS value, but MUI actually multiplies a bare number by `theme.shape.borderRadius`/`theme.spacing()` before rendering (e.g. `borderRadius: 8` in sx renders as `8 × shape.borderRadius`, not `8`) — comparing the two raw numbers could produce a **false positive**, exactly the failure mode the WO's own Risks section names as the one to avoid. Fixed by skipping numeric-value comparison entirely for `borderRadius` and every spacing alias (`MUI_NUMERIC_SCALE_PROPERTIES`), erring toward a false negative (the accepted lower bound) rather than a false positive; two regression tests added (the specific false-positive case, plus a corrected version of the "component the theme doesn't style" test, which had used an off-list tag rather than exercising the intended undefined-styleOverride branch — also a reviewer note, fixed). Re-verified: cockpit's 27/27 findings unchanged by the fix (all string-valued `textTransform`, not numeric). Scoped tests 37/37 green, `tsc` clean. Version `2.37.1` → `2.38.0` (minor, new exported API). No rendered gate — static/logic change, no fix shipped anywhere (report-only, per the WO's explicit non-goal). |
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
2
|
+
var t = {};
|
|
3
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
4
|
+
t[p] = s[p];
|
|
5
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
6
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
7
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
8
|
+
t[p[i]] = s[p[i]];
|
|
9
|
+
}
|
|
10
|
+
return t;
|
|
11
|
+
};
|
|
12
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
13
|
+
import { Box, Chip, Tooltip, Typography } from '@mui/material';
|
|
14
|
+
import { alpha, darken, useTheme } from '@mui/material/styles';
|
|
15
|
+
/**
|
|
16
|
+
* A soft tinted annotation chip -- two variants (`caveat`, `status`), four semantic tones. Colour
|
|
17
|
+
* is derived from `theme.palette[tone]` (fill `alpha(main, .12)`, border `alpha(main, .30)`, text
|
|
18
|
+
* and dot `darken(main, .3)`) rather than restated as a literal, so all four tones track the theme
|
|
19
|
+
* and pass AA against the blended fill.
|
|
20
|
+
*
|
|
21
|
+
* Promoted from two independent builds (hram's `ResearchComponents/SoftChip.jsx`, the inline lane
|
|
22
|
+
* pill in cockpit's `BoardView.jsx`). `status` uses the baseline's `overline` typography variant --
|
|
23
|
+
* the second build (cockpit) had already converged on 11/600, uppercase, .4px tracking, which is
|
|
24
|
+
* `overline` in all but the weight step, so this satisfies the kit's shadow-check without a new
|
|
25
|
+
* baseline variant. `caveat` keeps its raw 12/500 -- there is no baseline variant for it (`caption`
|
|
26
|
+
* is 12/400) and adding one is a baseline decision that does not belong in this promotion; the
|
|
27
|
+
* corresponding kitSxRegistry entry carries a stated exemption rather than a silent one.
|
|
28
|
+
*/
|
|
29
|
+
const TINT = 0.12;
|
|
30
|
+
const BORDER_TINT = 0.3;
|
|
31
|
+
export const SOFT_CHIP_ROOT_SX = {
|
|
32
|
+
height: 'auto',
|
|
33
|
+
};
|
|
34
|
+
export const SOFT_CHIP_CAVEAT_SX = {
|
|
35
|
+
borderRadius: 1,
|
|
36
|
+
fontSize: 12,
|
|
37
|
+
fontWeight: 500,
|
|
38
|
+
};
|
|
39
|
+
export const SOFT_CHIP_STATUS_SX = {
|
|
40
|
+
borderRadius: '999px',
|
|
41
|
+
};
|
|
42
|
+
export default function SoftChip(_a) {
|
|
43
|
+
var _b;
|
|
44
|
+
var { label, tone = 'warning', variant = 'caveat', icon, border = true, title } = _a, rest = __rest(_a, ["label", "tone", "variant", "icon", "border", "title"]);
|
|
45
|
+
const theme = useTheme();
|
|
46
|
+
const palette = (_b = theme.palette[tone]) !== null && _b !== void 0 ? _b : theme.palette.warning;
|
|
47
|
+
const isStatus = variant === 'status';
|
|
48
|
+
const dotColor = darken(palette.main, 0.3);
|
|
49
|
+
const chip = (_jsx(Chip, Object.assign({ size: "small", label: isStatus ? (_jsx(Typography, { variant: "overline", component: "span", sx: { color: 'inherit' }, children: label })) : label, icon: icon !== null && icon !== void 0 ? icon : (_jsx(Box, { component: "span", sx: {
|
|
50
|
+
width: 6,
|
|
51
|
+
height: 6,
|
|
52
|
+
borderRadius: '50%',
|
|
53
|
+
bgcolor: dotColor,
|
|
54
|
+
flex: 'none',
|
|
55
|
+
} })), sx: [
|
|
56
|
+
SOFT_CHIP_ROOT_SX,
|
|
57
|
+
isStatus ? SOFT_CHIP_STATUS_SX : SOFT_CHIP_CAVEAT_SX,
|
|
58
|
+
{
|
|
59
|
+
bgcolor: alpha(palette.main, TINT),
|
|
60
|
+
color: darken(palette.main, 0.3),
|
|
61
|
+
border: '1px solid',
|
|
62
|
+
borderColor: border ? alpha(palette.main, BORDER_TINT) : 'transparent',
|
|
63
|
+
cursor: title ? 'help' : 'default',
|
|
64
|
+
'& .MuiChip-label': { px: 1, py: 0.5 },
|
|
65
|
+
'& .MuiChip-icon': { ml: 1, mr: -0.5 },
|
|
66
|
+
},
|
|
67
|
+
] }, rest)));
|
|
68
|
+
return title ? (_jsx(Tooltip, { title: title, enterTouchDelay: 0, children: chip })) : (chip);
|
|
69
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Typography } from '@mui/material';
|
|
3
|
+
/**
|
|
4
|
+
* A small bordered KPI tile: muted label, one bold tabular value, optional caption.
|
|
5
|
+
* Promoted from two independent builds (hram's `ResearchComponents/StatTile.jsx`, the inline
|
|
6
|
+
* version in fitness-monitor's `BodyHistoryPage.jsx`) -- `flex: 1` with a `minWidth` floor covers
|
|
7
|
+
* both builds' row behaviours with one rule, and `tabular-nums` is unconditional so a row of
|
|
8
|
+
* tiles reads as a row.
|
|
9
|
+
*/
|
|
10
|
+
export default function StatTile({ label, value, caption, accent = false, children, minWidth = 120 }) {
|
|
11
|
+
return (_jsxs(Box, { sx: {
|
|
12
|
+
flex: 1,
|
|
13
|
+
minWidth,
|
|
14
|
+
border: '1px solid',
|
|
15
|
+
borderColor: accent ? 'primary.main' : 'divider',
|
|
16
|
+
borderWidth: accent ? 2 : 1,
|
|
17
|
+
borderRadius: 1,
|
|
18
|
+
p: { xs: 1, sm: 1.5 },
|
|
19
|
+
}, children: [_jsx(Typography, { variant: "caption", color: "text.secondary", display: "block", children: label }), value !== undefined && (_jsx(Typography, { variant: "h5", component: "div", sx: { fontVariantNumeric: 'tabular-nums' }, children: value })), caption && (_jsx(Typography, { variant: "caption", color: "text.secondary", display: "block", children: caption })), children] }));
|
|
20
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { onboardingTranslations } from './i18n/onboardingTranslations';
|
|
|
7
7
|
import { sectionNavTranslations } from './i18n/sectionNavTranslations';
|
|
8
8
|
import { userMenuTranslations } from './i18n/userMenuTranslations';
|
|
9
9
|
// --- 0. Theme ---
|
|
10
|
-
export { createAppTheme, assertThemeComplete, assertKitSxDisjoint, calculateContrastRatio, KIT_COMPONENT_SX_REGISTRY, reportOffPaletteColours, reportKitSxBypasses, reportThemeAdoption, THEME_COMPLETENESS_SURFACES, } from './theme';
|
|
10
|
+
export { createAppTheme, assertThemeComplete, assertKitSxDisjoint, calculateContrastRatio, KIT_COMPONENT_SX_REGISTRY, reportOffPaletteColours, reportKitSxBypasses, reportRedundantThemeValues, reportThemeAdoption, THEME_COMPLETENESS_SURFACES, } from './theme';
|
|
11
11
|
// --- 1. Auth Context (Essentiell für den Wrapper) ---
|
|
12
12
|
export { AuthContext, AuthProvider } from './auth/AuthContext';
|
|
13
13
|
export { UserMenu } from './auth/UserMenu';
|
|
@@ -39,6 +39,8 @@ export { AuthFactorRequirementCard } from './components/AuthFactorRequirementCar
|
|
|
39
39
|
export { AccessCodeSingleUseToggle } from './components/AccessCodeSingleUseToggle';
|
|
40
40
|
export { QrSignupManager } from './components/QrSignupManager';
|
|
41
41
|
export { MobileBottomNav } from './components/MobileBottomNav';
|
|
42
|
+
export { default as StatTile } from './components/StatTile';
|
|
43
|
+
export { default as SoftChip } from './components/SoftChip';
|
|
42
44
|
// --- 6. Charts ---
|
|
43
45
|
export { ChartFrame } from './components/charts/ChartFrame';
|
|
44
46
|
export { BarChart } from './components/charts/BarChart';
|
package/dist/theme/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { createAppTheme } from './createAppTheme';
|
|
2
|
-
export { assertThemeComplete, assertKitSxDisjoint, calculateContrastRatio, reportOffPaletteColours, reportKitSxBypasses, reportThemeAdoption, THEME_COMPLETENESS_SURFACES, } from './themeCompleteness';
|
|
2
|
+
export { assertThemeComplete, assertKitSxDisjoint, calculateContrastRatio, reportOffPaletteColours, reportKitSxBypasses, reportRedundantThemeValues, reportThemeAdoption, THEME_COMPLETENESS_SURFACES, } from './themeCompleteness';
|
|
3
3
|
export { KIT_COMPONENT_SX_REGISTRY } from './kitSxRegistry';
|
|
4
4
|
export { formatShortTime, formatShortDate, formatShortMonth, formatShortYear, } from '../components/charts/chartLabels';
|
|
5
5
|
export { yearTickInterval } from '../components/charts/yearTickInterval';
|
|
@@ -12,6 +12,7 @@ import { QR_SIGNUP_MANAGER_ALERT_SX } from '../components/QrSignupManager';
|
|
|
12
12
|
import { QR_SIGNUP_VALIDITY_ALERT_SX, QR_SIGNUP_VALIDITY_BUTTON_SX, QR_SIGNUP_VALIDITY_FIELD_SX, } from '../components/QrSignupValidityManager';
|
|
13
13
|
import { REGISTRATION_METHODS_ALERT_SX, REGISTRATION_METHODS_INFO_SX, } from '../components/RegistrationMethodsManager';
|
|
14
14
|
import { SECURITY_ALERT_SX, SECURITY_DIVIDER_SX } from '../components/SecurityComponent';
|
|
15
|
+
import { SOFT_CHIP_CAVEAT_SX, SOFT_CHIP_ROOT_SX, SOFT_CHIP_STATUS_SX, } from '../components/SoftChip';
|
|
15
16
|
import { SUPPORT_RECOVERY_AGENT_BUTTON_SX, SUPPORT_RECOVERY_AGENT_PAPER_SX, SUPPORT_RECOVERY_ALERT_SX, } from '../components/SupportRecoveryRequestsTab';
|
|
16
17
|
import { USER_INVITE_ACTION_SX, USER_INVITE_ALERT_SX } from '../components/UserInviteComponent';
|
|
17
18
|
import { USER_LIST_ACTION_SX, USER_LIST_ALERT_SX, USER_LIST_BODY_CELL_SX, USER_LIST_EMPTY_CELL_SX, USER_LIST_HEADER_CELL_SX, } from '../components/UserListComponent';
|
|
@@ -91,6 +92,9 @@ const SX_EXPORTS = {
|
|
|
91
92
|
SECURITY_DIVIDER_SX,
|
|
92
93
|
SECTION_NAV_DRAWER_PAPER_SX,
|
|
93
94
|
SECTION_NAV_GROUP_PAPER_SX,
|
|
95
|
+
SOFT_CHIP_CAVEAT_SX,
|
|
96
|
+
SOFT_CHIP_ROOT_SX,
|
|
97
|
+
SOFT_CHIP_STATUS_SX,
|
|
94
98
|
SIGNUP_CONFIRM_PAGE_ALERT_SX,
|
|
95
99
|
SIGN_UP_PAGE_ALERT_SX,
|
|
96
100
|
SUPPORT_RECOVERY_AGENT_BUTTON_SX,
|
|
@@ -151,6 +155,9 @@ export const KIT_COMPONENT_SX_REGISTRY = [
|
|
|
151
155
|
entry('RegistrationMethodsManager.info', 'MuiAlert', REGISTRATION_METHODS_INFO_SX),
|
|
152
156
|
entry('SecurityComponent.alert', 'MuiAlert', SECURITY_ALERT_SX),
|
|
153
157
|
entry('SecurityComponent.divider', 'MuiDivider', SECURITY_DIVIDER_SX),
|
|
158
|
+
entry('SoftChip.root', 'MuiChip', SOFT_CHIP_ROOT_SX),
|
|
159
|
+
entry('SoftChip.caveat', 'MuiChip', SOFT_CHIP_CAVEAT_SX),
|
|
160
|
+
entry('SoftChip.status', 'MuiChip', SOFT_CHIP_STATUS_SX),
|
|
154
161
|
entry('SupportRecoveryRequestsTab.agentPaper', 'MuiPaper', SUPPORT_RECOVERY_AGENT_PAPER_SX),
|
|
155
162
|
entry('SupportRecoveryRequestsTab.alert', 'MuiAlert', SUPPORT_RECOVERY_ALERT_SX),
|
|
156
163
|
entry('SupportRecoveryRequestsTab.agentAction', 'MuiButton', SUPPORT_RECOVERY_AGENT_BUTTON_SX),
|
|
@@ -757,6 +757,208 @@ export function reportOffPaletteColours(sources = [], { palette = BASELINE_PALET
|
|
|
757
757
|
}
|
|
758
758
|
return { findings };
|
|
759
759
|
}
|
|
760
|
+
function balancedBraceRegion(text, start) {
|
|
761
|
+
let depth = 0;
|
|
762
|
+
let quote = null;
|
|
763
|
+
for (let index = start; index < text.length; index += 1) {
|
|
764
|
+
const character = text[index];
|
|
765
|
+
if (quote) {
|
|
766
|
+
if (character === quote && text[index - 1] !== '\\')
|
|
767
|
+
quote = null;
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
if (character === '"' || character === "'" || character === '`') {
|
|
771
|
+
quote = character;
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
if (character === '{')
|
|
775
|
+
depth += 1;
|
|
776
|
+
else if (character === '}') {
|
|
777
|
+
depth -= 1;
|
|
778
|
+
if (depth === 0)
|
|
779
|
+
return text.slice(start, index + 1);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return text.slice(start);
|
|
783
|
+
}
|
|
784
|
+
function topLevelObjectEntries(objectText) {
|
|
785
|
+
const inner = objectText.slice(1, -1);
|
|
786
|
+
const entries = [];
|
|
787
|
+
let depth = 0;
|
|
788
|
+
let quote = null;
|
|
789
|
+
let entryStart = 0;
|
|
790
|
+
const push = (raw) => {
|
|
791
|
+
const trimmed = raw.trim();
|
|
792
|
+
if (trimmed)
|
|
793
|
+
entries.push(trimmed);
|
|
794
|
+
};
|
|
795
|
+
for (let index = 0; index < inner.length; index += 1) {
|
|
796
|
+
const character = inner[index];
|
|
797
|
+
if (quote) {
|
|
798
|
+
if (character === quote && inner[index - 1] !== '\\')
|
|
799
|
+
quote = null;
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
if (character === '"' || character === "'" || character === '`') {
|
|
803
|
+
quote = character;
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
if (character === '{' || character === '[' || character === '(')
|
|
807
|
+
depth += 1;
|
|
808
|
+
else if (character === '}' || character === ']' || character === ')')
|
|
809
|
+
depth -= 1;
|
|
810
|
+
else if (character === ',' && depth === 0) {
|
|
811
|
+
push(inner.slice(entryStart, index));
|
|
812
|
+
entryStart = index + 1;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
push(inner.slice(entryStart));
|
|
816
|
+
return entries;
|
|
817
|
+
}
|
|
818
|
+
function splitSxEntry(entry) {
|
|
819
|
+
let depth = 0;
|
|
820
|
+
let quote = null;
|
|
821
|
+
for (let index = 0; index < entry.length; index += 1) {
|
|
822
|
+
const character = entry[index];
|
|
823
|
+
if (quote) {
|
|
824
|
+
if (character === quote && entry[index - 1] !== '\\')
|
|
825
|
+
quote = null;
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
if (character === '"' || character === "'" || character === '`') {
|
|
829
|
+
quote = character;
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
if (character === '{' || character === '[' || character === '(')
|
|
833
|
+
depth += 1;
|
|
834
|
+
else if (character === '}' || character === ']' || character === ')')
|
|
835
|
+
depth -= 1;
|
|
836
|
+
else if (character === ':' && depth === 0) {
|
|
837
|
+
return { rawKey: entry.slice(0, index).trim(), rawValue: entry.slice(index + 1).trim() };
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
return null;
|
|
841
|
+
}
|
|
842
|
+
const SIMPLE_IDENTIFIER_PATTERN = /^[A-Za-z_$][\w$]*$/;
|
|
843
|
+
const QUOTED_STRING_PATTERN = /^(['"])((?:\\.|(?!\1)[\s\S])*)\1$/;
|
|
844
|
+
const NUMERIC_LITERAL_PATTERN = /^-?\d+(?:\.\d+)?$/;
|
|
845
|
+
/**
|
|
846
|
+
* Reads a top-level `sx={{ ... }}` object literal on a baseline-styled tag
|
|
847
|
+
* and returns its simple `{ property, value }` entries: identifier keys only
|
|
848
|
+
* (a quoted key is a nested selector like `'&:hover'` and is skipped, not a
|
|
849
|
+
* property), values that are a plain quoted string or bare number only. A
|
|
850
|
+
* template literal, identifier reference, spread, or function call is out of
|
|
851
|
+
* scope by design -- this is a lower bound, not a JS evaluator.
|
|
852
|
+
*/
|
|
853
|
+
function literalSxEntries(sxValueText) {
|
|
854
|
+
if (!/^\{\s*\{/.test(sxValueText))
|
|
855
|
+
return [];
|
|
856
|
+
const objectText = balancedBraceRegion(sxValueText, sxValueText.indexOf('{', 1));
|
|
857
|
+
const entries = [];
|
|
858
|
+
for (const rawEntry of topLevelObjectEntries(objectText)) {
|
|
859
|
+
const split = splitSxEntry(rawEntry);
|
|
860
|
+
if (!split)
|
|
861
|
+
continue;
|
|
862
|
+
const { rawKey, rawValue } = split;
|
|
863
|
+
if (!SIMPLE_IDENTIFIER_PATTERN.test(rawKey))
|
|
864
|
+
continue;
|
|
865
|
+
const quoted = rawValue.match(QUOTED_STRING_PATTERN);
|
|
866
|
+
if (quoted) {
|
|
867
|
+
entries.push({ property: rawKey, value: quoted[2] });
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
if (NUMERIC_LITERAL_PATTERN.test(rawValue)) {
|
|
871
|
+
entries.push({ property: rawKey, value: Number(rawValue) });
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
return entries;
|
|
875
|
+
}
|
|
876
|
+
// MUI's sx system multiplies a bare NUMBER by a theme scale factor for these
|
|
877
|
+
// properties (`theme.shape.borderRadius` for borderRadius, `theme.spacing()`
|
|
878
|
+
// for every spacing alias) before it ever reaches the DOM -- a raw
|
|
879
|
+
// `styleOverrides.root` value is already-resolved CSS, unscaled. Comparing
|
|
880
|
+
// the two numbers directly would be a false positive (e.g. `borderRadius: 8`
|
|
881
|
+
// in sx renders as `theme.shape.borderRadius * 8`, not `8`), and false
|
|
882
|
+
// positives are the failure mode this whole check exists to avoid. Numeric
|
|
883
|
+
// literals on these properties are therefore skipped, not compared -- a
|
|
884
|
+
// false negative (this check's accepted lower bound) is the safe direction
|
|
885
|
+
// to err in, a false positive is not.
|
|
886
|
+
const MUI_NUMERIC_SCALE_PROPERTIES = new Set([
|
|
887
|
+
'borderRadius',
|
|
888
|
+
...Object.keys(SPACING_SHORTHAND_LONGHANDS).filter((key) => key !== 'bgcolor' && key !== 'typography'),
|
|
889
|
+
]);
|
|
890
|
+
/**
|
|
891
|
+
* Resolves what an sx value would actually render as, for comparison against
|
|
892
|
+
* a theme styleOverride value already resolved to a real CSS value. A string
|
|
893
|
+
* sx value is tried as a `theme.palette.<value>` token path first (MUI's own
|
|
894
|
+
* resolution for palette-aware sx properties, e.g. `borderColor: 'divider'`
|
|
895
|
+
* -> `theme.palette.divider`); if that path is undefined, the raw string is
|
|
896
|
+
* used as-is (a plain CSS keyword like `'none'` is not a token path).
|
|
897
|
+
*/
|
|
898
|
+
function resolveSxValue(theme, value) {
|
|
899
|
+
if (typeof value !== 'string')
|
|
900
|
+
return value;
|
|
901
|
+
const paletteValue = getPath(theme, `palette.${value}`);
|
|
902
|
+
return paletteValue !== undefined ? paletteValue : value;
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* Reports an app's own `sx` value that already equals what its theme
|
|
906
|
+
* resolves for that MUI component and property -- not "you overrode the
|
|
907
|
+
* baseline" (in an app, that is the intended mechanism), but "you re-stated
|
|
908
|
+
* a value you already had", which silently diverges the next time the token
|
|
909
|
+
* changes. Attribution is by JSX tag name against the same
|
|
910
|
+
* `BASELINE_STYLED_MUI_COMPONENTS` list `reportKitSxBypasses` uses, checked
|
|
911
|
+
* against `theme.components.Mui<Tag>.styleOverrides.root.<property>` only
|
|
912
|
+
* (no other slot). Report-only; there is no exemption arm yet -- each
|
|
913
|
+
* adopting app makes this a hard assertion in its own follow-up WO once it
|
|
914
|
+
* has acted on the findings.
|
|
915
|
+
*
|
|
916
|
+
* This is a lower bound, and the bound is open, not enumerated: a `styled()`
|
|
917
|
+
* component, a component rendered through `component={...}` (so its JSX tag
|
|
918
|
+
* name is not the MUI component it becomes), an aliased import
|
|
919
|
+
* (`import { Paper as Surface }`), array/callback/conditional `sx`, and a
|
|
920
|
+
* template-literal or identifier-reference value are all unattributed or
|
|
921
|
+
* unresolved by this method. A numeric `sx` value on a property MUI scales
|
|
922
|
+
* by a theme factor (`borderRadius`, every spacing alias) is deliberately
|
|
923
|
+
* SKIPPED rather than compared -- see `MUI_NUMERIC_SCALE_PROPERTIES` -- since
|
|
924
|
+
* comparing the raw numbers would be a false positive, not a missed finding.
|
|
925
|
+
* A clean result means "nothing found by this method", not "no redundant
|
|
926
|
+
* value exists".
|
|
927
|
+
*/
|
|
928
|
+
export function reportRedundantThemeValues(sources = [], { theme } = {}) {
|
|
929
|
+
const findings = [];
|
|
930
|
+
if (!theme)
|
|
931
|
+
return { findings };
|
|
932
|
+
const componentPattern = BASELINE_STYLED_MUI_COMPONENTS.join('|');
|
|
933
|
+
const tagPattern = new RegExp(`<(${componentPattern})\\b`, 'g');
|
|
934
|
+
for (const { path, source } of normalizedSources(sources)) {
|
|
935
|
+
const scanSource = maskComments(maskQrPrintDocument(source, path.replaceAll('\\', '/')));
|
|
936
|
+
for (const match of scanSource.matchAll(tagPattern)) {
|
|
937
|
+
const tagName = match[1];
|
|
938
|
+
const tag = jsxOpeningTag(scanSource, match.index);
|
|
939
|
+
const sxValueText = topLevelSxValue(tag);
|
|
940
|
+
const line = source.slice(0, match.index).split(/\r?\n/).length;
|
|
941
|
+
for (const { property, value } of literalSxEntries(sxValueText)) {
|
|
942
|
+
if (typeof value === 'number' && MUI_NUMERIC_SCALE_PROPERTIES.has(property))
|
|
943
|
+
continue;
|
|
944
|
+
const [canonicalProperty] = expandShorthandProperty(property);
|
|
945
|
+
const lookupProperty = expandShorthandProperty(property).length === 1
|
|
946
|
+
? canonicalProperty
|
|
947
|
+
: property;
|
|
948
|
+
const themeValue = getPath(theme, `components.Mui${tagName}.styleOverrides.root.${lookupProperty}`);
|
|
949
|
+
if (themeValue === undefined)
|
|
950
|
+
continue;
|
|
951
|
+
if (sameValue(resolveSxValue(theme, value), themeValue)) {
|
|
952
|
+
findings.push({
|
|
953
|
+
surface: `${path}:${line}.Mui${tagName}.${property}`,
|
|
954
|
+
reason: `Mui${tagName}'s sx sets "${property}" to a value the theme already resolves to the same thing.`,
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return { findings };
|
|
961
|
+
}
|
|
760
962
|
/** Reports adoption signals as numbers; it never changes completeness. */
|
|
761
963
|
export function reportThemeAdoption(sources = []) {
|
|
762
964
|
const normalized = sources.map((entry, index) => typeof entry === 'string'
|
package/package.json
CHANGED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { Box, Chip, Tooltip, Typography } from '@mui/material';
|
|
2
|
+
import { alpha, darken, useTheme } from '@mui/material/styles';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A soft tinted annotation chip -- two variants (`caveat`, `status`), four semantic tones. Colour
|
|
6
|
+
* is derived from `theme.palette[tone]` (fill `alpha(main, .12)`, border `alpha(main, .30)`, text
|
|
7
|
+
* and dot `darken(main, .3)`) rather than restated as a literal, so all four tones track the theme
|
|
8
|
+
* and pass AA against the blended fill.
|
|
9
|
+
*
|
|
10
|
+
* Promoted from two independent builds (hram's `ResearchComponents/SoftChip.jsx`, the inline lane
|
|
11
|
+
* pill in cockpit's `BoardView.jsx`). `status` uses the baseline's `overline` typography variant --
|
|
12
|
+
* the second build (cockpit) had already converged on 11/600, uppercase, .4px tracking, which is
|
|
13
|
+
* `overline` in all but the weight step, so this satisfies the kit's shadow-check without a new
|
|
14
|
+
* baseline variant. `caveat` keeps its raw 12/500 -- there is no baseline variant for it (`caption`
|
|
15
|
+
* is 12/400) and adding one is a baseline decision that does not belong in this promotion; the
|
|
16
|
+
* corresponding kitSxRegistry entry carries a stated exemption rather than a silent one.
|
|
17
|
+
*/
|
|
18
|
+
const TINT = 0.12;
|
|
19
|
+
const BORDER_TINT = 0.3;
|
|
20
|
+
|
|
21
|
+
export const SOFT_CHIP_ROOT_SX = {
|
|
22
|
+
height: 'auto',
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const SOFT_CHIP_CAVEAT_SX = {
|
|
26
|
+
borderRadius: 1,
|
|
27
|
+
fontSize: 12,
|
|
28
|
+
fontWeight: 500,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const SOFT_CHIP_STATUS_SX = {
|
|
32
|
+
borderRadius: '999px',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export default function SoftChip({
|
|
36
|
+
label,
|
|
37
|
+
tone = 'warning',
|
|
38
|
+
variant = 'caveat',
|
|
39
|
+
icon,
|
|
40
|
+
border = true,
|
|
41
|
+
title,
|
|
42
|
+
...rest
|
|
43
|
+
}) {
|
|
44
|
+
const theme = useTheme();
|
|
45
|
+
const palette = theme.palette[tone] ?? theme.palette.warning;
|
|
46
|
+
const isStatus = variant === 'status';
|
|
47
|
+
const dotColor = darken(palette.main, 0.3);
|
|
48
|
+
|
|
49
|
+
const chip = (
|
|
50
|
+
<Chip
|
|
51
|
+
size="small"
|
|
52
|
+
label={isStatus ? (
|
|
53
|
+
<Typography variant="overline" component="span" sx={{ color: 'inherit' }}>
|
|
54
|
+
{label}
|
|
55
|
+
</Typography>
|
|
56
|
+
) : label}
|
|
57
|
+
icon={icon ?? (
|
|
58
|
+
<Box
|
|
59
|
+
component="span"
|
|
60
|
+
sx={{
|
|
61
|
+
width: 6,
|
|
62
|
+
height: 6,
|
|
63
|
+
borderRadius: '50%',
|
|
64
|
+
bgcolor: dotColor,
|
|
65
|
+
flex: 'none',
|
|
66
|
+
}}
|
|
67
|
+
/>
|
|
68
|
+
)}
|
|
69
|
+
sx={[
|
|
70
|
+
SOFT_CHIP_ROOT_SX,
|
|
71
|
+
isStatus ? SOFT_CHIP_STATUS_SX : SOFT_CHIP_CAVEAT_SX,
|
|
72
|
+
{
|
|
73
|
+
bgcolor: alpha(palette.main, TINT),
|
|
74
|
+
color: darken(palette.main, 0.3),
|
|
75
|
+
border: '1px solid',
|
|
76
|
+
borderColor: border ? alpha(palette.main, BORDER_TINT) : 'transparent',
|
|
77
|
+
cursor: title ? 'help' : 'default',
|
|
78
|
+
'& .MuiChip-label': { px: 1, py: 0.5 },
|
|
79
|
+
'& .MuiChip-icon': { ml: 1, mr: -0.5 },
|
|
80
|
+
},
|
|
81
|
+
]}
|
|
82
|
+
{...rest}
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
return title ? (
|
|
87
|
+
<Tooltip title={title} enterTouchDelay={0}>
|
|
88
|
+
{chip}
|
|
89
|
+
</Tooltip>
|
|
90
|
+
) : (
|
|
91
|
+
chip
|
|
92
|
+
);
|
|
93
|
+
}
|