@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.
- package/README.md +6 -1
- package/bin/loomiere.mjs +24 -0
- package/build/build-tokens.mjs +93 -49
- package/exporters/README.md +1 -1
- package/exporters/ag-grid-settings.mjs +7 -7
- package/exporters/ag-grid.mjs +6 -6
- package/exporters/chakra.mjs +43 -43
- package/exporters/component-styles.mjs +4 -4
- package/exporters/design-system.mjs +17 -12
- package/exporters/figma.mjs +774 -68
- package/exporters/mui.mjs +40 -40
- package/exporters/shadcn.mjs +5 -13
- package/exporters/tailwind.mjs +14 -5
- package/figma/README.md +136 -0
- package/figma/build-plugin.mjs +25 -0
- package/figma/engine.js +730 -0
- package/figma/library/README.md +92 -0
- package/figma/library/check/known-differences.md +20 -0
- package/figma/library/check/mojave.json +246 -0
- package/figma/library/kit/base.js +89 -0
- package/figma/library/kit/components.js +95 -0
- package/figma/library/kit/docs.js +37 -0
- package/figma/library/kit/fingerprint.js +143 -0
- package/figma/library/pages/02-palettes.js +40 -0
- package/figma/library/pages/03-color.js +126 -0
- package/figma/library/pages/04-type.js +56 -0
- package/figma/library/pages/05-scale.js +58 -0
- package/figma/library/pages/06-size.js +111 -0
- package/figma/library/pages/07-elevation-focus-motion.js +108 -0
- package/figma/library/pages/08-themes.js +36 -0
- package/figma/library/pages/09-fields/01-input.js +55 -0
- package/figma/library/pages/09-fields/02-checks.js +82 -0
- package/figma/library/pages/09-fields/03-field.js +32 -0
- package/figma/library/pages/09-fields/04-more-fields.js +144 -0
- package/figma/library/pages/09-fields/05-labels.js +14 -0
- package/figma/library/pages/09-fields/06-doc.js +75 -0
- package/figma/library/pages/10-buttons/01-buttons.js +97 -0
- package/figma/library/pages/10-buttons/02-doc.js +55 -0
- package/figma/library/pages/11-popups/01-popups.js +166 -0
- package/figma/library/pages/11-popups/02-more-popups.js +90 -0
- package/figma/library/pages/11-popups/03-labels.js +14 -0
- package/figma/library/pages/11-popups/04-doc.js +89 -0
- package/figma/library/pages/12-overlays/01-overlays.js +135 -0
- package/figma/library/pages/12-overlays/02-labels.js +10 -0
- package/figma/library/pages/12-overlays/03-doc.js +80 -0
- package/figma/library/pages/13-disclosure/01-disclosure.js +205 -0
- package/figma/library/pages/13-disclosure/02-collapsible.js +39 -0
- package/figma/library/pages/13-disclosure/03-labels.js +16 -0
- package/figma/library/pages/13-disclosure/04-doc.js +91 -0
- package/figma/library/pages/14-display/01-display.js +165 -0
- package/figma/library/pages/14-display/02-more-display.js +55 -0
- package/figma/library/pages/14-display/03-labels.js +14 -0
- package/figma/library/pages/14-display/04-doc.js +96 -0
- package/figma/library/pages/15-cover.js +87 -0
- package/figma/library/run-order.json +21 -0
- package/figma/library/tools/compare.mjs +11 -0
- package/figma/library/tools/diff-snippet.js +15 -0
- package/figma/library/tools/kit-script.mjs +21 -0
- package/figma/library/tools/run.mjs +27 -0
- package/figma/library/tools/runs.mjs +34 -0
- package/figma/plugin/code.js +746 -0
- package/figma/plugin/manifest.json +14 -0
- package/figma/plugin/ui.html +237 -0
- package/figma/plugin-main.js +14 -0
- package/generator/brand.mjs +16 -14
- package/generator/brand.schema.json +60 -0
- package/generator/generate.mjs +2 -2
- package/generator/pairs.mjs +6 -1
- package/generator/roles.mjs +55 -15
- package/generator/vision.mjs +7 -2
- package/lib/config.mjs +2 -0
- package/lib/contrast.mjs +8 -3
- package/lib/figma.mjs +338 -0
- package/lib/studio.mjs +4 -0
- package/lib/tool-server.mjs +7 -4
- package/package.json +5 -1
- package/tokens/README.md +10 -5
- package/tokens/component/avatar.json +41 -6
- package/tokens/semantic/color.json +1 -1
- package/tokens/semantic/elevation.json +8 -43
- package/tokens/semantic/typography.json +61 -94
- package/tools/brand-builder/builder.js +33 -4
package/exporters/mui.mjs
CHANGED
|
@@ -13,19 +13,19 @@ import { raw, styleKit, toSource } from './component-styles.mjs';
|
|
|
13
13
|
const MUI_GREYS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900];
|
|
14
14
|
/** MUI typography variant → text style. */
|
|
15
15
|
const TYPE = {
|
|
16
|
-
h1: 'maximize
|
|
17
|
-
h2: '
|
|
18
|
-
h3: '
|
|
19
|
-
h4: '
|
|
20
|
-
h5: '
|
|
21
|
-
h6: '
|
|
22
|
-
subtitle1: 'magnify',
|
|
23
|
-
subtitle2: 'base
|
|
24
|
-
body1: 'base',
|
|
25
|
-
body2: 'condense',
|
|
26
|
-
caption: 'minimize',
|
|
27
|
-
button: 'base
|
|
28
|
-
overline: 'condense
|
|
16
|
+
h1: 'title-maximize',
|
|
17
|
+
h2: 'title-amplify',
|
|
18
|
+
h3: 'title-magnify',
|
|
19
|
+
h4: 'title-base',
|
|
20
|
+
h5: 'title-condense',
|
|
21
|
+
h6: 'title-condense',
|
|
22
|
+
subtitle1: 'body-magnify',
|
|
23
|
+
subtitle2: 'label-base',
|
|
24
|
+
body1: 'body-base',
|
|
25
|
+
body2: 'body-condense',
|
|
26
|
+
caption: 'body-minimize',
|
|
27
|
+
button: 'trigger-base',
|
|
28
|
+
overline: 'eyebrow-condense',
|
|
29
29
|
};
|
|
30
30
|
/** MUI size → control tier. */
|
|
31
31
|
const SIZE = { small: 'sm', medium: 'md', large: 'lg' };
|
|
@@ -105,7 +105,7 @@ export default {
|
|
|
105
105
|
paddingBlock: 0,
|
|
106
106
|
paddingInline: v(`control-padding-inline${tier === 'md' ? '' : `-${tier}`}`),
|
|
107
107
|
},
|
|
108
|
-
...text(tier === 'sm' ? 'condense' : tier === 'lg' ? 'magnify' : 'base'),
|
|
108
|
+
...text(tier === 'sm' ? 'body-condense' : tier === 'lg' ? 'body-magnify' : 'body-base'),
|
|
109
109
|
});
|
|
110
110
|
|
|
111
111
|
const components = {
|
|
@@ -183,7 +183,7 @@ export default {
|
|
|
183
183
|
defaultProps: { shrink: true },
|
|
184
184
|
styleOverrides: {
|
|
185
185
|
root: {
|
|
186
|
-
...text('base
|
|
186
|
+
...text('label-base'),
|
|
187
187
|
position: 'relative',
|
|
188
188
|
transform: 'none',
|
|
189
189
|
maxWidth: 'none',
|
|
@@ -199,7 +199,7 @@ export default {
|
|
|
199
199
|
MuiFormLabel: {
|
|
200
200
|
styleOverrides: {
|
|
201
201
|
root: {
|
|
202
|
-
...text('base
|
|
202
|
+
...text('label-base'),
|
|
203
203
|
color: v('text-primary'),
|
|
204
204
|
'&.Mui-focused, &.Mui-error': { color: v('text-primary') },
|
|
205
205
|
'&.Mui-disabled': { color: v('text-disabled') },
|
|
@@ -209,16 +209,16 @@ export default {
|
|
|
209
209
|
MuiFormHelperText: {
|
|
210
210
|
styleOverrides: {
|
|
211
211
|
root: {
|
|
212
|
-
...text('condense'),
|
|
212
|
+
...text('body-condense'),
|
|
213
213
|
margin: `calc(${v('field-gap')} + ${v('field-message-spacing')}) 0 0`,
|
|
214
214
|
color: v('text-secondary'),
|
|
215
|
-
'&.Mui-error': { ...text('base
|
|
215
|
+
'&.Mui-error': { ...text('label-base'), color: v('status-error') },
|
|
216
216
|
},
|
|
217
217
|
},
|
|
218
218
|
},
|
|
219
219
|
MuiInputBase: {
|
|
220
220
|
styleOverrides: {
|
|
221
|
-
root: { ...text('base'), color: v('field-color') },
|
|
221
|
+
root: { ...text('body-base'), color: v('field-color') },
|
|
222
222
|
input: { '&::placeholder': k.fieldStates.placeholder, '&.Mui-disabled': { WebkitTextFillColor: v('field-color-disabled') } },
|
|
223
223
|
},
|
|
224
224
|
},
|
|
@@ -280,7 +280,7 @@ export default {
|
|
|
280
280
|
},
|
|
281
281
|
},
|
|
282
282
|
},
|
|
283
|
-
MuiListSubheader: { styleOverrides: { root: { ...k.groupLabel, margin: 0, lineHeight: v('font-condense-
|
|
283
|
+
MuiListSubheader: { styleOverrides: { root: { ...k.groupLabel, margin: 0, lineHeight: v('font-eyebrow-condense-line-height') } } },
|
|
284
284
|
MuiDivider: {
|
|
285
285
|
styleOverrides: {
|
|
286
286
|
root: {
|
|
@@ -300,8 +300,8 @@ export default {
|
|
|
300
300
|
'&[aria-selected="true"], &[aria-selected="true"].Mui-focused': k.itemStates.selected,
|
|
301
301
|
'&[aria-disabled="true"]': k.itemStates.disabled,
|
|
302
302
|
},
|
|
303
|
-
groupLabel: { ...k.groupLabel, lineHeight: v('font-condense-
|
|
304
|
-
noOptions: { ...text('condense'), color: v('text-tertiary'), textAlign: 'center', paddingBlock: v('popup-empty-padding-block') },
|
|
303
|
+
groupLabel: { ...k.groupLabel, lineHeight: v('font-eyebrow-condense-line-height') },
|
|
304
|
+
noOptions: { ...text('body-condense'), color: v('text-tertiary'), textAlign: 'center', paddingBlock: v('popup-empty-padding-block') },
|
|
305
305
|
clearIndicator: { ...k.iconButton, '&:hover': k.iconButtonHover },
|
|
306
306
|
popupIndicator: { color: v('text-tertiary') },
|
|
307
307
|
tag: { margin: v('space-0-5') },
|
|
@@ -318,9 +318,9 @@ export default {
|
|
|
318
318
|
color: v('text-primary'),
|
|
319
319
|
'&.Mui-disabled': { cursor: 'not-allowed' },
|
|
320
320
|
// A switch's label uses the label style, as SwitchLabel does.
|
|
321
|
-
'&:has(.MuiSwitch-root) .MuiFormControlLabel-label': text('base
|
|
321
|
+
'&:has(.MuiSwitch-root) .MuiFormControlLabel-label': text('label-base'),
|
|
322
322
|
},
|
|
323
|
-
label: { ...text('base'), '&.Mui-disabled': { color: v('text-disabled') } },
|
|
323
|
+
label: { ...text('body-base'), '&.Mui-disabled': { color: v('text-disabled') } },
|
|
324
324
|
// A switch's label is the label style, as SwitchLabel is.
|
|
325
325
|
|
|
326
326
|
},
|
|
@@ -416,7 +416,7 @@ export default {
|
|
|
416
416
|
border: `${v('check-border-width')} solid ${v('check-border-color')}`,
|
|
417
417
|
transition: `background-color ${v('duration-normal')} ${v('easing-in-out')}`,
|
|
418
418
|
'&::before': {
|
|
419
|
-
...text('condense
|
|
419
|
+
...text('eyebrow-condense'),
|
|
420
420
|
content: '"off"',
|
|
421
421
|
position: 'absolute',
|
|
422
422
|
insetBlock: 0,
|
|
@@ -464,7 +464,7 @@ export default {
|
|
|
464
464
|
},
|
|
465
465
|
mark: { width: v('stroke-emphasis'), height: `calc(${v('gauge-height-sm')} * 2)`, background: v('fill-strong'), borderRadius: v('radius-pill') },
|
|
466
466
|
markActive: { background: v('fill-strong'), opacity: 1 },
|
|
467
|
-
valueLabel: { ...text('condense'), background: v('surface-inverse'), color: v('text-on-inverse-surface'), borderRadius: v('radius-chrome') },
|
|
467
|
+
valueLabel: { ...text('body-condense'), background: v('surface-inverse'), color: v('text-on-inverse-surface'), borderRadius: v('radius-chrome') },
|
|
468
468
|
},
|
|
469
469
|
},
|
|
470
470
|
|
|
@@ -478,7 +478,7 @@ export default {
|
|
|
478
478
|
MuiTab: {
|
|
479
479
|
styleOverrides: {
|
|
480
480
|
root: {
|
|
481
|
-
...text('base'),
|
|
481
|
+
...text('body-base'),
|
|
482
482
|
textTransform: 'none',
|
|
483
483
|
minHeight: 0,
|
|
484
484
|
minWidth: 0,
|
|
@@ -487,7 +487,7 @@ export default {
|
|
|
487
487
|
opacity: 1,
|
|
488
488
|
transition: v('transition-button'),
|
|
489
489
|
'&:hover:not(.Mui-disabled)': { color: v('text-primary'), background: v('disclosure-trigger-background-hover') },
|
|
490
|
-
'&.Mui-selected': { ...text('base
|
|
490
|
+
'&.Mui-selected': { ...text('selected-base'), color: v('disclosure-selected-color') },
|
|
491
491
|
'&.Mui-focusVisible': k.focusRingInset,
|
|
492
492
|
'&.Mui-disabled': { color: v('text-disabled'), opacity: v('disclosure-opacity-disabled') },
|
|
493
493
|
},
|
|
@@ -509,7 +509,7 @@ export default {
|
|
|
509
509
|
MuiAccordionSummary: {
|
|
510
510
|
styleOverrides: {
|
|
511
511
|
root: {
|
|
512
|
-
...text('
|
|
512
|
+
...text('title-base'),
|
|
513
513
|
gap: v('space-4'),
|
|
514
514
|
minHeight: 0,
|
|
515
515
|
padding: `${v('space-4')} ${v('space-6')}`,
|
|
@@ -523,13 +523,13 @@ export default {
|
|
|
523
523
|
expandIconWrapper: { color: v('disclosure-icon-color') },
|
|
524
524
|
},
|
|
525
525
|
},
|
|
526
|
-
MuiAccordionDetails: { styleOverrides: { root: { ...text('base'), padding: `0 ${v('space-6')} ${v('space-4')}`, color: v('disclosure-content-color') } } },
|
|
526
|
+
MuiAccordionDetails: { styleOverrides: { root: { ...text('body-base'), padding: `0 ${v('space-6')} ${v('space-4')}`, color: v('disclosure-content-color') } } },
|
|
527
527
|
|
|
528
528
|
// Overlays.
|
|
529
529
|
MuiTooltip: {
|
|
530
530
|
styleOverrides: {
|
|
531
531
|
tooltip: {
|
|
532
|
-
...text('condense'),
|
|
532
|
+
...text('body-condense'),
|
|
533
533
|
maxWidth: v('tooltip-max-width'),
|
|
534
534
|
padding: `${v('space-1-5')} ${v('space-2-5')}`,
|
|
535
535
|
borderRadius: v('radius-chrome'),
|
|
@@ -546,9 +546,9 @@ export default {
|
|
|
546
546
|
paper: { ...k.modalSurface, width: v('modal-width'), maxWidth: v('modal-max-width'), maxHeight: v('modal-max-height'), margin: v('space-4') },
|
|
547
547
|
},
|
|
548
548
|
},
|
|
549
|
-
MuiDialogTitle: { styleOverrides: { root: { ...text('
|
|
549
|
+
MuiDialogTitle: { styleOverrides: { root: { ...text('title-amplify'), padding: 0, color: v('modal-title-color') } } },
|
|
550
550
|
MuiDialogContent: { styleOverrides: { root: { padding: 0, '.MuiDialogTitle-root + &': { paddingTop: 0 } } } },
|
|
551
|
-
MuiDialogContentText: { styleOverrides: { root: { ...text('base'), color: v('modal-description-color') } } },
|
|
551
|
+
MuiDialogContentText: { styleOverrides: { root: { ...text('body-base'), color: v('modal-description-color') } } },
|
|
552
552
|
MuiDialogActions: {
|
|
553
553
|
styleOverrides: { root: { padding: 0, gap: v('modal-actions-gap'), marginTop: v('modal-actions-spacing'), '& > :not(style) ~ :not(style)': { marginLeft: 0 } } },
|
|
554
554
|
},
|
|
@@ -557,11 +557,11 @@ export default {
|
|
|
557
557
|
},
|
|
558
558
|
|
|
559
559
|
// Feedback.
|
|
560
|
-
MuiSnackbarContent: { styleOverrides: { root: { ...k.popupSurface, ...text('base'), padding: v('notice-padding') } } },
|
|
560
|
+
MuiSnackbarContent: { styleOverrides: { root: { ...k.popupSurface, ...text('body-base'), padding: v('notice-padding') } } },
|
|
561
561
|
MuiAlert: {
|
|
562
562
|
styleOverrides: {
|
|
563
563
|
root: {
|
|
564
|
-
...text('base'),
|
|
564
|
+
...text('body-base'),
|
|
565
565
|
padding: v('notice-padding'),
|
|
566
566
|
gap: v('notice-gap'),
|
|
567
567
|
borderRadius: v('popup-radius'),
|
|
@@ -577,7 +577,7 @@ export default {
|
|
|
577
577
|
},
|
|
578
578
|
},
|
|
579
579
|
},
|
|
580
|
-
MuiAlertTitle: { styleOverrides: { root: { ...text('
|
|
580
|
+
MuiAlertTitle: { styleOverrides: { root: { ...text('title-base'), margin: `0 0 ${v('notice-text-gap')}` } } },
|
|
581
581
|
MuiLinearProgress: {
|
|
582
582
|
styleOverrides: {
|
|
583
583
|
root: {
|
|
@@ -599,7 +599,7 @@ export default {
|
|
|
599
599
|
MuiAvatar: {
|
|
600
600
|
styleOverrides: {
|
|
601
601
|
root: {
|
|
602
|
-
...text('
|
|
602
|
+
...text('title-base'),
|
|
603
603
|
width: v('avatar-size-md'),
|
|
604
604
|
height: v('avatar-size-md'),
|
|
605
605
|
boxSizing: 'border-box',
|
|
@@ -613,8 +613,8 @@ export default {
|
|
|
613
613
|
MuiAvatarGroup: { styleOverrides: { avatar: { border: `${v('stroke-emphasis')} solid ${v('surface-canvas')}`, boxSizing: 'border-box' } } },
|
|
614
614
|
MuiTableCell: {
|
|
615
615
|
styleOverrides: {
|
|
616
|
-
root: { ...text('condense'), color: v('table-color'), borderBottom: `${v('stroke-default')} solid ${v('table-row-border-color')}`, paddingInline: v('table-cell-padding-inline') },
|
|
617
|
-
head: { ...text('base
|
|
616
|
+
root: { ...text('body-condense'), color: v('table-color'), borderBottom: `${v('stroke-default')} solid ${v('table-row-border-color')}`, paddingInline: v('table-cell-padding-inline') },
|
|
617
|
+
head: { ...text('label-base'), background: v('table-header-background'), color: v('table-header-color') },
|
|
618
618
|
},
|
|
619
619
|
},
|
|
620
620
|
MuiTableRow: { styleOverrides: { root: { '&.MuiTableRow-hover:hover': { background: v('table-row-hover') }, '&.Mui-selected, &.Mui-selected:hover': { background: v('table-row-selected') } } } },
|
|
@@ -628,7 +628,7 @@ export default {
|
|
|
628
628
|
typography: {
|
|
629
629
|
fontFamily: theme.fonts.body,
|
|
630
630
|
...Object.fromEntries(Object.entries(TYPE).map(([variant, style]) => [variant, text(style)])),
|
|
631
|
-
button: { ...text('base
|
|
631
|
+
button: { ...text('trigger-base'), textTransform: 'none' },
|
|
632
632
|
},
|
|
633
633
|
components,
|
|
634
634
|
};
|
package/exporters/shadcn.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* It replaces everything after the imports in shadcn's globals.css. Only colors,
|
|
8
8
|
* the radius, fonts and text sizes follow the brand; shadcn's own spacing and shapes stay.
|
|
9
9
|
*/
|
|
10
|
+
import { darkVariant } from './tailwind.mjs';
|
|
10
11
|
|
|
11
12
|
/** shadcn variable → Loomiere token path. */
|
|
12
13
|
const VARIABLES = {
|
|
@@ -48,7 +49,7 @@ const VARIABLES = {
|
|
|
48
49
|
};
|
|
49
50
|
|
|
50
51
|
/** shadcn's text sizes → the text tier whose size they take. */
|
|
51
|
-
const SIZES = { xs: 'minimize', sm: 'condense', base: 'base', lg: 'magnify' };
|
|
52
|
+
const SIZES = { xs: 'body-minimize', sm: 'body-condense', base: 'body-base', lg: 'body-magnify' };
|
|
52
53
|
|
|
53
54
|
export default {
|
|
54
55
|
id: 'shadcn',
|
|
@@ -72,8 +73,8 @@ export default {
|
|
|
72
73
|
@import "<theme>/exports/shadcn.css";
|
|
73
74
|
Replaces everything after the imports in shadcn's globals.css. Every variable
|
|
74
75
|
points at a --${theme.prefix}- concept, so dark mode, contrast modes and color vision themes come
|
|
75
|
-
along. Dark mode is data-theme="dark" on <html> (next-themes:
|
|
76
|
-
system setting. With the Loomiere Tailwind theme as well, import this after it: text-xs to
|
|
76
|
+
along. Dark mode is data-theme="dark" or "dark-high-contrast" on <html> (next-themes:
|
|
77
|
+
attribute="data-theme"), or the system setting. With the Loomiere Tailwind theme as well, import this after it: text-xs to
|
|
77
78
|
text-lg come back for shadcn's components, sized by the text tiers. */
|
|
78
79
|
@import "../tokens.css";
|
|
79
80
|
|
|
@@ -81,16 +82,7 @@ export default {
|
|
|
81
82
|
${root.join('\n')}
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
|
|
85
|
-
&:where([data-theme='dark'], [data-theme='dark'] *) {
|
|
86
|
-
@slot;
|
|
87
|
-
}
|
|
88
|
-
@media (prefers-color-scheme: dark) {
|
|
89
|
-
&:where(:root:not([data-theme='light']), :root:not([data-theme='light']) *) {
|
|
90
|
-
@slot;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
85
|
+
${darkVariant(theme)}
|
|
94
86
|
|
|
95
87
|
@theme inline {
|
|
96
88
|
${[...colors, ...fonts, ...sizes].join('\n')}
|
package/exporters/tailwind.mjs
CHANGED
|
@@ -60,7 +60,7 @@ export default {
|
|
|
60
60
|
id: 'tailwind',
|
|
61
61
|
name: 'Tailwind CSS v4',
|
|
62
62
|
file: 'tailwind.css',
|
|
63
|
-
for: 'Tailwind v4 projects: import after tailwindcss (it brings tokens.css along). Utilities like bg-accent-solid, text-fg, bg-emphasis-bg, and text styles by tier: text-base, text-magnify
|
|
63
|
+
for: 'Tailwind v4 projects: import after tailwindcss (it brings tokens.css along). Utilities like bg-accent-solid, text-fg, bg-emphasis-bg, and text styles by tier: text-body-base, text-trigger-magnify.',
|
|
64
64
|
build(theme) {
|
|
65
65
|
const name = new Map(theme.tokens.map((t) => [t.path, t.name]));
|
|
66
66
|
const line = (twVar, path) => (name.has(path) ? ` ${twVar}: var(${name.get(path)});` : null);
|
|
@@ -102,7 +102,7 @@ export default {
|
|
|
102
102
|
for (const g of ['popup', 'modal']) lines.push(line(`--shadow-${g}`, `${g}.shadow`));
|
|
103
103
|
// Media queries can't read custom properties, so breakpoints are written as values.
|
|
104
104
|
const breakpoints = theme.tokens.filter((t) => /^breakpoint\.[\w-]+$/.test(t.path)).map((t) => ` --breakpoint-${t.path.slice(11)}: ${t.value};`);
|
|
105
|
-
// Each text style is one utility named by its tier (text-base, text-magnify
|
|
105
|
+
// Each text style is one utility named by its tier (text-body-base, text-trigger-magnify), so a
|
|
106
106
|
// class sets family, size, weight, line height and letter spacing together.
|
|
107
107
|
const styles = new Map();
|
|
108
108
|
for (const t of theme.tokens) {
|
|
@@ -113,9 +113,9 @@ export default {
|
|
|
113
113
|
// z-index layers by name (z-modal, z-toast); Tailwind's z-10 and friends still work.
|
|
114
114
|
const z = theme.tokens.filter((t) => /^z-index\.[\w-]+$/.test(t.path)).map((t) => `@utility z-${t.path.slice(8)} {\n z-index: var(${t.name});\n}`);
|
|
115
115
|
const durations = theme.tokens.filter((t) => /^duration\.[\w-]+$/.test(t.path)).map((t) => `@utility duration-${t.path.slice(9)} {\n --tw-duration: var(${t.name});\n transition-duration: var(${t.name});\n}`);
|
|
116
|
-
// dark: follows Loomiere's
|
|
117
|
-
// unless data-theme
|
|
118
|
-
const dark =
|
|
116
|
+
// dark: follows Loomiere's theme: data-theme="dark" or "dark-high-contrast" on <html>, or
|
|
117
|
+
// the system setting unless data-theme names a light theme (color vision themes are light).
|
|
118
|
+
const dark = darkVariant(theme);
|
|
119
119
|
// Brand pairs and color scopes as utilities (pair-stone-soft, scope-error), the same
|
|
120
120
|
// declarations as .${theme.prefix}-pair-<name> and [data-color='<name>'] in tokens.css, so
|
|
121
121
|
// variants work on them: md:pair-stone-soft, hover:scope-error.
|
|
@@ -137,3 +137,12 @@ export default {
|
|
|
137
137
|
@theme inline {\n${lines.filter(Boolean).join('\n')}\n}\n\n@theme {\n --breakpoint-*: initial;\n${breakpoints.join('\n')}\n}\n\n${[dark, ...utilities, ...z, ...durations, ...pairs, ...scopeUtilities].filter(Boolean).join('\n\n')}\n`;
|
|
138
138
|
},
|
|
139
139
|
};
|
|
140
|
+
|
|
141
|
+
/** Tailwind's dark variant on Loomiere's themes; shadcn.mjs uses it too. */
|
|
142
|
+
export function darkVariant(theme) {
|
|
143
|
+
const visions = [...new Set(theme.tokens.filter((t) => t.path.startsWith('vision.')).map((t) => t.path.split('.')[1]))];
|
|
144
|
+
const list = (names) => names.map((n) => `[data-theme='${n}']`).join(', ');
|
|
145
|
+
const dark = list(['dark', 'dark-high-contrast']);
|
|
146
|
+
const notLight = `:root:not(${list(['light', 'light-high-contrast', ...visions])})`;
|
|
147
|
+
return `@custom-variant dark {\n &:where(${dark}, :is(${dark}) *) {\n @slot;\n }\n @media (prefers-color-scheme: dark) {\n &:where(${notLight}, ${notLight} *) {\n @slot;\n }\n }\n}`;
|
|
148
|
+
}
|
package/figma/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Figma
|
|
2
|
+
|
|
3
|
+
Loomiere sends a built theme into a Figma file as variables, text styles and effect styles. The repo stays the
|
|
4
|
+
source of truth: Figma gets a copy that updates in place, so layers bound to a variable or text
|
|
5
|
+
style stay bound when the theme changes.
|
|
6
|
+
|
|
7
|
+
There are three ways to send it. All three run the same engine (`engine.js`), so the file ends
|
|
8
|
+
up the same whichever you use.
|
|
9
|
+
|
|
10
|
+
| Who | Way | What it needs |
|
|
11
|
+
| --------------------------------------- | -------------------------------------------------------------- | --------------------------------------------------------------------------------- |
|
|
12
|
+
| You, in a Claude chat about the project | Say "sync Figma" | Claude with the Figma connector |
|
|
13
|
+
| You, in a terminal | `npx loomiere figma push` | Claude Code with a Figma connector, or an Enterprise `FIGMA_TOKEN`, or the plugin |
|
|
14
|
+
| A designer without the repo | The Loomiere plugin: import `figma.json` or pull it from a URL | Figma desktop |
|
|
15
|
+
|
|
16
|
+
Nothing watches for changes. A push happens when someone asks for one.
|
|
17
|
+
|
|
18
|
+
## What goes into Figma
|
|
19
|
+
|
|
20
|
+
`loomiere build` writes `exports/figma.json`. It holds the variables in six collections and
|
|
21
|
+
one text style per text style token.
|
|
22
|
+
|
|
23
|
+
Hidden collections hold the raw values, with a group for each variant. Published collections are
|
|
24
|
+
what designers pick from and switch. Figma picks each collection's mode on its own, and one
|
|
25
|
+
collection's mode can't choose another's, so a variant that a single switch has to change is a
|
|
26
|
+
group in a hidden collection, and the published collection's modes point at the right group.
|
|
27
|
+
|
|
28
|
+
| Collection | Modes | Holds |
|
|
29
|
+
| -------------- | --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
30
|
+
| Brand (hidden) | Default | Font families, and a complete group of the brand's colors per theme: `default/color/…`, then `deuteranopia/color/…` and the other color vision themes. Data colors are here by name (`color/data/violet`). A color a vision theme doesn't change points at default's. |
|
|
31
|
+
| Scale (hidden) | Default | The raw steps: space, radius, stroke and icon size scales, and the font sizes as `desktop/`, `tablet/` and `mobile/font-size/…`, each `clamp()` read at that width (tablet at 768px). |
|
|
32
|
+
| Size | Desktop, Tablet, Mobile | The sizes designers pick: font sizes, and the named semantic and component sizes (control heights, gaps, padding), each pointing at a Scale step. |
|
|
33
|
+
| Theme | Light, Dark, Light high contrast, Dark high contrast, Deuteranopia, Protanopia, Tritanopia, Achromatopsia | The themes, as `data-theme` in CSS. Each color vision theme is a light theme of its own with the brand's colors redesigned for that kind of color vision. Holds every semantic color designers pick, the data series slots (`data/1`…, each pointing at the named data color that theme puts there), and, with no scopes, the palette scopes and each brand pair's parts (`pair/<name>/bg`…) that Status points at. |
|
|
34
|
+
| Status | Default, Info, Success, Warning, Error, Neutral | What each emphasis level is in each status (`bold/bg`, `subtle/fg`…): a brand pair the brand file picked (`roles.status`), or the level's style in the status's palette. As `data-status` in CSS; Neutral is the chrome palette, as `data-color="neutral"` (`palette="neutral"`). No scopes; Emphasis reads it. |
|
|
35
|
+
| Emphasis | Bold, Standard, Subtle, Quiet | The emphasis parts components paint with (`emphasis/bg`, `emphasis/fg`…), as `data-emphasis`, each pointing at its level in Status. |
|
|
36
|
+
|
|
37
|
+
So one frame can be Dark, Warning and Subtle at once, the same as the CSS, and an emphasized
|
|
38
|
+
component follows the theme set on the page around it.
|
|
39
|
+
|
|
40
|
+
Files synced before these names are renamed in place on the next sync (Scheme, Theme for the
|
|
41
|
+
pairs, Color, Pairs, Size base), and Brand's colors move into their theme groups, so layers bound
|
|
42
|
+
to them stay bound. Color Pair and Color Role are retired: a sync with "remove" deletes them.
|
|
43
|
+
|
|
44
|
+
Every variable has its CSS variable as code syntax (`var(--sl-text-primary)`), the token's
|
|
45
|
+
description, and scopes that keep it to the right pickers (text colors only show for text, radii
|
|
46
|
+
only for corners). Text styles follow the Size and Brand variables for their size and family, and
|
|
47
|
+
their description names the CSS class that matches.
|
|
48
|
+
|
|
49
|
+
Effect styles hold what a variable can't: elevation/xs to elevation/2xl from the shadow tokens,
|
|
50
|
+
focus/ring (gap, ring and halo), and focus/field and focus/field-error for fields. Shadow colors that are
|
|
51
|
+
Theme tokens stay bound, so the focus styles follow light, dark and the other themes.
|
|
52
|
+
|
|
53
|
+
### Plans
|
|
54
|
+
|
|
55
|
+
Modes per collection depend on the Figma plan: Professional allows 10, Organization 20. The sync
|
|
56
|
+
checks the file's limit each time.
|
|
57
|
+
|
|
58
|
+
- **Professional and up** get the layout above. If a collection has more modes than the plan
|
|
59
|
+
allows, the extra modes are left out and the sync says which.
|
|
60
|
+
- **Starter** allows one mode per collection, so it gets a simpler layout instead: Brand, Color Pair,
|
|
61
|
+
Theme and "Theme dark", with sizes at their desktop value and no color vision modes. Moving
|
|
62
|
+
the file to a paid plan and syncing again switches it to the full layout; sync with "remove"
|
|
63
|
+
to delete the old collections.
|
|
64
|
+
|
|
65
|
+
## 1. In a Claude chat
|
|
66
|
+
|
|
67
|
+
Ask Claude to sync Figma. Claude runs the same steps as `--via scripts` below with the Figma
|
|
68
|
+
connector:
|
|
69
|
+
|
|
70
|
+
1. `loomiere build`, then `loomiere figma push --via scripts`, which writes a snapshot script.
|
|
71
|
+
2. Runs the snapshot in the Figma file. It returns the plan's mode limit and a short hash for
|
|
72
|
+
each group of variables (`Brand:default/color/primary`, `Theme:text`…).
|
|
73
|
+
3. `loomiere figma push --via scripts --since <snapshot>` writes numbered scripts holding only
|
|
74
|
+
the groups that differ, plus a last one with the text and effect styles and anything to remove.
|
|
75
|
+
4. Runs them in order and reports what changed.
|
|
76
|
+
|
|
77
|
+
After the first push, a change to one palette sends one or two groups, not the whole theme.
|
|
78
|
+
|
|
79
|
+
## 2. In a terminal: `loomiere figma push`
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npx loomiere figma push # build, then push by the first way that works here
|
|
83
|
+
npx loomiere figma push --dry-run # report what would change
|
|
84
|
+
npx loomiere figma push --remove # also delete what the build no longer has
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Say which file in `loomiere.config.json`:
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{ "figma": { "file": "https://www.figma.com/design/<key>/<name>" } }
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
or with `--file <url>`. Then it picks a way, or you pick with `--via`:
|
|
94
|
+
|
|
95
|
+
| `--via` | When it's picked | How it works |
|
|
96
|
+
| --------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
97
|
+
| `api` | `FIGMA_TOKEN` is set | Writes variables through Figma's REST API. Only Enterprise plans can write variables this way, and the API can't write text or effect styles, so those still need one of the other ways. The token needs the `file_variables:write` scope. |
|
|
98
|
+
| `claude` | Claude Code is installed | Hands the push to Claude Code (`claude -p`), which runs the steps from the chat way with its Figma connector. Claude Code needs a Figma connector (`claude mcp add` or the claude.ai Figma connector). |
|
|
99
|
+
| `plugin` | Neither of the above | Serves the build at `http://localhost:4322/figma.json` and asks you to click **Pull from project** in the plugin. |
|
|
100
|
+
| `scripts` | Only when asked | Writes the push scripts to `<out>/figma-push/` and stops. What the chat way and `--via claude` run. |
|
|
101
|
+
|
|
102
|
+
`npx loomiere figma serve` serves the build for the plugin without pushing. `loomiere studio`
|
|
103
|
+
serves it too, at the same address, so a designer next to you can pull while you edit.
|
|
104
|
+
|
|
105
|
+
## 3. The Loomiere plugin
|
|
106
|
+
|
|
107
|
+
For designers who don't run the repo. Until it's published to the Figma Community:
|
|
108
|
+
|
|
109
|
+
1. Get this package (`npm pack @scattered-light/loomiere`, or a clone of the repo).
|
|
110
|
+
2. In the Figma desktop app: **Plugins › Development › Import plugin from manifest…** and pick
|
|
111
|
+
`figma/plugin/manifest.json`.
|
|
112
|
+
3. Run **Plugins › Development › Loomiere**.
|
|
113
|
+
|
|
114
|
+
Then bring a theme in one of three ways:
|
|
115
|
+
|
|
116
|
+
- **Theme file:** drop or choose a `figma.json`. Whoever builds the theme can send it over; it's
|
|
117
|
+
in `exports/` of every build.
|
|
118
|
+
- **Pull from project:** reads `http://localhost:4322/figma.json` from `loomiere studio` or
|
|
119
|
+
`loomiere figma serve` on the same computer.
|
|
120
|
+
- **Pull from URL:** a published theme package, for example
|
|
121
|
+
`https://unpkg.com/@scattered-light/loomiere-mojave/dist/exports/figma.json`.
|
|
122
|
+
|
|
123
|
+
The plugin checks the file first and lists what it will create, change and remove. Nothing
|
|
124
|
+
changes until you click **Apply**. Leave "Also delete" off to keep anything the theme no longer
|
|
125
|
+
has.
|
|
126
|
+
|
|
127
|
+
## Files
|
|
128
|
+
|
|
129
|
+
| File | What it is |
|
|
130
|
+
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
131
|
+
| `../exporters/figma.mjs` | Writes `figma.json` from the token catalog. |
|
|
132
|
+
| `engine.js` | The sync engine: plain JavaScript for Figma's plugin API, with no imports, so the plugin, the push scripts and Node all run the same text. |
|
|
133
|
+
| `../lib/figma.mjs` | `loomiere figma`: the push scripts, the four ways, `serve`. |
|
|
134
|
+
| `library/` | The scripts that draw the library pages (foundations, components, docs, Cover) in a file, with a run order and checks against Mojave. See [library/README.md](library/README.md). |
|
|
135
|
+
| `plugin/` | The plugin. `code.js` is generated from `engine.js` and `plugin-main.js` by `npm run build -w @scattered-light/loomiere`. |
|
|
136
|
+
| `../../../scripts/check-figma.mjs` | `npm run check:figma`: runs the engine against a stand-in for Figma's API in both layouts, whole and in parts, and checks a second run changes nothing. |
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Writes plugin/code.js: the sync engine plus the plugin's own few lines, as the one file a
|
|
3
|
+
* Figma plugin runs. `node figma/build-plugin.mjs --check` fails if code.js is out of date.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const out = path.join(here, 'plugin', 'code.js');
|
|
11
|
+
const code = [
|
|
12
|
+
'// Generated by figma/build-plugin.mjs from engine.js and plugin-main.js. Do not edit.',
|
|
13
|
+
fs.readFileSync(path.join(here, 'engine.js'), 'utf8'),
|
|
14
|
+
fs.readFileSync(path.join(here, 'plugin-main.js'), 'utf8'),
|
|
15
|
+
].join('\n');
|
|
16
|
+
|
|
17
|
+
if (process.argv.includes('--check')) {
|
|
18
|
+
if (!fs.existsSync(out) || fs.readFileSync(out, 'utf8') !== code) {
|
|
19
|
+
console.error('figma/plugin/code.js is out of date: run npm run build -w @scattered-light/loomiere');
|
|
20
|
+
process.exitCode = 1;
|
|
21
|
+
}
|
|
22
|
+
} else {
|
|
23
|
+
fs.writeFileSync(out, code);
|
|
24
|
+
console.log(`Wrote ${path.relative(process.cwd(), out)}.`);
|
|
25
|
+
}
|