@micha.bigler/ui-core-micha 2.33.2 → 2.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/WORK_ORDERS.md +2 -2
- package/dist/auth/ReauthModal.js +3 -1
- package/dist/components/AccessCodeManager.js +16 -14
- package/dist/components/AccessCodeSingleUseToggle.js +2 -1
- package/dist/components/AllowedEmailDomainsManager.js +3 -1
- package/dist/components/AuthFactorRequirementCard.js +2 -1
- package/dist/components/BulkInviteCsvTab.js +7 -7
- package/dist/components/LoginForm.js +2 -1
- package/dist/components/MFAComponent.js +11 -5
- package/dist/components/MfaLoginComponent.js +4 -1
- package/dist/components/PasskeysComponent.js +4 -2
- package/dist/components/QrSignupManager.js +2 -1
- package/dist/components/QrSignupValidityManager.js +4 -1
- package/dist/components/RegistrationMethodsManager.js +3 -1
- package/dist/components/SecurityComponent.js +3 -1
- package/dist/components/SupportRecoveryRequestsTab.js +4 -1
- package/dist/components/UserInviteComponent.js +8 -8
- package/dist/components/UserListComponent.js +8 -4
- package/dist/components/charts/ChartFrame.js +5 -2
- package/dist/components/charts/TimeSeriesChart.js +7 -4
- package/dist/index.js +1 -1
- package/dist/layout/PageLayout.js +3 -2
- package/dist/messaging/AttachmentList.js +6 -1
- package/dist/messaging/Composer.js +2 -1
- package/dist/messaging/DirectMessageLauncher.js +2 -1
- package/dist/messaging/MessageBubble.js +29 -8
- package/dist/messaging/PollCard.js +6 -4
- package/dist/messaging/Thread.js +2 -1
- package/dist/notifications/NotificationSettings.js +17 -3
- package/dist/notifications/PopupSurface.js +2 -1
- package/dist/onboarding/steps/BrowserPushStep.js +2 -1
- package/dist/pages/AccountPage.js +2 -1
- package/dist/pages/LoginPage.js +2 -1
- package/dist/pages/SignUpPage.js +2 -1
- package/dist/pages/SignupConfirmPage.js +2 -1
- package/dist/theme/index.js +2 -1
- package/dist/theme/kitSxRegistry.js +187 -0
- package/dist/theme/themeCompleteness.js +217 -0
- package/package.json +1 -1
- package/src/auth/ReauthModal.jsx +5 -2
- package/src/components/AccessCodeManager.jsx +22 -20
- package/src/components/AccessCodeSingleUseToggle.jsx +4 -2
- package/src/components/AllowedEmailDomainsManager.jsx +6 -3
- package/src/components/AuthFactorRequirementCard.jsx +4 -2
- package/src/components/BulkInviteCsvTab.jsx +11 -10
- package/src/components/LoginForm.jsx +5 -3
- package/src/components/MFAComponent.jsx +14 -7
- package/src/components/MfaLoginComponent.jsx +9 -5
- package/src/components/PasskeysComponent.jsx +7 -4
- package/src/components/QrSignupManager.jsx +6 -4
- package/src/components/QrSignupValidityManager.jsx +8 -4
- package/src/components/RegistrationMethodsManager.jsx +7 -4
- package/src/components/SecurityComponent.jsx +10 -7
- package/src/components/SupportRecoveryRequestsTab.jsx +8 -4
- package/src/components/UserInviteComponent.jsx +10 -10
- package/src/components/UserListComponent.jsx +12 -7
- package/src/components/charts/ChartFrame.jsx +6 -2
- package/src/components/charts/TimeSeriesChart.jsx +8 -4
- package/src/index.js +3 -0
- package/src/layout/PageLayout.jsx +4 -2
- package/src/messaging/AttachmentList.jsx +9 -3
- package/src/messaging/Composer.jsx +3 -1
- package/src/messaging/DirectMessageLauncher.jsx +3 -1
- package/src/messaging/MessageBubble.jsx +31 -9
- package/src/messaging/PollCard.jsx +8 -6
- package/src/messaging/Thread.jsx +3 -1
- package/src/notifications/NotificationSettings.jsx +24 -9
- package/src/notifications/PopupSurface.jsx +3 -1
- package/src/onboarding/steps/BrowserPushStep.jsx +3 -1
- package/src/pages/AccountPage.jsx +11 -9
- package/src/pages/LoginPage.jsx +6 -4
- package/src/pages/SignUpPage.jsx +4 -2
- package/src/pages/SignupConfirmPage.jsx +4 -2
- package/src/theme/index.js +3 -0
- package/src/theme/kitSxRegistry.js +246 -0
- package/src/theme/themeCompleteness.js +227 -0
- package/tests/themeCompleteness.test.js +199 -0
- package/work-orders/THEME-4.md +86 -0
|
@@ -1,13 +1,28 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
1
4
|
import { createTheme } from '@mui/material/styles';
|
|
2
5
|
import { describe, expect, it } from 'vitest';
|
|
3
6
|
|
|
4
7
|
import {
|
|
5
8
|
assertThemeComplete,
|
|
9
|
+
assertKitSxDisjoint,
|
|
6
10
|
calculateContrastRatio,
|
|
7
11
|
createAppTheme,
|
|
12
|
+
KIT_COMPONENT_SX_REGISTRY,
|
|
13
|
+
reportKitSxBypasses,
|
|
8
14
|
THEME_COMPLETENESS_SURFACES,
|
|
9
15
|
} from '../src/theme';
|
|
10
16
|
|
|
17
|
+
function jsxSources(directory) {
|
|
18
|
+
return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
|
19
|
+
const entryPath = path.join(directory, entry.name);
|
|
20
|
+
if (entry.isDirectory()) return jsxSources(entryPath);
|
|
21
|
+
if (!entry.name.endsWith('.jsx')) return [];
|
|
22
|
+
return [{ path: path.relative(process.cwd(), entryPath), source: fs.readFileSync(entryPath, 'utf8') }];
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
11
26
|
describe('theme completeness', () => {
|
|
12
27
|
it('finds untouched MUI surfaces and accepts createAppTheme', () => {
|
|
13
28
|
const bare = assertThemeComplete(createTheme());
|
|
@@ -154,3 +169,187 @@ describe('theme completeness', () => {
|
|
|
154
169
|
);
|
|
155
170
|
});
|
|
156
171
|
});
|
|
172
|
+
|
|
173
|
+
describe('kit sx disjointness', () => {
|
|
174
|
+
const theme = createAppTheme({ palette: { primary: { main: '#0F62FE' } } });
|
|
175
|
+
|
|
176
|
+
it('finds the real shadowing incident shape and names every part', () => {
|
|
177
|
+
const result = assertKitSxDisjoint(theme, {
|
|
178
|
+
registry: [{
|
|
179
|
+
component: 'FixtureBottomNav',
|
|
180
|
+
muiComponent: 'MuiBottomNavigation',
|
|
181
|
+
sx: { boxShadow: 'none' },
|
|
182
|
+
}],
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(result.findings).toEqual([{
|
|
186
|
+
surface: 'FixtureBottomNav.MuiBottomNavigation.boxShadow',
|
|
187
|
+
reason: expect.stringMatching(/FixtureBottomNav.*MuiBottomNavigation.*boxShadow/),
|
|
188
|
+
}]);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('checks same-root variant slots without conflating child slots', () => {
|
|
192
|
+
const chipResult = assertKitSxDisjoint(theme, {
|
|
193
|
+
registry: [{
|
|
194
|
+
component: 'FixtureChip',
|
|
195
|
+
muiComponent: 'MuiChip',
|
|
196
|
+
sx: { borderColor: 'red' },
|
|
197
|
+
}],
|
|
198
|
+
});
|
|
199
|
+
const bottomNavActionResult = assertKitSxDisjoint(theme, {
|
|
200
|
+
registry: [{
|
|
201
|
+
component: 'FixtureBottomNavAction',
|
|
202
|
+
muiComponent: 'MuiBottomNavigationAction',
|
|
203
|
+
sx: { maxWidth: 'none' },
|
|
204
|
+
}],
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
expect(chipResult.findings.map(({ surface }) => surface))
|
|
208
|
+
.toContain('FixtureChip.MuiChip.borderColor');
|
|
209
|
+
expect(bottomNavActionResult.findings).toEqual([]);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('keeps every registered kit sx object disjoint from the baseline', () => {
|
|
213
|
+
expect(KIT_COMPONENT_SX_REGISTRY.length).toBeGreaterThan(2);
|
|
214
|
+
expect(assertKitSxDisjoint(theme, {
|
|
215
|
+
exemptions: [
|
|
216
|
+
{
|
|
217
|
+
surface: 'UserListComponent.bodyCell.MuiTableCell.py',
|
|
218
|
+
reason: 'The user list intentionally uses a tighter row density than the baseline TableCell default.',
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
surface: 'UserListComponent.emptyCell.MuiTableCell.py',
|
|
222
|
+
reason: 'The empty-state message intentionally uses more vertical breathing room than the baseline TableCell default.',
|
|
223
|
+
},
|
|
224
|
+
],
|
|
225
|
+
}).findings).toEqual([]);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('flags inline sx while accepting a top-level exported object', () => {
|
|
229
|
+
const result = reportKitSxBypasses([
|
|
230
|
+
{
|
|
231
|
+
path: 'InlineFixture.jsx',
|
|
232
|
+
source: 'export function Fixture() { return <Button sx={{ minHeight: 12 }} />; }',
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
path: 'ExportedFixture.jsx',
|
|
236
|
+
source: 'export const FIXTURE_SX = { minHeight: 12 }; export function Fixture() { return <Button sx={FIXTURE_SX} />; }',
|
|
237
|
+
},
|
|
238
|
+
]);
|
|
239
|
+
|
|
240
|
+
expect(result.findings).toHaveLength(1);
|
|
241
|
+
expect(result.findings[0]).toEqual({
|
|
242
|
+
surface: 'InlineFixture.jsx:1.MuiButton.sx',
|
|
243
|
+
reason: expect.stringMatching(/Button.*inline sx object.*exported object/i),
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('flags a local sx identifier and ignores nested JSX props', () => {
|
|
248
|
+
const result = reportKitSxBypasses([
|
|
249
|
+
{
|
|
250
|
+
path: 'LocalFixture.jsx',
|
|
251
|
+
source: 'const LOCAL_SX = { minHeight: 12 }; export function Fixture() { return <Button sx={LOCAL_SX} />; }',
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
path: 'NestedFixture.jsx',
|
|
255
|
+
source: 'export function Fixture() { return <Button startIcon={<Box sx={{ width: 12 }} />} />; }',
|
|
256
|
+
},
|
|
257
|
+
]);
|
|
258
|
+
|
|
259
|
+
expect(result.findings).toEqual([{
|
|
260
|
+
surface: 'LocalFixture.jsx:1.MuiButton.sx',
|
|
261
|
+
reason: expect.stringMatching(/Button.*non-exported sx object.*LOCAL_SX/i),
|
|
262
|
+
}]);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('flags an exported sx object omitted from the registry', () => {
|
|
266
|
+
const result = reportKitSxBypasses([{
|
|
267
|
+
path: 'UnregisteredFixture.jsx',
|
|
268
|
+
source: 'export const UNREGISTERED_SX = { minHeight: 12 }; export function Fixture() { return <Button sx={UNREGISTERED_SX} />; }',
|
|
269
|
+
}], { registry: [] });
|
|
270
|
+
|
|
271
|
+
expect(result.findings).toEqual([{
|
|
272
|
+
surface: 'UnregisteredFixture.jsx:1.MuiButton.sx',
|
|
273
|
+
reason: expect.stringMatching(/Button.*exported sx object.*UNREGISTERED_SX.*MuiButton registry entry/i),
|
|
274
|
+
}]);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('flags an sx object registered against the wrong MUI key', () => {
|
|
278
|
+
const result = reportKitSxBypasses([{
|
|
279
|
+
path: 'WrongTargetFixture.jsx',
|
|
280
|
+
source: 'export const WRONG_TARGET_SX = { minHeight: 12 }; export function Fixture() { return <Button sx={WRONG_TARGET_SX} />; }',
|
|
281
|
+
}], {
|
|
282
|
+
registry: [{ exportName: 'WRONG_TARGET_SX', muiComponent: 'MuiPaper' }],
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
expect(result.findings).toEqual([{
|
|
286
|
+
surface: 'WrongTargetFixture.jsx:1.MuiButton.sx',
|
|
287
|
+
reason: expect.stringMatching(/WRONG_TARGET_SX.*MuiButton registry entry/i),
|
|
288
|
+
}]);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('does not let current kit source bypass the exported-object convention', () => {
|
|
292
|
+
expect(reportKitSxBypasses(
|
|
293
|
+
jsxSources(path.resolve('src')),
|
|
294
|
+
{ registry: KIT_COMPONENT_SX_REGISTRY },
|
|
295
|
+
).findings).toEqual([]);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('suppresses exactly one collision with a reason', () => {
|
|
299
|
+
const registry = [{
|
|
300
|
+
component: 'FixtureButton',
|
|
301
|
+
muiComponent: 'MuiButton',
|
|
302
|
+
sx: { minHeight: 12, textTransform: 'uppercase' },
|
|
303
|
+
}];
|
|
304
|
+
const before = assertKitSxDisjoint(theme, { registry }).findings;
|
|
305
|
+
const after = assertKitSxDisjoint(theme, {
|
|
306
|
+
registry,
|
|
307
|
+
exemptions: [{
|
|
308
|
+
surface: 'FixtureButton.MuiButton.minHeight',
|
|
309
|
+
reason: 'The fixture deliberately overrides one baseline property.',
|
|
310
|
+
}],
|
|
311
|
+
}).findings;
|
|
312
|
+
|
|
313
|
+
expect(before).toHaveLength(2);
|
|
314
|
+
expect(after).toHaveLength(1);
|
|
315
|
+
expect(after[0].surface).toBe('FixtureButton.MuiButton.textTransform');
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('honours a reasoned exemption declared on the theme', () => {
|
|
319
|
+
const exemptTheme = {
|
|
320
|
+
...theme,
|
|
321
|
+
themeCompleteness: {
|
|
322
|
+
...theme.themeCompleteness,
|
|
323
|
+
exemptions: [{
|
|
324
|
+
surface: 'FixtureButton.MuiButton.minHeight',
|
|
325
|
+
reason: 'The fixture deliberately owns its compact height.',
|
|
326
|
+
}],
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
const result = assertKitSxDisjoint(exemptTheme, {
|
|
330
|
+
registry: [{
|
|
331
|
+
component: 'FixtureButton',
|
|
332
|
+
muiComponent: 'MuiButton',
|
|
333
|
+
sx: { minHeight: 12 },
|
|
334
|
+
}],
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
expect(result.findings).toEqual([]);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('reports an exemption without a reason as its own finding', () => {
|
|
341
|
+
const result = assertKitSxDisjoint(theme, {
|
|
342
|
+
registry: [{
|
|
343
|
+
component: 'FixtureButton',
|
|
344
|
+
muiComponent: 'MuiButton',
|
|
345
|
+
sx: { minHeight: 12 },
|
|
346
|
+
}],
|
|
347
|
+
exemptions: [{ surface: 'FixtureButton.MuiButton.minHeight' }],
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
expect(result.findings.map(({ surface }) => surface)).toEqual([
|
|
351
|
+
'exemption.FixtureButton.MuiButton.minHeight',
|
|
352
|
+
'FixtureButton.MuiButton.minHeight',
|
|
353
|
+
]);
|
|
354
|
+
});
|
|
355
|
+
});
|
package/work-orders/THEME-4.md
CHANGED
|
@@ -178,6 +178,92 @@ note, not folded in as tidying.
|
|
|
178
178
|
> `git status` before your own review; an implementer commit is a blocker to surface, and a register
|
|
179
179
|
> row naming a review the Orchestrator did not itself start is invalid.
|
|
180
180
|
|
|
181
|
+
### Context package
|
|
182
|
+
|
|
183
|
+
**Precondition confirmed:** `THEME-3` is landed and published (`1b25117`, register row `done`).
|
|
184
|
+
`git log --oneline -3` at hand-off time shows the Orchestrator's own docs commits on top of it, no
|
|
185
|
+
implementer commit — clean to start from.
|
|
186
|
+
|
|
187
|
+
**The established `sx`-export convention (scope item 1's shape to follow), already shipped in
|
|
188
|
+
`src/components/MobileBottomNav.jsx`:**
|
|
189
|
+
```js
|
|
190
|
+
export const MOBILE_BOTTOM_NAV_ROOT_SX = {
|
|
191
|
+
position: 'fixed', left: 0, right: 0, bottom: 0,
|
|
192
|
+
borderTop: '1px solid', borderColor: 'divider',
|
|
193
|
+
paddingBottom: 'env(safe-area-inset-bottom)',
|
|
194
|
+
};
|
|
195
|
+
export const MOBILE_BOTTOM_NAV_ACTION_SX = { minWidth: 0, maxWidth: 'none' };
|
|
196
|
+
```
|
|
197
|
+
Both are plain top-level `export const` object literals, used directly as (or spread into) the
|
|
198
|
+
`sx` prop on the MUI element they style. This is the shape the registry (item 1) should match
|
|
199
|
+
against — a component "carries its own `sx` for a MUI component the baseline styles" means: look for
|
|
200
|
+
`<BottomNavigation`, `<Button`, `<Card`, etc. (the 23 baseline-styled MUI component names) receiving
|
|
201
|
+
an `sx=` prop inside `src/**/*.jsx`, then check whether that `sx` is a top-level exported identifier
|
|
202
|
+
(covered) or an inline/conditional literal (item 3's bypass check should flag the latter if it isn't
|
|
203
|
+
exported).
|
|
204
|
+
|
|
205
|
+
**The exemption contract to mirror exactly (scope item 2 + required test 4), from
|
|
206
|
+
`src/theme/themeCompleteness.js:333-350` (`assertThemeComplete`):**
|
|
207
|
+
```js
|
|
208
|
+
export function assertThemeComplete(theme, { exemptions = [] } = {}) {
|
|
209
|
+
const declaredExemptions = [...(theme?.themeCompleteness?.exemptions || []), ...exemptions];
|
|
210
|
+
const validExemptions = new Set();
|
|
211
|
+
const findings = [];
|
|
212
|
+
for (const exemption of declaredExemptions) {
|
|
213
|
+
if (!exemption?.surface || !exemption?.reason?.trim()) {
|
|
214
|
+
findings.push({ surface: `exemption.${exemption?.surface || 'unknown'}`,
|
|
215
|
+
reason: 'A completeness exemption must name a surface and include a reason.' });
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
validExemptions.add(exemption.surface);
|
|
219
|
+
}
|
|
220
|
+
...
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
The new check's `{ findings: [{ surface, reason }] }` return shape and exemption handling should be
|
|
224
|
+
structurally identical to this, not a reinvention — same two failure modes (missing reason is itself
|
|
225
|
+
a finding; a valid exemption suppresses exactly one finding).
|
|
226
|
+
|
|
227
|
+
**The bypass-check precedent (scope item 3), from `src/theme/themeCompleteness.js:370-379`
|
|
228
|
+
(`reportThemeAdoption`):**
|
|
229
|
+
```js
|
|
230
|
+
export function reportThemeAdoption(sources = []) {
|
|
231
|
+
const normalized = sources.map((entry, index) => typeof entry === 'string'
|
|
232
|
+
? { path: `source-${index + 1}`, source: entry }
|
|
233
|
+
: { path: entry.path || `source-${index + 1}`, source: entry.source || '' });
|
|
234
|
+
const themeBypassPaths = normalized
|
|
235
|
+
.filter(({ source }) => /\bcreateTheme\s*\(/.test(source) && !/\bcreateAppTheme\s*\(/.test(source))
|
|
236
|
+
.map(({ path }) => path);
|
|
237
|
+
...
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
Same module, same input shape (an array of `{ path, source }` or raw source strings), same
|
|
241
|
+
regex-over-source-text technique — the new bypass check takes the same input shape and regexes for
|
|
242
|
+
an inline `sx={{ ... }}` object literal that is not a reference to a top-level exported identifier.
|
|
243
|
+
|
|
244
|
+
**Where to put the new code:** co-locate in `src/theme/themeCompleteness.js` alongside
|
|
245
|
+
`reportThemeAdoption` (the WO's own "same module" language for item 3), or a new sibling file if
|
|
246
|
+
the additions make that file unwieldy — Codex's call, but export everything new from
|
|
247
|
+
`src/theme/index.js` and `src/index.js` (follow the existing export list pattern in both) so the
|
|
248
|
+
check is part of the public API per the WO's "additive API is a minor" versioning note.
|
|
249
|
+
|
|
250
|
+
**Discovery is the actual task, not pre-solved here:** the WO's own measurement (51 `.jsx` files
|
|
251
|
+
under `src/` carry `sx=`, baseline styles 23 MUI keys) is a starting estimate, not a fixed list —
|
|
252
|
+
grep `src/**/*.jsx` for `sx=` against the 23 keys named in the Envelope's "Why the whole kit,
|
|
253
|
+
measured" section to build the actual registry. This is scope item 1 itself; do not skip it by
|
|
254
|
+
registering only `MobileBottomNav`'s two already-known exports.
|
|
255
|
+
|
|
256
|
+
**On the "stop and report if large" clause:** if fixing every real finding from test 2 would grow
|
|
257
|
+
this WO well beyond a layer-relocation pass (i.e. touches component *behaviour*, not just moving an
|
|
258
|
+
`sx` value into an export), STOP before committing anything and report the finding list instead of
|
|
259
|
+
silently absorbing it all — this is explicitly named in the Envelope as an operator-level scope
|
|
260
|
+
decision, not an implementer judgment call.
|
|
261
|
+
|
|
262
|
+
**Known repeat-offense warning, restated because it has happened on the two immediately preceding
|
|
263
|
+
work orders in this exact repo:** do not run `git commit` or `git push` under any circumstance. Leave
|
|
264
|
+
the diff for the Orchestrator. If you find yourself about to commit "because everything passed", stop
|
|
265
|
+
— that decision is not yours in this workflow, regardless of how clean the result looks.
|
|
266
|
+
|
|
181
267
|
### Mini-handover
|
|
182
268
|
|
|
183
269
|
Repo: `ui-core-micha` (`C:\Users\biglmi\Documents\webapps\ui-core-micha`), branch `main`.
|