@kernlang/react 2.0.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.
@@ -0,0 +1,721 @@
1
+ import { stylesToTailwind, colorToTw, countTokens, serializeIR, camelKey, escapeJsxText, escapeJsxAttr, escapeJsString, buildTailwindProfile, applyTailwindTokenRules } from '@kernlang/core';
2
+ import { planStructure } from './structure.js';
3
+ import { buildStructuredArtifacts } from './artifact-utils.js';
4
+ /** Wrap text with t() for i18n, or emit raw string when i18n is disabled */
5
+ function tText(ctx, key, value) {
6
+ return ctx.i18nEnabled ? `{t('${escapeJsString(key)}', '${escapeJsString(value)}')}` : escapeJsxText(value);
7
+ }
8
+ function getProps(node) {
9
+ return node.props || {};
10
+ }
11
+ function getStyles(node) {
12
+ return getProps(node).styles || {};
13
+ }
14
+ function getThemeRefs(node) {
15
+ return getProps(node).themeRefs || [];
16
+ }
17
+ function getPseudoStyles(node) {
18
+ return getProps(node).pseudoStyles || {};
19
+ }
20
+ function twClasses(node, ctx, extra = '') {
21
+ const styles = getStyles(node);
22
+ const pseudo = getPseudoStyles(node);
23
+ let tw = stylesToTailwind(styles, ctx.colors);
24
+ if (ctx.twProfile)
25
+ tw = applyTailwindTokenRules(tw, ctx.twProfile);
26
+ // Generate pseudo-class Tailwind variants: hover:bg-red-500, active:scale-95
27
+ const pseudoClasses = [];
28
+ for (const [state, stateStyles] of Object.entries(pseudo)) {
29
+ const twState = state === 'press' ? 'active' : state; // :press → active:
30
+ let expanded = stylesToTailwind(stateStyles, ctx.colors);
31
+ if (ctx.twProfile)
32
+ expanded = applyTailwindTokenRules(expanded, ctx.twProfile);
33
+ if (expanded) {
34
+ pseudoClasses.push(expanded.split(' ').map(c => `${twState}:${c}`).join(' '));
35
+ }
36
+ }
37
+ const parts = [tw, ...pseudoClasses, extra].filter(Boolean);
38
+ return parts.length > 0 ? ` className="${parts.join(' ')}"` : '';
39
+ }
40
+ function renderNode(node, ctx, indent) {
41
+ const p = getProps(node);
42
+ const irLine = node.loc?.line || 0;
43
+ ctx.sourceMap.push({ irLine, irCol: node.loc?.col || 1, outLine: ctx.lines.length + 1, outCol: 1 });
44
+ switch (node.type) {
45
+ case 'state':
46
+ // Collect state declarations — rendered as useState in component header
47
+ ctx.stateDecls.push({ name: p.name, initial: p.initial });
48
+ return;
49
+ case 'logic':
50
+ // Collect logic blocks — rendered before return statement
51
+ ctx.logicBlocks.push(p.code);
52
+ return;
53
+ case 'screen':
54
+ renderScreen(node, ctx, indent);
55
+ break;
56
+ case 'section':
57
+ renderSection(node, ctx, indent);
58
+ break;
59
+ case 'card':
60
+ renderCard(node, ctx, indent);
61
+ break;
62
+ case 'row':
63
+ renderRow(node, ctx, indent);
64
+ break;
65
+ case 'col':
66
+ renderCol(node, ctx, indent);
67
+ break;
68
+ case 'text':
69
+ renderText(node, ctx, indent);
70
+ break;
71
+ case 'divider':
72
+ renderDivider(node, ctx, indent);
73
+ break;
74
+ case 'button':
75
+ renderButton(node, ctx, indent);
76
+ break;
77
+ case 'slider':
78
+ renderSlider(node, ctx, indent);
79
+ break;
80
+ case 'toggle':
81
+ renderToggle(node, ctx, indent);
82
+ break;
83
+ case 'grid':
84
+ renderGrid(node, ctx, indent);
85
+ break;
86
+ case 'conditional':
87
+ renderConditional(node, ctx, indent);
88
+ break;
89
+ case 'component':
90
+ renderComponent(node, ctx, indent);
91
+ break;
92
+ case 'icon':
93
+ renderIcon(node, ctx, indent);
94
+ break;
95
+ case 'image':
96
+ renderImage(node, ctx, indent);
97
+ break;
98
+ case 'list':
99
+ renderList(node, ctx, indent);
100
+ break;
101
+ case 'item':
102
+ renderItem(node, ctx, indent);
103
+ break;
104
+ case 'tabs':
105
+ renderTabs(node, ctx, indent);
106
+ break;
107
+ case 'tab':
108
+ renderTab(node, ctx, indent);
109
+ break;
110
+ case 'progress':
111
+ renderProgress(node, ctx, indent);
112
+ break;
113
+ case 'input':
114
+ renderInput(node, ctx, indent);
115
+ break;
116
+ case 'theme':
117
+ break; // theme definitions are meta, not rendered
118
+ default:
119
+ // Generic div fallback
120
+ ctx.lines.push(`${indent}<div${twClasses(node, ctx)}>`);
121
+ renderChildren(node, ctx, indent);
122
+ ctx.lines.push(`${indent}</div>`);
123
+ }
124
+ }
125
+ function renderChildren(node, ctx, indent) {
126
+ if (node.children) {
127
+ for (const child of node.children) {
128
+ renderNode(child, ctx, indent + ' ');
129
+ }
130
+ }
131
+ }
132
+ function renderScreen(node, ctx, indent) {
133
+ const p = getProps(node);
134
+ ctx.lines.push(`${indent}<div${twClasses(node, ctx, 'space-y-8')}>`);
135
+ renderChildren(node, ctx, indent);
136
+ ctx.lines.push(`${indent}</div>`);
137
+ }
138
+ function renderSection(node, ctx, indent) {
139
+ const p = getProps(node);
140
+ const title = p.title || '';
141
+ const key = p.key || camelKey(title);
142
+ const icon = p.icon;
143
+ const tooltip = p.tooltip;
144
+ const description = p.description;
145
+ // Check if this is the root section (has description) → use SettingsSection component
146
+ if (description) {
147
+ ctx.componentImports.add('SettingsSection');
148
+ ctx.lines.push(`${indent}<SettingsSection`);
149
+ if (ctx.i18nEnabled) {
150
+ ctx.lines.push(`${indent} title={t('${escapeJsString(key)}.title', '${escapeJsString(title)}')}`);
151
+ ctx.lines.push(`${indent} description={t('${escapeJsString(key)}.description', '${escapeJsString(description)}')}`);
152
+ }
153
+ else {
154
+ ctx.lines.push(`${indent} title="${escapeJsxAttr(title)}"`);
155
+ ctx.lines.push(`${indent} description="${escapeJsxAttr(description)}"`);
156
+ }
157
+ ctx.lines.push(`${indent}>`);
158
+ ctx.lines.push(`${indent} <div className="space-y-8">`);
159
+ renderChildren(node, ctx, indent + ' ');
160
+ ctx.lines.push(`${indent} </div>`);
161
+ ctx.lines.push(`${indent}</SettingsSection>`);
162
+ return;
163
+ }
164
+ ctx.lines.push(`${indent}<div>`);
165
+ if (icon) {
166
+ ctx.lines.push(`${indent} <div className="flex items-center gap-2 mb-4">`);
167
+ ctx.lines.push(`${indent} <h3 className="text-sm font-medium text-white">`);
168
+ ctx.lines.push(`${indent} ${tText(ctx, `${key}.title`, title)}`);
169
+ ctx.lines.push(`${indent} </h3>`);
170
+ ctx.componentImports.add('Icon');
171
+ ctx.lines.push(`${indent} <div className="relative group">`);
172
+ ctx.lines.push(`${indent} <Icon name="${icon}" size="sm" className="text-zinc-500 hover:text-orange-500 cursor-help transition-colors" />`);
173
+ ctx.lines.push(`${indent} <div className="absolute left-0 bottom-full mb-2 hidden group-hover:block z-50 w-72 p-3 bg-zinc-900 border border-zinc-700 rounded-lg shadow-xl text-xs text-zinc-300">`);
174
+ if (tooltip) {
175
+ ctx.lines.push(`${indent} ${ctx.i18nEnabled ? `{t('${escapeJsString(key)}.tooltip', ${JSON.stringify(tooltip)})}` : escapeJsxText(tooltip)}`);
176
+ }
177
+ ctx.lines.push(`${indent} </div>`);
178
+ ctx.lines.push(`${indent} </div>`);
179
+ ctx.lines.push(`${indent} </div>`);
180
+ }
181
+ else {
182
+ ctx.lines.push(`${indent} <h3 className="text-sm font-medium text-white mb-4">`);
183
+ ctx.lines.push(`${indent} ${tText(ctx, `${key}.title`, title)}`);
184
+ ctx.lines.push(`${indent} </h3>`);
185
+ }
186
+ renderChildren(node, ctx, indent);
187
+ ctx.lines.push(`${indent}</div>`);
188
+ }
189
+ function renderCard(node, ctx, indent) {
190
+ const styles = { ...getStyles(node) };
191
+ const border = styles.border;
192
+ delete styles.border;
193
+ let extra = '';
194
+ if (border) {
195
+ const borderClass = colorToTw('border', border, ctx.colors);
196
+ extra = `border ${borderClass}`;
197
+ }
198
+ // Use a shallow-copied styles object to avoid mutating the live IR node
199
+ if (node.props) {
200
+ const origStyles = node.props.styles;
201
+ node.props.styles = styles;
202
+ ctx.lines.push(`${indent}<div${twClasses(node, ctx, extra)}>`);
203
+ renderChildren(node, ctx, indent);
204
+ ctx.lines.push(`${indent}</div>`);
205
+ node.props.styles = origStyles;
206
+ }
207
+ else {
208
+ ctx.lines.push(`${indent}<div${twClasses(node, ctx, extra)}>`);
209
+ renderChildren(node, ctx, indent);
210
+ ctx.lines.push(`${indent}</div>`);
211
+ }
212
+ }
213
+ function renderRow(node, ctx, indent) {
214
+ ctx.lines.push(`${indent}<div${twClasses(node, ctx, 'flex')}>`);
215
+ renderChildren(node, ctx, indent);
216
+ ctx.lines.push(`${indent}</div>`);
217
+ }
218
+ function renderCol(node, ctx, indent) {
219
+ ctx.lines.push(`${indent}<div${twClasses(node, ctx, 'flex flex-col')}>`);
220
+ renderChildren(node, ctx, indent);
221
+ ctx.lines.push(`${indent}</div>`);
222
+ }
223
+ function isExpr(v) {
224
+ return typeof v === 'object' && v !== null && '__expr' in v;
225
+ }
226
+ function renderText(node, ctx, indent) {
227
+ const p = getProps(node);
228
+ const rawValue = p.value;
229
+ const bind = p.bind;
230
+ const format = p.format;
231
+ const key = p.key;
232
+ const tag = p.tag || 'span';
233
+ const el = tag === 'p' ? 'p' : tag === 'h1' ? 'h1' : tag === 'h2' ? 'h2' : tag === 'h3' ? 'h3' : tag === 'label' ? 'label' : 'span';
234
+ const tw = twClasses(node, ctx);
235
+ if (isExpr(rawValue)) {
236
+ ctx.lines.push(`${indent}<${el}${tw}>{${rawValue.code}}</${el}>`);
237
+ }
238
+ else if (bind) {
239
+ if (format) {
240
+ ctx.lines.push(`${indent}<${el}${tw}>{${bindExpr(bind, format)}}</${el}>`);
241
+ }
242
+ else {
243
+ ctx.lines.push(`${indent}<${el}${tw}>{${bindVar(bind)}}</${el}>`);
244
+ }
245
+ }
246
+ else if (rawValue) {
247
+ const value = rawValue;
248
+ const i18nKey = key || camelKey(value);
249
+ ctx.lines.push(`${indent}<${el}${tw}>${tText(ctx, i18nKey, value)}</${el}>`);
250
+ }
251
+ }
252
+ function renderDivider(node, ctx, indent) {
253
+ ctx.lines.push(`${indent}<div${twClasses(node, ctx, 'h-px')} />`);
254
+ }
255
+ function renderButton(node, ctx, indent) {
256
+ const p = getProps(node);
257
+ const text = p.text || '';
258
+ const onClick = p.onClick;
259
+ const variant = p.variant || 'primary';
260
+ const size = p.size || 'md';
261
+ const iconName = p.icon;
262
+ if (variant !== 'primary') {
263
+ ctx.componentImports.add('Button');
264
+ ctx.lines.push(`${indent}<Button variant="${variant}" size="${size}" onClick={${onClick}}>`);
265
+ if (iconName) {
266
+ ctx.componentImports.add('Icon');
267
+ ctx.lines.push(`${indent} <Icon name="${iconName}" size="sm" className="mr-2" />`);
268
+ }
269
+ ctx.lines.push(`${indent} ${tText(ctx, camelKey(text), text)}`);
270
+ ctx.lines.push(`${indent}</Button>`);
271
+ }
272
+ else {
273
+ ctx.lines.push(`${indent}<button${twClasses(node, ctx)} onClick={${onClick || '() => {}'}}>`);
274
+ ctx.lines.push(`${indent} ${tText(ctx, camelKey(text), text)}`);
275
+ ctx.lines.push(`${indent}</button>`);
276
+ }
277
+ }
278
+ function renderSlider(node, ctx, indent) {
279
+ const p = getProps(node);
280
+ const min = p.min || 0;
281
+ const max = p.max || 100;
282
+ const step = p.step || 1;
283
+ const bind = p.bind;
284
+ const accent = p.accent || '#007AFF';
285
+ const onDoubleClick = p.onDoubleClick;
286
+ const setter = bindSetter(bind);
287
+ const dblClick = onDoubleClick ? ` onDoubleClick={() => ${setter}(${onDoubleClick})}` : '';
288
+ ctx.lines.push(`${indent}<input`);
289
+ ctx.lines.push(`${indent} type="range"`);
290
+ ctx.lines.push(`${indent} min={${min}}`);
291
+ ctx.lines.push(`${indent} max={${max}}`);
292
+ ctx.lines.push(`${indent} step={${step}}`);
293
+ ctx.lines.push(`${indent} value={${bindVar(bind)}}`);
294
+ ctx.lines.push(`${indent} onChange={(e) => ${setter}(parseFloat(e.target.value))}`);
295
+ if (dblClick)
296
+ ctx.lines.push(`${indent} ${dblClick.trim()}`);
297
+ ctx.lines.push(`${indent} className="w-full h-2 bg-zinc-700 rounded-lg appearance-none cursor-pointer accent-[${accent}]"`);
298
+ ctx.lines.push(`${indent}/>`);
299
+ }
300
+ function renderToggle(node, ctx, indent) {
301
+ const p = getProps(node);
302
+ const bind = p.bind;
303
+ const accent = p.accent || '#ea580c';
304
+ const setter = bindSetter(bind);
305
+ ctx.lines.push(`${indent}<label className="relative inline-flex items-center cursor-pointer">`);
306
+ ctx.lines.push(`${indent} <input`);
307
+ ctx.lines.push(`${indent} type="checkbox"`);
308
+ ctx.lines.push(`${indent} className="sr-only peer"`);
309
+ ctx.lines.push(`${indent} checked={${bindVar(bind)}}`);
310
+ ctx.lines.push(`${indent} onChange={(e) => ${setter}(e.target.checked)}`);
311
+ ctx.lines.push(`${indent} />`);
312
+ ctx.lines.push(`${indent} <div className="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" />`);
313
+ ctx.lines.push(`${indent}</label>`);
314
+ }
315
+ function renderGrid(node, ctx, indent) {
316
+ const p = getProps(node);
317
+ const cols = p.cols || 1;
318
+ const gap = p.gap || 16;
319
+ ctx.lines.push(`${indent}<div className="grid grid-cols-1 md:grid-cols-${cols} gap-${Math.round(Number(gap) / 4)}">`);
320
+ renderChildren(node, ctx, indent);
321
+ ctx.lines.push(`${indent}</div>`);
322
+ }
323
+ function renderConditional(node, ctx, indent) {
324
+ const p = getProps(node);
325
+ const condition = p.if || 'true';
326
+ const jsCondition = irConditionToJs(condition);
327
+ ctx.lines.push(`${indent}{${jsCondition} && (`);
328
+ ctx.lines.push(`${indent} <>`);
329
+ renderChildren(node, ctx, indent + ' ');
330
+ ctx.lines.push(`${indent} </>`);
331
+ ctx.lines.push(`${indent})}`);
332
+ }
333
+ function renderComponent(node, ctx, indent) {
334
+ const p = getProps(node);
335
+ const ref = p.ref;
336
+ if (!ref)
337
+ return;
338
+ ctx.componentImports.add(ref);
339
+ const hasExplicitOnChange = 'onChange' in p;
340
+ const attrs = [];
341
+ for (const [k, v] of Object.entries(p)) {
342
+ if (k === 'ref' || k === 'styles' || k === 'pseudoStyles' || k === 'themeRefs')
343
+ continue;
344
+ if (k === 'bind') {
345
+ const varName = bindVar(v);
346
+ attrs.push(`value={${varName}}`);
347
+ // Only add auto-generated onChange if no explicit one exists
348
+ if (!hasExplicitOnChange) {
349
+ attrs.push(`onChange={${bindSetter(v)}}`);
350
+ }
351
+ }
352
+ else if (k === 'onChange') {
353
+ attrs.push(`onChange={${v}}`);
354
+ }
355
+ else if (k === 'props') {
356
+ // Multiple props passed through
357
+ const propNames = v.split(',');
358
+ for (const pn of propNames) {
359
+ attrs.push(`${pn.trim()}={${pn.trim()}}`);
360
+ }
361
+ }
362
+ else if (k === 'disabled') {
363
+ attrs.push(`disabled={${irConditionToJs(v)}}`);
364
+ }
365
+ else if (k === 'default') {
366
+ attrs.push(`defaultValue={${JSON.stringify(v)}}`);
367
+ }
368
+ else {
369
+ attrs.push(`${k}={${JSON.stringify(v)}}`);
370
+ }
371
+ }
372
+ const attrStr = attrs.length > 0 ? ' ' + attrs.join(' ') : '';
373
+ ctx.lines.push(`${indent}<${ref}${attrStr} />`);
374
+ }
375
+ function renderIcon(node, ctx, indent) {
376
+ const p = getProps(node);
377
+ const name = p.name;
378
+ ctx.componentImports.add('Icon');
379
+ ctx.lines.push(`${indent}<Icon name="${name}" size="sm"${twClasses(node, ctx)} />`);
380
+ }
381
+ function renderImage(node, ctx, indent) {
382
+ const p = getProps(node);
383
+ ctx.lines.push(`${indent}<img src="/${p.src}.png" alt="${p.src}"${twClasses(node, ctx)} />`);
384
+ }
385
+ function renderList(node, ctx, indent) {
386
+ ctx.lines.push(`${indent}<div${twClasses(node, ctx, 'space-y-2')}>`);
387
+ renderChildren(node, ctx, indent);
388
+ ctx.lines.push(`${indent}</div>`);
389
+ }
390
+ function renderItem(node, ctx, indent) {
391
+ const p = getProps(node);
392
+ const tw = twClasses(node, ctx, 'flex items-center justify-between p-3 border-b border-zinc-800');
393
+ const hasChildren = node.children && node.children.length > 0;
394
+ if (hasChildren) {
395
+ ctx.lines.push(`${indent}<div${tw}>`);
396
+ renderChildren(node, ctx, indent);
397
+ ctx.lines.push(`${indent}</div>`);
398
+ }
399
+ else {
400
+ // Render item props as content
401
+ const name = p.name;
402
+ const time = p.time;
403
+ const calories = p.calories;
404
+ const category = p.category;
405
+ ctx.lines.push(`${indent}<div${tw}>`);
406
+ ctx.lines.push(`${indent} <div>`);
407
+ if (name)
408
+ ctx.lines.push(`${indent} <span className="text-sm text-white font-medium">${escapeJsxText(name)}</span>`);
409
+ if (time)
410
+ ctx.lines.push(`${indent} <span className="text-xs text-zinc-500 ml-2">${escapeJsxText(time)}</span>`);
411
+ if (category)
412
+ ctx.lines.push(`${indent} <span className="text-xs text-zinc-500 ml-2">${escapeJsxText(category)}</span>`);
413
+ ctx.lines.push(`${indent} </div>`);
414
+ if (calories)
415
+ ctx.lines.push(`${indent} <span className="text-sm text-zinc-400">${escapeJsxText(calories)} kcal</span>`);
416
+ ctx.lines.push(`${indent}</div>`);
417
+ }
418
+ }
419
+ function renderTabs(node, ctx, indent) {
420
+ ctx.lines.push(`${indent}<nav${twClasses(node, ctx, 'flex')}>`);
421
+ renderChildren(node, ctx, indent);
422
+ ctx.lines.push(`${indent}</nav>`);
423
+ }
424
+ function renderTab(node, ctx, indent) {
425
+ const p = getProps(node);
426
+ ctx.lines.push(`${indent}<button${twClasses(node, ctx)}>${tText(ctx, camelKey(p.label), p.label)}</button>`);
427
+ }
428
+ function renderProgress(node, ctx, indent) {
429
+ const p = getProps(node);
430
+ const label = p.label || '';
431
+ const current = Number(p.current || 0);
432
+ const target = Number(p.target || 100);
433
+ const color = p.color || '#007AFF';
434
+ const pct = Math.round((current / target) * 100);
435
+ ctx.lines.push(`${indent}<div className="mb-3">`);
436
+ ctx.lines.push(`${indent} <div className="flex justify-between text-sm mb-1">`);
437
+ ctx.lines.push(`${indent} <span className="text-zinc-300">${escapeJsxText(String(label))}</span>`);
438
+ ctx.lines.push(`${indent} <span className="text-zinc-400">${current}/${target} ${escapeJsxText(String(p.unit || ''))}</span>`);
439
+ ctx.lines.push(`${indent} </div>`);
440
+ ctx.lines.push(`${indent} <div className="h-2 bg-zinc-700 rounded-full overflow-hidden">`);
441
+ ctx.lines.push(`${indent} <div className="h-full rounded-full bg-[${color}]" style={{ width: '${pct}%' }} />`);
442
+ ctx.lines.push(`${indent} </div>`);
443
+ ctx.lines.push(`${indent}</div>`);
444
+ }
445
+ function renderInput(node, ctx, indent) {
446
+ const p = getProps(node);
447
+ const attrs = [];
448
+ const tw = twClasses(node, ctx);
449
+ if (p.bind) {
450
+ const bind = p.bind;
451
+ const setter = bindSetter(bind);
452
+ attrs.push(`value={${bindVar(bind)}}`);
453
+ // Check if onChange is an expression
454
+ if (isExpr(p.onChange)) {
455
+ attrs.push(`onChange={${p.onChange.code}}`);
456
+ }
457
+ else if (p.onChange) {
458
+ attrs.push(`onChange={${p.onChange}}`);
459
+ }
460
+ else {
461
+ attrs.push(`onChange={(e) => ${setter}(e.target.value)}`);
462
+ }
463
+ }
464
+ if (p.placeholder)
465
+ attrs.push(`placeholder="${p.placeholder}"`);
466
+ if (p.type)
467
+ attrs.push(`type="${p.type}"`);
468
+ const attrStr = attrs.length > 0 ? ' ' + attrs.join(' ') : '';
469
+ ctx.lines.push(`${indent}<input${tw}${attrStr} />`);
470
+ }
471
+ // ── Helpers ─────────────────────────────────────────────────────────────
472
+ function bindExpr(bind, format) {
473
+ // "{v} dB ({presetLabel})" → `${fixStrengthDb.toFixed(1)} dB (${presetLabel})`
474
+ const expr = format
475
+ .replace(/\{v\}/g, `\${${bind}.toFixed(1)}`)
476
+ .replace(/\{(\w+)\}/g, (_, name) => `\${${name}}`);
477
+ return '`' + expr + '`';
478
+ }
479
+ function bindVar(bind) {
480
+ // "fixStrengthDb" → fixStrengthDb
481
+ // "settings.audioOutputDeviceId" → settings.audioOutputDeviceId
482
+ return bind;
483
+ }
484
+ function bindSetter(bind) {
485
+ // "fixStrengthDb" → setFixStrengthDb
486
+ // "normalizeReference" → setNormalizeReference
487
+ if (bind.includes('.')) {
488
+ const parts = bind.split('.');
489
+ return `set${parts[parts.length - 1].charAt(0).toUpperCase() + parts[parts.length - 1].slice(1)}`;
490
+ }
491
+ return `set${bind.charAt(0).toUpperCase() + bind.slice(1)}`;
492
+ }
493
+ function irConditionToJs(cond) {
494
+ // Handle expression objects from {{ }}
495
+ if (typeof cond === 'object' && cond !== null && '__expr' in cond) {
496
+ return cond.code;
497
+ }
498
+ // "isPro" → isPro
499
+ // "!isPro" → !isPro
500
+ // "isPro&perStemMode=manual" → isPro && perStemMode === 'manual'
501
+ return String(cond)
502
+ .replace(/&/g, ' && ')
503
+ .replace(/([a-zA-Z_]+)=([a-zA-Z_]+)/g, "$1 === '$2'");
504
+ }
505
+ // ── Main export ─────────────────────────────────────────────────────────
506
+ export function transpileTailwind(root, config) {
507
+ // Structured output path
508
+ if (config && config.structure !== 'flat') {
509
+ const plan = planStructure(root, config);
510
+ if (plan) {
511
+ return _transpileTailwindStructured(root, config, plan);
512
+ }
513
+ }
514
+ // Flat output path (default — unchanged)
515
+ return _transpileTailwindInner(root, config);
516
+ }
517
+ function _transpileTailwindInner(root, config) {
518
+ const i18nEnabled = config?.i18n?.enabled ?? true;
519
+ const ctx = {
520
+ lines: [],
521
+ sourceMap: [],
522
+ imports: new Set(),
523
+ hooks: new Map(),
524
+ componentImports: new Set(),
525
+ storeHooks: new Set(),
526
+ stateDecls: [],
527
+ logicBlocks: [],
528
+ i18nEnabled,
529
+ colors: config?.colors,
530
+ twProfile: config?.frameworkVersions ? buildTailwindProfile(config.frameworkVersions) : undefined,
531
+ };
532
+ // Render JSX tree (state/logic nodes are collected, not rendered)
533
+ renderNode(root, ctx, ' ');
534
+ const name = root.props?.name || 'Component';
535
+ const hasState = ctx.stateDecls.length > 0;
536
+ const hasLogic = ctx.logicBlocks.length > 0;
537
+ // Build output
538
+ const code = [];
539
+ code.push(`'use client';`);
540
+ code.push('');
541
+ // React imports
542
+ const reactImports = [];
543
+ if (hasState)
544
+ reactImports.push('useState');
545
+ if (hasLogic && ctx.logicBlocks.some(b => b.includes('useEffect')))
546
+ reactImports.push('useEffect');
547
+ if (hasLogic && ctx.logicBlocks.some(b => b.includes('useCallback')))
548
+ reactImports.push('useCallback');
549
+ if (hasLogic && ctx.logicBlocks.some(b => b.includes('useMemo')))
550
+ reactImports.push('useMemo');
551
+ if (hasLogic && ctx.logicBlocks.some(b => b.includes('useRef')))
552
+ reactImports.push('useRef');
553
+ if (reactImports.length > 0) {
554
+ code.push(`import React, { ${reactImports.join(', ')} } from 'react';`);
555
+ }
556
+ const i18nHook = config?.i18n?.hookName ?? 'useTranslation';
557
+ const i18nImport = config?.i18n?.importPath ?? 'react-i18next';
558
+ const uiLibrary = config?.components?.uiLibrary ?? '@components/ui';
559
+ if (i18nEnabled) {
560
+ code.push(`import { ${i18nHook} } from '${i18nImport}';`);
561
+ }
562
+ if (ctx.componentImports.size > 0) {
563
+ const uiImports = [...ctx.componentImports].filter(c => ['Icon', 'Button'].includes(c));
564
+ const featureImports = [...ctx.componentImports].filter(c => !['Icon', 'Button'].includes(c));
565
+ if (uiImports.length > 0) {
566
+ code.push(`import { ${uiImports.join(', ')} } from '${uiLibrary}';`);
567
+ }
568
+ for (const imp of featureImports) {
569
+ code.push(`import { ${imp} } from './${imp}';`);
570
+ }
571
+ }
572
+ code.push('');
573
+ code.push(`export function ${name}() {`);
574
+ if (i18nEnabled) {
575
+ code.push(` const { t } = ${i18nHook}();`);
576
+ }
577
+ // Generate useState declarations
578
+ for (const s of ctx.stateDecls) {
579
+ const setter = `set${s.name.charAt(0).toUpperCase() + s.name.slice(1)}`;
580
+ const init = s.initial === 'true' ? 'true' : s.initial === 'false' ? 'false' : isNaN(Number(s.initial)) ? `'${s.initial}'` : s.initial;
581
+ // Check if initial is an expression
582
+ const initProp = (root.children?.find(c => c.type === 'state' && c.props?.name === s.name)?.props?.initial);
583
+ const isExpr = typeof initProp === 'object' && initProp !== null && '__expr' in initProp;
584
+ const initVal = isExpr ? initProp.code : init;
585
+ code.push(` const [${s.name}, ${setter}] = useState(${initVal});`);
586
+ }
587
+ // Generate logic blocks
588
+ for (const block of ctx.logicBlocks) {
589
+ code.push('');
590
+ for (const line of block.split('\n')) {
591
+ code.push(` ${line}`);
592
+ }
593
+ }
594
+ code.push('');
595
+ code.push(' return (');
596
+ code.push(...ctx.lines);
597
+ code.push(' );');
598
+ code.push('}');
599
+ const output = code.join('\n');
600
+ const irText = serializeIR(root);
601
+ const irTokenCount = countTokens(irText);
602
+ const tsTokenCount = countTokens(output);
603
+ const tokenReduction = tsTokenCount > 0 ? Math.round((1 - irTokenCount / tsTokenCount) * 100) : 0;
604
+ return {
605
+ code: output,
606
+ sourceMap: ctx.sourceMap,
607
+ irTokenCount,
608
+ tsTokenCount,
609
+ tokenReduction,
610
+ };
611
+ }
612
+ // ── Structured output ────────────────────────────────────────────────────
613
+ function _renderTailwindFile(file, config) {
614
+ const i18nEnabled = config.i18n?.enabled ?? true;
615
+ const ctx = {
616
+ lines: [],
617
+ sourceMap: [],
618
+ imports: new Set(),
619
+ hooks: new Map(),
620
+ componentImports: new Set(),
621
+ storeHooks: new Set(),
622
+ stateDecls: [],
623
+ logicBlocks: [],
624
+ i18nEnabled,
625
+ colors: config.colors,
626
+ twProfile: config.frameworkVersions ? buildTailwindProfile(config.frameworkVersions) : undefined,
627
+ };
628
+ const rootNode = file.rootNode;
629
+ renderNode(rootNode, ctx, ' ');
630
+ const name = file.componentName
631
+ || rootNode.props?.name
632
+ || 'Component';
633
+ const hasState = ctx.stateDecls.length > 0;
634
+ const hasLogic = ctx.logicBlocks.length > 0;
635
+ const code = [];
636
+ // For non-entry files with state/logic, add 'use client'
637
+ const needsClient = hasState || hasLogic;
638
+ if (needsClient) {
639
+ code.push(`'use client';`);
640
+ code.push('');
641
+ }
642
+ // React imports
643
+ const reactImports = [];
644
+ if (hasState)
645
+ reactImports.push('useState');
646
+ if (hasLogic && ctx.logicBlocks.some(b => b.includes('useEffect')))
647
+ reactImports.push('useEffect');
648
+ if (hasLogic && ctx.logicBlocks.some(b => b.includes('useCallback')))
649
+ reactImports.push('useCallback');
650
+ if (hasLogic && ctx.logicBlocks.some(b => b.includes('useMemo')))
651
+ reactImports.push('useMemo');
652
+ if (hasLogic && ctx.logicBlocks.some(b => b.includes('useRef')))
653
+ reactImports.push('useRef');
654
+ if (reactImports.length > 0) {
655
+ code.push(`import React, { ${reactImports.join(', ')} } from 'react';`);
656
+ }
657
+ const i18nHook = config.i18n?.hookName ?? 'useTranslation';
658
+ const i18nImport = config.i18n?.importPath ?? 'react-i18next';
659
+ const uiLibrary = config.components?.uiLibrary ?? '@components/ui';
660
+ if (i18nEnabled) {
661
+ code.push(`import { ${i18nHook} } from '${i18nImport}';`);
662
+ }
663
+ if (ctx.componentImports.size > 0) {
664
+ const uiImports = [...ctx.componentImports].filter(c => ['Icon', 'Button'].includes(c));
665
+ const featureImports = [...ctx.componentImports].filter(c => !['Icon', 'Button'].includes(c));
666
+ if (uiImports.length > 0) {
667
+ code.push(`import { ${uiImports.join(', ')} } from '${uiLibrary}';`);
668
+ }
669
+ for (const imp of featureImports) {
670
+ code.push(`import { ${imp} } from './${imp}';`);
671
+ }
672
+ }
673
+ code.push('');
674
+ if (file.isEntry) {
675
+ code.push(`export function ${name}() {`);
676
+ }
677
+ else {
678
+ code.push(`export function ${name}() {`);
679
+ }
680
+ if (i18nEnabled) {
681
+ code.push(` const { t } = ${i18nHook}();`);
682
+ }
683
+ // State declarations (only if not extracted to hooks)
684
+ for (const s of ctx.stateDecls) {
685
+ const setter = `set${s.name.charAt(0).toUpperCase() + s.name.slice(1)}`;
686
+ const init = s.initial === 'true' ? 'true' : s.initial === 'false' ? 'false' : isNaN(Number(s.initial)) ? `'${s.initial}'` : s.initial;
687
+ // Check if initial is an expression
688
+ const initProp = (rootNode.children?.find(c => c.type === 'state' && c.props?.name === s.name)?.props?.initial);
689
+ const isExprInit = typeof initProp === 'object' && initProp !== null && '__expr' in initProp;
690
+ const initVal = isExprInit ? initProp.code : init;
691
+ code.push(` const [${s.name}, ${setter}] = useState(${initVal});`);
692
+ }
693
+ for (const block of ctx.logicBlocks) {
694
+ code.push('');
695
+ for (const line of block.split('\n')) {
696
+ code.push(` ${line}`);
697
+ }
698
+ }
699
+ code.push('');
700
+ code.push(' return (');
701
+ code.push(...ctx.lines);
702
+ code.push(' );');
703
+ code.push('}');
704
+ return code.join('\n');
705
+ }
706
+ function _transpileTailwindStructured(root, config, plan) {
707
+ const { entryCode, artifacts } = buildStructuredArtifacts(plan, (file, cfg) => _renderTailwindFile(file, cfg), root, config);
708
+ const irText = serializeIR(root);
709
+ const irTokenCount = countTokens(irText);
710
+ const tsTokenCount = countTokens(entryCode);
711
+ const tokenReduction = tsTokenCount > 0 ? Math.round((1 - irTokenCount / tsTokenCount) * 100) : 0;
712
+ return {
713
+ code: entryCode,
714
+ sourceMap: [],
715
+ irTokenCount,
716
+ tsTokenCount,
717
+ tokenReduction,
718
+ artifacts,
719
+ };
720
+ }
721
+ //# sourceMappingURL=transpiler-tailwind.js.map