@micha.bigler/ui-core-micha 2.33.2 → 2.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/WORK_ORDERS.md +9 -2
  2. package/dist/auth/ReauthModal.js +3 -1
  3. package/dist/components/AccessCodeManager.js +16 -14
  4. package/dist/components/AccessCodeSingleUseToggle.js +2 -1
  5. package/dist/components/AllowedEmailDomainsManager.js +3 -1
  6. package/dist/components/AuthFactorRequirementCard.js +2 -1
  7. package/dist/components/BulkInviteCsvTab.js +7 -7
  8. package/dist/components/LoginForm.js +2 -1
  9. package/dist/components/MFAComponent.js +15 -8
  10. package/dist/components/MfaLoginComponent.js +4 -1
  11. package/dist/components/PasskeysComponent.js +4 -2
  12. package/dist/components/QrSignupManager.js +2 -1
  13. package/dist/components/QrSignupValidityManager.js +4 -1
  14. package/dist/components/RegistrationMethodsManager.js +3 -1
  15. package/dist/components/SecurityComponent.js +3 -1
  16. package/dist/components/SocialLoginButtons.js +2 -1
  17. package/dist/components/SupportRecoveryRequestsTab.js +4 -1
  18. package/dist/components/UserInviteComponent.js +8 -8
  19. package/dist/components/UserListComponent.js +8 -4
  20. package/dist/components/charts/ChartFrame.js +8 -3
  21. package/dist/components/charts/TimeSeriesChart.js +7 -4
  22. package/dist/components/charts/exportChart.js +2 -2
  23. package/dist/index.js +1 -1
  24. package/dist/layout/PageLayout.js +3 -2
  25. package/dist/messaging/AttachmentList.js +6 -1
  26. package/dist/messaging/Composer.js +2 -1
  27. package/dist/messaging/DirectMessageLauncher.js +2 -1
  28. package/dist/messaging/MessageBubble.js +29 -8
  29. package/dist/messaging/PollCard.js +6 -4
  30. package/dist/messaging/Thread.js +2 -1
  31. package/dist/notifications/NotificationSettings.js +17 -3
  32. package/dist/notifications/PopupSurface.js +2 -1
  33. package/dist/onboarding/steps/BrowserPushStep.js +2 -1
  34. package/dist/pages/AccountPage.js +2 -1
  35. package/dist/pages/LoginPage.js +2 -1
  36. package/dist/pages/SignUpPage.js +2 -1
  37. package/dist/pages/SignupConfirmPage.js +2 -1
  38. package/dist/theme/index.js +2 -1
  39. package/dist/theme/kitSxRegistry.js +187 -0
  40. package/dist/theme/themeCompleteness.js +422 -1
  41. package/package.json +1 -1
  42. package/src/auth/ReauthModal.jsx +5 -2
  43. package/src/components/AccessCodeManager.jsx +22 -20
  44. package/src/components/AccessCodeSingleUseToggle.jsx +4 -2
  45. package/src/components/AllowedEmailDomainsManager.jsx +6 -3
  46. package/src/components/AuthFactorRequirementCard.jsx +4 -2
  47. package/src/components/BulkInviteCsvTab.jsx +11 -10
  48. package/src/components/LoginForm.jsx +5 -3
  49. package/src/components/MFAComponent.jsx +20 -12
  50. package/src/components/MfaLoginComponent.jsx +9 -5
  51. package/src/components/PasskeysComponent.jsx +7 -4
  52. package/src/components/QrSignupManager.jsx +6 -4
  53. package/src/components/QrSignupValidityManager.jsx +8 -4
  54. package/src/components/RegistrationMethodsManager.jsx +7 -4
  55. package/src/components/SecurityComponent.jsx +10 -7
  56. package/src/components/SocialLoginButtons.jsx +2 -1
  57. package/src/components/SupportRecoveryRequestsTab.jsx +8 -4
  58. package/src/components/UserInviteComponent.jsx +10 -10
  59. package/src/components/UserListComponent.jsx +12 -7
  60. package/src/components/charts/ChartFrame.jsx +13 -3
  61. package/src/components/charts/TimeSeriesChart.jsx +8 -4
  62. package/src/components/charts/exportChart.js +2 -2
  63. package/src/index.js +4 -0
  64. package/src/layout/PageLayout.jsx +4 -2
  65. package/src/messaging/AttachmentList.jsx +9 -3
  66. package/src/messaging/Composer.jsx +3 -1
  67. package/src/messaging/DirectMessageLauncher.jsx +3 -1
  68. package/src/messaging/MessageBubble.jsx +31 -9
  69. package/src/messaging/PollCard.jsx +8 -6
  70. package/src/messaging/Thread.jsx +3 -1
  71. package/src/notifications/NotificationSettings.jsx +24 -9
  72. package/src/notifications/PopupSurface.jsx +3 -1
  73. package/src/onboarding/steps/BrowserPushStep.jsx +3 -1
  74. package/src/pages/AccountPage.jsx +11 -9
  75. package/src/pages/LoginPage.jsx +6 -4
  76. package/src/pages/SignUpPage.jsx +4 -2
  77. package/src/pages/SignupConfirmPage.jsx +4 -2
  78. package/src/theme/index.js +4 -0
  79. package/src/theme/kitSxRegistry.js +246 -0
  80. package/src/theme/themeCompleteness.js +451 -1
  81. package/tests/ChartFrame.test.jsx +5 -0
  82. package/tests/themeCompleteness.test.js +351 -0
  83. package/work-orders/THEME-4.md +86 -0
  84. package/work-orders/THEME-5.md +341 -0
