@scattered-light/loomiere 0.3.0 → 0.4.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.
Files changed (82) hide show
  1. package/README.md +6 -1
  2. package/bin/loomiere.mjs +24 -0
  3. package/build/build-tokens.mjs +93 -49
  4. package/exporters/README.md +1 -1
  5. package/exporters/ag-grid-settings.mjs +7 -7
  6. package/exporters/ag-grid.mjs +6 -6
  7. package/exporters/chakra.mjs +43 -43
  8. package/exporters/component-styles.mjs +4 -4
  9. package/exporters/design-system.mjs +17 -12
  10. package/exporters/figma.mjs +774 -68
  11. package/exporters/mui.mjs +40 -40
  12. package/exporters/shadcn.mjs +5 -13
  13. package/exporters/tailwind.mjs +14 -5
  14. package/figma/README.md +136 -0
  15. package/figma/build-plugin.mjs +25 -0
  16. package/figma/engine.js +730 -0
  17. package/figma/library/README.md +92 -0
  18. package/figma/library/check/known-differences.md +20 -0
  19. package/figma/library/check/mojave.json +246 -0
  20. package/figma/library/kit/base.js +89 -0
  21. package/figma/library/kit/components.js +95 -0
  22. package/figma/library/kit/docs.js +37 -0
  23. package/figma/library/kit/fingerprint.js +143 -0
  24. package/figma/library/pages/02-palettes.js +40 -0
  25. package/figma/library/pages/03-color.js +126 -0
  26. package/figma/library/pages/04-type.js +56 -0
  27. package/figma/library/pages/05-scale.js +58 -0
  28. package/figma/library/pages/06-size.js +111 -0
  29. package/figma/library/pages/07-elevation-focus-motion.js +108 -0
  30. package/figma/library/pages/08-themes.js +36 -0
  31. package/figma/library/pages/09-fields/01-input.js +55 -0
  32. package/figma/library/pages/09-fields/02-checks.js +82 -0
  33. package/figma/library/pages/09-fields/03-field.js +32 -0
  34. package/figma/library/pages/09-fields/04-more-fields.js +144 -0
  35. package/figma/library/pages/09-fields/05-labels.js +14 -0
  36. package/figma/library/pages/09-fields/06-doc.js +75 -0
  37. package/figma/library/pages/10-buttons/01-buttons.js +97 -0
  38. package/figma/library/pages/10-buttons/02-doc.js +55 -0
  39. package/figma/library/pages/11-popups/01-popups.js +166 -0
  40. package/figma/library/pages/11-popups/02-more-popups.js +90 -0
  41. package/figma/library/pages/11-popups/03-labels.js +14 -0
  42. package/figma/library/pages/11-popups/04-doc.js +89 -0
  43. package/figma/library/pages/12-overlays/01-overlays.js +135 -0
  44. package/figma/library/pages/12-overlays/02-labels.js +10 -0
  45. package/figma/library/pages/12-overlays/03-doc.js +80 -0
  46. package/figma/library/pages/13-disclosure/01-disclosure.js +205 -0
  47. package/figma/library/pages/13-disclosure/02-collapsible.js +39 -0
  48. package/figma/library/pages/13-disclosure/03-labels.js +16 -0
  49. package/figma/library/pages/13-disclosure/04-doc.js +91 -0
  50. package/figma/library/pages/14-display/01-display.js +165 -0
  51. package/figma/library/pages/14-display/02-more-display.js +55 -0
  52. package/figma/library/pages/14-display/03-labels.js +14 -0
  53. package/figma/library/pages/14-display/04-doc.js +96 -0
  54. package/figma/library/pages/15-cover.js +87 -0
  55. package/figma/library/run-order.json +21 -0
  56. package/figma/library/tools/compare.mjs +11 -0
  57. package/figma/library/tools/diff-snippet.js +15 -0
  58. package/figma/library/tools/kit-script.mjs +21 -0
  59. package/figma/library/tools/run.mjs +27 -0
  60. package/figma/library/tools/runs.mjs +34 -0
  61. package/figma/plugin/code.js +746 -0
  62. package/figma/plugin/manifest.json +14 -0
  63. package/figma/plugin/ui.html +237 -0
  64. package/figma/plugin-main.js +14 -0
  65. package/generator/brand.mjs +16 -14
  66. package/generator/brand.schema.json +60 -0
  67. package/generator/generate.mjs +2 -2
  68. package/generator/pairs.mjs +6 -1
  69. package/generator/roles.mjs +55 -15
  70. package/generator/vision.mjs +7 -2
  71. package/lib/config.mjs +2 -0
  72. package/lib/contrast.mjs +8 -3
  73. package/lib/figma.mjs +338 -0
  74. package/lib/studio.mjs +4 -0
  75. package/lib/tool-server.mjs +7 -4
  76. package/package.json +5 -1
  77. package/tokens/README.md +10 -5
  78. package/tokens/component/avatar.json +41 -6
  79. package/tokens/semantic/color.json +1 -1
  80. package/tokens/semantic/elevation.json +8 -43
  81. package/tokens/semantic/typography.json +61 -94
  82. package/tools/brand-builder/builder.js +33 -4
