@micha.bigler/ui-core-micha 2.39.0 → 2.39.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/WORK_ORDERS.md CHANGED
@@ -80,3 +80,5 @@ Two chunks. **Chunk 1** (Part 1 + Part 2 of the Envelope): extended `SPACING_SHO
80
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. |
81
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
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). |
83
+ | THEME-7 | Guidance: a series slot that collides with an app's own accent | The baseline ships six `dataSeries.categorical` colours and no guidance for the case where a slot sits too close to something the consuming app owns. Measured case, not hypothesis (hram `IMARA-2`, frozen 2026-08-13, Lab ΔE calibrated against that operator's own rejected pair at ΔE 16.6): series 1 `#3D5A99` vs hram's primary `#2F4F96` = **ΔE 7.2**, and series 3 `#2E8F8A` vs hram's DALYs identity `#1F7A72` = **ΔE 8.3** (same hue, one lightness step). hram resolved both app-side by dropping slot 1 and replacing slot 3 with a navy. This WO adds the missing guidance to `DESIGN.md` and the token docs — how to check an accent against the palette, and why a colliding slot should be DROPPED rather than nudged — plus an explicit decide-and-record on whether to ship anything beyond guidance (ΔE helper / documented extension sequence / nothing). **Explicitly not a defect report: the six shipped values stay unchanged**, since they are correct for any app without a muted-indigo accent or a teal domain identity. Full order [`work-orders/THEME-7.md`](work-orders/THEME-7.md) | 2026-08-13 | planned | | Tier 3 (shared-core surface) though documentation-led; independent `reviewer` mandatory. Raised by the hram/Imara design-language work at the operator's explicit instruction to take the finding upstream rather than keep it app-local. Stated limitation to keep in the text: the guidance derives from ONE app's experience and its threshold is calibrated on one operator's judgement — a starting point, not a standard. |
84
+ | THEME-8 | Touch-Ziel des Selects sitzt am falschen Element und drückt den Text aus der Mitte | `src/theme/tokens.js` setzt die Touch-Mindesthöhe (40, im `@media (any-pointer: coarse)`-Zweig 44) auf `MuiSelect.styleOverrides.**select**` — also auf das INNERE Element, das MUIs variantenabhängiges Padding trägt. Beim `standard`-Select ist dieses Padding oben-verankert (`1px 24px 5px 0`), der Zusatzraum landet komplett unten, der Text reitet oben. **Von den sechs Komponenten mit Touch-Ziel (`MuiButton`, `MuiIconButton`, `MuiSelect`, `MuiCheckbox`, `MuiFormControlLabel`, `MuiTextField`) ist `MuiSelect` die EINZIGE, die einen Nicht-Root-Slot adressiert** — eine Inkonsistenz in der Baseline selbst, keine Designentscheidung. **Live gemessen** (hram, Administration → Users, Select in einer Tabellenzelle): heute 0 px über / 0 px unter dem Text (Inneres auf 44 px gestreckt); nach Verschieben der Regel auf die Wurzel **9 px / 9 px** — exakt zentriert, weil `MuiInputBase-root` bereits `display:flex; align-items:center` ist. Voller Auftrag [`work-orders/THEME-8.md`](work-orders/THEME-8.md) | 2026-08-14 | done (npm publish; consumer pin bumps still open) | ae0745d | Tier 3 (Shared-Core). Codex out of credits (2026-08-14, `.claude/codex-status.md`), Orchestrator implemented directly — authorship flip made `reviewer` + `ui_reviewer` mandatory (already required at Tier 3): both ran, no blocking findings. `reviewer`: config-level test doesn't render/measure the DOM (P3, accepted — Envelope's own proof is the live measurement, already done), no version bump at review time (P2, resolved by 2.39.0→2.39.1 in the same commit). `ui_reviewer`: MuiSelect's coarse branch still hand-writes `{minHeight:44}` instead of the shared `coarseHitArea` helper (P3, pre-existing, not introduced here). Both independently traced MUI source (`Select.js`/`InputBase.js`) and confirmed the root-slot fix reaches the right DOM node and centres via the inherited flex layout. No live browser re-render done (Claude-Code preview-scoping override: small/medium frontend change, code reasoning + tests suffice). **Wert bleibt 40/44 — nur der Anker ist falsch.** Verstärkt auf Touch-Geräten: der `coarse`-Zweig greift auch auf Laptops mit Touchscreen (Operator-Rechner meldet `any-pointer: coarse`), dort 44 statt 40 px — gleicher Fehler, 4 px grösser. **Estate-weite Sichtveränderung**: alle konsumierenden Apps bekommen re-zentrierte Selects; in den Release Notes nennen. **Nicht am npm-Publish abschliessen** — die Pin-Bumps der Konsumenten (zuerst hram, wo es gefunden wurde) gehören dazu. Gefunden beim Messen einer vermeintlichen hram-Regression; die App-seitige Schwester ist hram FIX-11 (anderer Fehler, gleiche Felder). |
@@ -45,8 +45,8 @@ const COMPONENT_SURFACES = [
45
45
  componentPath('MuiTooltip', 'defaultProps.slotProps.transition.timeout.enter'),
46
46
  componentPath('MuiTooltip', 'defaultProps.slotProps.transition.timeout.exit'),
47
47
  componentPath('MuiSelect', 'defaultProps.variant'),
48
- componentPath('MuiSelect', 'styleOverrides.select.minHeight'),
49
- componentKeyLeaf('MuiSelect', 'styleOverrides.select', '@media (any-pointer: coarse)', 'minHeight'),
48
+ componentPath('MuiSelect', 'styleOverrides.root.minHeight'),
49
+ componentKeyLeaf('MuiSelect', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minHeight'),
50
50
  componentPath('MuiCheckbox', 'styleOverrides.root.color'),
51
51
  componentKeyLeaf('MuiCheckbox', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minHeight'),
52
52
  componentKeyLeaf('MuiCheckbox', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minWidth'),
@@ -209,7 +209,7 @@ export const BASELINE_STATIC = {
209
209
  MuiSelect: {
210
210
  defaultProps: { variant: 'outlined' },
211
211
  styleOverrides: {
212
- select: {
212
+ root: {
213
213
  minHeight: 40,
214
214
  boxSizing: 'border-box',
215
215
  '@media (any-pointer: coarse)': { minHeight: 44 },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micha.bigler/ui-core-micha",
3
- "version": "2.39.0",
3
+ "version": "2.39.1",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "repository": {
@@ -73,8 +73,8 @@ const COMPONENT_SURFACES = [
73
73
  componentPath('MuiTooltip', 'defaultProps.slotProps.transition.timeout.enter'),
74
74
  componentPath('MuiTooltip', 'defaultProps.slotProps.transition.timeout.exit'),
75
75
  componentPath('MuiSelect', 'defaultProps.variant'),
76
- componentPath('MuiSelect', 'styleOverrides.select.minHeight'),
77
- componentKeyLeaf('MuiSelect', 'styleOverrides.select', '@media (any-pointer: coarse)', 'minHeight'),
76
+ componentPath('MuiSelect', 'styleOverrides.root.minHeight'),
77
+ componentKeyLeaf('MuiSelect', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minHeight'),
78
78
  componentPath('MuiCheckbox', 'styleOverrides.root.color'),
79
79
  componentKeyLeaf('MuiCheckbox', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minHeight'),
80
80
  componentKeyLeaf('MuiCheckbox', 'styleOverrides.root', '@media (any-pointer: coarse)', 'minWidth'),
@@ -255,7 +255,7 @@ export const BASELINE_STATIC = {
255
255
  MuiSelect: {
256
256
  defaultProps: { variant: 'outlined' },
257
257
  styleOverrides: {
258
- select: {
258
+ root: {
259
259
  minHeight: 40,
260
260
  boxSizing: 'border-box',
261
261
  '@media (any-pointer: coarse)': { minHeight: 44 },
@@ -171,6 +171,21 @@ describe('theme completeness', () => {
171
171
  ]),
172
172
  );
173
173
  });
174
+
175
+ it('anchors the select touch target on the input root, not the inner select slot', () => {
176
+ // THEME-8: a min-height on styleOverrides.select lands on a display:block
177
+ // element with variant-dependent, top-anchored padding -- the surplus
178
+ // height collects entirely below the text instead of centring it. The
179
+ // root is already flex/align-items:center, so anchoring there is what
180
+ // actually centres the text. A test only checking "44px somewhere" would
181
+ // pass on the broken version too.
182
+ const theme = createAppTheme({ palette: { primary: { main: '#0F62FE' } } });
183
+ const selectOverrides = theme.components.MuiSelect.styleOverrides;
184
+
185
+ expect(selectOverrides.select?.minHeight).toBeUndefined();
186
+ expect(selectOverrides.root.minHeight).toBe(40);
187
+ expect(selectOverrides.root['@media (any-pointer: coarse)'].minHeight).toBe(44);
188
+ });
174
189
  });
175
190
 
176
191
  describe('kit sx disjointness', () => {
@@ -0,0 +1,82 @@
1
+ # THEME-7 — Guidance: a series slot that collides with an app's own accent
2
+
3
+ ## Part A — Envelope
4
+
5
+ **Goal.** Give consuming apps a documented answer for the case where a baseline
6
+ `dataSeries.categorical` slot sits too close to something the app itself owns — the app's
7
+ accent, or one of its domain identity colours. Today the baseline offers six series colours
8
+ and no guidance for that collision, so each app rediscovers the problem on its own.
9
+
10
+ **Why — a measured case, not a hypothesis.** During hram's Imara design-language work
11
+ (hram `IMARA-2`, frozen 2026-08-13) two collisions were measured against the app's own
12
+ values, using Lab ΔE with the operator's own rejected candidate pair as the calibration
13
+ floor (that pair sat at ΔE 16.6 and was judged "not tellable apart"):
14
+
15
+ - baseline series 1 `#3D5A99` versus hram's chosen primary `#2F4F96` → **ΔE 7.2**, the
16
+ tightest pair in that app's whole system; a series line beside a primary button reads as
17
+ the same colour.
18
+ - baseline series 3 `#2E8F8A` versus hram's DALYs identity `#1F7A72` → **ΔE 8.3**; same
19
+ hue (a −28.7 vs −27.8), one lightness step apart (L 54 vs 46).
20
+
21
+ hram resolved both app-side: slot 1 dropped entirely (both replacement candidates measured
22
+ worse elsewhere — a neutral slate landed ΔE 5.0 from the shared `stale` fill, a muted green
23
+ ΔE 15.5 from the app's own olive slot), and slot 3 replaced by a navy, which simultaneously
24
+ restored the dark blue the first removal had taken away. The nine remaining slots plus an
25
+ overflow grey covered the app's needs.
26
+
27
+ **This is not a defect report against the baseline.** For an app whose primary is not a
28
+ muted indigo and which has no teal domain identity, series 1 and 3 are correct and useful.
29
+ What is missing is the *guidance*, and the observation that the baseline's own muted,
30
+ cool-leaning character makes a collision with a tastefully chosen accent more likely rather
31
+ than less — apps that like this palette tend to pick accents from the same region.
32
+
33
+ **Scope.**
34
+
35
+ 1. `DESIGN.md` — a short subsection under the data-colour principle: how to check a chosen
36
+ accent against the series palette (measure, do not eyeball; a usable threshold with the
37
+ calibration above named as its provenance), and what to do when it collides — drop the
38
+ slot rather than nudge it, because a nudged slot stays confusable while a dropped one is
39
+ simply absent.
40
+ 2. The same note in the theme package's own documentation where `dataSeries.categorical` is
41
+ described, so it is found by someone reading the token rather than the design doc.
42
+ 3. **Decide and record**: whether the baseline should additionally *ship* anything —
43
+ candidates being an exported ΔE helper apps can assert with, a documented extension
44
+ sequence for apps needing more than six slots, or nothing at all beyond the guidance.
45
+ Doing nothing is a legitimate outcome and should be recorded as a decision if chosen.
46
+
47
+ **Non-goals / do-not-touch.** Do **not** change the six shipped series values — hram's
48
+ divergence is app-specific and other consumers depend on the current set. No change to the
49
+ status palette, the completeness assertion, or any component. This is documentation plus,
50
+ at most, one small additive export.
51
+
52
+ **Tier 3** — shared-core surface, per the estate tiering table, even though the change is
53
+ documentation-led. Independent review mandatory.
54
+
55
+ **Tests to write.** None for the documentation. If item 3 lands a helper, that helper gets
56
+ its own unit test with the two measured pairs above as fixtures — they are real values with
57
+ known answers.
58
+
59
+ **Risks.** The guidance is derived from a single app's experience; state that plainly rather
60
+ than presenting it as a general law. The threshold in particular is calibrated on one
61
+ operator's judgement — useful as a starting point, not as a standard, and it should say so.
62
+
63
+ ## Part B — Implementation map (implementer)
64
+
65
+ PLACEHOLDER — Orchestrator fills this on `git pull`: context package with `path:line`
66
+ anchors into `DESIGN.md` and the token documentation, the absolute working directory, the
67
+ progress contract, the preamble. Not dispatchable while this stands.
68
+
69
+ ## Part C — Orchestrator only
70
+
71
+ **STOP — addressee guard.** If you are the implementer reading this file as your own spec:
72
+ this part is not addressed to you. It tells the Orchestrator how to invoke you; you ARE that
73
+ invocation — do not shell out to `codex exec`. Stop reading at this line.
74
+
75
+ - **Execution.** Codex first per the Tier-2/3 rule unless the status record says otherwise.
76
+ - **Review routing.** `reviewer` mandatory (Tier 3). No `ui_reviewer` unless item 3 turns
77
+ into code with a visual surface, which it should not.
78
+ - **Verification.** Scoped: the theme package's existing tests must stay green; any new
79
+ helper carries its own.
80
+ - **Cross-repo.** Reference hram `IMARA-2` and its frozen sheet as the evidence source; do
81
+ not restate the measurements from memory when the artifact holds them.
82
+
@@ -0,0 +1,211 @@
1
+ # THEME-8 — The select touch target sits on the wrong element and pushes its text off-centre
2
+
3
+ ## Part A — Envelope
4
+
5
+ **Goal.** Move the baseline's touch-target minimum height for `MuiSelect` from the inner
6
+ `select` slot to the input root, so the existing flex centring can do its job. Every
7
+ consuming app's select text is currently top-anchored instead of vertically centred.
8
+
9
+ **Why — measured, not inferred.** `src/theme/tokens.js` sets
10
+
11
+ ```js
12
+ MuiSelect: { styleOverrides: { select: {
13
+ minHeight: 40,
14
+ '@media (any-pointer: coarse)': { minHeight: 44 },
15
+ } } }
16
+ ```
17
+
18
+ `.MuiSelect-select` is the element that carries MUI's variant-dependent padding, and for
19
+ the `standard` variant that padding is deliberately top-anchored (`1px 24px 5px 0`). Forcing
20
+ a 40/44 px height onto a `display: block` element whose content needs ~26 px puts the whole
21
+ surplus below the text, so the text rides high in its box.
22
+
23
+ Measured live in a consuming app (hram, Administration → Users, a select inside a table
24
+ cell), then verified by moving the rule to the root at runtime:
25
+
26
+ | | space above the text | space below |
27
+ |---|---|---|
28
+ | today | 0 px | 0 px — inner stretched to 44 px, top-anchored |
29
+ | min-height on the root instead | **9 px** | **9 px** |
30
+
31
+ The root (`MuiInputBase-root`) is already `display: flex; align-items: center`, so it centres
32
+ the natural-height inner element for free.
33
+
34
+ **This is an inconsistency inside the baseline, not a design choice.** Of the six components
35
+ that carry a touch target — `MuiButton`, `MuiIconButton`, `MuiSelect`, `MuiCheckbox`,
36
+ `MuiFormControlLabel`, `MuiTextField` — **`MuiSelect` is the only one that targets a
37
+ non-root slot.** The other five all use `root`.
38
+
39
+ **Amplified on touch-capable machines.** The `@media (any-pointer: coarse)` branch raises it
40
+ to 44 px, and a laptop with a touchscreen matches that query. The operator's machine reports
41
+ `any-pointer: coarse`, which is why it looks worse there than on a non-touch machine — the
42
+ same defect, 4 px larger.
43
+
44
+ **Scope.**
45
+
46
+ 1. Move the minimum height off `styleOverrides.select` and onto the root, keeping both the
47
+ base value and the coarse-pointer branch. **Verify which root actually receives it** —
48
+ `MuiSelect`'s `root` slot composes with the underlying input, and the outlined and
49
+ standard variants compose differently; if `MuiSelect.root` does not carry it cleanly for
50
+ both, `MuiInputBase.root` is the alternative. Decide by measuring in a running app, not
51
+ by reading the MUI source.
52
+ 2. Check the outlined variant does not grow taller than a neighbouring `MuiTextField` after
53
+ the move — `MuiTextField` already carries its own root min-height, so the two must still
54
+ line up in a form row.
55
+ 3. A test that pins the intent: assert the minimum height is not applied to the `select`
56
+ slot (or, better, assert the rendered inner element's natural height is preserved). A
57
+ test that only checks "44 px somewhere" would pass on the broken version too.
58
+
59
+ **Non-goals / do-not-touch.** Do not change the touch-target *value* (40 / 44) — the size is
60
+ correct, only its anchor is wrong. Do not touch the other five components; they are already
61
+ correct. No colour, typography or density changes. Do not "fix" this per-app with an sx
62
+ override in a consumer — that is what this baseline exists to prevent.
63
+
64
+ **Tier 3** — shared-core surface. `reviewer` mandatory. Every consuming app's selects change
65
+ appearance (for the better), which is exactly why it belongs here rather than in one app.
66
+
67
+ **Tests to write.** The intent test above, plus whatever existing theme/completeness suite
68
+ covers component overrides. Keep it small; the real proof is the rendered measurement.
69
+
70
+ **Risks.**
71
+ - **Estate-wide visual change.** Every app using the baseline gets subtly taller-looking
72
+ select rows or re-centred text. That is the fix, but it will be visible in apps nobody is
73
+ currently working on — name it in the release notes rather than letting it surprise someone.
74
+ - Moving a min-height between slots can change how a select aligns next to a text field in
75
+ the same row. Scope item 2 exists for that; check it in a real form, not in isolation.
76
+ - The coarse-pointer branch means a reviewer on a non-touch machine sees 40 px and a milder
77
+ version of the defect. Say which machine the verification ran on.
78
+
79
+ **Delivery is not done at publish.** A version bump alone does not fix any app. The
80
+ consuming apps still need their pin bumped and a look at their own forms — hram in
81
+ particular, where this was found. Track the consumer bumps rather than closing this WO at
82
+ the npm publish.
83
+
84
+ ## Part B — Implementation map — ADDRESSED TO THE IMPLEMENTER
85
+
86
+ ### Context package
87
+
88
+ **Named file to change:** `src/theme/tokens.js`, the `MuiSelect` block, currently:
89
+
90
+ ```js
91
+ MuiSelect: {
92
+ defaultProps: { variant: 'outlined' },
93
+ styleOverrides: {
94
+ select: {
95
+ minHeight: 40,
96
+ boxSizing: 'border-box',
97
+ '@media (any-pointer: coarse)': { minHeight: 44 },
98
+ },
99
+ },
100
+ },
101
+ ```
102
+
103
+ (around `src/theme/tokens.js:255-264`). The shared coarse-pointer helper used by every other
104
+ component sits just above it:
105
+
106
+ ```js
107
+ const coarseHitArea = {
108
+ '@media (any-pointer: coarse)': {
109
+ minHeight: 44,
110
+ minWidth: 44,
111
+ },
112
+ };
113
+ ```
114
+
115
+ (`src/theme/tokens.js:135-140`). The five correct comparators, for the target shape (all use
116
+ `styleOverrides.root` and the shared `coarseHitArea` spread):
117
+
118
+ - `MuiButton` — `src/theme/tokens.js:233-245` (`root: { minHeight: 40, ..., ...coarseHitArea }`)
119
+ - `MuiIconButton` — `:249-251`
120
+ - `MuiCheckbox` — `:265-267`
121
+ - `MuiFormControlLabel` — `:268-273`
122
+ - `MuiTextField` — `:334-337`
123
+
124
+ Also relevant: `MuiOutlinedInput` already sets `root: { minHeight: 40, ...coarseHitArea }`
125
+ (`:338-346`) — since `MuiSelect` with `variant: 'outlined'` (the default here) composes with
126
+ `MuiOutlinedInput`'s root, moving `MuiSelect`'s own min-height onto `MuiSelect.styleOverrides.root`
127
+ must not double up with that existing rule for the outlined variant. Measure both variants
128
+ (`outlined` default, and `standard` where used) in a running app before deciding whether the
129
+ target is `MuiSelect.styleOverrides.root` or `MuiInputBase.styleOverrides.root` — the Envelope
130
+ already flags this as a measure-don't-read decision.
131
+
132
+ **Second file:** `src/theme/themeCompleteness.js` references the old path and must move with it:
133
+
134
+ ```
135
+ 75: componentPath('MuiSelect', 'defaultProps.variant'),
136
+ 76: componentPath('MuiSelect', 'styleOverrides.select.minHeight'),
137
+ 77: componentKeyLeaf('MuiSelect', 'styleOverrides.select', '@media (any-pointer: coarse)', 'minHeight'),
138
+ ```
139
+
140
+ Update lines 76-77 to point at wherever the min-height actually lands (`styleOverrides.root...`
141
+ or `styleOverrides.select` removed entirely if the property is dropped from that slot) — keep
142
+ line 75 (`defaultProps.variant`) as is, it is unrelated. This file drives the baseline's own
143
+ completeness assertion; a stale path here makes the completeness suite pass against dead code.
144
+
145
+ **Existing test suite for reference:** `tests/themeCompleteness.test.js` — no existing test
146
+ references `MuiSelect` by name today, so the new intent test (Envelope scope item 3) is net new,
147
+ not a modification of an existing case.
148
+
149
+ **Invariants / do-not-touch:** the touch-target *value* stays 40 / 44 — only the slot changes.
150
+ Do not touch `MuiButton`, `MuiIconButton`, `MuiCheckbox`, `MuiFormControlLabel`, `MuiTextField`,
151
+ or `MuiOutlinedInput` beyond what's needed to avoid the double-application noted above. No
152
+ colour/typography/density changes anywhere in this diff.
153
+
154
+ Directive: work from this package; do not explore broadly from scratch; open only the named
155
+ files to verify. If you must dig deeper, delegate to a read-only Explore sub-agent (Haiku).
156
+
157
+ ### Target repo working directory (absolute)
158
+
159
+ `C:\Users\biglmi\Documents\webapps\ui-core-micha`
160
+
161
+ ### Preamble — REQUIRED, addressed to the implementer
162
+
163
+ > The text above is the COMPLETE spec — the committed WO file's content, not a plan to refine;
164
+ > there is no separate plan file. Read the nearest `AGENTS.md`, the relevant
165
+ > `.codex/skills/<role>/SKILL.md`, and the app `MEMORY.md` ONLY for conventions. Stay in scope;
166
+ > do not touch auth/permissions/deps/schema/CI unless the spec says so; do not update
167
+ > `MEMORY.md`. **Do NOT edit `WORK_ORDERS.md`** — the register row and the review verdicts are
168
+ > the orchestrator's alone. Do NOT `git add`/`commit`/`push` — leave every change uncommitted in
169
+ > the working tree for the orchestrator's independent review. WRITE the tests the "Required
170
+ > tests" section (Part A) calls for AND **RUN the tests you just wrote** to confirm they execute
171
+ > and pass — that is the ONLY test run you do (NOT the app's affected/full suite, NOT any
172
+ > review). The orchestrator re-runs the authoritative set + does the independent review after
173
+ > you finish — those are the gate; your own run does not count as the gate.
174
+ >
175
+ > Narrate continuously: a `PLAN: <step1> | <step2> | …` line up front, then a single-line
176
+ > `PROGRESS: [<n>/<total>] <present-tense action>` before every relevant action (and `… done` on
177
+ > completion), spaced so no gap exceeds ~2 min, stdout unbuffered, plus exactly one final
178
+ > `RESULT: DONE|BLOCKED <reason>`.
179
+
180
+ ## Part C — Orchestrator only — NOT ADDRESSED TO THE IMPLEMENTER
181
+
182
+ > **If you are the implementer reading this work order as your own specification: STOP at this
183
+ > line. Everything below describes what the Orchestrator does AFTER you finish. You do none of
184
+ > it — no reviewers, no verification run, no register edit, no commit.** You ARE the invocation
185
+ > described below; do NOT shell out to `codex exec`.
186
+
187
+ ## Execution directive
188
+
189
+ Implement through `codex exec` in the background — invoked directly via Bash (never the
190
+ `debugger`/`*_coder` Agent wrappers) with BOTH flags `--skip-git-repo-check` and
191
+ `--dangerously-bypass-approvals-and-sandbox`. Fallback to direct Claude implementation only on
192
+ Codex quota/rate-limit/non-zero exit — the fallback flips authorship, so an independent
193
+ `reviewer` becomes mandatory (already required at Tier 3, so no incremental change there).
194
+
195
+ ## Review routing
196
+
197
+ `reviewer` mandatory (Tier 3); `ui_reviewer` too, since the visible result is a layout change
198
+ across every consumer. Both spawned in the same background batch, Sonnet-pinned (Tier 3), diff
199
+ inline, pointed at this WO's Envelope only.
200
+
201
+ ## Verification
202
+
203
+ The package's own suite (`tests/themeCompleteness.test.js` plus the new intent test), plus a
204
+ **rendered check in a consuming app**: a select in a table row and a select beside a text field,
205
+ both re-measured for equal space above and below the text. State which machine (coarse or fine
206
+ pointer) the measurement ran on.
207
+
208
+ ## Register + commit
209
+
210
+ Advance the THEME-8 row with the named reviewer verdicts and the measured before/after numbers.
211
+ Then track the consumer pin bumps (hram first) — this WO is not done at npm publish.