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,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design-system.ts — Design System Page Generation
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates building the "Design System" page in Figma.
|
|
5
|
+
*
|
|
6
|
+
* ## Incremental re-render
|
|
7
|
+
* Frames are no longer rebuilt from scratch on every run. Instead:
|
|
8
|
+
*
|
|
9
|
+
* - The "Design Tokens" row is rebuilt only when token values change
|
|
10
|
+
* (detected via a hash stored in Figma plugin data).
|
|
11
|
+
* - Component section frames are preserved and
|
|
12
|
+
* their contents are diffed at the per-component-block level — see
|
|
13
|
+
* story-builder.ts and frame-cache.ts for details.
|
|
14
|
+
*
|
|
15
|
+
* When a frame is unchanged its node ID is preserved, so designer annotations
|
|
16
|
+
* and manual repositioning survive plugin re-runs.
|
|
17
|
+
*
|
|
18
|
+
* yOffset / xOffset in opts are only applied when a section is created for the
|
|
19
|
+
* first time. Existing sections keep their current position.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { debug } from '../tokens';
|
|
23
|
+
import { getThemeNames, TOKENS } from '../tokens';
|
|
24
|
+
import {
|
|
25
|
+
demoFrameColors,
|
|
26
|
+
demoFrameRadii,
|
|
27
|
+
demoFrameFonts,
|
|
28
|
+
demoFrameSpacing,
|
|
29
|
+
demoFrameFontSizes,
|
|
30
|
+
demoFrameShadows,
|
|
31
|
+
demoFrameBreakpoints,
|
|
32
|
+
buildTokensCategorySection,
|
|
33
|
+
} from '../tokens';
|
|
34
|
+
import { createUIComponents, pruneGeneratedComponentLibrary } from './ui-builder';
|
|
35
|
+
import { hashString, stableStringify, getFrameHash, setFrameHash, findChildByName } from '../cache';
|
|
36
|
+
import { isGeneratedDesignSystemNode, tagGeneratedNode } from './generated-node';
|
|
37
|
+
|
|
38
|
+
const DESIGN_SYSTEM_PAGE_NAME = 'Design System';
|
|
39
|
+
const COMPONENT_LIBRARY_ROOT_NAME = '__Inkbridge Component Library';
|
|
40
|
+
// Synthetic preflight identifier — must match the constant in main.ts.
|
|
41
|
+
const DESIGN_TOKENS_TOGGLE = '__design_tokens__';
|
|
42
|
+
|
|
43
|
+
function removeStalePageLabels(page: PageNode | null, labels: string[]): void {
|
|
44
|
+
if (!page || !Array.isArray(page.children)) return;
|
|
45
|
+
const targets = new Set(labels);
|
|
46
|
+
const staleNodes = page.children.filter((node): node is TextNode =>
|
|
47
|
+
node.type === 'TEXT'
|
|
48
|
+
&& typeof node.characters === 'string'
|
|
49
|
+
&& targets.has(node.characters)
|
|
50
|
+
);
|
|
51
|
+
for (let i = 0; i < staleNodes.length; i++) {
|
|
52
|
+
staleNodes[i].remove();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Remove orphaned top-level nodes left behind by failed/interrupted runs.
|
|
57
|
+
// The Design System page is plugin-managed: only these named sections are
|
|
58
|
+
// expected as direct children. Anything else is stale generator output.
|
|
59
|
+
const KNOWN_TOP_LEVEL_SECTIONS = new Set([
|
|
60
|
+
'Design Tokens',
|
|
61
|
+
'UI Components',
|
|
62
|
+
COMPONENT_LIBRARY_ROOT_NAME,
|
|
63
|
+
]);
|
|
64
|
+
|
|
65
|
+
function removeDuplicateTopLevelSections(page: PageNode | null): void {
|
|
66
|
+
if (!page || !Array.isArray(page.children)) return;
|
|
67
|
+
const seen: Record<string, boolean> = {};
|
|
68
|
+
const children = page.children.slice();
|
|
69
|
+
for (let i = 0; i < children.length; i++) {
|
|
70
|
+
const node = children[i];
|
|
71
|
+
if (!node || !KNOWN_TOP_LEVEL_SECTIONS.has(node.name)) continue;
|
|
72
|
+
if (seen[node.name]) {
|
|
73
|
+
node.remove();
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
seen[node.name] = true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function removeOrphanedTopLevelNodes(page: PageNode | null): void {
|
|
81
|
+
if (!page || !Array.isArray(page.children)) return;
|
|
82
|
+
const orphans = page.children.filter(function(node) {
|
|
83
|
+
if (!node) return false;
|
|
84
|
+
if (KNOWN_TOP_LEVEL_SECTIONS.has(node.name)) return false;
|
|
85
|
+
// Primary path: generated metadata marks this node as plugin output.
|
|
86
|
+
if (isGeneratedDesignSystemNode(node)) return true;
|
|
87
|
+
// Legacy fallback: older plugin runs didn't tag generated nodes.
|
|
88
|
+
if (isLegacyGeneratedOrphan(node)) return true;
|
|
89
|
+
// Defensive cleanup: plugin-managed page should not contain any additional
|
|
90
|
+
// direct children at root level, regardless of node type.
|
|
91
|
+
return true;
|
|
92
|
+
});
|
|
93
|
+
for (let i = 0; i < orphans.length; i++) {
|
|
94
|
+
orphans[i].remove();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function nodeHasTextDescendantLike(node: BaseNode, phrase: string): boolean {
|
|
99
|
+
if (!node) return false;
|
|
100
|
+
const value = String(phrase || '').trim().toLowerCase();
|
|
101
|
+
if (!value) return false;
|
|
102
|
+
if (node.type === 'TEXT' && typeof node.characters === 'string') {
|
|
103
|
+
return node.characters.trim().toLowerCase().indexOf(value) !== -1;
|
|
104
|
+
}
|
|
105
|
+
if (!('children' in node) || !Array.isArray(node.children)) return false;
|
|
106
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
107
|
+
if (nodeHasTextDescendantLike(node.children[i], value)) return true;
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isLegacyGeneratedOrphan(node: BaseNode): boolean {
|
|
113
|
+
if (!node) return false;
|
|
114
|
+
if (node.name === COMPONENT_LIBRARY_ROOT_NAME) return true;
|
|
115
|
+
if (node.name === 'States' && nodeHasTextDescendantLike(node, 'state matrix')) return true;
|
|
116
|
+
if (node.name === 'Responsive' && nodeHasTextDescendantLike(node, 'responsive')) return true;
|
|
117
|
+
if (node.name === 'State Axes' || node.name === 'State Table') return true;
|
|
118
|
+
if (typeof node.name === 'string' && node.name.startsWith('State Row/')) return true;
|
|
119
|
+
if (node.type === 'TEXT' && typeof node.characters === 'string') {
|
|
120
|
+
const text = node.characters.trim().toLowerCase();
|
|
121
|
+
if (text.indexOf('state matrix') !== -1 || text === 'responsive') return true;
|
|
122
|
+
}
|
|
123
|
+
// Catch detached/renamed fragments that still carry legacy marker labels.
|
|
124
|
+
if (nodeHasTextDescendantLike(node, 'state matrix') || nodeHasTextDescendantLike(node, 'responsive')) return true;
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function removeLegacyArtifactsFromNonDesignSystemPages(): void {
|
|
129
|
+
const pages = figma.root && Array.isArray(figma.root.children) ? figma.root.children : [];
|
|
130
|
+
for (let i = 0; i < pages.length; i++) {
|
|
131
|
+
const page = pages[i];
|
|
132
|
+
if (!page || page.type !== 'PAGE' || page.name === DESIGN_SYSTEM_PAGE_NAME || !Array.isArray(page.children)) continue;
|
|
133
|
+
const staleNodes = page.children.filter(function(node: SceneNode) {
|
|
134
|
+
return isGeneratedDesignSystemNode(node) || isLegacyGeneratedOrphan(node);
|
|
135
|
+
});
|
|
136
|
+
for (let j = 0; j < staleNodes.length; j++) {
|
|
137
|
+
staleNodes[j].remove();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function removeGeneratedArtifactsInNode(node: BaseNode): number {
|
|
143
|
+
if (!node || !('children' in node) || !Array.isArray(node.children)) return 0;
|
|
144
|
+
let removed = 0;
|
|
145
|
+
const children = node.children.slice();
|
|
146
|
+
for (let i = 0; i < children.length; i++) {
|
|
147
|
+
const child = children[i];
|
|
148
|
+
if (!child) continue;
|
|
149
|
+
if (child.type === 'PAGE') {
|
|
150
|
+
removed += removeGeneratedArtifactsInNode(child);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const generated = isGeneratedDesignSystemNode(child);
|
|
155
|
+
const legacy = isLegacyGeneratedOrphan(child);
|
|
156
|
+
if (generated || legacy) {
|
|
157
|
+
child.remove();
|
|
158
|
+
removed += 1;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if ('children' in child && Array.isArray(child.children)) {
|
|
163
|
+
removed += removeGeneratedArtifactsInNode(child);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return removed;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function cleanGeneratedDesignSystemArtifacts(): { removedNodes: number; touchedPages: number } {
|
|
170
|
+
const pages = figma.root && Array.isArray(figma.root.children) ? figma.root.children : [];
|
|
171
|
+
let removedNodes = 0;
|
|
172
|
+
let touchedPages = 0;
|
|
173
|
+
for (let i = 0; i < pages.length; i++) {
|
|
174
|
+
const page = pages[i];
|
|
175
|
+
if (!page || page.type !== 'PAGE') continue;
|
|
176
|
+
const removedInPage = removeGeneratedArtifactsInNode(page);
|
|
177
|
+
if (removedInPage > 0) {
|
|
178
|
+
removedNodes += removedInPage;
|
|
179
|
+
touchedPages += 1;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return { removedNodes, touchedPages };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Loading-panel status callback signature. Accepts either a flat string
|
|
187
|
+
* (legacy single-line status) or `{ phase, detail }` where the phase is
|
|
188
|
+
* sticky context across detail-only updates (so "Building components…"
|
|
189
|
+
* stays as the header while detail messages cycle through theme /
|
|
190
|
+
* section names). Both forms are honoured by `main.ts:setStatus`.
|
|
191
|
+
*/
|
|
192
|
+
export type StatusUpdate =
|
|
193
|
+
| string
|
|
194
|
+
| { phase?: string; detail?: string; noYield?: boolean };
|
|
195
|
+
|
|
196
|
+
export interface BuildDesignSystemOptions {
|
|
197
|
+
onStatus?: (status: StatusUpdate) => void | Promise<void>;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function buildDesignSystemSinglePage(
|
|
201
|
+
excluded?: string[],
|
|
202
|
+
options?: BuildDesignSystemOptions,
|
|
203
|
+
): Promise<void> {
|
|
204
|
+
const onStatus = options && options.onStatus
|
|
205
|
+
? options.onStatus
|
|
206
|
+
: function (_s: StatusUpdate): void | Promise<void> { return; };
|
|
207
|
+
let ds: PageNode | null = figma.root.children.find(p => p.name === DESIGN_SYSTEM_PAGE_NAME) ?? null;
|
|
208
|
+
if (!ds) { ds = figma.createPage(); ds.name = DESIGN_SYSTEM_PAGE_NAME; }
|
|
209
|
+
tagGeneratedNode(ds, 'design-system-page');
|
|
210
|
+
removeLegacyArtifactsFromNonDesignSystemPages();
|
|
211
|
+
figma.currentPage = ds;
|
|
212
|
+
removeDuplicateTopLevelSections(ds);
|
|
213
|
+
removeOrphanedTopLevelNodes(ds);
|
|
214
|
+
removeStalePageLabels(ds, ['Design Tokens', 'UI Components']);
|
|
215
|
+
|
|
216
|
+
const themeNames = getThemeNames(TOKENS);
|
|
217
|
+
|
|
218
|
+
// Compute a single token hash for this run. Passed into createUIComponents
|
|
219
|
+
// so component blocks can include token state in their own hashes.
|
|
220
|
+
const tokenHash = hashString(stableStringify(TOKENS));
|
|
221
|
+
|
|
222
|
+
// The design-tokens toggle is a synthetic preflight item; strip it before
|
|
223
|
+
// forwarding the excluded list to the component builder so it isn't treated
|
|
224
|
+
// as a missing component.
|
|
225
|
+
const skipDesignTokens = (excluded || []).indexOf(DESIGN_TOKENS_TOGGLE) !== -1;
|
|
226
|
+
excluded = (excluded || []).filter(function (name) { return name !== DESIGN_TOKENS_TOGGLE; });
|
|
227
|
+
|
|
228
|
+
// --- Design Tokens row (incremental) ---
|
|
229
|
+
// Rebuilt only when token values have changed since the last run, and only
|
|
230
|
+
// when the design-tokens preflight toggle is selected.
|
|
231
|
+
let tokensRow: FrameNode | null = findChildByName(ds, 'Design Tokens') as FrameNode | null;
|
|
232
|
+
const tokensNeedRebuild = !skipDesignTokens && (!tokensRow || getFrameHash(tokensRow) !== tokenHash);
|
|
233
|
+
if (tokensNeedRebuild) await onStatus('Building design tokens…');
|
|
234
|
+
if (tokensNeedRebuild) {
|
|
235
|
+
const prevX: number = tokensRow ? tokensRow.x : 48;
|
|
236
|
+
const prevY: number = tokensRow ? tokensRow.y : 48;
|
|
237
|
+
if (tokensRow) tokensRow.remove();
|
|
238
|
+
|
|
239
|
+
tokensRow = figma.createFrame();
|
|
240
|
+
tokensRow.name = 'Design Tokens';
|
|
241
|
+
tokensRow.layoutMode = 'VERTICAL';
|
|
242
|
+
tokensRow.primaryAxisSizingMode = 'AUTO';
|
|
243
|
+
tokensRow.counterAxisSizingMode = 'AUTO';
|
|
244
|
+
tokensRow.itemSpacing = 32;
|
|
245
|
+
tokensRow.paddingLeft = tokensRow.paddingRight = 32;
|
|
246
|
+
tokensRow.paddingTop = tokensRow.paddingBottom = 24;
|
|
247
|
+
tokensRow.fills = [];
|
|
248
|
+
tokensRow.strokes = [];
|
|
249
|
+
const appendIfPresent = (node: SceneNode | null): void => {
|
|
250
|
+
if (node && tokensRow) tokensRow.appendChild(node);
|
|
251
|
+
};
|
|
252
|
+
appendIfPresent(buildTokensCategorySection(
|
|
253
|
+
'Colors',
|
|
254
|
+
themeNames.map((themeName) => demoFrameColors(themeName))
|
|
255
|
+
));
|
|
256
|
+
appendIfPresent(buildTokensCategorySection(
|
|
257
|
+
'Fonts',
|
|
258
|
+
themeNames.map((themeName) => demoFrameFonts(themeName))
|
|
259
|
+
));
|
|
260
|
+
appendIfPresent(buildTokensCategorySection('Font sizes', [demoFrameFontSizes()]));
|
|
261
|
+
appendIfPresent(buildTokensCategorySection('Radius', [demoFrameRadii()]));
|
|
262
|
+
appendIfPresent(buildTokensCategorySection('Spacing', [demoFrameSpacing()]));
|
|
263
|
+
appendIfPresent(buildTokensCategorySection('Breakpoints', [demoFrameBreakpoints()]));
|
|
264
|
+
appendIfPresent(buildTokensCategorySection(
|
|
265
|
+
'Shadows',
|
|
266
|
+
themeNames.map((themeName) => demoFrameShadows(themeName))
|
|
267
|
+
));
|
|
268
|
+
tokensRow.x = prevX;
|
|
269
|
+
tokensRow.y = prevY;
|
|
270
|
+
ds.appendChild(tokensRow);
|
|
271
|
+
setFrameHash(tokensRow, tokenHash);
|
|
272
|
+
tagGeneratedNode(tokensRow, 'design-tokens-row');
|
|
273
|
+
debug('Tokens row rebuilt', { columns: tokensRow.children.length, hash: tokenHash });
|
|
274
|
+
} else if (tokensRow) {
|
|
275
|
+
tagGeneratedNode(tokensRow, 'design-tokens-row');
|
|
276
|
+
debug('Tokens row unchanged — skipped', { hash: tokenHash, skipped: skipDesignTokens });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Default y for a new UI Components section (below tokens row).
|
|
280
|
+
// Only used when the section does not yet exist on the page. When the design
|
|
281
|
+
// tokens row was skipped and never existed, fall back to the page top.
|
|
282
|
+
const defaultUiY = tokensRow ? (tokensRow.y + tokensRow.height + 80) : 48;
|
|
283
|
+
|
|
284
|
+
// Keep hidden master library aligned with current scanner output and themes.
|
|
285
|
+
// Removes stale/duplicate generated masters from old runs.
|
|
286
|
+
pruneGeneratedComponentLibrary(themeNames);
|
|
287
|
+
|
|
288
|
+
const excludedNames: string[] = (excluded && excluded.length > 0) ? excluded : [];
|
|
289
|
+
|
|
290
|
+
// Set the phase as a sticky header; createUIComponents below emits
|
|
291
|
+
// detail-only updates per theme / section. Both lines stay visible
|
|
292
|
+
// together so the user always knows what bigger phase is in flight
|
|
293
|
+
// even as the detail cycles through theme + section names.
|
|
294
|
+
// Phase text is sticky; ui.html animates trailing dots via CSS so we
|
|
295
|
+
// don't bake them into the string here.
|
|
296
|
+
await onStatus({ phase: 'Building components', detail: '' });
|
|
297
|
+
await createUIComponents(ds, {
|
|
298
|
+
themeNames,
|
|
299
|
+
yOffset: defaultUiY,
|
|
300
|
+
xOffset: 48,
|
|
301
|
+
excludeComponents: excludedNames,
|
|
302
|
+
preserveUnselectedComponents: excludedNames.length > 0,
|
|
303
|
+
tokenHash,
|
|
304
|
+
showSectionHeader: false,
|
|
305
|
+
onStatus,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Reflow guard: the UI Components section y is only set on first creation.
|
|
309
|
+
// Partial-selection or subset runs previously produced an overlap with the
|
|
310
|
+
// Design Tokens row; enforce a minimum y every run while preserving any
|
|
311
|
+
// further-down position a designer may have set. Also align x so the
|
|
312
|
+
// section never drifts above/left of the tokens row.
|
|
313
|
+
const uiSection = findChildByName(ds, 'UI Components') as FrameNode | null;
|
|
314
|
+
if (uiSection && tokensRow) {
|
|
315
|
+
const minUiY = tokensRow.y + tokensRow.height + 80;
|
|
316
|
+
if (uiSection.y < minUiY) uiSection.y = minUiY;
|
|
317
|
+
if (uiSection.x < tokensRow.x) uiSection.x = tokensRow.x;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Post-build orphan sweep: frames can be abandoned during a partial run
|
|
321
|
+
// (e.g. a rebuild path that errors out mid-way leaves a dangling frame at
|
|
322
|
+
// the page root). Rerun the sweeps so nothing survives outside the known
|
|
323
|
+
// top-level sections.
|
|
324
|
+
removeDuplicateTopLevelSections(ds);
|
|
325
|
+
removeOrphanedTopLevelNodes(ds);
|
|
326
|
+
|
|
327
|
+
// Frame the freshly-built page so the user lands on the result instead of
|
|
328
|
+
// wherever the viewport was before the run. Filter out invisible children
|
|
329
|
+
// defensively — scrollAndZoomIntoView throws on an empty selection.
|
|
330
|
+
const visibleChildren = ds.children.filter((node) => node.visible);
|
|
331
|
+
if (visibleChildren.length > 0) {
|
|
332
|
+
figma.viewport.scrollAndZoomIntoView(visibleChildren);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { getBaseClass, isElementLikeNode, type NodeIR } from '../tailwind';
|
|
2
|
+
import { isTabsRootTag, isTabsListTag, isTabsContentTag } from './tag-predicates';
|
|
3
|
+
import { getNodeEffectiveClasses, getNodeMarginTopPx } from './node-helpers';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Post-render frame stabilizers. Each function takes a frame (and possibly
|
|
7
|
+
* its parent) that's already been auto-laid-out and applies a fix that
|
|
8
|
+
* couldn't be expressed cleanly during initial render — usually because
|
|
9
|
+
* the fix depends on the parent's resolved width or on sibling layout.
|
|
10
|
+
*
|
|
11
|
+
* - `constrainSingleHorizontalTextChild`: when a fixed-width row has one
|
|
12
|
+
* text child and other in-flow siblings, cap the text width so it doesn't
|
|
13
|
+
* overflow and steal space from a sibling.
|
|
14
|
+
* - `stabilizeHorizontalStretchChild`: pin a STRETCH'd horizontal child to
|
|
15
|
+
* its parent's content width when the parent is a fixed-width column,
|
|
16
|
+
* so the child renders at the same width Figma would compute on its own.
|
|
17
|
+
* - `reflowMxAutoChildren`: re-flow children that the layout pass tagged
|
|
18
|
+
* as 'mx-auto' so they fill the parent's content box width.
|
|
19
|
+
* - `applyVerticalMarginSpacing`: when stacked vertical children carry
|
|
20
|
+
* `mt-*` utilities, reflect the largest margin-top into the frame's
|
|
21
|
+
* itemSpacing (Figma can't apply per-child margin in auto-layout).
|
|
22
|
+
* - `enforceTabsChildSizing`: stretch a Tabs.List / Tabs.Content child to
|
|
23
|
+
* its parent's content width so the tab bar visually fills the row.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export function constrainSingleHorizontalTextChild(node: SceneNode): void {
|
|
27
|
+
if (node.type !== 'FRAME') return;
|
|
28
|
+
const frame = node;
|
|
29
|
+
if (
|
|
30
|
+
frame.layoutMode !== 'HORIZONTAL'
|
|
31
|
+
|| !Array.isArray(frame.children)
|
|
32
|
+
|| frame.children.length < 2
|
|
33
|
+
) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Keep naturally centered CTA/action rows intact.
|
|
38
|
+
// Constraining text width in CENTER rows makes one text child consume the
|
|
39
|
+
// remaining width, which shifts sibling controls off-center.
|
|
40
|
+
if (frame.primaryAxisAlignItems === 'CENTER') {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Width constraints only make sense once the row width is fixed.
|
|
45
|
+
if (frame.primaryAxisSizingMode !== 'FIXED') {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const textChildren = frame.children.filter((child): child is TextNode => child?.type === 'TEXT');
|
|
50
|
+
if (textChildren.length !== 1) return;
|
|
51
|
+
|
|
52
|
+
// CSS flex: absolute/fixed siblings are out-of-flow and don't consume space
|
|
53
|
+
// in the row. Including them would steal width from the text and force wrap
|
|
54
|
+
// (e.g. a Select item's absolute check-indicator shrinking the label text).
|
|
55
|
+
const inFlowNonText = frame.children.filter((child) => {
|
|
56
|
+
if (!child || child.type === 'TEXT') return false;
|
|
57
|
+
if ('layoutPositioning' in child && child.layoutPositioning === 'ABSOLUTE') return false;
|
|
58
|
+
return true;
|
|
59
|
+
});
|
|
60
|
+
const nonTextWidth = inFlowNonText.reduce((sum, child) => {
|
|
61
|
+
return sum + ('width' in child ? child.width : 0);
|
|
62
|
+
}, 0);
|
|
63
|
+
const gapContributingCount = inFlowNonText.length + textChildren.length;
|
|
64
|
+
|
|
65
|
+
const availableWidth = Math.max(
|
|
66
|
+
0,
|
|
67
|
+
frame.width
|
|
68
|
+
- (frame.paddingLeft || 0)
|
|
69
|
+
- (frame.paddingRight || 0)
|
|
70
|
+
- (frame.itemSpacing || 0) * Math.max(0, gapContributingCount - 1)
|
|
71
|
+
- nonTextWidth,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (availableWidth <= 0) return;
|
|
75
|
+
|
|
76
|
+
const textChild = textChildren[0];
|
|
77
|
+
// CSS-default behavior for text in a horizontal flex row is to overflow, not
|
|
78
|
+
// wrap. Only constrain if the text would actually exceed the available space
|
|
79
|
+
// AND there's another in-flow sibling competing for it.
|
|
80
|
+
if (inFlowNonText.length === 0) return;
|
|
81
|
+
if ((textChild.width || 0) <= availableWidth) return;
|
|
82
|
+
try {
|
|
83
|
+
textChild.textAutoResize = 'HEIGHT';
|
|
84
|
+
textChild.resize(availableWidth, textChild.height);
|
|
85
|
+
} catch (_err) {
|
|
86
|
+
// ignore resize errors
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function stabilizeHorizontalStretchChild(child: SceneNode, parent: SceneNode): void {
|
|
91
|
+
if (!parent || parent.type !== 'FRAME') return;
|
|
92
|
+
if (parent.layoutMode !== 'VERTICAL') return;
|
|
93
|
+
if (
|
|
94
|
+
!child
|
|
95
|
+
|| child.type !== 'FRAME'
|
|
96
|
+
|| child.layoutPositioning === 'ABSOLUTE'
|
|
97
|
+
|| child.layoutMode !== 'HORIZONTAL'
|
|
98
|
+
|| child.layoutAlign !== 'STRETCH'
|
|
99
|
+
|| child.primaryAxisSizingMode === 'FIXED'
|
|
100
|
+
|| child.name === 'mx-auto'
|
|
101
|
+
) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const parentContentWidth = Math.max(
|
|
106
|
+
0,
|
|
107
|
+
(parent.width || 0) - (parent.paddingLeft || 0) - (parent.paddingRight || 0),
|
|
108
|
+
);
|
|
109
|
+
if (parentContentWidth <= 0) return;
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
child.resize(parentContentWidth, Math.max(1, child.height || 1));
|
|
113
|
+
child.primaryAxisSizingMode = 'FIXED';
|
|
114
|
+
} catch (_err) {
|
|
115
|
+
// ignore resize errors
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function reflowMxAutoChildren(parent: SceneNode): void {
|
|
120
|
+
if (!parent || parent.type !== 'FRAME') return;
|
|
121
|
+
if (!Array.isArray(parent.children) || parent.children.length === 0) return;
|
|
122
|
+
const contentWidth = Math.max(0, (parent.width || 0) - (parent.paddingLeft || 0) - (parent.paddingRight || 0));
|
|
123
|
+
if (!contentWidth) return;
|
|
124
|
+
for (const child of parent.children) {
|
|
125
|
+
if (!child || child.name !== 'mx-auto') continue;
|
|
126
|
+
if (!('resize' in child)) continue;
|
|
127
|
+
try {
|
|
128
|
+
child.resize(contentWidth, Math.max(1, child.height || 1));
|
|
129
|
+
if ('layoutAlign' in child) child.layoutAlign = 'STRETCH';
|
|
130
|
+
if ('primaryAxisSizingMode' in child) child.primaryAxisSizingMode = 'FIXED';
|
|
131
|
+
if ('layoutSizingHorizontal' in child) child.layoutSizingHorizontal = 'FIXED';
|
|
132
|
+
} catch (_err) {
|
|
133
|
+
// ignore
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function applyVerticalMarginSpacing(frame: FrameNode, children: NodeIR[]): void {
|
|
139
|
+
if (!frame || frame.layoutMode !== 'VERTICAL' || !children || children.length <= 1) return;
|
|
140
|
+
let maxMarginTop = 0;
|
|
141
|
+
for (const child of children) {
|
|
142
|
+
if (!isElementLikeNode(child)) continue;
|
|
143
|
+
const mt = getNodeMarginTopPx(child);
|
|
144
|
+
if (mt > maxMarginTop) maxMarginTop = mt;
|
|
145
|
+
}
|
|
146
|
+
if (maxMarginTop > 0 && (!frame.itemSpacing || frame.itemSpacing < maxMarginTop)) {
|
|
147
|
+
frame.itemSpacing = maxMarginTop;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function enforceTabsChildSizing(
|
|
152
|
+
parentNode: NodeIR,
|
|
153
|
+
child: NodeIR,
|
|
154
|
+
childNode: SceneNode,
|
|
155
|
+
parentContentWidth?: number
|
|
156
|
+
): void {
|
|
157
|
+
if ((parentNode.kind !== 'element' && parentNode.kind !== 'component') || !isTabsRootTag(parentNode.tagName)) return;
|
|
158
|
+
if (child.kind !== 'element' && child.kind !== 'component') return;
|
|
159
|
+
|
|
160
|
+
const isTabsList = isTabsListTag(child.tagName);
|
|
161
|
+
const isTabsContent = isTabsContentTag(child.tagName);
|
|
162
|
+
if (!isTabsList && !isTabsContent) return;
|
|
163
|
+
|
|
164
|
+
const childClasses = getNodeEffectiveClasses(child);
|
|
165
|
+
const hasFullWidth = childClasses.some(cls => getBaseClass(cls) === 'w-full');
|
|
166
|
+
const shouldStretch = isTabsContent || (isTabsList && hasFullWidth);
|
|
167
|
+
if (!shouldStretch) return;
|
|
168
|
+
|
|
169
|
+
if ('layoutAlign' in childNode) {
|
|
170
|
+
try {
|
|
171
|
+
childNode.layoutAlign = 'STRETCH';
|
|
172
|
+
} catch (_err) {
|
|
173
|
+
// ignore
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (parentContentWidth == null || !Number.isFinite(parentContentWidth) || parentContentWidth <= 0) return;
|
|
178
|
+
if (childNode.type !== 'FRAME') return;
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
const targetWidth = Math.max(1, parentContentWidth);
|
|
182
|
+
childNode.resize(targetWidth, Math.max(1, childNode.height || 1));
|
|
183
|
+
if (childNode.layoutMode === 'HORIZONTAL') {
|
|
184
|
+
childNode.primaryAxisSizingMode = 'FIXED';
|
|
185
|
+
} else if (childNode.layoutMode === 'VERTICAL') {
|
|
186
|
+
childNode.counterAxisSizingMode = 'FIXED';
|
|
187
|
+
}
|
|
188
|
+
} catch (_err) {
|
|
189
|
+
// ignore resize errors
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic Figma frame helpers used by the design-system page builder.
|
|
3
|
+
* Kept distinct from layout helpers (which deal with auto-layout sizing)
|
|
4
|
+
* — these are children-array maintenance routines.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
+
export function isTextNode(node: any): boolean {
|
|
9
|
+
return !!node && node.type === 'TEXT';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
13
|
+
export function removeDirectTextChildren(parent: any, names?: string[]): void {
|
|
14
|
+
if (!parent || !Array.isArray(parent.children)) return;
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
const stale = parent.children.filter(function(child: any) {
|
|
17
|
+
if (!isTextNode(child)) return false;
|
|
18
|
+
if (!names || names.length === 0) return true;
|
|
19
|
+
return names.indexOf(child.name) !== -1 || names.indexOf(child.characters) !== -1;
|
|
20
|
+
});
|
|
21
|
+
for (let i = 0; i < stale.length; i++) {
|
|
22
|
+
stale[i].remove();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
27
|
+
export function removeDuplicateChildrenByName(parent: any, name: string, type?: string): void {
|
|
28
|
+
if (!parent || !Array.isArray(parent.children)) return;
|
|
29
|
+
let seen = false;
|
|
30
|
+
const children = parent.children.slice();
|
|
31
|
+
for (let i = 0; i < children.length; i++) {
|
|
32
|
+
const child = children[i];
|
|
33
|
+
if (!child || child.name !== name) continue;
|
|
34
|
+
if (type && child.type !== type) continue;
|
|
35
|
+
if (!seen) {
|
|
36
|
+
seen = true;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
child.remove();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function hasNodeChildren(node: BaseNode | null | undefined): boolean {
|
|
44
|
+
if (!node || !('children' in node)) return false;
|
|
45
|
+
return Array.isArray(node.children) && node.children.length > 0;
|
|
46
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const GENERATED_KEY = 'inkbridge:generated';
|
|
2
|
+
const GENERATED_SCOPE_KEY = 'inkbridge:scope';
|
|
3
|
+
const GENERATED_ROLE_KEY = 'inkbridge:role';
|
|
4
|
+
const FALLBACK_REASON_KEY = 'inkbridge:fallback-reason';
|
|
5
|
+
const SYMBOL_DECISION_KEY = 'inkbridge:symbol-decision';
|
|
6
|
+
const SYMBOL_IGNORED_PROPS_KEY = 'inkbridge:symbol-ignored-props';
|
|
7
|
+
const SYMBOL_TEXT_OVERRIDES_KEY = 'inkbridge:symbol-text-overrides';
|
|
8
|
+
const SYMBOL_SLOT_PROP_MAPPINGS_KEY = 'inkbridge:symbol-slot-prop-mappings';
|
|
9
|
+
const GENERATED_SCOPE_VALUE = 'design-system';
|
|
10
|
+
|
|
11
|
+
export function tagGeneratedNode(node: BaseNode | null | undefined, role: string): void {
|
|
12
|
+
if (!node) return;
|
|
13
|
+
try {
|
|
14
|
+
node.setPluginData(GENERATED_KEY, '1');
|
|
15
|
+
node.setPluginData(GENERATED_SCOPE_KEY, GENERATED_SCOPE_VALUE);
|
|
16
|
+
node.setPluginData(GENERATED_ROLE_KEY, role || 'unknown');
|
|
17
|
+
} catch (_error) {
|
|
18
|
+
// Not all node types support plugin data; ignore safely.
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function tagGeneratedSubtree(node: BaseNode | null | undefined, role: string): void {
|
|
23
|
+
if (!node) return;
|
|
24
|
+
tagGeneratedNode(node, role);
|
|
25
|
+
if (!('children' in node)) return;
|
|
26
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
27
|
+
tagGeneratedSubtree(node.children[i], role);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isGeneratedDesignSystemNode(node: BaseNode | null | undefined): boolean {
|
|
32
|
+
if (!node) return false;
|
|
33
|
+
try {
|
|
34
|
+
return node.getPluginData(GENERATED_KEY) === '1'
|
|
35
|
+
&& node.getPluginData(GENERATED_SCOPE_KEY) === GENERATED_SCOPE_VALUE;
|
|
36
|
+
} catch (_error) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function setGeneratedFallbackReason(node: BaseNode | null | undefined, reason: string): void {
|
|
42
|
+
if (!node) return;
|
|
43
|
+
tagGeneratedNode(node, 'instance-fallback');
|
|
44
|
+
try {
|
|
45
|
+
node.setPluginData(FALLBACK_REASON_KEY, String(reason || 'unknown'));
|
|
46
|
+
} catch (_error) {
|
|
47
|
+
// ignore
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function setPluginDataValue(node: BaseNode | null | undefined, key: string, value: string): void {
|
|
52
|
+
if (!node || !key) return;
|
|
53
|
+
try {
|
|
54
|
+
node.setPluginData(key, String(value || ''));
|
|
55
|
+
} catch (_error) {
|
|
56
|
+
// ignore
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function setGeneratedSymbolDebugData(
|
|
61
|
+
node: BaseNode | null | undefined,
|
|
62
|
+
data: {
|
|
63
|
+
decision?: string;
|
|
64
|
+
ignoredProps?: string[];
|
|
65
|
+
textOverrides?: Record<string, string>;
|
|
66
|
+
slotPropMappings?: Record<string, string>;
|
|
67
|
+
}
|
|
68
|
+
): void {
|
|
69
|
+
if (!node) return;
|
|
70
|
+
tagGeneratedNode(node, 'symbol-instance');
|
|
71
|
+
|
|
72
|
+
if (data && data.decision) {
|
|
73
|
+
setPluginDataValue(node, SYMBOL_DECISION_KEY, data.decision);
|
|
74
|
+
}
|
|
75
|
+
if (data && data.ignoredProps && data.ignoredProps.length > 0) {
|
|
76
|
+
setPluginDataValue(node, SYMBOL_IGNORED_PROPS_KEY, JSON.stringify(data.ignoredProps));
|
|
77
|
+
}
|
|
78
|
+
if (data && data.textOverrides && Object.keys(data.textOverrides).length > 0) {
|
|
79
|
+
setPluginDataValue(node, SYMBOL_TEXT_OVERRIDES_KEY, JSON.stringify(data.textOverrides));
|
|
80
|
+
}
|
|
81
|
+
if (data && data.slotPropMappings && Object.keys(data.slotPropMappings).length > 0) {
|
|
82
|
+
setPluginDataValue(node, SYMBOL_SLOT_PROP_MAPPINGS_KEY, JSON.stringify(data.slotPropMappings));
|
|
83
|
+
}
|
|
84
|
+
}
|