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,738 @@
|
|
|
1
|
+
// External modules.
|
|
2
|
+
import { jsxTreeHasNestedDataSlots } from './story-render-strategy';
|
|
3
|
+
import {
|
|
4
|
+
applyTailwindStylesToFrame,
|
|
5
|
+
buildCvaClassesForVariant,
|
|
6
|
+
cloneJsxNodeForBreakpoint,
|
|
7
|
+
collectTreeClasses,
|
|
8
|
+
extractBreakpointsFromClasses,
|
|
9
|
+
extractRootGridColsFromTree,
|
|
10
|
+
extractStatesFromClasses,
|
|
11
|
+
getBreakpointLabel,
|
|
12
|
+
getClassesForBreakpoint,
|
|
13
|
+
getResponsivePreviewWidth,
|
|
14
|
+
hasSignificantResponsiveChanges,
|
|
15
|
+
isHiddenAtBreakpoint,
|
|
16
|
+
mergeStatesWithDefinition,
|
|
17
|
+
parseUtilityClass,
|
|
18
|
+
propagateChildSelectorClasses,
|
|
19
|
+
splitClassName,
|
|
20
|
+
tailwindClassesToStyle,
|
|
21
|
+
treeClassSignature,
|
|
22
|
+
treeHasResponsiveClasses,
|
|
23
|
+
uniqueClassSignature,
|
|
24
|
+
type JsxNode,
|
|
25
|
+
type JsxElement,
|
|
26
|
+
type StateInfo,
|
|
27
|
+
} from '../tailwind';
|
|
28
|
+
import { parseColor } from '../tokens';
|
|
29
|
+
import { createTextNode, type CreateTextOptions } from '../text';
|
|
30
|
+
import { getRingInfoFromClasses, markRingNode, applyRingIfPossible } from '../layout';
|
|
31
|
+
import type {
|
|
32
|
+
ComponentDef,
|
|
33
|
+
ComponentStory,
|
|
34
|
+
} from '../components/scanner-types';
|
|
35
|
+
|
|
36
|
+
// Sibling design-system modules.
|
|
37
|
+
import { defaultGridWidth, normalizeLayoutClasses } from './story-layout';
|
|
38
|
+
import { tagGeneratedSubtree } from './generated-node';
|
|
39
|
+
import { type StoryBuilderContext } from './story-builder-context';
|
|
40
|
+
import { renderStandaloneStory } from './story-builder';
|
|
41
|
+
import {
|
|
42
|
+
buildStatePreviewLabel,
|
|
43
|
+
buildStoryStateClasses,
|
|
44
|
+
buildStoryInstanceClasses,
|
|
45
|
+
createStateStoryInstance,
|
|
46
|
+
} from './story-instance';
|
|
47
|
+
import { findMatchingInstance } from './story-tree-search';
|
|
48
|
+
import { getThemeContext, BOARD_LAYOUT } from './theme-context';
|
|
49
|
+
import { resolvePortalAwareStoryTree } from './portal-handling';
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// State-preview helpers (private to this module)
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
function getStateEntry(states: StateInfo[], name: string): StateInfo | null {
|
|
56
|
+
for (let i = 0; i < states.length; i++) {
|
|
57
|
+
if (states[i].name === name) return states[i];
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getStateNames(states: StateInfo[]): string[] {
|
|
63
|
+
const names: string[] = ['default'];
|
|
64
|
+
for (let i = 0; i < states.length; i++) {
|
|
65
|
+
const name = states[i].name;
|
|
66
|
+
if (name === 'default') continue;
|
|
67
|
+
names.push(name);
|
|
68
|
+
}
|
|
69
|
+
return names;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function buildStateClasses(states: StateInfo[], name: string): string[] {
|
|
73
|
+
const defaultEntry = getStateEntry(states, 'default');
|
|
74
|
+
const baseClasses = defaultEntry ? defaultEntry.classes.slice() : [];
|
|
75
|
+
if (name === 'default') return baseClasses;
|
|
76
|
+
const entry = getStateEntry(states, name);
|
|
77
|
+
if (!entry) return baseClasses;
|
|
78
|
+
return baseClasses.concat(entry.classes);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isVisualStateUtility(cls: string): boolean {
|
|
82
|
+
const atom = parseUtilityClass(cls);
|
|
83
|
+
const utility = atom.utility || cls;
|
|
84
|
+
if (!utility) return false;
|
|
85
|
+
if (utility.startsWith('text-')) return false;
|
|
86
|
+
if (utility.startsWith('font-')) return false;
|
|
87
|
+
if (utility === 'underline' || utility === 'no-underline') return false;
|
|
88
|
+
if (utility.startsWith('decoration-')) return false;
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
utility.startsWith('bg-')
|
|
92
|
+
|| utility.startsWith('border')
|
|
93
|
+
|| utility.startsWith('ring')
|
|
94
|
+
|| utility.startsWith('shadow')
|
|
95
|
+
|| utility.startsWith('opacity-')
|
|
96
|
+
|| utility.startsWith('outline')
|
|
97
|
+
|| utility.startsWith('cursor-')
|
|
98
|
+
|| utility.startsWith('pointer-events-')
|
|
99
|
+
|| utility.startsWith('scale-')
|
|
100
|
+
|| utility.startsWith('translate-')
|
|
101
|
+
|| utility.startsWith('rotate-')
|
|
102
|
+
|| utility.startsWith('skew-')
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function shouldSkipStatePreview(def: ComponentDef): boolean {
|
|
107
|
+
if (!def || def.type !== 'state') return false;
|
|
108
|
+
const states = def.states || {};
|
|
109
|
+
const names = Object.keys(states).filter(name => name !== 'default');
|
|
110
|
+
if (names.length === 0) return true;
|
|
111
|
+
|
|
112
|
+
let hasVisualState = false;
|
|
113
|
+
for (let i = 0; i < names.length; i++) {
|
|
114
|
+
const state = states[names[i]];
|
|
115
|
+
const classes = state && state.classes ? state.classes : [];
|
|
116
|
+
for (let j = 0; j < classes.length; j++) {
|
|
117
|
+
if (isVisualStateUtility(classes[j])) {
|
|
118
|
+
hasVisualState = true;
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (hasVisualState) break;
|
|
123
|
+
}
|
|
124
|
+
if (hasVisualState) return false;
|
|
125
|
+
|
|
126
|
+
const stories = def.stories || [];
|
|
127
|
+
for (let i = 0; i < stories.length; i++) {
|
|
128
|
+
const tree = stories[i] && stories[i].jsxTree ? stories[i].jsxTree : null;
|
|
129
|
+
if (!tree || tree.type !== 'element') continue;
|
|
130
|
+
const tagName = String((tree as JsxElement).tagName || '').toLowerCase();
|
|
131
|
+
if (
|
|
132
|
+
tagName === 'div'
|
|
133
|
+
|| tagName === 'section'
|
|
134
|
+
|| tagName === 'header'
|
|
135
|
+
|| tagName === 'footer'
|
|
136
|
+
|| tagName === 'main'
|
|
137
|
+
|| tagName === 'article'
|
|
138
|
+
|| tagName === 'nav'
|
|
139
|
+
) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
import { getPreviewTypography } from './typography';
|
|
147
|
+
export { getPreviewTypography };
|
|
148
|
+
|
|
149
|
+
export function measureTextWidth(label: string, options?: CreateTextOptions): number {
|
|
150
|
+
const node = createTextNode(label, options);
|
|
151
|
+
const width = node.width;
|
|
152
|
+
try {
|
|
153
|
+
node.remove();
|
|
154
|
+
} catch (_err) {
|
|
155
|
+
// ignore
|
|
156
|
+
}
|
|
157
|
+
return width;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function createPreviewLabelNode(label: string, classes: string[], colorGroup: Record<string, string>): TextNode {
|
|
161
|
+
const style = tailwindClassesToStyle(classes, 'default', colorGroup);
|
|
162
|
+
const textColor = style.text ? parseColor(style.text) :
|
|
163
|
+
(colorGroup.foreground ? parseColor(colorGroup.foreground) : { r: 0, g: 0, b: 0 });
|
|
164
|
+
const typography = getPreviewTypography(classes);
|
|
165
|
+
const text = createTextNode(label, { bold: typography.bold, fontSize: typography.fontSize, fill: textColor });
|
|
166
|
+
if (style.underline && text.textDecoration !== undefined) {
|
|
167
|
+
text.textDecoration = 'UNDERLINE';
|
|
168
|
+
}
|
|
169
|
+
return text;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Public: state preview block
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
export function createStatePreviewBlock(
|
|
177
|
+
def: ComponentDef,
|
|
178
|
+
story: ComponentStory,
|
|
179
|
+
theme: string,
|
|
180
|
+
ctx: StoryBuilderContext
|
|
181
|
+
): FrameNode | null {
|
|
182
|
+
if (!def || (def.type !== 'cva' && def.type !== 'state')) return null;
|
|
183
|
+
if (shouldSkipStatePreview(def)) return null;
|
|
184
|
+
const themeContext = getThemeContext(theme);
|
|
185
|
+
const colorGroup = themeContext.colorGroup;
|
|
186
|
+
const radiusGroup = themeContext.radiusGroup;
|
|
187
|
+
const instance = findMatchingInstance(def, story);
|
|
188
|
+
const props = instance && instance.props ? instance.props : {};
|
|
189
|
+
|
|
190
|
+
const matrixTitleStyle = { fontSize: 18, bold: true, opacity: 0.95 };
|
|
191
|
+
const axisLabelStyle = { fontSize: 13, bold: true, opacity: 0.78 };
|
|
192
|
+
const itemLabelStyle = { fontSize: 13, bold: true, opacity: 0.72 };
|
|
193
|
+
|
|
194
|
+
const label = buildStatePreviewLabel(def, instance);
|
|
195
|
+
|
|
196
|
+
function createStateCell(stateName: string, classes: string[], columnLabel: string): FrameNode {
|
|
197
|
+
if (String(def && def.type ? def.type : '').toLowerCase() === 'state') {
|
|
198
|
+
const baseProps = Object.assign({}, props || {});
|
|
199
|
+
const originalExtraClasses = splitClassName(props && props.className);
|
|
200
|
+
delete baseProps.className;
|
|
201
|
+
delete baseProps.state;
|
|
202
|
+
delete baseProps['data-state'];
|
|
203
|
+
delete baseProps.disabled;
|
|
204
|
+
delete baseProps['aria-disabled'];
|
|
205
|
+
delete baseProps['aria-invalid'];
|
|
206
|
+
delete baseProps.checked;
|
|
207
|
+
delete baseProps.defaultChecked;
|
|
208
|
+
delete baseProps['aria-checked'];
|
|
209
|
+
|
|
210
|
+
const normalizedState = String(stateName || 'default').trim().toLowerCase();
|
|
211
|
+
let visualState = normalizedState || 'default';
|
|
212
|
+
let checked = false;
|
|
213
|
+
|
|
214
|
+
if (visualState === 'checked') {
|
|
215
|
+
visualState = 'default';
|
|
216
|
+
checked = true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Keep only story-level extras (e.g. width wrappers) here.
|
|
220
|
+
// Visual state classes are applied in createStateStoryFrame via def.states.
|
|
221
|
+
if (originalExtraClasses.length > 0) {
|
|
222
|
+
baseProps.className = originalExtraClasses.join(' ');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (checked) {
|
|
226
|
+
baseProps.checked = 'true';
|
|
227
|
+
}
|
|
228
|
+
if (visualState && visualState !== 'default') {
|
|
229
|
+
baseProps.state = visualState;
|
|
230
|
+
}
|
|
231
|
+
if (visualState === 'disabled') {
|
|
232
|
+
baseProps.disabled = 'true';
|
|
233
|
+
} else if (visualState === 'error') {
|
|
234
|
+
baseProps['aria-invalid'] = 'true';
|
|
235
|
+
} else if (visualState === 'open') {
|
|
236
|
+
baseProps['data-state'] = 'open';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const stateNode = createStateStoryInstance(
|
|
240
|
+
def,
|
|
241
|
+
{ props: baseProps },
|
|
242
|
+
theme,
|
|
243
|
+
ctx,
|
|
244
|
+
story
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const cell = figma.createFrame();
|
|
248
|
+
cell.name = def.name + '/' + columnLabel + '/' + stateName;
|
|
249
|
+
cell.layoutMode = 'HORIZONTAL';
|
|
250
|
+
cell.primaryAxisSizingMode = 'AUTO';
|
|
251
|
+
cell.counterAxisSizingMode = 'AUTO';
|
|
252
|
+
cell.primaryAxisAlignItems = 'CENTER';
|
|
253
|
+
cell.counterAxisAlignItems = 'CENTER';
|
|
254
|
+
cell.fills = [];
|
|
255
|
+
cell.strokes = [];
|
|
256
|
+
ctx.applyClipBehavior(cell, []);
|
|
257
|
+
cell.appendChild(stateNode);
|
|
258
|
+
return cell;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const comp = figma.createFrame();
|
|
262
|
+
comp.name = def.name + '/' + columnLabel + '/' + stateName;
|
|
263
|
+
comp.primaryAxisSizingMode = 'AUTO';
|
|
264
|
+
comp.counterAxisSizingMode = 'AUTO';
|
|
265
|
+
comp.counterAxisAlignItems = 'CENTER';
|
|
266
|
+
comp.primaryAxisAlignItems = 'CENTER';
|
|
267
|
+
comp.itemSpacing = 8;
|
|
268
|
+
comp.fills = [];
|
|
269
|
+
comp.strokes = [];
|
|
270
|
+
ctx.applyClipBehavior(comp, classes);
|
|
271
|
+
|
|
272
|
+
applyTailwindStylesToFrame(comp, classes, colorGroup, radiusGroup, theme);
|
|
273
|
+
if (label && String(label).trim() !== '') {
|
|
274
|
+
comp.appendChild(createPreviewLabelNode(label, classes, colorGroup));
|
|
275
|
+
}
|
|
276
|
+
// Append any icon-like JSX-element child from the matched instance so
|
|
277
|
+
// icon-only components (e.g. Toggle with `<Bold />`) show their icon in
|
|
278
|
+
// every matrix cell instead of being invisible (default variant) or
|
|
279
|
+
// empty-bordered (outline variant). Use the first element child as a
|
|
280
|
+
// representative sample — the matrix is a state × variant grid, not a
|
|
281
|
+
// story-by-story render, so a single icon across cells is fine.
|
|
282
|
+
const jsxChildren = props && Array.isArray((props as { __jsxChildren?: unknown }).__jsxChildren)
|
|
283
|
+
? ((props as { __jsxChildren?: JsxNode[] }).__jsxChildren as JsxNode[])
|
|
284
|
+
: [];
|
|
285
|
+
for (let i = 0; i < jsxChildren.length; i++) {
|
|
286
|
+
const child = jsxChildren[i];
|
|
287
|
+
if (!child || (child as { type?: string }).type !== 'element') continue;
|
|
288
|
+
const iconNode = ctx.renderJsxTree(
|
|
289
|
+
child,
|
|
290
|
+
colorGroup,
|
|
291
|
+
radiusGroup,
|
|
292
|
+
theme,
|
|
293
|
+
0,
|
|
294
|
+
{ parentLayout: 'HORIZONTAL' }
|
|
295
|
+
);
|
|
296
|
+
if (iconNode) {
|
|
297
|
+
comp.appendChild(iconNode);
|
|
298
|
+
}
|
|
299
|
+
break; // one sample is enough
|
|
300
|
+
}
|
|
301
|
+
const ringInfo = getRingInfoFromClasses(classes, colorGroup);
|
|
302
|
+
if (ringInfo) markRingNode(comp, ringInfo);
|
|
303
|
+
|
|
304
|
+
const cell = figma.createFrame();
|
|
305
|
+
cell.name = def.name + '/' + columnLabel + '/' + stateName + '/Cell';
|
|
306
|
+
cell.layoutMode = 'HORIZONTAL';
|
|
307
|
+
cell.primaryAxisSizingMode = 'AUTO';
|
|
308
|
+
cell.counterAxisSizingMode = 'AUTO';
|
|
309
|
+
cell.primaryAxisAlignItems = 'CENTER';
|
|
310
|
+
cell.counterAxisAlignItems = 'CENTER';
|
|
311
|
+
cell.fills = [];
|
|
312
|
+
cell.strokes = [];
|
|
313
|
+
ctx.applyClipBehavior(cell, []);
|
|
314
|
+
cell.appendChild(comp);
|
|
315
|
+
// Comp is now in the cell — its dimensions are final, so we can safely
|
|
316
|
+
// apply the marked ring overlay. The host-FIXED-toggle inside
|
|
317
|
+
// applyRingIfPossible ensures appending the overlay can't inflate comp.
|
|
318
|
+
applyRingIfPossible(comp, cell);
|
|
319
|
+
return cell;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
type VariantColumn = { label: string; states: StateInfo[] };
|
|
323
|
+
const columns: VariantColumn[] = [];
|
|
324
|
+
|
|
325
|
+
if (def.type === 'cva' && def.variants && Object.keys(def.variants).length > 0) {
|
|
326
|
+
const variantKeys = Object.keys(def.variants);
|
|
327
|
+
const primaryKey = def.variants.variant ? 'variant' : variantKeys[0];
|
|
328
|
+
const primaryValues = def.variants[primaryKey] || [];
|
|
329
|
+
for (let i = 0; i < primaryValues.length; i++) {
|
|
330
|
+
const value = primaryValues[i];
|
|
331
|
+
const classes = buildCvaClassesForVariant(def, props, primaryKey, value);
|
|
332
|
+
const states = mergeStatesWithDefinition(extractStatesFromClasses(classes), def);
|
|
333
|
+
const labelText = value ? value.charAt(0).toUpperCase() + value.slice(1) : '';
|
|
334
|
+
columns.push({ label: labelText, states: states });
|
|
335
|
+
}
|
|
336
|
+
} else {
|
|
337
|
+
const classes = buildStoryStateClasses(def, instance);
|
|
338
|
+
if (classes.length === 0) return null;
|
|
339
|
+
const states = mergeStatesWithDefinition(extractStatesFromClasses(classes), def);
|
|
340
|
+
columns.push({ label: def.name || 'Component', states: states });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (columns.length === 0) return null;
|
|
344
|
+
|
|
345
|
+
const stateNames: string[] = columns.length > 0 ? getStateNames(columns[0].states) : [];
|
|
346
|
+
for (let i = 1; i < columns.length; i++) {
|
|
347
|
+
const names = getStateNames(columns[i].states);
|
|
348
|
+
for (let j = 0; j < names.length; j++) {
|
|
349
|
+
const name = names[j];
|
|
350
|
+
if (stateNames.indexOf(name) === -1) {
|
|
351
|
+
stateNames.push(name);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (stateNames.length <= 1) return null;
|
|
356
|
+
|
|
357
|
+
const block = figma.createFrame();
|
|
358
|
+
block.name = 'States';
|
|
359
|
+
block.layoutMode = 'VERTICAL';
|
|
360
|
+
block.itemSpacing = BOARD_LAYOUT.stateMatrixGap;
|
|
361
|
+
block.primaryAxisSizingMode = 'AUTO';
|
|
362
|
+
block.counterAxisSizingMode = 'AUTO';
|
|
363
|
+
block.fills = [];
|
|
364
|
+
ctx.applyClipBehavior(block, []);
|
|
365
|
+
block.appendChild(createTextNode('State Matrix', matrixTitleStyle));
|
|
366
|
+
|
|
367
|
+
const cellMatrix: FrameNode[][] = [];
|
|
368
|
+
const columnWidths: number[] = [];
|
|
369
|
+
for (let ci = 0; ci < columns.length; ci++) {
|
|
370
|
+
const column = columns[ci];
|
|
371
|
+
let maxWidth = measureTextWidth(column.label, itemLabelStyle);
|
|
372
|
+
const cells: FrameNode[] = [];
|
|
373
|
+
for (let si = 0; si < stateNames.length; si++) {
|
|
374
|
+
const stateName = stateNames[si];
|
|
375
|
+
const classes = buildStateClasses(column.states, stateName);
|
|
376
|
+
const cell = createStateCell(stateName, classes, column.label || def.name);
|
|
377
|
+
if (cell.width > maxWidth) maxWidth = cell.width;
|
|
378
|
+
cells.push(cell);
|
|
379
|
+
}
|
|
380
|
+
cellMatrix.push(cells);
|
|
381
|
+
columnWidths.push(maxWidth);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
let legendWidth = measureTextWidth('State', itemLabelStyle);
|
|
385
|
+
for (let i = 0; i < stateNames.length; i++) {
|
|
386
|
+
const width = measureTextWidth(stateNames[i], itemLabelStyle);
|
|
387
|
+
if (width > legendWidth) legendWidth = width;
|
|
388
|
+
}
|
|
389
|
+
legendWidth += 24;
|
|
390
|
+
|
|
391
|
+
function createLegendCell(textValue: string, style: CreateTextOptions): FrameNode {
|
|
392
|
+
const cell = figma.createFrame();
|
|
393
|
+
cell.layoutMode = 'VERTICAL';
|
|
394
|
+
cell.primaryAxisSizingMode = 'AUTO';
|
|
395
|
+
cell.counterAxisSizingMode = 'AUTO';
|
|
396
|
+
cell.fills = [];
|
|
397
|
+
cell.appendChild(createTextNode(textValue, style));
|
|
398
|
+
try {
|
|
399
|
+
cell.resize(legendWidth, cell.height);
|
|
400
|
+
cell.counterAxisSizingMode = 'FIXED';
|
|
401
|
+
} catch (_err) {
|
|
402
|
+
// ignore
|
|
403
|
+
}
|
|
404
|
+
return cell;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const axesRow = figma.createFrame();
|
|
408
|
+
axesRow.name = 'State Axes';
|
|
409
|
+
axesRow.layoutMode = 'HORIZONTAL';
|
|
410
|
+
axesRow.itemSpacing = BOARD_LAYOUT.stateMatrixAxisGap;
|
|
411
|
+
axesRow.primaryAxisSizingMode = 'AUTO';
|
|
412
|
+
axesRow.counterAxisSizingMode = 'AUTO';
|
|
413
|
+
axesRow.fills = [];
|
|
414
|
+
ctx.applyClipBehavior(axesRow, []);
|
|
415
|
+
axesRow.appendChild(createLegendCell('States', axisLabelStyle));
|
|
416
|
+
axesRow.appendChild(createTextNode('Variants', axisLabelStyle));
|
|
417
|
+
block.appendChild(axesRow);
|
|
418
|
+
|
|
419
|
+
const table = figma.createFrame();
|
|
420
|
+
table.name = 'State Table';
|
|
421
|
+
table.layoutMode = 'VERTICAL';
|
|
422
|
+
table.itemSpacing = BOARD_LAYOUT.stateMatrixGap;
|
|
423
|
+
table.primaryAxisSizingMode = 'AUTO';
|
|
424
|
+
table.counterAxisSizingMode = 'AUTO';
|
|
425
|
+
table.fills = [];
|
|
426
|
+
ctx.applyClipBehavior(table, []);
|
|
427
|
+
|
|
428
|
+
const tableHeader = figma.createFrame();
|
|
429
|
+
tableHeader.name = 'State Table Header';
|
|
430
|
+
tableHeader.layoutMode = 'HORIZONTAL';
|
|
431
|
+
tableHeader.itemSpacing = BOARD_LAYOUT.stateMatrixAxisGap;
|
|
432
|
+
tableHeader.primaryAxisSizingMode = 'AUTO';
|
|
433
|
+
tableHeader.counterAxisSizingMode = 'AUTO';
|
|
434
|
+
tableHeader.fills = [];
|
|
435
|
+
tableHeader.appendChild(createLegendCell('State', itemLabelStyle));
|
|
436
|
+
for (let ci = 0; ci < columns.length; ci++) {
|
|
437
|
+
const headerCell = figma.createFrame();
|
|
438
|
+
headerCell.layoutMode = 'VERTICAL';
|
|
439
|
+
headerCell.primaryAxisSizingMode = 'AUTO';
|
|
440
|
+
headerCell.counterAxisSizingMode = 'AUTO';
|
|
441
|
+
headerCell.fills = [];
|
|
442
|
+
headerCell.appendChild(createTextNode(columns[ci].label, itemLabelStyle));
|
|
443
|
+
try {
|
|
444
|
+
headerCell.resize(columnWidths[ci], headerCell.height);
|
|
445
|
+
headerCell.counterAxisSizingMode = 'FIXED';
|
|
446
|
+
} catch (_err) {
|
|
447
|
+
// ignore
|
|
448
|
+
}
|
|
449
|
+
tableHeader.appendChild(headerCell);
|
|
450
|
+
}
|
|
451
|
+
table.appendChild(tableHeader);
|
|
452
|
+
|
|
453
|
+
for (let si = 0; si < stateNames.length; si++) {
|
|
454
|
+
const row = figma.createFrame();
|
|
455
|
+
row.name = 'State Row/' + stateNames[si];
|
|
456
|
+
row.layoutMode = 'HORIZONTAL';
|
|
457
|
+
row.itemSpacing = BOARD_LAYOUT.stateMatrixAxisGap;
|
|
458
|
+
row.primaryAxisSizingMode = 'AUTO';
|
|
459
|
+
row.counterAxisSizingMode = 'AUTO';
|
|
460
|
+
row.counterAxisAlignItems = 'CENTER';
|
|
461
|
+
row.fills = [];
|
|
462
|
+
ctx.applyClipBehavior(row, []);
|
|
463
|
+
row.appendChild(createLegendCell(stateNames[si], itemLabelStyle));
|
|
464
|
+
for (let ci = 0; ci < columns.length; ci++) {
|
|
465
|
+
const cell = cellMatrix[ci][si];
|
|
466
|
+
try {
|
|
467
|
+
cell.resize(columnWidths[ci], cell.height);
|
|
468
|
+
if (cell.layoutMode === 'HORIZONTAL') {
|
|
469
|
+
cell.primaryAxisSizingMode = 'FIXED';
|
|
470
|
+
} else if (cell.layoutMode === 'VERTICAL') {
|
|
471
|
+
cell.counterAxisSizingMode = 'FIXED';
|
|
472
|
+
}
|
|
473
|
+
} catch (_err) {
|
|
474
|
+
// ignore
|
|
475
|
+
}
|
|
476
|
+
row.appendChild(cell);
|
|
477
|
+
}
|
|
478
|
+
table.appendChild(row);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
block.appendChild(table);
|
|
482
|
+
tagGeneratedSubtree(block, 'state-preview-block:' + def.name + ':' + (story && story.name ? story.name : 'default'));
|
|
483
|
+
return block;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ---------------------------------------------------------------------------
|
|
487
|
+
// Public: responsive preview block
|
|
488
|
+
// ---------------------------------------------------------------------------
|
|
489
|
+
|
|
490
|
+
export function createResponsivePreviewBlock(
|
|
491
|
+
def: ComponentDef,
|
|
492
|
+
story: ComponentStory,
|
|
493
|
+
theme: string,
|
|
494
|
+
ctx: StoryBuilderContext
|
|
495
|
+
): FrameNode | null {
|
|
496
|
+
if (!def) return null;
|
|
497
|
+
const instance = findMatchingInstance(def, story);
|
|
498
|
+
const layoutClasses = normalizeLayoutClasses(story && story.layoutClasses);
|
|
499
|
+
const layoutHasResponsive = hasSignificantResponsiveChanges(layoutClasses);
|
|
500
|
+
// Use the same portal-filtering logic as main rendering so trigger-only stories
|
|
501
|
+
// don't pick up sm: classes from portal content (e.g. Dialog's sm:max-w-lg).
|
|
502
|
+
const effectiveJsxTree = story && story.jsxTree
|
|
503
|
+
? resolvePortalAwareStoryTree(story.jsxTree)
|
|
504
|
+
: null;
|
|
505
|
+
const treeHasResponsive = effectiveJsxTree ? treeHasResponsiveClasses(effectiveJsxTree as JsxNode) : false;
|
|
506
|
+
const instanceClasses = buildStoryInstanceClasses(def, instance);
|
|
507
|
+
const instanceHasResponsive = hasSignificantResponsiveChanges(instanceClasses);
|
|
508
|
+
if (!layoutHasResponsive && !treeHasResponsive && !instanceHasResponsive) return null;
|
|
509
|
+
|
|
510
|
+
const sourceClasses: string[] = [];
|
|
511
|
+
if (layoutHasResponsive && layoutClasses.length > 0) {
|
|
512
|
+
for (let i = 0; i < layoutClasses.length; i++) sourceClasses.push(layoutClasses[i]);
|
|
513
|
+
}
|
|
514
|
+
if (treeHasResponsive && effectiveJsxTree) {
|
|
515
|
+
collectTreeClasses(effectiveJsxTree as JsxNode, sourceClasses);
|
|
516
|
+
}
|
|
517
|
+
if (sourceClasses.length === 0) {
|
|
518
|
+
for (let i = 0; i < instanceClasses.length; i++) sourceClasses.push(instanceClasses[i]);
|
|
519
|
+
}
|
|
520
|
+
const breakpoints = extractBreakpointsFromClasses(sourceClasses);
|
|
521
|
+
if (breakpoints.length <= 1) return null;
|
|
522
|
+
const entries: Array<{
|
|
523
|
+
bp: { name: string; minWidth: number };
|
|
524
|
+
layoutOverride: string[];
|
|
525
|
+
treeOverride: JsxNode | null;
|
|
526
|
+
instanceOverride: string[];
|
|
527
|
+
signature: string;
|
|
528
|
+
}> = [];
|
|
529
|
+
for (let i = 0; i < breakpoints.length; i++) {
|
|
530
|
+
const bp = breakpoints[i];
|
|
531
|
+
const layoutOverride = layoutClasses.length > 0 ? getClassesForBreakpoint(layoutClasses, bp.name) : layoutClasses;
|
|
532
|
+
let treeOverride: JsxNode | null = effectiveJsxTree ?? null;
|
|
533
|
+
if (treeHasResponsive && effectiveJsxTree) {
|
|
534
|
+
// Clone for this breakpoint, then propagate *:X child-wildcard classes so
|
|
535
|
+
// the renderer sees explicit per-element classes (e.g. *:w-full on a flex-col
|
|
536
|
+
// footer → w-full on each button at base, *:w-auto → w-auto at sm+).
|
|
537
|
+
// Propagation runs here — after breakpoint mapping — so the main story render
|
|
538
|
+
// path is unaffected and keeps its original auto-sizing behaviour.
|
|
539
|
+
treeOverride = propagateChildSelectorClasses(
|
|
540
|
+
cloneJsxNodeForBreakpoint(effectiveJsxTree as JsxNode, bp.name)
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
const instanceOverride = getClassesForBreakpoint(instanceClasses, bp.name);
|
|
544
|
+
let signature = 'I:' + uniqueClassSignature(instanceOverride);
|
|
545
|
+
if (layoutHasResponsive || treeHasResponsive) {
|
|
546
|
+
signature = 'L:' + uniqueClassSignature(layoutOverride) + '|T:' + treeClassSignature(treeOverride as JsxNode);
|
|
547
|
+
}
|
|
548
|
+
entries.push({
|
|
549
|
+
bp: { name: bp.name, minWidth: bp.minWidth },
|
|
550
|
+
layoutOverride: layoutOverride,
|
|
551
|
+
treeOverride: treeOverride,
|
|
552
|
+
instanceOverride: instanceOverride,
|
|
553
|
+
signature: signature,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
if (entries.length <= 1) return null;
|
|
557
|
+
// Drop breakpoints where the root element resolves to `display: none` at
|
|
558
|
+
// that breakpoint (e.g. `md:hidden` hides the whole component at md+). Walk
|
|
559
|
+
// the leading single-child spine so a decorator-wrapped component still
|
|
560
|
+
// exposes the inner root's visibility intent; deep-child `hidden` on a
|
|
561
|
+
// sibling branch is ignored.
|
|
562
|
+
const rootClassSet: string[] = [];
|
|
563
|
+
let spineNode: JsxNode | null | undefined = effectiveJsxTree;
|
|
564
|
+
let spineDepth = 0;
|
|
565
|
+
while (spineNode && spineNode.type === 'element' && spineDepth < 4) {
|
|
566
|
+
const el = spineNode as JsxElement;
|
|
567
|
+
const cls = el.props && el.props.className;
|
|
568
|
+
if (typeof cls === 'string') {
|
|
569
|
+
const list = splitClassName(cls);
|
|
570
|
+
for (let i = 0; i < list.length; i++) rootClassSet.push(list[i]);
|
|
571
|
+
}
|
|
572
|
+
const children = el.children || [];
|
|
573
|
+
if (children.length !== 1 || !children[0] || children[0].type !== 'element') break;
|
|
574
|
+
spineNode = children[0];
|
|
575
|
+
spineDepth++;
|
|
576
|
+
}
|
|
577
|
+
for (let i = 0; i < layoutClasses.length; i++) rootClassSet.push(layoutClasses[i]);
|
|
578
|
+
const visibleEntries = rootClassSet.length > 0
|
|
579
|
+
? entries.filter(e => !isHiddenAtBreakpoint(rootClassSet, e.bp.name))
|
|
580
|
+
: entries;
|
|
581
|
+
if (visibleEntries.length <= 1) return null;
|
|
582
|
+
const filteredEntries: typeof entries = [];
|
|
583
|
+
const baseSignature = visibleEntries[0].signature;
|
|
584
|
+
const seenSignatures: Record<string, boolean> = {};
|
|
585
|
+
seenSignatures[baseSignature] = true;
|
|
586
|
+
filteredEntries.push(visibleEntries[0]);
|
|
587
|
+
for (let i = 1; i < visibleEntries.length; i++) {
|
|
588
|
+
const entry = visibleEntries[i];
|
|
589
|
+
if (entry.signature === baseSignature) continue;
|
|
590
|
+
if (seenSignatures[entry.signature]) continue;
|
|
591
|
+
seenSignatures[entry.signature] = true;
|
|
592
|
+
filteredEntries.push(entry);
|
|
593
|
+
}
|
|
594
|
+
if (filteredEntries.length <= 1) return null;
|
|
595
|
+
|
|
596
|
+
const themeContext = getThemeContext(theme);
|
|
597
|
+
const colorGroup = themeContext.colorGroup;
|
|
598
|
+
const radiusGroup = themeContext.radiusGroup;
|
|
599
|
+
const label = buildStatePreviewLabel(def, instance);
|
|
600
|
+
|
|
601
|
+
const block = figma.createFrame();
|
|
602
|
+
block.name = 'Responsive';
|
|
603
|
+
block.layoutMode = 'VERTICAL';
|
|
604
|
+
block.itemSpacing = BOARD_LAYOUT.responsiveBlockGap;
|
|
605
|
+
block.primaryAxisSizingMode = 'AUTO';
|
|
606
|
+
block.counterAxisSizingMode = 'AUTO';
|
|
607
|
+
block.fills = [];
|
|
608
|
+
ctx.applyClipBehavior(block, []);
|
|
609
|
+
|
|
610
|
+
block.appendChild(createTextNode('Responsive', { fontSize: 16, lineHeight: 22, opacity: 0.6 }));
|
|
611
|
+
|
|
612
|
+
const row = figma.createFrame();
|
|
613
|
+
row.name = 'Responsive Strip';
|
|
614
|
+
row.layoutMode = 'HORIZONTAL';
|
|
615
|
+
row.itemSpacing = BOARD_LAYOUT.responsiveColumnGap;
|
|
616
|
+
row.primaryAxisSizingMode = 'AUTO';
|
|
617
|
+
row.counterAxisSizingMode = 'AUTO';
|
|
618
|
+
row.fills = [];
|
|
619
|
+
ctx.applyClipBehavior(row, []);
|
|
620
|
+
|
|
621
|
+
for (let i = 0; i < filteredEntries.length; i++) {
|
|
622
|
+
const entry = filteredEntries[i];
|
|
623
|
+
const bp = entry.bp;
|
|
624
|
+
const viewportWidth = getResponsivePreviewWidth(bp.name, bp.minWidth);
|
|
625
|
+
|
|
626
|
+
const col = figma.createFrame();
|
|
627
|
+
col.name = getBreakpointLabel(bp.name, bp.minWidth);
|
|
628
|
+
col.layoutMode = 'VERTICAL';
|
|
629
|
+
col.itemSpacing = BOARD_LAYOUT.responsiveLabelGap;
|
|
630
|
+
col.primaryAxisSizingMode = 'AUTO';
|
|
631
|
+
col.counterAxisSizingMode = 'AUTO';
|
|
632
|
+
col.fills = [];
|
|
633
|
+
|
|
634
|
+
col.appendChild(createTextNode(getBreakpointLabel(bp.name, bp.minWidth), { fontSize: 12, bold: true, opacity: 0.6 }));
|
|
635
|
+
const viewport = figma.createFrame();
|
|
636
|
+
viewport.name = 'Viewport';
|
|
637
|
+
viewport.layoutMode = 'VERTICAL';
|
|
638
|
+
viewport.itemSpacing = 0;
|
|
639
|
+
viewport.primaryAxisSizingMode = 'AUTO';
|
|
640
|
+
viewport.counterAxisSizingMode = 'FIXED';
|
|
641
|
+
viewport.fills = [];
|
|
642
|
+
viewport.strokes = [];
|
|
643
|
+
// Keep responsive previews fully visible vertically; clipping here caused
|
|
644
|
+
// taller panels (e.g. Dialog OpenPanel/Confirm) to appear truncated.
|
|
645
|
+
viewport.clipsContent = false;
|
|
646
|
+
viewport.resize(viewportWidth, viewport.height);
|
|
647
|
+
|
|
648
|
+
if (layoutHasResponsive || treeHasResponsive) {
|
|
649
|
+
const layoutOverride = entry.layoutOverride;
|
|
650
|
+
const treeOverride = entry.treeOverride;
|
|
651
|
+
const storyOverride = Object.assign({}, story, { layoutClasses: layoutOverride, jsxTree: treeOverride });
|
|
652
|
+
const preview = renderStandaloneStory(storyOverride, theme, ctx, viewportWidth);
|
|
653
|
+
if (preview) {
|
|
654
|
+
const colsOverride = treeOverride ? extractRootGridColsFromTree(treeOverride as JsxNode) : null;
|
|
655
|
+
// The `cols > 1` part of the gate now lives in
|
|
656
|
+
// `applyGridColumnsIfPossible`, so a `cols === 1` forward is a
|
|
657
|
+
// safe no-op. Keep the `preview.children.length === 1` check —
|
|
658
|
+
// that's preview-builder-specific (only reflow when the
|
|
659
|
+
// standalone-story render produced a single root frame to
|
|
660
|
+
// re-grid).
|
|
661
|
+
if (colsOverride != null && preview.children.length === 1) {
|
|
662
|
+
const target = preview.children[0];
|
|
663
|
+
if ('layoutMode' in target) {
|
|
664
|
+
const previewPadding = (preview.paddingLeft || 0) + (preview.paddingRight || 0);
|
|
665
|
+
const minWidth = defaultGridWidth(colsOverride);
|
|
666
|
+
let previewWidth = Number.isFinite(preview.width) ? preview.width - previewPadding : undefined;
|
|
667
|
+
if (!Number.isFinite(previewWidth) || (previewWidth != null && previewWidth < minWidth)) {
|
|
668
|
+
previewWidth = minWidth;
|
|
669
|
+
}
|
|
670
|
+
ctx.applyGridColumnsWithReflow(target as FrameNode, previewWidth, colsOverride);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if ('layoutAlign' in preview) {
|
|
674
|
+
preview.layoutAlign = 'STRETCH';
|
|
675
|
+
}
|
|
676
|
+
viewport.appendChild(preview);
|
|
677
|
+
}
|
|
678
|
+
} else {
|
|
679
|
+
const bpClasses = entry.instanceOverride;
|
|
680
|
+
const comp = figma.createFrame();
|
|
681
|
+
comp.name = def.name + '/' + bp.name;
|
|
682
|
+
comp.primaryAxisSizingMode = 'AUTO';
|
|
683
|
+
comp.counterAxisSizingMode = 'AUTO';
|
|
684
|
+
comp.counterAxisAlignItems = 'CENTER';
|
|
685
|
+
comp.primaryAxisAlignItems = 'CENTER';
|
|
686
|
+
comp.itemSpacing = 8;
|
|
687
|
+
comp.fills = [];
|
|
688
|
+
comp.strokes = [];
|
|
689
|
+
ctx.applyClipBehavior(comp, bpClasses);
|
|
690
|
+
|
|
691
|
+
applyTailwindStylesToFrame(comp, bpClasses, colorGroup, radiusGroup, theme);
|
|
692
|
+
comp.appendChild(createPreviewLabelNode(label, bpClasses, colorGroup));
|
|
693
|
+
|
|
694
|
+
if ('layoutAlign' in comp) {
|
|
695
|
+
comp.layoutAlign = 'STRETCH';
|
|
696
|
+
}
|
|
697
|
+
viewport.appendChild(comp);
|
|
698
|
+
}
|
|
699
|
+
col.appendChild(viewport);
|
|
700
|
+
row.appendChild(col);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
block.appendChild(row);
|
|
704
|
+
tagGeneratedSubtree(block, 'responsive-preview-block:' + def.name + ':' + (story && story.name ? story.name : 'default'));
|
|
705
|
+
return block;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// ---------------------------------------------------------------------------
|
|
709
|
+
// Public: gate functions (decide whether to render previews)
|
|
710
|
+
// ---------------------------------------------------------------------------
|
|
711
|
+
|
|
712
|
+
export function shouldRenderStatesForStory(def: ComponentDef, story: ComponentStory, storyIndex: number): boolean {
|
|
713
|
+
// Slider / Progress / future Tabs are mis-classified as `type:'state'` by
|
|
714
|
+
// the scanner but are structurally compound — the State Matrix renderer
|
|
715
|
+
// (createStateStoryFrame) can't reconstruct their geometry and would
|
|
716
|
+
// emit empty rows ("default" / "focus" / "disabled" with nothing in
|
|
717
|
+
// them). The same nested-data-slot signal we use to gate the
|
|
718
|
+
// tree-vs-instance choice also gates this preview block.
|
|
719
|
+
if (story && jsxTreeHasNestedDataSlots(story.jsxTree as JsxNode)) {
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
const name = String(story && story.name ? story.name : '').toLowerCase();
|
|
723
|
+
if (name && (name === 'default' || name.indexOf('default') !== -1)) {
|
|
724
|
+
return true;
|
|
725
|
+
}
|
|
726
|
+
const stories = def && def.stories ? def.stories : [];
|
|
727
|
+
let hasDefault = false;
|
|
728
|
+
for (let i = 0; i < stories.length; i++) {
|
|
729
|
+
const storyName = String(stories[i] && stories[i].name ? stories[i].name : '').toLowerCase();
|
|
730
|
+
if (storyName && (storyName === 'default' || storyName.indexOf('default') !== -1)) {
|
|
731
|
+
hasDefault = true;
|
|
732
|
+
break;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
if (hasDefault) return false;
|
|
736
|
+
return storyIndex === 0;
|
|
737
|
+
}
|
|
738
|
+
|