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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { pxFromSizeToken } from '
|
|
1
|
+
import { pxFromSizeToken } from '../tokens';
|
|
2
2
|
|
|
3
3
|
export type UtilityAtom = {
|
|
4
4
|
raw: string;
|
|
@@ -23,7 +23,17 @@ const MAX_WIDTH_MAP: Record<string, number> = {
|
|
|
23
23
|
'max-w-7xl': 1280,
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
// Memo for `parseUtilityClass` — the function is pure and the same class
|
|
27
|
+
// strings repeat heavily across a render tree (`flex` / `items-center` /
|
|
28
|
+
// `text-sm` appear on dozens of elements per story). The atom is treated
|
|
29
|
+
// as read-only by callers, so sharing the same instance across call sites
|
|
30
|
+
// is safe.
|
|
31
|
+
const PARSE_UTILITY_CACHE = new Map<string, UtilityAtom>();
|
|
32
|
+
|
|
26
33
|
export function parseUtilityClass(input: string): UtilityAtom {
|
|
34
|
+
const cached = PARSE_UTILITY_CACHE.get(input);
|
|
35
|
+
if (cached !== undefined) return cached;
|
|
36
|
+
|
|
27
37
|
let cls = input.trim();
|
|
28
38
|
let important = false;
|
|
29
39
|
if (cls.startsWith('!')) {
|
|
@@ -51,10 +61,11 @@ export function parseUtilityClass(input: string): UtilityAtom {
|
|
|
51
61
|
}
|
|
52
62
|
if (buf) parts.push(buf);
|
|
53
63
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
64
|
+
const atom: UtilityAtom = parts.length <= 1
|
|
65
|
+
? { raw: input, variants: [], important, utility: parts[0] || '' }
|
|
66
|
+
: { raw: input, variants: parts.slice(0, -1), important, utility: parts[parts.length - 1] };
|
|
67
|
+
PARSE_UTILITY_CACHE.set(input, atom);
|
|
68
|
+
return atom;
|
|
58
69
|
}
|
|
59
70
|
|
|
60
71
|
export function hasResponsiveVariant(variants: string[]): boolean {
|
|
@@ -120,6 +131,16 @@ export function resolveRadius(utility: string, radiusGroup: Record<string, strin
|
|
|
120
131
|
if (radiusGroup && radiusGroup[key]) {
|
|
121
132
|
return pxFromSizeToken(radiusGroup[key]);
|
|
122
133
|
}
|
|
123
|
-
const fallback: Record<string, number> = {
|
|
134
|
+
const fallback: Record<string, number> = {
|
|
135
|
+
none: 0,
|
|
136
|
+
sm: 2,
|
|
137
|
+
md: 6,
|
|
138
|
+
lg: 8,
|
|
139
|
+
xl: 12,
|
|
140
|
+
'2xl': 16,
|
|
141
|
+
'3xl': 24,
|
|
142
|
+
'4xl': 32,
|
|
143
|
+
full: 9999,
|
|
144
|
+
};
|
|
124
145
|
return fallback[key] ?? null;
|
|
125
146
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { type RGB, parseColor } from '
|
|
2
|
-
import { resolveTextColorValue, extractTextColorToken } from '
|
|
3
|
-
import { getLineHeightFromClasses, TAG_TYPOGRAPHY, createTextNode } from './text-builder';
|
|
4
|
-
import { tailwindClassesToStyle, type TailwindStyle } from '
|
|
5
|
-
import { type NodeIR, type NodeIRElement } from '
|
|
6
|
-
import type { RenderContext } from '
|
|
7
|
-
import { type NodeLayoutComputed } from '
|
|
1
|
+
import { type RGB, parseColor } from '../tokens';
|
|
2
|
+
import { resolveTextColorValue, extractTextColorToken } from '../tokens';
|
|
3
|
+
import { getLineHeightFromClasses, TAG_TYPOGRAPHY, createTextNode, type CreateTextOptions } from './text-builder';
|
|
4
|
+
import { tailwindClassesToStyle, type TailwindStyle } from '../tailwind';
|
|
5
|
+
import { type NodeIR, type NodeIRElement } from '../tailwind';
|
|
6
|
+
import type { RenderContext } from '../design-system';
|
|
7
|
+
import { type NodeLayoutComputed } from '../layout';
|
|
8
8
|
import { INLINE_TEXT_TAGS } from './text-builder';
|
|
9
|
-
import { TOKENS, getThemeFontFamily } from '
|
|
9
|
+
import { TOKENS, getThemeFontFamily } from '../tokens';
|
|
10
10
|
import { getFontStyleCandidatesForFamily, inferFontWeight } from './font-style-resolver';
|
|
11
11
|
|
|
12
12
|
export type InlineTextSegment = {
|
|
@@ -166,7 +166,7 @@ export function createInlineTextNode(
|
|
|
166
166
|
const textValue = segments.map(seg => seg.text).join('');
|
|
167
167
|
|
|
168
168
|
const typo = TAG_TYPOGRAPHY[node.tagLower] || { fontSize: 14 };
|
|
169
|
-
const textOpts:
|
|
169
|
+
const textOpts: CreateTextOptions & { theme?: string } = {
|
|
170
170
|
fontSize: typo.fontSize,
|
|
171
171
|
lineHeight: typo.lineHeight,
|
|
172
172
|
};
|
|
@@ -188,7 +188,7 @@ export function createInlineTextNode(
|
|
|
188
188
|
};
|
|
189
189
|
if (sizeMap[sizeClass]) textOpts.fontSize = sizeMap[sizeClass];
|
|
190
190
|
}
|
|
191
|
-
const lineHeight = getLineHeightFromClasses(node.classes, textOpts.fontSize);
|
|
191
|
+
const lineHeight = getLineHeightFromClasses(node.classes, textOpts.fontSize ?? 14);
|
|
192
192
|
if (lineHeight) {
|
|
193
193
|
textOpts.lineHeight = lineHeight;
|
|
194
194
|
}
|
|
@@ -196,11 +196,11 @@ export function createInlineTextNode(
|
|
|
196
196
|
textOpts.theme = theme;
|
|
197
197
|
const textNode = createTextNode(textValue, textOpts);
|
|
198
198
|
|
|
199
|
-
const baseFontName
|
|
200
|
-
const fontFamily = (baseFontName
|
|
199
|
+
const baseFontName: FontName | null = textNode.fontName !== figma.mixed ? textNode.fontName : null;
|
|
200
|
+
const fontFamily = (baseFontName ? baseFontName.family : null)
|
|
201
201
|
|| getThemeFontFamily(TOKENS, theme)
|
|
202
202
|
|| 'Inter';
|
|
203
|
-
const baseStyle = baseFontName
|
|
203
|
+
const baseStyle = baseFontName ? baseFontName.style : (baseFontStyle || 'Regular');
|
|
204
204
|
let cursor = 0;
|
|
205
205
|
for (const seg of segments) {
|
|
206
206
|
const start = cursor;
|
|
@@ -1,10 +1,23 @@
|
|
|
1
|
-
import { parseColor } from '
|
|
2
|
-
import { TOKENS, getCoreFontFamily, getThemeFontFamily } from '
|
|
3
|
-
import {
|
|
4
|
-
import { type
|
|
1
|
+
import { parseColor } from '../tokens';
|
|
2
|
+
import { TOKENS, getCoreFontFamily, getThemeFontFamily } from '../tokens';
|
|
3
|
+
import type { RGB } from '../tokens/colors';
|
|
4
|
+
import { tailwindClassesToStyle, type TailwindStyle } from '../tailwind';
|
|
5
|
+
import { type NodeIR, isElementLikeNode } from '../tailwind';
|
|
5
6
|
import { getFontStyleCandidatesForFamily } from './font-style-resolver';
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
export interface CreateTextOptions {
|
|
9
|
+
fontStyle?: string;
|
|
10
|
+
bold?: boolean;
|
|
11
|
+
fontRole?: string;
|
|
12
|
+
fontFamily?: string;
|
|
13
|
+
theme?: string;
|
|
14
|
+
fontSize?: number;
|
|
15
|
+
lineHeight?: number;
|
|
16
|
+
textAlignHorizontal?: 'LEFT' | 'CENTER' | 'RIGHT' | 'JUSTIFIED';
|
|
17
|
+
textAlignVertical?: 'TOP' | 'CENTER' | 'BOTTOM';
|
|
18
|
+
fill?: string | RGB;
|
|
19
|
+
opacity?: number;
|
|
20
|
+
}
|
|
8
21
|
|
|
9
22
|
const FONT_LOAD_STATE: Record<string, 'loading' | 'loaded'> = {};
|
|
10
23
|
const FONT_LOAD_PROMISES: Promise<void>[] = [];
|
|
@@ -48,10 +61,10 @@ function queueFontLoad(family: string, style: string): void {
|
|
|
48
61
|
}
|
|
49
62
|
}
|
|
50
63
|
|
|
51
|
-
export function createTextNode(text: string, options?:
|
|
64
|
+
export function createTextNode(text: string, options?: CreateTextOptions): TextNode {
|
|
52
65
|
const node = figma.createText();
|
|
53
66
|
node.characters = text;
|
|
54
|
-
const opts = options || {};
|
|
67
|
+
const opts: CreateTextOptions = options || {};
|
|
55
68
|
const style = opts.fontStyle || (opts.bold ? 'Bold' : 'Regular');
|
|
56
69
|
const fontRole = opts.fontRole || 'sans';
|
|
57
70
|
const fontFamily = opts.fontFamily || (opts.theme ? getThemeFontFamily(TOKENS, opts.theme, fontRole) : null) || getCoreFontFamily(TOKENS) || 'Inter';
|
|
@@ -80,6 +93,10 @@ export function createTextNode(text: string, options?: any): any {
|
|
|
80
93
|
if (opts.textAlignHorizontal) {
|
|
81
94
|
node.textAlignHorizontal = opts.textAlignHorizontal;
|
|
82
95
|
}
|
|
96
|
+
// Default to CENTER: identical to TOP for hug-sized single-line text, but
|
|
97
|
+
// correct when the text box is taller (row-synced in flex/grid with
|
|
98
|
+
// items-center, or layoutAlign=STRETCH). Callers can override with 'TOP'.
|
|
99
|
+
node.textAlignVertical = opts.textAlignVertical || 'CENTER';
|
|
83
100
|
if (opts.fill) {
|
|
84
101
|
const fill = typeof opts.fill === 'string' ? parseColor(opts.fill) : opts.fill;
|
|
85
102
|
node.fills = [{
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type NodeIR, type NodeIRComponent, splitClassName } from '
|
|
2
|
-
import { parseTextLineCount, parseWidthTokenList, TEXT_LINE_WIDTH_CLASSES } from '
|
|
1
|
+
import { type NodeIR, type NodeIRComponent, splitClassName } from '../tailwind';
|
|
2
|
+
import { parseTextLineCount, parseWidthTokenList, TEXT_LINE_WIDTH_CLASSES } from '../design-system';
|
|
3
3
|
|
|
4
4
|
export function buildTextLinePlaceholderNode(node: NodeIRComponent): NodeIR {
|
|
5
5
|
const props = node.props || {};
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { COMPONENT_DEFS } from './tokens';
|
|
2
|
-
import { extractFrameProperties, propsToTailwindClasses } from '
|
|
2
|
+
import { extractFrameProperties, propsToTailwindClasses, type FrameProperties } from '../tailwind';
|
|
3
3
|
|
|
4
4
|
interface ComponentFrame {
|
|
5
|
-
frame:
|
|
5
|
+
frame: SceneNode;
|
|
6
6
|
componentName: string;
|
|
7
7
|
variant: string;
|
|
8
8
|
state: string;
|
|
9
|
-
props:
|
|
9
|
+
props: FrameProperties;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
interface ChangeDetail {
|
|
@@ -21,7 +21,7 @@ interface ComponentChange {
|
|
|
21
21
|
type: string;
|
|
22
22
|
file: string;
|
|
23
23
|
changes: ChangeDetail[];
|
|
24
|
-
figmaProps:
|
|
24
|
+
figmaProps: FrameProperties;
|
|
25
25
|
suggestedClasses: string[];
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -38,10 +38,10 @@ interface ComponentPatch {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export function findComponentFrames(): ComponentFrame[] {
|
|
41
|
-
let dsPage:
|
|
42
|
-
for (let i = 0; i <
|
|
43
|
-
if (
|
|
44
|
-
dsPage =
|
|
41
|
+
let dsPage: PageNode | null = null;
|
|
42
|
+
for (let i = 0; i < figma.root.children.length; i++) {
|
|
43
|
+
if (figma.root.children[i].name === 'Design System') {
|
|
44
|
+
dsPage = figma.root.children[i];
|
|
45
45
|
break;
|
|
46
46
|
}
|
|
47
47
|
}
|
|
@@ -53,7 +53,7 @@ export function findComponentFrames(): ComponentFrame[] {
|
|
|
53
53
|
const components: ComponentFrame[] = [];
|
|
54
54
|
|
|
55
55
|
// Find component frames by name pattern: "ComponentName/variant/state" or "ComponentName States"
|
|
56
|
-
function searchFrames(node:
|
|
56
|
+
function searchFrames(node: SceneNode | PageNode, depth: number): void {
|
|
57
57
|
if (depth > 5) return; // Limit depth
|
|
58
58
|
|
|
59
59
|
if (node.type === 'FRAME' || node.type === 'COMPONENT') {
|
|
@@ -101,12 +101,12 @@ export function detectComponentChanges(): { changes?: ComponentChange[]; error?:
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
// Compare with code definitions
|
|
104
|
-
if (!COMPONENT_DEFS || !
|
|
104
|
+
if (!COMPONENT_DEFS || !COMPONENT_DEFS.components) {
|
|
105
105
|
return { error: 'No component definitions loaded. Start dev server and refresh.' };
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
for (let j = 0; j <
|
|
109
|
-
const codeDef =
|
|
108
|
+
for (let j = 0; j < COMPONENT_DEFS.components.length; j++) {
|
|
109
|
+
const codeDef = COMPONENT_DEFS.components[j];
|
|
110
110
|
const figmaFrames = figmaByComponent[codeDef.name] || [];
|
|
111
111
|
|
|
112
112
|
if (figmaFrames.length === 0) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { TOKENS, getThemeColors } from './tokens';
|
|
2
2
|
import type { RGB } from './colors';
|
|
3
|
+
import { getBaseClass } from '../tailwind';
|
|
3
4
|
|
|
4
5
|
const NON_COLOR_TEXT_PREFIXES = new Set([
|
|
5
6
|
'xs', 'sm', 'base', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl', '6xl', '7xl', '8xl', '9xl',
|
|
@@ -11,12 +12,6 @@ const NON_COLOR_TEXT_TOKENS = new Set([
|
|
|
11
12
|
'inherit',
|
|
12
13
|
]);
|
|
13
14
|
|
|
14
|
-
function getBaseClass(value: string): string | null {
|
|
15
|
-
if (!value) return null;
|
|
16
|
-
if (value.indexOf(':') !== -1) return null;
|
|
17
|
-
return value;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
15
|
function isRgbObject(value: unknown): value is RGB {
|
|
21
16
|
if (!value || typeof value !== 'object') return false;
|
|
22
17
|
const obj = value as Record<string, unknown>;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { Tokens } from './tokens';
|
|
2
|
+
|
|
1
3
|
// --- Color parsing: OKLCH -> sRGB ---
|
|
2
4
|
|
|
3
5
|
export interface RGB {
|
|
@@ -20,8 +22,12 @@ export const DEBUG = false;
|
|
|
20
22
|
export function debug(...args: unknown[]): void {
|
|
21
23
|
if (!DEBUG) return;
|
|
22
24
|
try {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
// Rest params compile to value-side arg-collection at es2017; the
|
|
26
|
+
// Figma sandbox supports this fine. What it can't handle is
|
|
27
|
+
// value-side call/array spread (`fn(...args)`, `[...arr]`) — see
|
|
28
|
+
// .ai/behaviour.md.
|
|
29
|
+
console.log.apply(console, (['[TailwindTokens]'] as unknown[]).concat(args));
|
|
30
|
+
} catch (_err) { /* ignore */ }
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
export function clamp01(x: number): number {
|
|
@@ -162,14 +168,15 @@ export function normalizeSizeValue(value: unknown): string {
|
|
|
162
168
|
|
|
163
169
|
// --- Color index for nearest-color lookups ---
|
|
164
170
|
|
|
165
|
-
export function rebuildColorIndex(tokens:
|
|
171
|
+
export function rebuildColorIndex(tokens: Tokens): void {
|
|
166
172
|
COLOR_INDEX.length = 0;
|
|
167
173
|
for (const theme of Object.keys(tokens)) {
|
|
168
|
-
const
|
|
169
|
-
|
|
174
|
+
const themeTokens = tokens[theme];
|
|
175
|
+
if (!themeTokens || !('color' in themeTokens) || !themeTokens.color) continue;
|
|
176
|
+
for (const [token, value] of Object.entries(themeTokens.color)) {
|
|
170
177
|
try {
|
|
171
178
|
COLOR_INDEX.push({ token, rgb: parseColor(value) });
|
|
172
|
-
} catch (
|
|
179
|
+
} catch (_err) {
|
|
173
180
|
// ignore bad color
|
|
174
181
|
}
|
|
175
182
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type TokenSourceMode = '
|
|
1
|
+
export type TokenSourceMode = 'css' | 'dtcg';
|
|
2
2
|
export type ResolvedTokenSourceMode = 'css' | 'dtcg' | 'embedded';
|
|
3
3
|
|
|
4
4
|
export interface ScannedThemeTokens {
|
|
@@ -8,6 +8,7 @@ export interface ScannedThemeTokens {
|
|
|
8
8
|
spacing?: Record<string, number>;
|
|
9
9
|
fontSize?: Record<string, number>;
|
|
10
10
|
shadows?: Record<string, string>;
|
|
11
|
+
breakpoints?: Record<string, number>;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export interface ScannedTokenMap {
|
|
@@ -20,6 +21,7 @@ export interface ScannedTokenMap {
|
|
|
20
21
|
spacing: Record<string, number>;
|
|
21
22
|
fontSize: Record<string, number>;
|
|
22
23
|
shadows: Record<string, string>;
|
|
24
|
+
breakpoints: Record<string, number>;
|
|
23
25
|
themes: Record<string, ScannedThemeTokens>;
|
|
24
26
|
}
|
|
25
27
|
|
|
@@ -38,6 +40,7 @@ export function createEmptyScannedTokenMap(
|
|
|
38
40
|
spacing: {},
|
|
39
41
|
fontSize: {},
|
|
40
42
|
shadows: {},
|
|
43
|
+
breakpoints: {},
|
|
41
44
|
themes: {},
|
|
42
45
|
};
|
|
43
46
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { parseColor, mergeTokens as deepMergeTokens } from './colors';
|
|
2
2
|
import { readVariableTokens } from './variables';
|
|
3
3
|
import type { ScannedTokenMap } from './token-source';
|
|
4
|
+
import type { ComponentDef } from '../components/scanner-types';
|
|
4
5
|
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
6
7
|
// Types
|
|
@@ -9,7 +10,7 @@ import type { ScannedTokenMap } from './token-source';
|
|
|
9
10
|
/** A flat record of token name -> value (e.g. color name -> oklch string). */
|
|
10
11
|
export type TokenGroup = Record<string, string>;
|
|
11
12
|
|
|
12
|
-
/** A theme block contains optional color, radius, spacing, fontSize, and
|
|
13
|
+
/** A theme block contains optional color, radius, spacing, fontSize, shadow, and breakpoint groups. */
|
|
13
14
|
export interface ThemeTokens {
|
|
14
15
|
color?: TokenGroup;
|
|
15
16
|
font?: TokenGroup;
|
|
@@ -17,6 +18,7 @@ export interface ThemeTokens {
|
|
|
17
18
|
spacing?: TokenGroup;
|
|
18
19
|
fontSize?: TokenGroup;
|
|
19
20
|
shadow?: TokenGroup;
|
|
21
|
+
breakpoint?: TokenGroup;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
/** Core (non-theme) tokens. */
|
|
@@ -46,6 +48,7 @@ export interface DTCGTokens {
|
|
|
46
48
|
description: string;
|
|
47
49
|
note: string;
|
|
48
50
|
};
|
|
51
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- DTCG groups are deeply nested and dynamically built
|
|
49
52
|
[key: string]: any;
|
|
50
53
|
}
|
|
51
54
|
|
|
@@ -59,9 +62,9 @@ export interface ComponentDefs {
|
|
|
59
62
|
schemaVersion?: number;
|
|
60
63
|
version?: string;
|
|
61
64
|
generatedAt?: string;
|
|
62
|
-
components:
|
|
63
|
-
spacingScale: Record<string,
|
|
64
|
-
colorTokens:
|
|
65
|
+
components: ComponentDef[];
|
|
66
|
+
spacingScale: Record<string, number>;
|
|
67
|
+
colorTokens: unknown[];
|
|
65
68
|
paletteTokens?: Record<string, string>;
|
|
66
69
|
iconRegistry?: Record<string, { module: string; exportName: string; svg: string }>;
|
|
67
70
|
styleMap?: Record<string, { declarations: Record<string, string>; media?: string }[]>;
|
|
@@ -71,7 +74,7 @@ export interface ComponentDefs {
|
|
|
71
74
|
// Mutable state
|
|
72
75
|
// ---------------------------------------------------------------------------
|
|
73
76
|
|
|
74
|
-
export let TOKENS: Tokens = {"primary":{"color":{"background":"oklch(1 0 0)","foreground":"oklch(0.141 0.005 285.823)","card":"oklch(1 0 0)","card-foreground":"oklch(0.141 0.005 285.823)","popover":"oklch(1 0 0)","popover-foreground":"oklch(0.141 0.005 285.823)","primary":"oklch(62.71% 0.17 149.21)","primary-
|
|
77
|
+
export let TOKENS: Tokens = {"primary":{"color":{"background":"oklch(1 0 0)","foreground":"oklch(0.141 0.005 285.823)","card":"oklch(1 0 0)","card-foreground":"oklch(0.141 0.005 285.823)","popover":"oklch(1 0 0)","popover-foreground":"oklch(0.141 0.005 285.823)","primary":"oklch(50% 0.14 145)","primary-foreground":"oklch(0.982 0.018 155.826)","primary-decorative":"oklch(62.71% 0.17 149.21)","primary-soft":"oklch(0.84 0.06 145)","secondary":"oklch(0.967 0.001 286.375)","secondary-foreground":"oklch(0.21 0.006 285.885)","muted":"oklch(0.967 0.001 286.375)","muted-foreground":"oklch(0.552 0.016 285.938)","accent":"oklch(0.967 0.001 286.375)","accent-foreground":"oklch(0.21 0.006 285.885)","destructive":"oklch(0.577 0.245 27.325)","border":"oklch(0.92 0.004 286.32)","input":"oklch(0.92 0.004 286.32)","ring":"oklch(0.723 0.219 149.579)","sidebar":"oklch(0.985 0 0)","sidebar-foreground":"oklch(0.141 0.005 285.823)","sidebar-primary":"oklch(0.723 0.219 149.579)","sidebar-primary-foreground":"oklch(0.982 0.018 155.826)","sidebar-accent":"oklch(0.967 0.001 286.375)","sidebar-accent-foreground":"oklch(0.21 0.006 285.885)","sidebar-border":"oklch(0.92 0.004 286.32)","sidebar-ring":"oklch(0.723 0.219 149.579)"},"font":{"sans":"\"Open Sans\", ui-sans-serif, system-ui, sans-serif","heading":"\"Open Sans\", ui-sans-serif, system-ui, sans-serif"},"radius":{"base":"0.5rem","sm":"0.25rem","md":"0.5rem","lg":"0.75rem","xl":"1rem","2xl":"1.25rem","full":"624rem"},"spacing":{"xs":"0.25rem","sm":"0.5rem","md":"1rem","lg":"1.5rem","xl":"2rem","2xl":"3rem","3xl":"4rem","4xl":"6rem"},"fontSize":{"xs":"0.75rem","sm":"0.875rem","base":"1rem","lg":"1.125rem","xl":"1.25rem","2xl":"1.5rem","3xl":"1.875rem","4xl":"2.25rem","5xl":"3rem","6xl":"3.75rem"},"shadow":{"sm":"0 1px 2px 0 rgb(0 0 0 / 0.05)","DEFAULT":"0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)","md":"0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)","lg":"0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.05)","xl":"0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.04)","2xl":"0 25px 50px -12px rgb(0 0 0 / 0.25)","inner":"inset 0 2px 4px 0 rgb(0 0 0 / 0.05)"}},"secondary":{"color":{"background":"oklch(1 0 0)","foreground":"oklch(0.141 0.005 285.823)","card":"oklch(1 0 0)","card-foreground":"oklch(0.141 0.005 285.823)","popover":"oklch(1 0 0)","popover-foreground":"oklch(0.141 0.005 285.823)","primary":"oklch(50% 0.16 250)","primary-foreground":"oklch(0.982 0.018 250)","primary-decorative":"oklch(62.71% 0.17 250)","primary-soft":"oklch(0.84 0.06 250)","secondary":"oklch(0.967 0.001 286.375)","secondary-foreground":"oklch(0.21 0.006 285.885)","muted":"oklch(0.967 0.001 286.375)","muted-foreground":"oklch(0.552 0.016 285.938)","accent":"oklch(0.967 0.001 286.375)","accent-foreground":"oklch(0.21 0.006 285.885)","destructive":"oklch(0.577 0.245 250)","border":"oklch(0.92 0.004 286.32)","input":"oklch(0.92 0.004 286.32)","ring":"oklch(0.723 0.219 250)","sidebar":"oklch(0.985 0 0)","sidebar-foreground":"oklch(0.141 0.005 285.823)","sidebar-primary":"oklch(0.723 0.219 250)","sidebar-primary-foreground":"oklch(0.982 0.018 250)","sidebar-accent":"oklch(0.967 0.001 286.375)","sidebar-accent-foreground":"oklch(0.21 0.006 285.885)","sidebar-border":"oklch(0.92 0.004 286.32)","sidebar-ring":"oklch(0.723 0.219 250)"},"font":{"sans":"\"Open Sans\", ui-sans-serif, system-ui, sans-serif","heading":"\"Playfair Display\", Georgia, serif"},"radius":{"base":"1rem","sm":"0.75rem","md":"1rem","lg":"1.25rem","xl":"1.5rem","2xl":"1.75rem","full":"624rem"}}};
|
|
75
78
|
const EMBEDDED_TOKENS_SNAPSHOT: Tokens = JSON.parse(JSON.stringify(TOKENS));
|
|
76
79
|
|
|
77
80
|
// Component definitions - fetched from dev server at runtime
|
|
@@ -86,18 +89,21 @@ function isPlainObject(v: unknown): v is Record<string, unknown> {
|
|
|
86
89
|
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
87
90
|
}
|
|
88
91
|
|
|
89
|
-
function mergeTokens(base:
|
|
92
|
+
function mergeTokens<T>(base: T, override: T): T {
|
|
90
93
|
if (!isPlainObject(base)) return override;
|
|
91
|
-
|
|
92
|
-
|
|
94
|
+
if (!isPlainObject(override)) return base;
|
|
95
|
+
const out: Record<string, unknown> = {};
|
|
96
|
+
for (const key in base) out[key] = (base as Record<string, unknown>)[key];
|
|
93
97
|
for (const key in override) {
|
|
94
|
-
|
|
95
|
-
|
|
98
|
+
const baseVal = (base as Record<string, unknown>)[key];
|
|
99
|
+
const overrideVal = (override as Record<string, unknown>)[key];
|
|
100
|
+
if (isPlainObject(baseVal) && isPlainObject(overrideVal)) {
|
|
101
|
+
out[key] = mergeTokens(baseVal, overrideVal);
|
|
96
102
|
} else {
|
|
97
|
-
out[key] =
|
|
103
|
+
out[key] = overrideVal;
|
|
98
104
|
}
|
|
99
105
|
}
|
|
100
|
-
return out;
|
|
106
|
+
return out as T;
|
|
101
107
|
}
|
|
102
108
|
|
|
103
109
|
function parseDimensionToPx(value: unknown): number | null {
|
|
@@ -293,16 +299,53 @@ function hasKeys(group: Record<string, unknown> | undefined): boolean {
|
|
|
293
299
|
return !!group && Object.keys(group).length > 0;
|
|
294
300
|
}
|
|
295
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Ensures a font-family CSS value always contains a quoted literal name so that
|
|
304
|
+
* round-trips through patchCssVariables are self-healing.
|
|
305
|
+
*
|
|
306
|
+
* e.g. `var(--font-playfair), Georgia, serif`
|
|
307
|
+
* → `var(--font-playfair), "Playfair", Georgia, serif`
|
|
308
|
+
*
|
|
309
|
+
* If a proper literal is already present the value is returned unchanged.
|
|
310
|
+
*/
|
|
311
|
+
function enrichFontValue(value: string): string {
|
|
312
|
+
const parts = value.split(',');
|
|
313
|
+
let hasLiteral = false;
|
|
314
|
+
let insertAfterIdx = -1;
|
|
315
|
+
|
|
316
|
+
for (let i = 0; i < parts.length; i++) {
|
|
317
|
+
const trimmed = parts[i].trim();
|
|
318
|
+
if (/^var\(/.test(trimmed)) {
|
|
319
|
+
if (insertAfterIdx === -1) insertAfterIdx = i;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const lower = trimmed.toLowerCase().replace(/^["']|["']$/g, '');
|
|
323
|
+
if (SYSTEM_FONT_KEYWORDS.has(lower)) continue;
|
|
324
|
+
if (trimmed.replace(/^["']|["']$/g, '').trim()) { hasLiteral = true; break; }
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (hasLiteral || insertAfterIdx === -1) return value;
|
|
328
|
+
|
|
329
|
+
// Derive best-effort literal from the first var(--font-*) name
|
|
330
|
+
const varMatch = parts[insertAfterIdx].trim().match(/^var\(--font-([a-z0-9-]+)\)/i);
|
|
331
|
+
if (!varMatch) return value;
|
|
332
|
+
const literal = varMatch[1].split('-').map((w: string) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
333
|
+
const result = parts.slice();
|
|
334
|
+
result.splice(insertAfterIdx + 1, 0, ` "${literal}"`);
|
|
335
|
+
return result.join(',');
|
|
336
|
+
}
|
|
337
|
+
|
|
296
338
|
function buildTokenPatchFromScanned(map: ScannedTokenMap): Tokens {
|
|
297
339
|
const patch: Tokens = {};
|
|
298
340
|
|
|
299
341
|
const primary: ThemeTokens = {};
|
|
300
342
|
if (hasKeys(map.colors)) primary.color = { ...map.colors };
|
|
301
|
-
if (hasKeys(map.fonts)) primary.font =
|
|
343
|
+
if (hasKeys(map.fonts)) primary.font = Object.fromEntries(Object.entries(map.fonts).map(([k, v]) => [k, enrichFontValue(String(v))]));
|
|
302
344
|
if (hasKeys(map.radius)) primary.radius = mapDimensionGroup(map.radius);
|
|
303
345
|
if (hasKeys(map.spacing)) primary.spacing = mapDimensionGroup(map.spacing);
|
|
304
346
|
if (hasKeys(map.fontSize)) primary.fontSize = mapDimensionGroup(map.fontSize);
|
|
305
347
|
if (hasKeys(map.shadows)) primary.shadow = { ...map.shadows };
|
|
348
|
+
if (hasKeys(map.breakpoints)) primary.breakpoint = mapDimensionGroup(map.breakpoints);
|
|
306
349
|
if (Object.keys(primary).length > 0) patch.primary = primary;
|
|
307
350
|
|
|
308
351
|
for (const themeName in map.themes) {
|
|
@@ -310,11 +353,12 @@ function buildTokenPatchFromScanned(map: ScannedTokenMap): Tokens {
|
|
|
310
353
|
if (!scanned) continue;
|
|
311
354
|
const themedPatch: ThemeTokens = {};
|
|
312
355
|
if (hasKeys(scanned.colors)) themedPatch.color = { ...scanned.colors };
|
|
313
|
-
if (hasKeys(scanned.fonts)) themedPatch.font =
|
|
356
|
+
if (hasKeys(scanned.fonts)) themedPatch.font = Object.fromEntries(Object.entries(scanned.fonts as Record<string, string>).map(([k, v]) => [k, enrichFontValue(v)]));
|
|
314
357
|
if (hasKeys(scanned.radius)) themedPatch.radius = mapDimensionGroup(scanned.radius as Record<string, number>);
|
|
315
358
|
if (hasKeys(scanned.spacing)) themedPatch.spacing = mapDimensionGroup(scanned.spacing as Record<string, number>);
|
|
316
359
|
if (hasKeys(scanned.fontSize)) themedPatch.fontSize = mapDimensionGroup(scanned.fontSize as Record<string, number>);
|
|
317
360
|
if (hasKeys(scanned.shadows)) themedPatch.shadow = { ...(scanned.shadows as Record<string, string>) };
|
|
361
|
+
if (hasKeys(scanned.breakpoints)) themedPatch.breakpoint = mapDimensionGroup(scanned.breakpoints as Record<string, number>);
|
|
318
362
|
if (Object.keys(themedPatch).length > 0) patch[themeName] = themedPatch;
|
|
319
363
|
}
|
|
320
364
|
|
|
@@ -455,6 +499,17 @@ export function getThemeFontSize(tokens: Tokens, theme: string): TokenGroup {
|
|
|
455
499
|
return {};
|
|
456
500
|
}
|
|
457
501
|
|
|
502
|
+
/** Get breakpoint tokens for a named theme, falling back to primary. */
|
|
503
|
+
export function getThemeBreakpoints(tokens: Tokens, theme: string): TokenGroup {
|
|
504
|
+
const block = tokens[theme];
|
|
505
|
+
if (block && 'breakpoint' in block) {
|
|
506
|
+
const themeBlock = block as ThemeTokens;
|
|
507
|
+
if (themeBlock.breakpoint && Object.keys(themeBlock.breakpoint).length) return themeBlock.breakpoint;
|
|
508
|
+
}
|
|
509
|
+
if (theme !== 'primary') return getThemeBreakpoints(tokens, 'primary');
|
|
510
|
+
return {};
|
|
511
|
+
}
|
|
512
|
+
|
|
458
513
|
/** Get named theme keys in declaration order, excluding the core block. */
|
|
459
514
|
export function getThemeNames(tokens: Tokens): string[] {
|
|
460
515
|
return Object.keys(tokens).filter((key) => {
|
|
@@ -471,6 +526,45 @@ export function getThemeNames(tokens: Tokens): string[] {
|
|
|
471
526
|
|
|
472
527
|
/** Get a font family for a specific theme and role ('sans' | 'heading' | 'mono').
|
|
473
528
|
* Falls back: theme+role → theme+sans → primary+role → primary+sans → null. */
|
|
529
|
+
const SYSTEM_FONT_KEYWORDS = new Set([
|
|
530
|
+
// Generic families & system keywords
|
|
531
|
+
'ui-sans-serif', 'ui-serif', 'ui-monospace', 'system-ui',
|
|
532
|
+
'sans-serif', 'serif', 'monospace', '-apple-system', 'inherit', 'initial',
|
|
533
|
+
// Common web-safe fonts that appear as fallbacks, not as the intended design font
|
|
534
|
+
'arial', 'helvetica', 'georgia', 'verdana', 'tahoma', 'trebuchet ms',
|
|
535
|
+
'times new roman', 'courier new', 'courier', 'palatino', 'garamond',
|
|
536
|
+
'bookman', 'comic sans ms', 'impact', 'lucida sans unicode',
|
|
537
|
+
// Emoji / symbol fallback fonts — present in Tailwind's default
|
|
538
|
+
// sans-serif stack but not in Figma's font registry. Without skipping
|
|
539
|
+
// these, the resolver picks "Apple Color Emoji" as the body font on
|
|
540
|
+
// any consumer that exposes Tailwind's default `font.sans` value,
|
|
541
|
+
// then `figma.loadFontAsync` throws "couldn't load font".
|
|
542
|
+
'apple color emoji', 'segoe ui emoji', 'segoe ui symbol', 'noto color emoji',
|
|
543
|
+
]);
|
|
544
|
+
|
|
545
|
+
export function extractFontName(raw: unknown): string | null {
|
|
546
|
+
const parts = String(raw || '').split(',');
|
|
547
|
+
let varFallback: string | null = null;
|
|
548
|
+
for (const part of parts) {
|
|
549
|
+
const trimmed = part.trim();
|
|
550
|
+
// Try to extract from CSS variable references like var(--font-open-sans)
|
|
551
|
+
const varMatch = trimmed.match(/^var\(--font-([a-z0-9-]+)\)/i);
|
|
552
|
+
if (varMatch) {
|
|
553
|
+
if (!varFallback) {
|
|
554
|
+
varFallback = varMatch[1].split('-').map((w: string) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
555
|
+
}
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
// Skip system font keywords
|
|
559
|
+
const lower = trimmed.toLowerCase().replace(/^["']|["']$/g, '');
|
|
560
|
+
if (SYSTEM_FONT_KEYWORDS.has(lower)) continue;
|
|
561
|
+
const name = trimmed.replace(/^["']|["']$/g, '');
|
|
562
|
+
if (name) return name;
|
|
563
|
+
}
|
|
564
|
+
// Fall back to name derived from the CSS variable (e.g. var(--font-open-sans) → "Open Sans")
|
|
565
|
+
return varFallback;
|
|
566
|
+
}
|
|
567
|
+
|
|
474
568
|
export function getThemeFontFamily(tokens: Tokens, theme: string, role: string = 'sans'): string | null {
|
|
475
569
|
const block = tokens[theme];
|
|
476
570
|
if (block && 'font' in block) {
|
|
@@ -478,8 +572,8 @@ export function getThemeFontFamily(tokens: Tokens, theme: string, role: string =
|
|
|
478
572
|
if (font) {
|
|
479
573
|
const raw = (font as TokenGroup)[role] || (role !== 'sans' ? (font as TokenGroup).sans : null) || Object.values(font as TokenGroup)[0];
|
|
480
574
|
if (raw) {
|
|
481
|
-
const
|
|
482
|
-
if (
|
|
575
|
+
const name = extractFontName(raw);
|
|
576
|
+
if (name) return name;
|
|
483
577
|
}
|
|
484
578
|
}
|
|
485
579
|
}
|
|
@@ -493,8 +587,10 @@ export function getCoreFontFamily(tokens: Tokens): string | null {
|
|
|
493
587
|
if (core && core.font) {
|
|
494
588
|
const raw = (core.font as TokenGroup).sans || Object.values(core.font as TokenGroup)[0];
|
|
495
589
|
if (raw) {
|
|
496
|
-
|
|
497
|
-
|
|
590
|
+
// Filter system / emoji / generic-keyword entries — they aren't
|
|
591
|
+
// valid Figma font families. Same logic as getThemeFontFamily.
|
|
592
|
+
const name = extractFontName(raw);
|
|
593
|
+
if (name) return name;
|
|
498
594
|
}
|
|
499
595
|
}
|
|
500
596
|
return getThemeFontFamily(tokens, 'primary');
|
|
@@ -534,7 +630,7 @@ export function tokensToDTCG(tokens: Tokens): DTCGTokens {
|
|
|
534
630
|
|
|
535
631
|
// Core (font)
|
|
536
632
|
if (tokens.core) {
|
|
537
|
-
dtcg.core = {} as Record<string,
|
|
633
|
+
dtcg.core = {} as Record<string, Record<string, DTCGEntry>>;
|
|
538
634
|
if (tokens.core.font) {
|
|
539
635
|
dtcg.core.font = {} as Record<string, DTCGEntry>;
|
|
540
636
|
for (const fk in tokens.core.font) {
|
|
@@ -554,7 +650,7 @@ export function tokensToDTCG(tokens: Tokens): DTCGTokens {
|
|
|
554
650
|
for (let ti = 0; ti < themes.length; ti++) {
|
|
555
651
|
const themeName = themes[ti];
|
|
556
652
|
if (!tokens[themeName]) continue;
|
|
557
|
-
dtcg[themeName] = {} as Record<string,
|
|
653
|
+
dtcg[themeName] = {} as Record<string, Record<string, DTCGEntry>>;
|
|
558
654
|
|
|
559
655
|
const themeBlock = tokens[themeName] as ThemeTokens;
|
|
560
656
|
|