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,235 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
|
|
3
|
+
(globalThis as unknown as { figma: unknown }).figma = {
|
|
4
|
+
notify: () => undefined,
|
|
5
|
+
showUI: () => undefined,
|
|
6
|
+
createFrame: () => makeStubFrame(),
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
import { LayoutParser, setFrameCrossAlign, setFrameFromBlockFlow, setFrameInlineAlign } from '../src/layout/layout-parser';
|
|
10
|
+
|
|
11
|
+
// The recurring "pill renders full width" bug — see
|
|
12
|
+
// `tools/figma-plugin/.ai/troubleshooting.md`. The root cause is that
|
|
13
|
+
// `LayoutParser.applyChildProperties` runs after the per-class parser
|
|
14
|
+
// (which sets `inline-flex` → HUG) and re-stretches inline-flex children
|
|
15
|
+
// when the parent has cross-align STRETCH (which is the default for any
|
|
16
|
+
// flex parent). Past fixes patched the per-class parser only, so any
|
|
17
|
+
// refactor that didn't carry the exclusion through the implicit-stretch
|
|
18
|
+
// path would silently re-introduce the bug.
|
|
19
|
+
//
|
|
20
|
+
// This fixture asserts the END-TO-END behaviour: after calling
|
|
21
|
+
// `applyChildProperties`, an inline-flex child of a STRETCH-cross-align
|
|
22
|
+
// vertical parent must NOT have `layoutAlign === 'STRETCH'`.
|
|
23
|
+
|
|
24
|
+
type StubFrame = {
|
|
25
|
+
type: 'FRAME';
|
|
26
|
+
layoutMode: 'NONE' | 'HORIZONTAL' | 'VERTICAL';
|
|
27
|
+
primaryAxisSizingMode: 'AUTO' | 'FIXED';
|
|
28
|
+
counterAxisSizingMode: 'AUTO' | 'FIXED';
|
|
29
|
+
primaryAxisAlignItems: string;
|
|
30
|
+
counterAxisAlignItems: string;
|
|
31
|
+
layoutAlign: string;
|
|
32
|
+
layoutGrow: number;
|
|
33
|
+
layoutSizingHorizontal: 'HUG' | 'FIXED' | 'FILL';
|
|
34
|
+
layoutSizingVertical: 'HUG' | 'FIXED' | 'FILL';
|
|
35
|
+
counterAxisAlignSelf?: string;
|
|
36
|
+
width: number;
|
|
37
|
+
height: number;
|
|
38
|
+
paddingLeft: number;
|
|
39
|
+
paddingRight: number;
|
|
40
|
+
paddingTop: number;
|
|
41
|
+
paddingBottom: number;
|
|
42
|
+
fills: unknown[];
|
|
43
|
+
strokes: unknown[];
|
|
44
|
+
effects: unknown[];
|
|
45
|
+
children: never[];
|
|
46
|
+
appendChild(): void;
|
|
47
|
+
resize(w: number, h: number): void;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function makeStubFrame(): StubFrame {
|
|
51
|
+
return {
|
|
52
|
+
type: 'FRAME',
|
|
53
|
+
layoutMode: 'NONE',
|
|
54
|
+
primaryAxisSizingMode: 'AUTO',
|
|
55
|
+
counterAxisSizingMode: 'AUTO',
|
|
56
|
+
primaryAxisAlignItems: 'MIN',
|
|
57
|
+
counterAxisAlignItems: 'MIN',
|
|
58
|
+
layoutAlign: 'INHERIT',
|
|
59
|
+
layoutGrow: 0,
|
|
60
|
+
layoutSizingHorizontal: 'HUG',
|
|
61
|
+
layoutSizingVertical: 'HUG',
|
|
62
|
+
width: 0,
|
|
63
|
+
height: 0,
|
|
64
|
+
paddingLeft: 0,
|
|
65
|
+
paddingRight: 0,
|
|
66
|
+
paddingTop: 0,
|
|
67
|
+
paddingBottom: 0,
|
|
68
|
+
fills: [],
|
|
69
|
+
strokes: [],
|
|
70
|
+
effects: [],
|
|
71
|
+
children: [],
|
|
72
|
+
appendChild() { /* no-op */ },
|
|
73
|
+
resize(w, h) { this.width = w; this.height = h; },
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function runRegression(): void {
|
|
78
|
+
// The pill / chip / badge bug applies to inline-flex children of a
|
|
79
|
+
// BLOCK-FLOW parent (a regular `<div>` promoted to VERTICAL by the plugin
|
|
80
|
+
// even though CSS-wise it's not a flex container). In that context inline
|
|
81
|
+
// children hug content per CSS spec — the plugin's "implicit stretch"
|
|
82
|
+
// pass would otherwise stretch them to full card width.
|
|
83
|
+
//
|
|
84
|
+
// Real flex parents (`<div class="flex flex-col">`) are different:
|
|
85
|
+
// CSS `align-items: stretch` (the flex default) applies to ALL items
|
|
86
|
+
// including inline-flex, so dialog footer buttons / cancel-save pairs
|
|
87
|
+
// stretch to full width on mobile.
|
|
88
|
+
|
|
89
|
+
// ---- Case 1: inline-flex pill in a BLOCK-FLOW vertical parent —
|
|
90
|
+
// the actual how-it-works / round-trip-section pill bug. Pill must NOT
|
|
91
|
+
// stretch. -------------------------------------------------------------
|
|
92
|
+
const blockFlowCard = makeStubFrame();
|
|
93
|
+
blockFlowCard.layoutMode = 'VERTICAL';
|
|
94
|
+
blockFlowCard.width = 600;
|
|
95
|
+
blockFlowCard.primaryAxisSizingMode = 'FIXED';
|
|
96
|
+
setFrameCrossAlign(blockFlowCard as unknown as FrameNode, 'MIN');
|
|
97
|
+
setFrameFromBlockFlow(blockFlowCard as unknown as FrameNode, true);
|
|
98
|
+
|
|
99
|
+
const pill = makeStubFrame();
|
|
100
|
+
pill.layoutMode = 'HORIZONTAL'; // inline-flex sets HORIZONTAL
|
|
101
|
+
pill.layoutAlign = 'INHERIT';
|
|
102
|
+
|
|
103
|
+
LayoutParser.applyChildProperties(
|
|
104
|
+
pill as unknown as FrameNode,
|
|
105
|
+
'inline-flex items-center rounded-full px-3 py-1 text-sm font-medium'.split(' '),
|
|
106
|
+
blockFlowCard as unknown as FrameNode,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
assert.notEqual(
|
|
110
|
+
pill.layoutAlign,
|
|
111
|
+
'STRETCH',
|
|
112
|
+
'inline-flex pill in a BLOCK-FLOW MIN parent must NOT be implicitly stretched',
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
// ---- Case 2: inline-block in BLOCK-FLOW parent — same exclusion. ----
|
|
116
|
+
const blockFlowCard2 = makeStubFrame();
|
|
117
|
+
blockFlowCard2.layoutMode = 'VERTICAL';
|
|
118
|
+
blockFlowCard2.width = 600;
|
|
119
|
+
blockFlowCard2.primaryAxisSizingMode = 'FIXED';
|
|
120
|
+
setFrameCrossAlign(blockFlowCard2 as unknown as FrameNode, 'MIN');
|
|
121
|
+
setFrameFromBlockFlow(blockFlowCard2 as unknown as FrameNode, true);
|
|
122
|
+
|
|
123
|
+
const inlineBlock = makeStubFrame();
|
|
124
|
+
inlineBlock.layoutAlign = 'INHERIT';
|
|
125
|
+
LayoutParser.applyChildProperties(
|
|
126
|
+
inlineBlock as unknown as FrameNode,
|
|
127
|
+
'inline-block px-2 py-1'.split(' '),
|
|
128
|
+
blockFlowCard2 as unknown as FrameNode,
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
assert.notEqual(
|
|
132
|
+
inlineBlock.layoutAlign,
|
|
133
|
+
'STRETCH',
|
|
134
|
+
'inline-block child of BLOCK-FLOW parent must also opt out of implicit STRETCH',
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// ---- Case 3: block-level child of BLOCK-FLOW parent SHOULD stretch
|
|
138
|
+
// (plugin emulates items-stretch for block children even in non-flex
|
|
139
|
+
// parents — preserves the "items fill card" expectation). -------------
|
|
140
|
+
const blockFlowCard3 = makeStubFrame();
|
|
141
|
+
blockFlowCard3.layoutMode = 'VERTICAL';
|
|
142
|
+
blockFlowCard3.width = 600;
|
|
143
|
+
blockFlowCard3.primaryAxisSizingMode = 'FIXED';
|
|
144
|
+
setFrameCrossAlign(blockFlowCard3 as unknown as FrameNode, 'MIN');
|
|
145
|
+
setFrameFromBlockFlow(blockFlowCard3 as unknown as FrameNode, true);
|
|
146
|
+
|
|
147
|
+
const blockChild = makeStubFrame();
|
|
148
|
+
blockChild.layoutMode = 'VERTICAL';
|
|
149
|
+
blockChild.layoutAlign = 'INHERIT';
|
|
150
|
+
|
|
151
|
+
LayoutParser.applyChildProperties(
|
|
152
|
+
blockChild as unknown as FrameNode,
|
|
153
|
+
'flex flex-col gap-2'.split(' '),
|
|
154
|
+
blockFlowCard3 as unknown as FrameNode,
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
assert.equal(
|
|
158
|
+
blockChild.layoutAlign,
|
|
159
|
+
'STRETCH',
|
|
160
|
+
'block-level child SHOULD stretch in BLOCK-FLOW parent (plugin emulates items-stretch)',
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// ---- Case 4: inline-flex BUTTON in a REAL FLEX parent SHOULD stretch
|
|
164
|
+
// (CSS default `align-items: stretch` applies to inline-flex flex items).
|
|
165
|
+
// The dialog footer at base breakpoint: `<DialogFooter className="flex
|
|
166
|
+
// flex-col-reverse">` with `<Button>` (inline-flex) — buttons must
|
|
167
|
+
// stretch to full footer width on mobile. ------------------------------
|
|
168
|
+
const flexFooter = makeStubFrame();
|
|
169
|
+
flexFooter.layoutMode = 'VERTICAL';
|
|
170
|
+
flexFooter.width = 400;
|
|
171
|
+
flexFooter.primaryAxisSizingMode = 'FIXED';
|
|
172
|
+
setFrameCrossAlign(flexFooter as unknown as FrameNode, 'STRETCH');
|
|
173
|
+
setFrameFromBlockFlow(flexFooter as unknown as FrameNode, false);
|
|
174
|
+
|
|
175
|
+
const button = makeStubFrame();
|
|
176
|
+
button.layoutMode = 'HORIZONTAL';
|
|
177
|
+
button.layoutAlign = 'INHERIT';
|
|
178
|
+
|
|
179
|
+
LayoutParser.applyChildProperties(
|
|
180
|
+
button as unknown as FrameNode,
|
|
181
|
+
'inline-flex items-center justify-center h-9 px-4 py-2 rounded-md'.split(' '),
|
|
182
|
+
flexFooter as unknown as FrameNode,
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
assert.equal(
|
|
186
|
+
button.layoutAlign,
|
|
187
|
+
'STRETCH',
|
|
188
|
+
'inline-flex button in a REAL FLEX-col parent (CSS default items-stretch) SHOULD stretch — dialog footer base breakpoint',
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
// ---- Case 5: inline-flex pill in BLOCK-FLOW MIN parent WITH text-center
|
|
192
|
+
// CSS centers inline content via the parent's text-align — the round-trip
|
|
193
|
+
// section pill `<span class="inline-flex ...">Round-trip in motion</span>`
|
|
194
|
+
// inside `<div class="text-center">` should appear horizontally centered.
|
|
195
|
+
// The pill must NOT stretch (case 1 rule), but its
|
|
196
|
+
// counterAxisAlignSelf should be CENTER so it sits at parent's center.
|
|
197
|
+
const centeredBlockParent = makeStubFrame() as StubFrame & { counterAxisAlignSelf?: string };
|
|
198
|
+
centeredBlockParent.layoutMode = 'VERTICAL';
|
|
199
|
+
centeredBlockParent.width = 600;
|
|
200
|
+
centeredBlockParent.primaryAxisSizingMode = 'FIXED';
|
|
201
|
+
setFrameCrossAlign(centeredBlockParent as unknown as FrameNode, 'MIN');
|
|
202
|
+
setFrameFromBlockFlow(centeredBlockParent as unknown as FrameNode, true);
|
|
203
|
+
setFrameInlineAlign(centeredBlockParent as unknown as FrameNode, 'CENTER');
|
|
204
|
+
|
|
205
|
+
const centeredPill = makeStubFrame() as StubFrame & { counterAxisAlignSelf?: string };
|
|
206
|
+
centeredPill.layoutMode = 'HORIZONTAL';
|
|
207
|
+
centeredPill.layoutAlign = 'INHERIT';
|
|
208
|
+
centeredPill.counterAxisAlignSelf = 'AUTO';
|
|
209
|
+
|
|
210
|
+
LayoutParser.applyChildProperties(
|
|
211
|
+
centeredPill as unknown as FrameNode,
|
|
212
|
+
'inline-flex items-center rounded-full px-3 py-1 text-sm font-medium'.split(' '),
|
|
213
|
+
centeredBlockParent as unknown as FrameNode,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
assert.notEqual(
|
|
217
|
+
centeredPill.layoutAlign,
|
|
218
|
+
'STRETCH',
|
|
219
|
+
'pill in text-center block-flow parent must still NOT stretch',
|
|
220
|
+
);
|
|
221
|
+
assert.equal(
|
|
222
|
+
centeredPill.counterAxisAlignSelf,
|
|
223
|
+
'CENTER',
|
|
224
|
+
'pill in text-center block-flow parent SHOULD be horizontally centered via counterAxisAlignSelf',
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
runRegression();
|
|
230
|
+
console.log('inline-flex-regression: PASS');
|
|
231
|
+
} catch (err) {
|
|
232
|
+
console.error('inline-flex-regression: FAIL');
|
|
233
|
+
console.error(err);
|
|
234
|
+
process.exit(1);
|
|
235
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
|
|
3
|
+
import { transformInputNodes } from '../src/tailwind/node-ir';
|
|
4
|
+
import type { NodeIR } from '../src/tailwind/node-ir';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Regression: `<input type="range">` is rewritten by `transformInputNodes`
|
|
8
|
+
* in `src/tailwind/node-ir.ts` into a synthetic 3-child tree (track +
|
|
9
|
+
* filled indicator + thumb). Locks:
|
|
10
|
+
*
|
|
11
|
+
* 1. The rewrite fires only for `type="range"` (other input types pass
|
|
12
|
+
* through untouched).
|
|
13
|
+
* 2. Geometry is `((value - min) / (max - min)) * 100`.
|
|
14
|
+
* 3. `defaultValue` is honored when `value` is missing.
|
|
15
|
+
* 4. `disabled` adds `opacity-50` to the wrapper.
|
|
16
|
+
* 5. Sizing classes from the consumer (`w-full`, paddings) survive on
|
|
17
|
+
* the wrapper.
|
|
18
|
+
*
|
|
19
|
+
* Why this fixture exists: a previous version of the plugin had no
|
|
20
|
+
* rendering path for `type="range"`. The default input branch in
|
|
21
|
+
* `ui-builder.ts` reads `value/defaultValue/placeholder` as text — for
|
|
22
|
+
* range that meant either a tiny "5" text node or, more often, an
|
|
23
|
+
* infinite hang as downstream layout deferred-pass solvers stalled on a
|
|
24
|
+
* 0-content frame with `w-full accent-primary`. The transform produces
|
|
25
|
+
* a real visual structure the existing pipeline renders cleanly.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// Minimal helpers (props are always stringified by the scanner — mirror
|
|
29
|
+
// that here so assertions match real-world IR input).
|
|
30
|
+
|
|
31
|
+
function inputEl(props: Record<string, string>, classes: string[] = []): NodeIR {
|
|
32
|
+
return {
|
|
33
|
+
kind: 'element',
|
|
34
|
+
tagName: 'input',
|
|
35
|
+
tagLower: 'input',
|
|
36
|
+
props,
|
|
37
|
+
classes,
|
|
38
|
+
children: [],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function rewrite(node: NodeIR): NodeIR {
|
|
43
|
+
return transformInputNodes(node);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function expectElement(node: NodeIR): Extract<NodeIR, { kind: 'element' }> {
|
|
47
|
+
assert.equal(node.kind, 'element', 'expected element node');
|
|
48
|
+
return node as Extract<NodeIR, { kind: 'element' }>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// (1) Bare `<input type="range">` with value/min/max → track + filled + thumb
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
{
|
|
56
|
+
const input = inputEl(
|
|
57
|
+
{ type: 'range', min: '0', max: '100', value: '25' },
|
|
58
|
+
['w-full', 'accent-primary'],
|
|
59
|
+
);
|
|
60
|
+
const out = rewrite(input);
|
|
61
|
+
const wrap = expectElement(out);
|
|
62
|
+
|
|
63
|
+
assert.equal(wrap.tagLower, 'div', 'rewrite produces a div wrapper');
|
|
64
|
+
// Structure mirrors shadcn Slider: track (flow child, contains filled),
|
|
65
|
+
// thumb (absolute sibling). Two top-level children, NOT three.
|
|
66
|
+
assert.equal(wrap.children.length, 2, 'wrapper has track + thumb');
|
|
67
|
+
|
|
68
|
+
// Consumer's sizing classes preserved on the wrapper; layout classes added.
|
|
69
|
+
assert.ok(wrap.classes.includes('w-full'), 'preserves consumer w-full');
|
|
70
|
+
assert.ok(wrap.classes.includes('relative'), 'wrapper is relative');
|
|
71
|
+
assert.ok(wrap.classes.includes('flex'), 'wrapper is flex');
|
|
72
|
+
assert.ok(wrap.classes.includes('items-center'), 'wrapper items-center');
|
|
73
|
+
assert.ok(wrap.classes.includes('h-4'), 'wrapper height set');
|
|
74
|
+
|
|
75
|
+
const [track, thumb] = wrap.children.map(expectElement);
|
|
76
|
+
|
|
77
|
+
// Track is a FLOW child (not absolute) — that's what gives the wrapper
|
|
78
|
+
// content to size against. Without this, all-absolute siblings collapse
|
|
79
|
+
// the auto-layout into a 0-content stall.
|
|
80
|
+
assert.ok(!track.classes.includes('absolute'), 'track is flow, not absolute');
|
|
81
|
+
assert.ok(track.classes.includes('w-full'), 'track full-width');
|
|
82
|
+
assert.ok(track.classes.includes('overflow-hidden'), 'track clips its fill');
|
|
83
|
+
assert.ok(track.classes.includes('bg-secondary'), 'track background neutral');
|
|
84
|
+
|
|
85
|
+
// Filled portion is a flow child of the track (not a sibling of it).
|
|
86
|
+
// No absolute positioning — sits at the left edge by natural flow.
|
|
87
|
+
assert.equal(track.children.length, 1, 'track has one filled child');
|
|
88
|
+
const filled = expectElement(track.children[0]);
|
|
89
|
+
assert.ok(!filled.classes.includes('absolute'), 'filled is flow inside track');
|
|
90
|
+
assert.ok(filled.classes.includes('w-[25%]'), 'filled width matches value');
|
|
91
|
+
assert.ok(filled.classes.includes('bg-primary'), 'filled paints accent');
|
|
92
|
+
|
|
93
|
+
// Thumb: absolute sibling positioned at 25% with -translate-x-1/2.
|
|
94
|
+
// No top-N — wrapper's h-4 + flex items-center centers via static position.
|
|
95
|
+
assert.ok(thumb.classes.includes('absolute'), 'thumb is absolute');
|
|
96
|
+
assert.ok(thumb.classes.includes('left-[25%]'), 'thumb at 25%');
|
|
97
|
+
assert.ok(thumb.classes.includes('-translate-x-1/2'), 'thumb centered on point');
|
|
98
|
+
assert.ok(thumb.classes.includes('rounded-full'), 'thumb circular');
|
|
99
|
+
assert.ok(!thumb.classes.some((c) => c.startsWith('top-')),
|
|
100
|
+
'thumb has no top-N — flex items-center on wrapper handles vertical centering');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// (2) Non-zero min — the case from greenhouse-app LeverageSlider
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
{
|
|
108
|
+
// ((5 - 1.1) / (100 - 1.1)) * 100 ≈ 3.9434...
|
|
109
|
+
const input = inputEl({ type: 'range', min: '1.1', max: '100', value: '5' });
|
|
110
|
+
const out = rewrite(input);
|
|
111
|
+
const wrap = expectElement(out);
|
|
112
|
+
const track = expectElement(wrap.children[0]);
|
|
113
|
+
const filled = expectElement(track.children[0]);
|
|
114
|
+
const thumb = expectElement(wrap.children[1]);
|
|
115
|
+
|
|
116
|
+
// Match the value the transform actually emits (two-decimal precision,
|
|
117
|
+
// trailing-zero strip). Locking exact percentage prevents silent
|
|
118
|
+
// rounding drift.
|
|
119
|
+
const widthClass = filled.classes.find((c) => c.startsWith('w-['));
|
|
120
|
+
assert.equal(widthClass, 'w-[3.94%]', `got ${widthClass}`);
|
|
121
|
+
|
|
122
|
+
const leftClass = thumb.classes.find((c) => c.startsWith('left-['));
|
|
123
|
+
assert.equal(leftClass, 'left-[3.94%]', `got ${leftClass}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// (3) `defaultValue` honored when `value` missing
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
{
|
|
131
|
+
const input = inputEl({ type: 'range', min: '0', max: '100', defaultValue: '75' });
|
|
132
|
+
const out = rewrite(input);
|
|
133
|
+
const wrap = expectElement(out);
|
|
134
|
+
const track = expectElement(wrap.children[0]);
|
|
135
|
+
const filled = expectElement(track.children[0]);
|
|
136
|
+
|
|
137
|
+
assert.ok(filled.classes.includes('w-[75%]'), 'defaultValue drives width');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// (4) `disabled` adds opacity-50 to wrapper
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
{
|
|
145
|
+
const input = inputEl({ type: 'range', min: '0', max: '100', value: '50', disabled: 'true' });
|
|
146
|
+
const out = rewrite(input);
|
|
147
|
+
const wrap = expectElement(out);
|
|
148
|
+
|
|
149
|
+
assert.ok(wrap.classes.includes('opacity-50'), 'disabled adds opacity-50');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
{
|
|
153
|
+
// disabled="false" should NOT trigger (some libs serialize false this way).
|
|
154
|
+
const input = inputEl({ type: 'range', min: '0', max: '100', value: '50', disabled: 'false' });
|
|
155
|
+
const out = rewrite(input);
|
|
156
|
+
const wrap = expectElement(out);
|
|
157
|
+
|
|
158
|
+
assert.ok(!wrap.classes.includes('opacity-50'), 'disabled=false does not dim');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// (5) Non-range inputs pass through untouched
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
|
|
165
|
+
{
|
|
166
|
+
const before = inputEl({ type: 'text', placeholder: 'Name' }, ['h-10']);
|
|
167
|
+
const after = rewrite(before);
|
|
168
|
+
// Identity is preserved for non-range inputs (cache-friendly).
|
|
169
|
+
assert.equal(after, before, 'type=text returned by reference');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
{
|
|
173
|
+
const before = inputEl({ type: 'number', value: '5' }, ['h-10']);
|
|
174
|
+
const after = rewrite(before);
|
|
175
|
+
assert.equal(after, before, 'type=number returned by reference');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// (6) Missing value defaults to 0%
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
{
|
|
183
|
+
const input = inputEl({ type: 'range', min: '0', max: '100' });
|
|
184
|
+
const out = rewrite(input);
|
|
185
|
+
const wrap = expectElement(out);
|
|
186
|
+
const track = expectElement(wrap.children[0]);
|
|
187
|
+
const filled = expectElement(track.children[0]);
|
|
188
|
+
|
|
189
|
+
assert.ok(filled.classes.includes('w-[0%]'), 'missing value → 0% fill');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
// (7) Range inside a larger tree gets rewritten in place
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
|
|
196
|
+
{
|
|
197
|
+
const tree: NodeIR = {
|
|
198
|
+
kind: 'element',
|
|
199
|
+
tagName: 'div',
|
|
200
|
+
tagLower: 'div',
|
|
201
|
+
props: {},
|
|
202
|
+
classes: ['p-4'],
|
|
203
|
+
children: [
|
|
204
|
+
inputEl({ type: 'range', min: '0', max: '100', value: '40' }, ['w-full']),
|
|
205
|
+
],
|
|
206
|
+
};
|
|
207
|
+
const out = rewrite(tree);
|
|
208
|
+
const outer = expectElement(out);
|
|
209
|
+
assert.equal(outer.children.length, 1);
|
|
210
|
+
const slider = expectElement(outer.children[0]);
|
|
211
|
+
assert.equal(slider.children.length, 2, 'nested range rewritten (track + thumb)');
|
|
212
|
+
const track = expectElement(slider.children[0]);
|
|
213
|
+
const filled = expectElement(track.children[0]);
|
|
214
|
+
assert.ok(filled.classes.includes('w-[40%]'), 'nested range geometry correct');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
console.log('input-range-regression: ok');
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import {
|
|
3
|
+
appendResolvedInstance,
|
|
4
|
+
renderGuardedPortalStoryInstances,
|
|
5
|
+
renderStoryInstances,
|
|
6
|
+
} from '../src/design-system/instance-rendering';
|
|
7
|
+
import type {
|
|
8
|
+
ComponentDef,
|
|
9
|
+
ComponentInstance,
|
|
10
|
+
ComponentStory,
|
|
11
|
+
CvaAnalysis,
|
|
12
|
+
LayoutInfo,
|
|
13
|
+
} from '../src/components';
|
|
14
|
+
import type { StoryBuilderContext } from '../src/design-system/story-builder-context';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Regression: instance-rendering is the Figma-API-heavy module that composes
|
|
18
|
+
* ComponentInstances into a Story Layout frame. Most of its surface needs a
|
|
19
|
+
* real Figma sandbox to exercise — but the early-return guards (no instances,
|
|
20
|
+
* no def name, missing analysis, unknown analysis type) are pure data and run
|
|
21
|
+
* before any Figma API call. Lock those.
|
|
22
|
+
*
|
|
23
|
+
* The full happy paths are exercised in real Figma during plugin runs.
|
|
24
|
+
*
|
|
25
|
+
* Extracted from `src/design-system/story-builder.ts` into
|
|
26
|
+
* `src/design-system/instance-rendering.ts`.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
// Minimal LayoutInfo stub — just captures appendChild calls and reports the
|
|
30
|
+
// number of appended children. Used to assert that early-return paths do NOT
|
|
31
|
+
// touch the layout.
|
|
32
|
+
function makeLayoutStub(): { layout: LayoutInfo; appended: unknown[] } {
|
|
33
|
+
const appended: unknown[] = [];
|
|
34
|
+
const layout = {
|
|
35
|
+
appendChild: (node: unknown) => { appended.push(node); },
|
|
36
|
+
children: [],
|
|
37
|
+
} as unknown as LayoutInfo;
|
|
38
|
+
return { layout, appended };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function makeCtxStub(): StoryBuilderContext {
|
|
42
|
+
return {
|
|
43
|
+
getComponentDefByName: () => null,
|
|
44
|
+
normalizeComponentDef: (def: ComponentDef) => def,
|
|
45
|
+
} as unknown as StoryBuilderContext;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// appendResolvedInstance — early-return guards
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
// null analysis → false, no layout touched
|
|
53
|
+
{
|
|
54
|
+
const { layout, appended } = makeLayoutStub();
|
|
55
|
+
const result = appendResolvedInstance(
|
|
56
|
+
layout,
|
|
57
|
+
null as unknown as CvaAnalysis,
|
|
58
|
+
{ componentName: 'X', props: {} } as ComponentInstance,
|
|
59
|
+
{ name: 's', instances: [] } as unknown as ComponentStory,
|
|
60
|
+
'primary',
|
|
61
|
+
{},
|
|
62
|
+
null,
|
|
63
|
+
makeCtxStub(),
|
|
64
|
+
);
|
|
65
|
+
assert.equal(result, false, 'null analysis returns false');
|
|
66
|
+
assert.equal(appended.length, 0, 'null analysis must not touch layout — no appendChild calls');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Unknown analysis.type → false, no layout touched
|
|
70
|
+
{
|
|
71
|
+
const { layout, appended } = makeLayoutStub();
|
|
72
|
+
const result = appendResolvedInstance(
|
|
73
|
+
layout,
|
|
74
|
+
{ type: 'mystery-type', name: 'X' } as unknown as CvaAnalysis,
|
|
75
|
+
{ componentName: 'X', props: {} } as ComponentInstance,
|
|
76
|
+
{ name: 's', instances: [] } as unknown as ComponentStory,
|
|
77
|
+
'primary',
|
|
78
|
+
{},
|
|
79
|
+
null,
|
|
80
|
+
makeCtxStub(),
|
|
81
|
+
);
|
|
82
|
+
assert.equal(result, false, 'unknown analysis.type returns false');
|
|
83
|
+
assert.equal(appended.length, 0, 'unknown type must not touch layout');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// renderStoryInstances — empty / missing data
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
// story.instances undefined → 0, no layout touched
|
|
91
|
+
{
|
|
92
|
+
const { layout, appended } = makeLayoutStub();
|
|
93
|
+
const added = renderStoryInstances(
|
|
94
|
+
layout,
|
|
95
|
+
{ name: 's' } as unknown as ComponentStory,
|
|
96
|
+
'primary',
|
|
97
|
+
{},
|
|
98
|
+
null,
|
|
99
|
+
makeCtxStub(),
|
|
100
|
+
);
|
|
101
|
+
assert.equal(added, 0, 'no instances → 0 added');
|
|
102
|
+
assert.equal(appended.length, 0, 'no instances must not touch layout');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Empty instances array → 0
|
|
106
|
+
{
|
|
107
|
+
const { layout, appended } = makeLayoutStub();
|
|
108
|
+
const added = renderStoryInstances(
|
|
109
|
+
layout,
|
|
110
|
+
{ name: 's', instances: [] } as unknown as ComponentStory,
|
|
111
|
+
'primary',
|
|
112
|
+
{},
|
|
113
|
+
null,
|
|
114
|
+
makeCtxStub(),
|
|
115
|
+
);
|
|
116
|
+
assert.equal(added, 0, 'empty instances array → 0 added');
|
|
117
|
+
assert.equal(appended.length, 0, 'empty instances array must not touch layout');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Instances with falsy / nameless entries → skipped
|
|
121
|
+
{
|
|
122
|
+
const { layout, appended } = makeLayoutStub();
|
|
123
|
+
const added = renderStoryInstances(
|
|
124
|
+
layout,
|
|
125
|
+
{
|
|
126
|
+
name: 's',
|
|
127
|
+
instances: [null, undefined, { componentName: '' }, { componentName: null }],
|
|
128
|
+
} as unknown as ComponentStory,
|
|
129
|
+
'primary',
|
|
130
|
+
{},
|
|
131
|
+
null,
|
|
132
|
+
makeCtxStub(),
|
|
133
|
+
);
|
|
134
|
+
assert.equal(added, 0, 'falsy / nameless instance entries are skipped — no fallback frames created');
|
|
135
|
+
assert.equal(appended.length, 0, 'no Figma frame creation triggered');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// renderGuardedPortalStoryInstances — early-return guards
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
// No instances → 0
|
|
143
|
+
{
|
|
144
|
+
const { layout, appended } = makeLayoutStub();
|
|
145
|
+
const added = renderGuardedPortalStoryInstances(
|
|
146
|
+
layout,
|
|
147
|
+
{ name: 'Sheet' } as ComponentDef,
|
|
148
|
+
{ name: 's' } as unknown as ComponentStory,
|
|
149
|
+
'primary',
|
|
150
|
+
{},
|
|
151
|
+
null,
|
|
152
|
+
makeCtxStub(),
|
|
153
|
+
);
|
|
154
|
+
assert.equal(added, 0, 'no instances → 0 added');
|
|
155
|
+
assert.equal(appended.length, 0, 'no layout touch');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Empty def.name → 0 (normalizeComponentName returns empty key)
|
|
159
|
+
{
|
|
160
|
+
const { layout, appended } = makeLayoutStub();
|
|
161
|
+
const added = renderGuardedPortalStoryInstances(
|
|
162
|
+
layout,
|
|
163
|
+
{ name: '' } as ComponentDef,
|
|
164
|
+
{
|
|
165
|
+
name: 's',
|
|
166
|
+
instances: [{ componentName: 'SheetTrigger', props: {} }],
|
|
167
|
+
} as unknown as ComponentStory,
|
|
168
|
+
'primary',
|
|
169
|
+
{},
|
|
170
|
+
null,
|
|
171
|
+
makeCtxStub(),
|
|
172
|
+
);
|
|
173
|
+
assert.equal(added, 0, 'empty def name → 0 added (cannot key portal slots)');
|
|
174
|
+
assert.equal(appended.length, 0, 'no layout touch');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Null def → 0
|
|
178
|
+
{
|
|
179
|
+
const { layout, appended } = makeLayoutStub();
|
|
180
|
+
const added = renderGuardedPortalStoryInstances(
|
|
181
|
+
layout,
|
|
182
|
+
null as unknown as ComponentDef,
|
|
183
|
+
{
|
|
184
|
+
name: 's',
|
|
185
|
+
instances: [{ componentName: 'X', props: {} }],
|
|
186
|
+
} as unknown as ComponentStory,
|
|
187
|
+
'primary',
|
|
188
|
+
{},
|
|
189
|
+
null,
|
|
190
|
+
makeCtxStub(),
|
|
191
|
+
);
|
|
192
|
+
assert.equal(added, 0, 'null def → 0 added defensively');
|
|
193
|
+
assert.equal(appended.length, 0, 'no layout touch');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// All instances are the root container only (no trigger/content) → 0
|
|
197
|
+
// (root instance is intentionally skipped — its visual content lives in slots)
|
|
198
|
+
{
|
|
199
|
+
const { layout, appended } = makeLayoutStub();
|
|
200
|
+
const added = renderGuardedPortalStoryInstances(
|
|
201
|
+
layout,
|
|
202
|
+
{ name: 'Sheet' } as ComponentDef,
|
|
203
|
+
{
|
|
204
|
+
name: 's',
|
|
205
|
+
instances: [
|
|
206
|
+
{ componentName: 'Sheet', props: { open: false } },
|
|
207
|
+
{ componentName: 'SheetProvider', props: {} },
|
|
208
|
+
{ componentName: 'SheetPositioner', props: {} },
|
|
209
|
+
],
|
|
210
|
+
} as unknown as ComponentStory,
|
|
211
|
+
'primary',
|
|
212
|
+
{},
|
|
213
|
+
null,
|
|
214
|
+
makeCtxStub(),
|
|
215
|
+
);
|
|
216
|
+
assert.equal(
|
|
217
|
+
added,
|
|
218
|
+
0,
|
|
219
|
+
'root + Provider + Positioner all skip — no trigger or content present',
|
|
220
|
+
);
|
|
221
|
+
assert.equal(appended.length, 0, 'no layout touch');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
console.log('instance-rendering-regression: PASS');
|