@micha.bigler/ui-core-micha 2.38.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/README.md +4 -0
- package/WORK_ORDERS.md +6 -3
- package/dist/components/SoftChip.js +69 -0
- package/dist/components/StatTile.js +20 -0
- package/dist/index.js +2 -0
- package/dist/theme/kitSxRegistry.js +7 -0
- package/dist/theme/themeCompleteness.js +2 -2
- package/dist/theme/tokens.js +1 -1
- package/package.json +1 -1
- package/src/components/SoftChip.jsx +93 -0
- package/src/components/StatTile.jsx +39 -0
- package/src/index.js +2 -0
- package/src/theme/kitSxRegistry.js +11 -0
- package/src/theme/themeCompleteness.js +2 -2
- package/src/theme/tokens.js +1 -1
- package/tests/SoftChip.test.jsx +105 -0
- package/tests/StatTile.test.jsx +57 -0
- package/tests/themeCompleteness.test.js +31 -0
- package/work-orders/PRIM-1.md +177 -0
- package/work-orders/THEME-7.md +82 -0
- package/work-orders/THEME-8.md +211 -0
- package/work-orders/assets/PRIM-1-dashboard-primitives.html +624 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
+
import { cleanup, render, screen } from '@testing-library/react';
|
|
4
|
+
import { ThemeProvider } from '@mui/material/styles';
|
|
5
|
+
|
|
6
|
+
import { createAppTheme } from '../src/theme';
|
|
7
|
+
import StatTile from '../src/components/StatTile';
|
|
8
|
+
|
|
9
|
+
const theme = createAppTheme({ palette: { primary: { main: '#0F62FE' } } });
|
|
10
|
+
|
|
11
|
+
function renderTile(props) {
|
|
12
|
+
return render(<ThemeProvider theme={theme}><StatTile {...props} /></ThemeProvider>);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
afterEach(() => cleanup());
|
|
16
|
+
|
|
17
|
+
describe('StatTile', () => {
|
|
18
|
+
it('renders label, value and caption', () => {
|
|
19
|
+
renderTile({ label: 'Coverage at budget', value: '71.4 %', caption: 'of maximum realizable' });
|
|
20
|
+
|
|
21
|
+
expect(screen.getByText('Coverage at budget')).toBeTruthy();
|
|
22
|
+
expect(screen.getByText('71.4 %')).toBeTruthy();
|
|
23
|
+
expect(screen.getByText('of maximum realizable')).toBeTruthy();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('omits the value row when value is undefined', () => {
|
|
27
|
+
renderTile({ label: 'Recovered' });
|
|
28
|
+
|
|
29
|
+
expect(screen.getByText('Recovered')).toBeTruthy();
|
|
30
|
+
expect(screen.queryByText('undefined')).toBeNull();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('renders children', () => {
|
|
34
|
+
renderTile({ label: 'Deaths averted', value: '63.8 %', children: <span>children slot</span> });
|
|
35
|
+
|
|
36
|
+
expect(screen.getByText('children slot')).toBeTruthy();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('applies the accent border only when accent is set', () => {
|
|
40
|
+
const { container: plain } = renderTile({ label: 'Body fat', value: '18.4 %' });
|
|
41
|
+
const plainStyle = window.getComputedStyle(plain.firstChild);
|
|
42
|
+
expect(plainStyle.borderWidth).toBe('1px');
|
|
43
|
+
|
|
44
|
+
cleanup();
|
|
45
|
+
|
|
46
|
+
const { container: accented } = renderTile({ label: 'Lean mass', value: '62.1 kg', accent: true });
|
|
47
|
+
const accentedStyle = window.getComputedStyle(accented.firstChild);
|
|
48
|
+
expect(accentedStyle.borderWidth).toBe('2px');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('renders the value with tabular-nums', () => {
|
|
52
|
+
renderTile({ label: 'theta*', value: '0.8421' });
|
|
53
|
+
|
|
54
|
+
const value = screen.getByText('0.8421');
|
|
55
|
+
expect(window.getComputedStyle(value).fontVariantNumeric).toBe('tabular-nums');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -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', () => {
|
|
@@ -245,6 +260,22 @@ describe('kit sx disjointness', () => {
|
|
|
245
260
|
surface: 'UserListComponent.emptyCell.MuiTableCell.py',
|
|
246
261
|
reason: 'The empty-state message intentionally uses more vertical breathing room than the baseline TableCell default.',
|
|
247
262
|
},
|
|
263
|
+
{
|
|
264
|
+
surface: 'SoftChip.root.MuiChip.height',
|
|
265
|
+
reason: 'SoftChip is a small annotation chip, deliberately auto-height rather than the baseline interactive Chip\'s fixed 32px.',
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
surface: 'SoftChip.caveat.MuiChip.borderRadius',
|
|
269
|
+
reason: 'The caveat variant uses the shared square-ish radius token (shape.borderRadius), not the baseline Chip\'s pill radius.',
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
surface: 'SoftChip.caveat.MuiChip.fontSize',
|
|
273
|
+
reason: 'PRIM-1 token delta: caveat text is 12/500, one weight step above the nearest baseline variant (caption, 12/400). Adding a variant for this is an app-level baseline decision out of scope for this promotion; deliberately left unresolved.',
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
surface: 'SoftChip.status.MuiChip.borderRadius',
|
|
277
|
+
reason: 'The status variant is a pill, matching cockpit\'s existing lane-status pill shape, not the baseline Chip\'s default radius.',
|
|
278
|
+
},
|
|
248
279
|
],
|
|
249
280
|
}).findings).toEqual([]);
|
|
250
281
|
});
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# PRIM-1 — Promote `StatTile` and `SoftChip` into the shared kit
|
|
2
|
+
|
|
3
|
+
**Target repo:** `ui-core-micha` (branch `main`)
|
|
4
|
+
**Tier:** 3 — shared-core.
|
|
5
|
+
|
|
6
|
+
New register prefix `PRIM-*`: shared dashboard/data primitives — small presentational components that
|
|
7
|
+
are neither chart (`CHART-*`) nor app chrome (`SHELL-*`).
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## A. Envelope
|
|
12
|
+
|
|
13
|
+
### Goal
|
|
14
|
+
|
|
15
|
+
Both components already exist **twice**, built independently, and the second build in each case is an
|
|
16
|
+
**inline component inside a page** rather than a file — which is why a filename search misses it:
|
|
17
|
+
|
|
18
|
+
| Component | First build | Second build |
|
|
19
|
+
|---|---|---|
|
|
20
|
+
| `StatTile` | `hram/frontend/src/components/ResearchComponents/StatTile.jsx` (36 lines) | `fitness-monitor/frontend/src/pages/BodyHistoryPage.jsx:554-568` |
|
|
21
|
+
| `SoftChip` | `hram/frontend/src/components/ResearchComponents/SoftChip.jsx` (67 lines) | `cockpit/frontend/src/cockpit/BoardView.jsx:65-73` (the lane pill) |
|
|
22
|
+
|
|
23
|
+
Promote both into the kit as the single implementation. The promotion trigger is met on evidence — two
|
|
24
|
+
independent builds each, and a third app in view for each.
|
|
25
|
+
|
|
26
|
+
### The visual spec, and the seven decisions in it
|
|
27
|
+
|
|
28
|
+
**Prototype:** [`work-orders/assets/PRIM-1-dashboard-primitives.html`](work-orders/assets/PRIM-1-dashboard-primitives.html).
|
|
29
|
+
It renders the promoted spec at 375 px and 1280 px, keeps both original builds side by side as the
|
|
30
|
+
record of what changed, and computes its contrast ratios in-page against the *blended* fill.
|
|
31
|
+
|
|
32
|
+
The two builds diverged in seven places. All seven were decided by the operator on 2026-08-12, and the
|
|
33
|
+
prototype already reflects them:
|
|
34
|
+
|
|
35
|
+
| | Decision | Which app changes |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| StatTile value size | **`h5` — 18/600** | hram, 16 → 18 |
|
|
38
|
+
| StatTile digits | **`tabular-nums`, always** — it is what makes a KPI row read as a row | fitness-monitor |
|
|
39
|
+
| StatTile row behaviour | **`flex: 1` with a `minWidth` floor** — one rule covering both existing behaviours, no prop | hram, tiles now share the row |
|
|
40
|
+
| StatTile `accent` | **kept** — 2 px `primary.main`; without it fitness-monitor cannot adopt | neither |
|
|
41
|
+
| StatTile padding | **responsive `{xs: 1, sm: 1.5}`** | hram, was fixed |
|
|
42
|
+
| SoftChip `status` typography | **`overline` — 11/600, uppercase, .4 px tracking** | hram (`passed` → `PASSED`); cockpit drops 700 → 600 |
|
|
43
|
+
| SoftChip leading element + border | **dot by default, any icon via prop; border optional** | neither — both adopt unchanged |
|
|
44
|
+
|
|
45
|
+
**Why `overline` rather than a new variant:** cockpit's build independently arrived at 11-with-uppercase-
|
|
46
|
+
and-.4 px-tracking, which is the baseline's `overline` in all but the weight step. So the shared choice
|
|
47
|
+
is what the second consumer already made, and it satisfies `THEME-4` **without a baseline change**.
|
|
48
|
+
|
|
49
|
+
### Token delta list
|
|
50
|
+
|
|
51
|
+
**One entry** — the rest is drawn from the baseline's own tokens, so there is nothing else to reconcile.
|
|
52
|
+
|
|
53
|
+
- **`caveat` variant text: 12/500.** The baseline's nearest variant is `caption` at 12/**400**; there is
|
|
54
|
+
no 12/500. **Resolved by the operator as: keep the prototype value, do not add a baseline variant in
|
|
55
|
+
this WO.** It will surface as a `THEME-4` finding on the promoted component and must be **exempted
|
|
56
|
+
with that reason stated** — not silently flattened to 400, and not fixed by extending the baseline
|
|
57
|
+
here. Adding a variant is an app-level decision that does not belong inside a component promotion.
|
|
58
|
+
|
|
59
|
+
### Scope
|
|
60
|
+
|
|
61
|
+
1. **`StatTile`** as a kit component, per the decisions above. Props: `label`, `value`, `caption`,
|
|
62
|
+
`accent`, `children`, and a `minWidth` floor with a sensible default.
|
|
63
|
+
2. **`SoftChip`** as a kit component. Props: `label`, `tone` (`success`/`warning`/`error`/`info`),
|
|
64
|
+
`variant` (`caveat`/`status`), `icon` (defaulting to the 6 px dot), a border toggle, and `title` for
|
|
65
|
+
the tooltip.
|
|
66
|
+
3. **Colour derivation stays as authored** — fill `alpha(main, .12)`, border `alpha(main, .30)`, text and
|
|
67
|
+
dot `darken(main, .3)`, read from `theme.palette[tone]`. **Do not restate any hex.** This is the
|
|
68
|
+
property that makes the component exemplary and the reason its four tones all pass AA (measured in the
|
|
69
|
+
prototype: 7.11 / 7.33 / 7.67 / 9.20).
|
|
70
|
+
4. **Keep `enterTouchDelay={0}`** on the tooltip. It is why SoftChip's `title` is reachable on a touch
|
|
71
|
+
pointer — the exact failure `AUTH-4` is blocked on elsewhere. Do not drop it as boilerplate.
|
|
72
|
+
5. Exports from the package root, and the README line.
|
|
73
|
+
|
|
74
|
+
### Non-goals / do not touch
|
|
75
|
+
|
|
76
|
+
- **The three app adoptions.** hram, fitness-monitor and cockpit each get their own small follow-up WO.
|
|
77
|
+
This WO ships the kit component and nothing in any app.
|
|
78
|
+
- **No baseline/theme change.** No new typography variant, no palette edit. See the token delta list.
|
|
79
|
+
- **The other primitives in hram's `ResearchComponents/`** — `CardTitle`, `InfoPopover`,
|
|
80
|
+
`ProvisionalChip`, `FieldInfo`. `ProvisionalChip` wraps `SoftChip` and is the obvious next candidate,
|
|
81
|
+
but it has one build and is out of scope here.
|
|
82
|
+
- **`SoftChip`'s `caveat` weight.** Left at 12/500 deliberately.
|
|
83
|
+
- Any chart wrapper, shell component, or existing kit surface.
|
|
84
|
+
|
|
85
|
+
### Risks
|
|
86
|
+
|
|
87
|
+
- **Two apps' visible appearance changes when they adopt** — hram's status chips become uppercase and its
|
|
88
|
+
KPI values grow 16 → 18; cockpit's pills lose a weight step. All three are decided, not accidental, and
|
|
89
|
+
all three land in the *adoption* WOs rather than here. Worth stating in each so nobody reports them as
|
|
90
|
+
regressions.
|
|
91
|
+
- **Neither `StatTile` build has a test.** The promoted component's tests are written here from scratch,
|
|
92
|
+
not ported. `SoftChip` has `SoftChip.test.jsx` in hram — read it, but do not assume it covers the
|
|
93
|
+
promoted API.
|
|
94
|
+
- **`THEME-4` will flag the `caveat` 12/500.** Expected. Exempt with the reason; an unexplained exemption
|
|
95
|
+
is itself a finding by that check's own design.
|
|
96
|
+
- **Version:** this is a new capability area (two new components), so a **minor** — `2.38.0` → `2.39.0`.
|
|
97
|
+
|
|
98
|
+
### Required tests to WRITE
|
|
99
|
+
|
|
100
|
+
- **`StatTile`**: renders label/value/caption; omits the value row when `value` is undefined; renders
|
|
101
|
+
`children`; `accent` applies the 2 px border and the default does not; the value carries
|
|
102
|
+
`tabular-nums`.
|
|
103
|
+
- **`SoftChip`**: each of the four tones derives fill/border/text from `theme.palette[tone]` rather than a
|
|
104
|
+
literal (assert against the theme object, not against a hex string); `status` vs `caveat` differ in
|
|
105
|
+
radius and typography; the default leading element is the dot and a passed `icon` replaces it; the
|
|
106
|
+
border toggle works; `title` renders a tooltip and `enterTouchDelay` is 0.
|
|
107
|
+
- **Do not write a contrast assertion using `getContrastRatio`** — it ignores alpha, so on a 12 % tint it
|
|
108
|
+
answers a different question and would pass while meaning nothing. The prototype computes the real
|
|
109
|
+
ratios; if a contrast test is wanted it must blend the fill first.
|
|
110
|
+
|
|
111
|
+
Scope the run to these two new specs plus anything the new exports touch. Not the full suite — no
|
|
112
|
+
dependency change, no theme change.
|
|
113
|
+
|
|
114
|
+
### Parity guardrail
|
|
115
|
+
|
|
116
|
+
Visual and API only. The promoted components must be capable of rendering **both** existing builds'
|
|
117
|
+
appearances where a decision preserved them, and exactly the decided appearance where it did not. No
|
|
118
|
+
behaviour, permission or data-contract surface exists here.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## B. Implementation map
|
|
123
|
+
|
|
124
|
+
*Filled by the Orchestrator on `git pull` — see `AGENTS.md` → "Work Order".*
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## C. Orchestrator only
|
|
129
|
+
|
|
130
|
+
> **STOP — if you are the implementer reading this work order as your own specification, this section is
|
|
131
|
+
> NOT addressed to you. Skip it entirely.** It tells the Orchestrator how to invoke you, how the result
|
|
132
|
+
> is reviewed, and how it is committed. **You ARE that invocation — do NOT shell out to `codex exec`, do
|
|
133
|
+
> NOT spawn reviewers, do NOT run tests beyond your own new ones, do NOT edit `WORK_ORDERS.md`, and do
|
|
134
|
+
> NOT `git add`/`commit`/`push`.** Leave the diff in the working tree.
|
|
135
|
+
|
|
136
|
+
### Execution directive
|
|
137
|
+
|
|
138
|
+
Check `.claude/codex-status.md` first. **No line for the current date means use Codex.** A line recording
|
|
139
|
+
`unavailable` for the current date means skip the attempt, implement directly in Claude, and name the
|
|
140
|
+
record and its date — that flips authorship. If the newest confirmation is older than today, make exactly
|
|
141
|
+
one probe and write the outcome back either way.
|
|
142
|
+
|
|
143
|
+
### Review routing
|
|
144
|
+
|
|
145
|
+
Tier 3: **independent `reviewer`** and **`ui_reviewer`**, concurrent, in one background batch, before the
|
|
146
|
+
commit. **No `sec_reviewer`** — no auth, permission, money or schema surface.
|
|
147
|
+
|
|
148
|
+
Ask `ui_reviewer` specifically whether the `overline` decision reads correctly at both variants, and
|
|
149
|
+
whether the `caveat` exemption is stated rather than assumed.
|
|
150
|
+
|
|
151
|
+
### Verification
|
|
152
|
+
|
|
153
|
+
**A prototype is in scope, so the rendered side-by-side at 375 px and 1280 px is a commit gate** — see
|
|
154
|
+
`AGENTS.md` → Reviews. Note the known constraint: the Browser pane composites only while displayed and
|
|
155
|
+
screenshots have not been legibly obtainable in this repo. If capture fails, **declare the substitution
|
|
156
|
+
in the register Notiz** and say what was inspected instead (computed styles, DOM) — an undeclared
|
|
157
|
+
substitution is a skipped gate, and that fallback cannot see whether a screen reads heavy or dense.
|
|
158
|
+
|
|
159
|
+
The prototype's own contrast numbers are computed at render time; re-read them rather than restating
|
|
160
|
+
these.
|
|
161
|
+
|
|
162
|
+
### Register + commit
|
|
163
|
+
|
|
164
|
+
One row, `PRIM-1`, with the `PRIM-*` prefix documented in the register header. `done` only with both
|
|
165
|
+
reviewers and their verdicts named, plus the rendered-gate evidence or its declared substitution.
|
|
166
|
+
|
|
167
|
+
Publishing is a separate step from committing: **a landed commit is not a publish.** If the three
|
|
168
|
+
adoption WOs are to follow, `2.39.0` must actually be on the registry first — verify against npm, not
|
|
169
|
+
against a commit.
|
|
170
|
+
|
|
171
|
+
### Mini-handover
|
|
172
|
+
|
|
173
|
+
Repo: `ui-core-micha` (`C:\Users\biglmi\Documents\webapps\ui-core-micha`), branch `main`.
|
|
174
|
+
WO: `work-orders/PRIM-1.md`. Promotes two components whose second builds are inline in
|
|
175
|
+
`fitness-monitor/BodyHistoryPage.jsx:554` and `cockpit/BoardView.jsx:65`. Seven divergences already
|
|
176
|
+
decided — the table in Part A is the spec, the prototype renders it. Tier 3, both reviewers, rendered
|
|
177
|
+
gate applies, minor version `2.39.0`. App adoptions are separate WOs. Follow `orchestrate-codex`.
|
|
@@ -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.
|