@@ -19,8 +19,8 @@ const CHAKRA_SLOTS = { contrast: 'on-solid', subtle: 'soft', muted: 'soft-hover'
19
19
  const STATUS = ['info', 'warning', 'success', 'error'];
20
20
  /** Text styles a Chakra user can name as textStyle="…". */
21
21
  const TEXT_STYLES = [
22
- 'minimize', 'condense', 'base', 'magnify', 'base-label', 'base-selected', 'condense-subtitle-label',
23
- 'condense-trigger', 'base-trigger', 'magnify-trigger', 'maximize-md', 'maximize-lg', 'maximize-xl', 'maximize-2xl', 'maximize-3xl', 'maximize-4xl',
22
+ 'body-minimize', 'body-condense', 'body-base', 'body-magnify', 'label-base', 'selected-base', 'eyebrow-condense',
23
+ 'trigger-condense', 'trigger-base', 'trigger-magnify', 'title-condense', 'title-base', 'title-magnify', 'title-amplify', 'title-maximize',
24
24
  ];
25
25
 
26
26
  /** Merges our recipe pieces into Chakra's own, in the generated file (see RESTYLE below). */
@@ -127,7 +127,7 @@ export default {
127
127
 
128
128
  const field = {
129
129
  ...k.field,
130
- ...text('base'),
130
+ ...text('body-base'),
131
131
  focusVisibleRing: null,
132
132
  focusRingColor: null,
133
133
  _hover: k.fieldStates.hover,
@@ -137,7 +137,7 @@ export default {
137
137
  _placeholder: k.fieldStates.placeholder,
138
138
  };
139
139
  const fieldSize = (tier) => ({
140
- ...text(tier === 'xs' || tier === 'sm' ? 'condense' : tier === 'lg' ? 'magnify' : 'base'),
140
+ ...text(tier === 'xs' || tier === 'sm' ? 'body-condense' : tier === 'lg' ? 'body-magnify' : 'body-base'),
141
141
  textStyle: null,
142
142
  '--input-height': v(`control-height-${tier}`),
143
143
  height: v(`control-height-${tier}`),
@@ -196,7 +196,7 @@ export default {
196
196
 
197
197
  const checkControl = (tier) => ({ boxSize: k.iconSize(tier), '& svg': { width: v(`icon-size-${{ xs: 'minimize', sm: 'minimize', md: 'condense', lg: 'base' }[tier]}`), height: v(`icon-size-${{ xs: 'minimize', sm: 'minimize', md: 'condense', lg: 'base' }[tier]}`) } });
198
198
  const checkRow = { gap: v('gap-control'), color: v('text-primary') };
199
- const label = { ...text('base'), textStyle: null, _disabled: { color: v('text-disabled'), opacity: null } };
199
+ const label = { ...text('body-base'), textStyle: null, _disabled: { color: v('text-disabled'), opacity: null } };
200
200
  const checked = '&:is([data-state=checked], [data-state=indeterminate])';
201
201
 
202
202
  const slotRecipes = {
@@ -204,9 +204,9 @@ export default {
204
204
  base: {
205
205
  root: { gap: v('field-gap') },
206
206
  // A disabled field fades as a whole, as a disabled Base UI Field.Root does.
207
- label: { ...text('base-label'), textStyle: null, gap: v('space-1'), marginBottom: v('field-label-spacing'), color: v('text-primary'), _disabled: { opacity: v('field-opacity-disabled') } },
208
- helperText: { ...text('condense'), textStyle: null, margin: `${v('field-message-spacing')} 0 0`, color: v('text-secondary'), _disabled: { opacity: v('field-opacity-disabled') } },
209
- errorText: { ...text('base-label'), textStyle: null, margin: `${v('field-message-spacing')} 0 0`, color: v('status-error') },
207
+ label: { ...text('label-base'), textStyle: null, gap: v('space-1'), marginBottom: v('field-label-spacing'), color: v('text-primary'), _disabled: { opacity: v('field-opacity-disabled') } },
208
+ helperText: { ...text('body-condense'), textStyle: null, margin: `${v('field-message-spacing')} 0 0`, color: v('text-secondary'), _disabled: { opacity: v('field-opacity-disabled') } },
209
+ errorText: { ...text('label-base'), textStyle: null, margin: `${v('field-message-spacing')} 0 0`, color: v('status-error') },
210
210
  requiredIndicator: { color: v('status-error') },
211
211
  },
212
212
  },
@@ -260,7 +260,7 @@ export default {
260
260
  switch: {
261
261
  base: {
262
262
  root: { gap: v('gap-control'), '--switch-diff': 'calc(var(--switch-width) - var(--switch-height))' },
263
- label: { ...text('base-label'), fontSize: v('font-base-label-font-size'), color: v('text-primary'), _disabled: { color: v('text-disabled'), opacity: null } },
263
+ label: { ...text('label-base'), fontSize: v('font-label-base-font-size'), color: v('text-primary'), _disabled: { color: v('text-disabled'), opacity: null } },
264
264
  control: {
265
265
  boxSizing: 'border-box',
266
266
  alignItems: 'center',
@@ -271,7 +271,7 @@ export default {
271
271
  transition: `background-color ${v('duration-normal')} ${v('easing-in-out')}`,
272
272
  focusVisibleRing: null,
273
273
  _before: {
274
- ...text('condense-subtitle-label'),
274
+ ...text('eyebrow-condense'),
275
275
  content: '"off"',
276
276
  position: 'absolute',
277
277
  insetBlock: 0,
@@ -318,9 +318,9 @@ export default {
318
318
  },
319
319
  slider: {
320
320
  base: {
321
- root: { ...text('base'), textStyle: null, gap: v('gauge-gap'), _disabled: { opacity: v('check-opacity-disabled') } },
322
- label: { ...text('base-label'), textStyle: null, color: v('gauge-label-color') },
323
- valueText: { ...text('base'), color: v('gauge-value-color') },
321
+ root: { ...text('body-base'), textStyle: null, gap: v('gauge-gap'), _disabled: { opacity: v('check-opacity-disabled') } },
322
+ label: { ...text('label-base'), textStyle: null, color: v('gauge-label-color') },
323
+ valueText: { ...text('body-base'), color: v('gauge-value-color') },
324
324
  track: { bg: v('gauge-track-color'), borderRadius: v('gauge-radius'), boxShadow: 'none' },
325
325
  range: { bg: 'colorPalette.solid', borderRadius: v('gauge-radius') },
326
326
  thumb: {
@@ -343,7 +343,7 @@ export default {
343
343
  base: {
344
344
  list: { borderColor: v('disclosure-divider-color') },
345
345
  trigger: {
346
- ...text('base'),
346
+ ...text('body-base'),
347
347
  height: 'auto',
348
348
  minW: 0,
349
349
  padding: `${v('space-2')} ${v('space-4')}`,
@@ -361,7 +361,7 @@ export default {
361
361
  list: { display: 'flex', borderColor: v('disclosure-divider-color'), _horizontal: { borderBottomWidth: v('stroke-default') }, _vertical: { borderEndWidth: v('stroke-default') } },
362
362
  trigger: {
363
363
  _selected: {
364
- ...text('base-selected'),
364
+ ...text('selected-base'),
365
365
  color: v('disclosure-selected-color'),
366
366
  _horizontal: { layerStyle: 'indicator.bottom', '--indicator-offset-y': `calc(${v('stroke-default')} * -1)`, '--indicator-color': v('disclosure-indicator-color'), '--indicator-thickness': v('disclosure-indicator-width') },
367
367
  _vertical: { layerStyle: 'indicator.end', '--indicator-offset-x': `calc(${v('stroke-default')} * -1)`, '--indicator-color': v('disclosure-indicator-color'), '--indicator-thickness': v('disclosure-indicator-width') },
@@ -376,7 +376,7 @@ export default {
376
376
  root: { color: v('text-primary') },
377
377
  item: { borderBottom: `${v('stroke-default')} solid ${v('disclosure-divider-color')}`, _last: { borderBottom: 'none' } },
378
378
  itemTrigger: {
379
- ...text('maximize-md'),
379
+ ...text('title-base'),
380
380
  justifyContent: 'space-between',
381
381
  gap: v('space-4'),
382
382
  padding: `${v('space-4')} ${v('space-6')}`,
@@ -388,7 +388,7 @@ export default {
388
388
  _disabled: { opacity: v('disclosure-opacity-disabled'), layerStyle: null },
389
389
  },
390
390
  itemIndicator: { color: v('disclosure-icon-color') },
391
- itemBody: { ...text('base'), padding: `0 ${v('space-6')} ${v('space-4')}`, color: v('disclosure-content-color') },
391
+ itemBody: { ...text('body-base'), padding: `0 ${v('space-6')} ${v('space-4')}`, color: v('disclosure-content-color') },
392
392
  },
393
393
  variants: {
394
394
  variant: { outline: { item: {} } },
@@ -401,7 +401,7 @@ export default {
401
401
  content: { ...popup, minW: v('popup-min-width'), padding: v('popup-padding'), overflow: null },
402
402
  item: { ...row, paddingBlock: v('item-padding-block-dense'), '&[data-type]': { ps: null } },
403
403
  itemGroupLabel: groupLabel,
404
- itemCommand: { ...text('condense'), color: v('text-tertiary'), opacity: null, letterSpacing: null },
404
+ itemCommand: { ...text('body-condense'), color: v('text-tertiary'), opacity: null, letterSpacing: null },
405
405
  itemIndicator: { color: v('item-indicator-color') },
406
406
  separator: { ...k.separator, mx: 0, my: null },
407
407
  arrow: { '--arrow-background': v('popup-background') },
@@ -424,7 +424,7 @@ export default {
424
424
  _placeholderShown: { color: v('field-placeholder-color') },
425
425
  },
426
426
  indicator: { color: v('text-tertiary') },
427
- label: { ...text('base-label'), textStyle: null, color: v('text-primary') },
427
+ label: { ...text('label-base'), textStyle: null, color: v('text-primary') },
428
428
  ...listParts,
429
429
  },
430
430
  variants: {
@@ -438,9 +438,9 @@ export default {
438
438
  combobox: {
439
439
  base: {
440
440
  input: { ...field, focusVisibleRing: null },
441
- label: { ...text('base-label'), textStyle: null, color: v('text-primary') },
441
+ label: { ...text('label-base'), textStyle: null, color: v('text-primary') },
442
442
  trigger: { color: v('text-tertiary') },
443
- empty: { ...text('condense'), color: v('text-tertiary'), justifyContent: 'center', paddingBlock: v('popup-empty-padding-block') },
443
+ empty: { ...text('body-condense'), color: v('text-tertiary'), justifyContent: 'center', paddingBlock: v('popup-empty-padding-block') },
444
444
  ...listParts,
445
445
  },
446
446
  variants: {
@@ -450,16 +450,16 @@ export default {
450
450
  },
451
451
  popover: {
452
452
  base: {
453
- content: { ...popup, '--popover-bg': v('popup-background'), textStyle: null, ...text('base') },
454
- title: { ...text('maximize-md') },
455
- description: { ...text('base'), color: v('text-secondary') },
453
+ content: { ...popup, '--popover-bg': v('popup-background'), textStyle: null, ...text('body-base') },
454
+ title: { ...text('title-base') },
455
+ description: { ...text('body-base'), color: v('text-secondary') },
456
456
  arrow: { '--arrow-background': v('popup-background') },
457
457
  },
458
458
  },
459
459
  tooltip: {
460
460
  base: {
461
461
  content: {
462
- ...text('condense'),
462
+ ...text('body-condense'),
463
463
  textStyle: null,
464
464
  maxW: v('tooltip-max-width'),
465
465
  padding: `${v('space-1-5')} ${v('space-2-5')}`,
@@ -476,12 +476,12 @@ export default {
476
476
  dialog: {
477
477
  base: {
478
478
  backdrop: { bg: v('modal-backdrop') },
479
- content: { ...k.modalSurface, bg: null, background: v('modal-background'), textStyle: null, ...text('base'), width: v('modal-width'), maxH: v('modal-max-height') },
479
+ content: { ...k.modalSurface, bg: null, background: v('modal-background'), textStyle: null, ...text('body-base'), width: v('modal-width'), maxH: v('modal-max-height') },
480
480
  header: { padding: 0, gap: v('space-2') },
481
481
  body: { padding: 0 },
482
482
  footer: { padding: 0, gap: v('modal-actions-gap'), marginTop: v('modal-actions-spacing') },
483
- title: { ...text('maximize-xl'), textStyle: null, color: v('modal-title-color') },
484
- description: { ...text('base'), color: v('modal-description-color') },
483
+ title: { ...text('title-amplify'), textStyle: null, color: v('modal-title-color') },
484
+ description: { ...text('body-base'), color: v('modal-description-color') },
485
485
  closeTrigger: { top: v('space-3'), insetEnd: v('space-3') },
486
486
  },
487
487
  variants: { size: { md: { content: { maxW: v('modal-max-width') } } } },
@@ -489,9 +489,9 @@ export default {
489
489
  drawer: {
490
490
  base: {
491
491
  backdrop: { bg: v('modal-backdrop') },
492
- content: { bg: v('modal-background'), color: v('text-primary'), boxShadow: v('modal-shadow'), textStyle: null, ...text('base') },
493
- title: { ...text('maximize-xl'), textStyle: null, color: v('modal-title-color') },
494
- description: { ...text('base'), color: v('modal-description-color') },
492
+ content: { bg: v('modal-background'), color: v('text-primary'), boxShadow: v('modal-shadow'), textStyle: null, ...text('body-base') },
493
+ title: { ...text('title-amplify'), textStyle: null, color: v('modal-title-color') },
494
+ description: { ...text('body-base'), color: v('modal-description-color') },
495
495
  footer: { gap: v('modal-actions-gap') },
496
496
  },
497
497
  },
@@ -500,7 +500,7 @@ export default {
500
500
  base: {
501
501
  root: {
502
502
  ...k.popupSurface,
503
- ...text('base'),
503
+ ...text('body-base'),
504
504
  padding: v('notice-padding'),
505
505
  gap: v('notice-gap'),
506
506
  '--toast-trigger-bg': v('surface-raised'),
@@ -508,16 +508,16 @@ export default {
508
508
  STATUS.filter((s) => scopes[s]).map((s) => [`&[data-type=${s}]`, { colorPalette: s, bg: 'colorPalette.soft', borderColor: 'colorPalette.border', color: 'colorPalette.on-soft' }]),
509
509
  ),
510
510
  },
511
- title: { ...text('maximize-md'), textStyle: null },
512
- description: { ...text('base'), textStyle: null, marginTop: v('notice-text-gap'), color: v('notice-description-color'), '[data-type] &': { color: 'inherit' } },
511
+ title: { ...text('title-base'), textStyle: null },
512
+ description: { ...text('body-base'), textStyle: null, marginTop: v('notice-text-gap'), color: v('notice-description-color'), '[data-type] &': { color: 'inherit' } },
513
513
  closeTrigger: k.iconButton,
514
514
  },
515
515
  },
516
516
  alert: {
517
517
  base: {
518
- root: { ...text('base'), textStyle: null, padding: v('notice-padding'), gap: v('notice-gap'), borderRadius: v('popup-radius'), borderWidth: v('popup-border-width'), borderStyle: 'solid', borderColor: 'transparent' },
519
- title: { ...text('maximize-md') },
520
- description: { ...text('base'), marginTop: v('notice-text-gap') },
518
+ root: { ...text('body-base'), textStyle: null, padding: v('notice-padding'), gap: v('notice-gap'), borderRadius: v('popup-radius'), borderWidth: v('popup-border-width'), borderStyle: 'solid', borderColor: 'transparent' },
519
+ title: { ...text('title-base') },
520
+ description: { ...text('body-base'), marginTop: v('notice-text-gap') },
521
521
  indicator: { color: 'inherit' },
522
522
  },
523
523
  variants: {
@@ -534,8 +534,8 @@ export default {
534
534
  progress: {
535
535
  base: {
536
536
  root: { gap: v('gauge-gap') },
537
- label: { ...text('base-label'), textStyle: null, color: v('gauge-label-color') },
538
- valueText: { ...text('base'), textStyle: null, color: v('gauge-value-color') },
537
+ label: { ...text('label-base'), textStyle: null, color: v('gauge-label-color') },
538
+ valueText: { ...text('body-base'), textStyle: null, color: v('gauge-value-color') },
539
539
  track: { bg: v('gauge-track-color'), borderRadius: v('gauge-radius') },
540
540
  range: { bg: 'colorPalette.solid', borderRadius: v('gauge-radius'), transition: v('gauge-transition') },
541
541
  },
@@ -546,7 +546,7 @@ export default {
546
546
  },
547
547
  avatar: {
548
548
  base: {
549
- root: { ...text('maximize-md'), boxSizing: 'border-box', border: `${v('stroke-emphasis')} solid ${v('surface-canvas')}`, boxShadow: v('elevation-sm'), textTransform: 'uppercase' },
549
+ root: { ...text('title-base'), boxSizing: 'border-box', border: `${v('stroke-emphasis')} solid ${v('surface-canvas')}`, boxShadow: v('elevation-sm'), textTransform: 'uppercase' },
550
550
  },
551
551
  variants: {
552
552
  variant: Object.fromEntries(['solid', 'subtle', 'outline'].map((s) => [s, { root: { bg: 'colorPalette.solid', color: 'colorPalette.on-solid' } }])),
@@ -555,8 +555,8 @@ export default {
555
555
  },
556
556
  table: {
557
557
  base: {
558
- root: { ...text('condense'), textStyle: null, color: v('table-color') },
559
- columnHeader: { ...text('base-label'), bg: v('table-header-background'), color: v('table-header-color'), borderColor: v('table-row-border-color') },
558
+ root: { ...text('body-condense'), textStyle: null, color: v('table-color') },
559
+ columnHeader: { ...text('label-base'), bg: v('table-header-background'), color: v('table-header-color'), borderColor: v('table-row-border-color') },
560
560
  cell: { borderColor: v('table-row-border-color'), paddingInline: v('table-cell-padding-inline') },
561
561
  row: { '&[data-selected]': { bg: v('table-row-selected') } },
562
562
  },
@@ -567,7 +567,7 @@ export default {
567
567
 
568
568
  const config = {
569
569
  // Dark follows data-theme, the attribute tokens.css (and Base UI's stories) use.
570
- conditions: { light: ':root &, .light &, [data-theme=light] &', dark: '.dark &, [data-theme=dark] &' },
570
+ conditions: { light: ':root &, .light &, [data-theme=light] &', dark: '.dark &, [data-theme=dark] &, [data-theme=dark-high-contrast] &' },
571
571
  globalCss: { html: { colorPalette: 'accent' } },
572
572
  theme: {
573
573
  tokens: {
@@ -69,7 +69,7 @@ export function styleKit(prefix, roles) {
69
69
  const control = (size, { trigger = false } = {}) => {
70
70
  const s = SIZES[size];
71
71
  return {
72
- ...text(trigger ? `${s.text}-trigger` : s.text),
72
+ ...text(trigger ? `trigger-${s.text}` : `body-${s.text}`),
73
73
  height: v(`control-height${s.suffix || '-md'}`),
74
74
  paddingInline: v(`control-padding-inline${s.suffix}`),
75
75
  gap: v(size === 'xs' ? 'gap-control-xs' : size === 'sm' ? 'gap-control-sm' : 'gap-control'),
@@ -107,7 +107,7 @@ export function styleKit(prefix, roles) {
107
107
  };
108
108
 
109
109
  const itemRow = {
110
- ...text('base'),
110
+ ...text('body-base'),
111
111
  display: 'flex',
112
112
  alignItems: 'center',
113
113
  gap: v('item-gap'),
@@ -120,11 +120,11 @@ export function styleKit(prefix, roles) {
120
120
  };
121
121
  const itemStates = {
122
122
  hover: { background: v('item-background-hover'), color: v('item-color-hover') },
123
- selected: { ...text('base-selected'), background: v('item-background-selected'), color: v('item-color-selected') },
123
+ selected: { ...text('selected-base'), background: v('item-background-selected'), color: v('item-color-selected') },
124
124
  disabled: { color: v('item-color-disabled'), opacity: v('item-opacity-disabled') },
125
125
  };
126
126
  const groupLabel = {
127
- ...text('condense-subtitle-label'),
127
+ ...text('eyebrow-condense'),
128
128
  paddingBlock: `${v('popup-group-label-padding-top')} ${v('popup-group-label-padding-bottom')}`,
129
129
  paddingInline: v('item-padding-inline'),
130
130
  color: v('text-secondary'),
@@ -97,8 +97,8 @@ export default {
97
97
  // ------------------------------------------------------------------------------- type
98
98
  const styleNames = [...new Set(theme.tokens.filter((t) => /^font\.[^.]+\.font$/.test(t.path)).map((t) => t.path.split('.')[1]))];
99
99
  const familyKey = (style) => byPath.get(`font.${style}.font-family`)?.references?.match(/^\{font-family\.([^}]+)\}$/)?.[1] ?? 'body';
100
- const tierOf = (style) => (style === 'code' ? 'code' : style.split('-')[0]);
101
- const TIERS = ['minimize', 'condense', 'base', 'magnify', 'maximize', 'code'];
100
+ const tierOf = (style) => style.split('-').pop();
101
+ const TIERS = ['minimize', 'condense', 'base', 'magnify', 'amplify', 'maximize'];
102
102
  const tiers = [...new Set(styleNames.map(tierOf))].sort((a, b) => (TIERS.indexOf(a) + 1 || 99) - (TIERS.indexOf(b) + 1 || 99));
103
103
  const styleOf = (style) => {
104
104
  const v = (prop) => byPath.get(`font.${style}.${prop}`)?.value;
@@ -256,6 +256,12 @@ function brandBook({ theme, brand, byPath, cssName, usageOf, tokens, tiers, styl
256
256
  '',
257
257
  `\`data-color="<palette>"\` on any element repaints everything inside it in that palette (\`primary\`, \`secondary\`, \`error\`, \`accent\`, \`chrome\`...): a destructive button is \`<Button data-emphasis="bold" data-color="error">\`.`,
258
258
  '',
259
+ '### Statuses',
260
+ '',
261
+ `\`data-status="info|success|warning|error"\` on any element gives every emphasis level inside it that status's version, so a subtle warning banner is \`<div data-status="warning" data-emphasis="subtle">\`. A level is the level's style in the status's palette unless the brand picked a pair for it:`,
262
+ '',
263
+ ...Object.entries(roles.status ?? {}).flatMap(([status, picks]) => Object.entries(picks).map(([level, pair]) => `- ${level} in ${status}: the \`${pair}\` pair`)),
264
+ ...(Object.keys(roles.status ?? {}).length ? [''] : ['(This brand picks none, so every level is automatic.)', '']),
259
265
  );
260
266
  const pairs = Object.entries(brand?.color?.pairs ?? {});
261
267
  if (pairs.length) {
@@ -284,16 +290,16 @@ function brandBook({ theme, brand, byPath, cssName, usageOf, tokens, tiers, styl
284
290
  condense: 'compact UI: small controls, table cells, secondary labels.',
285
291
  base: 'the default UI and body text.',
286
292
  magnify: 'emphasized body text: lead paragraphs, larger controls.',
287
- maximize: 'display text for headings and hero copy, in steps xs to 4xl.',
288
- code: 'code, in the mono family.',
293
+ amplify: 'titles of dialogs, drawers and page sections.',
294
+ maximize: 'the page head.',
289
295
  };
290
296
  for (const tier of tiers) {
291
- const variants = styleNames.filter((s) => s.startsWith(`${tier}-`)).map((s) => s.slice(tier.length + 1));
292
- out.push(`- **${tier}**: ${tierWhy[tier] ?? ''}${variants.length ? ` Variants: ${variants.map((x) => `\`${x}\``).join(', ')}.` : ''}`);
297
+ const variants = styleNames.filter((s) => s.endsWith(`-${tier}`)).map((s) => s.slice(0, -tier.length - 1));
298
+ out.push(`- **${tier}**: ${tierWhy[tier] ?? ''}${variants.length ? ` Roles: ${variants.map((x) => `\`${x}\``).join(', ')}.` : ''}`);
293
299
  }
294
300
  out.push(
295
301
  '',
296
- 'Inside a tier, `-trigger` is for buttons and triggers, `-selected` for the current tab, toggle or nav item, `-link` for links, `-subtitle` for subheads and `-label` for form labels. Sizes below the base are fixed; the base and up grow with the viewport.',
302
+ 'Names are role first, then tier: `body-` for running text, `trigger-` for buttons and triggers, `selected-` for the current tab, toggle or nav item, `link-` for links, `subtitle-` for subheads, `label-` for form labels, `eyebrow-` for small uppercase labels, `title-` for titles in the display family, `code-` for code in the mono family. Sizes below the base are fixed; the base and up grow with the viewport.',
297
303
  '',
298
304
  '## Space, shape and depth',
299
305
  '',
@@ -309,7 +315,7 @@ function brandBook({ theme, brand, byPath, cssName, usageOf, tokens, tiers, styl
309
315
  '',
310
316
  '## Modes',
311
317
  '',
312
- `Light and dark: \`data-theme="light|dark"\` on the root, or the system setting when neither is set. High contrast follows \`prefers-contrast: more\`. Color vision themes: \`data-vision="deuteranopia|protanopia|tritanopia|achromatopsia"\`, each designed from this brand. Never hand-pick dark colors; the tokens already flip.`,
318
+ `Themes: \`data-theme="light|dark|light-high-contrast|dark-high-contrast"\` on the root, or the system setting (including \`prefers-contrast: more\`) when none is set. Color vision themes are themes of their own, each a light theme designed from this brand: \`data-theme="deuteranopia|protanopia|tritanopia|achromatopsia"\`. Never hand-pick dark colors; the tokens already flip.`,
313
319
  '',
314
320
  );
315
321
  if (componentNames.length) {
@@ -332,7 +338,7 @@ function brandBook({ theme, brand, byPath, cssName, usageOf, tokens, tiers, styl
332
338
  '',
333
339
  '`window.Loomiere` holds two kinds of component:',
334
340
  '',
335
- `- **Wrappers** put their own classes on, so use them bare: ${['Button', 'IconButton', 'Input', 'Field', 'Fieldset', 'Form', 'NavigationMenu'].filter(has).map((n) => `\`Loomiere.${n}\``).join(', ')}. \`Loomiere.Button\` also takes \`emphasis\` (\`bold|standard|subtle|quiet\`) and \`palette\` props, and a Button holding only an icon draws itself square (it sets \`data-icon-only\`). A plain \`.Button\`, such as a Popover or Toolbar trigger, is drawn square when it has an \`aria-label\` and a single icon inside, or when you set \`data-icon-only\` on it.`,
341
+ `- **Wrappers** put their own classes on, so use them bare: ${['Button', 'Input', 'Field', 'Fieldset', 'Form', 'NavigationMenu'].filter(has).map((n) => `\`Loomiere.${n}\``).join(', ')}. \`Loomiere.Button\` also takes \`emphasis\` (\`bold|standard|subtle|quiet\`) and \`palette\` props, and a Button holding only an icon draws itself square (it sets \`data-icon-only\`). A plain \`.Button\`, such as a Popover or Toolbar trigger, is drawn square when it has an \`aria-label\` and a single icon inside, or when you set \`data-icon-only\` on it.`,
336
342
  '- **Everything else is the bare Base UI part** under its usual name (`Loomiere.Popover.Popup`, `Loomiere.ToggleGroup`). It draws nothing until you give each part its class, which is the component and part names joined: `<Loomiere.Popover.Popup className="PopoverPopup">`, `<Loomiere.Toggle className="Toggle">`. The component READMEs list every class.',
337
343
  '',
338
344
  '`Loomiere.Base` holds the bare Base UI parts for the wrappers too (`Loomiere.Base.Button`), for when you need the part without its classes.',
@@ -344,8 +350,7 @@ function brandBook({ theme, brand, byPath, cssName, usageOf, tokens, tiers, styl
344
350
  '| Component | Use it to |',
345
351
  '|---|---|',
346
352
  ...pick([
347
- ['Button', 'Run one action. Set its emphasis by how much the action matters.'],
348
- ['IconButton', 'Run a small, frequent action shown as an icon alone, where a Button would be too loud: calendar arrows, row actions, a clear button. Always give it an aria-label.'],
353
+ ['Button', 'Run one action. Set its emphasis by how much the action matters. For a small, frequent action shown as an icon alone (calendar arrows, row actions), use a quiet neutral icon-only Button: `<Loomiere.Button emphasis="quiet" palette="neutral" data-size="xs" aria-label="Next month">` with one icon inside.'],
349
354
  ['Switch', 'Turn one setting on or off, taking effect at once.'],
350
355
  ['Checkbox', 'Turn one option on or off inside a form that is submitted later.'],
351
356
  ['Toggle', 'A tool that stays pressed, such as Bold in a toolbar.'],
@@ -443,7 +448,7 @@ function cover({ theme, brand, title }) {
443
448
  function agentsGuide({ theme, readme, componentNames }) {
444
449
  return `# ${cap(theme.brand)} theme: guide for agents
445
450
 
446
- This folder is a Loomiere theme. Load \`tokens.css\` (every token as a \`--${theme.prefix}-\` variable, light and dark) and \`utilities.css\` (text styles, focus ring, brand pairs). Framework adapters sit in \`exports/\`: \`tailwind.css\` (Tailwind v4: text styles by tier such as \`text-base\`, concept classes such as \`bg-field\` and \`text-link\`, no bare sizes like \`text-sm\`), \`shadcn.css\` (shadcn/ui), \`mui.js\`, \`chakra.js\`, AG Grid themes, \`tokens.dtcg.json\`. \`tokens.json\` lists every token with its path, value, modes and the reason it exists; read a token's \`description\` before using it.
451
+ This folder is a Loomiere theme. Load \`tokens.css\` (every token as a \`--${theme.prefix}-\` variable, light and dark) and \`utilities.css\` (text styles, focus ring, brand pairs). Framework adapters sit in \`exports/\`: \`tailwind.css\` (Tailwind v4: text styles by tier such as \`text-body-base\`, concept classes such as \`bg-field\` and \`text-link\`, no bare sizes like \`text-sm\`), \`shadcn.css\` (shadcn/ui), \`mui.js\`, \`chakra.js\`, AG Grid themes, \`tokens.dtcg.json\`. \`tokens.json\` lists every token with its path, value, modes and the reason it exists; read a token's \`description\` before using it.
447
452
  ${componentNames ? `\nComponents: \`@scattered-light/loomiere-base-ui\` (Base UI parts styled by class name; import \`@scattered-light/loomiere-base-ui/styles.css\` after the theme). \`exports/design-system/components/<Name>/README.md\` lists each component's classes and an example.\n` : ''}
448
453
  Change the brand only in the brand file and rebuild (\`loomiere build\`); never edit generated files.
449
454