@kernlang/vue 3.1.4 → 3.1.6

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.
@@ -0,0 +1,697 @@
1
+ /**
2
+ * Vue 3 Tailwind Transpiler — generates <script setup> + <template> with Tailwind classes
3
+ *
4
+ * Mirrors @kernlang/react/transpiler-tailwind.ts but outputs Vue SFCs with
5
+ * Tailwind utility classes instead of React JSX. No <style scoped> block —
6
+ * all styling is done via Tailwind class attributes.
7
+ */
8
+ import { stylesToTailwind, colorToTw, countTokens, serializeIR, camelKey, escapeJsString, buildTailwindProfile, applyTailwindTokenRules, getProps, getStyles, getPseudoStyles, getThemeRefs, buildDiagnostics, accountNode, expandStyles, } from '@kernlang/core';
9
+ function createBuilder(config) {
10
+ return {
11
+ templateLines: [],
12
+ vueImports: new Set(),
13
+ stateDecls: [],
14
+ eventHandlers: [],
15
+ logicBlocks: [],
16
+ sourceMap: [],
17
+ themes: {},
18
+ config,
19
+ i18nEnabled: config?.i18n?.enabled ?? false,
20
+ colors: config?.colors,
21
+ twProfile: config?.frameworkVersions ? buildTailwindProfile(config.frameworkVersions) : undefined,
22
+ };
23
+ }
24
+ // ── Theme Collection ─────────────────────────────────────────────────────
25
+ function collectThemes(node, ctx) {
26
+ if (node.type === 'theme' && node.props?.styles) {
27
+ const name = node.props.name || `theme_${ctx.templateLines.length}`;
28
+ ctx.themes[name] = node.props.styles;
29
+ }
30
+ if (node.children)
31
+ node.children.forEach(c => collectThemes(c, ctx));
32
+ }
33
+ // ── Tailwind Class Generation ────────────────────────────────────────────
34
+ function twClasses(node, ctx, extra = '') {
35
+ // Merge theme refs + inline styles — cast to string since Tailwind only uses string values
36
+ let merged = {};
37
+ for (const ref of getThemeRefs(node)) {
38
+ if (ctx.themes[ref]) {
39
+ const expanded = expandStyles(ctx.themes[ref]);
40
+ for (const [k, v] of Object.entries(expanded))
41
+ merged[k] = String(v);
42
+ }
43
+ }
44
+ const styles = getStyles(node);
45
+ if (Object.keys(styles).length > 0) {
46
+ const expanded = expandStyles(styles);
47
+ for (const [k, v] of Object.entries(expanded))
48
+ merged[k] = String(v);
49
+ }
50
+ let tw = stylesToTailwind(merged, ctx.colors);
51
+ if (ctx.twProfile)
52
+ tw = applyTailwindTokenRules(tw, ctx.twProfile);
53
+ // Generate pseudo-class Tailwind variants
54
+ const pseudo = getPseudoStyles(node);
55
+ const pseudoClasses = [];
56
+ for (const [state, stateStyles] of Object.entries(pseudo)) {
57
+ const twState = state === 'press' ? 'active' : state;
58
+ let expanded = stylesToTailwind(stateStyles, ctx.colors);
59
+ if (ctx.twProfile)
60
+ expanded = applyTailwindTokenRules(expanded, ctx.twProfile);
61
+ if (expanded) {
62
+ pseudoClasses.push(expanded.split(' ').map(c => `${twState}:${c}`).join(' '));
63
+ }
64
+ }
65
+ const parts = [tw, ...pseudoClasses, extra].filter(Boolean);
66
+ return parts.length > 0 ? ` class="${parts.join(' ')}"` : '';
67
+ }
68
+ // ── i18n helper ──────────────────────────────────────────────────────────
69
+ function tText(ctx, key, value) {
70
+ return ctx.i18nEnabled ? `{{ t('${escapeJsString(key)}', '${escapeJsString(value)}') }}` : value;
71
+ }
72
+ // ── Semantic text elements ───────────────────────────────────────────────
73
+ function textElement(variant) {
74
+ if (!variant)
75
+ return 'p';
76
+ const map = { h1: 'h1', h2: 'h2', h3: 'h3', h4: 'h4', h5: 'h5', h6: 'h6', caption: 'small', small: 'small', code: 'code' };
77
+ return map[variant] || 'p';
78
+ }
79
+ // ── Dark background detection ────────────────────────────────────────────
80
+ function isDarkColor(hex) {
81
+ if (!hex || !hex.startsWith('#'))
82
+ return false;
83
+ const c = hex.replace('#', '');
84
+ const r = parseInt(c.substring(0, 2), 16);
85
+ const g = parseInt(c.substring(2, 4), 16);
86
+ const b = parseInt(c.substring(4, 6), 16);
87
+ return (r * 0.299 + g * 0.587 + b * 0.114) < 128;
88
+ }
89
+ // ── Event Type Mapping ──────────────────────────────────────────────────
90
+ function eventParamType(event) {
91
+ if (event === 'click')
92
+ return 'e: MouseEvent';
93
+ if (event === 'submit')
94
+ return 'e: Event';
95
+ if (event === 'change')
96
+ return 'e: Event';
97
+ if (event === 'key' || event === 'keydown' || event === 'keyup')
98
+ return 'e: KeyboardEvent';
99
+ if (event === 'focus' || event === 'blur')
100
+ return 'e: FocusEvent';
101
+ if (event === 'drag' || event === 'drop')
102
+ return 'e: DragEvent';
103
+ if (event === 'scroll')
104
+ return 'e: Event';
105
+ if (event === 'resize')
106
+ return '';
107
+ if (event === 'input')
108
+ return 'e: Event';
109
+ return 'e: Event';
110
+ }
111
+ function collectOnHandler(node, ctx) {
112
+ const props = getProps(node);
113
+ const event = (props.event || props.name);
114
+ const handlerRef = props.handler;
115
+ const key = props.key;
116
+ const isAsync = props.async === 'true' || props.async === true;
117
+ const handlerChild = (node.children || []).find(c => c.type === 'handler');
118
+ const code = handlerChild ? (getProps(handlerChild).code || '') : '';
119
+ if (handlerRef && !code)
120
+ return;
121
+ const fnName = handlerRef || `handle${event.charAt(0).toUpperCase() + event.slice(1)}`;
122
+ const paramType = eventParamType(event);
123
+ const needsMounted = event === 'key' || event === 'keydown' || event === 'keyup' || event === 'resize';
124
+ if (needsMounted) {
125
+ ctx.vueImports.add('onMounted');
126
+ ctx.vueImports.add('onUnmounted');
127
+ }
128
+ ctx.eventHandlers.push({ event, fnName, code, isAsync, key, paramType });
129
+ }
130
+ // ── Node rendering ───────────────────────────────────────────────────────
131
+ const NON_VISUAL = new Set(['state', 'logic', 'theme', 'handler', 'on']);
132
+ function renderNode(node, ctx, indent) {
133
+ const props = getProps(node);
134
+ const irLine = node.loc?.line || 0;
135
+ ctx.sourceMap.push({ irLine, irCol: node.loc?.col || 1, outLine: ctx.templateLines.length + 1, outCol: 1 });
136
+ switch (node.type) {
137
+ case 'state':
138
+ ctx.vueImports.add('ref');
139
+ ctx.stateDecls.push({ name: props.name, initial: props.initial });
140
+ return;
141
+ case 'logic':
142
+ ctx.logicBlocks.push(props.code);
143
+ return;
144
+ case 'on':
145
+ collectOnHandler(node, ctx);
146
+ return;
147
+ case 'theme':
148
+ case 'handler':
149
+ return;
150
+ default:
151
+ break;
152
+ }
153
+ switch (node.type) {
154
+ case 'screen':
155
+ renderScreen(node, ctx, indent);
156
+ break;
157
+ case 'section':
158
+ renderSection(node, ctx, indent);
159
+ break;
160
+ case 'card':
161
+ renderCard(node, ctx, indent);
162
+ break;
163
+ case 'row':
164
+ renderRow(node, ctx, indent);
165
+ break;
166
+ case 'col':
167
+ renderCol(node, ctx, indent);
168
+ break;
169
+ case 'text':
170
+ renderText(node, ctx, indent);
171
+ break;
172
+ case 'divider':
173
+ renderDivider(node, ctx, indent);
174
+ break;
175
+ case 'button':
176
+ renderButton(node, ctx, indent);
177
+ break;
178
+ case 'input':
179
+ renderInput(node, ctx, indent);
180
+ break;
181
+ case 'slider':
182
+ renderSlider(node, ctx, indent);
183
+ break;
184
+ case 'toggle':
185
+ renderToggle(node, ctx, indent);
186
+ break;
187
+ case 'grid':
188
+ renderGrid(node, ctx, indent);
189
+ break;
190
+ case 'conditional':
191
+ renderConditional(node, ctx, indent);
192
+ break;
193
+ case 'icon':
194
+ renderIcon(node, ctx, indent);
195
+ break;
196
+ case 'svg':
197
+ renderSvgNode(node, ctx, indent);
198
+ break;
199
+ case 'form':
200
+ ctx.templateLines.push(`${indent}<form${twClasses(node, ctx)}>`);
201
+ renderChildren(node, ctx, indent);
202
+ ctx.templateLines.push(`${indent}</form>`);
203
+ break;
204
+ case 'image':
205
+ renderImage(node, ctx, indent);
206
+ break;
207
+ case 'list':
208
+ renderList(node, ctx, indent);
209
+ break;
210
+ case 'item':
211
+ renderItem(node, ctx, indent);
212
+ break;
213
+ case 'tabs':
214
+ renderTabs(node, ctx, indent);
215
+ break;
216
+ case 'tab':
217
+ renderTab(node, ctx, indent);
218
+ break;
219
+ case 'progress':
220
+ renderProgress(node, ctx, indent);
221
+ break;
222
+ default:
223
+ ctx.templateLines.push(`${indent}<div${twClasses(node, ctx)}>`);
224
+ renderChildren(node, ctx, indent);
225
+ ctx.templateLines.push(`${indent}</div>`);
226
+ }
227
+ }
228
+ function renderChildren(node, ctx, indent) {
229
+ if (node.children) {
230
+ for (const child of node.children) {
231
+ renderNode(child, ctx, indent + ' ');
232
+ }
233
+ }
234
+ }
235
+ function renderScreen(node, ctx, indent) {
236
+ // Check raw styles + theme refs for background color
237
+ let bgColor = '';
238
+ for (const ref of getThemeRefs(node)) {
239
+ if (ctx.themes[ref]?.backgroundColor)
240
+ bgColor = ctx.themes[ref].backgroundColor;
241
+ if (ctx.themes[ref]?.bg)
242
+ bgColor = ctx.themes[ref].bg;
243
+ }
244
+ const styles = getStyles(node);
245
+ if (styles.backgroundColor)
246
+ bgColor = styles.backgroundColor;
247
+ if (styles.bg)
248
+ bgColor = styles.bg;
249
+ const isDark = isDarkColor(bgColor);
250
+ const textClass = isDark ? 'text-white' : 'text-zinc-900';
251
+ ctx.templateLines.push(`${indent}<div${twClasses(node, ctx, `space-y-8 ${textClass} min-h-screen`)}>`);
252
+ renderChildren(node, ctx, indent);
253
+ ctx.templateLines.push(`${indent}</div>`);
254
+ }
255
+ function renderSection(node, ctx, indent) {
256
+ const p = getProps(node);
257
+ const title = p.title || '';
258
+ const key = p.key || camelKey(title);
259
+ ctx.templateLines.push(`${indent}<div>`);
260
+ if (title) {
261
+ ctx.templateLines.push(`${indent} <h3 class="text-sm font-medium text-white mb-4">`);
262
+ ctx.templateLines.push(`${indent} ${tText(ctx, `${key}.title`, title)}`);
263
+ ctx.templateLines.push(`${indent} </h3>`);
264
+ }
265
+ renderChildren(node, ctx, indent);
266
+ ctx.templateLines.push(`${indent}</div>`);
267
+ }
268
+ function renderCard(node, ctx, indent) {
269
+ const styles = getStyles(node);
270
+ const border = styles.border;
271
+ let extra = 'shadow-sm';
272
+ if (border) {
273
+ const borderClass = colorToTw('border', border, ctx.colors);
274
+ extra = `shadow-sm border ${borderClass}`;
275
+ }
276
+ // Temporarily remove border from styles for tw conversion
277
+ if (node.props && border) {
278
+ const origStyles = node.props.styles;
279
+ const cleaned = { ...origStyles };
280
+ delete cleaned.border;
281
+ node.props.styles = cleaned;
282
+ ctx.templateLines.push(`${indent}<div${twClasses(node, ctx, extra)}>`);
283
+ renderChildren(node, ctx, indent);
284
+ ctx.templateLines.push(`${indent}</div>`);
285
+ node.props.styles = origStyles;
286
+ }
287
+ else {
288
+ ctx.templateLines.push(`${indent}<div${twClasses(node, ctx, extra)}>`);
289
+ renderChildren(node, ctx, indent);
290
+ ctx.templateLines.push(`${indent}</div>`);
291
+ }
292
+ }
293
+ function renderRow(node, ctx, indent) {
294
+ ctx.templateLines.push(`${indent}<div${twClasses(node, ctx, 'flex')}>`);
295
+ renderChildren(node, ctx, indent);
296
+ ctx.templateLines.push(`${indent}</div>`);
297
+ }
298
+ function renderCol(node, ctx, indent) {
299
+ ctx.templateLines.push(`${indent}<div${twClasses(node, ctx, 'flex flex-col')}>`);
300
+ renderChildren(node, ctx, indent);
301
+ ctx.templateLines.push(`${indent}</div>`);
302
+ }
303
+ function renderText(node, ctx, indent) {
304
+ const p = getProps(node);
305
+ const rawValue = p.value;
306
+ const variant = p.variant;
307
+ const tag = p.tag || undefined;
308
+ const el = tag || textElement(variant);
309
+ const tw = twClasses(node, ctx);
310
+ if (!rawValue)
311
+ return;
312
+ // Expression object: { __expr: true, code: "count" } → {{ count }}
313
+ if (typeof rawValue === 'object' && rawValue !== null && '__expr' in rawValue) {
314
+ ctx.templateLines.push(`${indent}<${el}${tw}>{{ ${rawValue.code} }}</${el}>`);
315
+ return;
316
+ }
317
+ const value = rawValue;
318
+ if (typeof value !== 'string')
319
+ return;
320
+ if (value.startsWith('{{') && value.endsWith('}}')) {
321
+ ctx.templateLines.push(`${indent}<${el}${tw}>{{ ${value.slice(2, -2).trim()} }}</${el}>`);
322
+ }
323
+ else {
324
+ const i18nKey = p.key || camelKey(value);
325
+ ctx.templateLines.push(`${indent}<${el}${tw}>${tText(ctx, i18nKey, value)}</${el}>`);
326
+ }
327
+ }
328
+ function renderDivider(node, ctx, indent) {
329
+ ctx.templateLines.push(`${indent}<div${twClasses(node, ctx, 'h-px')} />`);
330
+ }
331
+ function renderButton(node, ctx, indent) {
332
+ const p = getProps(node);
333
+ const text = p.text || '';
334
+ const to = p.to;
335
+ const onClick = p.onClick;
336
+ const action = p.action;
337
+ if (to) {
338
+ ctx.templateLines.push(`${indent}<router-link to="/${to}"${twClasses(node, ctx)}>`);
339
+ ctx.templateLines.push(`${indent} ${tText(ctx, camelKey(text), text)}`);
340
+ ctx.templateLines.push(`${indent}</router-link>`);
341
+ }
342
+ else {
343
+ const clickAttr = onClick ? ` @click="${onClick}"` : action ? ` @click="${action}"` : '';
344
+ ctx.templateLines.push(`${indent}<button${twClasses(node, ctx)}${clickAttr}>`);
345
+ ctx.templateLines.push(`${indent} ${tText(ctx, camelKey(text), text)}`);
346
+ ctx.templateLines.push(`${indent}</button>`);
347
+ }
348
+ }
349
+ function renderInput(node, ctx, indent) {
350
+ const p = getProps(node);
351
+ const attrs = [];
352
+ const tw = twClasses(node, ctx);
353
+ if (p.bind)
354
+ attrs.push(`v-model="${p.bind}"`);
355
+ if (p.placeholder)
356
+ attrs.push(`placeholder="${p.placeholder}"`);
357
+ if (p.type)
358
+ attrs.push(`type="${p.type}"`);
359
+ const attrStr = attrs.length > 0 ? ' ' + attrs.join(' ') : '';
360
+ ctx.templateLines.push(`${indent}<input${tw}${attrStr} />`);
361
+ }
362
+ function renderSlider(node, ctx, indent) {
363
+ const p = getProps(node);
364
+ const min = p.min || 0;
365
+ const max = p.max || 100;
366
+ const step = p.step || 1;
367
+ const bind = p.bind;
368
+ const accent = p.accent || '#007AFF';
369
+ ctx.templateLines.push(`${indent}<input`);
370
+ ctx.templateLines.push(`${indent} type="range"`);
371
+ ctx.templateLines.push(`${indent} :min="${min}"`);
372
+ ctx.templateLines.push(`${indent} :max="${max}"`);
373
+ ctx.templateLines.push(`${indent} :step="${step}"`);
374
+ if (bind)
375
+ ctx.templateLines.push(`${indent} v-model="${bind}"`);
376
+ ctx.templateLines.push(`${indent} class="w-full h-2 bg-zinc-700 rounded-lg appearance-none cursor-pointer accent-[${accent}]"`);
377
+ ctx.templateLines.push(`${indent}/>`);
378
+ }
379
+ function renderToggle(node, ctx, indent) {
380
+ const p = getProps(node);
381
+ const bind = p.bind;
382
+ ctx.templateLines.push(`${indent}<label class="relative inline-flex items-center cursor-pointer">`);
383
+ ctx.templateLines.push(`${indent} <input`);
384
+ ctx.templateLines.push(`${indent} type="checkbox"`);
385
+ ctx.templateLines.push(`${indent} class="sr-only peer"`);
386
+ if (bind)
387
+ ctx.templateLines.push(`${indent} v-model="${bind}"`);
388
+ ctx.templateLines.push(`${indent} />`);
389
+ ctx.templateLines.push(`${indent} <div class="w-11 h-6 bg-zinc-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-orange-600" />`);
390
+ ctx.templateLines.push(`${indent}</label>`);
391
+ }
392
+ function renderGrid(node, ctx, indent) {
393
+ const p = getProps(node);
394
+ const cols = parseInt(String(p.cols || 1), 10) || 1;
395
+ const gap = parseInt(String(p.gap || 16), 10) || 16;
396
+ ctx.templateLines.push(`${indent}<div class="grid grid-cols-1 md:grid-cols-${cols} gap-${Math.round(gap / 4)}">`);
397
+ renderChildren(node, ctx, indent);
398
+ ctx.templateLines.push(`${indent}</div>`);
399
+ }
400
+ function renderConditional(node, ctx, indent) {
401
+ const p = getProps(node);
402
+ const condition = p.if || 'true';
403
+ ctx.templateLines.push(`${indent}<template v-if="${condition}">`);
404
+ renderChildren(node, ctx, indent);
405
+ ctx.templateLines.push(`${indent}</template>`);
406
+ }
407
+ const SVG_ICON_INNER = {
408
+ home: '<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>',
409
+ plus: '<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="16"/><line x1="8" y1="12" x2="16" y2="12"/>',
410
+ chart: '<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>',
411
+ search: '<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>',
412
+ settings: '<circle cx="12" cy="12" r="3"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>',
413
+ heart: '<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>',
414
+ profile: '<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>',
415
+ check: '<polyline points="20 6 9 17 4 12"/>',
416
+ x: '<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>',
417
+ arrow: '<line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/>',
418
+ };
419
+ function renderIcon(node, ctx, indent) {
420
+ const p = getProps(node);
421
+ const name = p.name;
422
+ const size = parseInt(String(p.size || 20), 10) || 20;
423
+ const inner = SVG_ICON_INNER[name] || '<circle cx="12" cy="12" r="4"/>';
424
+ ctx.templateLines.push(`${indent}<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"${twClasses(node, ctx)}>${inner}</svg>`);
425
+ }
426
+ function renderSvgNode(node, ctx, indent) {
427
+ const p = getProps(node);
428
+ const icon = p.icon;
429
+ const size = parseInt(String(p.size || 24), 10) || 24;
430
+ if (icon) {
431
+ const inner = SVG_ICON_INNER[icon] || '<circle cx="12" cy="12" r="4"/>';
432
+ ctx.templateLines.push(`${indent}<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"${twClasses(node, ctx)}>${inner}</svg>`);
433
+ }
434
+ else {
435
+ const viewBox = p.viewBox || '0 0 24 24';
436
+ const width = parseInt(String(p.width || size), 10) || size;
437
+ const height = parseInt(String(p.height || size), 10) || size;
438
+ const fill = p.fill || 'none';
439
+ const stroke = p.stroke || 'currentColor';
440
+ const content = p.content || '';
441
+ ctx.templateLines.push(`${indent}<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="${viewBox}" fill="${fill}" stroke="${stroke}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"${twClasses(node, ctx)}>${content}</svg>`);
442
+ }
443
+ }
444
+ function renderImage(node, ctx, indent) {
445
+ const p = getProps(node);
446
+ const src = p.src || '';
447
+ if (src.startsWith('http')) {
448
+ ctx.templateLines.push(`${indent}<img src="${src}" alt="${src}"${twClasses(node, ctx)} />`);
449
+ }
450
+ else {
451
+ ctx.templateLines.push(`${indent}<img :src="'/${src}.png'" alt="${src}"${twClasses(node, ctx)} />`);
452
+ }
453
+ }
454
+ function renderList(node, ctx, indent) {
455
+ ctx.templateLines.push(`${indent}<div${twClasses(node, ctx, 'space-y-2')}>`);
456
+ renderChildren(node, ctx, indent);
457
+ ctx.templateLines.push(`${indent}</div>`);
458
+ }
459
+ function renderItem(node, ctx, indent) {
460
+ const hasChildren = node.children && node.children.some(c => !NON_VISUAL.has(c.type));
461
+ ctx.templateLines.push(`${indent}<div${twClasses(node, ctx, 'flex items-center justify-between py-3 px-1 border-b border-zinc-800')}>`);
462
+ if (hasChildren) {
463
+ renderChildren(node, ctx, indent);
464
+ }
465
+ ctx.templateLines.push(`${indent}</div>`);
466
+ }
467
+ function renderTabs(node, ctx, indent) {
468
+ const tabs = (node.children || []).filter(c => c.type === 'tab');
469
+ if (tabs.length === 0) {
470
+ ctx.templateLines.push(`${indent}<div${twClasses(node, ctx)}>`);
471
+ renderChildren(node, ctx, indent);
472
+ ctx.templateLines.push(`${indent}</div>`);
473
+ return;
474
+ }
475
+ ctx.vueImports.add('ref');
476
+ const tabIdx = ctx.stateDecls.length;
477
+ const tabVarName = `activeTab_${tabIdx}`;
478
+ const firstTabName = getProps(tabs[0]).name || '0';
479
+ ctx.stateDecls.push({ name: tabVarName, initial: `'${firstTabName}'` });
480
+ ctx.templateLines.push(`${indent}<div${twClasses(node, ctx)}>`);
481
+ ctx.templateLines.push(`${indent} <div class="flex gap-2 mb-4">`);
482
+ for (const tab of tabs) {
483
+ const tp = getProps(tab);
484
+ const tabName = tp.name || tp.label || 'tab';
485
+ const label = tp.label || tp.name || 'Tab';
486
+ ctx.templateLines.push(`${indent} <button @click="${tabVarName} = '${tabName}'" :class="{ 'font-bold': ${tabVarName} === '${tabName}' }" class="px-3 py-1 text-sm rounded">${label}</button>`);
487
+ }
488
+ ctx.templateLines.push(`${indent} </div>`);
489
+ for (const tab of tabs) {
490
+ const tp = getProps(tab);
491
+ const tabName = tp.name || tp.label || 'tab';
492
+ ctx.templateLines.push(`${indent} <div v-if="${tabVarName} === '${tabName}'">`);
493
+ if (tab.children) {
494
+ for (const child of tab.children) {
495
+ renderNode(child, ctx, indent + ' ');
496
+ }
497
+ }
498
+ ctx.templateLines.push(`${indent} </div>`);
499
+ }
500
+ ctx.templateLines.push(`${indent}</div>`);
501
+ }
502
+ function renderTab(node, ctx, indent) {
503
+ // Handled by renderTabs parent — standalone tab renders as button
504
+ const p = getProps(node);
505
+ const label = p.label || '';
506
+ const icon = p.icon;
507
+ ctx.templateLines.push(`${indent}<button${twClasses(node, ctx, 'flex flex-col items-center gap-1 text-xs text-zinc-500')}>`);
508
+ if (icon) {
509
+ const inner = SVG_ICON_INNER[icon] || '<circle cx="12" cy="12" r="4"/>';
510
+ ctx.templateLines.push(`${indent} <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${inner}</svg>`);
511
+ }
512
+ ctx.templateLines.push(`${indent} ${tText(ctx, camelKey(label), label)}`);
513
+ ctx.templateLines.push(`${indent}</button>`);
514
+ }
515
+ function renderProgress(node, ctx, indent) {
516
+ const p = getProps(node);
517
+ const label = p.label || '';
518
+ const current = Number(p.current || 0);
519
+ const target = Number(p.target || 100);
520
+ const color = p.color || '#007AFF';
521
+ const pct = Math.round((current / target) * 100);
522
+ ctx.templateLines.push(`${indent}<div class="mb-4">`);
523
+ ctx.templateLines.push(`${indent} <div class="flex justify-between text-sm mb-1.5">`);
524
+ ctx.templateLines.push(`${indent} <span class="font-semibold text-white">${label}</span>`);
525
+ ctx.templateLines.push(`${indent} <span class="text-zinc-400">${current}/${target} ${p.unit || ''}</span>`);
526
+ ctx.templateLines.push(`${indent} </div>`);
527
+ ctx.templateLines.push(`${indent} <div class="h-1.5 rounded-full overflow-hidden bg-zinc-800">`);
528
+ ctx.templateLines.push(`${indent} <div class="h-full rounded-full transition-all" :style="{ width: '${pct}%', backgroundColor: '${color}' }" />`);
529
+ ctx.templateLines.push(`${indent} </div>`);
530
+ ctx.templateLines.push(`${indent}</div>`);
531
+ }
532
+ // ── Script Setup Generation ──────────────────────────────────────────────
533
+ function generateScriptSetup(ctx) {
534
+ const lines = [];
535
+ // Vue imports
536
+ if (ctx.vueImports.size > 0) {
537
+ lines.push(`import { ${[...ctx.vueImports].sort().join(', ')} } from 'vue';`);
538
+ lines.push('');
539
+ }
540
+ // i18n import — use Vue-native defaults, override React defaults from resolveConfig
541
+ if (ctx.i18nEnabled) {
542
+ const rawHook = ctx.config?.i18n?.hookName;
543
+ const rawImport = ctx.config?.i18n?.importPath;
544
+ // Override React-style defaults from resolveConfig
545
+ const hookName = (rawHook && rawHook !== 'useTranslation') ? rawHook : 'useI18n';
546
+ const importPath = (rawImport && rawImport !== 'react-i18next') ? rawImport : 'vue-i18n';
547
+ lines.push(`import { ${hookName} } from '${importPath}';`);
548
+ lines.push('');
549
+ lines.push(`const { t } = ${hookName}();`);
550
+ lines.push('');
551
+ }
552
+ // State → ref()
553
+ for (const s of ctx.stateDecls) {
554
+ const initial = s.initial;
555
+ if (initial === undefined || initial === 'undefined') {
556
+ lines.push(`const ${s.name} = ref();`);
557
+ }
558
+ else if (initial === 'true' || initial === 'false' || !isNaN(Number(initial))) {
559
+ lines.push(`const ${s.name} = ref(${initial});`);
560
+ }
561
+ else if (initial.startsWith("'") || initial.startsWith('"') || initial.startsWith('[') || initial.startsWith('{')) {
562
+ lines.push(`const ${s.name} = ref(${initial});`);
563
+ }
564
+ else {
565
+ lines.push(`const ${s.name} = ref('${initial}');`);
566
+ }
567
+ }
568
+ if (ctx.stateDecls.length > 0)
569
+ lines.push('');
570
+ // Logic blocks
571
+ for (const block of ctx.logicBlocks) {
572
+ lines.push(block);
573
+ lines.push('');
574
+ }
575
+ // Event handlers
576
+ for (const handler of ctx.eventHandlers) {
577
+ const asyncKw = handler.isAsync ? 'async ' : '';
578
+ const keyGuard = handler.key ? ` if ((e as KeyboardEvent).key !== '${handler.key}') return;\n` : '';
579
+ const needsMounted = handler.event === 'key' || handler.event === 'keydown' ||
580
+ handler.event === 'keyup' || handler.event === 'resize';
581
+ if (needsMounted) {
582
+ const param = handler.paramType || '';
583
+ lines.push(`${asyncKw}function ${handler.fnName}(${param}) {`);
584
+ if (keyGuard)
585
+ lines.push(keyGuard.trimEnd());
586
+ if (handler.code) {
587
+ for (const line of handler.code.split('\n')) {
588
+ lines.push(` ${line}`);
589
+ }
590
+ }
591
+ lines.push('}');
592
+ lines.push('');
593
+ const domEvent = handler.event === 'key' ? 'keydown' : handler.event;
594
+ lines.push(`onMounted(() => {`);
595
+ lines.push(` window.addEventListener('${domEvent}', ${handler.fnName} as EventListener);`);
596
+ lines.push(`});`);
597
+ lines.push('');
598
+ lines.push(`onUnmounted(() => {`);
599
+ lines.push(` window.removeEventListener('${domEvent}', ${handler.fnName} as EventListener);`);
600
+ lines.push(`});`);
601
+ lines.push('');
602
+ }
603
+ else {
604
+ const param = handler.paramType || '';
605
+ lines.push(`${asyncKw}function ${handler.fnName}(${param}) {`);
606
+ if (handler.code) {
607
+ for (const line of handler.code.split('\n')) {
608
+ lines.push(` ${line}`);
609
+ }
610
+ }
611
+ lines.push('}');
612
+ lines.push('');
613
+ }
614
+ }
615
+ return lines.join('\n');
616
+ }
617
+ // ── Main Transpiler ──────────────────────────────────────────────────────
618
+ import { planVueStructure } from './structure-vue.js';
619
+ import { buildVueStructuredArtifacts } from './artifact-utils-vue.js';
620
+ export function transpileTailwindVue(root, config) {
621
+ // Structured output path
622
+ if (config && config.structure !== 'flat') {
623
+ const plan = planVueStructure(root, config);
624
+ if (plan) {
625
+ return _transpileTailwindVueStructured(root, config, plan);
626
+ }
627
+ }
628
+ // Flat output path (default)
629
+ return _transpileTailwindVueFlat(root, config);
630
+ }
631
+ function _buildTailwindVueSFC(root, config) {
632
+ const ctx = createBuilder(config);
633
+ collectThemes(root, ctx);
634
+ renderNode(root, ctx, ' ');
635
+ const scriptSetup = generateScriptSetup(ctx);
636
+ const sfc = [];
637
+ sfc.push('<script setup lang="ts">');
638
+ if (scriptSetup.trim()) {
639
+ sfc.push(scriptSetup.trimEnd());
640
+ }
641
+ sfc.push('</script>');
642
+ sfc.push('');
643
+ sfc.push('<template>');
644
+ sfc.push(...ctx.templateLines);
645
+ sfc.push('</template>');
646
+ return { code: sfc.join('\n') + '\n', sourceMap: ctx.sourceMap };
647
+ }
648
+ function _transpileTailwindVueFlat(root, config) {
649
+ const { code, sourceMap } = _buildTailwindVueSFC(root, config);
650
+ const irText = serializeIR(root);
651
+ const irTokenCount = countTokens(irText);
652
+ const tsTokenCount = countTokens(code);
653
+ const tokenReduction = tsTokenCount > 0 ? Math.round((1 - irTokenCount / tsTokenCount) * 100) : 0;
654
+ return {
655
+ code,
656
+ sourceMap,
657
+ irTokenCount,
658
+ tsTokenCount,
659
+ tokenReduction,
660
+ diagnostics: (() => {
661
+ const accounted = new Map();
662
+ accountNode(accounted, root, 'expressed', undefined, true);
663
+ const CONSUMED = new Set(['state', 'logic', 'on', 'theme', 'handler']);
664
+ for (const child of root.children || []) {
665
+ if (CONSUMED.has(child.type))
666
+ accountNode(accounted, child, 'consumed', child.type + ' pre-pass', true);
667
+ }
668
+ return buildDiagnostics(root, accounted, 'tailwind-vue');
669
+ })(),
670
+ };
671
+ }
672
+ function _transpileTailwindVueStructured(root, config, plan) {
673
+ const { entryCode, artifacts } = buildVueStructuredArtifacts(plan, (file, cfg) => _buildTailwindVueSFC(file.rootNode, cfg).code, root, config);
674
+ const irText = serializeIR(root);
675
+ const irTokenCount = countTokens(irText);
676
+ const tsTokenCount = countTokens(entryCode);
677
+ const tokenReduction = tsTokenCount > 0 ? Math.round((1 - irTokenCount / tsTokenCount) * 100) : 0;
678
+ return {
679
+ code: entryCode,
680
+ sourceMap: [],
681
+ irTokenCount,
682
+ tsTokenCount,
683
+ tokenReduction,
684
+ artifacts,
685
+ diagnostics: (() => {
686
+ const accounted = new Map();
687
+ accountNode(accounted, root, 'expressed', undefined, true);
688
+ const CONSUMED = new Set(['state', 'logic', 'on', 'theme', 'handler']);
689
+ for (const child of root.children || []) {
690
+ if (CONSUMED.has(child.type))
691
+ accountNode(accounted, child, 'consumed', child.type + ' pre-pass', true);
692
+ }
693
+ return buildDiagnostics(root, accounted, 'tailwind-vue');
694
+ })(),
695
+ };
696
+ }
697
+ //# sourceMappingURL=transpiler-tailwind-vue.js.map