prev-cli 0.24.16 → 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 (37) hide show
  1. package/dist/cli.js +2 -1
  2. package/package.json +5 -2
  3. package/src/jsx/adapters/html.test.ts +191 -0
  4. package/src/jsx/adapters/html.ts +404 -0
  5. package/src/jsx/adapters/react.test.ts +172 -0
  6. package/src/jsx/adapters/react.tsx +346 -0
  7. package/src/jsx/define-component.ts +129 -0
  8. package/src/jsx/index.ts +47 -0
  9. package/src/jsx/jsx-runtime.test.ts +63 -0
  10. package/src/jsx/jsx-runtime.ts +117 -0
  11. package/src/jsx/migrate.test.ts +72 -0
  12. package/src/jsx/migrate.ts +451 -0
  13. package/src/jsx/schemas/index.ts +4 -0
  14. package/src/jsx/schemas/primitives.ts +107 -0
  15. package/src/jsx/schemas/tokens.ts +60 -0
  16. package/src/jsx/validation.ts +77 -0
  17. package/src/jsx/vnode.ts +159 -0
  18. package/src/primitives/index.ts +8 -0
  19. package/src/primitives/migrate.test.ts +317 -0
  20. package/src/primitives/migrate.ts +364 -0
  21. package/src/primitives/parser.test.ts +265 -0
  22. package/src/primitives/parser.ts +442 -0
  23. package/src/primitives/template-parser.test.ts +297 -0
  24. package/src/primitives/template-parser.ts +374 -0
  25. package/src/primitives/template-renderer.test.ts +359 -0
  26. package/src/primitives/template-renderer.ts +497 -0
  27. package/src/primitives/tokens.css +82 -0
  28. package/src/primitives/types.ts +248 -0
  29. package/src/tokens/defaults.test.ts +137 -0
  30. package/src/tokens/defaults.ts +77 -0
  31. package/src/tokens/defaults.yaml +76 -0
  32. package/src/tokens/resolver.test.ts +229 -0
  33. package/src/tokens/resolver.ts +173 -0
  34. package/src/tokens/utils.test.ts +172 -0
  35. package/src/tokens/utils.ts +104 -0
  36. package/src/tokens/validation.test.ts +118 -0
  37. package/src/tokens/validation.ts +226 -0
