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
|
@@ -166,20 +166,51 @@ export function resolveSpacing(value: string): number | undefined {
|
|
|
166
166
|
// State Modifiers
|
|
167
167
|
// ============================================================================
|
|
168
168
|
|
|
169
|
-
|
|
169
|
+
/**
|
|
170
|
+
* Modifiers that imply the component has its OWN interactive/aria state
|
|
171
|
+
* (it receives focus, can be disabled directly, etc.). These are the
|
|
172
|
+
* trigger set for state-component classification — if a root className
|
|
173
|
+
* uses any of these, the component participates in a state-variant matrix
|
|
174
|
+
* and the plugin renders it via the state-master path.
|
|
175
|
+
*
|
|
176
|
+
* Deliberately excludes `group-*:` and `peer-*:` modifiers — those are
|
|
177
|
+
* *passive reactions* to a parent's or sibling's state (e.g. Label
|
|
178
|
+
* dimming when its `<input>` peer is `:disabled`). They don't justify
|
|
179
|
+
* promoting the component to a state primitive. A Label that only
|
|
180
|
+
* carries `peer-disabled:opacity-50` should stay `simple`, render its
|
|
181
|
+
* text children normally, and not get hoisted into a state-master that
|
|
182
|
+
* wipes its content.
|
|
183
|
+
*/
|
|
184
|
+
export const OWN_STATE_MODIFIERS = [
|
|
170
185
|
'hover:',
|
|
171
186
|
'focus:',
|
|
172
187
|
'focus-visible:',
|
|
173
188
|
'focus-within:',
|
|
174
189
|
'active:',
|
|
175
190
|
'disabled:',
|
|
191
|
+
'checked:',
|
|
176
192
|
'aria-invalid:',
|
|
177
193
|
'aria-selected:',
|
|
178
194
|
'aria-expanded:',
|
|
179
195
|
'data-[state=open]:',
|
|
180
196
|
'data-[state=checked]:',
|
|
181
197
|
'data-[state=active]:',
|
|
198
|
+
'data-[checked]:',
|
|
182
199
|
'data-[disabled]:',
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* All recognized state modifiers — used during state-grouping after a
|
|
204
|
+
* component has been classified, so passive `group-*` / `peer-*`
|
|
205
|
+
* variants still get their classes captured even though they don't
|
|
206
|
+
* trigger classification on their own.
|
|
207
|
+
*/
|
|
208
|
+
export const STATE_MODIFIERS = [
|
|
209
|
+
...OWN_STATE_MODIFIERS,
|
|
210
|
+
'group-data-[checked]:',
|
|
211
|
+
'group-data-[disabled]:',
|
|
212
|
+
'peer-data-[checked]:',
|
|
213
|
+
'peer-data-[disabled]:',
|
|
183
214
|
'group-hover:',
|
|
184
215
|
'group-focus:',
|
|
185
216
|
'peer-focus:',
|
|
@@ -377,13 +408,13 @@ export function extractPaletteTokens(classes: string[]): Record<string, string>
|
|
|
377
408
|
const resolvePaletteColor = (token: string): string | null => {
|
|
378
409
|
if (!token) return null;
|
|
379
410
|
if (token === 'transparent' || token === 'current' || token === 'inherit') return null;
|
|
380
|
-
const direct = (colors as
|
|
411
|
+
const direct = (colors as Record<string, string | Record<string, string>>)[token];
|
|
381
412
|
if (typeof direct === 'string') return direct;
|
|
382
413
|
const parts = token.split('-');
|
|
383
414
|
if (parts.length < 2) return null;
|
|
384
415
|
const shade = parts[parts.length - 1];
|
|
385
416
|
const name = parts.slice(0, -1).join('-');
|
|
386
|
-
const group = (colors as
|
|
417
|
+
const group = (colors as Record<string, string | Record<string, string>>)[name];
|
|
387
418
|
if (group && typeof group === 'object' && group[shade]) return group[shade];
|
|
388
419
|
return null;
|
|
389
420
|
};
|
|
@@ -506,7 +537,18 @@ export function groupClassesByState(classes: string[]): Record<string, string[]>
|
|
|
506
537
|
if (!groups[stateName]) {
|
|
507
538
|
groups[stateName] = [];
|
|
508
539
|
}
|
|
509
|
-
|
|
540
|
+
// Reconstitute the utility with its opacity suffix when one was
|
|
541
|
+
// parsed off — without this, `focus-visible:ring-ring/50` and
|
|
542
|
+
// `aria-invalid:ring-destructive/20` get stripped to plain
|
|
543
|
+
// `ring-ring` / `ring-destructive` (full opacity), losing the
|
|
544
|
+
// translucent look shadcn uses for soft focus / invalid rings.
|
|
545
|
+
// Symptom: State Matrix focus/error variants render solid bright
|
|
546
|
+
// rings instead of the brand-tinted, semi-transparent look the
|
|
547
|
+
// browser shows.
|
|
548
|
+
const utilityWithOpacity = parsed.opacity != null
|
|
549
|
+
? parsed.utility + '/' + parsed.opacity
|
|
550
|
+
: parsed.utility;
|
|
551
|
+
groups[stateName].push(utilityWithOpacity);
|
|
510
552
|
} else if (!parsed.modifier) {
|
|
511
553
|
groups.default.push(cls);
|
|
512
554
|
} else {
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
|
|
3
|
+
import { resolveTextResizeWidth, type TextResizeFrameInput } from '../src/layout/text-resize-decision';
|
|
4
|
+
|
|
5
|
+
// MATRIX REGRESSION — text-resize-to-parent-content-width decision.
|
|
6
|
+
//
|
|
7
|
+
// This is fix-site (c) of the recurring inline-flex pill bug (see
|
|
8
|
+
// `tools/figma-plugin/.ai/troubleshooting.md`). The decision lives in the
|
|
9
|
+
// ui-builder's frame-with-text branch and gates whether the inserted text
|
|
10
|
+
// node is resized to `(parent.maxWidth - parent.padding)` or left HUG-sized.
|
|
11
|
+
//
|
|
12
|
+
// Before this matrix the playbook explicitly named this site as the
|
|
13
|
+
// "youngest fix and most likely to drift through refactors" — no automated
|
|
14
|
+
// coverage. Extracting the decision into a pure helper made it testable
|
|
15
|
+
// without stubbing the buildFigmaNode pipeline.
|
|
16
|
+
//
|
|
17
|
+
// Skip rules (text stays HUG-sized):
|
|
18
|
+
// - HORIZONTAL + primaryAxisAlignItems=CENTER → numbered-circle case
|
|
19
|
+
// - HORIZONTAL + primaryAxisSizingMode=AUTO (HUG) → inline-flex pill case
|
|
20
|
+
// - contextMaxWidth missing / 0 / negative → no resize to compute
|
|
21
|
+
// - padding consumes the entire maxWidth → no positive width to resize to
|
|
22
|
+
|
|
23
|
+
function makeFrame(overrides: Partial<TextResizeFrameInput> = {}): TextResizeFrameInput {
|
|
24
|
+
return {
|
|
25
|
+
layoutMode: 'NONE',
|
|
26
|
+
primaryAxisAlignItems: 'MIN',
|
|
27
|
+
primaryAxisSizingMode: 'AUTO',
|
|
28
|
+
paddingLeft: 0,
|
|
29
|
+
paddingRight: 0,
|
|
30
|
+
...overrides,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface Cell {
|
|
35
|
+
frame: Partial<TextResizeFrameInput>;
|
|
36
|
+
contextMaxWidth: number | undefined | null;
|
|
37
|
+
expect: number | undefined;
|
|
38
|
+
note: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const CELLS: Cell[] = [
|
|
42
|
+
// ---- resize-allowed cells ----------------------------------------------
|
|
43
|
+
{
|
|
44
|
+
frame: { layoutMode: 'NONE' },
|
|
45
|
+
contextMaxWidth: 600,
|
|
46
|
+
expect: 600,
|
|
47
|
+
note: 'NONE parent: resize text to context max width',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
frame: { layoutMode: 'NONE', paddingLeft: 16, paddingRight: 16 },
|
|
51
|
+
contextMaxWidth: 600,
|
|
52
|
+
expect: 568,
|
|
53
|
+
note: 'padding subtracted from target width',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
frame: { layoutMode: 'VERTICAL' },
|
|
57
|
+
contextMaxWidth: 600,
|
|
58
|
+
expect: 600,
|
|
59
|
+
note: 'VERTICAL parent: resize text — the common block-flow case',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
frame: { layoutMode: 'VERTICAL', primaryAxisSizingMode: 'FIXED' },
|
|
63
|
+
contextMaxWidth: 600,
|
|
64
|
+
expect: 600,
|
|
65
|
+
note: 'VERTICAL + FIXED-primary still resizes (sizing mode only affects HORIZONTAL skip)',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
frame: { layoutMode: 'HORIZONTAL', primaryAxisSizingMode: 'FIXED' },
|
|
69
|
+
contextMaxWidth: 600,
|
|
70
|
+
expect: 600,
|
|
71
|
+
note: 'HORIZONTAL + FIXED-primary (not HUG): resize allowed — only HORIZONTAL+HUG is the pill case',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
frame: {
|
|
75
|
+
layoutMode: 'HORIZONTAL',
|
|
76
|
+
primaryAxisAlignItems: 'SPACE_BETWEEN',
|
|
77
|
+
primaryAxisSizingMode: 'FIXED',
|
|
78
|
+
},
|
|
79
|
+
contextMaxWidth: 600,
|
|
80
|
+
expect: 600,
|
|
81
|
+
note: 'SPACE_BETWEEN is not CENTER; resize allowed',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
frame: { layoutMode: 'GRID' },
|
|
85
|
+
contextMaxWidth: 600,
|
|
86
|
+
expect: 600,
|
|
87
|
+
note: 'GRID parent: not HORIZONTAL, so no HUG/CENTER skip applies — resize allowed',
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
// ---- skip cells (the bugs that motivated the helper) -------------------
|
|
91
|
+
{
|
|
92
|
+
frame: { layoutMode: 'HORIZONTAL', primaryAxisSizingMode: 'AUTO' },
|
|
93
|
+
contextMaxWidth: 600,
|
|
94
|
+
expect: undefined,
|
|
95
|
+
note: 'HORIZONTAL + AUTO = HUG: inline-flex pill case (fix-site c) — must NOT resize',
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
frame: {
|
|
99
|
+
layoutMode: 'HORIZONTAL',
|
|
100
|
+
primaryAxisAlignItems: 'CENTER',
|
|
101
|
+
primaryAxisSizingMode: 'AUTO',
|
|
102
|
+
},
|
|
103
|
+
contextMaxWidth: 600,
|
|
104
|
+
expect: undefined,
|
|
105
|
+
note: 'HORIZONTAL + CENTER: numbered-circle case — must NOT resize',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
frame: {
|
|
109
|
+
layoutMode: 'HORIZONTAL',
|
|
110
|
+
primaryAxisAlignItems: 'CENTER',
|
|
111
|
+
primaryAxisSizingMode: 'FIXED',
|
|
112
|
+
},
|
|
113
|
+
contextMaxWidth: 600,
|
|
114
|
+
expect: undefined,
|
|
115
|
+
note: 'HORIZONTAL + CENTER + FIXED: still CENTER skip applies — must NOT resize',
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
// ---- edge cases --------------------------------------------------------
|
|
119
|
+
{
|
|
120
|
+
frame: { layoutMode: 'NONE' },
|
|
121
|
+
contextMaxWidth: undefined,
|
|
122
|
+
expect: undefined,
|
|
123
|
+
note: 'missing context maxWidth: no resize',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
frame: { layoutMode: 'NONE' },
|
|
127
|
+
contextMaxWidth: null,
|
|
128
|
+
expect: undefined,
|
|
129
|
+
note: 'null context maxWidth: no resize',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
frame: { layoutMode: 'NONE' },
|
|
133
|
+
contextMaxWidth: 0,
|
|
134
|
+
expect: undefined,
|
|
135
|
+
note: 'zero context maxWidth: no resize',
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
frame: { layoutMode: 'NONE', paddingLeft: 400, paddingRight: 400 },
|
|
139
|
+
contextMaxWidth: 600,
|
|
140
|
+
expect: undefined,
|
|
141
|
+
note: 'padding > maxWidth: target would be negative — no resize (no degenerate textNode.resize(0))',
|
|
142
|
+
},
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
function runRegression(): void {
|
|
146
|
+
const failures: string[] = [];
|
|
147
|
+
|
|
148
|
+
for (let i = 0; i < CELLS.length; i++) {
|
|
149
|
+
const cell = CELLS[i]!;
|
|
150
|
+
const frame = makeFrame(cell.frame);
|
|
151
|
+
const result = resolveTextResizeWidth(frame, cell.contextMaxWidth);
|
|
152
|
+
if (result !== cell.expect) {
|
|
153
|
+
failures.push(
|
|
154
|
+
`cell ${i}: expected ${cell.expect}, got ${result}\n → ${cell.note}\n → frame=${JSON.stringify(cell.frame)} ctxMaxWidth=${cell.contextMaxWidth}`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (failures.length > 0) {
|
|
160
|
+
for (const msg of failures) console.error(' ✗ ' + msg);
|
|
161
|
+
assert.fail(`${failures.length}/${CELLS.length} text-resize matrix cells failed`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
console.log(`text-resize-matrix-regression: PASS (${CELLS.length} cells)`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
runRegression();
|
|
169
|
+
} catch (err) {
|
|
170
|
+
console.error('text-resize-matrix-regression: FAIL');
|
|
171
|
+
console.error(err);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict';
|
|
|
2
2
|
import {
|
|
3
3
|
centerPlacedRotationTransform,
|
|
4
4
|
rotationTransformAroundPointRadians,
|
|
5
|
-
} from '../src/transform-math';
|
|
5
|
+
} from '../src/tailwind/transform-math';
|
|
6
6
|
|
|
7
7
|
function approxEqual(actual: number, expected: number, epsilon = 0.0001): void {
|
|
8
8
|
assert.ok(Math.abs(actual - expected) <= epsilon, `expected ${expected}, got ${actual}`);
|
package/scanner/types.ts
CHANGED
|
@@ -41,13 +41,23 @@ export interface StoryInfo {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
|
-
* A single component usage found in a story (flat extraction)
|
|
44
|
+
* A single component usage found in a story (flat extraction).
|
|
45
|
+
*
|
|
46
|
+
* `props` is a string map for ordinary attributes (`variant="default"`,
|
|
47
|
+
* `size="sm"`, `className="..."`) but the scanner also stuffs a special
|
|
48
|
+
* `__jsxChildren` key with a `JsxNode[]` payload when the component has
|
|
49
|
+
* structural children that need to be re-rendered (e.g. a `<DialogTrigger>`
|
|
50
|
+
* inside `<Dialog>`). Consumers either treat props as the string map or
|
|
51
|
+
* read `__jsxChildren` after a runtime `Array.isArray` check.
|
|
45
52
|
*/
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- props mixes string attrs with __jsxChildren: JsxNode[]
|
|
54
|
+
export type ComponentInstanceProps = Record<string, any>;
|
|
55
|
+
|
|
46
56
|
export interface ComponentInstance {
|
|
47
57
|
/** Component tag name (e.g., "Button", "Card") */
|
|
48
58
|
componentName: string;
|
|
49
59
|
/** Props on the component (e.g., { variant: "default", size: "sm" }) */
|
|
50
|
-
props:
|
|
60
|
+
props: ComponentInstanceProps;
|
|
51
61
|
/** Text content children */
|
|
52
62
|
children?: string;
|
|
53
63
|
}
|
|
@@ -295,4 +305,18 @@ export interface EnrichedComponentAnalysis {
|
|
|
295
305
|
responsive: ResponsiveInfo;
|
|
296
306
|
/** Light/dark mode class split */
|
|
297
307
|
colorScheme: ColorSchemeInfo;
|
|
308
|
+
/** Atomic design classification inferred from component graph + structure */
|
|
309
|
+
kind?: AtomicKind;
|
|
310
|
+
/** Number of unique components this component references in stories */
|
|
311
|
+
usesCount?: number;
|
|
312
|
+
/** Number of unique components that reference this component in stories */
|
|
313
|
+
usedByCount?: number;
|
|
314
|
+
/** True when this component references no other component */
|
|
315
|
+
isLeaf?: boolean;
|
|
316
|
+
/** Whether this component should be emitted as a reusable Figma symbol candidate */
|
|
317
|
+
symbolCandidate?: boolean;
|
|
318
|
+
/** Precomputed safe text override prop keys for non-CVA/non-compound symbol inference */
|
|
319
|
+
safeTextOverrideProps?: string[];
|
|
298
320
|
}
|
|
321
|
+
|
|
322
|
+
export type AtomicKind = 'atom' | 'molecule' | 'organism' | 'utility' | 'other';
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* frame-cache.ts — Incremental re-render helpers
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
* The Design System page used to be rebuilt from scratch on every plugin run:
|
|
6
|
+
* every node was deleted and recreated. This destroyed Figma node IDs, which
|
|
7
|
+
* meant any designer annotations, comments, or manual repositioning attached
|
|
8
|
+
* to component frames were lost.
|
|
9
|
+
*
|
|
10
|
+
* ## How it works
|
|
11
|
+
* Each component block frame stores a content hash in Figma plugin data
|
|
12
|
+
* (key: "inkbridge:hash"). On the next run we compare the stored hash against
|
|
13
|
+
* a freshly computed one. If they match, the frame is left completely untouched
|
|
14
|
+
* — its node ID, position, and any attached annotations survive. Only frames
|
|
15
|
+
* whose hash changed (or that are newly added) are removed and rebuilt.
|
|
16
|
+
*
|
|
17
|
+
* ## Hash strategy
|
|
18
|
+
* Two inputs are combined per component block:
|
|
19
|
+
* defHash — hash of the component definition (name, type, stories, classes)
|
|
20
|
+
* tokenHash — hash of the global TOKENS object (covers all themes)
|
|
21
|
+
*
|
|
22
|
+
* blockHash = defHash + ":" + tokenHash
|
|
23
|
+
*
|
|
24
|
+
* If only tokens changed, all component blocks are rebuilt (they carry colour
|
|
25
|
+
* and radius information). If only one component's code changed, only that
|
|
26
|
+
* component's block is rebuilt. Everything else is preserved.
|
|
27
|
+
*
|
|
28
|
+
* ## Hash algorithm
|
|
29
|
+
* djb2 — simple, deterministic, no crypto API needed in the Figma sandbox.
|
|
30
|
+
* Output is a base-36 string (~7 chars), compact and alphanumeric.
|
|
31
|
+
*
|
|
32
|
+
* ## Plugin data key
|
|
33
|
+
* "inkbridge:hash" is written to every component block frame and to the
|
|
34
|
+
* Design Tokens row. No other Figma data is modified.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const PLUGIN_HASH_KEY = 'inkbridge:hash';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* djb2 string hash. Works in Figma plugin sandbox (no crypto API required).
|
|
41
|
+
* Returns a base-36 string.
|
|
42
|
+
*/
|
|
43
|
+
export function hashString(s: string): string {
|
|
44
|
+
let h = 5381;
|
|
45
|
+
for (let i = 0; i < s.length; i++) {
|
|
46
|
+
h = (((h << 5) + h) ^ s.charCodeAt(i)) >>> 0;
|
|
47
|
+
}
|
|
48
|
+
return h.toString(36);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Stable JSON stringify with recursive key sorting.
|
|
53
|
+
* Prevents hash churn when equivalent objects are created with different key order.
|
|
54
|
+
*/
|
|
55
|
+
export function stableStringify(value: unknown): string {
|
|
56
|
+
if (value === null || value === undefined) return 'null';
|
|
57
|
+
const t = typeof value;
|
|
58
|
+
if (t === 'string' || t === 'number' || t === 'boolean') return JSON.stringify(value);
|
|
59
|
+
if (Array.isArray(value)) {
|
|
60
|
+
return '[' + value.map(stableStringify).join(',') + ']';
|
|
61
|
+
}
|
|
62
|
+
if (t === 'object') {
|
|
63
|
+
const obj = value as Record<string, unknown>;
|
|
64
|
+
const keys = Object.keys(obj).sort();
|
|
65
|
+
const parts: string[] = [];
|
|
66
|
+
for (let i = 0; i < keys.length; i++) {
|
|
67
|
+
const k = keys[i];
|
|
68
|
+
const v = obj[k];
|
|
69
|
+
if (typeof v === 'undefined') continue;
|
|
70
|
+
parts.push(JSON.stringify(k) + ':' + stableStringify(v));
|
|
71
|
+
}
|
|
72
|
+
return '{' + parts.join(',') + '}';
|
|
73
|
+
}
|
|
74
|
+
return JSON.stringify(String(value));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Compute a stable hash for a component definition.
|
|
79
|
+
*
|
|
80
|
+
* Only fields that affect rendering are included. Transient scanner metadata
|
|
81
|
+
* (generatedAt, schemaVersion, file path) is excluded so a re-scan that
|
|
82
|
+
* produces identical components does not trigger unnecessary rebuilds.
|
|
83
|
+
*/
|
|
84
|
+
export function hashDef(def: Record<string, unknown>): string {
|
|
85
|
+
const stable = {
|
|
86
|
+
name: def.name,
|
|
87
|
+
type: def.type,
|
|
88
|
+
stories: def.stories,
|
|
89
|
+
classes: def.classes,
|
|
90
|
+
baseClasses: def.baseClasses,
|
|
91
|
+
variants: def.variants,
|
|
92
|
+
states: def.states,
|
|
93
|
+
subComponents: def.subComponents,
|
|
94
|
+
};
|
|
95
|
+
// Use stableStringify so nested object key-order differences from scanner
|
|
96
|
+
// output do not trigger false-positive "changed" component hashes.
|
|
97
|
+
return hashString(stableStringify(stable));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Read the stored content hash from a Figma frame's plugin data.
|
|
102
|
+
* Returns null if no hash has been stored (e.g. frame was built before
|
|
103
|
+
* incremental re-render was introduced, or plugin data is unavailable).
|
|
104
|
+
*/
|
|
105
|
+
export function getFrameHash(frame: BaseNode): string | null {
|
|
106
|
+
try {
|
|
107
|
+
const v = frame.getPluginData(PLUGIN_HASH_KEY);
|
|
108
|
+
return v || null;
|
|
109
|
+
} catch (_e) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Store a content hash on a Figma frame via plugin data.
|
|
116
|
+
* Silently ignored if the node type does not support plugin data.
|
|
117
|
+
*/
|
|
118
|
+
export function setFrameHash(frame: BaseNode, hash: string): void {
|
|
119
|
+
try {
|
|
120
|
+
frame.setPluginData(PLUGIN_HASH_KEY, hash);
|
|
121
|
+
} catch (_e) {
|
|
122
|
+
// Some node types (e.g. text nodes) do not support plugin data — ignore.
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Find the first direct child of `parent` whose `.name` matches `name`.
|
|
128
|
+
* Returns null if not found or if parent has no children.
|
|
129
|
+
*/
|
|
130
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
131
|
+
export function findChildByName(parent: any, name: string): any | null {
|
|
132
|
+
if (!parent || !parent.children) return null;
|
|
133
|
+
for (let i = 0; i < parent.children.length; i++) {
|
|
134
|
+
if (parent.children[i].name === name) return parent.children[i];
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Find the index of the first direct child of `parent` whose `.name` matches
|
|
141
|
+
* `name`. Returns -1 if not found.
|
|
142
|
+
*/
|
|
143
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
144
|
+
export function findChildIndexByName(parent: any, name: string): number {
|
|
145
|
+
if (!parent || !parent.children) return -1;
|
|
146
|
+
for (let i = 0; i < parent.children.length; i++) {
|
|
147
|
+
if (parent.children[i].name === name) return i;
|
|
148
|
+
}
|
|
149
|
+
return -1;
|
|
150
|
+
}
|
|
@@ -1,12 +1,27 @@
|
|
|
1
|
-
import { COMPONENT_DEFS } from '
|
|
1
|
+
import { COMPONENT_DEFS } from '../tokens';
|
|
2
|
+
import type {
|
|
3
|
+
ComponentAnalysis,
|
|
4
|
+
CVAComponentAnalysis,
|
|
5
|
+
CompoundComponentAnalysis,
|
|
6
|
+
EnrichedComponentAnalysis,
|
|
7
|
+
StateComponentAnalysis,
|
|
8
|
+
SubComponentInfo,
|
|
9
|
+
} from '../../scanner/types';
|
|
2
10
|
|
|
3
11
|
// --- Component Definition Helpers ---
|
|
4
12
|
// These helpers let the plugin dynamically read from scanned component definitions
|
|
5
13
|
|
|
6
|
-
|
|
14
|
+
type RawEntry = EnrichedComponentAnalysis | ComponentAnalysis;
|
|
15
|
+
|
|
16
|
+
function unwrap(raw: RawEntry): ComponentAnalysis {
|
|
17
|
+
return 'analysis' in raw ? raw.analysis : raw;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function getComponentDef(name: string): ComponentAnalysis | null {
|
|
7
21
|
if (!COMPONENT_DEFS || !COMPONENT_DEFS.components) return null;
|
|
8
|
-
for (const raw of COMPONENT_DEFS.components) {
|
|
9
|
-
|
|
22
|
+
for (const raw of COMPONENT_DEFS.components as RawEntry[]) {
|
|
23
|
+
if (!raw) continue;
|
|
24
|
+
const def = unwrap(raw);
|
|
10
25
|
if (def && def.name && def.name.toLowerCase() === name.toLowerCase()) {
|
|
11
26
|
return def;
|
|
12
27
|
}
|
|
@@ -14,7 +29,7 @@ export function getComponentDef(name: string): any | null {
|
|
|
14
29
|
return null;
|
|
15
30
|
}
|
|
16
31
|
|
|
17
|
-
export function getCVAComponentVariants(name: string):
|
|
32
|
+
export function getCVAComponentVariants(name: string): CVAComponentAnalysis['variants'] | null {
|
|
18
33
|
const def = getComponentDef(name);
|
|
19
34
|
if (!def || def.type !== 'cva') return null;
|
|
20
35
|
return def.variants;
|
|
@@ -36,19 +51,19 @@ export function getCVAComponentClasses(name: string, variantName?: string, varia
|
|
|
36
51
|
export function getStateComponentStates(name: string): string[] | null {
|
|
37
52
|
const def = getComponentDef(name);
|
|
38
53
|
if (!def || def.type !== 'state') return null;
|
|
39
|
-
return Object.keys(def.states);
|
|
54
|
+
return Object.keys((def as StateComponentAnalysis).states);
|
|
40
55
|
}
|
|
41
56
|
|
|
42
|
-
export function getCompoundSubComponents(name: string):
|
|
57
|
+
export function getCompoundSubComponents(name: string): SubComponentInfo[] | null {
|
|
43
58
|
const def = getComponentDef(name);
|
|
44
59
|
if (!def || def.type !== 'compound') return null;
|
|
45
|
-
return def.subComponents;
|
|
60
|
+
return (def as CompoundComponentAnalysis).subComponents;
|
|
46
61
|
}
|
|
47
62
|
|
|
48
63
|
export function listAllComponents(): { name: string; type: string }[] {
|
|
49
64
|
if (!COMPONENT_DEFS || !COMPONENT_DEFS.components) return [];
|
|
50
|
-
return COMPONENT_DEFS.components.map((raw
|
|
51
|
-
const def = raw
|
|
65
|
+
return (COMPONENT_DEFS.components as RawEntry[]).map((raw) => {
|
|
66
|
+
const def = unwrap(raw);
|
|
52
67
|
return { name: def?.name, type: def?.type };
|
|
53
68
|
});
|
|
54
69
|
}
|