inkbridge 0.1.0-beta.2 → 0.1.0-beta.21
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 +108 -25
- package/bin/inkbridge.mjs +354 -83
- package/code.js +40 -11802
- package/manifest.json +1 -0
- package/package.json +74 -23
- package/scanner/adapter-utils-regression.ts +159 -0
- package/scanner/aspect-percent-position-regression.ts +237 -0
- package/scanner/aspect-ratio-regression.ts +90 -0
- package/scanner/blob-placement-regression.ts +2 -2
- package/scanner/block-cache-regression.ts +195 -0
- package/scanner/bundle-size-regression.ts +50 -0
- package/scanner/child-sizing-matrix-regression.ts +303 -0
- package/scanner/cli.ts +342 -13
- package/scanner/component-scanner.ts +2108 -174
- package/scanner/component-sections-regression.ts +198 -0
- package/scanner/compound-classes-lookup-regression.ts +163 -0
- package/scanner/css-token-reader-regression.ts +7 -6
- package/scanner/css-token-reader.ts +152 -31
- package/scanner/cva-jsx-child-fallback-regression.ts +98 -0
- package/scanner/cva-master-icon-regression.ts +315 -0
- package/scanner/data-attr-prop-alias-regression.ts +129 -0
- package/scanner/explicit-size-root-regression.ts +102 -0
- package/scanner/font-family-extract-regression.ts +113 -0
- package/scanner/font-style-resolver-regression.ts +1 -1
- package/scanner/framework-adapter-shadcn-regression.ts +480 -0
- package/scanner/full-width-matrix-regression.ts +338 -0
- package/scanner/grid-cols-extraction-regression.ts +110 -0
- package/scanner/image-src-collector-regression.ts +204 -0
- package/scanner/inline-flex-regression.ts +235 -0
- package/scanner/input-range-regression.ts +217 -0
- package/scanner/instance-rendering-regression.ts +224 -0
- package/scanner/jsx-prop-unresolved-regression.ts +178 -0
- package/scanner/jsx-text-regression.ts +178 -0
- package/scanner/layout-alignment-regression.ts +108 -0
- package/scanner/layout-flex-regression.ts +90 -0
- package/scanner/layout-mode-regression.ts +71 -0
- package/scanner/layout-sizing-regression.ts +227 -0
- package/scanner/layout-spacing-regression.ts +135 -0
- package/scanner/local-const-className-regression.ts +331 -0
- package/scanner/percent-position-regression.ts +105 -0
- package/scanner/provider-cascade-regression.ts +224 -0
- package/scanner/provider-flatten-regression.ts +235 -0
- package/scanner/radial-gradient-regression.ts +1 -1
- package/scanner/render-prop-parser-regression.ts +161 -0
- package/scanner/ring-utility-regression.ts +153 -0
- package/scanner/sandbox-spread-regression.ts +125 -0
- package/scanner/selection-pressed-regression.ts +241 -0
- package/scanner/size-full-normalization-regression.ts +127 -0
- package/scanner/state-classification-regression.ts +175 -0
- package/scanner/story-diagnostics-regression.ts +216 -0
- package/scanner/story-dimensioning-regression.ts +298 -0
- package/scanner/story-render-strategy-regression.ts +205 -0
- package/scanner/stretch-to-parent-width-regression.ts +147 -0
- package/scanner/svg-fill-parent-regression.ts +98 -0
- package/scanner/svg-group-inheritance-regression.ts +166 -0
- package/scanner/svg-marker-inline-regression.ts +211 -0
- package/scanner/svg-marker-regression.ts +116 -0
- package/scanner/tailwind-parser.ts +46 -4
- package/scanner/text-resize-matrix-regression.ts +173 -0
- package/scanner/transform-math-regression.ts +1 -1
- package/scanner/types.ts +26 -2
- package/src/cache/frame-cache.ts +150 -0
- package/src/cache/index.ts +2 -0
- package/src/{component-defs.ts → components/component-defs.ts} +25 -10
- package/src/{component-gen.ts → components/component-gen.ts} +43 -116
- package/src/components/component-instance.ts +386 -0
- package/src/components/component-library.ts +44 -0
- package/src/components/component-lookup.ts +161 -0
- package/src/components/index.ts +7 -0
- package/src/components/scanner-types.ts +39 -0
- package/src/components/symbol-instance-policy.ts +312 -0
- package/src/design-system/block-cache.ts +130 -0
- package/src/design-system/component-sections.ts +107 -0
- package/src/design-system/cva-inference.ts +187 -0
- package/src/design-system/cva-master.ts +427 -0
- package/src/design-system/cva-utils.ts +29 -0
- package/src/design-system/design-system.ts +334 -0
- package/src/design-system/frame-stabilizers.ts +191 -0
- package/src/design-system/frame-utils.ts +46 -0
- package/src/design-system/generated-node.ts +84 -0
- package/src/design-system/icon-rendering.ts +229 -0
- package/src/design-system/index.ts +13 -0
- package/src/design-system/instance-rendering.ts +307 -0
- package/src/design-system/master-shared.ts +133 -0
- package/src/design-system/node-helpers.ts +237 -0
- package/src/design-system/node-variants.ts +196 -0
- package/src/design-system/non-cva-master.ts +104 -0
- package/src/design-system/portal-handling.ts +138 -0
- package/src/design-system/preview-builder.ts +738 -0
- package/src/{render-context.ts → design-system/render-context.ts} +32 -6
- package/src/design-system/render-prop-parser.ts +50 -0
- package/src/design-system/responsive-resolver.ts +180 -0
- package/src/design-system/selectable-state.ts +157 -0
- package/src/design-system/state-master.ts +267 -0
- package/src/design-system/state-utils.ts +15 -0
- package/src/design-system/story-builder-context.ts +40 -0
- package/src/design-system/story-builder.ts +1322 -0
- package/src/design-system/story-diagnostics.ts +80 -0
- package/src/design-system/story-dimensioning.ts +272 -0
- package/src/design-system/story-frames.ts +400 -0
- package/src/design-system/story-instance.ts +333 -0
- package/src/{story-layout.ts → design-system/story-layout.ts} +2 -2
- package/src/design-system/story-render-strategy.ts +150 -0
- package/src/design-system/story-tree-search.ts +110 -0
- package/src/design-system/symbol-fallback.ts +89 -0
- package/src/design-system/symbol-source.ts +172 -0
- package/src/design-system/table-helpers.ts +56 -0
- package/src/design-system/tag-predicates.ts +99 -0
- package/src/design-system/theme-context.ts +52 -0
- package/src/design-system/typography.ts +100 -0
- package/src/design-system/ui-builder.ts +2676 -0
- package/src/{clip-path-decorative.ts → effects/clip-path-decorative.ts} +11 -11
- package/src/effects/icon-builder.ts +1074 -0
- package/src/effects/index.ts +5 -0
- package/src/effects/portal-panel.ts +369 -0
- package/src/{radial-gradient.ts → effects/radial-gradient.ts} +1 -1
- package/src/framework-adapters/index.ts +47 -0
- package/src/framework-adapters/shadcn.ts +541 -0
- package/src/{github.ts → github/github.ts} +46 -21
- package/src/github/index.ts +1 -0
- package/src/layout/deferred-layout.ts +1556 -0
- package/src/layout/index.ts +24 -0
- package/src/layout/layout-parser.ts +375 -0
- package/src/{layout-utils.ts → layout/layout-utils.ts} +23 -17
- package/src/layout/parser/alignment.ts +54 -0
- package/src/layout/parser/flex.ts +59 -0
- package/src/layout/parser/index.ts +65 -0
- package/src/layout/parser/ir.ts +80 -0
- package/src/layout/parser/layout-mode.ts +57 -0
- package/src/layout/parser/sizing.ts +241 -0
- package/src/layout/parser/spacing-scale.ts +78 -0
- package/src/layout/parser/spacing.ts +134 -0
- package/src/layout/ring-utils.ts +120 -0
- package/src/layout/size-utils.ts +143 -0
- package/src/layout/text-resize-decision.ts +51 -0
- package/src/{width-solver.ts → layout/width-solver.ts} +168 -37
- package/src/main.ts +444 -162
- package/src/{config.ts → plugin/config.ts} +12 -12
- package/src/{dev-server.ts → plugin/dev-server.ts} +3 -3
- package/src/plugin/image-src-collector.ts +52 -0
- package/src/plugin/index.ts +3 -0
- package/src/plugin/packs/index.ts +2 -0
- package/src/{pack-provider.ts → plugin/packs/pack-provider.ts} +12 -12
- package/src/{packs.ts → plugin/packs/packs.ts} +22 -17
- package/src/render-engine-version.ts +2 -0
- package/src/tailwind/adapter-utils.ts +137 -0
- package/src/{class-utils.ts → tailwind/class-utils.ts} +33 -6
- package/src/tailwind/index.ts +8 -0
- package/src/tailwind/jsx-utils.ts +319 -0
- package/src/{node-ir.ts → tailwind/node-ir.ts} +208 -19
- package/src/{responsive-analyzer.ts → tailwind/responsive-analyzer.ts} +32 -2
- package/src/{state-analyzer.ts → tailwind/state-analyzer.ts} +71 -5
- package/src/{tailwind.ts → tailwind/tailwind.ts} +423 -674
- package/src/{utility-resolver.ts → tailwind/utility-resolver.ts} +27 -6
- package/src/{font-style-resolver.ts → text/font-style-resolver.ts} +0 -2
- package/src/text/index.ts +4 -0
- package/src/{inline-text.ts → text/inline-text.ts} +13 -13
- package/src/{text-builder.ts → text/text-builder.ts} +24 -7
- package/src/{text-line.ts → text/text-line.ts} +2 -2
- package/src/{change-detection.ts → tokens/change-detection.ts} +12 -12
- package/src/{color-resolver.ts → tokens/color-resolver.ts} +1 -6
- package/src/{colors.ts → tokens/colors.ts} +13 -6
- package/src/tokens/index.ts +6 -0
- package/src/{token-source.ts → tokens/token-source.ts} +4 -1
- package/src/{tokens.ts → tokens/tokens.ts} +116 -20
- package/src/{variables.ts → tokens/variables.ts} +447 -102
- package/templates/patch-tokens-route.ts +25 -6
- package/templates/scan-components-route.ts +26 -5
- package/ui.html +485 -37
- package/src/component-lookup.ts +0 -82
- package/src/design-system.ts +0 -59
- package/src/icon-builder.ts +0 -607
- package/src/layout-parser.ts +0 -667
- package/src/story-builder.ts +0 -1706
- package/src/ui-builder.ts +0 -1996
- /package/src/{image-cache.ts → cache/image-cache.ts} +0 -0
- /package/src/{blob-placement.ts → effects/blob-placement.ts} +0 -0
- /package/src/{transform-math.ts → tailwind/transform-math.ts} +0 -0
|
@@ -5,16 +5,17 @@ import { parseColor, colorToLabel, debug, normalizeThemeName, normalizeGroupName
|
|
|
5
5
|
// Module-level state
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
7
|
|
|
8
|
-
let _variableCollection:
|
|
9
|
-
let _themeCollections: Record<string,
|
|
8
|
+
let _variableCollection: VariableCollection | null = null; // Single-collection (paid) or default-theme collection (free)
|
|
9
|
+
let _themeCollections: Record<string, VariableCollection> = {}; // Free-plan: one collection per theme
|
|
10
10
|
let _variableModeIds: Record<string, string> = {}; // Multi-mode: theme -> modeId
|
|
11
11
|
let _defaultThemeName = 'primary';
|
|
12
|
-
let _colorVariables: Record<string,
|
|
13
|
-
let _themeColorVariables: Record<string, Record<string,
|
|
14
|
-
let _radiusVariables: Record<string,
|
|
15
|
-
let _fontVariables: Record<string,
|
|
16
|
-
let _spacingVariables: Record<string,
|
|
17
|
-
let _fontSizeVariables: Record<string,
|
|
12
|
+
let _colorVariables: Record<string, Variable> = {}; // default theme tokenKey -> Variable
|
|
13
|
+
let _themeColorVariables: Record<string, Record<string, Variable>> = {}; // theme -> tokenKey -> Variable
|
|
14
|
+
let _radiusVariables: Record<string, Variable> = {}; // key -> FLOAT Variable (radius/sm, radius/md …)
|
|
15
|
+
let _fontVariables: Record<string, Variable> = {}; // key -> STRING Variable (font/sans …)
|
|
16
|
+
let _spacingVariables: Record<string, Variable> = {}; // key -> FLOAT Variable (spacing/sm, spacing/lg …)
|
|
17
|
+
let _fontSizeVariables: Record<string, Variable> = {}; // key -> FLOAT Variable (fontSize/sm, fontSize/xl …)
|
|
18
|
+
let _breakpointVariables: Record<string, Variable> = {}; // key -> FLOAT Variable (breakpoint/sm, breakpoint/lg …)
|
|
18
19
|
let _useMultiMode = false; // true if paid plan (multi-mode available)
|
|
19
20
|
|
|
20
21
|
// ---------------------------------------------------------------------------
|
|
@@ -46,12 +47,13 @@ function getThemeDisplayName(theme: string): string {
|
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
function getThemeNamesFromTokens(): string[] {
|
|
49
|
-
const
|
|
50
|
+
const tokensRecord = TOKENS as unknown as Record<string, Record<string, unknown> | undefined>;
|
|
51
|
+
const names = Object.keys(tokensRecord || {}).filter((key) => {
|
|
50
52
|
if (key === 'core') return false;
|
|
51
|
-
const block =
|
|
53
|
+
const block = tokensRecord[key];
|
|
52
54
|
if (!block || typeof block !== 'object') return false;
|
|
53
55
|
return Boolean(
|
|
54
|
-
block.color || block.radius || block.font || block.spacing || block.fontSize || block.shadow
|
|
56
|
+
block.color || block.radius || block.font || block.spacing || block.fontSize || block.shadow || block.breakpoint
|
|
55
57
|
);
|
|
56
58
|
});
|
|
57
59
|
if (names.length === 0) return ['primary'];
|
|
@@ -59,22 +61,23 @@ function getThemeNamesFromTokens(): string[] {
|
|
|
59
61
|
return ['primary'].concat(names.filter((name) => name !== 'primary'));
|
|
60
62
|
}
|
|
61
63
|
|
|
62
|
-
function getThemeGroup(theme: string, group: string): Record<string,
|
|
63
|
-
const
|
|
64
|
-
|
|
64
|
+
function getThemeGroup(theme: string, group: string): Record<string, string> {
|
|
65
|
+
const tokensRecord = TOKENS as unknown as Record<string, Record<string, unknown> | undefined>;
|
|
66
|
+
const block = (tokensRecord[theme] && typeof tokensRecord[theme] === 'object')
|
|
67
|
+
? tokensRecord[theme]!
|
|
65
68
|
: {};
|
|
66
69
|
const value = block[group];
|
|
67
|
-
return value && typeof value === 'object' ? value : {};
|
|
70
|
+
return value && typeof value === 'object' ? (value as Record<string, string>) : {};
|
|
68
71
|
}
|
|
69
72
|
|
|
70
|
-
function findCollectionByName(collections:
|
|
73
|
+
function findCollectionByName(collections: VariableCollection[], name: string): VariableCollection | null {
|
|
71
74
|
const needle = String(name || '').toLowerCase();
|
|
72
|
-
return collections.find((c
|
|
75
|
+
return collections.find((c) => String(c.name || '').toLowerCase() === needle) || null;
|
|
73
76
|
}
|
|
74
77
|
|
|
75
|
-
export function resolveVariableValue(value:
|
|
78
|
+
export function resolveVariableValue(value: VariableValue | null | undefined, modeId?: string, visited?: Record<string, boolean>): VariableValue | null {
|
|
76
79
|
if (!value) return null;
|
|
77
|
-
if (value.type === 'VARIABLE_ALIAS' && value.id) {
|
|
80
|
+
if (typeof value === 'object' && 'type' in value && value.type === 'VARIABLE_ALIAS' && value.id) {
|
|
78
81
|
if (!visited) visited = {};
|
|
79
82
|
if (visited[value.id]) return null;
|
|
80
83
|
visited[value.id] = true;
|
|
@@ -88,7 +91,16 @@ export function resolveVariableValue(value: any, modeId?: string, visited?: Reco
|
|
|
88
91
|
return value;
|
|
89
92
|
}
|
|
90
93
|
|
|
91
|
-
|
|
94
|
+
type VariableTokenBlock = {
|
|
95
|
+
color?: Record<string, string>;
|
|
96
|
+
radius?: Record<string, string>;
|
|
97
|
+
font?: Record<string, string>;
|
|
98
|
+
spacing?: Record<string, string>;
|
|
99
|
+
fontSize?: Record<string, string>;
|
|
100
|
+
};
|
|
101
|
+
type VariableTokens = Record<string, VariableTokenBlock>;
|
|
102
|
+
|
|
103
|
+
export function readVariableTokens(): VariableTokens | null {
|
|
92
104
|
if (!figma.variables || !figma.variables.getLocalVariables) {
|
|
93
105
|
debug('Variables API unavailable, using embedded tokens.');
|
|
94
106
|
return null;
|
|
@@ -99,7 +111,7 @@ export function readVariableTokens(): any {
|
|
|
99
111
|
return null;
|
|
100
112
|
}
|
|
101
113
|
const collections = figma.variables.getLocalVariableCollections ? figma.variables.getLocalVariableCollections() : [];
|
|
102
|
-
const collectionById: Record<string,
|
|
114
|
+
const collectionById: Record<string, VariableCollection> = {};
|
|
103
115
|
const modeNameById: Record<string, string> = {};
|
|
104
116
|
for (const col of collections) {
|
|
105
117
|
collectionById[col.id] = col;
|
|
@@ -112,36 +124,36 @@ export function readVariableTokens(): any {
|
|
|
112
124
|
|
|
113
125
|
// Full token structure read back from Figma Variables.
|
|
114
126
|
// Keep this dynamic: theme keys can be arbitrary (not just primary/secondary).
|
|
115
|
-
const out:
|
|
127
|
+
const out: VariableTokens = {
|
|
116
128
|
core: { font: {} },
|
|
117
129
|
primary: { color: {}, radius: {}, font: {}, spacing: {}, fontSize: {} },
|
|
118
130
|
};
|
|
119
131
|
let wrote = false;
|
|
120
132
|
|
|
121
|
-
function ensureTheme(themeName: string):
|
|
133
|
+
function ensureTheme(themeName: string): Required<VariableTokenBlock> {
|
|
122
134
|
if (!out[themeName] || typeof out[themeName] !== 'object') {
|
|
123
135
|
out[themeName] = {};
|
|
124
136
|
}
|
|
125
|
-
const block = out[themeName]
|
|
137
|
+
const block = out[themeName];
|
|
126
138
|
if (!block.color || typeof block.color !== 'object') block.color = {};
|
|
127
139
|
if (!block.radius || typeof block.radius !== 'object') block.radius = {};
|
|
128
140
|
if (!block.font || typeof block.font !== 'object') block.font = {};
|
|
129
141
|
if (!block.spacing || typeof block.spacing !== 'object') block.spacing = {};
|
|
130
142
|
if (!block.fontSize || typeof block.fontSize !== 'object') block.fontSize = {};
|
|
131
|
-
return block
|
|
143
|
+
return block as Required<VariableTokenBlock>;
|
|
132
144
|
}
|
|
133
145
|
|
|
134
|
-
function assign(theme: string, group: string, parts: string[], value:
|
|
146
|
+
function assign(theme: string, group: string, parts: string[], value: VariableValue | null): void {
|
|
135
147
|
if (!value) return;
|
|
136
148
|
const key = parts && parts.length ? parts.join('-') : null;
|
|
137
149
|
if (!key) return;
|
|
138
150
|
|
|
139
151
|
const resolvedTheme = theme && theme !== 'core' ? theme : 'primary';
|
|
140
|
-
let target: Record<string,
|
|
152
|
+
let target: Record<string, string>;
|
|
141
153
|
|
|
142
154
|
if (group === 'color') {
|
|
143
155
|
target = ensureTheme(resolvedTheme).color;
|
|
144
|
-
target[key] = value;
|
|
156
|
+
target[key] = String(value);
|
|
145
157
|
wrote = true;
|
|
146
158
|
return;
|
|
147
159
|
}
|
|
@@ -153,7 +165,7 @@ export function readVariableTokens(): any {
|
|
|
153
165
|
}
|
|
154
166
|
if (group === 'font') {
|
|
155
167
|
// Route unscoped fonts into core, scoped fonts into their theme block.
|
|
156
|
-
if (!theme || theme === 'core') target = out.core.font
|
|
168
|
+
if (!theme || theme === 'core') target = out.core.font!;
|
|
157
169
|
else target = ensureTheme(theme).font;
|
|
158
170
|
target[key] = String(value);
|
|
159
171
|
wrote = true;
|
|
@@ -187,7 +199,7 @@ export function readVariableTokens(): any {
|
|
|
187
199
|
if (!group) {
|
|
188
200
|
// Backward compatibility: older variable sets used flat color names like
|
|
189
201
|
// `primary`, `foreground` (without `color/` prefix). Infer group by type.
|
|
190
|
-
if (String(
|
|
202
|
+
if (String(variable.resolvedType || '').toUpperCase() === 'COLOR') {
|
|
191
203
|
group = 'color';
|
|
192
204
|
if (cursor.length === 0) {
|
|
193
205
|
theme = null;
|
|
@@ -232,9 +244,9 @@ export function readVariableTokens(): any {
|
|
|
232
244
|
return wrote ? out : null;
|
|
233
245
|
}
|
|
234
246
|
|
|
235
|
-
export function upsertPaintStyle(name: string, color: { r: number; g: number; b: number; a?: number }):
|
|
247
|
+
export function upsertPaintStyle(name: string, color: { r: number; g: number; b: number; a?: number }): PaintStyle {
|
|
236
248
|
const styles = figma.getLocalPaintStyles();
|
|
237
|
-
const existing = styles.find((s
|
|
249
|
+
const existing = styles.find((s) => s.name === name);
|
|
238
250
|
const paint = { type: 'SOLID' as const, color: { r: color.r, g: color.g, b: color.b }, opacity: (color.a == null ? 1 : color.a) };
|
|
239
251
|
if (existing) {
|
|
240
252
|
existing.paints = [paint];
|
|
@@ -354,7 +366,7 @@ function upsertEffectStyle(name: string, cssValue: string): void {
|
|
|
354
366
|
const layers = parseCssBoxShadow(cssValue);
|
|
355
367
|
if (!layers.length) return;
|
|
356
368
|
|
|
357
|
-
const effects:
|
|
369
|
+
const effects: Effect[] = [];
|
|
358
370
|
for (const l of layers) {
|
|
359
371
|
effects.push({
|
|
360
372
|
type: l.inset ? 'INNER_SHADOW' : 'DROP_SHADOW',
|
|
@@ -367,7 +379,7 @@ function upsertEffectStyle(name: string, cssValue: string): void {
|
|
|
367
379
|
});
|
|
368
380
|
}
|
|
369
381
|
|
|
370
|
-
const existing = figma.getLocalEffectStyles().find((s
|
|
382
|
+
const existing = figma.getLocalEffectStyles().find((s) => s.name === name);
|
|
371
383
|
if (existing) {
|
|
372
384
|
existing.effects = effects;
|
|
373
385
|
} else {
|
|
@@ -403,10 +415,10 @@ export function removeStaleColorStyles(): void {
|
|
|
403
415
|
/**
|
|
404
416
|
* Populate variables in a collection for a given theme's color tokens.
|
|
405
417
|
*/
|
|
406
|
-
export function populateColorVariables(collection:
|
|
418
|
+
export function populateColorVariables(collection: VariableCollection, modeId: string, colorTokens: Record<string, string>, outMap: Record<string, Variable>): void {
|
|
407
419
|
// Index existing variables in this collection
|
|
408
420
|
const existingVars = figma.variables.getLocalVariables('COLOR');
|
|
409
|
-
const existingByName: Record<string,
|
|
421
|
+
const existingByName: Record<string, Variable> = {};
|
|
410
422
|
for (const v of existingVars) {
|
|
411
423
|
if (v.variableCollectionId === collection.id) {
|
|
412
424
|
existingByName[v.name] = v;
|
|
@@ -428,9 +440,9 @@ export function populateColorVariables(collection: any, modeId: string, colorTok
|
|
|
428
440
|
/**
|
|
429
441
|
* Populate FLOAT radius variables in a collection for a given mode.
|
|
430
442
|
*/
|
|
431
|
-
export function populateRadiusVariables(collection:
|
|
443
|
+
export function populateRadiusVariables(collection: VariableCollection, modeId: string, radiusTokens: Record<string, string>, outMap: Record<string, Variable>): void {
|
|
432
444
|
const existingVars = figma.variables.getLocalVariables('FLOAT');
|
|
433
|
-
const existingByName: Record<string,
|
|
445
|
+
const existingByName: Record<string, Variable> = {};
|
|
434
446
|
for (const v of existingVars) {
|
|
435
447
|
if (v.variableCollectionId === collection.id) {
|
|
436
448
|
existingByName[v.name] = v;
|
|
@@ -450,9 +462,9 @@ export function populateRadiusVariables(collection: any, modeId: string, radiusT
|
|
|
450
462
|
/**
|
|
451
463
|
* Populate STRING font-family variables in a collection for a given mode.
|
|
452
464
|
*/
|
|
453
|
-
export function populateFontVariables(collection:
|
|
465
|
+
export function populateFontVariables(collection: VariableCollection, modeId: string, fontTokens: Record<string, string>, outMap: Record<string, Variable>): void {
|
|
454
466
|
const existingVars = figma.variables.getLocalVariables('STRING');
|
|
455
|
-
const existingByName: Record<string,
|
|
467
|
+
const existingByName: Record<string, Variable> = {};
|
|
456
468
|
for (const v of existingVars) {
|
|
457
469
|
if (v.variableCollectionId === collection.id) {
|
|
458
470
|
existingByName[v.name] = v;
|
|
@@ -474,9 +486,9 @@ export function populateFontVariables(collection: any, modeId: string, fontToken
|
|
|
474
486
|
/**
|
|
475
487
|
* Populate FLOAT spacing variables in a collection for a given mode.
|
|
476
488
|
*/
|
|
477
|
-
export function populateSpacingVariables(collection:
|
|
489
|
+
export function populateSpacingVariables(collection: VariableCollection, modeId: string, spacingTokens: Record<string, string>, outMap: Record<string, Variable>): void {
|
|
478
490
|
const existingVars = figma.variables.getLocalVariables('FLOAT');
|
|
479
|
-
const existingByName: Record<string,
|
|
491
|
+
const existingByName: Record<string, Variable> = {};
|
|
480
492
|
for (const v of existingVars) {
|
|
481
493
|
if (v.variableCollectionId === collection.id) existingByName[v.name] = v;
|
|
482
494
|
}
|
|
@@ -494,9 +506,9 @@ export function populateSpacingVariables(collection: any, modeId: string, spacin
|
|
|
494
506
|
/**
|
|
495
507
|
* Populate FLOAT font-size variables in a collection for a given mode.
|
|
496
508
|
*/
|
|
497
|
-
export function populateFontSizeVariables(collection:
|
|
509
|
+
export function populateFontSizeVariables(collection: VariableCollection, modeId: string, fontSizeTokens: Record<string, string>, outMap: Record<string, Variable>): void {
|
|
498
510
|
const existingVars = figma.variables.getLocalVariables('FLOAT');
|
|
499
|
-
const existingByName: Record<string,
|
|
511
|
+
const existingByName: Record<string, Variable> = {};
|
|
500
512
|
for (const v of existingVars) {
|
|
501
513
|
if (v.variableCollectionId === collection.id) existingByName[v.name] = v;
|
|
502
514
|
}
|
|
@@ -511,6 +523,26 @@ export function populateFontSizeVariables(collection: any, modeId: string, fontS
|
|
|
511
523
|
}
|
|
512
524
|
}
|
|
513
525
|
|
|
526
|
+
/**
|
|
527
|
+
* Populate FLOAT breakpoint variables in a collection for a given mode.
|
|
528
|
+
*/
|
|
529
|
+
export function populateBreakpointVariables(collection: VariableCollection, modeId: string, breakpointTokens: Record<string, string>, outMap: Record<string, Variable>): void {
|
|
530
|
+
const existingVars = figma.variables.getLocalVariables('FLOAT');
|
|
531
|
+
const existingByName: Record<string, Variable> = {};
|
|
532
|
+
for (const v of existingVars) {
|
|
533
|
+
if (v.variableCollectionId === collection.id) existingByName[v.name] = v;
|
|
534
|
+
}
|
|
535
|
+
for (const key in breakpointTokens) {
|
|
536
|
+
const varName = 'breakpoint/' + key;
|
|
537
|
+
let variable = existingByName[varName];
|
|
538
|
+
if (!variable) {
|
|
539
|
+
variable = figma.variables.createVariable(varName, collection, 'FLOAT');
|
|
540
|
+
}
|
|
541
|
+
variable.setValueForMode(modeId, pxFromSizeToken(breakpointTokens[key]));
|
|
542
|
+
outMap[key] = variable;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
514
546
|
/**
|
|
515
547
|
* Create or update color variables.
|
|
516
548
|
* Tries multi-mode first (paid plans). Falls back to two collections (free plan).
|
|
@@ -527,17 +559,19 @@ export function createOrUpdateVariables(): void {
|
|
|
527
559
|
const defaultTheme = _defaultThemeName;
|
|
528
560
|
const defaultDisplayName = getThemeDisplayName(defaultTheme);
|
|
529
561
|
|
|
530
|
-
const themeColors: Record<string, Record<string,
|
|
531
|
-
const themeRadius: Record<string, Record<string,
|
|
532
|
-
const themeFont: Record<string, Record<string,
|
|
533
|
-
const themeSpacing: Record<string, Record<string,
|
|
534
|
-
const themeFontSize: Record<string, Record<string,
|
|
562
|
+
const themeColors: Record<string, Record<string, string>> = {};
|
|
563
|
+
const themeRadius: Record<string, Record<string, string>> = {};
|
|
564
|
+
const themeFont: Record<string, Record<string, string>> = {};
|
|
565
|
+
const themeSpacing: Record<string, Record<string, string>> = {};
|
|
566
|
+
const themeFontSize: Record<string, Record<string, string>> = {};
|
|
567
|
+
const themeBreakpoint: Record<string, Record<string, string>> = {};
|
|
535
568
|
for (const theme of themeNames) {
|
|
536
569
|
themeColors[theme] = getThemeGroup(theme, 'color');
|
|
537
570
|
themeRadius[theme] = getThemeGroup(theme, 'radius');
|
|
538
571
|
themeFont[theme] = getThemeGroup(theme, 'font');
|
|
539
572
|
themeSpacing[theme] = getThemeGroup(theme, 'spacing');
|
|
540
573
|
themeFontSize[theme] = getThemeGroup(theme, 'fontSize');
|
|
574
|
+
themeBreakpoint[theme] = getThemeGroup(theme, 'breakpoint');
|
|
541
575
|
}
|
|
542
576
|
|
|
543
577
|
_themeCollections = {};
|
|
@@ -548,13 +582,14 @@ export function createOrUpdateVariables(): void {
|
|
|
548
582
|
_fontVariables = {};
|
|
549
583
|
_spacingVariables = {};
|
|
550
584
|
_fontSizeVariables = {};
|
|
585
|
+
_breakpointVariables = {};
|
|
551
586
|
|
|
552
587
|
const collections = figma.variables.getLocalVariableCollections();
|
|
553
588
|
|
|
554
|
-
function ensureThemeModes(collection:
|
|
589
|
+
function ensureThemeModes(collection: VariableCollection): boolean {
|
|
555
590
|
const map: Record<string, string> = {};
|
|
556
591
|
const modes = Array.isArray(collection.modes) ? collection.modes.slice() : [];
|
|
557
|
-
const unclaimed: string[] = modes.map((mode
|
|
592
|
+
const unclaimed: string[] = modes.map((mode) => mode.modeId);
|
|
558
593
|
const removeUnclaimed = (modeId: string): void => {
|
|
559
594
|
const idx = unclaimed.indexOf(modeId);
|
|
560
595
|
if (idx >= 0) unclaimed.splice(idx, 1);
|
|
@@ -580,8 +615,8 @@ export function createOrUpdateVariables(): void {
|
|
|
580
615
|
}
|
|
581
616
|
try {
|
|
582
617
|
map[theme] = collection.addMode(getThemeDisplayName(theme));
|
|
583
|
-
} catch (e
|
|
584
|
-
debug('Cannot add mode (free plan): ' + (e.message
|
|
618
|
+
} catch (e) {
|
|
619
|
+
debug('Cannot add mode (free plan): ' + (e instanceof Error ? e.message : String(e)));
|
|
585
620
|
return false;
|
|
586
621
|
}
|
|
587
622
|
}
|
|
@@ -625,16 +660,18 @@ export function createOrUpdateVariables(): void {
|
|
|
625
660
|
const allFontKeys: Record<string, boolean> = {};
|
|
626
661
|
const allSpacingKeys: Record<string, boolean> = {};
|
|
627
662
|
const allFontSizeKeys: Record<string, boolean> = {};
|
|
663
|
+
const allBreakpointKeys: Record<string, boolean> = {};
|
|
628
664
|
for (const theme of themeNames) {
|
|
629
665
|
for (const key in themeColors[theme]) allColorKeys[key] = true;
|
|
630
666
|
for (const key in themeRadius[theme]) allRadiusKeys[key] = true;
|
|
631
667
|
for (const key in themeFont[theme]) allFontKeys[key] = true;
|
|
632
668
|
for (const key in themeSpacing[theme]) allSpacingKeys[key] = true;
|
|
633
669
|
for (const key in themeFontSize[theme]) allFontSizeKeys[key] = true;
|
|
670
|
+
for (const key in themeBreakpoint[theme]) allBreakpointKeys[key] = true;
|
|
634
671
|
}
|
|
635
672
|
|
|
636
673
|
const existingColorVars = figma.variables.getLocalVariables('COLOR');
|
|
637
|
-
const existingColorByName: Record<string,
|
|
674
|
+
const existingColorByName: Record<string, Variable> = {};
|
|
638
675
|
for (const v of existingColorVars) {
|
|
639
676
|
if (v.variableCollectionId === _variableCollection.id) {
|
|
640
677
|
existingColorByName[v.name] = v;
|
|
@@ -642,7 +679,7 @@ export function createOrUpdateVariables(): void {
|
|
|
642
679
|
}
|
|
643
680
|
|
|
644
681
|
const existingFloatVars = figma.variables.getLocalVariables('FLOAT');
|
|
645
|
-
const existingFloatByName: Record<string,
|
|
682
|
+
const existingFloatByName: Record<string, Variable> = {};
|
|
646
683
|
for (const v of existingFloatVars) {
|
|
647
684
|
if (v.variableCollectionId === _variableCollection.id) {
|
|
648
685
|
existingFloatByName[v.name] = v;
|
|
@@ -650,7 +687,7 @@ export function createOrUpdateVariables(): void {
|
|
|
650
687
|
}
|
|
651
688
|
|
|
652
689
|
const existingStringVars = figma.variables.getLocalVariables('STRING');
|
|
653
|
-
const existingStringByName: Record<string,
|
|
690
|
+
const existingStringByName: Record<string, Variable> = {};
|
|
654
691
|
for (const v of existingStringVars) {
|
|
655
692
|
if (v.variableCollectionId === _variableCollection.id) {
|
|
656
693
|
existingStringByName[v.name] = v;
|
|
@@ -752,6 +789,24 @@ export function createOrUpdateVariables(): void {
|
|
|
752
789
|
_fontSizeVariables[key] = variable;
|
|
753
790
|
}
|
|
754
791
|
|
|
792
|
+
const defaultBreakpoint = themeBreakpoint[defaultTheme] || {};
|
|
793
|
+
for (const key in allBreakpointKeys) {
|
|
794
|
+
const varName = 'breakpoint/' + key;
|
|
795
|
+
let variable = existingFloatByName[varName];
|
|
796
|
+
if (!variable) variable = figma.variables.createVariable(varName, _variableCollection, 'FLOAT');
|
|
797
|
+
|
|
798
|
+
for (const theme of themeNames) {
|
|
799
|
+
const modeId = _variableModeIds[theme];
|
|
800
|
+
if (!modeId) continue;
|
|
801
|
+
const value = (themeBreakpoint[theme] && themeBreakpoint[theme][key] !== undefined)
|
|
802
|
+
? themeBreakpoint[theme][key]
|
|
803
|
+
: defaultBreakpoint[key];
|
|
804
|
+
if (value === undefined) continue;
|
|
805
|
+
variable.setValueForMode(modeId, pxFromSizeToken(value));
|
|
806
|
+
}
|
|
807
|
+
_breakpointVariables[key] = variable;
|
|
808
|
+
}
|
|
809
|
+
|
|
755
810
|
for (const col of collections) {
|
|
756
811
|
if (!col || col.id === _variableCollection.id) continue;
|
|
757
812
|
const mapped = normalizeThemeName(col.name) || normalizeCustomThemeName(col.name);
|
|
@@ -798,6 +853,7 @@ export function createOrUpdateVariables(): void {
|
|
|
798
853
|
populateFontVariables(collection, modeId, themeFont[theme] || {}, _fontVariables);
|
|
799
854
|
populateSpacingVariables(collection, modeId, themeSpacing[theme] || {}, _spacingVariables);
|
|
800
855
|
populateFontSizeVariables(collection, modeId, themeFontSize[theme] || {}, _fontSizeVariables);
|
|
856
|
+
populateBreakpointVariables(collection, modeId, themeBreakpoint[theme] || {}, _breakpointVariables);
|
|
801
857
|
}
|
|
802
858
|
}
|
|
803
859
|
|
|
@@ -808,13 +864,14 @@ export function createOrUpdateVariables(): void {
|
|
|
808
864
|
if (!mapped) continue;
|
|
809
865
|
if (themeNames.includes(mapped)) continue;
|
|
810
866
|
if (collection.id === (_variableCollection && _variableCollection.id)) continue;
|
|
811
|
-
const varsInCollection = figma.variables.getLocalVariables().filter((v
|
|
812
|
-
const looksLikePluginThemeCollection = varsInCollection.some((v
|
|
867
|
+
const varsInCollection = figma.variables.getLocalVariables().filter((v) => v.variableCollectionId === collection.id);
|
|
868
|
+
const looksLikePluginThemeCollection = varsInCollection.some((v) =>
|
|
813
869
|
/^color\//.test(String(v.name || '')) ||
|
|
814
870
|
/^radius\//.test(String(v.name || '')) ||
|
|
815
871
|
/^font\//.test(String(v.name || '')) ||
|
|
816
872
|
/^spacing\//.test(String(v.name || '')) ||
|
|
817
|
-
/^fontSize\//.test(String(v.name || ''))
|
|
873
|
+
/^fontSize\//.test(String(v.name || '')) ||
|
|
874
|
+
/^breakpoint\//.test(String(v.name || ''))
|
|
818
875
|
);
|
|
819
876
|
if (!looksLikePluginThemeCollection) continue;
|
|
820
877
|
try { collection.remove(); } catch (_e) {}
|
|
@@ -834,7 +891,7 @@ export function createOrUpdateVariables(): void {
|
|
|
834
891
|
export function createOrUpdateStylesFallback(): void {
|
|
835
892
|
const themes = getThemeNamesFromTokens();
|
|
836
893
|
for (const theme of themes) {
|
|
837
|
-
const col: Record<string,
|
|
894
|
+
const col: Record<string, string> = getThemeGroup(theme, 'color');
|
|
838
895
|
for (const [key, val] of Object.entries(col)) {
|
|
839
896
|
const rgb = parseColor(val);
|
|
840
897
|
upsertPaintStyle(`${theme.toUpperCase()}/Color/${key}`, rgb);
|
|
@@ -845,7 +902,7 @@ export function createOrUpdateStylesFallback(): void {
|
|
|
845
902
|
/**
|
|
846
903
|
* Bind a frame's fill to a color variable (semantic token).
|
|
847
904
|
*/
|
|
848
|
-
export function bindColorVariable(node:
|
|
905
|
+
export function bindColorVariable(node: SceneNode, tokenKey: string, fillOrStroke: string, theme?: string): boolean {
|
|
849
906
|
const resolvedTheme =
|
|
850
907
|
(theme && (_themeColorVariables[theme] || _variableModeIds[theme])) ? theme :
|
|
851
908
|
_defaultThemeName;
|
|
@@ -857,24 +914,24 @@ export function bindColorVariable(node: any, tokenKey: string, fillOrStroke: str
|
|
|
857
914
|
if (!variable) return false;
|
|
858
915
|
|
|
859
916
|
try {
|
|
860
|
-
if (fillOrStroke === 'fill') {
|
|
861
|
-
const fills = JSON.parse(JSON.stringify(node.fills || []));
|
|
917
|
+
if (fillOrStroke === 'fill' && 'fills' in node) {
|
|
918
|
+
const fills: Paint[] = JSON.parse(JSON.stringify(node.fills || []));
|
|
862
919
|
if (fills.length === 0) {
|
|
863
920
|
fills.push({ type: 'SOLID', color: { r: 1, g: 1, b: 1 }, opacity: 1 });
|
|
864
921
|
}
|
|
865
|
-
fills[0] = figma.variables.setBoundVariableForPaint(fills[0], 'color', variable);
|
|
922
|
+
fills[0] = figma.variables.setBoundVariableForPaint(fills[0] as SolidPaint, 'color', variable);
|
|
866
923
|
node.fills = fills;
|
|
867
|
-
} else if (fillOrStroke === 'stroke') {
|
|
868
|
-
const strokes = JSON.parse(JSON.stringify(node.strokes || []));
|
|
924
|
+
} else if (fillOrStroke === 'stroke' && 'strokes' in node) {
|
|
925
|
+
const strokes: Paint[] = JSON.parse(JSON.stringify(node.strokes || []));
|
|
869
926
|
if (strokes.length === 0) {
|
|
870
927
|
strokes.push({ type: 'SOLID', color: { r: 0.9, g: 0.9, b: 0.9 } });
|
|
871
928
|
}
|
|
872
|
-
strokes[0] = figma.variables.setBoundVariableForPaint(strokes[0], 'color', variable);
|
|
929
|
+
strokes[0] = figma.variables.setBoundVariableForPaint(strokes[0] as SolidPaint, 'color', variable);
|
|
873
930
|
node.strokes = strokes;
|
|
874
931
|
}
|
|
875
932
|
return true;
|
|
876
|
-
} catch (e
|
|
877
|
-
debug('Failed to bind variable ' + tokenKey + ': ' + (e.message
|
|
933
|
+
} catch (e) {
|
|
934
|
+
debug('Failed to bind variable ' + tokenKey + ': ' + (e instanceof Error ? e.message : String(e)));
|
|
878
935
|
return false;
|
|
879
936
|
}
|
|
880
937
|
}
|
|
@@ -883,7 +940,7 @@ export function bindColorVariable(node: any, tokenKey: string, fillOrStroke: str
|
|
|
883
940
|
* Set the theme mode on a frame (only works on paid plans with multi-mode).
|
|
884
941
|
* On free plan this is a no-op (themes are separate collections instead).
|
|
885
942
|
*/
|
|
886
|
-
export function setThemeMode(frame:
|
|
943
|
+
export function setThemeMode(frame: SceneNode, theme: string): void {
|
|
887
944
|
if (!_useMultiMode || !_variableCollection) return;
|
|
888
945
|
const normalized = normalizeThemeName(theme) || normalizeCustomThemeName(theme);
|
|
889
946
|
const resolvedTheme = (theme && _variableModeIds[theme]) ? theme :
|
|
@@ -892,8 +949,8 @@ export function setThemeMode(frame: any, theme: string): void {
|
|
|
892
949
|
if (!modeId) return;
|
|
893
950
|
try {
|
|
894
951
|
frame.setExplicitVariableModeForCollection(_variableCollection, modeId);
|
|
895
|
-
} catch (e
|
|
896
|
-
debug('Failed to set theme mode: ' + (e.message
|
|
952
|
+
} catch (e) {
|
|
953
|
+
debug('Failed to set theme mode: ' + (e instanceof Error ? e.message : String(e)));
|
|
897
954
|
}
|
|
898
955
|
}
|
|
899
956
|
|
|
@@ -940,17 +997,18 @@ export function pxFromSizeToken(v: unknown): number {
|
|
|
940
997
|
* Bind a frame's cornerRadius corners to a radius variable.
|
|
941
998
|
* radiusKey is a token key like 'md', 'lg', 'base', etc.
|
|
942
999
|
*/
|
|
943
|
-
export function bindRadiusVariable(node:
|
|
1000
|
+
export function bindRadiusVariable(node: SceneNode, radiusKey: string, _theme?: string): boolean {
|
|
944
1001
|
const variable = _radiusVariables[radiusKey];
|
|
945
1002
|
if (!variable) return false;
|
|
1003
|
+
if (!('setBoundVariable' in node)) return false;
|
|
946
1004
|
try {
|
|
947
1005
|
node.setBoundVariable('topLeftRadius', variable);
|
|
948
1006
|
node.setBoundVariable('topRightRadius', variable);
|
|
949
1007
|
node.setBoundVariable('bottomLeftRadius', variable);
|
|
950
1008
|
node.setBoundVariable('bottomRightRadius', variable);
|
|
951
1009
|
return true;
|
|
952
|
-
} catch (e
|
|
953
|
-
debug('Failed to bind radius variable ' + radiusKey + ': ' + (e.message
|
|
1010
|
+
} catch (e) {
|
|
1011
|
+
debug('Failed to bind radius variable ' + radiusKey + ': ' + (e instanceof Error ? e.message : String(e)));
|
|
954
1012
|
return false;
|
|
955
1013
|
}
|
|
956
1014
|
}
|
|
@@ -967,8 +1025,8 @@ export function createOrUpdateStyles(): void {
|
|
|
967
1025
|
}
|
|
968
1026
|
}
|
|
969
1027
|
|
|
970
|
-
export function ensureTokensPage():
|
|
971
|
-
let page = figma.root.children.find((p
|
|
1028
|
+
export function ensureTokensPage(): PageNode {
|
|
1029
|
+
let page = figma.root.children.find((p) => p.name === 'Design Tokens');
|
|
972
1030
|
if (!page) {
|
|
973
1031
|
page = figma.createPage();
|
|
974
1032
|
page.name = 'Design Tokens';
|
|
@@ -977,9 +1035,96 @@ export function ensureTokensPage(): any {
|
|
|
977
1035
|
return page;
|
|
978
1036
|
}
|
|
979
1037
|
|
|
980
|
-
|
|
1038
|
+
function makeSectionTitle(text: string): TextNode {
|
|
1039
|
+
const node = figma.createText();
|
|
1040
|
+
try {
|
|
1041
|
+
node.fontName = { family: 'Inter', style: 'Bold' };
|
|
1042
|
+
} catch (_e) {
|
|
1043
|
+
// Bold not loaded; fall back to whatever style createText assigns.
|
|
1044
|
+
}
|
|
1045
|
+
node.characters = text;
|
|
1046
|
+
node.fontSize = 14;
|
|
1047
|
+
return node;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function makeCategoryHeading(text: string): TextNode {
|
|
1051
|
+
const node = figma.createText();
|
|
1052
|
+
try {
|
|
1053
|
+
node.fontName = { family: 'Inter', style: 'Bold' };
|
|
1054
|
+
} catch (_e) {
|
|
1055
|
+
// Bold not loaded; fall back to whatever style createText assigns.
|
|
1056
|
+
}
|
|
1057
|
+
node.characters = text;
|
|
1058
|
+
node.fontSize = 18;
|
|
1059
|
+
return node;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// Equal-width column widths so the side-by-side theme frames don't drift in
|
|
1063
|
+
// width based on content (mirrors the per-story column treatment used in the
|
|
1064
|
+
// component grid).
|
|
1065
|
+
const TOKENS_THEME_COLUMN_WIDTH = 460;
|
|
1066
|
+
const TOKENS_THEME_COLUMN_GAP = 24;
|
|
1067
|
+
|
|
1068
|
+
export function buildTokensCategorySection(label: string, frames: Array<FrameNode | null>): FrameNode | null {
|
|
1069
|
+
const visible: FrameNode[] = frames.filter((f): f is FrameNode => !!f);
|
|
1070
|
+
if (visible.length === 0) return null;
|
|
1071
|
+
|
|
1072
|
+
const section = figma.createFrame();
|
|
1073
|
+
section.name = label;
|
|
1074
|
+
section.layoutMode = 'VERTICAL';
|
|
1075
|
+
section.primaryAxisSizingMode = 'AUTO';
|
|
1076
|
+
section.counterAxisSizingMode = 'AUTO';
|
|
1077
|
+
section.itemSpacing = 12;
|
|
1078
|
+
section.fills = [];
|
|
1079
|
+
section.strokes = [];
|
|
1080
|
+
section.appendChild(makeCategoryHeading(label));
|
|
1081
|
+
|
|
1082
|
+
if (visible.length === 1) {
|
|
1083
|
+
section.appendChild(visible[0]);
|
|
1084
|
+
return section;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
const row = figma.createFrame();
|
|
1088
|
+
row.name = label + ' / themes';
|
|
1089
|
+
row.layoutMode = 'HORIZONTAL';
|
|
1090
|
+
row.primaryAxisSizingMode = 'AUTO';
|
|
1091
|
+
row.counterAxisSizingMode = 'AUTO';
|
|
1092
|
+
row.itemSpacing = TOKENS_THEME_COLUMN_GAP;
|
|
1093
|
+
row.counterAxisAlignItems = 'MIN';
|
|
1094
|
+
row.fills = [];
|
|
1095
|
+
row.strokes = [];
|
|
1096
|
+
|
|
1097
|
+
for (const frame of visible) {
|
|
1098
|
+
row.appendChild(frame);
|
|
1099
|
+
// Lock each theme demo's width so Primary and Secondary columns stay
|
|
1100
|
+
// identical regardless of label or swatch content. counterAxisSizingMode
|
|
1101
|
+
// is the horizontal sizing for a vertical autolayout frame.
|
|
1102
|
+
if ('counterAxisSizingMode' in frame) {
|
|
1103
|
+
frame.counterAxisSizingMode = 'FIXED';
|
|
1104
|
+
}
|
|
1105
|
+
frame.resize(TOKENS_THEME_COLUMN_WIDTH, Math.max(frame.height || 1, 1));
|
|
1106
|
+
}
|
|
1107
|
+
section.appendChild(row);
|
|
1108
|
+
return section;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
function makeBodyText(text: string, options?: { family?: string; style?: string; size?: number }): TextNode {
|
|
1112
|
+
const node = figma.createText();
|
|
1113
|
+
const family = (options && options.family) || 'Inter';
|
|
1114
|
+
const style = (options && options.style) || 'Regular';
|
|
1115
|
+
try {
|
|
1116
|
+
node.fontName = { family, style };
|
|
1117
|
+
} catch (_e) {
|
|
1118
|
+
// Requested font not loaded; fall back to default font.
|
|
1119
|
+
}
|
|
1120
|
+
node.characters = text;
|
|
1121
|
+
if (options && options.size) node.fontSize = options.size;
|
|
1122
|
+
return node;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function makeSectionFrame(name: string): FrameNode {
|
|
981
1126
|
const frame = figma.createFrame();
|
|
982
|
-
frame.name =
|
|
1127
|
+
frame.name = name;
|
|
983
1128
|
frame.layoutMode = 'VERTICAL';
|
|
984
1129
|
frame.primaryAxisSizingMode = 'AUTO';
|
|
985
1130
|
frame.counterAxisSizingMode = 'AUTO';
|
|
@@ -987,8 +1132,13 @@ export function demoFrameColors(theme: string): any {
|
|
|
987
1132
|
frame.paddingLeft = frame.paddingRight = frame.paddingTop = frame.paddingBottom = 16;
|
|
988
1133
|
frame.fills = [];
|
|
989
1134
|
frame.strokes = [{ type: 'SOLID', color: { r: 0.9, g: 0.9, b: 0.9 } }];
|
|
990
|
-
|
|
991
|
-
|
|
1135
|
+
return frame;
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
export function demoFrameColors(theme: string): FrameNode {
|
|
1139
|
+
const frame = makeSectionFrame(theme.toUpperCase() + ' Colors');
|
|
1140
|
+
frame.appendChild(makeSectionTitle(getThemeDisplayName(theme)));
|
|
1141
|
+
const col: Record<string, string> = getThemeGroup(theme, 'color');
|
|
992
1142
|
for (const [key, val] of Object.entries(col)) {
|
|
993
1143
|
const row = figma.createFrame();
|
|
994
1144
|
row.layoutMode = 'HORIZONTAL';
|
|
@@ -1001,42 +1151,237 @@ export function demoFrameColors(theme: string): any {
|
|
|
1001
1151
|
swatch.resize(48, 32);
|
|
1002
1152
|
const rgb = parseColor(val);
|
|
1003
1153
|
swatch.fills = [{ type: 'SOLID', color: { r: rgb.r, g: rgb.g, b: rgb.b }, opacity: (rgb.a == null ? 1 : rgb.a) }];
|
|
1004
|
-
// Bind to Figma Variable so swatch updates when variable changes
|
|
1005
1154
|
bindColorVariable(swatch, key, 'fill', theme);
|
|
1006
1155
|
swatch.strokes = [];
|
|
1007
1156
|
|
|
1008
|
-
const label =
|
|
1009
|
-
label.characters = key + ' \u2014 ' + colorToLabel(val);
|
|
1010
|
-
// Load default font for text placement
|
|
1011
|
-
// Note: plugin will await font load asynchronously below
|
|
1157
|
+
const label = makeBodyText(key + ' \u2014 ' + colorToLabel(val));
|
|
1012
1158
|
row.appendChild(swatch);
|
|
1013
1159
|
row.appendChild(label);
|
|
1014
1160
|
frame.appendChild(row);
|
|
1015
|
-
idx++;
|
|
1016
1161
|
}
|
|
1017
1162
|
return frame;
|
|
1018
1163
|
}
|
|
1019
1164
|
|
|
1020
|
-
export function demoFrameRadii():
|
|
1021
|
-
const frame =
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
const
|
|
1032
|
-
for (const k of keys) {
|
|
1165
|
+
export function demoFrameRadii(): FrameNode {
|
|
1166
|
+
const frame = makeSectionFrame('Radii');
|
|
1167
|
+
|
|
1168
|
+
const row = figma.createFrame();
|
|
1169
|
+
row.layoutMode = 'HORIZONTAL';
|
|
1170
|
+
row.primaryAxisSizingMode = 'AUTO';
|
|
1171
|
+
row.counterAxisSizingMode = 'AUTO';
|
|
1172
|
+
row.itemSpacing = 16;
|
|
1173
|
+
row.fills = [];
|
|
1174
|
+
row.strokes = [];
|
|
1175
|
+
|
|
1176
|
+
const r: Record<string, string> = getThemeGroup(_defaultThemeName, 'radius');
|
|
1177
|
+
for (const k of Object.keys(r)) {
|
|
1178
|
+
const cell = figma.createFrame();
|
|
1179
|
+
cell.layoutMode = 'VERTICAL';
|
|
1180
|
+
cell.primaryAxisSizingMode = 'AUTO';
|
|
1181
|
+
cell.counterAxisSizingMode = 'AUTO';
|
|
1182
|
+
cell.itemSpacing = 6;
|
|
1183
|
+
cell.fills = [];
|
|
1184
|
+
cell.strokes = [];
|
|
1185
|
+
cell.name = 'radius/' + k;
|
|
1186
|
+
|
|
1033
1187
|
const rect = figma.createRectangle();
|
|
1034
1188
|
rect.resize(96, 64);
|
|
1035
1189
|
rect.cornerRadius = pxFromSizeToken(r[k]);
|
|
1036
1190
|
rect.fills = [{ type: 'SOLID', color: { r: 0.9, g: 0.9, b: 0.9 } }];
|
|
1037
1191
|
rect.strokes = [{ type: 'SOLID', color: { r: 0.75, g: 0.75, b: 0.75 } }];
|
|
1038
|
-
rect.name = '
|
|
1039
|
-
|
|
1192
|
+
rect.name = 'swatch';
|
|
1193
|
+
|
|
1194
|
+
cell.appendChild(rect);
|
|
1195
|
+
cell.appendChild(makeBodyText(k, { size: 12 }));
|
|
1196
|
+
row.appendChild(cell);
|
|
1197
|
+
}
|
|
1198
|
+
frame.appendChild(row);
|
|
1199
|
+
return frame;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
export function demoFrameFonts(theme: string): FrameNode | null {
|
|
1203
|
+
const fonts: Record<string, string> = getThemeGroup(theme, 'font');
|
|
1204
|
+
const keys = Object.keys(fonts);
|
|
1205
|
+
if (keys.length === 0) return null;
|
|
1206
|
+
|
|
1207
|
+
const frame = makeSectionFrame(theme.toUpperCase() + ' Fonts');
|
|
1208
|
+
frame.appendChild(makeSectionTitle(getThemeDisplayName(theme)));
|
|
1209
|
+
|
|
1210
|
+
for (const key of keys) {
|
|
1211
|
+
const family = (function () {
|
|
1212
|
+
const raw = String(fonts[key] || '');
|
|
1213
|
+
const parts = raw.split(',');
|
|
1214
|
+
for (const part of parts) {
|
|
1215
|
+
const trimmed = part.trim();
|
|
1216
|
+
const varMatch = trimmed.match(/^var\(--font-([a-z0-9-]+)\)/i);
|
|
1217
|
+
if (varMatch) {
|
|
1218
|
+
return varMatch[1].split('-').map((w: string) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
1219
|
+
}
|
|
1220
|
+
const lower = trimmed.toLowerCase().replace(/^["']|["']$/g, '');
|
|
1221
|
+
if (lower === 'ui-sans-serif' || lower === 'system-ui' || lower === 'sans-serif' || lower === 'serif' || lower === 'monospace' || lower === 'ui-monospace') continue;
|
|
1222
|
+
const cleaned = trimmed.replace(/^["']|["']$/g, '');
|
|
1223
|
+
if (cleaned) return cleaned;
|
|
1224
|
+
}
|
|
1225
|
+
return 'Inter';
|
|
1226
|
+
})();
|
|
1227
|
+
|
|
1228
|
+
const cell = figma.createFrame();
|
|
1229
|
+
cell.layoutMode = 'VERTICAL';
|
|
1230
|
+
cell.primaryAxisSizingMode = 'AUTO';
|
|
1231
|
+
cell.counterAxisSizingMode = 'AUTO';
|
|
1232
|
+
cell.itemSpacing = 4;
|
|
1233
|
+
cell.fills = [];
|
|
1234
|
+
cell.strokes = [];
|
|
1235
|
+
cell.name = 'font/' + key;
|
|
1236
|
+
|
|
1237
|
+
cell.appendChild(makeBodyText(key + ' \u2014 ' + family, { size: 12 }));
|
|
1238
|
+
cell.appendChild(makeBodyText('The quick brown fox jumps over the lazy dog', { family, size: 18 }));
|
|
1239
|
+
frame.appendChild(cell);
|
|
1240
|
+
}
|
|
1241
|
+
return frame;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
export function demoFrameSpacing(): FrameNode | null {
|
|
1245
|
+
const spacing: Record<string, string> = getThemeGroup(_defaultThemeName, 'spacing');
|
|
1246
|
+
const keys = Object.keys(spacing);
|
|
1247
|
+
if (keys.length === 0) return null;
|
|
1248
|
+
|
|
1249
|
+
const frame = makeSectionFrame('Spacing');
|
|
1250
|
+
|
|
1251
|
+
for (const k of keys) {
|
|
1252
|
+
const px = pxFromSizeToken(spacing[k]);
|
|
1253
|
+
const row = figma.createFrame();
|
|
1254
|
+
row.layoutMode = 'HORIZONTAL';
|
|
1255
|
+
row.primaryAxisSizingMode = 'AUTO';
|
|
1256
|
+
row.counterAxisSizingMode = 'AUTO';
|
|
1257
|
+
row.itemSpacing = 12;
|
|
1258
|
+
row.counterAxisAlignItems = 'CENTER';
|
|
1259
|
+
row.fills = [];
|
|
1260
|
+
row.strokes = [];
|
|
1261
|
+
row.name = 'spacing/' + k;
|
|
1262
|
+
|
|
1263
|
+
const bar = figma.createRectangle();
|
|
1264
|
+
bar.resize(Math.max(1, px), 16);
|
|
1265
|
+
bar.fills = [{ type: 'SOLID', color: { r: 0.5, g: 0.5, b: 0.5 } }];
|
|
1266
|
+
bar.strokes = [];
|
|
1267
|
+
|
|
1268
|
+
row.appendChild(bar);
|
|
1269
|
+
row.appendChild(makeBodyText(k + ' \u2014 ' + Math.round(px) + 'px', { size: 12 }));
|
|
1270
|
+
frame.appendChild(row);
|
|
1271
|
+
}
|
|
1272
|
+
return frame;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
export function demoFrameFontSizes(): FrameNode | null {
|
|
1276
|
+
const sizes: Record<string, string> = getThemeGroup(_defaultThemeName, 'fontSize');
|
|
1277
|
+
const keys = Object.keys(sizes);
|
|
1278
|
+
if (keys.length === 0) return null;
|
|
1279
|
+
|
|
1280
|
+
const frame = makeSectionFrame('Font sizes');
|
|
1281
|
+
|
|
1282
|
+
for (const k of keys) {
|
|
1283
|
+
const px = pxFromSizeToken(sizes[k]);
|
|
1284
|
+
frame.appendChild(makeBodyText(k + ' \u2014 ' + Math.round(px) + 'px sample', { size: px }));
|
|
1285
|
+
}
|
|
1286
|
+
return frame;
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
export function demoFrameBreakpoints(): FrameNode | null {
|
|
1290
|
+
const breakpoints: Record<string, string> = getThemeGroup(_defaultThemeName, 'breakpoint');
|
|
1291
|
+
const keys = Object.keys(breakpoints);
|
|
1292
|
+
if (keys.length === 0) return null;
|
|
1293
|
+
|
|
1294
|
+
const sorted = keys
|
|
1295
|
+
.map((k) => ({ key: k, px: pxFromSizeToken(breakpoints[k]) }))
|
|
1296
|
+
.filter((entry) => Number.isFinite(entry.px))
|
|
1297
|
+
.sort((a, b) => a.px - b.px);
|
|
1298
|
+
if (sorted.length === 0) return null;
|
|
1299
|
+
|
|
1300
|
+
const frame = makeSectionFrame('Breakpoints');
|
|
1301
|
+
const maxPx = sorted[sorted.length - 1].px;
|
|
1302
|
+
// Cap the visualization width so a 96rem breakpoint doesn't blow out the
|
|
1303
|
+
// canvas; everything scales relative to the largest breakpoint.
|
|
1304
|
+
const maxBarWidth = 480;
|
|
1305
|
+
const scale = maxPx > 0 ? maxBarWidth / maxPx : 0;
|
|
1306
|
+
|
|
1307
|
+
for (const entry of sorted) {
|
|
1308
|
+
const row = figma.createFrame();
|
|
1309
|
+
row.layoutMode = 'HORIZONTAL';
|
|
1310
|
+
row.primaryAxisSizingMode = 'AUTO';
|
|
1311
|
+
row.counterAxisSizingMode = 'AUTO';
|
|
1312
|
+
row.itemSpacing = 12;
|
|
1313
|
+
row.counterAxisAlignItems = 'CENTER';
|
|
1314
|
+
row.fills = [];
|
|
1315
|
+
row.strokes = [];
|
|
1316
|
+
row.name = 'breakpoint/' + entry.key;
|
|
1317
|
+
|
|
1318
|
+
const bar = figma.createRectangle();
|
|
1319
|
+
bar.resize(Math.max(1, Math.round(entry.px * scale)), 12);
|
|
1320
|
+
bar.fills = [{ type: 'SOLID', color: { r: 0.5, g: 0.5, b: 0.5 } }];
|
|
1321
|
+
bar.strokes = [];
|
|
1322
|
+
|
|
1323
|
+
row.appendChild(bar);
|
|
1324
|
+
row.appendChild(makeBodyText(entry.key + ' \u2014 ' + Math.round(entry.px) + 'px', { size: 12 }));
|
|
1325
|
+
frame.appendChild(row);
|
|
1326
|
+
}
|
|
1327
|
+
return frame;
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
// Sized scale we want to surface in the design tokens overview, in ascending
|
|
1331
|
+
// visual weight. Anything else (DEFAULT alias, inner/drop/text variants) is
|
|
1332
|
+
// skipped — they pollute the order and inset shadows render nothing on a
|
|
1333
|
+
// solid-fill rect.
|
|
1334
|
+
const SHADOW_SIZE_ORDER = ['2xs', 'xs', 'sm', 'md', 'lg', 'xl', '2xl'];
|
|
1335
|
+
|
|
1336
|
+
export function demoFrameShadows(theme: string): FrameNode | null {
|
|
1337
|
+
const shadows: Record<string, string> = getThemeGroup(theme, 'shadow');
|
|
1338
|
+
const ordered = SHADOW_SIZE_ORDER.filter((k) => shadows[k]);
|
|
1339
|
+
if (ordered.length === 0) return null;
|
|
1340
|
+
|
|
1341
|
+
const frame = makeSectionFrame(theme.toUpperCase() + ' Shadows');
|
|
1342
|
+
// Larger gap so big shadows (shadow-xl, shadow-2xl) don't visually overlap
|
|
1343
|
+
// the next swatch — the default itemSpacing is too tight for shadow-2xl
|
|
1344
|
+
// which extends ~38px below its rect.
|
|
1345
|
+
frame.itemSpacing = 56;
|
|
1346
|
+
// Keep shadows from being clipped by the wrapper's bounds.
|
|
1347
|
+
frame.clipsContent = false;
|
|
1348
|
+
frame.appendChild(makeSectionTitle(getThemeDisplayName(theme)));
|
|
1349
|
+
|
|
1350
|
+
for (const k of ordered) {
|
|
1351
|
+
const cell = figma.createFrame();
|
|
1352
|
+
cell.layoutMode = 'VERTICAL';
|
|
1353
|
+
cell.primaryAxisSizingMode = 'AUTO';
|
|
1354
|
+
cell.counterAxisSizingMode = 'AUTO';
|
|
1355
|
+
cell.itemSpacing = 8;
|
|
1356
|
+
cell.fills = [];
|
|
1357
|
+
cell.strokes = [];
|
|
1358
|
+
cell.clipsContent = false;
|
|
1359
|
+
cell.name = 'shadow/' + k;
|
|
1360
|
+
|
|
1361
|
+
const swatch = figma.createRectangle();
|
|
1362
|
+
swatch.resize(96, 48);
|
|
1363
|
+
swatch.cornerRadius = 6;
|
|
1364
|
+
swatch.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
|
|
1365
|
+
swatch.strokes = [];
|
|
1366
|
+
|
|
1367
|
+
const layers = parseCssBoxShadow(String(shadows[k] || ''));
|
|
1368
|
+
if (layers.length > 0) {
|
|
1369
|
+
swatch.effects = layers
|
|
1370
|
+
.filter((l) => !l.inset)
|
|
1371
|
+
.map((l) => ({
|
|
1372
|
+
type: 'DROP_SHADOW' as const,
|
|
1373
|
+
color: { r: l.r, g: l.g, b: l.b, a: l.a },
|
|
1374
|
+
offset: { x: l.x, y: l.y },
|
|
1375
|
+
radius: l.blur,
|
|
1376
|
+
spread: l.spread,
|
|
1377
|
+
visible: true,
|
|
1378
|
+
blendMode: 'NORMAL' as const,
|
|
1379
|
+
}));
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
cell.appendChild(swatch);
|
|
1383
|
+
cell.appendChild(makeBodyText(k, { size: 12 }));
|
|
1384
|
+
frame.appendChild(cell);
|
|
1040
1385
|
}
|
|
1041
1386
|
return frame;
|
|
1042
1387
|
}
|