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
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
cvaInstanceHasOverridingJsxChildren,
|
|
5
|
+
cvaInstanceHasOverridingState,
|
|
6
|
+
getFirstElementJsxChildTagName,
|
|
7
|
+
MASTER_ICON_NAME_KEY,
|
|
8
|
+
tryCreateCvaComponentInstance,
|
|
9
|
+
} from '../src/components/component-instance';
|
|
10
|
+
|
|
11
|
+
// Regression: the smart fall-back in tryCreateCvaComponentInstance.
|
|
12
|
+
//
|
|
13
|
+
// Background: a CVA component used with JSX-element children (an icon)
|
|
14
|
+
// can't be cleanly represented as a symbol instance, because Figma's
|
|
15
|
+
// instance-property API can't per-instance-swap SVG vectors. So when an
|
|
16
|
+
// instance's icon DIFFERS from the icon recorded on the master, we must
|
|
17
|
+
// fall back to frame rendering. When they MATCH, the symbol instance
|
|
18
|
+
// renders the same icon as the master would, so it's safe to use.
|
|
19
|
+
//
|
|
20
|
+
// History: an initial fix returned null for ANY CVA instance with element
|
|
21
|
+
// children. That correctly preserved per-instance icons via frame
|
|
22
|
+
// rendering but cost the symbol benefit for every Toggle/Toggle-group
|
|
23
|
+
// instance — even cases like `Toggle.Sizes` where every item used the
|
|
24
|
+
// same icon as the master. The smart fall-back keeps the symbol path
|
|
25
|
+
// when icons match, falls back when they differ.
|
|
26
|
+
//
|
|
27
|
+
// This file tests:
|
|
28
|
+
// 1. The pure helpers (predicate + tagName extractor).
|
|
29
|
+
// 2. The end-to-end function with a minimal stubbed backend, asserting
|
|
30
|
+
// that match vs. differ produce instance vs. null per the contract.
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Helper-level cases
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
// cvaInstanceHasOverridingJsxChildren
|
|
37
|
+
{
|
|
38
|
+
assert.equal(cvaInstanceHasOverridingJsxChildren(null), false, 'null props');
|
|
39
|
+
assert.equal(cvaInstanceHasOverridingJsxChildren({}), false, 'empty props');
|
|
40
|
+
assert.equal(
|
|
41
|
+
cvaInstanceHasOverridingJsxChildren({ __jsxChildren: [{ type: 'text', content: 'x' }] }),
|
|
42
|
+
false,
|
|
43
|
+
'text-only children do not override',
|
|
44
|
+
);
|
|
45
|
+
assert.equal(
|
|
46
|
+
cvaInstanceHasOverridingJsxChildren({
|
|
47
|
+
__jsxChildren: [{ type: 'element', tagName: 'Bold' }],
|
|
48
|
+
}),
|
|
49
|
+
true,
|
|
50
|
+
'a single element child overrides',
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// getFirstElementJsxChildTagName
|
|
55
|
+
{
|
|
56
|
+
assert.equal(getFirstElementJsxChildTagName(null), null, 'null returns null');
|
|
57
|
+
assert.equal(getFirstElementJsxChildTagName('not-an-array'), null, 'non-array returns null');
|
|
58
|
+
assert.equal(getFirstElementJsxChildTagName([]), null, 'empty array returns null');
|
|
59
|
+
assert.equal(
|
|
60
|
+
getFirstElementJsxChildTagName([{ type: 'text', content: 'Hello' }]),
|
|
61
|
+
null,
|
|
62
|
+
'text-only returns null (the predicate-side guard)',
|
|
63
|
+
);
|
|
64
|
+
assert.equal(
|
|
65
|
+
getFirstElementJsxChildTagName([{ type: 'element', tagName: 'Italic' }]),
|
|
66
|
+
'Italic',
|
|
67
|
+
'extracts first element tagName',
|
|
68
|
+
);
|
|
69
|
+
assert.equal(
|
|
70
|
+
getFirstElementJsxChildTagName([
|
|
71
|
+
{ type: 'text', content: ' ' },
|
|
72
|
+
{ type: 'element', tagName: 'Bold' },
|
|
73
|
+
{ type: 'element', tagName: 'Italic' },
|
|
74
|
+
]),
|
|
75
|
+
'Bold',
|
|
76
|
+
'extracts FIRST element tagName, skipping leading text',
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// End-to-end cases — minimal stubbed backend + figmaInstance.
|
|
82
|
+
// These are the cases the kind/type bug slipped through earlier: a unit
|
|
83
|
+
// test of the helper passes while the full function silently mis-routes.
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
interface StubBackend {
|
|
87
|
+
enableSymbolMasters: boolean;
|
|
88
|
+
splitClassName: (s: string | undefined) => string[];
|
|
89
|
+
ensureCvaComponentSet: () => unknown;
|
|
90
|
+
getInstantiableComponent: () => StubComponent | null;
|
|
91
|
+
getCvaSelectionFromInstance: () => Record<string, string>;
|
|
92
|
+
toFigmaVariantPropertyName: (k: string) => string;
|
|
93
|
+
toFigmaVariantPropertyValue: (v: string) => string;
|
|
94
|
+
getInstanceTextOverride: () => string | null;
|
|
95
|
+
applyTextOverrideToInstance: () => boolean;
|
|
96
|
+
setGeneratedSymbolDebugData: () => void;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface StubComponent {
|
|
100
|
+
type: 'COMPONENT';
|
|
101
|
+
iconNamePluginData: string;
|
|
102
|
+
getPluginData(key: string): string;
|
|
103
|
+
createInstance(): StubInstance;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
interface StubInstance {
|
|
107
|
+
type: 'INSTANCE';
|
|
108
|
+
setProperties(_p: Record<string, string>): void;
|
|
109
|
+
fills: unknown[];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function makeStubComponent(iconName: string): StubComponent {
|
|
113
|
+
return {
|
|
114
|
+
type: 'COMPONENT',
|
|
115
|
+
iconNamePluginData: iconName,
|
|
116
|
+
getPluginData(key: string): string {
|
|
117
|
+
if (key === MASTER_ICON_NAME_KEY) return this.iconNamePluginData;
|
|
118
|
+
return '';
|
|
119
|
+
},
|
|
120
|
+
createInstance(): StubInstance {
|
|
121
|
+
return {
|
|
122
|
+
type: 'INSTANCE',
|
|
123
|
+
setProperties() { /* noop */ },
|
|
124
|
+
fills: [],
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function makeBackend(master: StubComponent | null): StubBackend {
|
|
131
|
+
return {
|
|
132
|
+
enableSymbolMasters: true,
|
|
133
|
+
splitClassName: (s) => (s ? s.split(/\s+/).filter(Boolean) : []),
|
|
134
|
+
ensureCvaComponentSet: () => ({}),
|
|
135
|
+
getInstantiableComponent: () => master,
|
|
136
|
+
getCvaSelectionFromInstance: () => ({}),
|
|
137
|
+
toFigmaVariantPropertyName: (k) => k,
|
|
138
|
+
toFigmaVariantPropertyValue: (v) => v,
|
|
139
|
+
getInstanceTextOverride: () => null,
|
|
140
|
+
applyTextOverrideToInstance: () => false,
|
|
141
|
+
setGeneratedSymbolDebugData: () => { /* noop */ },
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const cvaDef = {
|
|
146
|
+
type: 'cva',
|
|
147
|
+
name: 'Toggle',
|
|
148
|
+
variants: { variant: ['default', 'outline'], size: ['default'] },
|
|
149
|
+
defaultVariants: { variant: 'default', size: 'default' },
|
|
150
|
+
} as const;
|
|
151
|
+
|
|
152
|
+
// (a) Instance icon matches master icon → tryCreate succeeds (returns InstanceNode).
|
|
153
|
+
{
|
|
154
|
+
const master = makeStubComponent('Bold');
|
|
155
|
+
const result = tryCreateCvaComponentInstance(
|
|
156
|
+
cvaDef as unknown as Parameters<typeof tryCreateCvaComponentInstance>[0],
|
|
157
|
+
{
|
|
158
|
+
componentName: 'Toggle',
|
|
159
|
+
props: {
|
|
160
|
+
__jsxChildren: [{ type: 'element', tagName: 'Bold' }],
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
'default',
|
|
164
|
+
{} as Parameters<typeof tryCreateCvaComponentInstance>[3],
|
|
165
|
+
makeBackend(master) as unknown as Parameters<typeof tryCreateCvaComponentInstance>[4],
|
|
166
|
+
);
|
|
167
|
+
assert.ok(result, 'matching icon must produce a symbol instance, got null');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// (b) Instance icon differs from master icon → tryCreate returns null (fall back).
|
|
171
|
+
{
|
|
172
|
+
const master = makeStubComponent('Bold');
|
|
173
|
+
const result = tryCreateCvaComponentInstance(
|
|
174
|
+
cvaDef as unknown as Parameters<typeof tryCreateCvaComponentInstance>[0],
|
|
175
|
+
{
|
|
176
|
+
componentName: 'Toggle',
|
|
177
|
+
props: {
|
|
178
|
+
__jsxChildren: [{ type: 'element', tagName: 'Italic' }],
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
'default',
|
|
182
|
+
{} as Parameters<typeof tryCreateCvaComponentInstance>[3],
|
|
183
|
+
makeBackend(master) as unknown as Parameters<typeof tryCreateCvaComponentInstance>[4],
|
|
184
|
+
);
|
|
185
|
+
assert.equal(result, null, 'differing icon must trigger fall back (null), got an instance');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// (c) No element children (text only) → fall-back does NOT trigger; the
|
|
189
|
+
// symbol-instance path proceeds even if master has a recorded icon
|
|
190
|
+
// (text-override path handles the content).
|
|
191
|
+
{
|
|
192
|
+
const master = makeStubComponent('Bold');
|
|
193
|
+
const result = tryCreateCvaComponentInstance(
|
|
194
|
+
cvaDef as unknown as Parameters<typeof tryCreateCvaComponentInstance>[0],
|
|
195
|
+
{
|
|
196
|
+
componentName: 'Toggle',
|
|
197
|
+
props: {
|
|
198
|
+
__jsxChildren: [{ type: 'text', content: 'Click me' }],
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
'default',
|
|
202
|
+
{} as Parameters<typeof tryCreateCvaComponentInstance>[3],
|
|
203
|
+
makeBackend(master) as unknown as Parameters<typeof tryCreateCvaComponentInstance>[4],
|
|
204
|
+
);
|
|
205
|
+
assert.ok(result, 'text-only children must NOT trigger fall back, got null');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// (d) Element child present but master has no recorded icon name (older
|
|
209
|
+
// build, or no story-content for this variant) → fall back. Avoids
|
|
210
|
+
// silently picking the wrong icon at instance time.
|
|
211
|
+
{
|
|
212
|
+
const master = makeStubComponent('');
|
|
213
|
+
const result = tryCreateCvaComponentInstance(
|
|
214
|
+
cvaDef as unknown as Parameters<typeof tryCreateCvaComponentInstance>[0],
|
|
215
|
+
{
|
|
216
|
+
componentName: 'Toggle',
|
|
217
|
+
props: {
|
|
218
|
+
__jsxChildren: [{ type: 'element', tagName: 'Bold' }],
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
'default',
|
|
222
|
+
{} as Parameters<typeof tryCreateCvaComponentInstance>[3],
|
|
223
|
+
makeBackend(master) as unknown as Parameters<typeof tryCreateCvaComponentInstance>[4],
|
|
224
|
+
);
|
|
225
|
+
assert.equal(result, null, 'unknown master icon must fall back, got an instance');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// State-overriding props (defaultPressed, defaultChecked, disabled, ...)
|
|
230
|
+
// MUST also trigger fall-back, even when the icon matches. The master was
|
|
231
|
+
// built from one story whose state is baked in — Figma instances can't
|
|
232
|
+
// per-instance-activate the data-[X]:* CSS at render time.
|
|
233
|
+
// ---------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
// cvaInstanceHasOverridingState helper checks
|
|
236
|
+
{
|
|
237
|
+
assert.equal(cvaInstanceHasOverridingState(null), false, 'null props');
|
|
238
|
+
assert.equal(cvaInstanceHasOverridingState({}), false, 'empty props');
|
|
239
|
+
assert.equal(
|
|
240
|
+
cvaInstanceHasOverridingState({ defaultPressed: 'true' }),
|
|
241
|
+
true,
|
|
242
|
+
'defaultPressed="true" triggers',
|
|
243
|
+
);
|
|
244
|
+
assert.equal(
|
|
245
|
+
cvaInstanceHasOverridingState({ defaultPressed: 'false' }),
|
|
246
|
+
false,
|
|
247
|
+
'defaultPressed="false" does NOT trigger — instance not in pressed state',
|
|
248
|
+
);
|
|
249
|
+
assert.equal(
|
|
250
|
+
cvaInstanceHasOverridingState({ defaultPressed: true }),
|
|
251
|
+
true,
|
|
252
|
+
'boolean defaultPressed=true triggers',
|
|
253
|
+
);
|
|
254
|
+
assert.equal(
|
|
255
|
+
cvaInstanceHasOverridingState({ defaultPressed: '' }),
|
|
256
|
+
true,
|
|
257
|
+
'bare attribute (defaultPressed without value) = empty string = truthy',
|
|
258
|
+
);
|
|
259
|
+
assert.equal(
|
|
260
|
+
cvaInstanceHasOverridingState({ disabled: 'true' }),
|
|
261
|
+
true,
|
|
262
|
+
'disabled triggers (visual state baked into master would differ)',
|
|
263
|
+
);
|
|
264
|
+
assert.equal(
|
|
265
|
+
cvaInstanceHasOverridingState({ 'aria-disabled': 'true' }),
|
|
266
|
+
false,
|
|
267
|
+
'aria-disabled alone is metadata — does NOT trigger fall-back',
|
|
268
|
+
);
|
|
269
|
+
assert.equal(
|
|
270
|
+
cvaInstanceHasOverridingState({ defaultChecked: 'true' }),
|
|
271
|
+
true,
|
|
272
|
+
'defaultChecked triggers (checkboxes / radios)',
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// End-to-end: matching icon BUT defaultPressed=true → fall back so the
|
|
277
|
+
// variant engine can activate data-[pressed]:* per-instance at render time.
|
|
278
|
+
{
|
|
279
|
+
const master = makeStubComponent('Bold');
|
|
280
|
+
const result = tryCreateCvaComponentInstance(
|
|
281
|
+
cvaDef as unknown as Parameters<typeof tryCreateCvaComponentInstance>[0],
|
|
282
|
+
{
|
|
283
|
+
componentName: 'Toggle',
|
|
284
|
+
props: {
|
|
285
|
+
__jsxChildren: [{ type: 'element', tagName: 'Bold' }],
|
|
286
|
+
defaultPressed: 'true',
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
'default',
|
|
290
|
+
{} as Parameters<typeof tryCreateCvaComponentInstance>[3],
|
|
291
|
+
makeBackend(master) as unknown as Parameters<typeof tryCreateCvaComponentInstance>[4],
|
|
292
|
+
);
|
|
293
|
+
assert.equal(result, null, 'icon matches BUT defaultPressed=true must still fall back, got an instance');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// And the negative control: icon matches AND no state props → still uses
|
|
297
|
+
// the symbol instance (pure happy path).
|
|
298
|
+
{
|
|
299
|
+
const master = makeStubComponent('Bold');
|
|
300
|
+
const result = tryCreateCvaComponentInstance(
|
|
301
|
+
cvaDef as unknown as Parameters<typeof tryCreateCvaComponentInstance>[0],
|
|
302
|
+
{
|
|
303
|
+
componentName: 'Toggle',
|
|
304
|
+
props: {
|
|
305
|
+
__jsxChildren: [{ type: 'element', tagName: 'Bold' }],
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
'default',
|
|
309
|
+
{} as Parameters<typeof tryCreateCvaComponentInstance>[3],
|
|
310
|
+
makeBackend(master) as unknown as Parameters<typeof tryCreateCvaComponentInstance>[4],
|
|
311
|
+
);
|
|
312
|
+
assert.ok(result, 'icon matches and no state props → symbol instance (regression on (a))');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
console.log('cva-master-icon-regression: PASS (4 helper + 4 e2e + 9 state-helper + 2 state-e2e cases)');
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
|
|
3
|
+
import { expandActiveConditionalVariants } from '../src/design-system/node-variants';
|
|
4
|
+
import type { NodeIR } from '../src/tailwind';
|
|
5
|
+
|
|
6
|
+
// Regression: Tailwind v4 `data-[X]:` conditional variants must resolve
|
|
7
|
+
// when the consumer expresses the state via the JSX prop convention
|
|
8
|
+
// (e.g. `defaultPressed`, `defaultChecked`, `disabled`) rather than a
|
|
9
|
+
// literal `data-X` attribute. Without this, `data-[pressed]:bg-accent`
|
|
10
|
+
// on a `<Toggle defaultPressed>` never activates — the variant engine
|
|
11
|
+
// only sees `defaultPressed="true"`, never an actual `data-pressed`.
|
|
12
|
+
//
|
|
13
|
+
// History: pressed state on the Toggle.Pressed story rendered as default
|
|
14
|
+
// (no `bg-accent`) because the engine only matched literal `data-*`. The
|
|
15
|
+
// fix adds a DATA_ATTR_PROP_ALIASES table inside `getNodePropValue` so a
|
|
16
|
+
// `data-*` lookup falls back to known prop-convention aliases.
|
|
17
|
+
|
|
18
|
+
function makeComponentNode(props: Record<string, string>, classes: string[]): NodeIR {
|
|
19
|
+
return {
|
|
20
|
+
kind: 'component',
|
|
21
|
+
tagName: 'TogglePrimitive',
|
|
22
|
+
tagLower: 'toggleprimitive',
|
|
23
|
+
props,
|
|
24
|
+
classes: classes.slice(),
|
|
25
|
+
children: [],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface Case {
|
|
30
|
+
name: string;
|
|
31
|
+
props: Record<string, string>;
|
|
32
|
+
classes: string[];
|
|
33
|
+
expectedAdded: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const CASES: Case[] = [
|
|
37
|
+
// ---- data-pressed (Toggle/Toggle-group pressed state) -------------------
|
|
38
|
+
{
|
|
39
|
+
name: 'defaultPressed=true activates data-[pressed]:* classes',
|
|
40
|
+
props: { defaultPressed: 'true' },
|
|
41
|
+
classes: ['data-[pressed]:bg-accent', 'data-[pressed]:text-accent-foreground'],
|
|
42
|
+
expectedAdded: ['bg-accent', 'text-accent-foreground'],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'pressed=true (alternate prop name) activates data-[pressed]:*',
|
|
46
|
+
props: { pressed: 'true' },
|
|
47
|
+
classes: ['data-[pressed]:bg-accent'],
|
|
48
|
+
expectedAdded: ['bg-accent'],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: 'defaultPressed=false does NOT activate data-[pressed]:*',
|
|
52
|
+
props: { defaultPressed: 'false' },
|
|
53
|
+
classes: ['data-[pressed]:bg-accent'],
|
|
54
|
+
expectedAdded: [],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'literal data-pressed attribute still works (and takes precedence)',
|
|
58
|
+
props: { 'data-pressed': 'true' },
|
|
59
|
+
classes: ['data-[pressed]:bg-accent'],
|
|
60
|
+
expectedAdded: ['bg-accent'],
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// ---- data-disabled ------------------------------------------------------
|
|
64
|
+
{
|
|
65
|
+
name: 'disabled=true activates data-[disabled]:* classes',
|
|
66
|
+
props: { disabled: 'true' },
|
|
67
|
+
classes: ['data-[disabled]:opacity-50', 'data-[disabled]:pointer-events-none'],
|
|
68
|
+
expectedAdded: ['opacity-50', 'pointer-events-none'],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'aria-disabled=true also activates data-[disabled]:*',
|
|
72
|
+
props: { 'aria-disabled': 'true' },
|
|
73
|
+
classes: ['data-[disabled]:opacity-50'],
|
|
74
|
+
expectedAdded: ['opacity-50'],
|
|
75
|
+
},
|
|
76
|
+
|
|
77
|
+
// ---- data-checked -------------------------------------------------------
|
|
78
|
+
{
|
|
79
|
+
name: 'defaultChecked=true activates data-[checked]:* classes',
|
|
80
|
+
props: { defaultChecked: 'true' },
|
|
81
|
+
classes: ['data-[checked]:bg-primary'],
|
|
82
|
+
expectedAdded: ['bg-primary'],
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// ---- data-open ----------------------------------------------------------
|
|
86
|
+
{
|
|
87
|
+
name: 'defaultOpen=true activates data-[open]:* classes',
|
|
88
|
+
props: { defaultOpen: 'true' },
|
|
89
|
+
classes: ['data-[open]:rotate-180'],
|
|
90
|
+
expectedAdded: ['rotate-180'],
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
// ---- negative control --------------------------------------------------
|
|
94
|
+
{
|
|
95
|
+
name: 'unknown prop alias does NOT leak (e.g. defaultPressed does not activate data-[foo])',
|
|
96
|
+
props: { defaultPressed: 'true' },
|
|
97
|
+
classes: ['data-[foo]:bg-red-500'],
|
|
98
|
+
expectedAdded: [],
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
// ---- bare Tailwind v4 syntax (data-pressed: without brackets) ----------
|
|
102
|
+
{
|
|
103
|
+
name: 'bare `data-pressed:` selector also resolves via the alias',
|
|
104
|
+
props: { defaultPressed: 'true' },
|
|
105
|
+
classes: ['data-pressed:font-bold'],
|
|
106
|
+
expectedAdded: ['font-bold'],
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
let failed = 0;
|
|
111
|
+
for (const c of CASES) {
|
|
112
|
+
const node = makeComponentNode(c.props, c.classes);
|
|
113
|
+
const result = expandActiveConditionalVariants(c.classes, node);
|
|
114
|
+
const addedUtilities = result.filter((cls) => !c.classes.includes(cls));
|
|
115
|
+
const sortedActual = addedUtilities.slice().sort();
|
|
116
|
+
const sortedExpected = c.expectedAdded.slice().sort();
|
|
117
|
+
if (JSON.stringify(sortedActual) !== JSON.stringify(sortedExpected)) {
|
|
118
|
+
console.error(` ✗ ${c.name}`);
|
|
119
|
+
console.error(` expected added: ${JSON.stringify(sortedExpected)}`);
|
|
120
|
+
console.error(` actual added: ${JSON.stringify(sortedActual)}`);
|
|
121
|
+
failed++;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (failed > 0) {
|
|
126
|
+
assert.fail(`${failed}/${CASES.length} data-attr-prop-alias cases failed`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
console.log(`data-attr-prop-alias-regression: PASS (${CASES.length} cases)`);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
|
|
3
|
+
import { getNodeLayoutComputed } from '../src/layout/width-solver';
|
|
4
|
+
import type { NodeIR } from '../src/tailwind';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Regression: at depth=0 (story root) `buildFigmaNode` overrides the frame
|
|
8
|
+
* width with `context.maxWidth` (typically 900 — the page canvas) so that
|
|
9
|
+
* page-level containers fill the canvas. That override must NOT fire when
|
|
10
|
+
* the root element has an explicit width class (size-N / w-N / size-full
|
|
11
|
+
* etc.), or a 40×40 avatar becomes a 900-wide pill.
|
|
12
|
+
*
|
|
13
|
+
* The override is gated on `layoutComputed.hasExplicitSize`. This file
|
|
14
|
+
* locks the predicate: every variant of "explicit size" that downstream
|
|
15
|
+
* authors reasonably reach for must register as `hasExplicitSize=true`,
|
|
16
|
+
* and nothing else must.
|
|
17
|
+
*
|
|
18
|
+
* History: shadcn's `Avatar` is `<AvatarPrimitive.Root className="relative
|
|
19
|
+
* flex size-10 ...">`. `flattenComponentNodes` rewrites compound primitives
|
|
20
|
+
* to `kind: 'element', tagLower: 'div'` so they fall into the generic
|
|
21
|
+
* container path. There, the `depth === 0` width override kicked in and
|
|
22
|
+
* resized the 40×40 root to 900×40. The fix: skip the override when the
|
|
23
|
+
* element class list carries an explicit width. This regression keeps that
|
|
24
|
+
* decision intact.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
function el(classes: string[]): NodeIR {
|
|
28
|
+
return {
|
|
29
|
+
kind: 'element',
|
|
30
|
+
tagName: 'div',
|
|
31
|
+
tagLower: 'div',
|
|
32
|
+
props: {},
|
|
33
|
+
classes,
|
|
34
|
+
children: [],
|
|
35
|
+
} as unknown as NodeIR;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Positive cases — these MUST register as hasExplicitSize.
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
const positives: Array<[string, string[]]> = [
|
|
43
|
+
['size-10 (shadcn Avatar)', ['relative', 'flex', 'size-10', 'shrink-0']],
|
|
44
|
+
['size-8 (small avatar)', ['size-8']],
|
|
45
|
+
['size-12.5 (decimal allowed)', ['size-12.5']],
|
|
46
|
+
['size-[40px] (arbitrary)', ['size-[40px]']],
|
|
47
|
+
['size-full (avatar Image/Fallback)', ['size-full']],
|
|
48
|
+
['w-10 (width only)', ['w-10']],
|
|
49
|
+
['h-10 (height only)', ['h-10']],
|
|
50
|
+
['w-[200px] (arbitrary width)', ['w-[200px]']],
|
|
51
|
+
['h-[64px] (arbitrary height)', ['h-[64px]']],
|
|
52
|
+
['w-full', ['w-full']],
|
|
53
|
+
['h-full', ['h-full']],
|
|
54
|
+
['w-1/2 (fractional)', ['w-1/2']],
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
for (const [label, classes] of positives) {
|
|
58
|
+
const computed = getNodeLayoutComputed(el(classes));
|
|
59
|
+
assert.equal(
|
|
60
|
+
computed.hasExplicitSize,
|
|
61
|
+
true,
|
|
62
|
+
'positive case must register hasExplicitSize=true: ' + label,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Negative cases — must NOT register. A page-level container with only
|
|
68
|
+
// layout / padding / colors needs the depth=0 canvas-width override to
|
|
69
|
+
// fire, otherwise full-page stories would collapse to hug width.
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
const negatives: Array<[string, string[]]> = [
|
|
73
|
+
['empty class list', []],
|
|
74
|
+
['layout only', ['flex', 'flex-col', 'gap-4']],
|
|
75
|
+
['padding only', ['p-6', 'pt-8']],
|
|
76
|
+
['bg + text only', ['bg-card', 'text-foreground']],
|
|
77
|
+
['max-w-* alone (a cap, not a fixed width)', ['max-w-md']],
|
|
78
|
+
['min-w-* alone', ['min-w-0']],
|
|
79
|
+
// size-* token alias like `size-full` is positive, but `size-fit` etc.
|
|
80
|
+
// are not handled by the plugin's sizing parser today — keep them OUT
|
|
81
|
+
// of the predicate until they are wired through so we don't silently
|
|
82
|
+
// suppress the canvas-width override for shapes the renderer can't size.
|
|
83
|
+
['size-fit (not supported as fixed size)', ['size-fit']],
|
|
84
|
+
['size-auto', ['size-auto']],
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
for (const [label, classes] of negatives) {
|
|
88
|
+
const computed = getNodeLayoutComputed(el(classes));
|
|
89
|
+
assert.equal(
|
|
90
|
+
computed.hasExplicitSize,
|
|
91
|
+
false,
|
|
92
|
+
'negative case must register hasExplicitSize=false: ' + label,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
console.log(
|
|
97
|
+
'explicit-size-root-regression: PASS ('
|
|
98
|
+
+ positives.length
|
|
99
|
+
+ ' positive + '
|
|
100
|
+
+ negatives.length
|
|
101
|
+
+ ' negative cases)',
|
|
102
|
+
);
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
|
|
3
|
+
import { extractFontName } from '../src/tokens/tokens';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Regression: `extractFontName` in `src/tokens/tokens.ts` walks a
|
|
7
|
+
* comma-separated font stack and returns the first entry that ISN'T a
|
|
8
|
+
* system keyword / generic family / web-safe fallback / emoji-fallback
|
|
9
|
+
* font. The returned name is what's passed to `figma.loadFontAsync` —
|
|
10
|
+
* a wrong pick (e.g. "Apple Color Emoji" from Tailwind's default sans
|
|
11
|
+
* stack, or "ui-sans-serif" from a stack with no quoted font) throws
|
|
12
|
+
* "couldn't load font" inside the Figma sandbox.
|
|
13
|
+
*
|
|
14
|
+
* Bug that motivated this fixture: Tailwind's default sans-serif
|
|
15
|
+
* value is `ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
|
|
16
|
+
* 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'`. The original
|
|
17
|
+
* keyword set didn't include the emoji-fallback entries, so the resolver
|
|
18
|
+
* skipped past the three generic-family entries and picked "Apple Color
|
|
19
|
+
* Emoji" as the body font. The fix adds the four emoji fallbacks to
|
|
20
|
+
* SYSTEM_FONT_KEYWORDS.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Quoted intended font in a Tailwind-style stack
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
assert.equal(
|
|
28
|
+
extractFontName('"Open Sans", ui-sans-serif, system-ui, sans-serif'),
|
|
29
|
+
'Open Sans',
|
|
30
|
+
'quoted family wins over generic-family fallbacks',
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
assert.equal(
|
|
34
|
+
extractFontName("'Inter', system-ui, sans-serif"),
|
|
35
|
+
'Inter',
|
|
36
|
+
'single-quoted name is unwrapped',
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Tailwind default sans stack — the canonical "emoji bug" case
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
const tailwindDefaultSans =
|
|
44
|
+
"ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'";
|
|
45
|
+
|
|
46
|
+
assert.equal(
|
|
47
|
+
extractFontName(tailwindDefaultSans),
|
|
48
|
+
null,
|
|
49
|
+
'stack of only generic-families + emoji-fallbacks returns null so the caller falls back',
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Variants of the same emoji-fallback names with/without quotes
|
|
53
|
+
assert.equal(extractFontName('Apple Color Emoji'), null);
|
|
54
|
+
assert.equal(extractFontName('"Apple Color Emoji"'), null);
|
|
55
|
+
assert.equal(extractFontName("'Segoe UI Emoji'"), null);
|
|
56
|
+
assert.equal(extractFontName('Noto Color Emoji'), null);
|
|
57
|
+
assert.equal(extractFontName('Segoe UI Symbol'), null);
|
|
58
|
+
|
|
59
|
+
// Case-insensitive: extractFontName lowercases before comparing
|
|
60
|
+
assert.equal(extractFontName('APPLE COLOR EMOJI'), null);
|
|
61
|
+
|
|
62
|
+
// Real-world: intended font wins even with the emoji fallbacks appended
|
|
63
|
+
assert.equal(
|
|
64
|
+
extractFontName(`"Open Sans", ${tailwindDefaultSans}`),
|
|
65
|
+
'Open Sans',
|
|
66
|
+
'quoted intended font wins over the full Tailwind default tail',
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Generic-family / system keywords are skipped
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
assert.equal(extractFontName('ui-sans-serif'), null);
|
|
74
|
+
assert.equal(extractFontName('system-ui'), null);
|
|
75
|
+
assert.equal(extractFontName('sans-serif'), null);
|
|
76
|
+
assert.equal(extractFontName('-apple-system'), null);
|
|
77
|
+
assert.equal(
|
|
78
|
+
extractFontName('Arial, Helvetica, sans-serif'),
|
|
79
|
+
null,
|
|
80
|
+
'web-safe-only stacks return null — the consumer hadn\'t picked a design font',
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// CSS variable references — return the derived name if no intended font
|
|
85
|
+
// is present, but a real font name still wins
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
assert.equal(
|
|
89
|
+
extractFontName('var(--font-open-sans), ui-sans-serif, sans-serif'),
|
|
90
|
+
'Open Sans',
|
|
91
|
+
'var(--font-foo) derives a font name when nothing else matches',
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
assert.equal(
|
|
95
|
+
extractFontName('var(--font-geist-mono), monospace'),
|
|
96
|
+
'Geist Mono',
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
assert.equal(
|
|
100
|
+
extractFontName('"Inter", var(--font-fallback), sans-serif'),
|
|
101
|
+
'Inter',
|
|
102
|
+
'real quoted font beats var() fallback',
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Edge cases
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
assert.equal(extractFontName(''), null);
|
|
110
|
+
assert.equal(extractFontName(undefined), null);
|
|
111
|
+
assert.equal(extractFontName(null), null);
|
|
112
|
+
|
|
113
|
+
console.log('font-family-extract-regression: ok');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
-
import { inferFontWeight, rankStylesForRequest } from '../src/font-style-resolver';
|
|
2
|
+
import { inferFontWeight, rankStylesForRequest } from '../src/text/font-style-resolver';
|
|
3
3
|
|
|
4
4
|
assert.equal(inferFontWeight('Regular'), 400);
|
|
5
5
|
assert.equal(inferFontWeight('Medium'), 500);
|