prev-cli 0.24.18 → 0.24.19

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.
Files changed (36) hide show
  1. package/package.json +5 -2
  2. package/src/jsx/adapters/html.test.ts +191 -0
  3. package/src/jsx/adapters/html.ts +404 -0
  4. package/src/jsx/adapters/react.test.ts +172 -0
  5. package/src/jsx/adapters/react.tsx +346 -0
  6. package/src/jsx/define-component.ts +129 -0
  7. package/src/jsx/index.ts +47 -0
  8. package/src/jsx/jsx-runtime.test.ts +63 -0
  9. package/src/jsx/jsx-runtime.ts +117 -0
  10. package/src/jsx/migrate.test.ts +72 -0
  11. package/src/jsx/migrate.ts +451 -0
  12. package/src/jsx/schemas/index.ts +4 -0
  13. package/src/jsx/schemas/primitives.ts +107 -0
  14. package/src/jsx/schemas/tokens.ts +60 -0
  15. package/src/jsx/validation.ts +77 -0
  16. package/src/jsx/vnode.ts +159 -0
  17. package/src/primitives/index.ts +8 -0
  18. package/src/primitives/migrate.test.ts +317 -0
  19. package/src/primitives/migrate.ts +364 -0
  20. package/src/primitives/parser.test.ts +265 -0
  21. package/src/primitives/parser.ts +442 -0
  22. package/src/primitives/template-parser.test.ts +297 -0
  23. package/src/primitives/template-parser.ts +374 -0
  24. package/src/primitives/template-renderer.test.ts +359 -0
  25. package/src/primitives/template-renderer.ts +497 -0
  26. package/src/primitives/tokens.css +82 -0
  27. package/src/primitives/types.ts +248 -0
  28. package/src/tokens/defaults.test.ts +137 -0
  29. package/src/tokens/defaults.ts +77 -0
  30. package/src/tokens/defaults.yaml +76 -0
  31. package/src/tokens/resolver.test.ts +229 -0
  32. package/src/tokens/resolver.ts +173 -0
  33. package/src/tokens/utils.test.ts +172 -0
  34. package/src/tokens/utils.ts +104 -0
  35. package/src/tokens/validation.test.ts +118 -0
  36. package/src/tokens/validation.ts +226 -0
