@scattered-light/loomiere 0.0.0-stage → 0.1.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 (73) hide show
  1. package/README.md +60 -2
  2. package/bin/loomiere.mjs +112 -0
  3. package/build/build-tokens.mjs +485 -0
  4. package/exporters/README.md +45 -0
  5. package/exporters/ag-grid-settings.mjs +177 -0
  6. package/exporters/ag-grid.mjs +112 -0
  7. package/exporters/chakra.mjs +630 -0
  8. package/exporters/component-styles.mjs +207 -0
  9. package/exporters/dtcg.mjs +31 -0
  10. package/exporters/figma.mjs +100 -0
  11. package/exporters/index.mjs +38 -0
  12. package/exporters/mui.mjs +666 -0
  13. package/exporters/tailwind.mjs +35 -0
  14. package/exporters/theme.mjs +93 -0
  15. package/generator/brand.mjs +328 -0
  16. package/generator/brand.schema.json +657 -0
  17. package/generator/color.mjs +130 -0
  18. package/generator/data-colors.mjs +104 -0
  19. package/generator/format-brand.mjs +60 -0
  20. package/generator/generate.mjs +43 -0
  21. package/generator/pairs.mjs +236 -0
  22. package/generator/roles.mjs +258 -0
  23. package/generator/vision.mjs +167 -0
  24. package/lib/config.mjs +71 -0
  25. package/lib/contrast.mjs +134 -0
  26. package/lib/lab.mjs +108 -0
  27. package/lib/studio.mjs +127 -0
  28. package/lib/tool-server.mjs +96 -0
  29. package/package.json +51 -4
  30. package/templates/brand.json +104 -0
  31. package/tokens/README.md +219 -0
  32. package/tokens/component/avatar.json +10 -0
  33. package/tokens/component/button.json +12 -0
  34. package/tokens/component/drawer.json +26 -0
  35. package/tokens/component/navigation-menu.json +13 -0
  36. package/tokens/component/popover.json +13 -0
  37. package/tokens/component/preview-card.json +13 -0
  38. package/tokens/component/slider.json +13 -0
  39. package/tokens/component/switch.json +41 -0
  40. package/tokens/component/toast.json +21 -0
  41. package/tokens/component/toggle.json +9 -0
  42. package/tokens/component/toolbar.json +9 -0
  43. package/tokens/component/tooltip.json +9 -0
  44. package/tokens/foundation/layering.json +57 -0
  45. package/tokens/foundation/layout.json +75 -0
  46. package/tokens/foundation/motion.json +41 -0
  47. package/tokens/foundation/type.json +92 -0
  48. package/tokens/semantic/accessibility.json +39 -0
  49. package/tokens/semantic/bar.json +45 -0
  50. package/tokens/semantic/check.json +57 -0
  51. package/tokens/semantic/color.json +270 -0
  52. package/tokens/semantic/content.json +124 -0
  53. package/tokens/semantic/disclosure.json +49 -0
  54. package/tokens/semantic/elevation.json +72 -0
  55. package/tokens/semantic/field.json +77 -0
  56. package/tokens/semantic/focus.json +36 -0
  57. package/tokens/semantic/gauge.json +45 -0
  58. package/tokens/semantic/icon-button.json +33 -0
  59. package/tokens/semantic/icon.json +30 -0
  60. package/tokens/semantic/item.json +65 -0
  61. package/tokens/semantic/modal.json +86 -0
  62. package/tokens/semantic/motion.json +20 -0
  63. package/tokens/semantic/notice.json +37 -0
  64. package/tokens/semantic/popup.json +89 -0
  65. package/tokens/semantic/scheme.json +11 -0
  66. package/tokens/semantic/shape.json +54 -0
  67. package/tokens/semantic/spacing.json +110 -0
  68. package/tokens/semantic/table.json +73 -0
  69. package/tokens/semantic/typography.json +312 -0
  70. package/tools/brand-builder/builder.js +1119 -0
  71. package/tools/brand-builder/index.html +329 -0
  72. package/tools/pair-lab/index.html +119 -0
  73. package/tools/pair-lab/lab.js +249 -0
