inkhouse 0.1.0-beta.0
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 +201 -0
- package/bin/inkhouse.mjs +171 -0
- package/code.js +11802 -0
- package/manifest.json +30 -0
- package/package.json +45 -0
- package/scanner/blob-placement-regression.ts +132 -0
- package/scanner/class-collector.ts +69 -0
- package/scanner/cli.ts +336 -0
- package/scanner/component-scanner.ts +2876 -0
- package/scanner/css-patch-regression.ts +112 -0
- package/scanner/css-token-reader-regression.ts +92 -0
- package/scanner/css-token-reader.ts +477 -0
- package/scanner/font-style-resolver-regression.ts +32 -0
- package/scanner/index.ts +9 -0
- package/scanner/radial-gradient-regression.ts +53 -0
- package/scanner/style-map.ts +145 -0
- package/scanner/tailwind-parser.ts +644 -0
- package/scanner/transform-math-regression.ts +42 -0
- package/scanner/types.ts +298 -0
- package/src/blob-placement.ts +111 -0
- package/src/change-detection.ts +204 -0
- package/src/class-utils.ts +105 -0
- package/src/clip-path-decorative.ts +194 -0
- package/src/color-resolver.ts +98 -0
- package/src/colors.ts +196 -0
- package/src/component-defs.ts +54 -0
- package/src/component-gen.ts +561 -0
- package/src/component-lookup.ts +82 -0
- package/src/config.ts +115 -0
- package/src/design-system.ts +59 -0
- package/src/dev-server.ts +173 -0
- package/src/figma-globals.d.ts +3 -0
- package/src/font-style-resolver.ts +171 -0
- package/src/github.ts +1465 -0
- package/src/icon-builder.ts +607 -0
- package/src/image-cache.ts +22 -0
- package/src/inline-text.ts +271 -0
- package/src/layout-parser.ts +667 -0
- package/src/layout-utils.ts +155 -0
- package/src/main.ts +687 -0
- package/src/node-ir.ts +595 -0
- package/src/pack-provider.ts +148 -0
- package/src/packs.ts +126 -0
- package/src/radial-gradient.ts +84 -0
- package/src/render-context.ts +138 -0
- package/src/responsive-analyzer.ts +139 -0
- package/src/state-analyzer.ts +143 -0
- package/src/story-builder.ts +1706 -0
- package/src/story-layout.ts +38 -0
- package/src/tailwind.ts +2379 -0
- package/src/text-builder.ts +116 -0
- package/src/text-line.ts +42 -0
- package/src/token-source.ts +43 -0
- package/src/tokens.ts +717 -0
- package/src/transform-math.ts +44 -0
- package/src/ui-builder.ts +1996 -0
- package/src/utility-resolver.ts +125 -0
- package/src/variables.ts +1042 -0
- package/src/width-solver.ts +466 -0
- package/templates/patch-tokens-route.ts +165 -0
- package/templates/scan-components-route.ts +57 -0
- package/ui.html +1222 -0
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import { TOKENS, COMPONENT_DEFS, getThemeColors, getThemeRadius } from './tokens';
|
|
2
|
+
import { parseColor, debug } from './colors';
|
|
3
|
+
import { getComponentDef } from './component-defs';
|
|
4
|
+
import { tailwindClassesToStyle, applyTailwindStylesToFrame } from './tailwind';
|
|
5
|
+
import { bindColorVariable, pxFromSizeToken } from './variables';
|
|
6
|
+
import { createTextNode } from './text-builder';
|
|
7
|
+
import { extractStatesFromClasses, mergeStatesWithDefinition, type StateInfo } from './state-analyzer';
|
|
8
|
+
import { extractArbitraryValue, parseLength } from './utility-resolver';
|
|
9
|
+
|
|
10
|
+
type RingInfo = { width: number; color: { r: number; g: number; b: number; a?: number } };
|
|
11
|
+
|
|
12
|
+
function getStateEntry(states: StateInfo[], name: string): StateInfo | null {
|
|
13
|
+
for (let i = 0; i < states.length; i++) {
|
|
14
|
+
if (states[i].name === name) return states[i];
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getStateNames(states: StateInfo[]): string[] {
|
|
20
|
+
const names: string[] = ['default'];
|
|
21
|
+
for (let i = 0; i < states.length; i++) {
|
|
22
|
+
const name = states[i].name;
|
|
23
|
+
if (name === 'default') continue;
|
|
24
|
+
names.push(name);
|
|
25
|
+
}
|
|
26
|
+
return names;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function buildStateClasses(states: StateInfo[], name: string): string[] {
|
|
30
|
+
const defaultEntry = getStateEntry(states, 'default');
|
|
31
|
+
const baseClasses = defaultEntry ? defaultEntry.classes.slice() : [];
|
|
32
|
+
if (name === 'default') return baseClasses;
|
|
33
|
+
const entry = getStateEntry(states, name);
|
|
34
|
+
if (!entry) return baseClasses;
|
|
35
|
+
return baseClasses.concat(entry.classes);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function parseRingWidth(utility: string): number | null {
|
|
39
|
+
if (utility === 'ring') return 3;
|
|
40
|
+
if (!utility.startsWith('ring-')) return null;
|
|
41
|
+
const token = utility.substring(5);
|
|
42
|
+
if (token === 'inset' || token.startsWith('offset-')) return null;
|
|
43
|
+
if (token.startsWith('[')) {
|
|
44
|
+
const arbitrary = extractArbitraryValue(utility);
|
|
45
|
+
if (!arbitrary) return null;
|
|
46
|
+
return parseLength(arbitrary);
|
|
47
|
+
}
|
|
48
|
+
const num = parseFloat(token);
|
|
49
|
+
if (!Number.isNaN(num) && String(num) === token) return num;
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parseRingColor(utility: string, colorGroup: Record<string, string>): { r: number; g: number; b: number; a?: number } | null {
|
|
54
|
+
if (!utility.startsWith('ring-')) return null;
|
|
55
|
+
const token = utility.substring(5);
|
|
56
|
+
if (token === 'inset' || token.startsWith('offset-')) return null;
|
|
57
|
+
if (token.startsWith('[')) return null;
|
|
58
|
+
const num = parseFloat(token);
|
|
59
|
+
if (!Number.isNaN(num) && String(num) === token) return null;
|
|
60
|
+
const resolved = colorGroup[token];
|
|
61
|
+
if (!resolved) return null;
|
|
62
|
+
return parseColor(resolved);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getRingInfoFromClasses(classes: string[], colorGroup: Record<string, string>): RingInfo | null {
|
|
66
|
+
let width: number | null = null;
|
|
67
|
+
let color: { r: number; g: number; b: number; a?: number } | null = null;
|
|
68
|
+
|
|
69
|
+
for (let i = 0; i < classes.length; i++) {
|
|
70
|
+
const cls = classes[i];
|
|
71
|
+
const nextWidth = parseRingWidth(cls);
|
|
72
|
+
if (nextWidth != null) width = nextWidth;
|
|
73
|
+
const nextColor = parseRingColor(cls, colorGroup);
|
|
74
|
+
if (nextColor) color = nextColor;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (width == null && color == null) return null;
|
|
78
|
+
if (width == null) width = 3;
|
|
79
|
+
if (!color) {
|
|
80
|
+
const fallback = colorGroup.ring || colorGroup.primary;
|
|
81
|
+
if (!fallback) return null;
|
|
82
|
+
color = parseColor(fallback);
|
|
83
|
+
}
|
|
84
|
+
if (!width || width <= 0) return null;
|
|
85
|
+
return { width, color };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function hasVisibleFill(node: FrameNode): boolean {
|
|
89
|
+
const fills = (node as any).fills;
|
|
90
|
+
if (!Array.isArray(fills)) return false;
|
|
91
|
+
for (let i = 0; i < fills.length; i++) {
|
|
92
|
+
const fill = fills[i];
|
|
93
|
+
if (!fill || fill.visible === false) continue;
|
|
94
|
+
if (fill.opacity != null && fill.opacity <= 0) continue;
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function applyRingEffect(node: FrameNode, classes: string[], colorGroup: Record<string, string>): void {
|
|
101
|
+
const ring = getRingInfoFromClasses(classes, colorGroup);
|
|
102
|
+
if (!ring) return;
|
|
103
|
+
const useSpread = (node as any).clipsContent === true && hasVisibleFill(node);
|
|
104
|
+
if (!useSpread) {
|
|
105
|
+
const strokeWeight = typeof node.strokeWeight === 'number' ? node.strokeWeight : 0;
|
|
106
|
+
node.strokes = [{
|
|
107
|
+
type: 'SOLID',
|
|
108
|
+
color: { r: ring.color.r, g: ring.color.g, b: ring.color.b },
|
|
109
|
+
opacity: ring.color.a == null ? 1 : ring.color.a,
|
|
110
|
+
}];
|
|
111
|
+
node.strokeWeight = Math.max(strokeWeight, ring.width);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const effect: DropShadowEffect = {
|
|
115
|
+
type: 'DROP_SHADOW',
|
|
116
|
+
color: { r: ring.color.r, g: ring.color.g, b: ring.color.b, a: ring.color.a == null ? 1 : ring.color.a },
|
|
117
|
+
offset: { x: 0, y: 0 },
|
|
118
|
+
radius: 0,
|
|
119
|
+
spread: ring.width,
|
|
120
|
+
visible: true,
|
|
121
|
+
blendMode: 'NORMAL'
|
|
122
|
+
};
|
|
123
|
+
const effects: Effect[] = [];
|
|
124
|
+
if (node.effects && node.effects.length > 0) {
|
|
125
|
+
for (let i = 0; i < node.effects.length; i++) effects.push(node.effects[i]);
|
|
126
|
+
}
|
|
127
|
+
effects.push(effect);
|
|
128
|
+
node.effects = effects;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function createCVAComponentSet(parent: any, def: any, theme: string): any {
|
|
132
|
+
const colorGroup = getThemeColors(TOKENS, theme);
|
|
133
|
+
const radiusGroup = getThemeRadius(TOKENS, theme);
|
|
134
|
+
|
|
135
|
+
const container = figma.createFrame();
|
|
136
|
+
container.name = def.name + ' Variants';
|
|
137
|
+
container.layoutMode = 'VERTICAL';
|
|
138
|
+
container.itemSpacing = 24;
|
|
139
|
+
container.primaryAxisSizingMode = 'AUTO';
|
|
140
|
+
container.counterAxisSizingMode = 'AUTO';
|
|
141
|
+
container.fills = [];
|
|
142
|
+
|
|
143
|
+
// Get variant keys (e.g., ['variant', 'size'])
|
|
144
|
+
const variantKeys = Object.keys(def.variants || {});
|
|
145
|
+
if (variantKeys.length === 0) {
|
|
146
|
+
debug('CVA component has no variants', def.name);
|
|
147
|
+
return container;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Get the primary variant (usually 'variant')
|
|
151
|
+
const primaryVariant = variantKeys[0];
|
|
152
|
+
const primaryValues = def.variants[primaryVariant] || [];
|
|
153
|
+
|
|
154
|
+
// Create a section for each primary variant value
|
|
155
|
+
for (let vi = 0; vi < primaryValues.length; vi++) {
|
|
156
|
+
const variantValue = primaryValues[vi];
|
|
157
|
+
|
|
158
|
+
const variantSection = figma.createFrame();
|
|
159
|
+
variantSection.name = def.name + ' - ' + variantValue;
|
|
160
|
+
variantSection.layoutMode = 'VERTICAL';
|
|
161
|
+
variantSection.itemSpacing = 8;
|
|
162
|
+
variantSection.primaryAxisSizingMode = 'AUTO';
|
|
163
|
+
variantSection.counterAxisSizingMode = 'AUTO';
|
|
164
|
+
variantSection.fills = [];
|
|
165
|
+
|
|
166
|
+
// Label for this variant
|
|
167
|
+
const label = createTextNode(variantValue.charAt(0).toUpperCase() + variantValue.slice(1), { fontSize: 12, bold: true });
|
|
168
|
+
variantSection.appendChild(label);
|
|
169
|
+
|
|
170
|
+
// Row for states
|
|
171
|
+
const stateRow = figma.createFrame();
|
|
172
|
+
stateRow.name = 'States';
|
|
173
|
+
stateRow.layoutMode = 'HORIZONTAL';
|
|
174
|
+
stateRow.itemSpacing = 12;
|
|
175
|
+
stateRow.primaryAxisSizingMode = 'AUTO';
|
|
176
|
+
stateRow.counterAxisSizingMode = 'AUTO';
|
|
177
|
+
stateRow.fills = [];
|
|
178
|
+
|
|
179
|
+
// Get classes for this variant
|
|
180
|
+
let classes = def.baseClasses.slice();
|
|
181
|
+
if (def.variantClasses && def.variantClasses[primaryVariant] && def.variantClasses[primaryVariant][variantValue]) {
|
|
182
|
+
classes = classes.concat(def.variantClasses[primaryVariant][variantValue]);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const stateInfo = extractStatesFromClasses(classes);
|
|
186
|
+
const stateNames = getStateNames(stateInfo);
|
|
187
|
+
|
|
188
|
+
for (let si = 0; si < stateNames.length; si++) {
|
|
189
|
+
const state = stateNames[si];
|
|
190
|
+
const stateClasses = buildStateClasses(stateInfo, state);
|
|
191
|
+
|
|
192
|
+
// Create component frame
|
|
193
|
+
const comp = figma.createFrame();
|
|
194
|
+
comp.name = def.name + '/' + variantValue + '/' + state;
|
|
195
|
+
comp.layoutMode = 'HORIZONTAL';
|
|
196
|
+
comp.primaryAxisSizingMode = 'AUTO';
|
|
197
|
+
comp.counterAxisSizingMode = 'AUTO';
|
|
198
|
+
comp.counterAxisAlignItems = 'CENTER';
|
|
199
|
+
comp.primaryAxisAlignItems = 'CENTER';
|
|
200
|
+
comp.itemSpacing = 8;
|
|
201
|
+
comp.paddingLeft = comp.paddingRight = 16;
|
|
202
|
+
comp.paddingTop = comp.paddingBottom = 8;
|
|
203
|
+
comp.cornerRadius = pxFromSizeToken(radiusGroup.base || '0.5rem');
|
|
204
|
+
comp.fills = [];
|
|
205
|
+
comp.strokes = [];
|
|
206
|
+
|
|
207
|
+
// Get style for this state (base + state overrides)
|
|
208
|
+
const style = tailwindClassesToStyle(stateClasses, 'default', colorGroup);
|
|
209
|
+
|
|
210
|
+
// Apply background - try variable binding first, fall back to raw color
|
|
211
|
+
if (style.bg) {
|
|
212
|
+
const bgBound = style.bgToken && bindColorVariable(comp, style.bgToken, 'fill', theme);
|
|
213
|
+
if (!bgBound) {
|
|
214
|
+
const bg = parseColor(style.bg);
|
|
215
|
+
const bgOpacity = style.bgOpacity != null ? style.bgOpacity : (bg.a == null ? 1 : bg.a);
|
|
216
|
+
comp.fills = [{ type: 'SOLID', color: { r: bg.r, g: bg.g, b: bg.b }, opacity: bgOpacity }];
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Apply border - try variable binding first, fall back to raw color
|
|
221
|
+
if (style.border) {
|
|
222
|
+
const borderBound = style.borderToken && bindColorVariable(comp, style.borderToken, 'stroke', theme);
|
|
223
|
+
if (!borderBound) {
|
|
224
|
+
const borderColor = parseColor(style.border);
|
|
225
|
+
comp.strokes = [{ type: 'SOLID', color: { r: borderColor.r, g: borderColor.g, b: borderColor.b } }];
|
|
226
|
+
}
|
|
227
|
+
comp.strokeWeight = 1;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Apply opacity for disabled
|
|
231
|
+
if (style.opacity != null) {
|
|
232
|
+
comp.opacity = style.opacity;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Create text
|
|
236
|
+
const textColor = style.text ? parseColor(style.text) : colorGroup.foreground ? parseColor(colorGroup.foreground) : { r: 0, g: 0, b: 0 };
|
|
237
|
+
const text = createTextNode(def.name, { bold: true, fontSize: 14, fill: { r: textColor.r, g: textColor.g, b: textColor.b } });
|
|
238
|
+
|
|
239
|
+
// Apply underline for link variant
|
|
240
|
+
if (style.underline && text.textDecoration !== undefined) {
|
|
241
|
+
text.textDecoration = 'UNDERLINE';
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
comp.appendChild(text);
|
|
245
|
+
applyRingEffect(comp, stateClasses, colorGroup);
|
|
246
|
+
stateRow.appendChild(comp);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
variantSection.appendChild(stateRow);
|
|
250
|
+
container.appendChild(variantSection);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
parent.appendChild(container);
|
|
254
|
+
return container;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function createStateComponentSet(parent: any, def: any, theme: string): any {
|
|
258
|
+
const colorGroup = getThemeColors(TOKENS, theme);
|
|
259
|
+
const radiusGroup = getThemeRadius(TOKENS, theme);
|
|
260
|
+
|
|
261
|
+
const container = figma.createFrame();
|
|
262
|
+
container.name = def.name + ' States';
|
|
263
|
+
container.layoutMode = 'VERTICAL';
|
|
264
|
+
container.itemSpacing = 16;
|
|
265
|
+
container.primaryAxisSizingMode = 'AUTO';
|
|
266
|
+
container.counterAxisSizingMode = 'AUTO';
|
|
267
|
+
container.fills = [];
|
|
268
|
+
|
|
269
|
+
// Label
|
|
270
|
+
const label = createTextNode(def.name, { fontSize: 14, bold: true });
|
|
271
|
+
container.appendChild(label);
|
|
272
|
+
|
|
273
|
+
const stateRow = figma.createFrame();
|
|
274
|
+
stateRow.name = 'States';
|
|
275
|
+
stateRow.layoutMode = 'HORIZONTAL';
|
|
276
|
+
stateRow.itemSpacing = 16;
|
|
277
|
+
stateRow.primaryAxisSizingMode = 'AUTO';
|
|
278
|
+
stateRow.counterAxisSizingMode = 'AUTO';
|
|
279
|
+
stateRow.fills = [];
|
|
280
|
+
|
|
281
|
+
const baseClasses = def.baseClasses ? def.baseClasses.slice() : [];
|
|
282
|
+
const extractedStates = extractStatesFromClasses(baseClasses);
|
|
283
|
+
const mergedStates = mergeStatesWithDefinition(extractedStates, def);
|
|
284
|
+
const stateNames = getStateNames(mergedStates);
|
|
285
|
+
|
|
286
|
+
for (let i = 0; i < stateNames.length; i++) {
|
|
287
|
+
const stateName = stateNames[i];
|
|
288
|
+
const stateClasses = buildStateClasses(mergedStates, stateName);
|
|
289
|
+
|
|
290
|
+
const comp = figma.createFrame();
|
|
291
|
+
comp.name = def.name + '/' + stateName;
|
|
292
|
+
comp.layoutMode = 'HORIZONTAL';
|
|
293
|
+
comp.primaryAxisSizingMode = 'FIXED';
|
|
294
|
+
comp.counterAxisSizingMode = 'AUTO';
|
|
295
|
+
comp.resize(200, comp.height);
|
|
296
|
+
comp.paddingLeft = comp.paddingRight = 12;
|
|
297
|
+
comp.paddingTop = comp.paddingBottom = 8;
|
|
298
|
+
comp.cornerRadius = pxFromSizeToken(radiusGroup.base || '0.5rem');
|
|
299
|
+
comp.fills = [];
|
|
300
|
+
comp.strokes = [];
|
|
301
|
+
|
|
302
|
+
// Apply base + state styles
|
|
303
|
+
const style = tailwindClassesToStyle(stateClasses, 'default', colorGroup);
|
|
304
|
+
|
|
305
|
+
if (style.bg) {
|
|
306
|
+
const bgBound = style.bgToken && bindColorVariable(comp, style.bgToken, 'fill', theme);
|
|
307
|
+
if (!bgBound) {
|
|
308
|
+
const bg = parseColor(style.bg);
|
|
309
|
+
const bgOpacity = style.bgOpacity != null ? style.bgOpacity : (bg.a == null ? 1 : bg.a);
|
|
310
|
+
comp.fills = [{ type: 'SOLID', color: { r: bg.r, g: bg.g, b: bg.b }, opacity: bgOpacity }];
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (style.border) {
|
|
315
|
+
const borderBound = style.borderToken && bindColorVariable(comp, style.borderToken, 'stroke', theme);
|
|
316
|
+
if (!borderBound) {
|
|
317
|
+
const borderColor = parseColor(style.border);
|
|
318
|
+
comp.strokes = [{ type: 'SOLID', color: { r: borderColor.r, g: borderColor.g, b: borderColor.b } }];
|
|
319
|
+
}
|
|
320
|
+
comp.strokeWeight = 1;
|
|
321
|
+
} else {
|
|
322
|
+
let hasBorderWidth = false;
|
|
323
|
+
for (let j = 0; j < stateClasses.length; j++) {
|
|
324
|
+
const cls = stateClasses[j];
|
|
325
|
+
if (cls === 'border' || /^border-\\d+$/.test(cls)) {
|
|
326
|
+
hasBorderWidth = true;
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (hasBorderWidth) {
|
|
331
|
+
const inputBorder = colorGroup.input || colorGroup.border;
|
|
332
|
+
if (inputBorder) {
|
|
333
|
+
const borderColor = parseColor(inputBorder);
|
|
334
|
+
comp.strokes = [{ type: 'SOLID', color: { r: borderColor.r, g: borderColor.g, b: borderColor.b } }];
|
|
335
|
+
comp.strokeWeight = 1;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (style.opacity != null) {
|
|
341
|
+
comp.opacity = style.opacity;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Add placeholder text
|
|
345
|
+
let placeholderColor = colorGroup['muted-foreground'] ? parseColor(colorGroup['muted-foreground']) : { r: 0.5, g: 0.5, b: 0.5 };
|
|
346
|
+
if (style.text) placeholderColor = parseColor(style.text);
|
|
347
|
+
const placeholder = createTextNode(stateName === 'default' ? 'Placeholder...' : stateName.charAt(0).toUpperCase() + stateName.slice(1), {
|
|
348
|
+
fontSize: 14,
|
|
349
|
+
fill: { r: placeholderColor.r, g: placeholderColor.g, b: placeholderColor.b }
|
|
350
|
+
});
|
|
351
|
+
comp.appendChild(placeholder);
|
|
352
|
+
applyRingEffect(comp, stateClasses, colorGroup);
|
|
353
|
+
|
|
354
|
+
stateRow.appendChild(comp);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
container.appendChild(stateRow);
|
|
358
|
+
parent.appendChild(container);
|
|
359
|
+
return container;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function createCompoundComponent(parent: any, def: any, theme: string): any {
|
|
363
|
+
const colorGroup = getThemeColors(TOKENS, theme);
|
|
364
|
+
const radiusGroup = getThemeRadius(TOKENS, theme);
|
|
365
|
+
|
|
366
|
+
const container = figma.createFrame();
|
|
367
|
+
container.name = def.name;
|
|
368
|
+
container.layoutMode = 'VERTICAL';
|
|
369
|
+
container.primaryAxisSizingMode = 'AUTO';
|
|
370
|
+
container.counterAxisSizingMode = 'AUTO';
|
|
371
|
+
container.fills = [];
|
|
372
|
+
container.strokes = [];
|
|
373
|
+
|
|
374
|
+
// Find the main container sub-component
|
|
375
|
+
let mainSub: any = null;
|
|
376
|
+
const subComponents = def.subComponents || [];
|
|
377
|
+
for (let i = 0; i < subComponents.length; i++) {
|
|
378
|
+
if (subComponents[i].slot === 'container' || subComponents[i].name === def.name) {
|
|
379
|
+
mainSub = subComponents[i];
|
|
380
|
+
break;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Apply main container styles
|
|
385
|
+
if (mainSub) {
|
|
386
|
+
applyTailwindStylesToFrame(container, mainSub.classes, colorGroup, radiusGroup, theme);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Set minimum size for card-like components
|
|
390
|
+
container.resize(320, container.height);
|
|
391
|
+
container.primaryAxisSizingMode = 'AUTO';
|
|
392
|
+
|
|
393
|
+
// Helper to find a sub-component by slot
|
|
394
|
+
function findSub(slot: string): any {
|
|
395
|
+
for (let j = 0; j < subComponents.length; j++) {
|
|
396
|
+
if (subComponents[j].slot === slot) return subComponents[j];
|
|
397
|
+
}
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Build structure: header wraps title + description, then content, then footer
|
|
402
|
+
// This mirrors how shadcn compound components actually nest
|
|
403
|
+
const headerSub = findSub('header');
|
|
404
|
+
const titleSub = findSub('title');
|
|
405
|
+
const descSub = findSub('description');
|
|
406
|
+
const contentSub = findSub('content');
|
|
407
|
+
const footerSub = findSub('footer');
|
|
408
|
+
|
|
409
|
+
// --- Header (contains title + description) ---
|
|
410
|
+
const hasTitle = !!titleSub;
|
|
411
|
+
const hasDesc = !!descSub;
|
|
412
|
+
if (headerSub && (hasTitle || hasDesc)) {
|
|
413
|
+
const headerFrame = figma.createFrame();
|
|
414
|
+
headerFrame.name = headerSub.name;
|
|
415
|
+
headerFrame.layoutMode = 'VERTICAL';
|
|
416
|
+
headerFrame.primaryAxisSizingMode = 'AUTO';
|
|
417
|
+
headerFrame.counterAxisSizingMode = 'AUTO';
|
|
418
|
+
headerFrame.fills = [];
|
|
419
|
+
applyTailwindStylesToFrame(headerFrame, headerSub.classes, colorGroup, radiusGroup, theme);
|
|
420
|
+
|
|
421
|
+
if (hasTitle) {
|
|
422
|
+
const titleFrame = figma.createFrame();
|
|
423
|
+
titleFrame.name = titleSub.name;
|
|
424
|
+
titleFrame.layoutMode = 'VERTICAL';
|
|
425
|
+
titleFrame.primaryAxisSizingMode = 'AUTO';
|
|
426
|
+
titleFrame.counterAxisSizingMode = 'AUTO';
|
|
427
|
+
titleFrame.fills = [];
|
|
428
|
+
applyTailwindStylesToFrame(titleFrame, titleSub.classes, colorGroup, radiusGroup, theme);
|
|
429
|
+
const titleText = createTextNode(def.name + ' Title', { fontSize: 18, bold: true });
|
|
430
|
+
titleFrame.appendChild(titleText);
|
|
431
|
+
headerFrame.appendChild(titleFrame);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (hasDesc) {
|
|
435
|
+
const descFrame = figma.createFrame();
|
|
436
|
+
descFrame.name = descSub.name;
|
|
437
|
+
descFrame.layoutMode = 'VERTICAL';
|
|
438
|
+
descFrame.primaryAxisSizingMode = 'AUTO';
|
|
439
|
+
descFrame.counterAxisSizingMode = 'AUTO';
|
|
440
|
+
descFrame.fills = [];
|
|
441
|
+
applyTailwindStylesToFrame(descFrame, descSub.classes, colorGroup, radiusGroup, theme);
|
|
442
|
+
const descColor = colorGroup['muted-foreground'] ? parseColor(colorGroup['muted-foreground']) : { r: 0.5, g: 0.5, b: 0.5 };
|
|
443
|
+
const descText = createTextNode('Description text goes here', { fontSize: 14, fill: { r: descColor.r, g: descColor.g, b: descColor.b } });
|
|
444
|
+
descFrame.appendChild(descText);
|
|
445
|
+
headerFrame.appendChild(descFrame);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
container.appendChild(headerFrame);
|
|
449
|
+
} else {
|
|
450
|
+
// No header wrapper - add title and description directly
|
|
451
|
+
if (hasTitle) {
|
|
452
|
+
const titleText2 = createTextNode(def.name + ' Title', { fontSize: 18, bold: true });
|
|
453
|
+
container.appendChild(titleText2);
|
|
454
|
+
}
|
|
455
|
+
if (hasDesc) {
|
|
456
|
+
const descColor2 = colorGroup['muted-foreground'] ? parseColor(colorGroup['muted-foreground']) : { r: 0.5, g: 0.5, b: 0.5 };
|
|
457
|
+
const descText2 = createTextNode('Description text goes here', { fontSize: 14, fill: { r: descColor2.r, g: descColor2.g, b: descColor2.b } });
|
|
458
|
+
container.appendChild(descText2);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// --- Content ---
|
|
463
|
+
if (contentSub) {
|
|
464
|
+
const contentFrame = figma.createFrame();
|
|
465
|
+
contentFrame.name = contentSub.name;
|
|
466
|
+
contentFrame.layoutMode = 'VERTICAL';
|
|
467
|
+
contentFrame.primaryAxisSizingMode = 'AUTO';
|
|
468
|
+
contentFrame.counterAxisSizingMode = 'AUTO';
|
|
469
|
+
contentFrame.fills = [];
|
|
470
|
+
applyTailwindStylesToFrame(contentFrame, contentSub.classes, colorGroup, radiusGroup, theme);
|
|
471
|
+
const contentText = createTextNode('Content area', { fontSize: 14 });
|
|
472
|
+
contentFrame.appendChild(contentText);
|
|
473
|
+
container.appendChild(contentFrame);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// --- Footer ---
|
|
477
|
+
if (footerSub) {
|
|
478
|
+
const footerFrame = figma.createFrame();
|
|
479
|
+
footerFrame.name = footerSub.name;
|
|
480
|
+
footerFrame.layoutMode = 'HORIZONTAL';
|
|
481
|
+
footerFrame.primaryAxisSizingMode = 'AUTO';
|
|
482
|
+
footerFrame.counterAxisSizingMode = 'AUTO';
|
|
483
|
+
footerFrame.fills = [];
|
|
484
|
+
applyTailwindStylesToFrame(footerFrame, footerSub.classes, colorGroup, radiusGroup, theme);
|
|
485
|
+
const footerText = createTextNode('Footer', { fontSize: 12 });
|
|
486
|
+
footerFrame.appendChild(footerText);
|
|
487
|
+
container.appendChild(footerFrame);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
parent.appendChild(container);
|
|
491
|
+
return container;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export function createSimpleComponent(parent: any, def: any, theme: string): any {
|
|
495
|
+
const colorGroup = getThemeColors(TOKENS, theme);
|
|
496
|
+
const radiusGroup = getThemeRadius(TOKENS, theme);
|
|
497
|
+
|
|
498
|
+
const frame = figma.createFrame();
|
|
499
|
+
frame.name = def.name;
|
|
500
|
+
frame.layoutMode = 'VERTICAL';
|
|
501
|
+
frame.primaryAxisSizingMode = 'AUTO';
|
|
502
|
+
frame.counterAxisSizingMode = 'AUTO';
|
|
503
|
+
frame.fills = [];
|
|
504
|
+
|
|
505
|
+
applyTailwindStylesToFrame(frame, def.classes || [], colorGroup, radiusGroup, theme);
|
|
506
|
+
|
|
507
|
+
// Add label
|
|
508
|
+
const label = createTextNode(def.name, { fontSize: 14 });
|
|
509
|
+
frame.appendChild(label);
|
|
510
|
+
|
|
511
|
+
parent.appendChild(frame);
|
|
512
|
+
return frame;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export function createComponentFromDef(parent: any, componentName: string, theme: string, options?: any): any {
|
|
516
|
+
const def = getComponentDef(componentName);
|
|
517
|
+
if (!def) {
|
|
518
|
+
debug('Component not found in COMPONENT_DEFS:', componentName);
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
debug('Creating component from def:', { name: componentName, type: def.type });
|
|
523
|
+
|
|
524
|
+
switch (def.type) {
|
|
525
|
+
case 'cva':
|
|
526
|
+
return createCVAComponentSet(parent, def, theme);
|
|
527
|
+
case 'compound':
|
|
528
|
+
return createCompoundComponent(parent, def, theme);
|
|
529
|
+
case 'state':
|
|
530
|
+
return createStateComponentSet(parent, def, theme);
|
|
531
|
+
case 'simple':
|
|
532
|
+
return createSimpleComponent(parent, def, theme);
|
|
533
|
+
default:
|
|
534
|
+
debug('Unknown component type:', def.type);
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export function createAllComponentsFromDefs(parent: any, theme: string, options?: any): any[] {
|
|
540
|
+
options = options || {};
|
|
541
|
+
const allowedTypes = options.types || ['cva', 'compound', 'state', 'simple'];
|
|
542
|
+
const excludeNames = options.exclude || [];
|
|
543
|
+
|
|
544
|
+
const components = COMPONENT_DEFS && COMPONENT_DEFS.components ? COMPONENT_DEFS.components : [];
|
|
545
|
+
const created: any[] = [];
|
|
546
|
+
|
|
547
|
+
for (let i = 0; i < components.length; i++) {
|
|
548
|
+
const def = components[i];
|
|
549
|
+
|
|
550
|
+
// Filter by type
|
|
551
|
+
if (allowedTypes.indexOf(def.type) === -1) continue;
|
|
552
|
+
|
|
553
|
+
// Filter by exclude list
|
|
554
|
+
if (excludeNames.indexOf(def.name) !== -1) continue;
|
|
555
|
+
|
|
556
|
+
const comp = createComponentFromDef(parent, def.name, theme, options);
|
|
557
|
+
if (comp) created.push(comp);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return created;
|
|
561
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { COMPONENT_DEFS } from './tokens';
|
|
2
|
+
|
|
3
|
+
export type IconRegistryEntry = {
|
|
4
|
+
module: string;
|
|
5
|
+
exportName: string;
|
|
6
|
+
svg: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const REACT_ICON_MAP: Record<string, string> = {
|
|
10
|
+
HiOutlineHome: 'home',
|
|
11
|
+
HiHome: 'home',
|
|
12
|
+
RiTableView: 'portfolio',
|
|
13
|
+
HiMiniSquares2x2: 'portfolio',
|
|
14
|
+
RiCompass3Line: 'strategy',
|
|
15
|
+
RiWallet2Fill: 'pda',
|
|
16
|
+
RiAccountBoxLine: 'account',
|
|
17
|
+
HiLogout: 'logout',
|
|
18
|
+
RiLogoutBoxLine: 'logout',
|
|
19
|
+
HiMenu: 'menu',
|
|
20
|
+
HiX: 'close',
|
|
21
|
+
HiCheck: 'check',
|
|
22
|
+
ChevronDown: 'chevron-down',
|
|
23
|
+
ChevronDownIcon: 'chevron-down',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
let COMPONENT_DEF_MAP: Record<string, any> | null = null;
|
|
27
|
+
let ICON_REGISTRY_MAP: Record<string, IconRegistryEntry> | null = null;
|
|
28
|
+
|
|
29
|
+
export function getComponentDefByName(name: string): any | null {
|
|
30
|
+
const defName = String(name || '').toLowerCase();
|
|
31
|
+
if (!COMPONENT_DEF_MAP) {
|
|
32
|
+
COMPONENT_DEF_MAP = {};
|
|
33
|
+
const defsArray = (COMPONENT_DEFS && COMPONENT_DEFS.components) ? COMPONENT_DEFS.components : [];
|
|
34
|
+
for (let i = 0; i < defsArray.length; i++) {
|
|
35
|
+
const d = defsArray[i];
|
|
36
|
+
const analysis = d.analysis || d;
|
|
37
|
+
if (!analysis || !analysis.name) continue;
|
|
38
|
+
const key = String(analysis.name).toLowerCase();
|
|
39
|
+
if (!COMPONENT_DEF_MAP[key]) {
|
|
40
|
+
COMPONENT_DEF_MAP[key] = d;
|
|
41
|
+
}
|
|
42
|
+
// Also index under dash-stripped key so PascalCase JSX refs (e.g. "HeroSection")
|
|
43
|
+
// resolve to kebab-case scanner names (e.g. "Hero-section" → "herosection").
|
|
44
|
+
const dashlessKey = key.replace(/-/g, '');
|
|
45
|
+
if (dashlessKey !== key && !COMPONENT_DEF_MAP[dashlessKey]) {
|
|
46
|
+
COMPONENT_DEF_MAP[dashlessKey] = d;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return COMPONENT_DEF_MAP[defName] || null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getIconRegistry(): Record<string, IconRegistryEntry> {
|
|
54
|
+
if (ICON_REGISTRY_MAP) return ICON_REGISTRY_MAP;
|
|
55
|
+
const registry = (COMPONENT_DEFS && COMPONENT_DEFS.iconRegistry) ? COMPONENT_DEFS.iconRegistry : {};
|
|
56
|
+
ICON_REGISTRY_MAP = {};
|
|
57
|
+
for (const key in registry) {
|
|
58
|
+
ICON_REGISTRY_MAP[key] = registry[key];
|
|
59
|
+
}
|
|
60
|
+
return ICON_REGISTRY_MAP;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getIconRegistryEntry(name: string): IconRegistryEntry | null {
|
|
64
|
+
if (!name) return null;
|
|
65
|
+
const registry = getIconRegistry();
|
|
66
|
+
if (registry[name]) return registry[name];
|
|
67
|
+
const target = name.toLowerCase();
|
|
68
|
+
for (const key in registry) {
|
|
69
|
+
if (key.toLowerCase() === target) return registry[key];
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getReactIconKey(name: string): string | null {
|
|
75
|
+
if (!name) return null;
|
|
76
|
+
if (REACT_ICON_MAP[name]) return REACT_ICON_MAP[name];
|
|
77
|
+
const target = name.toLowerCase();
|
|
78
|
+
for (const key in REACT_ICON_MAP) {
|
|
79
|
+
if (key.toLowerCase() === target) return REACT_ICON_MAP[key];
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|