@@ -0,0 +1,172 @@
1
+ // src/jsx/adapters/react.test.ts
2
+ import { describe, test, expect, beforeEach } from 'bun:test'
3
+ import { toReact, setTokensConfig } from './react'
4
+ import { resolveTokens } from '../../tokens/resolver'
5
+
6
+ // Get default tokens once at module load for test stability
7
+ const defaultTokens = resolveTokens({})
8
+
9
+ describe('React adapter token resolution', () => {
10
+ beforeEach(() => {
11
+ // Explicitly set default tokens for test isolation
12
+ // This avoids relying on filesystem state during tests
13
+ setTokensConfig(defaultTokens)
14
+ })
15
+
16
+ test('Box resolves bg token to background value', () => {
17
+ const node = {
18
+ type: 'box' as const,
19
+ id: 'test-1',
20
+ props: { bg: 'primary' },
21
+ children: []
22
+ }
23
+ const element = toReact(node) as React.ReactElement
24
+ expect(element.props.style.background).toBe('#2563eb')
25
+ })
26
+
27
+ test('Box resolves padding token to spacing value', () => {
28
+ const node = {
29
+ type: 'box' as const,
30
+ id: 'test-2',
31
+ props: { padding: 'lg' },
32
+ children: []
33
+ }
34
+ const element = toReact(node) as React.ReactElement
35
+ expect(element.props.style.padding).toBe('24px')
36
+ })
37
+
38
+ test('Text resolves color token', () => {
39
+ const node = {
40
+ type: 'text' as const,
41
+ id: 'test-3',
42
+ props: { content: 'hi', color: 'muted-foreground' },
43
+ children: []
44
+ }
45
+ const element = toReact(node) as React.ReactElement
46
+ expect(element.props.style.color).toBe('#64748b')
47
+ })
48
+
49
+ test('Text resolves size token', () => {
50
+ const node = {
51
+ type: 'text' as const,
52
+ id: 'test-4',
53
+ props: { content: 'hi', size: 'lg' },
54
+ children: []
55
+ }
56
+ const element = toReact(node) as React.ReactElement
57
+ expect(element.props.style.fontSize).toBe('18px')
58
+ })
59
+
60
+ test('Text resolves weight token', () => {
61
+ const node = {
62
+ type: 'text' as const,
63
+ id: 'test-5',
64
+ props: { content: 'hi', weight: 'bold' },
65
+ children: []
66
+ }
67
+ const element = toReact(node) as React.ReactElement
68
+ expect(element.props.style.fontWeight).toBe(700)
69
+ })
70
+
71
+ test('Box resolves radius token', () => {
72
+ const node = {
73
+ type: 'box' as const,
74
+ id: 'test-6',
75
+ props: { radius: 'lg' },
76
+ children: []
77
+ }
78
+ const element = toReact(node) as React.ReactElement
79
+ expect(element.props.style.borderRadius).toBe('8px')
80
+ })
81
+
82
+ test('Row resolves gap token', () => {
83
+ const node = {
84
+ type: 'row' as const,
85
+ id: 'test-7',
86
+ props: { gap: 'md' },
87
+ children: []
88
+ }
89
+ const element = toReact(node) as React.ReactElement
90
+ expect(element.props.style.gap).toBe('16px')
91
+ })
92
+
93
+ test('uses custom tokens when set via setTokensConfig', () => {
94
+ // Import TokensConfig type
95
+ const customTokens = {
96
+ colors: {
97
+ foreground: '#000',
98
+ 'card-foreground': '#000',
99
+ 'popover-foreground': '#000',
100
+ primary: '#ff0000', // Custom red
101
+ 'primary-foreground': '#fff',
102
+ secondary: '#64748b',
103
+ 'secondary-foreground': '#000',
104
+ muted: '#94a3b8',
105
+ 'muted-foreground': '#64748b',
106
+ accent: '#2563eb',
107
+ 'accent-foreground': '#fff',
108
+ destructive: '#ef4444',
109
+ 'destructive-foreground': '#fff',
110
+ border: '#e2e8f0',
111
+ ring: '#2563eb',
112
+ },
113
+ backgrounds: {
114
+ transparent: 'transparent',
115
+ background: '#fff',
116
+ card: '#fff',
117
+ popover: '#fff',
118
+ primary: '#ff0000', // Custom red
119
+ secondary: '#f1f5f9',
120
+ muted: '#f1f5f9',
121
+ accent: '#f1f5f9',
122
+ destructive: '#ef4444',
123
+ input: '#fff',
124
+ },
125
+ spacing: {
126
+ none: '0',
127
+ xs: '4px',
128
+ sm: '8px',
129
+ md: '16px',
130
+ lg: '24px',
131
+ xl: '32px',
132
+ '2xl': '48px',
133
+ },
134
+ typography: {
135
+ sizes: { xs: '12px', sm: '14px', base: '16px', lg: '18px', xl: '20px', '2xl': '24px' },
136
+ weights: { normal: 400, medium: 500, semibold: 600, bold: 700 },
137
+ },
138
+ radius: { none: '0', sm: '4px', md: '6px', lg: '8px', xl: '12px', full: '9999px' },
139
+ shadows: { none: 'none', sm: '0 1px 2px', md: '0 4px 6px', lg: '0 10px 15px', xl: '0 20px 25px' },
140
+ }
141
+
142
+ setTokensConfig(customTokens)
143
+
144
+ const node = {
145
+ type: 'box' as const,
146
+ id: 'test-8',
147
+ props: { bg: 'primary' },
148
+ children: []
149
+ }
150
+ const element = toReact(node) as React.ReactElement
151
+ expect(element.props.style.background).toBe('#ff0000') // Custom red
152
+ })
153
+
154
+ test('throws error for missing token in custom config', () => {
155
+ const incompleteTokens = {
156
+ ...defaultTokens,
157
+ spacing: { ...defaultTokens.spacing }
158
+ }
159
+ delete (incompleteTokens.spacing as Record<string, unknown>)['lg'] // Remove lg
160
+
161
+ setTokensConfig(incompleteTokens as any)
162
+
163
+ const node = {
164
+ type: 'box' as const,
165
+ id: 'test-missing',
166
+ props: { padding: 'lg' },
167
+ children: []
168
+ }
169
+
170
+ expect(() => toReact(node)).toThrow(/Missing spacing token: "lg"/)
171
+ })
172
+ })
@@ -0,0 +1,346 @@
1
+ // src/jsx/adapters/react.tsx
2
+ // React adapter - renders VNode tree to React elements
3
+ import React from 'react'
4
+ import type { VNodeType } from '../vnode'
5
+ import type {
6
+ SpacingToken,
7
+ AlignToken,
8
+ BackgroundToken,
9
+ RadiusToken,
10
+ ColorToken,
11
+ SizeToken,
12
+ WeightToken,
13
+ FitToken,
14
+ } from '../schemas/tokens'
15
+ // TokensConfig type - must match resolver.ts
16
+ // Duplicated here to avoid importing Node.js-only resolver in browser
17
+ export interface TokensConfig {
18
+ colors: Record<string, string>
19
+ backgrounds: Record<string, string>
20
+ spacing: Record<string, string>
21
+ typography: {
22
+ sizes: Record<string, string>
23
+ weights: Record<string, number>
24
+ }
25
+ radius: Record<string, string>
26
+ shadows: Record<string, string>
27
+ }
28
+
29
+ // ============================================================
30
+ // Token resolution with caching
31
+ // ============================================================
32
+
33
+ // Cached tokens - must be set via setTokensConfig() before rendering
34
+ let cachedTokens: TokensConfig | null = null
35
+
36
+ // Get cached tokens - throws if not initialized
37
+ // In browser: setTokensConfig() must be called first
38
+ // In Node.js tests: setTokensConfig() with resolved tokens
39
+ function getTokens(): TokensConfig {
40
+ if (!cachedTokens) {
41
+ throw new Error(
42
+ 'Tokens not initialized. Call setTokensConfig(tokens) before rendering. ' +
43
+ 'In browser preview, this should be done automatically by the preview runtime.'
44
+ )
45
+ }
46
+ return cachedTokens
47
+ }
48
+
49
+ // Allow setting custom tokens (for testing and runtime override)
50
+ export function setTokensConfig(tokens: TokensConfig | undefined): void {
51
+ cachedTokens = tokens || null
52
+ }
53
+
54
+ // Helper functions to get token values - throw on missing tokens to surface config bugs early
55
+ function getSpacing(token: SpacingToken): string {
56
+ const value = getTokens().spacing[token]
57
+ if (value === undefined) {
58
+ throw new Error(`Missing spacing token: "${token}". Check your tokens.yaml configuration.`)
59
+ }
60
+ return value
61
+ }
62
+
63
+ function getBackground(token: BackgroundToken): string {
64
+ const value = getTokens().backgrounds[token]
65
+ if (value === undefined) {
66
+ throw new Error(`Missing background token: "${token}". Check your tokens.yaml configuration.`)
67
+ }
68
+ return value
69
+ }
70
+
71
+ function getRadius(token: RadiusToken): string {
72
+ const value = getTokens().radius[token]
73
+ if (value === undefined) {
74
+ throw new Error(`Missing radius token: "${token}". Check your tokens.yaml configuration.`)
75
+ }
76
+ return value
77
+ }
78
+
79
+ function getColor(token: ColorToken): string {
80
+ const value = getTokens().colors[token]
81
+ if (value === undefined) {
82
+ throw new Error(`Missing color token: "${token}". Check your tokens.yaml configuration.`)
83
+ }
84
+ return value
85
+ }
86
+
87
+ function getTypographySize(token: SizeToken): string {
88
+ const value = getTokens().typography.sizes[token]
89
+ if (value === undefined) {
90
+ throw new Error(`Missing typography.sizes token: "${token}". Check your tokens.yaml configuration.`)
91
+ }
92
+ return value
93
+ }
94
+
95
+ function getTypographyWeight(token: WeightToken): number {
96
+ const value = getTokens().typography.weights[token]
97
+ if (value === undefined) {
98
+ throw new Error(`Missing typography.weights token: "${token}". Check your tokens.yaml configuration.`)
99
+ }
100
+ return value
101
+ }
102
+
103
+ // ============================================================
104
+ // Non-token style mappings (CSS values, not design tokens)
105
+ // ============================================================
106
+
107
+ const ALIGN_VALUES: Record<AlignToken, React.CSSProperties> = {
108
+ start: { alignItems: 'flex-start' },
109
+ center: { alignItems: 'center' },
110
+ end: { alignItems: 'flex-end' },
111
+ stretch: { alignItems: 'stretch' },
112
+ between: { justifyContent: 'space-between' },
113
+ }
114
+
115
+ const ICON_SIZE_VALUES: Record<SizeToken, string> = {
116
+ xs: '12px',
117
+ sm: '16px',
118
+ base: '20px',
119
+ lg: '24px',
120
+ xl: '28px',
121
+ '2xl': '32px',
122
+ }
123
+
124
+ const FIT_VALUES: Record<FitToken, React.CSSProperties['objectFit']> = {
125
+ cover: 'cover',
126
+ contain: 'contain',
127
+ fill: 'fill',
128
+ }
129
+
130
+ // ============================================================
131
+ // Render context
132
+ // ============================================================
133
+
134
+ export interface ReactRenderContext {
135
+ /** Current state for slot resolution */
136
+ state?: string
137
+ /** Slots mapping: slotName -> stateName -> VNode */
138
+ slots?: Record<string, Record<string, VNodeType>>
139
+ /** Callback to render component nodes */
140
+ renderComponent?: (name: string, props: Record<string, unknown>, children?: React.ReactNode) => React.ReactNode
141
+ }
142
+
143
+ // ============================================================
144
+ // Main render function
145
+ // ============================================================
146
+
147
+ /**
148
+ * Render a VNode tree to React elements
149
+ */
150
+ export function toReact(node: VNodeType | VNodeType[], context: ReactRenderContext = {}): React.ReactNode {
151
+ if (Array.isArray(node)) {
152
+ return node.map((n, i) => <React.Fragment key={n.id || i}>{renderNode(n, context)}</React.Fragment>)
153
+ }
154
+ return renderNode(node, context)
155
+ }
156
+
157
+ function renderNode(node: VNodeType, context: ReactRenderContext): React.ReactNode {
158
+ switch (node.type) {
159
+ case 'col':
160
+ return renderCol(node, context)
161
+ case 'row':
162
+ return renderRow(node, context)
163
+ case 'box':
164
+ return renderBox(node, context)
165
+ case 'spacer':
166
+ return renderSpacer(node)
167
+ case 'slot':
168
+ return renderSlot(node, context)
169
+ case 'text':
170
+ return renderText(node)
171
+ case 'icon':
172
+ return renderIcon(node)
173
+ case 'image':
174
+ return renderImage(node)
175
+ case 'component':
176
+ return renderComponent(node, context)
177
+ default:
178
+ return null
179
+ }
180
+ }
181
+
182
+ function renderChildren(children: VNodeType[] | undefined, context: ReactRenderContext): React.ReactNode {
183
+ if (!children || children.length === 0) return null
184
+ return children.map((child) => (
185
+ <React.Fragment key={child.id}>{renderNode(child, context)}</React.Fragment>
186
+ ))
187
+ }
188
+
189
+ // ============================================================
190
+ // Primitive renderers
191
+ // ============================================================
192
+
193
+ function renderCol(node: VNodeType, context: ReactRenderContext): React.ReactNode {
194
+ const { gap, align, padding } = node.props as { gap?: SpacingToken; align?: AlignToken; padding?: SpacingToken }
195
+
196
+ const style: React.CSSProperties = {
197
+ display: 'flex',
198
+ flexDirection: 'column',
199
+ ...(gap && { gap: getSpacing(gap) }),
200
+ ...(align && ALIGN_VALUES[align]),
201
+ ...(padding && { padding: getSpacing(padding) }),
202
+ }
203
+
204
+ return (
205
+ <div data-primitive="col" data-node-id={node.id} style={style}>
206
+ {renderChildren(node.children, context)}
207
+ </div>
208
+ )
209
+ }
210
+
211
+ function renderRow(node: VNodeType, context: ReactRenderContext): React.ReactNode {
212
+ const { gap, align, padding } = node.props as { gap?: SpacingToken; align?: AlignToken; padding?: SpacingToken }
213
+
214
+ const style: React.CSSProperties = {
215
+ display: 'flex',
216
+ flexDirection: 'row',
217
+ alignItems: 'center', // Row defaults to center
218
+ ...(gap && { gap: getSpacing(gap) }),
219
+ ...(align && ALIGN_VALUES[align]),
220
+ ...(padding && { padding: getSpacing(padding) }),
221
+ }
222
+
223
+ return (
224
+ <div data-primitive="row" data-node-id={node.id} style={style}>
225
+ {renderChildren(node.children, context)}
226
+ </div>
227
+ )
228
+ }
229
+
230
+ function renderBox(node: VNodeType, context: ReactRenderContext): React.ReactNode {
231
+ const { padding, bg, radius } = node.props as { padding?: SpacingToken; bg?: BackgroundToken; radius?: RadiusToken }
232
+
233
+ const style: React.CSSProperties = {
234
+ ...(padding && { padding: getSpacing(padding) }),
235
+ ...(bg && { background: getBackground(bg) }),
236
+ ...(radius && { borderRadius: getRadius(radius) }),
237
+ }
238
+
239
+ return (
240
+ <div data-primitive="box" data-node-id={node.id} style={style}>
241
+ {renderChildren(node.children, context)}
242
+ </div>
243
+ )
244
+ }
245
+
246
+ function renderSpacer(node: VNodeType): React.ReactNode {
247
+ const { size } = node.props as { size?: SpacingToken }
248
+
249
+ const style: React.CSSProperties = size
250
+ ? { width: getSpacing(size), height: getSpacing(size), flexShrink: 0 }
251
+ : { flex: 1 }
252
+
253
+ return <div data-primitive="spacer" data-node-id={node.id} style={style} />
254
+ }
255
+
256
+ function renderSlot(node: VNodeType, context: ReactRenderContext): React.ReactNode {
257
+ const { name } = node.props as { name: string }
258
+ const state = context.state || 'default'
259
+
260
+ if (context.slots && context.slots[name]) {
261
+ const stateMapping = context.slots[name]
262
+ const content = stateMapping[state] || stateMapping.default
263
+
264
+ if (content) {
265
+ return renderNode(content, context)
266
+ }
267
+ }
268
+
269
+ return <div data-primitive="slot" data-slot-name={name} data-node-id={node.id} />
270
+ }
271
+
272
+ function renderText(node: VNodeType): React.ReactNode {
273
+ const { content, size, weight, color } = node.props as {
274
+ content?: string
275
+ size?: SizeToken
276
+ weight?: WeightToken
277
+ color?: ColorToken
278
+ }
279
+
280
+ const style: React.CSSProperties = {
281
+ ...(size && { fontSize: getTypographySize(size) }),
282
+ ...(weight && { fontWeight: getTypographyWeight(weight) }),
283
+ ...(color && { color: getColor(color) }),
284
+ }
285
+
286
+ return (
287
+ <span data-primitive="text" data-node-id={node.id} style={style}>
288
+ {content || ''}
289
+ </span>
290
+ )
291
+ }
292
+
293
+ function renderIcon(node: VNodeType): React.ReactNode {
294
+ const { name, size, color } = node.props as { name: string; size?: SizeToken; color?: ColorToken }
295
+
296
+ const iconSize = size ? ICON_SIZE_VALUES[size] : ICON_SIZE_VALUES.base
297
+
298
+ const style: React.CSSProperties = {
299
+ display: 'inline-flex',
300
+ alignItems: 'center',
301
+ justifyContent: 'center',
302
+ width: iconSize,
303
+ height: iconSize,
304
+ ...(color && { color: getColor(color) }),
305
+ }
306
+
307
+ // Placeholder for icon - in real implementation, would use icon library
308
+ return (
309
+ <span data-primitive="icon" data-icon={name} data-node-id={node.id} style={style}>
310
+ {/* Icon placeholder */}
311
+ </span>
312
+ )
313
+ }
314
+
315
+ function renderImage(node: VNodeType): React.ReactNode {
316
+ const { src, alt, fit } = node.props as { src: string; alt?: string; fit?: FitToken }
317
+
318
+ const style: React.CSSProperties = {
319
+ maxWidth: '100%',
320
+ ...(fit && { objectFit: FIT_VALUES[fit] }),
321
+ }
322
+
323
+ return (
324
+ <img
325
+ data-primitive="image"
326
+ data-node-id={node.id}
327
+ src={src}
328
+ alt={alt || ''}
329
+ style={style}
330
+ />
331
+ )
332
+ }
333
+
334
+ function renderComponent(node: VNodeType, context: ReactRenderContext): React.ReactNode {
335
+ const componentName = node.componentName || 'Unknown'
336
+
337
+ if (context.renderComponent) {
338
+ return context.renderComponent(componentName, node.props, renderChildren(node.children, context))
339
+ }
340
+
341
+ return (
342
+ <div data-component={componentName} data-node-id={node.id}>
343
+ {renderChildren(node.children, context)}
344
+ </div>
345
+ )
346
+ }
@@ -0,0 +1,129 @@
1
+ // src/jsx/define-component.ts
2
+ // API for defining custom components with typed props and states
3
+ import { z } from 'zod'
4
+ import { createComponentVNode, type VNodeType } from './vnode'
5
+ import { validateProps } from './validation'
6
+
7
+ /**
8
+ * Render context passed to component render function
9
+ */
10
+ export interface ComponentContext<TProps, TState> {
11
+ props: TProps
12
+ state: TState
13
+ }
14
+
15
+ /**
16
+ * Component definition options
17
+ */
18
+ export interface ComponentDefinition<
19
+ TProps extends z.ZodType,
20
+ TStates extends z.ZodType,
21
+ > {
22
+ /** Component name (used in VNode tree) */
23
+ name: string
24
+ /** Zod schema for props */
25
+ props: TProps
26
+ /** Zod schema for states */
27
+ states: TStates
28
+ /** Default state value */
29
+ defaultState: z.infer<TStates>
30
+ /** Render function that returns a VNode tree */
31
+ render: (ctx: ComponentContext<z.infer<TProps>, z.infer<TStates>>) => VNodeType
32
+ }
33
+
34
+ /**
35
+ * Component function type returned by defineComponent
36
+ */
37
+ export type ComponentFunction<TProps, TState> = {
38
+ (props: TProps, state?: TState): VNodeType
39
+ componentName: string
40
+ propsSchema: z.ZodType<TProps>
41
+ statesSchema: z.ZodType<TState>
42
+ defaultState: TState
43
+ }
44
+
45
+ /**
46
+ * Define a reusable component with typed props and states
47
+ *
48
+ * @example
49
+ * ```tsx
50
+ * const Button = defineComponent({
51
+ * name: 'Button',
52
+ * props: z.object({
53
+ * label: z.string(),
54
+ * variant: z.enum(['primary', 'secondary']).optional(),
55
+ * }),
56
+ * states: z.enum(['idle', 'loading', 'disabled']),
57
+ * defaultState: 'idle',
58
+ * render: ({ props, state }) => (
59
+ * <Box bg={state === 'loading' ? 'muted' : 'primary'} padding="md" radius="md">
60
+ * <Text>{props.label}</Text>
61
+ * </Box>
62
+ * ),
63
+ * })
64
+ *
65
+ * // Usage
66
+ * <Button label="Click me" />
67
+ * <Button label="Processing..." state="loading" />
68
+ * ```
69
+ */
70
+ export function defineComponent<
71
+ TProps extends z.ZodType,
72
+ TStates extends z.ZodType,
73
+ >(
74
+ definition: ComponentDefinition<TProps, TStates>
75
+ ): ComponentFunction<z.infer<TProps>, z.infer<TStates>> {
76
+ const { name, props: propsSchema, states: statesSchema, defaultState, render } = definition
77
+
78
+ // Validate default state at definition time
79
+ statesSchema.parse(defaultState)
80
+
81
+ const Component = function (
82
+ props: z.infer<TProps>,
83
+ state?: z.infer<TStates>
84
+ ): VNodeType {
85
+ // Validate props based on current validation mode
86
+ validateProps(propsSchema, props, name)
87
+
88
+ // Use provided state or default
89
+ const resolvedState = state !== undefined ? state : defaultState
90
+
91
+ // Validate state if provided
92
+ if (state !== undefined) {
93
+ validateProps(statesSchema, state, `${name}[state]`)
94
+ }
95
+
96
+ // Render the component
97
+ const rendered = render({ props, state: resolvedState })
98
+
99
+ // Wrap in component VNode
100
+ return createComponentVNode(name, props as Record<string, unknown>, [rendered])
101
+ }
102
+
103
+ // Attach metadata
104
+ Component.componentName = name
105
+ Component.propsSchema = propsSchema
106
+ Component.statesSchema = statesSchema
107
+ Component.defaultState = defaultState
108
+
109
+ return Component as ComponentFunction<z.infer<TProps>, z.infer<TStates>>
110
+ }
111
+
112
+ /**
113
+ * Helper to create a simple component without states
114
+ */
115
+ export function defineStatelessComponent<TProps extends z.ZodType>(definition: {
116
+ name: string
117
+ props: TProps
118
+ render: (props: z.infer<TProps>) => VNodeType
119
+ }): (props: z.infer<TProps>) => VNodeType {
120
+ const NoState = z.literal('default')
121
+
122
+ return defineComponent({
123
+ name: definition.name,
124
+ props: definition.props,
125
+ states: NoState,
126
+ defaultState: 'default',
127
+ render: ({ props }) => definition.render(props),
128
+ })
129
+ }
@@ -0,0 +1,47 @@
1
+ // src/jsx/index.ts
2
+ // Main entry point for JSX primitives
3
+
4
+ // Schemas
5
+ export * from './schemas'
6
+
7
+ // VNode
8
+ export {
9
+ createVNode,
10
+ createComponentVNode,
11
+ normalizeChildren,
12
+ vnodeEquals,
13
+ resetIdCounter,
14
+ VNode,
15
+ type VNodeType,
16
+ } from './vnode'
17
+
18
+ // JSX runtime (for direct use)
19
+ export {
20
+ Col,
21
+ Row,
22
+ Box,
23
+ Spacer,
24
+ Slot,
25
+ Text,
26
+ Icon,
27
+ Image,
28
+ Fragment,
29
+ } from './jsx-runtime'
30
+
31
+ // defineComponent API
32
+ export {
33
+ defineComponent,
34
+ defineStatelessComponent,
35
+ type ComponentContext,
36
+ type ComponentDefinition,
37
+ type ComponentFunction,
38
+ } from './define-component'
39
+
40
+ // HTML adapter
41
+ export { renderToHtml, type RenderContext } from './adapters/html'
42
+
43
+ // React adapter
44
+ export { toReact, setTokensConfig, type ReactRenderContext, type TokensConfig } from './adapters/react'
45
+
46
+ // Migration tool
47
+ export { migrateYamlToJsx, type MigrationResult, type YamlConfig } from './migrate'