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,480 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
|
|
3
|
+
import { applyShadcnAdapter, getShadcnSlotInjections } from '../src/framework-adapters/shadcn';
|
|
4
|
+
import type { NodeIR } from '../src/tailwind/node-ir';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Regression: `src/framework-adapters/shadcn.ts` is the single source of
|
|
8
|
+
* truth for class-list patches that translate shadcn / base-ui runtime
|
|
9
|
+
* CSS into Tailwind classes the plugin can see — patches the consumer's
|
|
10
|
+
* JSX *should* have had if those frameworks didn't use a stylesheet to
|
|
11
|
+
* do half the layout.
|
|
12
|
+
*
|
|
13
|
+
* This file locks the contract:
|
|
14
|
+
* 1. The registry has the slots we expect (so a casual edit can't
|
|
15
|
+
* silently delete `avatar-image`'s `absolute inset-0` patch).
|
|
16
|
+
* 2. `applyShadcnAdapter` actually injects them onto the matching node.
|
|
17
|
+
* 3. Non-matching nodes are returned by reference (cache-friendly).
|
|
18
|
+
* 4. Recursion reaches into nested children.
|
|
19
|
+
* 5. Existing classes are preserved; duplicates are de-duped.
|
|
20
|
+
*
|
|
21
|
+
* Adding a new slot? Add an entry here AND in
|
|
22
|
+
* `src/framework-adapters/shadcn.ts:SLOT_CLASS_INJECTIONS` AND a brief
|
|
23
|
+
* note in `tools/figma-plugin/.ai/framework-adapters.md`. All three
|
|
24
|
+
* together — registry, test, doc — keep this from drifting.
|
|
25
|
+
*
|
|
26
|
+
* Adding a whole new framework adapter (MUI, Headless UI, ...) is a new
|
|
27
|
+
* file under `src/framework-adapters/`, one line in the dispatcher
|
|
28
|
+
* `index.ts`, and a sibling regression file mirroring this one.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
function el(props: Record<string, string>, classes: string[] = [], children: NodeIR[] = []): NodeIR {
|
|
32
|
+
return {
|
|
33
|
+
kind: 'element',
|
|
34
|
+
tagName: 'div',
|
|
35
|
+
tagLower: 'div',
|
|
36
|
+
props,
|
|
37
|
+
classes,
|
|
38
|
+
children,
|
|
39
|
+
} as unknown as NodeIR;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Registry contents
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
// avatar-image: base-ui pins `position: absolute; inset: 0` via stylesheet.
|
|
47
|
+
// We can't see CSS, so we re-add the equivalent Tailwind classes here.
|
|
48
|
+
{
|
|
49
|
+
const extras = getShadcnSlotInjections('avatar-image');
|
|
50
|
+
assert.ok(extras, 'avatar-image slot must be registered');
|
|
51
|
+
assert.deepEqual(
|
|
52
|
+
[...(extras as readonly string[])],
|
|
53
|
+
['absolute', 'inset-0'],
|
|
54
|
+
'avatar-image injection must be ["absolute", "inset-0"]',
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Unknown slots: null (so the recursion is a no-op).
|
|
59
|
+
assert.equal(getShadcnSlotInjections('not-a-real-slot'), null, 'unknown slot returns null');
|
|
60
|
+
assert.equal(getShadcnSlotInjections(''), null, 'empty slot returns null');
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// applyShadcnAdapter — class injection on the matching node
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
// (a) Matching slot: classes get the registered patches appended.
|
|
67
|
+
{
|
|
68
|
+
const before = el({ 'data-slot': 'avatar-image', src: '/x.svg' }, ['aspect-square', 'size-full']);
|
|
69
|
+
const after = applyShadcnAdapter(before);
|
|
70
|
+
assert.notEqual(after, before, 'matching slot returns a new node (immutable update)');
|
|
71
|
+
const afterEl = after as Extract<NodeIR, { kind: 'element' }>;
|
|
72
|
+
assert.deepEqual(
|
|
73
|
+
afterEl.classes,
|
|
74
|
+
['aspect-square', 'size-full', 'absolute', 'inset-0'],
|
|
75
|
+
'existing classes preserved; injection appended in order',
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// (b) Already has the classes: no duplication, same-reference return.
|
|
80
|
+
{
|
|
81
|
+
const before = el(
|
|
82
|
+
{ 'data-slot': 'avatar-image', src: '/x.svg' },
|
|
83
|
+
['absolute', 'inset-0', 'size-full'],
|
|
84
|
+
);
|
|
85
|
+
const after = applyShadcnAdapter(before);
|
|
86
|
+
assert.equal(
|
|
87
|
+
after,
|
|
88
|
+
before,
|
|
89
|
+
'when all injection classes are already present, return same reference (cache-friendly)',
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// (c) Element without data-slot: untouched.
|
|
94
|
+
{
|
|
95
|
+
const before = el({}, ['flex', 'gap-4']);
|
|
96
|
+
const after = applyShadcnAdapter(before);
|
|
97
|
+
assert.equal(after, before, 'no data-slot → same reference');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// (d) Element with unknown data-slot: untouched.
|
|
101
|
+
{
|
|
102
|
+
const before = el({ 'data-slot': 'unknown-slot' }, ['flex']);
|
|
103
|
+
const after = applyShadcnAdapter(before);
|
|
104
|
+
assert.equal(after, before, 'unrecognised slot → same reference');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Recursion — reaches into children, returns same ref when nothing changes
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
// (e) Avatar tree with a real image src: Image gets the injection, Fallback is
|
|
112
|
+
// DROPPED (shadcn's runtime hides Fallback when Image loads — we can't observe
|
|
113
|
+
// load state but we can observe "there is a real-looking src").
|
|
114
|
+
{
|
|
115
|
+
const image = el({ 'data-slot': 'avatar-image', src: '/x.svg' }, ['aspect-square', 'size-full']);
|
|
116
|
+
const fallback = el({ 'data-slot': 'avatar-fallback' }, ['flex', 'size-full']);
|
|
117
|
+
const root = el(
|
|
118
|
+
{ 'data-slot': 'avatar' },
|
|
119
|
+
['relative', 'flex', 'size-10', 'rounded-full'],
|
|
120
|
+
[image, fallback],
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
const next = applyShadcnAdapter(root);
|
|
124
|
+
assert.notEqual(next, root, 'tree with a child injection / fallback drop produces a new root');
|
|
125
|
+
|
|
126
|
+
const nextRoot = next as Extract<NodeIR, { kind: 'element' }>;
|
|
127
|
+
const rootEl = root as Extract<NodeIR, { kind: 'element' }>;
|
|
128
|
+
// Root's own classes unchanged (avatar slot itself has no injection).
|
|
129
|
+
assert.deepEqual(
|
|
130
|
+
nextRoot.classes,
|
|
131
|
+
rootEl.classes,
|
|
132
|
+
'avatar-root classes unchanged (no slot registration for it)',
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// Fallback dropped — only the image remains.
|
|
136
|
+
assert.equal(
|
|
137
|
+
nextRoot.children.length,
|
|
138
|
+
1,
|
|
139
|
+
'avatar-fallback dropped when avatar-image has a real src — only image remains',
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
// Image child has injection.
|
|
143
|
+
const nextImage = nextRoot.children[0] as Extract<NodeIR, { kind: 'element' }>;
|
|
144
|
+
assert.deepEqual(
|
|
145
|
+
nextImage.classes,
|
|
146
|
+
['aspect-square', 'size-full', 'absolute', 'inset-0'],
|
|
147
|
+
'avatar-image child receives [absolute, inset-0] injection',
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// (e2) Avatar tree WITHOUT an image (or image without src): Fallback survives.
|
|
152
|
+
{
|
|
153
|
+
const fallback = el({ 'data-slot': 'avatar-fallback' }, ['flex', 'size-full']);
|
|
154
|
+
const root = el(
|
|
155
|
+
{ 'data-slot': 'avatar' },
|
|
156
|
+
['relative', 'flex', 'size-10', 'rounded-full'],
|
|
157
|
+
[fallback],
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const next = applyShadcnAdapter(root);
|
|
161
|
+
assert.equal(next, root, 'no avatar-image → fallback kept, tree returned by reference');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// (e3) Avatar tree with an image whose src is just a placeholder string: Fallback survives.
|
|
165
|
+
// This guards against false-positive drops when the consumer JSX has
|
|
166
|
+
// `src={undefined}` (some bundlers emit the literal "src" token) or
|
|
167
|
+
// `src=""`. The looksLikeImageSrc predicate must agree with the renderer's.
|
|
168
|
+
{
|
|
169
|
+
const image = el({ 'data-slot': 'avatar-image', src: '' }, ['aspect-square', 'size-full']);
|
|
170
|
+
const fallback = el({ 'data-slot': 'avatar-fallback' }, ['flex', 'size-full']);
|
|
171
|
+
const root = el(
|
|
172
|
+
{ 'data-slot': 'avatar' },
|
|
173
|
+
['relative', 'flex', 'size-10', 'rounded-full'],
|
|
174
|
+
[image, fallback],
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const next = applyShadcnAdapter(root);
|
|
178
|
+
const nextRoot = next as Extract<NodeIR, { kind: 'element' }>;
|
|
179
|
+
// Image still gets its class injection.
|
|
180
|
+
// Fallback survives because the image src isn't a fetchable path.
|
|
181
|
+
assert.equal(nextRoot.children.length, 2, 'empty src → fallback survives');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// (f) Tree with no matching slots anywhere: same reference all the way down.
|
|
185
|
+
{
|
|
186
|
+
const root = el({}, ['flex'], [el({}, ['gap-4']), el({}, ['p-4'])]);
|
|
187
|
+
const next = applyShadcnAdapter(root);
|
|
188
|
+
assert.equal(next, root, 'tree with no matches → same reference (no allocation)');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Progress: indicator width derived from Root's `value` prop
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
function findIndicator(root: NodeIR): Extract<NodeIR, { kind: 'element' }> | null {
|
|
196
|
+
const elNode = root as Extract<NodeIR, { kind: 'element' }>;
|
|
197
|
+
if (elNode.props && elNode.props['data-slot'] === 'progress-indicator') {
|
|
198
|
+
return elNode;
|
|
199
|
+
}
|
|
200
|
+
if (!('children' in elNode) || !Array.isArray(elNode.children)) return null;
|
|
201
|
+
for (const c of elNode.children) {
|
|
202
|
+
const r = findIndicator(c);
|
|
203
|
+
if (r) return r;
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function progressTree(value: string | undefined): NodeIR {
|
|
209
|
+
const props: Record<string, string> = { 'data-slot': 'progress' };
|
|
210
|
+
if (value !== undefined) props.value = value;
|
|
211
|
+
const indicator = el(
|
|
212
|
+
{ 'data-slot': 'progress-indicator' },
|
|
213
|
+
['h-full', 'bg-primary', 'transition-all'],
|
|
214
|
+
);
|
|
215
|
+
const track = el(
|
|
216
|
+
{ 'data-slot': 'progress-track' },
|
|
217
|
+
['relative', 'h-2', 'w-full', 'overflow-hidden', 'rounded-full', 'bg-primary/20'],
|
|
218
|
+
[indicator],
|
|
219
|
+
);
|
|
220
|
+
return el(props, ['flex', 'w-full', 'flex-col', 'gap-2'], [track]);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// (g) value="60" → indicator gets w-[60%]
|
|
224
|
+
{
|
|
225
|
+
const root = progressTree('60');
|
|
226
|
+
const next = applyShadcnAdapter(root);
|
|
227
|
+
const indicator = findIndicator(next);
|
|
228
|
+
assert.ok(indicator, 'progress-indicator must still exist after adapter');
|
|
229
|
+
assert.ok(
|
|
230
|
+
indicator!.classes.includes('w-[60%]'),
|
|
231
|
+
'value="60" → w-[60%] injected on progress-indicator',
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// (g2) value="100" → w-[100%]
|
|
236
|
+
{
|
|
237
|
+
const root = progressTree('100');
|
|
238
|
+
const indicator = findIndicator(applyShadcnAdapter(root));
|
|
239
|
+
assert.ok(
|
|
240
|
+
indicator!.classes.includes('w-[100%]'),
|
|
241
|
+
'value="100" → w-[100%] (Complete state)',
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// (g3) value="0" → w-[0%]
|
|
246
|
+
{
|
|
247
|
+
const root = progressTree('0');
|
|
248
|
+
const indicator = findIndicator(applyShadcnAdapter(root));
|
|
249
|
+
assert.ok(
|
|
250
|
+
indicator!.classes.includes('w-[0%]'),
|
|
251
|
+
'value="0" → w-[0%] (Empty state)',
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// (g4) value undefined / null → indeterminate 33%
|
|
256
|
+
{
|
|
257
|
+
const root = progressTree(undefined);
|
|
258
|
+
const indicator = findIndicator(applyShadcnAdapter(root));
|
|
259
|
+
assert.ok(
|
|
260
|
+
indicator!.classes.includes('w-[33%]'),
|
|
261
|
+
'missing value → w-[33%] (Indeterminate static representation)',
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// (g5) value out of range → clamped
|
|
266
|
+
{
|
|
267
|
+
const indicator250 = findIndicator(applyShadcnAdapter(progressTree('250')));
|
|
268
|
+
assert.ok(
|
|
269
|
+
indicator250!.classes.includes('w-[100%]'),
|
|
270
|
+
'value="250" clamped to 100%',
|
|
271
|
+
);
|
|
272
|
+
const indicatorNeg = findIndicator(applyShadcnAdapter(progressTree('-10')));
|
|
273
|
+
assert.ok(
|
|
274
|
+
indicatorNeg!.classes.includes('w-[0%]'),
|
|
275
|
+
'value="-10" clamped to 0%',
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// Slider: defaultValue (number | number[]) → indicator + thumb positioning
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
function elTyped<P extends Record<string, unknown> = Record<string, string>>(
|
|
284
|
+
props: P,
|
|
285
|
+
classes: string[] = [],
|
|
286
|
+
children: NodeIR[] = [],
|
|
287
|
+
): NodeIR {
|
|
288
|
+
return {
|
|
289
|
+
kind: 'element',
|
|
290
|
+
tagName: 'div',
|
|
291
|
+
tagLower: 'div',
|
|
292
|
+
props,
|
|
293
|
+
classes,
|
|
294
|
+
children,
|
|
295
|
+
} as unknown as NodeIR;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function sliderTree(rootProps: Record<string, unknown>): NodeIR {
|
|
299
|
+
const indicator = el({ 'data-slot': 'slider-indicator' }, ['absolute', 'h-full', 'bg-primary']);
|
|
300
|
+
const track = el(
|
|
301
|
+
{ 'data-slot': 'slider-track' },
|
|
302
|
+
['relative', 'h-1.5', 'w-full', 'overflow-hidden'],
|
|
303
|
+
[indicator],
|
|
304
|
+
);
|
|
305
|
+
const thumb = el(
|
|
306
|
+
{ 'data-slot': 'slider-thumb' },
|
|
307
|
+
['block', 'size-4', 'shrink-0', 'rounded-full'],
|
|
308
|
+
);
|
|
309
|
+
const control = el(
|
|
310
|
+
{ 'data-slot': 'slider-control' },
|
|
311
|
+
['relative', 'flex', 'h-5', 'w-full', 'grow', 'items-center'],
|
|
312
|
+
[track, thumb],
|
|
313
|
+
);
|
|
314
|
+
return elTyped(
|
|
315
|
+
Object.assign({ 'data-slot': 'slider' }, rootProps),
|
|
316
|
+
['relative', 'flex', 'w-full', 'items-center'],
|
|
317
|
+
[control],
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function findBySlot(root: NodeIR, slot: string): Array<Extract<NodeIR, { kind: 'element' }>> {
|
|
322
|
+
const out: Array<Extract<NodeIR, { kind: 'element' }>> = [];
|
|
323
|
+
const walk = (n: NodeIR) => {
|
|
324
|
+
const el = n as Extract<NodeIR, { kind: 'element' }>;
|
|
325
|
+
if (el.props && el.props['data-slot'] === slot) out.push(el);
|
|
326
|
+
if ('children' in el && Array.isArray(el.children)) {
|
|
327
|
+
for (const c of el.children) walk(c);
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
walk(root);
|
|
331
|
+
return out;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// (h) Single value (number, max=100) → indicator [0, 50%], one thumb @ 50%
|
|
335
|
+
{
|
|
336
|
+
const root = sliderTree({ defaultValue: 50, max: 100 });
|
|
337
|
+
const next = applyShadcnAdapter(root);
|
|
338
|
+
const ind = findBySlot(next, 'slider-indicator')[0];
|
|
339
|
+
assert.ok(ind.classes.includes('left-[0%]'), 'single value → indicator left-[0%]');
|
|
340
|
+
assert.ok(ind.classes.includes('w-[50%]'), 'single value → indicator w-[50%]');
|
|
341
|
+
const thumbs = findBySlot(next, 'slider-thumb');
|
|
342
|
+
assert.equal(thumbs.length, 1, 'single value → one thumb');
|
|
343
|
+
assert.ok(thumbs[0].classes.includes('left-[50%]'), 'thumb at 50%');
|
|
344
|
+
assert.ok(thumbs[0].classes.includes('-translate-x-1/2'), 'thumb gets centering translate');
|
|
345
|
+
assert.ok(thumbs[0].classes.includes('absolute'), 'thumb gets absolute');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// (h2) Range value [25, 75] → indicator [25%, 50% wide], two thumbs @ 25 and 75
|
|
349
|
+
{
|
|
350
|
+
const root = sliderTree({ defaultValue: [25, 75], max: 100 });
|
|
351
|
+
const next = applyShadcnAdapter(root);
|
|
352
|
+
const ind = findBySlot(next, 'slider-indicator')[0];
|
|
353
|
+
assert.ok(ind.classes.includes('left-[25%]'), 'range → indicator left at min%');
|
|
354
|
+
assert.ok(ind.classes.includes('w-[50%]'), 'range → indicator width = (max-min)%');
|
|
355
|
+
const thumbs = findBySlot(next, 'slider-thumb');
|
|
356
|
+
assert.equal(thumbs.length, 2, 'range → thumb template cloned to 2 instances');
|
|
357
|
+
assert.ok(thumbs[0].classes.includes('left-[25%]'), 'first thumb at min%');
|
|
358
|
+
assert.ok(thumbs[1].classes.includes('left-[75%]'), 'second thumb at max%');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// (h3) max=200 → percent scales correctly: defaultValue=100 → 50%
|
|
362
|
+
{
|
|
363
|
+
const root = sliderTree({ defaultValue: 100, max: 200 });
|
|
364
|
+
const next = applyShadcnAdapter(root);
|
|
365
|
+
const thumb = findBySlot(next, 'slider-thumb')[0];
|
|
366
|
+
assert.ok(thumb.classes.includes('left-[50%]'), 'value=100, max=200 → thumb at 50%');
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// (h4) Stringified array "[25, 75]" — safety net for older scanner emit forms
|
|
370
|
+
{
|
|
371
|
+
const root = sliderTree({ defaultValue: '[25, 75]', max: 100 });
|
|
372
|
+
const next = applyShadcnAdapter(root);
|
|
373
|
+
assert.equal(findBySlot(next, 'slider-thumb').length, 2, 'stringified array also clones');
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// (h5) No value, no defaultValue → default to single thumb @ 0%
|
|
377
|
+
{
|
|
378
|
+
const root = sliderTree({});
|
|
379
|
+
const next = applyShadcnAdapter(root);
|
|
380
|
+
const thumbs = findBySlot(next, 'slider-thumb');
|
|
381
|
+
assert.equal(thumbs.length, 1, 'no value → one thumb');
|
|
382
|
+
assert.ok(thumbs[0].classes.includes('left-[0%]'), 'no value → thumb at 0%');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ---------------------------------------------------------------------------
|
|
386
|
+
// (i) Radix ScrollArea — strip runtime control children
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
//
|
|
389
|
+
// `<ScrollAreaPrimitive.Root>` wraps a viewport + a scrollbar + thumb +
|
|
390
|
+
// corner. The scrollbar/thumb/corner only render at runtime in response
|
|
391
|
+
// to scroll position; in a static design-system render they show up as
|
|
392
|
+
// visible content blobs (a dark capsule over the viewport's content
|
|
393
|
+
// area, which is what greenhouse-app's ScrollArea story exposed). The
|
|
394
|
+
// adapter drops them so only the Viewport survives.
|
|
395
|
+
|
|
396
|
+
function scrollAreaTree(extraScrollbarClasses: string[] = []): NodeIR {
|
|
397
|
+
return el({}, [], [
|
|
398
|
+
{
|
|
399
|
+
kind: 'element',
|
|
400
|
+
tagName: 'ScrollAreaPrimitive.Root',
|
|
401
|
+
tagLower: 'div',
|
|
402
|
+
props: {},
|
|
403
|
+
classes: ['relative', 'overflow-hidden'],
|
|
404
|
+
children: [
|
|
405
|
+
{
|
|
406
|
+
kind: 'element',
|
|
407
|
+
tagName: 'ScrollAreaPrimitive.Viewport',
|
|
408
|
+
tagLower: 'div',
|
|
409
|
+
props: {},
|
|
410
|
+
classes: ['h-full', 'w-full'],
|
|
411
|
+
children: [el({}, ['p-3'], [])],
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
kind: 'element',
|
|
415
|
+
tagName: 'ScrollAreaPrimitive.Scrollbar',
|
|
416
|
+
tagLower: 'div',
|
|
417
|
+
props: { orientation: 'vertical' },
|
|
418
|
+
classes: ['flex', 'p-0.5', 'sm:bg-black/60', ...extraScrollbarClasses],
|
|
419
|
+
children: [{
|
|
420
|
+
kind: 'element',
|
|
421
|
+
tagName: 'ScrollAreaPrimitive.Thumb',
|
|
422
|
+
tagLower: 'div',
|
|
423
|
+
props: {},
|
|
424
|
+
classes: ['flex-1', 'rounded-full', 'bg-muted-foreground/40'],
|
|
425
|
+
children: [],
|
|
426
|
+
}],
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
kind: 'element',
|
|
430
|
+
tagName: 'ScrollAreaPrimitive.Corner',
|
|
431
|
+
tagLower: 'div',
|
|
432
|
+
props: {},
|
|
433
|
+
classes: [],
|
|
434
|
+
children: [],
|
|
435
|
+
},
|
|
436
|
+
],
|
|
437
|
+
} as unknown as NodeIR,
|
|
438
|
+
]);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// (i1) Scrollbar + Thumb + Corner are removed; Viewport is flattened
|
|
442
|
+
// (its children become Root's direct children).
|
|
443
|
+
{
|
|
444
|
+
const tree = scrollAreaTree();
|
|
445
|
+
const next = applyShadcnAdapter(tree);
|
|
446
|
+
const root = (next as { children: NodeIR[] }).children[0] as { children: NodeIR[] };
|
|
447
|
+
assert.equal(
|
|
448
|
+
root.children.length,
|
|
449
|
+
1,
|
|
450
|
+
'Root should have one child after Viewport flatten (the Viewport content was a single styled div)',
|
|
451
|
+
);
|
|
452
|
+
// The surviving child is the *content of* the Viewport, not the
|
|
453
|
+
// Viewport wrapper itself. The Viewport carried no visual styling
|
|
454
|
+
// worth keeping and Figma was painting it as a ghost-bordered
|
|
455
|
+
// duplicate of Root.
|
|
456
|
+
assert.notEqual(
|
|
457
|
+
(root.children[0] as { tagName?: string }).tagName,
|
|
458
|
+
'ScrollAreaPrimitive.Viewport',
|
|
459
|
+
'Viewport wrapper must be flattened — its children move up to Root',
|
|
460
|
+
);
|
|
461
|
+
assert.equal(
|
|
462
|
+
(root.children[0] as { tagName?: string }).tagName,
|
|
463
|
+
'div',
|
|
464
|
+
'the surviving child is the original Viewport content (a styled <div>)',
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// (i2) Viewport content's own children are preserved unchanged.
|
|
469
|
+
{
|
|
470
|
+
const tree = scrollAreaTree();
|
|
471
|
+
const next = applyShadcnAdapter(tree);
|
|
472
|
+
const root = (next as { children: NodeIR[] }).children[0] as { children: { classes: string[] }[] };
|
|
473
|
+
assert.deepEqual(
|
|
474
|
+
root.children[0].classes,
|
|
475
|
+
['p-3'],
|
|
476
|
+
"the inner content's class list should be preserved verbatim after flatten",
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
console.log('framework-adapter-shadcn-regression: PASS (3 registry + 4 leaf + 4 avatar + 5 progress + 5 slider + 2 scroll-area cases)');
|