@@ -0,0 +1,497 @@
1
+ // src/primitives/template-renderer.ts
2
+ // Renders templates to HTML using design tokens
3
+
4
+ import {
5
+ isPrimitive,
6
+ isRef,
7
+ isContainerNode,
8
+ type TemplateNode,
9
+ type Template,
10
+ type Slots,
11
+ type SpacingToken,
12
+ type AlignToken,
13
+ type BackgroundToken,
14
+ type RadiusToken,
15
+ type ColorToken,
16
+ type SizeToken,
17
+ type WeightToken,
18
+ type FitToken,
19
+ } from './types'
20
+ import { parsePrimitive } from './parser'
21
+
22
+ /**
23
+ * Design token to Tailwind class mappings
24
+ * Using shadcn/ui-compatible naming conventions
25
+ */
26
+
27
+ // Spacing token to Tailwind scale
28
+ const SPACING_GAP: Record<SpacingToken, string> = {
29
+ none: 'gap-0',
30
+ xs: 'gap-1',
31
+ sm: 'gap-2',
32
+ md: 'gap-4',
33
+ lg: 'gap-6',
34
+ xl: 'gap-8',
35
+ '2xl': 'gap-12',
36
+ }
37
+
38
+ const SPACING_PADDING: Record<SpacingToken, string> = {
39
+ none: 'p-0',
40
+ xs: 'p-1',
41
+ sm: 'p-2',
42
+ md: 'p-4',
43
+ lg: 'p-6',
44
+ xl: 'p-8',
45
+ '2xl': 'p-12',
46
+ }
47
+
48
+ // For spacer fixed sizes (width/height)
49
+ const SPACING_SIZE: Record<SpacingToken, string> = {
50
+ none: 'w-0 h-0',
51
+ xs: 'w-1 h-1',
52
+ sm: 'w-2 h-2',
53
+ md: 'w-4 h-4',
54
+ lg: 'w-6 h-6',
55
+ xl: 'w-8 h-8',
56
+ '2xl': 'w-12 h-12',
57
+ }
58
+
59
+ const ALIGN_ITEMS: Record<AlignToken, string> = {
60
+ start: 'items-start',
61
+ center: 'items-center',
62
+ end: 'items-end',
63
+ stretch: 'items-stretch',
64
+ between: 'justify-between',
65
+ }
66
+
67
+ // Background tokens - shadcn-compatible
68
+ const BG_CLASS: Record<BackgroundToken, string> = {
69
+ transparent: 'bg-transparent',
70
+ background: 'bg-background',
71
+ card: 'bg-card',
72
+ primary: 'bg-primary',
73
+ secondary: 'bg-secondary',
74
+ muted: 'bg-muted',
75
+ accent: 'bg-accent',
76
+ destructive: 'bg-destructive',
77
+ input: 'bg-input',
78
+ }
79
+
80
+ const RADIUS_CLASS: Record<RadiusToken, string> = {
81
+ none: 'rounded-none',
82
+ sm: 'rounded-sm',
83
+ md: 'rounded-md',
84
+ lg: 'rounded-lg',
85
+ xl: 'rounded-xl',
86
+ full: 'rounded-full',
87
+ }
88
+
89
+ // Text color tokens - shadcn-compatible
90
+ const COLOR_CLASS: Record<ColorToken, string> = {
91
+ foreground: 'text-foreground',
92
+ 'card-foreground': 'text-card-foreground',
93
+ primary: 'text-primary',
94
+ 'primary-foreground': 'text-primary-foreground',
95
+ secondary: 'text-secondary',
96
+ 'secondary-foreground': 'text-secondary-foreground',
97
+ muted: 'text-muted',
98
+ 'muted-foreground': 'text-muted-foreground',
99
+ accent: 'text-accent',
100
+ 'accent-foreground': 'text-accent-foreground',
101
+ destructive: 'text-destructive',
102
+ 'destructive-foreground': 'text-destructive-foreground',
103
+ border: 'text-border',
104
+ ring: 'text-ring',
105
+ }
106
+
107
+ const SIZE_CLASS: Record<SizeToken, string> = {
108
+ xs: 'text-xs',
109
+ sm: 'text-sm',
110
+ base: 'text-base',
111
+ lg: 'text-lg',
112
+ xl: 'text-xl',
113
+ '2xl': 'text-2xl',
114
+ }
115
+
116
+ // Icon sizes use width/height
117
+ const ICON_SIZE_CLASS: Record<SizeToken, string> = {
118
+ xs: 'w-3 h-3',
119
+ sm: 'w-4 h-4',
120
+ base: 'w-5 h-5',
121
+ lg: 'w-6 h-6',
122
+ xl: 'w-7 h-7',
123
+ '2xl': 'w-8 h-8',
124
+ }
125
+
126
+ const WEIGHT_CLASS: Record<WeightToken, string> = {
127
+ normal: 'font-normal',
128
+ medium: 'font-medium',
129
+ semibold: 'font-semibold',
130
+ bold: 'font-bold',
131
+ }
132
+
133
+ const FIT_CLASS: Record<FitToken, string> = {
134
+ cover: 'object-cover',
135
+ contain: 'object-contain',
136
+ fill: 'object-fill',
137
+ }
138
+
139
+ export interface RenderContext {
140
+ /** Current state for slot resolution */
141
+ state?: string
142
+ /** Slots mapping for state-dependent content */
143
+ slots?: Slots
144
+ /** Props for component rendering */
145
+ props?: Record<string, unknown>
146
+ /** Callback to render component refs */
147
+ renderRef?: (ref: string) => string
148
+ }
149
+
150
+ /**
151
+ * Render a template to HTML
152
+ */
153
+ export function renderTemplate(template: Template, context: RenderContext = {}): string {
154
+ return renderNode(template.root, 'root', context)
155
+ }
156
+
157
+ /**
158
+ * Render a single template node to HTML
159
+ */
160
+ function renderNode(node: TemplateNode, nodeId: string, context: RenderContext): string {
161
+ // String node: primitive or ref
162
+ if (typeof node === 'string') {
163
+ return renderLeaf(node, nodeId, context)
164
+ }
165
+
166
+ // Container node: has type and possibly children
167
+ if (isContainerNode(node)) {
168
+ return renderContainer(node, nodeId, context)
169
+ }
170
+
171
+ return `<!-- unknown node type -->`
172
+ }
173
+
174
+ /**
175
+ * Render a leaf node (primitive string or component ref)
176
+ */
177
+ function renderLeaf(value: string, nodeId: string, context: RenderContext): string {
178
+ if (isPrimitive(value)) {
179
+ return renderPrimitive(value, nodeId, context)
180
+ }
181
+
182
+ if (isRef(value)) {
183
+ // Delegate to ref renderer if provided
184
+ if (context.renderRef) {
185
+ return context.renderRef(value)
186
+ }
187
+ // Default: placeholder
188
+ return `<div data-ref="${value}" data-node-id="${nodeId}"><!-- ${value} --></div>`
189
+ }
190
+
191
+ // Treat as literal text
192
+ return escapeHtml(value)
193
+ }
194
+
195
+ /**
196
+ * Render a container node with children
197
+ */
198
+ function renderContainer(
199
+ node: { type: string; children?: Record<string, TemplateNode> },
200
+ nodeId: string,
201
+ context: RenderContext
202
+ ): string {
203
+ const parseResult = parsePrimitive(node.type)
204
+ if (!parseResult.success) {
205
+ return `<!-- invalid primitive: ${node.type} -->`
206
+ }
207
+
208
+ const primitive = parseResult.primitive
209
+ let childrenHtml = ''
210
+
211
+ if (node.children) {
212
+ childrenHtml = Object.entries(node.children)
213
+ .map(([childId, childNode]) => renderNode(childNode, childId, context))
214
+ .join('\n')
215
+ }
216
+
217
+ switch (primitive.type) {
218
+ case '$col':
219
+ return renderCol(primitive, nodeId, childrenHtml)
220
+ case '$row':
221
+ return renderRow(primitive, nodeId, childrenHtml)
222
+ case '$box':
223
+ return renderBox(primitive, nodeId, childrenHtml)
224
+ default:
225
+ return `<div data-primitive="${primitive.type}" data-node-id="${nodeId}">${childrenHtml}</div>`
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Render a primitive string (without children)
231
+ */
232
+ function renderPrimitive(value: string, nodeId: string, context: RenderContext): string {
233
+ const parseResult = parsePrimitive(value)
234
+ if (!parseResult.success) {
235
+ return `<!-- invalid primitive: ${value} -->`
236
+ }
237
+
238
+ const primitive = parseResult.primitive
239
+
240
+ switch (primitive.type) {
241
+ case '$col':
242
+ return renderCol(primitive, nodeId, '')
243
+ case '$row':
244
+ return renderRow(primitive, nodeId, '')
245
+ case '$box':
246
+ return renderBox(primitive, nodeId, '')
247
+ case '$spacer':
248
+ return renderSpacer(primitive, nodeId)
249
+ case '$slot':
250
+ return renderSlot(primitive, nodeId, context)
251
+ case '$text':
252
+ return renderText(primitive, nodeId, context)
253
+ case '$icon':
254
+ return renderIcon(primitive, nodeId, context)
255
+ case '$image':
256
+ return renderImage(primitive, nodeId, context)
257
+ default:
258
+ // Exhaustive check - should never reach here
259
+ return `<!-- unknown primitive: ${value} -->`
260
+ }
261
+ }
262
+
263
+ // Primitive renderers
264
+
265
+ function renderCol(
266
+ primitive: { gap?: SpacingToken; align?: AlignToken; padding?: SpacingToken },
267
+ nodeId: string,
268
+ children: string
269
+ ): string {
270
+ const classes: string[] = ['flex', 'flex-col']
271
+ if (primitive.gap) classes.push(SPACING_GAP[primitive.gap])
272
+ if (primitive.align) classes.push(ALIGN_ITEMS[primitive.align])
273
+ if (primitive.padding) classes.push(SPACING_PADDING[primitive.padding])
274
+
275
+ return `<div data-primitive="$col" data-node-id="${nodeId}" class="${classes.join(' ')}">${children}</div>`
276
+ }
277
+
278
+ function renderRow(
279
+ primitive: { gap?: SpacingToken; align?: AlignToken; padding?: SpacingToken },
280
+ nodeId: string,
281
+ children: string
282
+ ): string {
283
+ const classes: string[] = ['flex', 'flex-row']
284
+ if (primitive.gap) classes.push(SPACING_GAP[primitive.gap])
285
+ // $row defaults to items-center per spec
286
+ if (primitive.align) {
287
+ classes.push(ALIGN_ITEMS[primitive.align])
288
+ } else {
289
+ classes.push('items-center')
290
+ }
291
+ if (primitive.padding) classes.push(SPACING_PADDING[primitive.padding])
292
+
293
+ return `<div data-primitive="$row" data-node-id="${nodeId}" class="${classes.join(' ')}">${children}</div>`
294
+ }
295
+
296
+ function renderBox(
297
+ primitive: { padding?: SpacingToken; bg?: BackgroundToken; radius?: RadiusToken },
298
+ nodeId: string,
299
+ children: string
300
+ ): string {
301
+ const classes: string[] = []
302
+ if (primitive.padding) classes.push(SPACING_PADDING[primitive.padding])
303
+ if (primitive.bg) classes.push(BG_CLASS[primitive.bg])
304
+ if (primitive.radius) classes.push(RADIUS_CLASS[primitive.radius])
305
+
306
+ return `<div data-primitive="$box" data-node-id="${nodeId}" class="${classes.join(' ')}">${children}</div>`
307
+ }
308
+
309
+ function renderSpacer(
310
+ primitive: { size?: SpacingToken },
311
+ nodeId: string
312
+ ): string {
313
+ if (primitive.size) {
314
+ // Fixed size spacer
315
+ return `<div data-primitive="$spacer" data-node-id="${nodeId}" class="${SPACING_SIZE[primitive.size]} shrink-0"></div>`
316
+ }
317
+ // Flex spacer
318
+ return `<div data-primitive="$spacer" data-node-id="${nodeId}" class="flex-1"></div>`
319
+ }
320
+
321
+ function renderSlot(
322
+ primitive: { name: string },
323
+ nodeId: string,
324
+ context: RenderContext
325
+ ): string {
326
+ const slotName = primitive.name
327
+ const state = context.state || 'default'
328
+
329
+ // Look up slot content
330
+ if (context.slots && context.slots[slotName]) {
331
+ const stateMapping = context.slots[slotName]
332
+ const content = stateMapping[state] || stateMapping.default
333
+
334
+ if (content) {
335
+ // Render the content (ref or primitive)
336
+ return renderLeaf(content, `${nodeId}-content`, context)
337
+ }
338
+ }
339
+
340
+ // No slot content found
341
+ return `<div data-primitive="$slot" data-slot-name="${slotName}" data-node-id="${nodeId}"><!-- slot: ${slotName} --></div>`
342
+ }
343
+
344
+ function renderText(
345
+ primitive: { content: string; size?: SizeToken; weight?: WeightToken; color?: ColorToken },
346
+ nodeId: string,
347
+ context: RenderContext
348
+ ): string {
349
+ const classes: string[] = []
350
+ if (primitive.size) classes.push(SIZE_CLASS[primitive.size])
351
+ if (primitive.weight) classes.push(WEIGHT_CLASS[primitive.weight])
352
+ if (primitive.color) classes.push(COLOR_CLASS[primitive.color])
353
+
354
+ // Resolve content (prop reference or literal)
355
+ let content = primitive.content
356
+ if (content.startsWith('"') || content.startsWith("'")) {
357
+ // Quoted literal - strip quotes
358
+ content = content.slice(1, -1)
359
+ } else if (context.props && content in context.props) {
360
+ // Prop reference
361
+ content = String(context.props[content] ?? '')
362
+ }
363
+
364
+ return `<span data-primitive="$text" data-node-id="${nodeId}" class="${classes.join(' ')}">${escapeHtml(content)}</span>`
365
+ }
366
+
367
+ function renderIcon(
368
+ primitive: { name: string; size?: SizeToken; color?: ColorToken },
369
+ nodeId: string,
370
+ context: RenderContext
371
+ ): string {
372
+ const classes: string[] = ['inline-flex', 'items-center', 'justify-center']
373
+ if (primitive.size) classes.push(ICON_SIZE_CLASS[primitive.size])
374
+ if (primitive.color) classes.push(COLOR_CLASS[primitive.color])
375
+
376
+ // Resolve icon name
377
+ let iconName = primitive.name
378
+ if (iconName.startsWith('"') || iconName.startsWith("'")) {
379
+ // Quoted literal - strip quotes
380
+ iconName = iconName.slice(1, -1)
381
+ } else if (context.props && iconName in context.props) {
382
+ // Prop reference
383
+ iconName = String(context.props[iconName] ?? '')
384
+ }
385
+
386
+ return `<span data-primitive="$icon" data-icon="${escapeHtml(iconName)}" data-node-id="${nodeId}" class="${classes.join(' ')}"><!-- icon: ${escapeHtml(iconName)} --></span>`
387
+ }
388
+
389
+ function renderImage(
390
+ primitive: { src: string; alt?: string; fit?: FitToken },
391
+ nodeId: string,
392
+ context: RenderContext
393
+ ): string {
394
+ const classes: string[] = ['max-w-full']
395
+ if (primitive.fit) classes.push(FIT_CLASS[primitive.fit])
396
+
397
+ // Resolve src
398
+ let src = primitive.src
399
+ if (src.startsWith('"') || src.startsWith("'")) {
400
+ // Quoted literal - strip quotes
401
+ src = src.slice(1, -1)
402
+ } else if (context.props && src in context.props) {
403
+ // Prop reference
404
+ src = String(context.props[src] ?? '')
405
+ }
406
+
407
+ const alt = primitive.alt ?? ''
408
+
409
+ return `<img data-primitive="$image" data-node-id="${nodeId}" src="${escapeHtml(src)}" alt="${escapeHtml(alt)}" class="${classes.join(' ')}" />`
410
+ }
411
+
412
+ /**
413
+ * Escape HTML special characters
414
+ */
415
+ function escapeHtml(text: string): string {
416
+ return text
417
+ .replace(/&/g, '&amp;')
418
+ .replace(/</g, '&lt;')
419
+ .replace(/>/g, '&gt;')
420
+ .replace(/"/g, '&quot;')
421
+ .replace(/'/g, '&#039;')
422
+ }
423
+
424
+ /**
425
+ * Generate CSS custom properties for design tokens (shadcn-compatible)
426
+ */
427
+ export function generateTokenCSS(): string {
428
+ return `
429
+ @tailwind base;
430
+ @tailwind components;
431
+ @tailwind utilities;
432
+
433
+ @layer base {
434
+ :root {
435
+ --background: 0 0% 100%;
436
+ --foreground: 222.2 84% 4.9%;
437
+
438
+ --card: 0 0% 100%;
439
+ --card-foreground: 222.2 84% 4.9%;
440
+
441
+ --popover: 0 0% 100%;
442
+ --popover-foreground: 222.2 84% 4.9%;
443
+
444
+ --primary: 221.2 83.2% 53.3%;
445
+ --primary-foreground: 210 40% 98%;
446
+
447
+ --secondary: 210 40% 96.1%;
448
+ --secondary-foreground: 222.2 47.4% 11.2%;
449
+
450
+ --muted: 210 40% 96.1%;
451
+ --muted-foreground: 215.4 16.3% 46.9%;
452
+
453
+ --accent: 210 40% 96.1%;
454
+ --accent-foreground: 222.2 47.4% 11.2%;
455
+
456
+ --destructive: 0 84.2% 60.2%;
457
+ --destructive-foreground: 210 40% 98%;
458
+
459
+ --border: 214.3 31.8% 91.4%;
460
+ --input: 214.3 31.8% 91.4%;
461
+ --ring: 221.2 83.2% 53.3%;
462
+
463
+ --radius: 0.5rem;
464
+ }
465
+
466
+ .dark {
467
+ --background: 222.2 84% 4.9%;
468
+ --foreground: 210 40% 98%;
469
+
470
+ --card: 222.2 84% 4.9%;
471
+ --card-foreground: 210 40% 98%;
472
+
473
+ --popover: 222.2 84% 4.9%;
474
+ --popover-foreground: 210 40% 98%;
475
+
476
+ --primary: 217.2 91.2% 59.8%;
477
+ --primary-foreground: 222.2 47.4% 11.2%;
478
+
479
+ --secondary: 217.2 32.6% 17.5%;
480
+ --secondary-foreground: 210 40% 98%;
481
+
482
+ --muted: 217.2 32.6% 17.5%;
483
+ --muted-foreground: 215 20.2% 65.1%;
484
+
485
+ --accent: 217.2 32.6% 17.5%;
486
+ --accent-foreground: 210 40% 98%;
487
+
488
+ --destructive: 0 62.8% 30.6%;
489
+ --destructive-foreground: 210 40% 98%;
490
+
491
+ --border: 217.2 32.6% 17.5%;
492
+ --input: 217.2 32.6% 17.5%;
493
+ --ring: 224.3 76.3% 48%;
494
+ }
495
+ }
496
+ `.trim()
497
+ }
@@ -0,0 +1,82 @@
1
+ /*
2
+ * Design Tokens - shadcn/ui compatible
3
+ * These CSS variables enable Tailwind utilities like bg-background, text-foreground, etc.
4
+ */
5
+
6
+ @tailwind base;
7
+ @tailwind components;
8
+ @tailwind utilities;
9
+
10
+ @layer base {
11
+ :root {
12
+ --background: 0 0% 100%;
13
+ --foreground: 222.2 84% 4.9%;
14
+
15
+ --card: 0 0% 100%;
16
+ --card-foreground: 222.2 84% 4.9%;
17
+
18
+ --popover: 0 0% 100%;
19
+ --popover-foreground: 222.2 84% 4.9%;
20
+
21
+ --primary: 221.2 83.2% 53.3%;
22
+ --primary-foreground: 210 40% 98%;
23
+
24
+ --secondary: 210 40% 96.1%;
25
+ --secondary-foreground: 222.2 47.4% 11.2%;
26
+
27
+ --muted: 210 40% 96.1%;
28
+ --muted-foreground: 215.4 16.3% 46.9%;
29
+
30
+ --accent: 210 40% 96.1%;
31
+ --accent-foreground: 222.2 47.4% 11.2%;
32
+
33
+ --destructive: 0 84.2% 60.2%;
34
+ --destructive-foreground: 210 40% 98%;
35
+
36
+ --border: 214.3 31.8% 91.4%;
37
+ --input: 214.3 31.8% 91.4%;
38
+ --ring: 221.2 83.2% 53.3%;
39
+
40
+ --radius: 0.5rem;
41
+ }
42
+
43
+ .dark {
44
+ --background: 222.2 84% 4.9%;
45
+ --foreground: 210 40% 98%;
46
+
47
+ --card: 222.2 84% 4.9%;
48
+ --card-foreground: 210 40% 98%;
49
+
50
+ --popover: 222.2 84% 4.9%;
51
+ --popover-foreground: 210 40% 98%;
52
+
53
+ --primary: 217.2 91.2% 59.8%;
54
+ --primary-foreground: 222.2 47.4% 11.2%;
55
+
56
+ --secondary: 217.2 32.6% 17.5%;
57
+ --secondary-foreground: 210 40% 98%;
58
+
59
+ --muted: 217.2 32.6% 17.5%;
60
+ --muted-foreground: 215 20.2% 65.1%;
61
+
62
+ --accent: 217.2 32.6% 17.5%;
63
+ --accent-foreground: 210 40% 98%;
64
+
65
+ --destructive: 0 62.8% 30.6%;
66
+ --destructive-foreground: 210 40% 98%;
67
+
68
+ --border: 217.2 32.6% 17.5%;
69
+ --input: 217.2 32.6% 17.5%;
70
+ --ring: 224.3 76.3% 48%;
71
+ }
72
+ }
73
+
74
+ /* Base styles */
75
+ @layer base {
76
+ * {
77
+ @apply border-border;
78
+ }
79
+ body {
80
+ @apply bg-background text-foreground;
81
+ }
82
+ }