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,229 @@
|
|
|
1
|
+
import { parseColor, resolveTextColorValue, type RGB } from '../tokens';
|
|
2
|
+
import {
|
|
3
|
+
ICON_PATHS,
|
|
4
|
+
createIcon,
|
|
5
|
+
createIconFromSvg,
|
|
6
|
+
getVectorPaintUsage,
|
|
7
|
+
resizeSvgNodeTo,
|
|
8
|
+
resolveIconSizeFromClasses,
|
|
9
|
+
wrapIconNode,
|
|
10
|
+
} from '../effects';
|
|
11
|
+
import { getIconRegistryEntry } from '../components';
|
|
12
|
+
import type { RenderContext } from './render-context';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Icon-rendering layer for the design-system page builder. Sits between
|
|
16
|
+
* effects/icon-builder.ts (which creates raw VECTOR / SVG nodes) and
|
|
17
|
+
* ui-builder's renderJsxTree (which classifies a JSX element as an icon
|
|
18
|
+
* and asks for one).
|
|
19
|
+
*
|
|
20
|
+
* Two source paths:
|
|
21
|
+
* - **Mapped icons**: Tailwind class names like `<X />` map to one of the
|
|
22
|
+
* plugin's hand-rolled `ICON_PATHS` SVGs (close, chevron-down, …).
|
|
23
|
+
* - **Registry icons**: scanner-extracted SVGs for arbitrary icon-library
|
|
24
|
+
* imports (Lucide, Heroicons, Phosphor, Tabler, …) keyed by component
|
|
25
|
+
* name.
|
|
26
|
+
*
|
|
27
|
+
* Both paths normalize size from class strings (`size-4`, `h-5 w-5`) and
|
|
28
|
+
* pad strokes so corners aren't clipped.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
function resolveContextTextColorValue(
|
|
32
|
+
context: RenderContext,
|
|
33
|
+
colorGroup: Record<string, string>,
|
|
34
|
+
theme: string
|
|
35
|
+
): string | RGB | null {
|
|
36
|
+
return resolveTextColorValue(context.textColor, context.textColorToken, colorGroup, theme);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function parseInlineStyleColor(style: unknown): string | null {
|
|
40
|
+
if (!style) return null;
|
|
41
|
+
if (typeof style === 'object') {
|
|
42
|
+
const raw = (style as Record<string, unknown>).color;
|
|
43
|
+
if (typeof raw === 'string' && raw.trim() !== '') return raw.trim();
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
if (typeof style === 'string') {
|
|
47
|
+
const match = style.match(/(?:^|;)\s*color\s*:\s*([^;]+)/i);
|
|
48
|
+
if (match) return match[1].trim();
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function resolveIconForegroundColor(
|
|
54
|
+
context: RenderContext,
|
|
55
|
+
colorGroup: Record<string, string>,
|
|
56
|
+
theme: string,
|
|
57
|
+
props?: Record<string, unknown>
|
|
58
|
+
): RGB {
|
|
59
|
+
// Inline `style={{ color: "..." }}` (common for react-icons / SiFigma etc.)
|
|
60
|
+
// takes precedence over class-inherited text color.
|
|
61
|
+
if (props) {
|
|
62
|
+
const inline = parseInlineStyleColor(props.style);
|
|
63
|
+
if (inline) return parseColor(inline);
|
|
64
|
+
}
|
|
65
|
+
const resolvedTextColor = resolveContextTextColorValue(context, colorGroup, theme);
|
|
66
|
+
if (resolvedTextColor) return parseColor(resolvedTextColor);
|
|
67
|
+
if (colorGroup.foreground) return parseColor(colorGroup.foreground);
|
|
68
|
+
return { r: 0.1, g: 0.1, b: 0.1 };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function wrapSvgIconWithSize(
|
|
72
|
+
icon: SceneNode,
|
|
73
|
+
width: number,
|
|
74
|
+
height: number,
|
|
75
|
+
name: string
|
|
76
|
+
): FrameNode {
|
|
77
|
+
resizeSvgNodeTo(icon, width, height);
|
|
78
|
+
const usage = getVectorPaintUsage(icon);
|
|
79
|
+
const pad = usage.hasStrokes ? 1 : 0;
|
|
80
|
+
return wrapIconNode(icon, width, height, pad, name);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function wrapMappedIconWithSize(
|
|
84
|
+
icon: SceneNode,
|
|
85
|
+
iconKey: string,
|
|
86
|
+
width: number,
|
|
87
|
+
height: number,
|
|
88
|
+
name: string,
|
|
89
|
+
rotate: boolean
|
|
90
|
+
): FrameNode {
|
|
91
|
+
resizeSvgNodeTo(icon, width, height);
|
|
92
|
+
const pad = ICON_PATHS[iconKey].stroke
|
|
93
|
+
? Math.max(1, Math.ceil((ICON_PATHS[iconKey].strokeWidth || 1.5) / 2))
|
|
94
|
+
: 0;
|
|
95
|
+
const wrapped = wrapIconNode(icon, width, height, pad, name);
|
|
96
|
+
if (rotate) {
|
|
97
|
+
wrapped.rotation = 180;
|
|
98
|
+
}
|
|
99
|
+
return wrapped;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function wrapSvgIcon(
|
|
103
|
+
icon: SceneNode,
|
|
104
|
+
classes: string[],
|
|
105
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
106
|
+
props: Record<string, any>,
|
|
107
|
+
name: string
|
|
108
|
+
): FrameNode {
|
|
109
|
+
const normalizedProps: Record<string, string> = {};
|
|
110
|
+
const source = props || {};
|
|
111
|
+
const keys = Object.keys(source);
|
|
112
|
+
for (let i = 0; i < keys.length; i++) {
|
|
113
|
+
const key = keys[i];
|
|
114
|
+
const value = source[key];
|
|
115
|
+
if (value == null) continue;
|
|
116
|
+
normalizedProps[key] = String(value);
|
|
117
|
+
}
|
|
118
|
+
const size = resolveIconSizeFromClasses(classes, normalizedProps);
|
|
119
|
+
return wrapSvgIconWithSize(icon, size.width, size.height, name);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const ICON_KEY_ALIASES: Record<string, string> = {
|
|
123
|
+
x: 'close',
|
|
124
|
+
xicon: 'close',
|
|
125
|
+
closeicon: 'close',
|
|
126
|
+
menuicon: 'menu',
|
|
127
|
+
hambugericon: 'menu',
|
|
128
|
+
checkicon: 'check',
|
|
129
|
+
chevrondownicon: 'chevron-down',
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
function toIconKey(tagName: string): string | null {
|
|
133
|
+
const raw = String(tagName || '').trim();
|
|
134
|
+
if (!raw) return null;
|
|
135
|
+
const basename = raw.split('.').pop() || raw;
|
|
136
|
+
const hasNamespace = raw.includes('.');
|
|
137
|
+
const namespace = hasNamespace ? raw.slice(0, raw.lastIndexOf('.')) : '';
|
|
138
|
+
const basenameIsExplicitIcon = /(?:icon|svg)$/i.test(basename);
|
|
139
|
+
const namespaceLooksIconish = /(?:icon|icons|lucide|heroicons|phosphor|tabler)/i.test(namespace);
|
|
140
|
+
// Avoid collapsing non-icon primitives/components (e.g. DialogPrimitive.Close)
|
|
141
|
+
// into icon nodes just because their basename happens to match an icon alias.
|
|
142
|
+
if (hasNamespace && !basenameIsExplicitIcon && !namespaceLooksIconish) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const compact = basename.replace(/[^a-zA-Z0-9]/g, '');
|
|
146
|
+
const kebab = compact
|
|
147
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
148
|
+
.replace(/^-+|-+$/g, '')
|
|
149
|
+
.toLowerCase();
|
|
150
|
+
if (ICON_PATHS[kebab]) return kebab;
|
|
151
|
+
|
|
152
|
+
const compactLower = compact.toLowerCase();
|
|
153
|
+
const alias = ICON_KEY_ALIASES[compactLower];
|
|
154
|
+
if (alias && ICON_PATHS[alias]) return alias;
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function resolveRegistryIconSvg(tagName: string): string | null {
|
|
159
|
+
const raw = String(tagName || '').trim();
|
|
160
|
+
if (!raw) return null;
|
|
161
|
+
|
|
162
|
+
const candidates = new Set<string>();
|
|
163
|
+
candidates.add(raw);
|
|
164
|
+
|
|
165
|
+
const basename = raw.split('.').pop() || raw;
|
|
166
|
+
if (basename) candidates.add(basename);
|
|
167
|
+
|
|
168
|
+
if (basename.endsWith('Icon')) {
|
|
169
|
+
candidates.add(basename.slice(0, -4));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
for (const candidate of candidates) {
|
|
173
|
+
const entry = getIconRegistryEntry(candidate);
|
|
174
|
+
if (entry && typeof entry.svg === 'string' && entry.svg.trim()) {
|
|
175
|
+
return entry.svg;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function renderRegistryIcon(
|
|
183
|
+
tagName: string,
|
|
184
|
+
classes: string[],
|
|
185
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
186
|
+
props: Record<string, any>,
|
|
187
|
+
fgColor: RGB
|
|
188
|
+
): FrameNode | null {
|
|
189
|
+
const svg = resolveRegistryIconSvg(tagName);
|
|
190
|
+
const icon = svg ? createIconFromSvg(svg, fgColor) : null;
|
|
191
|
+
if (!icon) return null;
|
|
192
|
+
const normalizedProps: Record<string, string> = {};
|
|
193
|
+
const source = props || {};
|
|
194
|
+
const keys = Object.keys(source);
|
|
195
|
+
for (let i = 0; i < keys.length; i++) {
|
|
196
|
+
const propKey = keys[i];
|
|
197
|
+
const value = source[propKey];
|
|
198
|
+
if (value == null) continue;
|
|
199
|
+
normalizedProps[propKey] = String(value);
|
|
200
|
+
}
|
|
201
|
+
const size = resolveIconSizeFromClasses(classes, normalizedProps);
|
|
202
|
+
return wrapSvgIconWithSize(icon, size.width, size.height, String(tagName || 'icon/svg'));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function renderMappedIcon(
|
|
206
|
+
tagName: string,
|
|
207
|
+
classes: string[],
|
|
208
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
209
|
+
props: Record<string, any>,
|
|
210
|
+
fgColor: RGB,
|
|
211
|
+
rotateChevron: boolean
|
|
212
|
+
): FrameNode | null {
|
|
213
|
+
const key = toIconKey(tagName);
|
|
214
|
+
if (!key) return null;
|
|
215
|
+
const icon = createIcon(key, fgColor);
|
|
216
|
+
if (!icon) return null;
|
|
217
|
+
const normalizedProps: Record<string, string> = {};
|
|
218
|
+
const source = props || {};
|
|
219
|
+
const keys = Object.keys(source);
|
|
220
|
+
for (let i = 0; i < keys.length; i++) {
|
|
221
|
+
const propKey = keys[i];
|
|
222
|
+
const value = source[propKey];
|
|
223
|
+
if (value == null) continue;
|
|
224
|
+
normalizedProps[propKey] = String(value);
|
|
225
|
+
}
|
|
226
|
+
const size = resolveIconSizeFromClasses(classes, normalizedProps);
|
|
227
|
+
const shouldRotate = rotateChevron && key === 'chevron-down';
|
|
228
|
+
return wrapMappedIconWithSize(icon, key, size.width, size.height, 'icon/' + key, shouldRotate);
|
|
229
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export * from './design-system';
|
|
2
|
+
export * from './ui-builder';
|
|
3
|
+
export * from './story-builder-context';
|
|
4
|
+
export * from './story-layout';
|
|
5
|
+
export * from './preview-builder';
|
|
6
|
+
export * from './render-context';
|
|
7
|
+
export * from './generated-node';
|
|
8
|
+
|
|
9
|
+
// story-builder has `createUIComponents` / `pruneGeneratedComponentLibrary`
|
|
10
|
+
// that ui-builder re-exports as wrappers; the public API comes from
|
|
11
|
+
// ui-builder. Only story-builder's `computePreflightData` is consumed
|
|
12
|
+
// externally, so re-export that one explicitly.
|
|
13
|
+
export { computePreflightData } from './story-builder';
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { applyTailwindStylesToFrame, splitClassName } from '../tailwind';
|
|
2
|
+
import {
|
|
3
|
+
createCompoundComponent,
|
|
4
|
+
tryCreateNonCvaComponentInstance as tryCreateNonCvaComponentInstanceShared,
|
|
5
|
+
type ComponentDef,
|
|
6
|
+
type ComponentInstance,
|
|
7
|
+
type ComponentStory,
|
|
8
|
+
type CvaAnalysis,
|
|
9
|
+
type LayoutInfo,
|
|
10
|
+
} from '../components';
|
|
11
|
+
import { createTextNode } from '../text';
|
|
12
|
+
import {
|
|
13
|
+
componentInstanceBackend,
|
|
14
|
+
createCVAStoryInstance,
|
|
15
|
+
createStateStoryInstance,
|
|
16
|
+
createSimpleStoryInstance,
|
|
17
|
+
} from './story-instance';
|
|
18
|
+
import { createSimpleStoryFrame } from './story-frames';
|
|
19
|
+
import { normalizeComponentName } from './story-tree-search';
|
|
20
|
+
import { isTruthyStateProp } from './state-master';
|
|
21
|
+
import { setGeneratedFallbackReason } from './generated-node';
|
|
22
|
+
import { getNonCvaSymbolFallbackReason } from './symbol-fallback';
|
|
23
|
+
import { parseRenderPropToSyntheticInstance } from './render-prop-parser';
|
|
24
|
+
import type { StoryBuilderContext, StoryRenderContext } from './story-builder-context';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Renders the JSX children of a trigger component (e.g. `<DialogTrigger>` /
|
|
28
|
+
* `<SheetTrigger>`) directly — the trigger itself is invisible at runtime but
|
|
29
|
+
* its visible children (the button, link, etc.) are what the user sees on the
|
|
30
|
+
* page. Used inside `renderGuardedPortalStoryInstances`.
|
|
31
|
+
*/
|
|
32
|
+
function renderGuardedTriggerJsxChildren(
|
|
33
|
+
instance: ComponentInstance,
|
|
34
|
+
theme: string,
|
|
35
|
+
colorGroup: Record<string, string>,
|
|
36
|
+
radiusGroup: Record<string, string> | null,
|
|
37
|
+
ctx: StoryBuilderContext
|
|
38
|
+
): SceneNode[] {
|
|
39
|
+
const props = instance && instance.props ? instance.props : {};
|
|
40
|
+
const jsxChildren = Array.isArray(props.__jsxChildren) ? props.__jsxChildren : [];
|
|
41
|
+
if (jsxChildren.length === 0) return [];
|
|
42
|
+
|
|
43
|
+
const nodes: SceneNode[] = [];
|
|
44
|
+
const triggerContext: StoryRenderContext = { parentLayout: 'HORIZONTAL' };
|
|
45
|
+
for (let i = 0; i < jsxChildren.length; i++) {
|
|
46
|
+
const child = jsxChildren[i];
|
|
47
|
+
if (!child || typeof child !== 'object') continue;
|
|
48
|
+
const rendered = ctx.renderJsxTree(child, colorGroup, radiusGroup, theme, 0, triggerContext);
|
|
49
|
+
if (rendered) {
|
|
50
|
+
nodes.push(rendered);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return nodes;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Dispatches a resolved ComponentInstance to the right creator based on its
|
|
58
|
+
* analysis.type:
|
|
59
|
+
* - `cva` → variant-matrix instance via createCVAStoryInstance
|
|
60
|
+
* - `state` → state-matrix instance via createStateStoryInstance
|
|
61
|
+
* - `simple` → single instance via createSimpleStoryInstance
|
|
62
|
+
* - `compound` → symbol-instance attempt, falling back to compound frame
|
|
63
|
+
* - anything else → returns false (caller renders an opaque fallback frame)
|
|
64
|
+
*
|
|
65
|
+
* Returns `true` when the layout received an appended child, `false` when the
|
|
66
|
+
* caller should fall through to its own fallback rendering.
|
|
67
|
+
*/
|
|
68
|
+
export function appendResolvedInstance(
|
|
69
|
+
layout: LayoutInfo,
|
|
70
|
+
analysis: CvaAnalysis,
|
|
71
|
+
instance: ComponentInstance,
|
|
72
|
+
story: ComponentStory,
|
|
73
|
+
theme: string,
|
|
74
|
+
colorGroup: Record<string, string>,
|
|
75
|
+
radiusGroup: Record<string, string> | null,
|
|
76
|
+
ctx: StoryBuilderContext
|
|
77
|
+
): boolean {
|
|
78
|
+
if (!analysis) return false;
|
|
79
|
+
if (analysis.type === 'cva') {
|
|
80
|
+
layout.appendChild(createCVAStoryInstance(analysis, instance, theme, ctx));
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
if (analysis.type === 'state') {
|
|
84
|
+
layout.appendChild(createStateStoryInstance(analysis, instance, theme, ctx, story));
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
if (analysis.type === 'simple') {
|
|
88
|
+
layout.appendChild(createSimpleStoryInstance(analysis, instance, theme, ctx, story));
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
if (analysis.type === 'compound') {
|
|
92
|
+
const symbolInstance = tryCreateNonCvaComponentInstanceShared(analysis, instance, theme, ctx, componentInstanceBackend, story);
|
|
93
|
+
if (symbolInstance) {
|
|
94
|
+
layout.appendChild(symbolInstance);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
const comp = createCompoundComponent(layout, analysis, theme);
|
|
98
|
+
if (comp) {
|
|
99
|
+
setGeneratedFallbackReason(comp, getNonCvaSymbolFallbackReason(analysis, instance));
|
|
100
|
+
}
|
|
101
|
+
const extra = splitClassName(instance.props && instance.props.className);
|
|
102
|
+
if (comp && comp.type === 'FRAME' && extra.length > 0) {
|
|
103
|
+
applyTailwindStylesToFrame(comp, extra, colorGroup, radiusGroup, theme);
|
|
104
|
+
}
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Renders portal-trigger components (Sheet, Dialog, Drawer, Popover, …) as
|
|
112
|
+
* "trigger row + popup content stack". The plugin can't actually open a Radix
|
|
113
|
+
* portal so it composes the two halves of the story manually:
|
|
114
|
+
*
|
|
115
|
+
* - `<XTrigger>`: rendered inline at the top of the Story Layout. Multiple
|
|
116
|
+
* triggers get a horizontal Trigger Row container; a single trigger lands
|
|
117
|
+
* directly. Trigger JSX children are rendered via renderJsxTree;
|
|
118
|
+
* `render="…"` strings are parsed via parseRenderPropToSyntheticInstance.
|
|
119
|
+
* - `<XContent>` / `<XPopup>`: rendered below the trigger AS A FRAME (not a
|
|
120
|
+
* symbol instance) so popup text stays visible. Skipped when the story
|
|
121
|
+
* doesn't carry an `open` / `defaultOpen` / `visible` / `defaultVisible`
|
|
122
|
+
* truthy prop on the root instance.
|
|
123
|
+
* - `<XProvider>` / `<XPositioner>`: skipped (zero visual contribution).
|
|
124
|
+
* - The root instance itself: skipped (its visual content lives in the trigger
|
|
125
|
+
* + content slots, not on the root).
|
|
126
|
+
*
|
|
127
|
+
* Returns the number of child nodes appended to `layout`.
|
|
128
|
+
*/
|
|
129
|
+
export function renderGuardedPortalStoryInstances(
|
|
130
|
+
layout: LayoutInfo,
|
|
131
|
+
def: ComponentDef,
|
|
132
|
+
story: ComponentStory,
|
|
133
|
+
theme: string,
|
|
134
|
+
colorGroup: Record<string, string>,
|
|
135
|
+
radiusGroup: Record<string, string> | null,
|
|
136
|
+
ctx: StoryBuilderContext
|
|
137
|
+
): number {
|
|
138
|
+
const instances = (story && Array.isArray(story.instances)) ? story.instances : [];
|
|
139
|
+
if (instances.length === 0) return 0;
|
|
140
|
+
|
|
141
|
+
const defKey = normalizeComponentName(def && def.name ? def.name : '');
|
|
142
|
+
if (!defKey) return 0;
|
|
143
|
+
|
|
144
|
+
let rootIsOpen = false;
|
|
145
|
+
for (let i = 0; i < instances.length; i++) {
|
|
146
|
+
const instance = instances[i];
|
|
147
|
+
if (!instance || normalizeComponentName(instance.componentName) !== defKey) continue;
|
|
148
|
+
const props = instance.props || {};
|
|
149
|
+
if (
|
|
150
|
+
isTruthyStateProp(props.open) ||
|
|
151
|
+
isTruthyStateProp(props.defaultOpen) ||
|
|
152
|
+
isTruthyStateProp(props.visible) ||
|
|
153
|
+
isTruthyStateProp(props.defaultVisible)
|
|
154
|
+
) {
|
|
155
|
+
rootIsOpen = true;
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function buildResolvedNode(analysis: CvaAnalysis, instance: ComponentInstance): SceneNode | null {
|
|
161
|
+
if (!analysis) return null;
|
|
162
|
+
if (analysis.type === 'cva') return createCVAStoryInstance(analysis, instance, theme, ctx);
|
|
163
|
+
if (analysis.type === 'state') return createStateStoryInstance(analysis, instance, theme, ctx, story);
|
|
164
|
+
if (analysis.type === 'simple') return createSimpleStoryInstance(analysis, instance, theme, ctx, story);
|
|
165
|
+
if (analysis.type === 'compound') {
|
|
166
|
+
const symbolInstance = tryCreateNonCvaComponentInstanceShared(analysis, instance, theme, ctx, componentInstanceBackend, story);
|
|
167
|
+
if (symbolInstance) return symbolInstance;
|
|
168
|
+
const holder = figma.createFrame();
|
|
169
|
+
holder.name = analysis.name;
|
|
170
|
+
holder.layoutMode = 'VERTICAL';
|
|
171
|
+
holder.primaryAxisSizingMode = 'AUTO';
|
|
172
|
+
holder.counterAxisSizingMode = 'AUTO';
|
|
173
|
+
holder.fills = [];
|
|
174
|
+
const comp = createCompoundComponent(holder, analysis, theme);
|
|
175
|
+
if (comp) {
|
|
176
|
+
setGeneratedFallbackReason(comp, getNonCvaSymbolFallbackReason(analysis, instance));
|
|
177
|
+
const extra = splitClassName(instance.props && instance.props.className);
|
|
178
|
+
if (comp.type === 'FRAME' && extra.length > 0) {
|
|
179
|
+
applyTailwindStylesToFrame(comp, extra, colorGroup, radiusGroup, theme);
|
|
180
|
+
}
|
|
181
|
+
return comp;
|
|
182
|
+
}
|
|
183
|
+
holder.remove();
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const triggerNodes: SceneNode[] = [];
|
|
189
|
+
const contentNodes: SceneNode[] = [];
|
|
190
|
+
for (let i = 0; i < instances.length; i++) {
|
|
191
|
+
const instance = instances[i];
|
|
192
|
+
if (!instance || !instance.componentName) continue;
|
|
193
|
+
const instanceKey = normalizeComponentName(instance.componentName);
|
|
194
|
+
|
|
195
|
+
// Root container instance for guarded/portal components usually doesn't carry
|
|
196
|
+
// renderable content by itself and creates empty placeholder cards in Figma.
|
|
197
|
+
if (instanceKey === defKey) continue;
|
|
198
|
+
|
|
199
|
+
if (instanceKey === defKey + 'provider' || instanceKey === defKey + 'positioner') {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (instanceKey === defKey + 'trigger') {
|
|
204
|
+
const triggerNodesFromJsx = renderGuardedTriggerJsxChildren(instance, theme, colorGroup, radiusGroup, ctx);
|
|
205
|
+
if (triggerNodesFromJsx.length > 0) {
|
|
206
|
+
for (let i = 0; i < triggerNodesFromJsx.length; i++) {
|
|
207
|
+
triggerNodes.push(triggerNodesFromJsx[i]);
|
|
208
|
+
}
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const synthetic = parseRenderPropToSyntheticInstance(instance.props && instance.props.render);
|
|
212
|
+
if (!synthetic) continue;
|
|
213
|
+
const triggerDef = ctx.getComponentDefByName(synthetic.componentName);
|
|
214
|
+
if (!triggerDef) continue;
|
|
215
|
+
const triggerAnalysis = ctx.normalizeComponentDef(triggerDef);
|
|
216
|
+
const triggerNode = buildResolvedNode(triggerAnalysis, synthetic);
|
|
217
|
+
if (triggerNode) triggerNodes.push(triggerNode);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (instanceKey === defKey + 'content' || instanceKey === defKey + 'popup') {
|
|
222
|
+
if (!rootIsOpen) continue;
|
|
223
|
+
const popupInstance = {
|
|
224
|
+
props: instance.props || {},
|
|
225
|
+
children: instance.children || (instance.props && instance.props.children) || '',
|
|
226
|
+
};
|
|
227
|
+
// Force frame rendering here (not symbol instance) so popup text is always visible.
|
|
228
|
+
contentNodes.push(createSimpleStoryFrame(def, popupInstance, theme, ctx, 'portal_content_preview'));
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
let added = 0;
|
|
234
|
+
if (triggerNodes.length > 1) {
|
|
235
|
+
const triggerRow = figma.createFrame();
|
|
236
|
+
triggerRow.name = 'Trigger Row';
|
|
237
|
+
triggerRow.layoutMode = 'HORIZONTAL';
|
|
238
|
+
triggerRow.primaryAxisSizingMode = 'AUTO';
|
|
239
|
+
triggerRow.counterAxisSizingMode = 'AUTO';
|
|
240
|
+
triggerRow.counterAxisAlignItems = 'CENTER';
|
|
241
|
+
triggerRow.itemSpacing = 12;
|
|
242
|
+
triggerRow.fills = [];
|
|
243
|
+
ctx.applyClipBehavior(triggerRow, ['flex', 'items-center', 'gap-3']);
|
|
244
|
+
for (const node of triggerNodes) triggerRow.appendChild(node);
|
|
245
|
+
layout.appendChild(triggerRow);
|
|
246
|
+
added += triggerNodes.length;
|
|
247
|
+
} else if (triggerNodes.length === 1) {
|
|
248
|
+
layout.appendChild(triggerNodes[0]);
|
|
249
|
+
added += 1;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
for (const node of contentNodes) {
|
|
253
|
+
layout.appendChild(node);
|
|
254
|
+
added += 1;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return added;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Generic instance-rendering path used when a story does NOT need portal
|
|
262
|
+
* guarding. Walks story.instances, resolves each via the scanner's component
|
|
263
|
+
* defs, and dispatches to the right creator via appendResolvedInstance.
|
|
264
|
+
* Unresolved components fall back to a labelled empty frame so the story
|
|
265
|
+
* still surfaces what was expected.
|
|
266
|
+
*/
|
|
267
|
+
export function renderStoryInstances(
|
|
268
|
+
layout: LayoutInfo,
|
|
269
|
+
story: ComponentStory,
|
|
270
|
+
theme: string,
|
|
271
|
+
colorGroup: Record<string, string>,
|
|
272
|
+
radiusGroup: Record<string, string> | null,
|
|
273
|
+
ctx: StoryBuilderContext
|
|
274
|
+
): number {
|
|
275
|
+
let added = 0;
|
|
276
|
+
for (const instance of story.instances || []) {
|
|
277
|
+
if (!instance || !instance.componentName) continue;
|
|
278
|
+
const compDef = ctx.getComponentDefByName(instance.componentName);
|
|
279
|
+
if (compDef) {
|
|
280
|
+
const analysis = ctx.normalizeComponentDef(compDef);
|
|
281
|
+
if (appendResolvedInstance(layout, analysis, instance, story, theme, colorGroup, radiusGroup, ctx)) {
|
|
282
|
+
added++;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const fallback = figma.createFrame();
|
|
288
|
+
fallback.name = instance.componentName || 'Component';
|
|
289
|
+
fallback.layoutMode = 'VERTICAL';
|
|
290
|
+
fallback.primaryAxisSizingMode = 'AUTO';
|
|
291
|
+
fallback.counterAxisSizingMode = 'AUTO';
|
|
292
|
+
fallback.fills = [];
|
|
293
|
+
const extra = splitClassName(instance.props && instance.props.className);
|
|
294
|
+
ctx.applyClipBehavior(fallback, extra);
|
|
295
|
+
if (extra.length > 0) {
|
|
296
|
+
applyTailwindStylesToFrame(fallback, extra, colorGroup, radiusGroup, theme);
|
|
297
|
+
}
|
|
298
|
+
const label = instance.children || instance.props?.children || instance.componentName;
|
|
299
|
+
if (label) {
|
|
300
|
+
fallback.appendChild(createTextNode(String(label), { fontSize: 12, opacity: 0.7 }));
|
|
301
|
+
}
|
|
302
|
+
setGeneratedFallbackReason(fallback, 'component_definition_unresolved');
|
|
303
|
+
layout.appendChild(fallback);
|
|
304
|
+
added++;
|
|
305
|
+
}
|
|
306
|
+
return added;
|
|
307
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { COMPONENT_LIBRARY_ROOT_NAME } from '../components';
|
|
2
|
+
import { findChildByName } from '../cache';
|
|
3
|
+
import { tagGeneratedNode } from './generated-node';
|
|
4
|
+
import { removeDuplicateChildrenByName } from './frame-utils';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Shared infrastructure used by both the CVA-master and state-master
|
|
8
|
+
* extraction paths in the design-system page builder.
|
|
9
|
+
*
|
|
10
|
+
* - `ENABLE_SYMBOL_MASTERS` is the global feature flag for symbol-based
|
|
11
|
+
* rendering (component sets per CVA / per state). When false, the
|
|
12
|
+
* plugin renders raw frames instead.
|
|
13
|
+
* - `toFigmaVariantPropertyName` / `toFigmaVariantPropertyValue` are the
|
|
14
|
+
* case-normalizers Figma's variant API expects.
|
|
15
|
+
* - The `ensureComponentLibraryRoot` / `ensureThemeComponentLibrary`
|
|
16
|
+
* helpers create or reuse the hidden frames on the current page that
|
|
17
|
+
* hold the generated symbol masters.
|
|
18
|
+
* - `shouldCreateNonCvaMaster` answers whether a non-CVA, non-state def
|
|
19
|
+
* should get a symbol master.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export const ENABLE_SYMBOL_MASTERS = true;
|
|
23
|
+
|
|
24
|
+
export function toFigmaVariantPropertyName(rawKey: string): string {
|
|
25
|
+
const key = String(rawKey || '').trim();
|
|
26
|
+
if (!key) return 'Variant';
|
|
27
|
+
return key.charAt(0).toUpperCase() + key.slice(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function toFigmaVariantPropertyValue(rawValue: unknown): string {
|
|
31
|
+
const value = String(rawValue == null ? '' : rawValue).trim();
|
|
32
|
+
if (!value) return 'Default';
|
|
33
|
+
const lowered = value.toLowerCase();
|
|
34
|
+
if (lowered === 'true') return 'True';
|
|
35
|
+
if (lowered === 'false') return 'False';
|
|
36
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
40
|
+
export function findExistingComponentLibraryRoot(): any | null {
|
|
41
|
+
const pages = figma.root && Array.isArray(figma.root.children) ? figma.root.children : [];
|
|
42
|
+
for (let i = 0; i < pages.length; i++) {
|
|
43
|
+
const page = pages[i];
|
|
44
|
+
if (!page || page.type !== 'PAGE' || !Array.isArray(page.children)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const root = findChildByName(page, COMPONENT_LIBRARY_ROOT_NAME);
|
|
48
|
+
if (root && root.type === 'FRAME') return root;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
54
|
+
export function ensureComponentLibraryRoot(): any | null {
|
|
55
|
+
// Prefer an existing hidden library root regardless of page name.
|
|
56
|
+
// Design systems may be generated on arbitrary page names (e.g. "Page 3"),
|
|
57
|
+
// so requiring a page named "Design System" breaks symbol instancing.
|
|
58
|
+
let root = findExistingComponentLibraryRoot();
|
|
59
|
+
if (root && root.type === 'FRAME') {
|
|
60
|
+
const parent = root.parent;
|
|
61
|
+
if (parent && Array.isArray(parent.children)) {
|
|
62
|
+
removeDuplicateChildrenByName(parent, COMPONENT_LIBRARY_ROOT_NAME, 'FRAME');
|
|
63
|
+
root = findChildByName(parent, COMPONENT_LIBRARY_ROOT_NAME) || root;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let page = figma.currentPage;
|
|
68
|
+
if ((!root || root.type !== 'FRAME') && (!page || !Array.isArray(page.children))) {
|
|
69
|
+
const pages = figma.root && Array.isArray(figma.root.children) ? figma.root.children : [];
|
|
70
|
+
for (let i = 0; i < pages.length; i++) {
|
|
71
|
+
const candidate = pages[i];
|
|
72
|
+
if (candidate && candidate.type === 'PAGE' && Array.isArray(candidate.children)) {
|
|
73
|
+
page = candidate;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (!root && (!page || !Array.isArray(page.children))) return null;
|
|
79
|
+
|
|
80
|
+
if (!root && page) {
|
|
81
|
+
removeDuplicateChildrenByName(page, COMPONENT_LIBRARY_ROOT_NAME, 'FRAME');
|
|
82
|
+
root = findChildByName(page, COMPONENT_LIBRARY_ROOT_NAME);
|
|
83
|
+
}
|
|
84
|
+
if (!root) {
|
|
85
|
+
root = figma.createFrame();
|
|
86
|
+
root.name = COMPONENT_LIBRARY_ROOT_NAME;
|
|
87
|
+
root.layoutMode = 'VERTICAL';
|
|
88
|
+
root.primaryAxisSizingMode = 'AUTO';
|
|
89
|
+
root.counterAxisSizingMode = 'AUTO';
|
|
90
|
+
root.itemSpacing = 32;
|
|
91
|
+
root.fills = [];
|
|
92
|
+
root.x = -20000;
|
|
93
|
+
root.y = -20000;
|
|
94
|
+
root.visible = false;
|
|
95
|
+
page.appendChild(root);
|
|
96
|
+
}
|
|
97
|
+
tagGeneratedNode(root, 'component-library-root');
|
|
98
|
+
|
|
99
|
+
return root;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
103
|
+
export function ensureThemeComponentLibrary(theme: string): any | null {
|
|
104
|
+
if (!ENABLE_SYMBOL_MASTERS) return null;
|
|
105
|
+
const root = ensureComponentLibraryRoot();
|
|
106
|
+
if (!root) return null;
|
|
107
|
+
|
|
108
|
+
const themeName = String(theme || 'primary');
|
|
109
|
+
const frameName = themeName + ' Masters';
|
|
110
|
+
removeDuplicateChildrenByName(root, frameName, 'FRAME');
|
|
111
|
+
let themeFrame = findChildByName(root, frameName);
|
|
112
|
+
if (!themeFrame) {
|
|
113
|
+
themeFrame = figma.createFrame();
|
|
114
|
+
themeFrame.name = frameName;
|
|
115
|
+
themeFrame.layoutMode = 'VERTICAL';
|
|
116
|
+
themeFrame.primaryAxisSizingMode = 'AUTO';
|
|
117
|
+
themeFrame.counterAxisSizingMode = 'AUTO';
|
|
118
|
+
themeFrame.itemSpacing = 16;
|
|
119
|
+
themeFrame.fills = [];
|
|
120
|
+
themeFrame.visible = false;
|
|
121
|
+
root.appendChild(themeFrame);
|
|
122
|
+
}
|
|
123
|
+
tagGeneratedNode(themeFrame, 'component-library-theme:' + themeName);
|
|
124
|
+
|
|
125
|
+
return themeFrame;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
129
|
+
export function shouldCreateNonCvaMaster(def: any): boolean {
|
|
130
|
+
if (!def || def.type === 'cva' || def.type === 'state') return false;
|
|
131
|
+
if (def.symbolCandidate === false) return false;
|
|
132
|
+
return true;
|
|
133
|
+
}
|