@micha.bigler/ui-core-micha 2.34.0 → 2.36.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 (37) hide show
  1. package/WORK_ORDERS.md +17 -3
  2. package/dev/entries.jsx +79 -0
  3. package/dev/main.jsx +7 -1
  4. package/dist/components/MFAComponent.js +7 -6
  5. package/dist/components/SocialLoginButtons.js +2 -1
  6. package/dist/components/charts/ChartFrame.js +3 -1
  7. package/dist/components/charts/exportChart.js +2 -2
  8. package/dist/i18n/authTranslations.js +24 -0
  9. package/dist/i18n/sectionNavTranslations.js +20 -0
  10. package/dist/index.js +3 -1
  11. package/dist/layout/SectionNav.js +138 -0
  12. package/dist/pages/AccountPage.js +44 -2
  13. package/dist/theme/index.js +1 -1
  14. package/dist/theme/kitSxRegistry.js +8 -1
  15. package/dist/theme/themeCompleteness.js +218 -14
  16. package/package.json +1 -1
  17. package/src/components/MFAComponent.jsx +7 -6
  18. package/src/components/SocialLoginButtons.jsx +2 -1
  19. package/src/components/charts/ChartFrame.jsx +7 -1
  20. package/src/components/charts/exportChart.js +2 -2
  21. package/src/i18n/authTranslations.ts +24 -0
  22. package/src/i18n/sectionNavTranslations.ts +20 -0
  23. package/src/index.js +3 -0
  24. package/src/layout/SectionNav.jsx +299 -0
  25. package/src/pages/AccountPage.jsx +55 -14
  26. package/src/theme/index.js +1 -0
  27. package/src/theme/kitSxRegistry.js +11 -1
  28. package/src/theme/themeCompleteness.js +237 -14
  29. package/tests/AccountPage.test.jsx +193 -0
  30. package/tests/ChartFrame.test.jsx +5 -0
  31. package/tests/SectionNav.test.jsx +274 -0
  32. package/tests/themeCompleteness.test.js +156 -4
  33. package/work-orders/AUTH-4.md +183 -0
  34. package/work-orders/I18N-1.md +254 -0
  35. package/work-orders/SHELL-5.md +345 -0
  36. package/work-orders/THEME-5.md +341 -0
  37. package/work-orders/assets/AUTH-4-user-list-card.html +407 -0