@@ -1,5 +1,6 @@
1
1
  import { createTheme, getContrastRatio as getMuiContrastRatio } from '@mui/material/styles';
2
- import { STATUS_KEYS, TYPOGRAPHY_VARIANTS } from './tokens';
2
+ import { BASELINE_PALETTE, BASELINE_STATIC, STATUS_KEYS, TYPOGRAPHY_VARIANTS, } from './tokens';
3
+ import { KIT_COMPONENT_SX_REGISTRY } from './kitSxRegistry';
3
4
  const MUI_DEFAULT_THEME = createTheme();
4
5
  function getPath(value, path) {
5
6
  return path.split('.').reduce((current, key) => current === null || current === void 0 ? void 0 : current[key], value);
@@ -336,6 +337,426 @@ export function assertThemeComplete(theme, { exemptions = [] } = {}) {
336
337
  findings.push(...contrastFindings(theme));
337
338
  return { findings };
338
339
  }
340
+ const SAME_ROOT_STYLE_OVERRIDE_SLOTS = {
341
+ MuiAlert: ['standardSuccess', 'standardWarning', 'standardError', 'standardInfo'],
342
+ MuiButton: ['root', 'contained', 'outlined'],
343
+ MuiChip: ['root', 'outlined'],
344
+ };
345
+ // MUI's sx prop accepts aliases and CSS spacing shorthands that
346
+ // styleOverrides' plain CSS-in-JS objects do not. Each known alias is
347
+ // expanded to the canonical CSS properties it can affect so the
348
+ // disjointness check compares effects, not raw key spellings; a property
349
+ // outside this table expands to itself.
350
+ const SPACING_SHORTHAND_LONGHANDS = {
351
+ bgcolor: ['backgroundColor'],
352
+ typography: [
353
+ 'fontFamily',
354
+ 'fontSize',
355
+ 'fontStyle',
356
+ 'fontWeight',
357
+ 'letterSpacing',
358
+ 'lineHeight',
359
+ 'textTransform',
360
+ ],
361
+ p: ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'],
362
+ padding: ['paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft'],
363
+ px: ['paddingLeft', 'paddingRight'],
364
+ paddingX: ['paddingLeft', 'paddingRight'],
365
+ py: ['paddingTop', 'paddingBottom'],
366
+ paddingY: ['paddingTop', 'paddingBottom'],
367
+ pt: ['paddingTop'],
368
+ pr: ['paddingRight'],
369
+ pb: ['paddingBottom'],
370
+ pl: ['paddingLeft'],
371
+ m: ['marginTop', 'marginRight', 'marginBottom', 'marginLeft'],
372
+ margin: ['marginTop', 'marginRight', 'marginBottom', 'marginLeft'],
373
+ mx: ['marginLeft', 'marginRight'],
374
+ marginX: ['marginLeft', 'marginRight'],
375
+ my: ['marginTop', 'marginBottom'],
376
+ marginY: ['marginTop', 'marginBottom'],
377
+ mt: ['marginTop'],
378
+ mr: ['marginRight'],
379
+ mb: ['marginBottom'],
380
+ ml: ['marginLeft'],
381
+ };
382
+ function expandShorthandProperty(property) {
383
+ return SPACING_SHORTHAND_LONGHANDS[property] || [property];
384
+ }
385
+ function styleOverrideKeys(componentConfig, slots = ['root']) {
386
+ var _a;
387
+ const keys = new Set();
388
+ for (const slot of slots) {
389
+ const slotStyles = (_a = componentConfig === null || componentConfig === void 0 ? void 0 : componentConfig.styleOverrides) === null || _a === void 0 ? void 0 : _a[slot];
390
+ if (!slotStyles || typeof slotStyles !== 'object' || Array.isArray(slotStyles))
391
+ continue;
392
+ Object.keys(slotStyles).forEach((key) => expandShorthandProperty(key).forEach((longhand) => keys.add(longhand)));
393
+ }
394
+ return keys;
395
+ }
396
+ /**
397
+ * Reports top-level property collisions between registered kit sx objects and
398
+ * the baseline MUI component they target. Known sx aliases are compared by
399
+ * the CSS properties they affect, not by literal key name.
400
+ *
401
+ * This is deliberately a lower bound on shadowing: unknown normalisation gaps
402
+ * remain. A clean result proves only that the registered top-level key sets
403
+ * are disjoint; it is not proof that no component can shadow the baseline.
404
+ */
405
+ export function assertKitSxDisjoint(theme, { registry = KIT_COMPONENT_SX_REGISTRY, exemptions = [] } = {}) {
406
+ var _a, _b, _c;
407
+ const declaredExemptions = [
408
+ ...(((_a = theme === null || theme === void 0 ? void 0 : theme.themeCompleteness) === null || _a === void 0 ? void 0 : _a.exemptions) || []),
409
+ ...exemptions,
410
+ ];
411
+ const validExemptions = new Set();
412
+ const findings = [];
413
+ for (const exemption of declaredExemptions) {
414
+ if (!(exemption === null || exemption === void 0 ? void 0 : exemption.surface) || !((_b = exemption === null || exemption === void 0 ? void 0 : exemption.reason) === null || _b === void 0 ? void 0 : _b.trim())) {
415
+ findings.push({
416
+ surface: `exemption.${(exemption === null || exemption === void 0 ? void 0 : exemption.surface) || 'unknown'}`,
417
+ reason: 'A completeness exemption must name a surface and include a reason.',
418
+ });
419
+ continue;
420
+ }
421
+ validExemptions.add(exemption.surface);
422
+ }
423
+ for (const entry of registry) {
424
+ const baselineKeys = styleOverrideKeys((_c = theme === null || theme === void 0 ? void 0 : theme.components) === null || _c === void 0 ? void 0 : _c[entry.muiComponent], entry.slots || SAME_ROOT_STYLE_OVERRIDE_SLOTS[entry.muiComponent]);
425
+ for (const property of Object.keys(entry.sx || {})) {
426
+ const surface = `${entry.component}.${entry.muiComponent}.${property}`;
427
+ const collides = expandShorthandProperty(property).some((longhand) => baselineKeys.has(longhand));
428
+ if (collides && !validExemptions.has(surface)) {
429
+ findings.push({
430
+ surface,
431
+ reason: `${entry.component} sx shadows ${entry.muiComponent} styleOverrides property "${property}".`,
432
+ });
433
+ }
434
+ }
435
+ }
436
+ return { findings };
437
+ }
438
+ const BASELINE_STYLED_MUI_COMPONENTS = [
439
+ 'Alert',
440
+ 'BottomNavigation',
441
+ 'BottomNavigationAction',
442
+ 'Button',
443
+ 'Card',
444
+ 'Checkbox',
445
+ 'Chip',
446
+ 'Container',
447
+ 'CssBaseline',
448
+ 'Dialog',
449
+ 'Divider',
450
+ 'Drawer',
451
+ 'FilledInput',
452
+ 'FormControlLabel',
453
+ 'IconButton',
454
+ 'Input',
455
+ 'Menu',
456
+ 'OutlinedInput',
457
+ 'Paper',
458
+ 'Select',
459
+ 'TableCell',
460
+ 'TextField',
461
+ 'Tooltip',
462
+ ];
463
+ function jsxOpeningTag(source, start) {
464
+ let braces = 0;
465
+ let quote = null;
466
+ for (let index = start; index < source.length; index += 1) {
467
+ const character = source[index];
468
+ if (quote) {
469
+ if (character === quote && source[index - 1] !== '\\')
470
+ quote = null;
471
+ continue;
472
+ }
473
+ if (character === '"' || character === "'" || character === '`') {
474
+ quote = character;
475
+ }
476
+ else if (character === '{') {
477
+ braces += 1;
478
+ }
479
+ else if (character === '}') {
480
+ braces = Math.max(0, braces - 1);
481
+ }
482
+ else if (character === '>' && braces === 0) {
483
+ return source.slice(start, index + 1);
484
+ }
485
+ }
486
+ return source.slice(start);
487
+ }
488
+ function topLevelSxValue(tag) {
489
+ var _a;
490
+ let braces = 0;
491
+ let quote = null;
492
+ for (let index = 0; index < tag.length; index += 1) {
493
+ const character = tag[index];
494
+ if (quote) {
495
+ if (character === quote && tag[index - 1] !== '\\')
496
+ quote = null;
497
+ continue;
498
+ }
499
+ if (character === '"' || character === "'" || character === '`') {
500
+ quote = character;
501
+ continue;
502
+ }
503
+ if (braces === 0 && (index === 0 || /\s/.test(tag[index - 1]))) {
504
+ const attribute = (_a = tag.slice(index).match(/^sx\b\s*=\s*/)) === null || _a === void 0 ? void 0 : _a[0];
505
+ if (attribute)
506
+ return tag.slice(index + attribute.length);
507
+ }
508
+ if (character === '{')
509
+ braces += 1;
510
+ if (character === '}')
511
+ braces = Math.max(0, braces - 1);
512
+ }
513
+ return '';
514
+ }
515
+ /**
516
+ * Reports baseline-styled MUI tags that bypass the exported-object convention.
517
+ * Like assertKitSxDisjoint, this is a documented lower bound: conditional,
518
+ * array, callback, and otherwise nested sx expressions are not inspected.
519
+ */
520
+ export function reportKitSxBypasses(sources = [], { registry } = {}) {
521
+ var _a, _b;
522
+ const normalized = sources.map((entry, index) => typeof entry === 'string'
523
+ ? { path: `source-${index + 1}`, source: entry }
524
+ : { path: entry.path || `source-${index + 1}`, source: entry.source || '' });
525
+ const findings = [];
526
+ const registeredTargets = registry
527
+ ? registry.reduce((targets, { exportName, muiComponent }) => {
528
+ if (!exportName || !muiComponent)
529
+ return targets;
530
+ if (!targets.has(exportName))
531
+ targets.set(exportName, new Set());
532
+ targets.get(exportName).add(muiComponent);
533
+ return targets;
534
+ }, new Map())
535
+ : null;
536
+ const componentPattern = BASELINE_STYLED_MUI_COMPONENTS.join('|');
537
+ const tagPattern = new RegExp(`<(${componentPattern})\\b`, 'g');
538
+ for (const { path, source } of normalized) {
539
+ const exportedObjects = new Set([...source.matchAll(/\bexport\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*\{/g)]
540
+ .map((match) => match[1]));
541
+ for (const match of source.matchAll(tagPattern)) {
542
+ const tag = jsxOpeningTag(source, match.index);
543
+ const sxValue = topLevelSxValue(tag);
544
+ const inline = /^\{\s*\{/.test(sxValue);
545
+ const identifier = (_a = sxValue.match(/^\{\s*([A-Za-z_$][\w$]*)\s*\}/)) === null || _a === void 0 ? void 0 : _a[1];
546
+ const exported = identifier && exportedObjects.has(identifier);
547
+ const muiComponent = `Mui${match[1]}`;
548
+ const unregistered = exported
549
+ && registeredTargets
550
+ && !((_b = registeredTargets.get(identifier)) === null || _b === void 0 ? void 0 : _b.has(muiComponent));
551
+ if (!inline && (!identifier || (exported && !unregistered)))
552
+ continue;
553
+ const line = source.slice(0, match.index).split(/\r?\n/).length;
554
+ findings.push({
555
+ surface: `${path}:${line}.Mui${match[1]}.sx`,
556
+ reason: inline
557
+ ? `${match[1]} uses an inline sx object instead of a top-level exported object.`
558
+ : unregistered
559
+ ? `${match[1]} uses exported sx object "${identifier}" without a ${muiComponent} registry entry.`
560
+ : `${match[1]} uses non-exported sx object "${identifier}".`,
561
+ });
562
+ }
563
+ }
564
+ return { findings };
565
+ }
566
+ const BASIC_CSS_COLOUR_NAMES = new Set([
567
+ 'aqua',
568
+ 'black',
569
+ 'blue',
570
+ 'fuchsia',
571
+ 'gray',
572
+ 'green',
573
+ 'grey',
574
+ 'lime',
575
+ 'maroon',
576
+ 'navy',
577
+ 'olive',
578
+ 'purple',
579
+ 'red',
580
+ 'silver',
581
+ 'teal',
582
+ 'white',
583
+ 'yellow',
584
+ ]);
585
+ const MUI_DEFAULT_NUMERIC_RAMP_NAMES = new Set([
586
+ 'amber',
587
+ 'blue',
588
+ 'bluegrey',
589
+ 'brown',
590
+ 'cyan',
591
+ 'deeporange',
592
+ 'deeppurple',
593
+ 'green',
594
+ 'grey',
595
+ 'indigo',
596
+ 'lightblue',
597
+ 'lightgreen',
598
+ 'lime',
599
+ 'orange',
600
+ 'pink',
601
+ 'purple',
602
+ 'red',
603
+ 'teal',
604
+ 'yellow',
605
+ ]);
606
+ const HEX_COLOUR_PATTERN = /#(?:[\da-f]{3,4}|[\da-f]{6}|[\da-f]{8})(?![\da-f])/gi;
607
+ const RGB_COLOUR_PATTERN = /rgba?\(\s*[\d.]+\s*,\s*[\d.]+\s*,\s*[\d.]+(?:\s*,\s*[\d.]+)?\s*\)/gi;
608
+ const NAMED_COLOUR_PATTERN = new RegExp(`\\b(?:${[...BASIC_CSS_COLOUR_NAMES].join('|')})\\b`, 'gi');
609
+ const COLOUR_PROPERTY_PATTERN = /\b(?:background(?:Color)?|bgcolor|border(?:Top|Right|Bottom|Left)?(?:Color)?|boxShadow|caretColor|color|fill|fillStyle|outline(?:Color)?|stroke|strokeStyle|textShadow|WebkitTextFillColor)\s*(?::|=)\s*$/;
610
+ function colourKey(value) {
611
+ const colour = parseColour(value);
612
+ return colour ? `${colour.r},${colour.g},${colour.b},${colour.a}` : null;
613
+ }
614
+ function collectBaselineColourKeys(...roots) {
615
+ const keys = new Set();
616
+ const visit = (value) => {
617
+ if (typeof value === 'string') {
618
+ for (const match of value.matchAll(HEX_COLOUR_PATTERN)) {
619
+ const key = colourKey(match[0]);
620
+ if (key)
621
+ keys.add(key);
622
+ }
623
+ for (const match of value.matchAll(RGB_COLOUR_PATTERN)) {
624
+ const key = colourKey(match[0]);
625
+ if (key)
626
+ keys.add(key);
627
+ }
628
+ return;
629
+ }
630
+ if (Array.isArray(value)) {
631
+ value.forEach(visit);
632
+ return;
633
+ }
634
+ if (value && typeof value === 'object')
635
+ Object.values(value).forEach(visit);
636
+ };
637
+ roots.forEach(visit);
638
+ return keys;
639
+ }
640
+ function maskComments(source) {
641
+ const characters = [...source];
642
+ let quote = null;
643
+ for (let index = 0; index < characters.length; index += 1) {
644
+ const character = characters[index];
645
+ if (quote) {
646
+ if (character === quote && characters[index - 1] !== '\\')
647
+ quote = null;
648
+ continue;
649
+ }
650
+ if (character === '"' || character === "'" || character === '`') {
651
+ quote = character;
652
+ continue;
653
+ }
654
+ if (character === '/' && characters[index + 1] === '/') {
655
+ for (; index < characters.length && characters[index] !== '\n'; index += 1) {
656
+ characters[index] = ' ';
657
+ }
658
+ index -= 1;
659
+ }
660
+ else if (character === '/' && characters[index + 1] === '*') {
661
+ characters[index] = ' ';
662
+ characters[index + 1] = ' ';
663
+ index += 2;
664
+ while (index < characters.length
665
+ && !(characters[index] === '*' && characters[index + 1] === '/')) {
666
+ if (characters[index] !== '\n' && characters[index] !== '\r')
667
+ characters[index] = ' ';
668
+ index += 1;
669
+ }
670
+ if (index < characters.length) {
671
+ characters[index] = ' ';
672
+ characters[index + 1] = ' ';
673
+ index += 1;
674
+ }
675
+ }
676
+ }
677
+ return characters.join('');
678
+ }
679
+ function maskQrPrintDocument(source, path) {
680
+ if (!path.replaceAll('\\', '/').endsWith('/QrSignupManager.jsx'))
681
+ return source;
682
+ const call = /\bprintWindow\.document\.write\s*\(\s*`/g.exec(source);
683
+ if (!call)
684
+ return source;
685
+ const start = call.index + call[0].lastIndexOf('`');
686
+ let end = start + 1;
687
+ while (end < source.length) {
688
+ if (source[end] === '`' && source[end - 1] !== '\\')
689
+ break;
690
+ end += 1;
691
+ }
692
+ if (end >= source.length)
693
+ return source;
694
+ // The generated print document is standalone HTML rendered outside the
695
+ // React tree, where no application theme is available. Mask the region as
696
+ // one unit instead of accumulating permanent per-colour exemptions.
697
+ return source.slice(0, start)
698
+ + source.slice(start, end + 1).replace(/[^\r\n]/g, ' ')
699
+ + source.slice(end + 1);
700
+ }
701
+ function normalizedSources(sources) {
702
+ return sources.map((entry, index) => typeof entry === 'string'
703
+ ? { path: `source-${index + 1}`, source: entry }
704
+ : { path: entry.path || `source-${index + 1}`, source: entry.source || '' });
705
+ }
706
+ /**
707
+ * Reports colour literals in component source that do not come from the
708
+ * baseline palette, plus report-only uses of MUI's untouched numeric ramps.
709
+ */
710
+ export function reportOffPaletteColours(sources = [], { palette = BASELINE_PALETTE, baselineStatic = BASELINE_STATIC } = {}) {
711
+ const allowedColours = collectBaselineColourKeys(palette, baselineStatic);
712
+ const findings = [];
713
+ const stringPattern = /(["'`])((?:\\.|(?!\1)[\s\S])*)\1/g;
714
+ for (const { path, source } of normalizedSources(sources)) {
715
+ const normalizedPath = path.replaceAll('\\', '/');
716
+ if (/(^|\/)src\/theme\//.test(normalizedPath))
717
+ continue;
718
+ const scanSource = maskComments(maskQrPrintDocument(source, normalizedPath));
719
+ for (const match of scanSource.matchAll(stringPattern)) {
720
+ const value = match[2].trim();
721
+ const line = source.slice(0, match.index).split(/\r?\n/).length;
722
+ const surface = `${path}:${line}.colour`;
723
+ for (const hex of value.matchAll(HEX_COLOUR_PATTERN)) {
724
+ if (!allowedColours.has(colourKey(hex[0]))) {
725
+ findings.push({
726
+ surface,
727
+ reason: `Off-palette hex colour "${hex[0]}" is not derived from the baseline palette.`,
728
+ });
729
+ }
730
+ }
731
+ for (const rgb of value.matchAll(RGB_COLOUR_PATTERN)) {
732
+ if (!allowedColours.has(colourKey(rgb[0]))) {
733
+ findings.push({
734
+ surface,
735
+ reason: `Off-palette rgb colour "${rgb[0]}" is not derived from the baseline palette.`,
736
+ });
737
+ }
738
+ }
739
+ const before = scanSource.slice(Math.max(0, match.index - 40), match.index);
740
+ const isColourProperty = COLOUR_PROPERTY_PATTERN.test(before);
741
+ const numericRamp = value.match(/^([a-z]+)\.(\d+)$/i);
742
+ if (isColourProperty && !numericRamp) {
743
+ for (const namedColour of value.matchAll(NAMED_COLOUR_PATTERN)) {
744
+ findings.push({
745
+ surface,
746
+ reason: `Named CSS colour "${namedColour[0]}" bypasses the baseline palette.`,
747
+ });
748
+ }
749
+ }
750
+ if (numericRamp && MUI_DEFAULT_NUMERIC_RAMP_NAMES.has(numericRamp[1].toLowerCase())) {
751
+ findings.push({
752
+ surface,
753
+ reason: `Report only: "${value}" uses MUI's untouched numeric palette ramp, not a baseline token.`,
754
+ });
755
+ }
756
+ }
757
+ }
758
+ return { findings };
759
+ }
339
760
  /** Reports adoption signals as numbers; it never changes completeness. */
340
761
  export function reportThemeAdoption(sources = []) {
341
762
  const normalized = sources.map((entry, index) => typeof entry === 'string'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@micha.bigler/ui-core-micha",
3
- "version": "2.33.2",
3
+ "version": "2.35.0",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.js",
6
6
  "repository": {
@@ -13,6 +13,9 @@ import apiClient from './apiClient';
13
13
  import { HEADLESS_BASE } from './authConfig';
14
14
  import { rejectReauth, resolveReauth, subscribe } from './reauth';
15
15
 
16
+ export const REAUTH_MODAL_ALERT_SX = { mb: 2 };
17
+ export const REAUTH_MODAL_TEXT_FIELD_SX = { mt: 1 };
18
+
16
19
  export function ReauthModal() {
17
20
  const { t } = useTranslation();
18
21
  const [open, setOpen] = useState(false);
@@ -53,7 +56,7 @@ export function ReauthModal() {
53
56
  <form onSubmit={handleSubmit}>
54
57
  <DialogTitle>{t('Auth.REAUTH_TITLE')}</DialogTitle>
55
58
  <DialogContent>
56
- {error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
59
+ {error && <Alert severity="error" sx={REAUTH_MODAL_ALERT_SX}>{error}</Alert>}
57
60
  <TextField
58
61
  autoFocus
59
62
  fullWidth
@@ -62,7 +65,7 @@ export function ReauthModal() {
62
65
  value={password}
63
66
  onChange={(e) => setPassword(e.target.value)}
64
67
  disabled={loading}
65
- sx={{ mt: 1 }}
68
+ sx={REAUTH_MODAL_TEXT_FIELD_SX}
66
69
  />
67
70
  </DialogContent>
68
71
  <DialogActions>
@@ -21,15 +21,22 @@ import {
21
21
  deleteAccessCode,
22
22
  } from '../auth/authApi';
23
23
 
24
+ export const ACCESS_CODE_ACTION_SX = {
25
+ minWidth: 120,
26
+ height: 40,
27
+ whiteSpace: 'nowrap',
28
+ };
29
+ export const ACCESS_CODE_PRIMARY_ACTION_SX = { ...ACCESS_CODE_ACTION_SX, mt: 1 };
30
+ export const ACCESS_CODE_ALERT_SX = { mb: 2 };
31
+ export const ACCESS_CODE_VALUE_FIELD_SX = {
32
+ '& .MuiInputBase-input': {
33
+ fontFamily: 'monospace',
34
+ letterSpacing: '0.04em',
35
+ },
36
+ };
37
+
24
38
  export function AccessCodeManager() {
25
39
  const { t } = useTranslation();
26
- const actionButtonSx = {
27
- minWidth: 120,
28
- height: 40,
29
- textTransform: 'none',
30
- whiteSpace: 'nowrap',
31
- };
32
-
33
40
  const [codes, setCodes] = useState([]);
34
41
  const [loading, setLoading] = useState(true);
35
42
  const [submitting, setSubmitting] = useState(false);
@@ -142,17 +149,17 @@ export function AccessCodeManager() {
142
149
  return (
143
150
  <Box>
144
151
  {errorKey && (
145
- <Alert severity="error" sx={{ mb: 2 }}>
152
+ <Alert severity="error" sx={ACCESS_CODE_ALERT_SX}>
146
153
  {t(errorKey)}
147
154
  </Alert>
148
155
  )}
149
156
  {successKey && (
150
- <Alert severity="success" sx={{ mb: 2 }}>
157
+ <Alert severity="success" sx={ACCESS_CODE_ALERT_SX}>
151
158
  {t(successKey)}
152
159
  </Alert>
153
160
  )}
154
161
  {copyNotice && (
155
- <Alert severity="info" sx={{ mb: 2 }}>
162
+ <Alert severity="info" sx={ACCESS_CODE_ALERT_SX}>
156
163
  {copyNotice}
157
164
  </Alert>
158
165
  )}
@@ -190,19 +197,14 @@ export function AccessCodeManager() {
190
197
  onFocus: (event) => event.target.select(),
191
198
  },
192
199
  }}
193
- sx={{
194
- '& .MuiInputBase-input': {
195
- fontFamily: 'monospace',
196
- letterSpacing: '0.04em',
197
- },
198
- }}
200
+ sx={ACCESS_CODE_VALUE_FIELD_SX}
199
201
  />
200
202
  <Button
201
203
  variant="outlined"
202
204
  size="small"
203
205
  onClick={() => handleCopyCode(code.code)}
204
206
  startIcon={<ContentCopyIcon fontSize="small" />}
205
- sx={actionButtonSx}
207
+ sx={ACCESS_CODE_ACTION_SX}
206
208
  >
207
209
  {t('Auth.ACCESS_CODE_COPY_BUTTON', t('Auth.MFA_RECOVERY_COPY_TOOLTIP', 'Kopieren'))}
208
210
  </Button>
@@ -212,7 +214,7 @@ export function AccessCodeManager() {
212
214
  size="small"
213
215
  onClick={() => handleDelete(code.id)}
214
216
  startIcon={<DeleteIcon fontSize="small" />}
215
- sx={actionButtonSx}
217
+ sx={ACCESS_CODE_ACTION_SX}
216
218
  >
217
219
  {t('Common.DELETE', 'Löschen')}
218
220
  </Button>
@@ -246,7 +248,7 @@ export function AccessCodeManager() {
246
248
  <Button
247
249
  variant="contained"
248
250
  size="small"
249
- sx={{ ...actionButtonSx, mt: 1 }}
251
+ sx={ACCESS_CODE_PRIMARY_ACTION_SX}
250
252
  onClick={handleGenerateClick}
251
253
  disabled={submitting}
252
254
  >
@@ -275,7 +277,7 @@ export function AccessCodeManager() {
275
277
  size="small"
276
278
  onClick={handleAddManual}
277
279
  disabled={submitting}
278
- sx={actionButtonSx}
280
+ sx={ACCESS_CODE_ACTION_SX}
279
281
  >
280
282
  {t('Auth.ACCESS_CODE_ADD_BUTTON')}
281
283
  </Button>
@@ -9,6 +9,8 @@ import {
9
9
  import { useTranslation } from 'react-i18next';
10
10
  import { fetchAuthPolicy, updateAuthPolicy } from '../auth/authApi';
11
11
 
12
+ export const ACCESS_CODE_SINGLE_USE_ALERT_SX = { mb: 2 };
13
+
12
14
  export function AccessCodeSingleUseToggle({ canEdit = true, policy = null, onPolicyChange = null }) {
13
15
  const { t } = useTranslation();
14
16
  const [value, setValue] = useState(false);
@@ -72,8 +74,8 @@ export function AccessCodeSingleUseToggle({ canEdit = true, policy = null, onPol
72
74
  'When enabled, each access code can be redeemed only once. Recommended.',
73
75
  )}
74
76
  </Typography>
75
- {error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
76
- {success && <Alert severity="success" sx={{ mb: 2 }}>{success}</Alert>}
77
+ {error && <Alert severity="error" sx={ACCESS_CODE_SINGLE_USE_ALERT_SX}>{error}</Alert>}
78
+ {success && <Alert severity="success" sx={ACCESS_CODE_SINGLE_USE_ALERT_SX}>{success}</Alert>}
77
79
  <FormControlLabel
78
80
  control={
79
81
  <Switch
@@ -9,6 +9,9 @@ import {
9
9
  import { useTranslation } from 'react-i18next';
10
10
  import { updateAuthPolicy } from '../auth/authApi';
11
11
 
12
+ export const ALLOWED_EMAIL_DOMAINS_ALERT_SX = { mb: 2 };
13
+ export const ALLOWED_EMAIL_DOMAINS_BUTTON_SX = { mt: 2 };
14
+
12
15
  export function AllowedEmailDomainsManager({
13
16
  domains = [],
14
17
  enabled = false,
@@ -63,8 +66,8 @@ export function AllowedEmailDomainsManager({
63
66
  )}
64
67
  </Typography>
65
68
 
66
- {error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
67
- {success && <Alert severity="success" sx={{ mb: 2 }}>{success}</Alert>}
69
+ {error && <Alert severity="error" sx={ALLOWED_EMAIL_DOMAINS_ALERT_SX}>{error}</Alert>}
70
+ {success && <Alert severity="success" sx={ALLOWED_EMAIL_DOMAINS_ALERT_SX}>{success}</Alert>}
68
71
 
69
72
  <TextField
70
73
  label={t('Auth.ALLOWED_EMAIL_DOMAINS_LABEL', 'Allowed email domains')}
@@ -80,7 +83,7 @@ export function AllowedEmailDomainsManager({
80
83
  disabled={busy || !canEdit}
81
84
  />
82
85
 
83
- <Button variant="contained" sx={{ mt: 2 }} onClick={handleSave} disabled={busy || !canEdit}>
86
+ <Button variant="contained" sx={ALLOWED_EMAIL_DOMAINS_BUTTON_SX} onClick={handleSave} disabled={busy || !canEdit}>
84
87
  {t('Common.SAVE', 'Save')}
85
88
  </Button>
86
89
  </Box>
@@ -11,6 +11,8 @@ import {
11
11
  import { useTranslation } from 'react-i18next';
12
12
  import { fetchAuthPolicy, updateAuthPolicy } from '../auth/authApi';
13
13
 
14
+ export const AUTH_FACTOR_REQUIREMENT_ALERT_SX = { mb: 2 };
15
+
14
16
  export function AuthFactorRequirementCard({ canEdit = true, policy = null }) {
15
17
  const { t } = useTranslation();
16
18
  const [value, setValue] = useState('1');
@@ -71,8 +73,8 @@ export function AuthFactorRequirementCard({ canEdit = true, policy = null }) {
71
73
  'Define the minimum number of authentication factors required for sign-in.',
72
74
  )}
73
75
  </Typography>
74
- {error && <Alert severity="error" sx={{ mb: 2 }}>{error}</Alert>}
75
- {success && <Alert severity="success" sx={{ mb: 2 }}>{success}</Alert>}
76
+ {error && <Alert severity="error" sx={AUTH_FACTOR_REQUIREMENT_ALERT_SX}>{error}</Alert>}
77
+ {success && <Alert severity="success" sx={AUTH_FACTOR_REQUIREMENT_ALERT_SX}>{success}</Alert>}
76
78
  <FormControl>
77
79
  <RadioGroup value={value} onChange={handleChange}>
78
80
  <FormControlLabel