@@ -0,0 +1,630 @@
1
+ /**
2
+ * Chakra UI v3 system: every brand palette as a Chakra color palette, plus recipe overrides
3
+ * that give Chakra's components the same look as loomiere-base-ui: Button variants are the
4
+ * brand's emphasis levels, inputs are the field concept, menus are popups and rows, and so
5
+ * on. Tokens and recipes read the theme's --sl- variables, so load tokens.css too.
6
+ * import { ChakraProvider } from '@chakra-ui/react';
7
+ * import { system } from './theme/exports/chakra.js';
8
+ * <ChakraProvider value={system}>…</ChakraProvider>
9
+ */
10
+ import { STYLES } from '../generator/roles.mjs';
11
+ import { styleKit, toSource } from './component-styles.mjs';
12
+
13
+ /** Chakra size → control tier. */
14
+ const TIER = { '2xs': 'xs', xs: 'xs', sm: 'sm', md: 'md', lg: 'lg', xl: 'lg', '2xl': 'lg' };
15
+ /** Button variant → emphasis level, as data-variant does for the Base UI Button. */
16
+ const VARIANT_LEVEL = { solid: 'bold', outline: 'standard', subtle: 'subtle', surface: 'subtle', ghost: 'quiet', plain: 'quiet' };
17
+ /** Chakra's own color-palette keys → scope slots, so Chakra components we leave alone still fit. */
18
+ const CHAKRA_SLOTS = { contrast: 'on-solid', subtle: 'soft', muted: 'soft-hover', emphasized: 'soft-active', focusRing: 'focus' };
19
+ const STATUS = ['info', 'warning', 'success', 'error'];
20
+ /** Text styles a Chakra user can name as textStyle="…". */
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',
24
+ ];
25
+
26
+ /** Merges our recipe pieces into Chakra's own, in the generated file (see RESTYLE below). */
27
+ const RESTYLE = `/**
28
+ * Our recipe pieces merged into Chakra's own: a base style merges key by key into
29
+ * Chakra's (null removes a key), and a variant's style replaces Chakra's for that slot.
30
+ * Anything we leave out keeps Chakra's style.
31
+ */
32
+ const merge = (a = {}, b) => {
33
+ const out = { ...a, ...b };
34
+ for (const key of Object.keys(out)) if (out[key] === null) delete out[key];
35
+ return out;
36
+ };
37
+ function restyle(recipe, ours) {
38
+ const slotted = Boolean(recipe.slots);
39
+ const base = !ours.base ? recipe.base : slotted ? Object.fromEntries([...new Set([...Object.keys(recipe.base ?? {}), ...Object.keys(ours.base)])].map((slot) => [slot, merge(recipe.base?.[slot], ours.base[slot] ?? {})])) : merge(recipe.base, ours.base);
40
+ const variants = { ...recipe.variants };
41
+ for (const [group, values] of Object.entries(ours.variants ?? {})) {
42
+ variants[group] = { ...recipe.variants?.[group] };
43
+ for (const [value, style] of Object.entries(values)) variants[group][value] = slotted ? { ...recipe.variants?.[group]?.[value], ...style } : style;
44
+ }
45
+ return { ...recipe, base, variants, compoundVariants: ours.compoundVariants ?? recipe.compoundVariants ?? [], defaultVariants: { ...recipe.defaultVariants, ...ours.defaultVariants } };
46
+ }
47
+ const restyleAll = (defaults, ours) => ({ ...defaults, ...Object.fromEntries(Object.entries(ours).map(([name, r]) => [name, restyle(defaults[name], r)])) });`;
48
+
49
+ export default {
50
+ id: 'chakra',
51
+ name: 'Chakra UI v3',
52
+ file: 'chakra.js',
53
+ for: 'React apps on @chakra-ui/react v3: pass the exported system to ChakraProvider, and load tokens.css.',
54
+ build(theme) {
55
+ const { scopes, palettes, roles } = theme;
56
+ const prefix = theme.prefix ?? 'sl';
57
+ const k = styleKit(prefix, roles);
58
+ const { v, text } = k;
59
+ const sizes = (names, fn) => Object.fromEntries(names.map((s) => [s, fn(TIER[s], s)]));
60
+
61
+ // Colors: each palette's steps, and each scope's slots as a Chakra color palette.
62
+ const value = (x) => ({ value: x });
63
+ const colors = Object.fromEntries(Object.entries(palettes).map(([name, steps]) => [name, Object.fromEntries(Object.entries(steps).map(([s, hex]) => [s, value(hex)]))]));
64
+ const scopeColors = Object.fromEntries(
65
+ Object.entries(scopes).map(([name, s]) => {
66
+ const slots = Object.keys(s.light);
67
+ return [
68
+ name,
69
+ {
70
+ ...Object.fromEntries(slots.map((slot) => [slot, value(k.scope(name, slot))])),
71
+ ...Object.fromEntries(Object.entries(CHAKRA_SLOTS).map(([key, slot]) => [key, value(k.scope(name, slot))])),
72
+ },
73
+ ];
74
+ }),
75
+ );
76
+ // Chakra's own semantic colors, pointed at ours, so the components we leave alone match too.
77
+ const semanticColors = {
78
+ ...scopeColors,
79
+ bg: {
80
+ DEFAULT: value(v('surface-canvas')),
81
+ subtle: value(v('surface-raised')),
82
+ muted: value(v('surface-overlay')),
83
+ emphasized: value(v('fill-default')),
84
+ panel: value(v('popup-background')),
85
+ inverted: value(v('surface-inverse')),
86
+ ...Object.fromEntries(STATUS.map((s) => [s, value(k.scope(s, 'soft'))])),
87
+ },
88
+ fg: {
89
+ DEFAULT: value(v('text-primary')),
90
+ muted: value(v('text-secondary')),
91
+ subtle: value(v('text-tertiary')),
92
+ inverted: value(v('text-on-inverse-surface')),
93
+ ...Object.fromEntries(STATUS.map((s) => [s, value(k.scope(s, 'fg'))])),
94
+ },
95
+ border: {
96
+ DEFAULT: value(v('border-default')),
97
+ muted: value(v('border-subtle')),
98
+ subtle: value(v('border-subtle')),
99
+ emphasized: value(v('border-strong')),
100
+ inverted: value(v('surface-canvas')),
101
+ ...Object.fromEntries(STATUS.map((s) => [s, value(k.scope(s, 'border'))])),
102
+ },
103
+ };
104
+
105
+ // An emphasis level in Chakra's colorPalette, so colorPalette="…" re-colors it the way
106
+ // data-color re-colors a Base UI Button. Without one the level keeps its own color.
107
+ const palette = (slot) => (slot === 'transparent' ? 'transparent' : `colorPalette.${slot}`);
108
+ const paletteVar = (slot) => `var(--chakra-colors-color-palette-${slot})`;
109
+ const level = (name) => {
110
+ const { style, color } = roles.emphasis[name];
111
+ const s = STYLES[style];
112
+ const rest = { bg: palette(s.bg), color: palette(s.fg), borderColor: palette(s.border) };
113
+ return {
114
+ colorPalette: color,
115
+ ...rest,
116
+ _hover: { bg: palette(s['bg-hover']), borderColor: palette(s['border-hover']) },
117
+ _active: { bg: palette(s['bg-active']) },
118
+ _focusVisible: {
119
+ outline: `${v('a11y-focus-outline-width')} solid ${paletteVar('focus')}`,
120
+ outlineOffset: v('a11y-focus-outline-offset'),
121
+ boxShadow: `0 0 0 ${v('focus-halo-spread')} ${paletteVar('soft-hover')}`,
122
+ },
123
+ _disabled: { ...rest, opacity: v('button-opacity-disabled'), cursor: 'not-allowed' },
124
+ };
125
+ };
126
+ const control = (tier, opts) => ({ ...k.control(tier, opts), textStyle: null, minW: 0, px: null, h: null });
127
+
128
+ const field = {
129
+ ...k.field,
130
+ ...text('base'),
131
+ focusVisibleRing: null,
132
+ focusRingColor: null,
133
+ _hover: k.fieldStates.hover,
134
+ _focus: k.fieldStates.focus,
135
+ _invalid: { ...k.fieldStates.error, _focus: k.fieldStates.errorFocus },
136
+ _disabled: { ...k.fieldStates.disabled, layerStyle: null },
137
+ _placeholder: k.fieldStates.placeholder,
138
+ };
139
+ const fieldSize = (tier) => ({
140
+ ...text(tier === 'xs' || tier === 'sm' ? 'condense' : tier === 'lg' ? 'magnify' : 'base'),
141
+ textStyle: null,
142
+ '--input-height': v(`control-height-${tier}`),
143
+ height: v(`control-height-${tier}`),
144
+ px: v(`control-padding-inline${tier === 'md' ? '' : `-${tier}`}`),
145
+ });
146
+
147
+ const popup = { ...k.popupSurface, bg: null, '--menu-bg': v('popup-background') };
148
+ const row = {
149
+ ...k.itemRow,
150
+ textStyle: null,
151
+ cursor: 'default',
152
+ _highlighted: k.itemStates.hover,
153
+ _disabled: { ...k.itemStates.disabled, layerStyle: null },
154
+ };
155
+ const groupLabel = { ...k.groupLabel, textStyle: null };
156
+ const listParts = {
157
+ content: { ...popup, borderRadius: v('popup-radius-field'), padding: v('popup-padding'), boxShadow: v('popup-shadow') },
158
+ item: { ...row, '&[data-state=checked]': k.itemStates.selected },
159
+ itemGroupLabel: groupLabel,
160
+ itemIndicator: { color: v('item-indicator-color') },
161
+ };
162
+ const clearListSizes = (names, extra = () => ({})) => sizes(names, (tier) => ({ content: {}, item: {}, itemGroupLabel: {}, ...extra(tier) }));
163
+
164
+ const statusPalette = (s) => (scopes[s] ? s : 'chrome');
165
+
166
+ const recipes = {
167
+ button: {
168
+ base: {
169
+ ...control('md', { trigger: true }),
170
+ // Longhands: Chakra drops a variant's borderColor when the base has the shorthand.
171
+ borderWidth: v('stroke-default'),
172
+ borderStyle: 'solid',
173
+ borderColor: 'transparent',
174
+ borderRadius: v('button-radius'),
175
+ transition: v('transition-button'),
176
+ transitionProperty: null,
177
+ transitionDuration: null,
178
+ focusVisibleRing: null,
179
+ _disabled: null,
180
+ _icon: { flexShrink: 0, width: k.iconSize('md'), height: k.iconSize('md') },
181
+ },
182
+ variants: {
183
+ variant: Object.fromEntries(Object.entries(VARIANT_LEVEL).map(([variant, name]) => [variant, level(name)])),
184
+ size: sizes(['2xs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl'], (tier) => ({ ...control(tier, { trigger: true }), _icon: { width: k.iconSize(tier), height: k.iconSize(tier) } })),
185
+ },
186
+ },
187
+ input: {
188
+ base: { ...field, height: v('control-height-md'), minW: 0 },
189
+ variants: { size: sizes(['2xs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl'], fieldSize), variant: { outline: {} } },
190
+ },
191
+ textarea: {
192
+ base: { ...field, minHeight: '80px', paddingBlock: v('control-padding-block'), px: v('control-padding-inline') },
193
+ variants: { size: sizes(['xs', 'sm', 'md', 'lg', 'xl'], () => ({})), variant: { outline: {} } },
194
+ },
195
+ };
196
+
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
+ const checkRow = { gap: v('gap-control'), color: v('text-primary') };
199
+ const label = { ...text('base'), textStyle: null, _disabled: { color: v('text-disabled'), opacity: null } };
200
+ const checked = '&:is([data-state=checked], [data-state=indeterminate])';
201
+
202
+ const slotRecipes = {
203
+ field: {
204
+ base: {
205
+ root: { gap: v('field-gap') },
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') },
210
+ requiredIndicator: { color: v('status-error') },
211
+ },
212
+ },
213
+
214
+ checkbox: {
215
+ base: {
216
+ root: checkRow,
217
+ control: {
218
+ ...k.checkBox,
219
+ ...checkControl('md'),
220
+ borderRadius: v('check-radius'),
221
+ p: 0,
222
+ focusVisibleRing: null,
223
+ _hover: k.checkStates.hover,
224
+ [checked]: k.checkStates.checked,
225
+ _focusVisible: k.focusRing,
226
+ _invalid: { borderColor: v('field-border-color-error'), colorPalette: null },
227
+ _disabled: { ...k.checkStates.disabled, cursor: 'not-allowed' },
228
+ },
229
+ label,
230
+ },
231
+ variants: {
232
+ variant: { solid: { control: {} }, outline: { control: {} }, subtle: { control: {} } },
233
+ size: sizes(['xs', 'sm', 'md', 'lg'], (tier) => ({ root: { gap: v(tier === 'xs' ? 'gap-control-xs' : tier === 'sm' ? 'gap-control-sm' : 'gap-control') }, label: {}, control: checkControl(tier) })),
234
+ },
235
+ },
236
+ radioGroup: {
237
+ base: {
238
+ root: { display: 'flex', gap: v('gap-stack-sm') },
239
+ item: { ...checkRow, fontWeight: null },
240
+ itemText: label,
241
+ label,
242
+ itemControl: {
243
+ ...k.checkBox,
244
+ boxSize: k.iconSize('md'),
245
+ borderRadius: v('radius-pill'),
246
+ _focusVisible: k.focusRing,
247
+ _hover: k.checkStates.hover,
248
+ _checked: k.checkStates.checked,
249
+ _invalid: { borderColor: v('field-border-color-error'), colorPalette: null },
250
+ _disabled: { ...k.checkStates.disabled, cursor: 'not-allowed' },
251
+ '& .dot': { width: `round(100% * ${v('check-dot-scale')}, 1px)`, height: `round(100% * ${v('check-dot-scale')}, 1px)`, scale: null, borderRadius: v('radius-pill'), bg: v('check-mark-color') },
252
+ '&:not([data-state=checked]) .dot': { display: 'none' },
253
+ },
254
+ },
255
+ variants: {
256
+ variant: { solid: { itemControl: {} }, outline: { itemControl: {} }, subtle: { itemControl: {} } },
257
+ size: sizes(['xs', 'sm', 'md', 'lg'], (tier) => ({ item: { gap: v(tier === 'xs' ? 'gap-control-xs' : tier === 'sm' ? 'gap-control-sm' : 'gap-control') }, itemControl: { boxSize: k.iconSize(tier) } })),
258
+ },
259
+ },
260
+ switch: {
261
+ base: {
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 } },
264
+ control: {
265
+ boxSizing: 'border-box',
266
+ alignItems: 'center',
267
+ padding: v('switch-track-padding'),
268
+ border: `${v('check-border-width')} solid ${v('check-border-color')}`,
269
+ borderRadius: v('radius-pill'),
270
+ bg: v('fill-default'),
271
+ transition: `background-color ${v('duration-normal')} ${v('easing-in-out')}`,
272
+ focusVisibleRing: null,
273
+ _before: {
274
+ ...text('condense-subtitle-label'),
275
+ content: '"off"',
276
+ position: 'absolute',
277
+ insetBlock: 0,
278
+ right: v('switch-track-padding'),
279
+ width: 'var(--switch-diff)',
280
+ display: 'flex',
281
+ alignItems: 'center',
282
+ justifyContent: 'center',
283
+ fontSize: 'var(--switch-indicator-font-size)',
284
+ lineHeight: 1,
285
+ color: v('text-secondary'),
286
+ pointerEvents: 'none',
287
+ },
288
+ _hover: { bg: v('fill-strong') },
289
+ _checked: {
290
+ bg: v('check-background-checked'),
291
+ borderColor: v('check-border-color-checked'),
292
+ _hover: { bg: v('interactive-hover') },
293
+ _before: { content: '"on"', right: 'auto', left: v('switch-track-padding'), color: v('check-mark-color') },
294
+ },
295
+ _focusVisible: k.focusRing,
296
+ _disabled: { opacity: v('check-opacity-disabled'), cursor: 'not-allowed', _hover: { bg: v('fill-default') } },
297
+ },
298
+ thumb: {
299
+ '--thumb-size': `calc(var(--switch-height) - 2 * (${v('switch-track-padding')} + ${v('check-border-width')}))`,
300
+ width: 'var(--thumb-size)',
301
+ height: 'var(--thumb-size)',
302
+ position: 'relative',
303
+ zIndex: 1,
304
+ transitionDuration: v('duration-normal'),
305
+ bg: v('check-background'),
306
+ boxShadow: v('elevation-sm'),
307
+ },
308
+ },
309
+ variants: {
310
+ variant: { solid: { control: {}, thumb: {} }, raised: { control: {}, thumb: {} } },
311
+ size: Object.fromEntries(
312
+ ['xs', 'sm', 'md', 'lg'].map((s) => {
313
+ const suffix = s === 'xs' || s === 'sm' ? '-sm' : s === 'lg' ? '-lg' : '';
314
+ return [s, { root: { '--switch-width': v(`switch-track-width${suffix}`), '--switch-height': v(`switch-track-height${suffix}`), '--switch-indicator-font-size': v(`switch-track-text-font-size${suffix}`) } }];
315
+ }),
316
+ ),
317
+ },
318
+ },
319
+ slider: {
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') },
324
+ track: { bg: v('gauge-track-color'), borderRadius: v('gauge-radius'), boxShadow: 'none' },
325
+ range: { bg: 'colorPalette.solid', borderRadius: v('gauge-radius') },
326
+ thumb: {
327
+ boxSizing: 'border-box',
328
+ bg: v('field-background'),
329
+ border: `${v('stroke-emphasis')} solid ${paletteVar('solid')}`,
330
+ transition: `box-shadow ${v('duration-normal')} ${v('easing-default')}`,
331
+ _hover: { boxShadow: `0 0 0 ${v('focus-halo-spread')} ${paletteVar('soft-hover')}` },
332
+ _dragging: { boxShadow: `0 0 0 calc(${v('focus-halo-spread')} * 1.5) ${paletteVar('soft-active')}` },
333
+ _focusVisible: { ...k.focusRing, ring: null, ringColor: null },
334
+ },
335
+ },
336
+ variants: {
337
+ variant: { outline: { track: {}, range: {}, thumb: {} }, solid: { track: {}, range: {}, thumb: {} } },
338
+ size: sizes(['sm', 'md', 'lg'], (tier) => ({ root: { '--slider-thumb-size': v('slider-thumb-size'), '--slider-track-size': v(tier === 'lg' ? 'gauge-height' : 'gauge-height-sm'), '--slider-marker-size': v('stroke-emphasis') } })),
339
+ },
340
+ },
341
+
342
+ tabs: {
343
+ base: {
344
+ list: { borderColor: v('disclosure-divider-color') },
345
+ trigger: {
346
+ ...text('base'),
347
+ height: 'auto',
348
+ minW: 0,
349
+ padding: `${v('space-2')} ${v('space-4')}`,
350
+ color: v('text-secondary'),
351
+ transition: v('transition-button'),
352
+ _hover: { color: v('text-primary'), bg: v('disclosure-trigger-background-hover') },
353
+ _focusVisible: { ...k.focusRingInset, zIndex: 1, outlineColor: null },
354
+ _disabled: { color: v('text-disabled'), opacity: v('disclosure-opacity-disabled'), cursor: 'not-allowed' },
355
+ },
356
+ },
357
+ variants: {
358
+ size: sizes(['sm', 'md', 'lg'], () => ({ trigger: {} })),
359
+ variant: {
360
+ line: {
361
+ list: { display: 'flex', borderColor: v('disclosure-divider-color'), _horizontal: { borderBottomWidth: v('stroke-default') }, _vertical: { borderEndWidth: v('stroke-default') } },
362
+ trigger: {
363
+ _selected: {
364
+ ...text('base-selected'),
365
+ color: v('disclosure-selected-color'),
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
+ _vertical: { layerStyle: 'indicator.end', '--indicator-offset-x': `calc(${v('stroke-default')} * -1)`, '--indicator-color': v('disclosure-indicator-color'), '--indicator-thickness': v('disclosure-indicator-width') },
368
+ },
369
+ },
370
+ },
371
+ },
372
+ },
373
+ },
374
+ accordion: {
375
+ base: {
376
+ root: { color: v('text-primary') },
377
+ item: { borderBottom: `${v('stroke-default')} solid ${v('disclosure-divider-color')}`, _last: { borderBottom: 'none' } },
378
+ itemTrigger: {
379
+ ...text('maximize-md'),
380
+ justifyContent: 'space-between',
381
+ gap: v('space-4'),
382
+ padding: `${v('space-4')} ${v('space-6')}`,
383
+ borderRadius: 0,
384
+ transition: `background-color ${v('disclosure-animation-duration')} ${v('disclosure-animation-easing')}`,
385
+ _hover: { bg: v('disclosure-trigger-background-hover') },
386
+ _active: { bg: v('disclosure-trigger-background-active') },
387
+ _focusVisible: { ...k.focusRingInset, outlineColor: null, borderRadius: v('radius-item') },
388
+ _disabled: { opacity: v('disclosure-opacity-disabled'), layerStyle: null },
389
+ },
390
+ itemIndicator: { color: v('disclosure-icon-color') },
391
+ itemBody: { ...text('base'), padding: `0 ${v('space-6')} ${v('space-4')}`, color: v('disclosure-content-color') },
392
+ },
393
+ variants: {
394
+ variant: { outline: { item: {} } },
395
+ size: sizes(['sm', 'md', 'lg'], () => ({ itemTrigger: {} })),
396
+ },
397
+ },
398
+
399
+ menu: {
400
+ base: {
401
+ content: { ...popup, minW: v('popup-min-width'), padding: v('popup-padding'), overflow: null },
402
+ item: { ...row, paddingBlock: v('item-padding-block-dense'), '&[data-type]': { ps: null } },
403
+ itemGroupLabel: groupLabel,
404
+ itemCommand: { ...text('condense'), color: v('text-tertiary'), opacity: null, letterSpacing: null },
405
+ itemIndicator: { color: v('item-indicator-color') },
406
+ separator: { ...k.separator, mx: 0, my: null },
407
+ arrow: { '--arrow-background': v('popup-background') },
408
+ },
409
+ variants: {
410
+ variant: { subtle: { item: {} }, solid: { item: { _highlighted: { bg: 'colorPalette.solid', color: 'colorPalette.on-solid' } } } },
411
+ size: sizes(['sm', 'md'], () => ({ content: {}, item: {} })),
412
+ },
413
+ },
414
+ select: {
415
+ base: {
416
+ trigger: {
417
+ ...field,
418
+ gap: v('gap-control'),
419
+ minH: null,
420
+ height: 'var(--select-trigger-height)',
421
+ cursor: 'pointer',
422
+ _focus: null,
423
+ _focusVisible: k.fieldStates.focus,
424
+ _placeholderShown: { color: v('field-placeholder-color') },
425
+ },
426
+ indicator: { color: v('text-tertiary') },
427
+ label: { ...text('base-label'), textStyle: null, color: v('text-primary') },
428
+ ...listParts,
429
+ },
430
+ variants: {
431
+ variant: { outline: { trigger: {} } },
432
+ size: clearListSizes(['xs', 'sm', 'md', 'lg'], (tier) => ({
433
+ root: { '--select-trigger-height': v(`control-height-${tier}`), '--select-trigger-padding-x': v(`control-padding-inline${tier === 'md' ? '' : `-${tier}`}`) },
434
+ trigger: {},
435
+ })),
436
+ },
437
+ },
438
+ combobox: {
439
+ base: {
440
+ input: { ...field, focusVisibleRing: null },
441
+ label: { ...text('base-label'), textStyle: null, color: v('text-primary') },
442
+ trigger: { color: v('text-tertiary') },
443
+ empty: { ...text('condense'), color: v('text-tertiary'), justifyContent: 'center', paddingBlock: v('popup-empty-padding-block') },
444
+ ...listParts,
445
+ },
446
+ variants: {
447
+ variant: { outline: { input: {} } },
448
+ size: clearListSizes(['xs', 'sm', 'md', 'lg'], (tier) => ({ input: fieldSize(tier) })),
449
+ },
450
+ },
451
+ popover: {
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') },
456
+ arrow: { '--arrow-background': v('popup-background') },
457
+ },
458
+ },
459
+ tooltip: {
460
+ base: {
461
+ content: {
462
+ ...text('condense'),
463
+ textStyle: null,
464
+ maxW: v('tooltip-max-width'),
465
+ padding: `${v('space-1-5')} ${v('space-2-5')}`,
466
+ borderRadius: v('radius-chrome'),
467
+ bg: v('surface-inverse'),
468
+ color: v('text-on-inverse-surface'),
469
+ boxShadow: v('popup-shadow'),
470
+ '--tooltip-bg': v('surface-inverse'),
471
+ },
472
+ arrow: { '--arrow-background': v('surface-inverse') },
473
+ },
474
+ },
475
+
476
+ dialog: {
477
+ base: {
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') },
480
+ header: { padding: 0, gap: v('space-2') },
481
+ body: { padding: 0 },
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') },
485
+ closeTrigger: { top: v('space-3'), insetEnd: v('space-3') },
486
+ },
487
+ variants: { size: { md: { content: { maxW: v('modal-max-width') } } } },
488
+ },
489
+ drawer: {
490
+ base: {
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') },
495
+ footer: { gap: v('modal-actions-gap') },
496
+ },
497
+ },
498
+
499
+ toast: {
500
+ base: {
501
+ root: {
502
+ ...k.popupSurface,
503
+ ...text('base'),
504
+ padding: v('notice-padding'),
505
+ gap: v('notice-gap'),
506
+ '--toast-trigger-bg': v('surface-raised'),
507
+ ...Object.fromEntries(
508
+ STATUS.filter((s) => scopes[s]).map((s) => [`&[data-type=${s}]`, { colorPalette: s, bg: 'colorPalette.soft', borderColor: 'colorPalette.border', color: 'colorPalette.on-soft' }]),
509
+ ),
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' } },
513
+ closeTrigger: k.iconButton,
514
+ },
515
+ },
516
+ alert: {
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') },
521
+ indicator: { color: 'inherit' },
522
+ },
523
+ variants: {
524
+ status: { ...Object.fromEntries(STATUS.map((s) => [s, { root: { colorPalette: statusPalette(s) } }])), neutral: { root: { colorPalette: 'chrome' } } },
525
+ variant: {
526
+ subtle: { root: { bg: 'colorPalette.soft', borderColor: 'colorPalette.border', color: 'colorPalette.on-soft' } },
527
+ surface: { root: { bg: 'colorPalette.soft', borderColor: 'colorPalette.border', color: 'colorPalette.on-soft' } },
528
+ outline: { root: { bg: 'transparent', borderColor: 'colorPalette.border', color: 'colorPalette.fg' } },
529
+ solid: { root: { bg: 'colorPalette.solid', borderColor: 'colorPalette.solid', color: 'colorPalette.on-solid' } },
530
+ },
531
+ size: sizes(['sm', 'md', 'lg'], () => ({ root: {} })),
532
+ },
533
+ },
534
+ progress: {
535
+ base: {
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') },
539
+ track: { bg: v('gauge-track-color'), borderRadius: v('gauge-radius') },
540
+ range: { bg: 'colorPalette.solid', borderRadius: v('gauge-radius'), transition: v('gauge-transition') },
541
+ },
542
+ variants: {
543
+ variant: { outline: { track: {}, range: {} }, subtle: { track: {}, range: {} } },
544
+ size: sizes(['xs', 'sm', 'md', 'lg', 'xl'], (tier, s) => ({ track: { height: v(s === 'xs' || s === 'sm' ? 'gauge-height-sm' : s === 'xl' ? 'gauge-height-lg' : 'gauge-height') } })),
545
+ },
546
+ },
547
+ avatar: {
548
+ base: {
549
+ root: { ...text('maximize-md'), boxSizing: 'border-box', border: `${v('stroke-emphasis')} solid ${v('surface-canvas')}`, boxShadow: v('elevation-sm'), textTransform: 'uppercase' },
550
+ },
551
+ variants: {
552
+ variant: Object.fromEntries(['solid', 'subtle', 'outline'].map((s) => [s, { root: { bg: 'colorPalette.solid', color: 'colorPalette.on-solid' } }])),
553
+ size: Object.fromEntries(['2xs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl'].map((s) => [s, { root: { '--avatar-size': v(`avatar-size-${{ '2xs': 'xs', '2xl': 'xl' }[s] ?? s}`), '--avatar-font-size': null } }])),
554
+ },
555
+ },
556
+ table: {
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') },
560
+ cell: { borderColor: v('table-row-border-color'), paddingInline: v('table-cell-padding-inline') },
561
+ row: { '&[data-selected]': { bg: v('table-row-selected') } },
562
+ },
563
+ },
564
+ numberInput: { base: { input: { ...field } } },
565
+ pinInput: { base: { input: { ...field, textAlign: 'center', boxSize: v('control-height-md'), paddingInline: 0 } } },
566
+ };
567
+
568
+ const config = {
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] &' },
571
+ globalCss: { html: { colorPalette: 'accent' } },
572
+ theme: {
573
+ tokens: {
574
+ colors,
575
+ fonts: { heading: value(v('font-family-display')), body: value(v('font-family-body')), mono: value(v('font-family-mono')) },
576
+ radii: { l1: value(v('radius-item')), l2: value(v('radius-chrome')), l3: value(v('radius-surface')) },
577
+ },
578
+ semanticTokens: { colors: semanticColors },
579
+ textStyles: Object.fromEntries(TEXT_STYLES.map((s) => [s, { value: text(s) }])),
580
+ },
581
+ };
582
+
583
+ const js = `/**
584
+ * Chakra UI v3 system for "${theme.brand}". Generated by loomiere build; do not edit.
585
+ * Load the theme's tokens.css as well: tokens and recipes read its --${prefix}- variables, so
586
+ * Chakra's components match loomiere-base-ui in light and dark (data-theme="dark" on <html>).
587
+ *
588
+ * import './tokens.css';
589
+ * import { ChakraProvider } from '@chakra-ui/react';
590
+ * import { system } from './chakra.js';
591
+ * <ChakraProvider value={system}>…</ChakraProvider>
592
+ *
593
+ * Every palette, plus "accent" and "chrome", works as a colorPalette. Button variants are
594
+ * the brand's emphasis levels (solid = bold, outline = standard, subtle = subtle,
595
+ * ghost = quiet); each keeps its own color unless you pass colorPalette.
596
+ * To add your own config on top: createSystem(config, yours).
597
+ */
598
+ import { createSystem, defaultConfig, mergeConfigs } from '@chakra-ui/react';
599
+
600
+ ${RESTYLE}
601
+
602
+ const recipes = ${toSource(recipes)};
603
+
604
+ const slotRecipes = ${toSource(slotRecipes)};
605
+
606
+ const merged = mergeConfigs(defaultConfig, ${toSource(config)});
607
+
608
+ export const config = {
609
+ ...merged,
610
+ theme: {
611
+ ...merged.theme,
612
+ recipes: restyleAll(merged.theme.recipes, recipes),
613
+ slotRecipes: restyleAll(merged.theme.slotRecipes, slotRecipes),
614
+ },
615
+ };
616
+
617
+ export const system = createSystem(config);
618
+
619
+ export default system;
620
+ `;
621
+ const dts = `/** Chakra UI v3 system for "${theme.brand}". Generated by loomiere build; do not edit. */
622
+ import type { SystemConfig, SystemContext } from '@chakra-ui/react';
623
+
624
+ export declare const config: SystemConfig;
625
+ export declare const system: SystemContext;
626
+ export default system;
627
+ `;
628
+ return { 'chakra.js': js, 'chakra.d.ts': dts };
629
+ },
630
+ };