@@ -0,0 +1,254 @@
1
+ # I18N-1 — One export, so a consumer cannot half-register the kit's strings
2
+
3
+ **Target repo:** `ui-core-micha` (branch `main`)
4
+ **Tier:** 3 — shared-core, and it adds a public export every consumer will depend on
5
+ **Target version:** `2.36.0` — minor. A new capability, fully backwards compatible.
6
+ **Prototype:** none. Nothing visual changes.
7
+
8
+ ---
9
+
10
+ ## A. Envelope
11
+
12
+ ### Goal
13
+
14
+ `ui-core-micha` exports its UI strings as **six separate bundles** that each consuming app must
15
+ spread into its own i18n resources by hand. Adopting a component without also adding its bundle
16
+ produces **a raw key rendered as visible UI text** — silently, with no type error and no runtime
17
+ error. Ship one aggregate export so that cannot happen, and so a *future* ucm feature is carried
18
+ automatically instead of requiring twelve apps to notice.
19
+
20
+ ### The evidence
21
+
22
+ **Two apps hit by the same mechanism, one of them still live.**
23
+
24
+ - **cockpit, 2026-08-11 (`UI-3`).** Adopted `UserMenu`; the matching `userMenuTranslations` was not
25
+ spread, and the header button rendered the literal string `UserMenu.TITLE`. **Both independent
26
+ static reviews were clean** — the defect is structurally invisible to `Read`/`Grep` review. It
27
+ was caught by the rendered click-through, and only by DOM inspection at that, because screenshot
28
+ capture was unavailable in that session. Fixed there.
29
+ - **fitness-monitor, live now.** Imports `ChartFrame` (`frontend/src/pages/BodyHistoryPage.jsx:7`,
30
+ used at `:295` and `:445`) but registers **only** `authTranslations`
31
+ (`frontend/i18n/index.js`), and carries no `ChartFrame.*` key of its own. So
32
+ `ChartFrame.jsx:74`'s `t('ChartFrame.LOADING')` and its sibling empty/error states render their
33
+ raw keys. Nobody noticed because those are transient edge states — which is exactly the class
34
+ `DESIGN.md` #7 says must be designed rather than left to chance.
35
+
36
+ **The estate survey, measured 2026-08-11.** Twelve apps register ucm bundles:
37
+
38
+ | App | Bundles registered | ucm components used beyond auth | Verdict |
39
+ |---|---|---|---|
40
+ | jg-ferien | all six + surveyRenderer | Bell, Settings, Wizard, UserMenu | correct |
41
+ | cockpit | auth, notifications, onboarding, userMenu | Bell, Settings, Wizard, UserMenu | correct (since `UI-3`) |
42
+ | hram | auth, charts, userMenu | ChartFrame, UserMenu | correct |
43
+ | **fitness-monitor** | **auth only** | **ChartFrame** | **defective** |
44
+ | spesix, survey_app, kerzenziehen, webshop-guenter, webapp-template, hpc-bridge, bigler-consult, survey_contact_app | auth only | none | correct today, exposed the moment they adopt anything |
45
+
46
+ Eight of the twelve are correct only because they have not yet adopted a second component. That is
47
+ not a safety property; it is a coincidence with an expiry date.
48
+
49
+ **The merge is safe.** All six bundles together hold **504 keys with zero collisions**, verified by
50
+ enumerating them; every key is namespace-prefixed (`Auth.*`, `ChartFrame.*`, `Messaging*.*`,
51
+ `NotificationBell.*`, `Onboarding.*`, `UserMenu.*`, plus the bare allauth error codes
52
+ `AUTH-2` deals with). A flat aggregate therefore changes no resolution today.
53
+
54
+ ### Scope
55
+
56
+ 1. **Export `uiCoreTranslations`** from the package root — the flat merge of all six bundles.
57
+ Consumers spread one thing instead of six.
58
+ 2. **Keep the six per-feature exports.** Removing them would break twelve apps for no gain; this
59
+ work order is additive. An app may spread both during migration — harmless, because the values
60
+ are identical and there are no collisions.
61
+ 3. **A ratchet test, and this is the part that makes the fix permanent.** Assert that the aggregate
62
+ contains every key of every bundle in `src/i18n/`, **enumerated from the directory rather than
63
+ from a hand-written list**. A new bundle added later that is not folded into the aggregate must
64
+ fail this test. Without that, the aggregate is correct today and rots the same way the six
65
+ exports did.
66
+ 4. **A collision test.** Assert the union has no duplicate key across bundles. It holds today at
67
+ 504 keys; the test is what stops a future bundle from silently shadowing an existing string.
68
+ 5. **Document the contract** where a consumer will actually meet it: one line in the README's i18n
69
+ section saying that spreading `uiCoreTranslations` is the supported way, and that the
70
+ per-feature exports remain for apps that want to be selective and accept the pairing burden.
71
+
72
+ ### Non-goals / do not touch
73
+
74
+ - **Self-registering components.** Feasible — ucm components use `useTranslation()` against the
75
+ consumer's own instance, so a component could call `addResourceBundle` itself — but it is a
76
+ separate question and a separate work order (`I18N-2`), deliberately: all twelve consumers flatten
77
+ everything into the **default namespace** with dotted keys, so the registration shape has to match
78
+ exactly, and registering at render time risks a first-paint flash of the raw key. That would
79
+ replace an invisible defect with a visible one. Framed as a bundle-size optimisation, not a
80
+ correctness fix, because this work order already closes the correctness hole.
81
+ - **Namespaces**, and the flat dotted-key shape generally. Twelve apps depend on it.
82
+ - **The bare allauth error codes** in `authTranslations` — `AUTH-2` owns those.
83
+ - **Any consumer repository.** See the rollout note below.
84
+ - Removing or renaming any existing export.
85
+
86
+ ### Risks
87
+
88
+ - **Bundle size for the eight auth-only apps.** `authTranslations` is 316 keys on its own; the other
89
+ five add 188, of which `messagingTranslations` is 116. An auth-only app that switches to the
90
+ aggregate ships roughly 30 KB of source strings it never renders. Judged acceptable against a
91
+ defect class that has already fired twice — but **measure the built delta and report it**, because
92
+ that number is the whole input to `I18N-2`, and a guess there would make that decision
93
+ unfalsifiable.
94
+ - **The ratchet test can be faked.** A test that enumerates the directory but compares against a
95
+ re-export (`export * from './i18n'`) would pass while proving nothing. It must compare *keys of
96
+ the aggregate value* against *keys read from each bundle module*.
97
+ - **Consumers spreading both during migration.** Safe today because of the zero-collision property
98
+ — which is precisely why item 4 is a test and not a comment.
99
+ - A parallel session is active in this repo (`THEME-*` work landed today up to `2.35.0`). Check
100
+ `git status` at start and sequence.
101
+
102
+ ### Rollout — and the constraint that shapes it
103
+
104
+ **A work order implements in one repository.** The twelve consumers cannot be migrated from here,
105
+ so this work order ships the export and the rollout lands as named rows in the consuming repos.
106
+
107
+ - **`fitness-monitor/FM-14` is mandatory and is the reason this is not merely hygiene** — it is the
108
+ app with a live defect. It is written and blocked on this work order's release.
109
+ **State the exposure plainly: fitness-monitor renders raw keys until `FM-14` lands.** The operator
110
+ chose to route the fix through this rollout rather than patch it separately, so that it cannot be
111
+ lost between two work orders; the cost of that choice is the delay, and it is recorded here rather
112
+ than left implicit.
113
+ - The other eleven apps are **correct today**. Migrating them to the aggregate is prevention, not
114
+ repair, and it can ride along the next time each is touched. Do not open eleven rows now.
115
+
116
+ ### Required tests to WRITE
117
+
118
+ 1. `uiCoreTranslations` contains every key of every module in `src/i18n/`, with the module list read
119
+ from the directory, not hand-written.
120
+ 2. The union has no duplicate key across bundles (504 today; assert the property, not the number).
121
+ 3. Every value keeps its `{ de, fr, en, sw }` shape through the merge — no bundle is flattened or
122
+ partially spread.
123
+ 4. The six per-feature exports still exist and are unchanged, so no consumer breaks.
124
+
125
+ Run ucm's own affected set, not the full suite: the i18n tests plus whatever imports from
126
+ `src/index.js`.
127
+
128
+ ### Parity guardrail
129
+
130
+ Purely additive. No existing export changes, no key changes, no rendered output changes in any app
131
+ that is correct today.
132
+
133
+ ---
134
+
135
+ ## B. Implementation map
136
+
137
+ *Filled by the Orchestrator on `git pull` — see `AGENTS.md` → "Work Order".*
138
+
139
+ ### Context package
140
+
141
+ **Bundle files** (all in `src/i18n/`, each `export const <name>Translations = { 'Key.NAME': { de, fr,
142
+ en, sw }, ... }`, flat object, no nesting):
143
+
144
+ - `src/i18n/authTranslations.ts` → `authTranslations` (316 keys, includes bare allauth codes like
145
+ `email_password_mismatch` alongside `Auth.*` keys — still a flat top-level key, no special casing
146
+ needed for the merge)
147
+ - `src/i18n/chartsTranslations.ts` → `chartsTranslations` (`ChartFrame.*`)
148
+ - `src/i18n/messagingTranslations.ts` → `messagingTranslations` (`Messaging*.*`, 116 keys)
149
+ - `src/i18n/notificationsTranslations.ts` → `notificationsTranslations` (`NotificationBell.*`)
150
+ - `src/i18n/onboardingTranslations.ts` → `onboardingTranslations` (`Onboarding.*`)
151
+ - `src/i18n/userMenuTranslations.ts` → `userMenuTranslations` (`UserMenu.*`)
152
+
153
+ **Root export site:** `src/index.js` — the six per-feature translation exports currently sit at two
154
+ places: `export { authTranslations } ... export { userMenuTranslations } ...` under the `// --- 7.
155
+ Translations ---` comment (~line 78-79), and `export { notificationsTranslations } ...
156
+ onboardingTranslations ... chartsTranslations ... messagingTranslations` under `// --- 10.
157
+ Translations ---` (~line 121-124). Add the new `uiCoreTranslations` export near those, importing all
158
+ six bundle modules and merging with a plain object spread (`{ ...authTranslations, ...chartsTranslations,
159
+ ... }`) — order doesn't matter, zero-collision is already verified. Do NOT remove or move the existing
160
+ six exports.
161
+
162
+ **Test file to add:** `tests/i18nAggregate.test.js` (new), following the directory-enumeration pattern
163
+ already used in `tests/themeCompleteness.test.js` (`fs.readdirSync` over a directory, don't hand-write
164
+ the file list) and the flat-object assertion style in `tests/authTranslations.test.js`. Enumerate
165
+ `src/i18n/*.ts`, `require`/dynamic-`import()` each module, find its exported `*Translations` object
166
+ (the module's only export), and:
167
+ 1. assert every key of every bundle is present in `uiCoreTranslations` with an identical value
168
+ (deep-equal, not just key presence — catches a bundle being spread partially);
169
+ 2. assert the union of all bundle keys has no duplicates (build a `Map<key, sourceModule>`, fail with
170
+ the two colliding module names if a key is seen twice);
171
+ 3. assert every value in `uiCoreTranslations` has the `{ de, fr, en, sw }` shape (all four locale keys
172
+ present, non-empty strings) — this must run against the aggregate, not against the individual
173
+ bundles, or it doesn't actually check the merge preserved shape.
174
+ Import `uiCoreTranslations` from `../src/index` (the public export), not from a private merge module —
175
+ the ratchet only means something if it tests what a consumer actually imports.
176
+
177
+ **Do not touch:** the six existing named exports, any bundle file's content/keys, `src/theme/**`
178
+ (`THEME-*` work is active in parallel — check `git status`/`git log` before starting and do not touch
179
+ theme files even incidentally).
180
+
181
+ **README:** add one line to the i18n section (`grep -n "i18n" README.md` to find it) stating
182
+ `uiCoreTranslations` is the supported aggregate import and the six per-feature exports remain for
183
+ apps that want to stay selective.
184
+
185
+ **Bundle-size measurement:** after implementing, report the built size delta for an auth-only app
186
+ switching from `authTranslations` alone to `uiCoreTranslations` — e.g. `du -h` or a byte count on the
187
+ relevant JSON/object literal, or diff `dist/index.js` chunk sizes before/after a `pnpm build` if that's
188
+ cheaper than hand-measuring. A rough KB figure is enough; this number feeds `I18N-2`, not this WO's
189
+ acceptance.
190
+
191
+ ### Target repo working directory (absolute)
192
+
193
+ `C:\Users\biglmi\Documents\webapps\ui-core-micha` — the repo root (no backend/frontend split here).
194
+
195
+ ### Preamble
196
+
197
+ > The text above is the COMPLETE spec — the committed WO file's content, not a plan to refine; there
198
+ > is no separate plan file. Read the nearest `AGENTS.md`, the relevant `.codex/skills/<role>/SKILL.md`,
199
+ > and the app `MEMORY.md` ONLY for conventions. Stay in scope; do not touch auth/permissions/deps/
200
+ > schema/CI unless the spec says so; do not update `MEMORY.md`. **Do NOT edit `WORK_ORDERS.md` — the
201
+ > register row and the review verdicts are the orchestrator's alone.** Do NOT `git add`/`commit`/
202
+ > `push` — leave every change uncommitted in the working tree for the orchestrator's independent
203
+ > review. WRITE the tests the `Required tests` section calls for AND **RUN the tests you just wrote**
204
+ > to confirm they execute and pass — that is the ONLY test run you do (NOT the app's affected/full
205
+ > suite, NOT any review). The orchestrator re-runs the authoritative set + does the independent review
206
+ > after you finish — those are the gate; your own run does not count as the gate.
207
+ >
208
+ > Narrate continuously: a `PLAN: <step1> | <step2> | …` line up front, then a single-line
209
+ > `PROGRESS: [<n>/<total>] <present-tense action>` before every relevant action (and `… done` on
210
+ > completion), spaced so no gap exceeds ~2 min, stdout unbuffered, plus exactly one final
211
+ > `RESULT: DONE|BLOCKED <reason>`.
212
+
213
+ ### Mini-handover
214
+
215
+ Repo: `ui-core-micha` (`C:\Users\biglmi\Documents\webapps\ui-core-micha`), branch `main`.
216
+ WO: `work-orders/I18N-1.md`. Target `2.36.0`. Downstream: `fitness-monitor/FM-14` is blocked on the
217
+ release and is the acceptance condition. Follow `orchestrate-codex`.
218
+
219
+ ---
220
+
221
+ ## C. Orchestrator only — NOT ADDRESSED TO THE IMPLEMENTER
222
+
223
+ > **If you are the implementer reading this work order as your own specification: STOP at this line.
224
+ > Everything below describes what the Orchestrator does AFTER you finish. You do none of it — no
225
+ > reviewers, no verification run, no register edit, no commit.** You ARE the invocation described
226
+ > below; do NOT shell out to `codex exec`.
227
+
228
+ ### Execution directive
229
+
230
+ Implement through `codex exec` in the background — invoked directly via Bash (never the
231
+ `debugger`/`*_coder` Agent wrappers) with BOTH flags `--skip-git-repo-check` and
232
+ `--dangerously-bypass-approvals-and-sandbox`. Fallback to direct Claude implementation only on Codex
233
+ quota/rate-limit/non-zero exit — the fallback flips authorship, so an independent reviewer becomes
234
+ mandatory.
235
+
236
+ ### Review routing
237
+
238
+ Independent `reviewer` **and** `ui_reviewer`, concurrent, same background batch. The `ui_reviewer`
239
+ pass is a formality here — the diff renders nothing — but Tier 3 asks for it on a frontend-touching
240
+ diff and a shared-core work order is the wrong place to argue an exemption.
241
+
242
+ ### Verification
243
+
244
+ No rendered check — this work order renders nothing. The verification that matters is `FM-14`'s:
245
+ `ChartFrame`'s loading and empty states in fitness-monitor must show real text. That is this work
246
+ order's real acceptance condition even though it is executed in another repo — record in the register
247
+ Notiz that `FM-14` is the pending downstream confirmation, so the export shipping is never mistaken
248
+ for the defect being fixed.
249
+
250
+ ### Register + commit
251
+
252
+ `WORK_ORDERS.md` row for `I18N-1`: `done` only once `reviewer` + `ui_reviewer` verdicts are named,
253
+ the affected-set tests pass, and the bundle-size delta is recorded in the Notiz. Commit to `main`
254
+ (this repo's trunk) on green.
@@ -0,0 +1,345 @@
1
+ # SHELL-5 — Promote the secondary-navigation shell
2
+
3
+ **Target repo:** `ui-core-micha` (branch `main` — this repo has no `develop`)
4
+ **Tier:** 3 (shared core)
5
+ **Review:** independent `reviewer` **and** `ui_reviewer` — both mandatory, concurrent
6
+ **Version target:** the next **minor** after `THEME-5` — a new exported component is additive API.
7
+ Read the published version at implementation time; do **not** hardcode a number.
8
+ **Prototype:** `work-orders/assets/SHELL-2-account-navigation.html` — the **composition** spec. The
9
+ two-width rendered side-by-side is therefore a commit gate.
10
+ **Strand:** `DS-11`. **Supersedes `SHELL-2`**, whose row moves to `dropped` — it described a different
11
+ shape (inline in `AccountPage`, no promotion) and its measurements are superseded by the ones below.
12
+ **Follow-ups, both named now:** `jg-ferien` migrates onto this and **deletes its own drawer**
13
+ (operator requirement); the `AccountPage` users table is `AUTH-4`, deliberately not here.
14
+
15
+ ---
16
+
17
+ ## A. Envelope
18
+
19
+ ### Goal
20
+
21
+ After the primary destinations (the bottom bar, `SHELL-3`) comes the choice *within* a section.
22
+ Two apps solve it separately: jg-ferien has `SectionSwitcher` (119 lines, grouped list + bottom
23
+ drawer), and ucm's own `AccountPage` uses a horizontal `Tabs` strip that fails on a phone. Promote
24
+ jg's shape, make `AccountPage` its first consumer, and let jg's copy be deleted afterwards.
25
+
26
+ **Look and feel follow jg-ferien, by operator decision.** This is not a redesign — it is a promotion
27
+ plus one new consumer.
28
+
29
+ ### The driver, measured 2026-08-11 in cockpit at the current baseline
30
+
31
+ Re-measured because `SHELL-2`'s numbers were three days old. All figures at **`innerWidth` 411 CSS px**
32
+ — the "mobile" preset yields 411, not 375, so **at the real 375 target every number is worse**.
33
+
34
+ | | measured |
35
+ |---|---|
36
+ | Sections rendered | **6** — Profile, Security, Users, Invite, Support, Notifications (the last is the app's `extraTab`) |
37
+ | Strip container | 327 px |
38
+ | Strip content | 566 px → **239 px overflow** |
39
+ | Off the visible edge | **3 of 6** — Invite, Support, Notifications |
40
+ | Scroll buttons | **none**; not in `variant="scrollable"` |
41
+ | Scroller `overflow-x` | `auto` — so it **is** swipeable |
42
+ | Desktop 1280 | 566 px used of 1136 px → **570 px headroom, zero overflow** |
43
+
44
+ **Two corrections to the recorded claim, both from this measurement:**
45
+
46
+ 1. **The sections are not unreachable.** `overflow-x: auto` means a horizontal swipe reaches them. The
47
+ precise defect is that **there is no affordance whatsoever** — no arrows, no edge fade, and no
48
+ scrollbar on a touch pointer. It is a discoverability failure, not an unreachability one. Say it
49
+ that way; the stronger claim would not survive a reviewer.
50
+ 2. **The desktop is not broken.** 570 px of headroom, and at ~94 px per tab the strip would only fill
51
+ at about **twelve** sections. So the grouped sidebar on desktop is a **structural improvement, not
52
+ a space fix** — it must be argued as organisation and scanability, never as "it does not fit".
53
+
54
+ **Why a drawer rather than a better strip:** the section count is **not fixed and not knowable at
55
+ design time.** `AccountPage` builds it from permissions — Profile and Security always, then Users,
56
+ Invite, Support conditionally — plus whatever `extraTabs` the app injects (measured: cockpit 1,
57
+ fitness-monitor 1, **jg 3**, so a jg superuser sees up to **eight**). No "fit N tabs" solution can be
58
+ correct against a variable, permission-dependent count. That argument, not the 375 px measurement, is
59
+ the load-bearing one.
60
+
61
+ ### Scope
62
+
63
+ #### Part 1 — the component
64
+
65
+ Export `SectionNav` (name it whatever the kit's convention prefers; the point is that it is one
66
+ component with a `mode`, not two). Derived from `jg-ferien/frontend/src/components/EventInfoHub/SectionSwitcher.jsx`,
67
+ read at `e551ff7`.
68
+
69
+ | Prop | Meaning |
70
+ |---|---|
71
+ | `mode` | `'desktop'` (sticky grouped sidebar) or `'mobile'` (bottom drawer) |
72
+ | `groups` | `[{ key, label, items: [{ key, label }] }]` — labels are **finished strings**, as `SHELL-1`, `SHELL-3` and `DS-11` already settled |
73
+ | `activeKey` / `onSelect` | the app derives the active key; the component does not guess |
74
+ | `open` / `onClose` | drawer only |
75
+ | `title` | drawer heading |
76
+ | `overviewItem` | **optional** `{ label }` — renders jg's special first entry, selected when `activeKey == null` |
77
+ | `rememberedKey` | **optional** — the "last opened" secondary line |
78
+ | `headerOffset` | sticky top offset in the desktop mode |
79
+ | `zIndex` | drawer only, default `theme.zIndex.drawer + 3` |
80
+ | `sidebarWidth` | default `280` |
81
+
82
+ **Each jg-specific concept becomes optional, and `AccountPage` is what proves it optional** — the same
83
+ argument structure that worked in `SHELL-3`:
84
+
85
+ - **The "Übersicht" entry is hardcoded today** (`SectionSwitcher.jsx:27-30`) as a separate `Paper` with
86
+ one `ListItemButton`, selected when `activeKey == null`. That is jg's event-overview model.
87
+ `AccountPage` has no such concept → `overviewItem`, omitted there.
88
+ - **`rememberedKey`** and its "Zuletzt geöffnet" secondary line (`:50-53`) are jg-only.
89
+ `AccountPage` omits it.
90
+ - **`APP_HEADER_HEIGHT`** is imported from jg's own `mobileShell/constants` (`:13`, used at `:79` as
91
+ `top: APP_HEADER_HEIGHT + 24`). ucm cannot know a host's header height → `headerOffset`.
92
+ - **`zIndex.drawer + 3`** is hardcoded (`:95`). Keep **+3 as the default and document why**: `SHELL-3`'s
93
+ bottom bar sits at `+2`, so the drawer must render above it. The ordering is meaningful, not
94
+ arbitrary — a consumer that lowers it will put the drawer under the bar.
95
+ - **The 280 px sidebar and the two-column grid live in the *consumer* today**
96
+ (`EventInfoPage.jsx:1146`, `gridTemplateColumns: { md: "280px minmax(0, 1fr)" }`). **The component
97
+ takes it over** and renders `children` beside the sidebar, so no consumer re-derives the desktop
98
+ layout. This is the one place the promotion widens jg's component rather than copying it.
99
+ - **German literal fallbacks go.** `t("EventInfoPage.OVERVIEW", "Übersicht")` and
100
+ `t("EventInfoPage.LAST_OPENED_VIEW", "Zuletzt geöffnet")` disappear with finished-string labels.
101
+ Note `t("SectionSwitcher.TITLE")` at `:75` has **no fallback at all** — in the kit that key must
102
+ exist per the `SHELL-1` pattern (`userMenuTranslations`) or the raw key renders on screen.
103
+
104
+ Keep from jg unchanged: the `18` px drawer top radius, `78dvh` max height, `p: 2`, and
105
+ `pb: max(16px, env(safe-area-inset-bottom))` — the safe-area handling is **already correct there**.
106
+
107
+ #### Part 2 — the trigger, which is net-new
108
+
109
+ jg has **no trigger bar** — verified, its trigger sits in the page header. The 56 px trigger bar is
110
+ specified only by `SHELL-2`'s prototype. It is therefore the one genuinely new piece, and it is what
111
+ supplies the missing affordance the measurement found. Build it as part of this component (a
112
+ consumer must not have to invent the thing that makes the drawer discoverable).
113
+
114
+ #### Part 3 — `AccountPage` as the first consumer
115
+
116
+ Replace the `Tabs` strip: grouped sidebar at `md` and up, trigger bar plus drawer below it. The
117
+ existing `?tab=<value>` query param and every permission gate stay exactly as they are —
118
+ `AccountPage` keeps computing which sections exist, the shell only presents them.
119
+
120
+ **Grouping is a real decision, not a mechanical wrap.** Six to eight flat sections must become named
121
+ groups; the prototype's composition is the spec. If it does not cover a section an app injects via
122
+ `extraTabs`, those land in a final group — do not invent a name for someone else's tab.
123
+
124
+ ### Non-goals / do not touch
125
+
126
+ - **The users table** — `AUTH-4`. Measured today: 1053 px inside a 325 px container, the container
127
+ scrolls, and **the document does not overflow**. Six fixed-width desktop columns on a phone is a
128
+ content-density problem with a different fix class, and bundling it here would put an unbounded
129
+ redesign into a promotion. `SHELL-2`'s 1045 px from 2026-08-08 still holds (+8 px) — cell padding is
130
+ already the baseline's `10px 16px`, so `THEME-4`/`THEME-5` did not move it.
131
+ - **jg-ferien's migration** — its own follow-up WO, which is what makes this a removal of duplication
132
+ rather than an addition.
133
+ - `AccountPage`'s permission logic, its `extraTabs` contract, and every section's content.
134
+ - The bottom bar (`SHELL-3`/`SHELL-4`) and `UserMenu` (`SHELL-1`).
135
+ - No baseline token change. The shell consumes tokens; it introduces none, so there is **no token
136
+ delta list** — the prototype predates the design language, but `AccountPage` renders against the
137
+ **host** theme, so composition comes from the prototype and every value from the app.
138
+
139
+ ### Risks
140
+
141
+ - **No staging net.** A version bump on `main` publishes; the two reviews are the only gate.
142
+ - **`AccountPage` is mounted by at least ten apps** — bigler-consult, cockpit, fitness-monitor,
143
+ hpc-bridge, hram, innoservice, jg-ferien, kerzenziehen, reimbursements. This changes the account
144
+ screen's navigation everywhere at once. There is no way to stage it; the rendered gate and the
145
+ reviews carry it.
146
+ - **The safe-area padding cannot be verified.** A desktop 375 px viewport always reports an inset of
147
+ zero — the wall `TPL-1` hit. jg's value is already correct; assert the CSS and record it unverified.
148
+ - **Screenshot capture has failed repeatedly in this estate.** If it fails again, DOM/computed-style
149
+ inspection is the declared substitution — and say so, because it can confirm a token resolved but
150
+ not whether a screen reads well.
151
+ - **The trigger bar is the only unproven piece.** Everything else is promotion of running code.
152
+
153
+ ### Required tests to WRITE
154
+
155
+ 1. `groups` renders one section per item, grouped, in order, with finished labels.
156
+ 2. `activeKey` selects exactly the matching item; a key matching nothing selects none and does not
157
+ throw; `activeKey == null` selects `overviewItem` **when it is supplied**.
158
+ 3. `overviewItem` and `rememberedKey` omitted → neither renders, no empty `Paper`, no stray secondary
159
+ line. **This is the "AccountPage proves them optional" test.**
160
+ 4. `onSelect` fires with the item's key; selecting in the drawer closes it.
161
+ 5. `mode="desktop"` renders the sidebar plus `children` in the two-column grid; `mode="mobile"`
162
+ renders the trigger and no sidebar.
163
+ 6. The drawer's default `zIndex` resolves **above** `SHELL-3`'s bar (`drawer + 3` vs `drawer + 2`).
164
+ 7. Theme-agnosticism, per `SHELL-4`'s pattern: render under a `createAppTheme` theme **and** a plain
165
+ `createTheme()` and assert resolved surface/divider values follow each. jg is a non-baseline
166
+ consumer, so this is not hypothetical.
167
+ 8. `THEME-4`'s shadowing check still returns no findings — any `sx` this component owns must not
168
+ duplicate a baseline `styleOverrides` key.
169
+ 9. `AccountPage`: all six cockpit-shaped sections reachable at 375 px without horizontal scrolling;
170
+ the `?tab=` param still selects; permission-gated sections still absent when not permitted.
171
+
172
+ Plus `tsc -p tsconfig.build.json --noEmit` clean. **No full-suite run** — affected set is the new
173
+ component's spec, `AccountPage`'s, and the theme specs touched by test 8.
174
+
175
+ ### Verification
176
+
177
+ The **two-width rendered side-by-side at 375 px and 1280 px** against
178
+ `assets/SHELL-2-account-navigation.html`, per the standing gate. What to look at: at 375 px the
179
+ trigger must be visible without scrolling and the drawer must reach every section; at 1280 px the
180
+ sidebar and content must sit in the two-column grid with no strip left behind.
181
+
182
+ Measure, do not eyeball, the one number this WO exists to fix: **the strip's 239 px overflow must
183
+ become zero**, and no section may sit outside its container. State whether real screenshots were
184
+ obtained or DOM inspection substituted.
185
+
186
+ ### Parity guardrail
187
+
188
+ Presentation only. No permission, behaviour, data-contract or routing change; `?tab=` keeps working.
189
+ Every section that renders today still renders, reached differently.
190
+
191
+ ---
192
+
193
+ ## B. Implementation map
194
+
195
+ *Filled by the Orchestrator on `git pull` — see `AGENTS.md` → "Work Order".*
196
+
197
+ ### Execution directive (read this first)
198
+
199
+ > **If you are the implementer reading this work order as your own specification: this section is
200
+ > NOT addressed to you.** It tells the Orchestrator how to invoke you. **You ARE that invocation —
201
+ > do NOT shell out to `codex exec`.**
202
+ >
203
+ > Implement through `codex exec` in the background — invoked directly via Bash (never the
204
+ > `debugger`/`*_coder` Agent wrappers) with BOTH flags `--skip-git-repo-check` and
205
+ > `--dangerously-bypass-approvals-and-sandbox`. **Pass the WO via stdin, not as a positional
206
+ > argument** (a positional arg hits Windows' command-line length limit — proven repeatedly in this
207
+ > repo). Fallback to direct Claude implementation only on Codex quota/rate-limit/non-zero exit — and
208
+ > note that the fallback flips authorship, so both reviewers stay mandatory either way.
209
+ >
210
+ > **Read `git log origin/main..HEAD` and `git status`, and re-read this WO's register row, before your
211
+ > own review.** Across four ucm work orders the implementer produced an invalid review claim three
212
+ > times — twice by self-committing with a self-report, once by writing a **fabricated** verdict into
213
+ > `WORK_ORDERS.md` before either reviewer had started. The preamble now forbids editing that file;
214
+ > verify it was respected.
215
+
216
+ ### Context package
217
+
218
+ **Precondition confirmed:** `THEME-5` landed and published (`8df44c5`, `package.json` reads `2.35.0`).
219
+ Clean starting tree.
220
+
221
+ **Reference implementation, read at `e551ff7`** (read-only, do not edit):
222
+ `jg-ferien/frontend/src/components/EventInfoHub/SectionSwitcher.jsx` (119 lines, reproduced in full
223
+ below for convenience — re-verify against the live file if it has moved) and its consumer
224
+ `jg-ferien/frontend/src/pages/EventInfoPage.jsx:1110-1160` (the mode switch + the 280px desktop grid
225
+ this WO's component takes over, `gridTemplateColumns: { md: "280px minmax(0, 1fr)" }`).
226
+
227
+ ```jsx
228
+ // SectionSwitcher.jsx — current jg source, annotated with what changes per the Envelope:
229
+ function SectionSwitcherList({ groupedSections, activeKey, rememberedKey, onSelectSection }) {
230
+ // renders: Paper(Übersicht item) -- ONLY IF overviewItem is supplied, see below --
231
+ // then one Paper per group: group-head Box + List of ListItemButton(selected, primary=label,
232
+ // secondary="Zuletzt geöffnet" when item.key===rememberedKey && !selected)
233
+ }
234
+ export default function SectionSwitcher({ mode, open=false, onClose, groupedSections, activeKey=null,
235
+ rememberedKey=null, onSelectSection, title }) {
236
+ // mode==='desktop': Box{position:'sticky', top: APP_HEADER_HEIGHT+24} wrapping SectionSwitcherList
237
+ // else: Drawer{anchor:'bottom', open, onClose, sx:{zIndex: theme=>theme.zIndex.drawer+3},
238
+ // PaperProps.sx:{borderTopLeftRadius:18, borderTopRightRadius:18, maxHeight:'78dvh', p:2,
239
+ // pb:'max(16px, env(safe-area-inset-bottom))'}} containing a title Typography + scrollable list
240
+ }
241
+ ```
242
+
243
+ **File location:** `src/layout/SectionNav.jsx` — this is page-chrome/navigation-shell, the same
244
+ category as `src/layout/PageLayout.jsx`, not a leaf UI widget (`MobileBottomNav`'s placement in
245
+ `src/components/` was already slightly inconsistent with this; use your judgement but `src/layout/`
246
+ reads as the better fit here since this component, like `PageLayout`, wraps page content).
247
+
248
+ **Component API — resolving one structural ambiguity the Envelope leaves implicit:** the prop table
249
+ lists `open`/`onClose` unchanged from jg ("drawer only"), but jg's drawer was triggered by a button
250
+ that lived OUTSIDE `SectionSwitcher` (in jg's own page header), whereas Part 2 requires the new
251
+ trigger bar to be built INSIDE this component. A trigger owned by the component needs to be able to
252
+ open a drawer whose `open` state jg's original code expected the CONSUMER to own — these two
253
+ requirements only reconcile with the standard optional-controlled React pattern (the same shape MUI's
254
+ own components use):
255
+ ```js
256
+ const [internalOpen, setInternalOpen] = useState(false);
257
+ const isControlled = open !== undefined;
258
+ const drawerOpen = isControlled ? open : internalOpen;
259
+ const handleTriggerClick = () => { if (!isControlled) setInternalOpen(true); };
260
+ const handleDrawerClose = () => { if (!isControlled) setInternalOpen(false); onClose?.(); };
261
+ ```
262
+ `AccountPage` (this WO's consumer) passes neither `open` nor `onClose` — fully uncontrolled, the
263
+ trigger manages itself. This also leaves the door open for jg's later migration (a separate WO) to
264
+ pass `open`/`onClose` in controlled mode if it turns out to still want its own external trigger
265
+ instead of this component's built-in one — not solved here, just not foreclosed.
266
+
267
+ **i18n split — two different kinds of text, do not conflate them:**
268
+ - **Group/item labels are finished strings** the consumer supplies via the `groups` prop (per
269
+ `SHELL-1`/`SHELL-3`/`DS-11`'s settled convention) — the component does not call `t()` on them.
270
+ - **The component's own chrome text is NOT consumer-supplied and needs its own i18n keys**, registered
271
+ in a new `src/i18n/sectionNavTranslations.ts`, following `src/i18n/userMenuTranslations.ts`'s exact
272
+ shape (`{ 'SectionNav.KEY': { de, fr, en, sw } }`) and exported from `src/index.js`'s translations
273
+ section (see `export { userMenuTranslations }`). Needed keys: a default drawer `title` (jg's
274
+ `t("SectionSwitcher.TITLE")` **had no fallback at all** — the Envelope calls this out explicitly as
275
+ a trap: in the kit that key must exist or the raw key renders on screen) and the trigger's "Bereich"
276
+ eyebrow label (jg hardcodes this as static JSX text in the prototype, not a translation call today —
277
+ give it one here, e.g. `SectionNav.TRIGGER_EYEBROW`). Both `title` and any future eyebrow-override
278
+ stay optional props with these keys as the default, same pattern as `UserMenu`'s `profileLink`.
279
+
280
+ **`kitSxRegistry.js` registration is mandatory, not optional — this is new territory `SHELL-3` predates
281
+ knowing about:** `THEME-4`/`THEME-5` wired a hard-failing kit-wide check
282
+ (`src/theme/themeCompleteness.js`: `assertKitSxDisjoint`, `reportKitSxBypasses`,
283
+ `reportOffPaletteColours`) that runs against `src/theme/kitSxRegistry.js`. Any `sx` this new component
284
+ owns for a baseline-styled MUI component (check `BASELINE_STYLED_MUI_COMPONENTS` in
285
+ `themeCompleteness.js` for the 23-key list — `MuiDrawer`/`MuiPaper` are both on it) **must** be
286
+ exported as a top-level `const X_SX = {...}` (follow `MOBILE_BOTTOM_NAV_ROOT_SX`'s exact shape) and
287
+ added to `kitSxRegistry.js`'s imports/`SX_EXPORTS`/`KIT_COMPONENT_SX_REGISTRY` — an inline, unexported
288
+ `sx={{ borderTopLeftRadius: 18, ... }}` on the `Drawer`'s `PaperProps` would otherwise trip
289
+ `reportKitSxBypasses` in the affected-set test run. Also: no colour literal (`SHELL-4`/`THEME-5`'s
290
+ `reportOffPaletteColours` scan) — jg's code already uses no hex here (all values are numeric/px/token
291
+ strings), so this should be a non-issue if the port is faithful, but verify.
292
+
293
+ **`zIndex` default:** keep jg's `theme.zIndex.drawer + 3` as the literal default (a plain JS function
294
+ default parameter on the component, exactly like `MobileBottomNav`'s `defaultZIndex` — NOT registered
295
+ in the theme, since `SHELL-4` established functions cannot live in the static baseline tree). The
296
+ Envelope's own reasoning: `SHELL-3`'s bottom bar sits at `+2`, so this must render above it — document
297
+ that ordering as a comment, do not silently pick a different offset.
298
+
299
+ **`AccountPage.jsx` grouping** (`src/pages/AccountPage.jsx`, current flat `tabs` `useMemo` at
300
+ `:125-160`, current `<Tabs>`/`<Tab>` block at `:196-206`): the prototype's footer states the fixed
301
+ grouping explicitly — **"Mein Konto"** (`profile`, `security`), **"Verwaltung"** (`users`, `invite`),
302
+ **"Hilfe"** (`support`), and a trailing **"Weitere"** catch-all for anything else (this is exactly
303
+ where `extraTabs` without a named group lands — the prototype's own worked example is cockpit's
304
+ "Benachrichtigungen" `extraTab` falling into "Weitere"). No standalone "Übersicht" entry — the
305
+ prototype explicitly starts at the first group (panel 1's own caption: `AccountPage` has no overview
306
+ page for one to point at) — so **do not pass `overviewItem`**. Group labels need their own i18n keys
307
+ (new `Account.GROUP_MY_ACCOUNT` / `Account.GROUP_MANAGEMENT` / `Account.GROUP_HELP` /
308
+ `Account.GROUP_MORE` — this repo already has an `Account.*` key family, e.g. `Account.TAB_PROFILE` at
309
+ `:130` — follow that naming). Build the `groups` array by mapping the existing flat `tabs` list into
310
+ these four buckets by `value`, preserving each tab's already-computed `label`; do not change the
311
+ permission logic that produces `tabs` in the first place. `activeKey`/`onSelect` continue to be driven
312
+ by the existing `safeTab`/`handleTabChange` — the `?tab=` contract and `activeTabExists` fallback logic
313
+ at `:182-188` are unchanged (Non-goal, explicit parity guardrail).
314
+
315
+ **Mode switching in `AccountPage`:** add `const isMobile = useMediaQuery(theme.breakpoints.down('md'))`
316
+ (needs `useTheme`/`useMediaQuery` imports) — same breakpoint jg's `EventInfoPage.jsx:138` uses and the
317
+ same one `SHELL-3`'s `MobileBottomNav` defaults `hideAbove` to, so this is the estate's established
318
+ mobile/desktop split, not a new number to justify. Desktop: render `<SectionNav mode="desktop">`
319
+ beside content in the two-column grid (the component now owns this grid — do not keep
320
+ `AccountPage`'s own `gridTemplateColumns` if you add one, the component takes it over per Part 1's
321
+ last bullet). Mobile: render `<SectionNav mode="mobile">` (uncontrolled, no `open`/`onClose` passed)
322
+ ahead of the tab content — its own trigger renders first per the prototype ("the first thing rendered
323
+ on mobile").
324
+
325
+ **Dev harness:** add one specimen to `dev/entries.jsx` (follow the existing pattern, e.g.
326
+ `MobileBottomNavEntry`) exercising both `mode="desktop"` and `mode="mobile"` side by side if
327
+ practical, using the same four-group shape as the prototype, so the two-width verification below has
328
+ something to point the preview browser at beyond `AccountPage` itself (which needs a full
329
+ `AuthContext`/backend-shaped fixture to render meaningfully in the harness).
330
+
331
+ ### Verification procedure
332
+
333
+ Per `preview-running-app` skill: `ui-core-micha-dev` launch config exists (`.claude/launch.json`,
334
+ port 5199) — a prior session in this repo could not get the Browser pane to composite frames at all
335
+ (not a rendering bug, the pane itself did not display). Attempt it again fresh; if it fails again,
336
+ DOM/computed-style inspection is the declared, accepted substitution per this WO's own Risks section
337
+ — state which happened in the register note, and specifically capture the two numbers the WO frames
338
+ as the actual deliverable: the strip's now-absent overflow (should be 0, there is no strip anymore)
339
+ and confirm every section's item is reachable in the rendered `groups` list at 375px without
340
+ horizontal scroll.
341
+
342
+ ### Mini-handover
343
+
344
+ Repo: `ui-core-micha` (`C:\Users\biglmi\Documents\webapps\ui-core-micha`), branch `main`.
345
+ WO: `work-orders/SHELL-5.md`. Follow `orchestrate-codex`.