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,248 @@
1
+ // src/primitives/types.ts
2
+ // Type definitions for layout and content primitives
3
+
4
+ /**
5
+ * Design token types for consistent spacing, sizing, and styling
6
+ * Using shadcn/ui-compatible naming conventions
7
+ */
8
+ export type SpacingToken = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl'
9
+ export type AlignToken = 'start' | 'center' | 'end' | 'stretch' | 'between'
10
+
11
+ // Background tokens - shadcn-compatible
12
+ export type BackgroundToken =
13
+ | 'transparent'
14
+ | 'background'
15
+ | 'card'
16
+ | 'primary'
17
+ | 'secondary'
18
+ | 'muted'
19
+ | 'accent'
20
+ | 'destructive'
21
+ | 'input'
22
+
23
+ export type RadiusToken = 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full'
24
+
25
+ // Color tokens - shadcn-compatible (foreground colors for text)
26
+ export type ColorToken =
27
+ | 'foreground'
28
+ | 'card-foreground'
29
+ | 'primary'
30
+ | 'primary-foreground'
31
+ | 'secondary'
32
+ | 'secondary-foreground'
33
+ | 'muted'
34
+ | 'muted-foreground'
35
+ | 'accent'
36
+ | 'accent-foreground'
37
+ | 'destructive'
38
+ | 'destructive-foreground'
39
+ | 'border'
40
+ | 'ring'
41
+
42
+ // Typography size tokens
43
+ export type SizeToken = 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl'
44
+
45
+ export type WeightToken = 'normal' | 'medium' | 'semibold' | 'bold'
46
+ export type FitToken = 'cover' | 'contain' | 'fill'
47
+
48
+ /**
49
+ * All primitive type names
50
+ */
51
+ export type PrimitiveType =
52
+ | '$col'
53
+ | '$row'
54
+ | '$box'
55
+ | '$spacer'
56
+ | '$slot'
57
+ | '$text'
58
+ | '$icon'
59
+ | '$image'
60
+
61
+ /**
62
+ * Layout primitive: $col - Vertical stack
63
+ */
64
+ export interface ColPrimitive {
65
+ type: '$col'
66
+ gap?: SpacingToken
67
+ align?: AlignToken
68
+ padding?: SpacingToken
69
+ }
70
+
71
+ /**
72
+ * Layout primitive: $row - Horizontal stack
73
+ */
74
+ export interface RowPrimitive {
75
+ type: '$row'
76
+ gap?: SpacingToken
77
+ align?: AlignToken
78
+ padding?: SpacingToken
79
+ }
80
+
81
+ /**
82
+ * Layout primitive: $box - Container with styling
83
+ */
84
+ export interface BoxPrimitive {
85
+ type: '$box'
86
+ padding?: SpacingToken
87
+ bg?: BackgroundToken
88
+ radius?: RadiusToken
89
+ }
90
+
91
+ /**
92
+ * Layout primitive: $spacer - Whitespace
93
+ */
94
+ export interface SpacerPrimitive {
95
+ type: '$spacer'
96
+ size?: SpacingToken // If absent, flex grow
97
+ }
98
+
99
+ /**
100
+ * Layout primitive: $slot - State-dependent placeholder
101
+ */
102
+ export interface SlotPrimitive {
103
+ type: '$slot'
104
+ name: string
105
+ }
106
+
107
+ /**
108
+ * Content primitive: $text - Text content
109
+ */
110
+ export interface TextPrimitive {
111
+ type: '$text'
112
+ content: string // Prop reference or quoted literal
113
+ size?: SizeToken
114
+ weight?: WeightToken
115
+ color?: ColorToken
116
+ }
117
+
118
+ /**
119
+ * Content primitive: $icon - Icon
120
+ */
121
+ export interface IconPrimitive {
122
+ type: '$icon'
123
+ name: string // Prop reference or string
124
+ size?: SizeToken
125
+ color?: ColorToken
126
+ }
127
+
128
+ /**
129
+ * Content primitive: $image - Image
130
+ */
131
+ export interface ImagePrimitive {
132
+ type: '$image'
133
+ src: string // Prop reference or URL
134
+ alt?: string
135
+ fit?: FitToken
136
+ }
137
+
138
+ /**
139
+ * Union of all primitives
140
+ */
141
+ export type Primitive =
142
+ | ColPrimitive
143
+ | RowPrimitive
144
+ | BoxPrimitive
145
+ | SpacerPrimitive
146
+ | SlotPrimitive
147
+ | TextPrimitive
148
+ | IconPrimitive
149
+ | ImagePrimitive
150
+
151
+ /**
152
+ * Parsed primitive with original string
153
+ */
154
+ export interface ParsedPrimitive<T extends Primitive = Primitive> {
155
+ primitive: T
156
+ raw: string
157
+ }
158
+
159
+ /**
160
+ * Template node - container or leaf
161
+ */
162
+ export interface TemplateContainerNode {
163
+ type: string // Primitive string like "$col(gap:lg)"
164
+ children?: Record<string, TemplateNode>
165
+ }
166
+
167
+ export interface TemplateLeafNode {
168
+ value: string // Primitive or ref like "$text(label)" or "components/button"
169
+ }
170
+
171
+ export type TemplateNode = TemplateContainerNode | string
172
+
173
+ /**
174
+ * Template root structure
175
+ */
176
+ export interface Template {
177
+ root: TemplateNode
178
+ }
179
+
180
+ /**
181
+ * Slots mapping: state name -> content ref
182
+ */
183
+ export type Slots = Record<string, Record<string, string>>
184
+
185
+ /**
186
+ * Screen v2 config with template and slots
187
+ */
188
+ export interface ScreenV2Config {
189
+ kind: 'screen'
190
+ id: string
191
+ title: string
192
+ schemaVersion: '2.0'
193
+ states?: Record<string, { description?: string }>
194
+ template: Template
195
+ slots?: Slots
196
+ // Legacy field for backwards compatibility
197
+ layoutByRenderer?: Record<string, unknown>
198
+ }
199
+
200
+ /**
201
+ * Component v2 config with props and template
202
+ */
203
+ export interface ComponentV2Config {
204
+ kind: 'component'
205
+ id: string
206
+ title: string
207
+ schemaVersion: '2.0'
208
+ props?: Record<string, {
209
+ type?: string
210
+ required?: boolean
211
+ default?: unknown
212
+ values?: unknown[]
213
+ }>
214
+ template?: Template
215
+ }
216
+
217
+ /**
218
+ * Check if a string is a primitive (starts with $)
219
+ */
220
+ export function isPrimitive(value: string): boolean {
221
+ return value.startsWith('$')
222
+ }
223
+
224
+ /**
225
+ * Check if a string is a component/screen/flow ref (contains /)
226
+ */
227
+ export function isRef(value: string): boolean {
228
+ return value.includes('/') && !value.startsWith('$')
229
+ }
230
+
231
+ /**
232
+ * Check if a template node is a container (has type and children)
233
+ */
234
+ export function isContainerNode(node: unknown): node is TemplateContainerNode {
235
+ return (
236
+ typeof node === 'object' &&
237
+ node !== null &&
238
+ 'type' in node &&
239
+ typeof (node as Record<string, unknown>).type === 'string'
240
+ )
241
+ }
242
+
243
+ /**
244
+ * Check if a template node is a leaf (string value)
245
+ */
246
+ export function isLeafNode(node: unknown): node is string {
247
+ return typeof node === 'string'
248
+ }
@@ -0,0 +1,137 @@
1
+ // src/tokens/defaults.test.ts
2
+ import { describe, test, expect, beforeAll } from 'bun:test'
3
+ import { load as parse } from 'js-yaml'
4
+ import { readFileSync } from 'fs'
5
+ import { join } from 'path'
6
+
7
+ interface TokensConfig {
8
+ colors: Record<string, string>
9
+ backgrounds: Record<string, string>
10
+ spacing: Record<string, string>
11
+ typography: {
12
+ sizes: Record<string, string>
13
+ weights: Record<string, number>
14
+ }
15
+ radius: Record<string, string>
16
+ shadows: Record<string, string>
17
+ }
18
+
19
+ describe('defaults.yaml', () => {
20
+ let tokens: TokensConfig
21
+
22
+ beforeAll(() => {
23
+ const content = readFileSync(join(import.meta.dir, 'defaults.yaml'), 'utf-8')
24
+ tokens = parse(content) as TokensConfig
25
+ })
26
+
27
+ test('is valid YAML', () => {
28
+ const content = readFileSync(join(import.meta.dir, 'defaults.yaml'), 'utf-8')
29
+ expect(() => parse(content)).not.toThrow()
30
+ })
31
+
32
+ test('has all shadcn color tokens', () => {
33
+ expect(tokens.colors).toHaveProperty('primary')
34
+ expect(tokens.colors).toHaveProperty('secondary')
35
+ expect(tokens.colors).toHaveProperty('muted-foreground')
36
+ expect(tokens.colors).toHaveProperty('destructive')
37
+ })
38
+
39
+ test('has spacing scale', () => {
40
+ expect(Object.keys(tokens.spacing)).toEqual(['none', 'xs', 'sm', 'md', 'lg', 'xl', '2xl'])
41
+ })
42
+
43
+ test('spacing values match Tailwind scale', () => {
44
+ expect(tokens.spacing.none).toBe('0')
45
+ expect(tokens.spacing.xs).toBe('4px')
46
+ expect(tokens.spacing.sm).toBe('8px')
47
+ expect(tokens.spacing.md).toBe('16px')
48
+ expect(tokens.spacing.lg).toBe('24px')
49
+ expect(tokens.spacing.xl).toBe('32px')
50
+ expect(tokens.spacing['2xl']).toBe('48px')
51
+ })
52
+
53
+ test('has all shadcn foreground tokens', () => {
54
+ expect(tokens.colors).toHaveProperty('foreground')
55
+ expect(tokens.colors).toHaveProperty('card-foreground')
56
+ expect(tokens.colors).toHaveProperty('primary-foreground')
57
+ expect(tokens.colors).toHaveProperty('secondary-foreground')
58
+ expect(tokens.colors).toHaveProperty('accent-foreground')
59
+ expect(tokens.colors).toHaveProperty('destructive-foreground')
60
+ expect(tokens.colors).toHaveProperty('popover-foreground')
61
+ })
62
+
63
+ test('has border and ring tokens', () => {
64
+ expect(tokens.colors).toHaveProperty('border')
65
+ expect(tokens.colors).toHaveProperty('ring')
66
+ })
67
+
68
+ test('primary color is blue', () => {
69
+ expect(tokens.colors.primary).toBe('#2563eb')
70
+ })
71
+
72
+ test('destructive color is red', () => {
73
+ expect(tokens.colors.destructive).toBe('#ef4444')
74
+ })
75
+
76
+ test('has typography sizes', () => {
77
+ expect(Object.keys(tokens.typography.sizes)).toEqual(['xs', 'sm', 'base', 'lg', 'xl', '2xl'])
78
+ })
79
+
80
+ test('typography size values are correct', () => {
81
+ expect(tokens.typography.sizes.xs).toBe('12px')
82
+ expect(tokens.typography.sizes.sm).toBe('14px')
83
+ expect(tokens.typography.sizes.base).toBe('16px')
84
+ expect(tokens.typography.sizes.lg).toBe('18px')
85
+ expect(tokens.typography.sizes.xl).toBe('20px')
86
+ expect(tokens.typography.sizes['2xl']).toBe('24px')
87
+ })
88
+
89
+ test('has typography weights', () => {
90
+ expect(Object.keys(tokens.typography.weights)).toEqual(['normal', 'medium', 'semibold', 'bold'])
91
+ })
92
+
93
+ test('typography weight values are correct', () => {
94
+ expect(tokens.typography.weights.normal).toBe(400)
95
+ expect(tokens.typography.weights.medium).toBe(500)
96
+ expect(tokens.typography.weights.semibold).toBe(600)
97
+ expect(tokens.typography.weights.bold).toBe(700)
98
+ })
99
+
100
+ test('has radius scale', () => {
101
+ expect(Object.keys(tokens.radius)).toEqual(['none', 'sm', 'md', 'lg', 'xl', 'full'])
102
+ })
103
+
104
+ test('radius values are correct', () => {
105
+ expect(tokens.radius.none).toBe('0')
106
+ expect(tokens.radius.sm).toBe('4px')
107
+ expect(tokens.radius.md).toBe('6px')
108
+ expect(tokens.radius.lg).toBe('8px')
109
+ expect(tokens.radius.xl).toBe('12px')
110
+ expect(tokens.radius.full).toBe('9999px')
111
+ })
112
+
113
+ test('has shadow scale', () => {
114
+ expect(Object.keys(tokens.shadows)).toEqual(['none', 'sm', 'md', 'lg', 'xl'])
115
+ })
116
+
117
+ test('has background tokens', () => {
118
+ expect(tokens.backgrounds).toHaveProperty('transparent')
119
+ expect(tokens.backgrounds).toHaveProperty('background')
120
+ expect(tokens.backgrounds).toHaveProperty('card')
121
+ expect(tokens.backgrounds).toHaveProperty('primary')
122
+ expect(tokens.backgrounds).toHaveProperty('secondary')
123
+ expect(tokens.backgrounds).toHaveProperty('muted')
124
+ expect(tokens.backgrounds).toHaveProperty('accent')
125
+ expect(tokens.backgrounds).toHaveProperty('destructive')
126
+ expect(tokens.backgrounds).toHaveProperty('input')
127
+ expect(tokens.backgrounds).toHaveProperty('popover')
128
+ })
129
+
130
+ test('background values are correct', () => {
131
+ expect(tokens.backgrounds.transparent).toBe('transparent')
132
+ expect(tokens.backgrounds.background).toBe('#ffffff')
133
+ expect(tokens.backgrounds.card).toBe('#ffffff')
134
+ expect(tokens.backgrounds.popover).toBe('#ffffff')
135
+ expect(tokens.backgrounds.primary).toBe('#2563eb')
136
+ })
137
+ })
@@ -0,0 +1,77 @@
1
+ // src/tokens/defaults.ts
2
+ // Default shadcn design tokens for prev-cli
3
+ // Users can override these in their project's tokens.yaml
4
+
5
+ import type { TokensConfig } from './resolver'
6
+
7
+ export const DEFAULT_TOKENS: TokensConfig = {
8
+ colors: {
9
+ foreground: "#0f172a",
10
+ "card-foreground": "#0f172a",
11
+ "popover-foreground": "#0f172a",
12
+ primary: "#2563eb",
13
+ "primary-foreground": "#ffffff",
14
+ secondary: "#64748b",
15
+ "secondary-foreground": "#0f172a",
16
+ muted: "#94a3b8",
17
+ "muted-foreground": "#64748b",
18
+ accent: "#2563eb",
19
+ "accent-foreground": "#ffffff",
20
+ destructive: "#ef4444",
21
+ "destructive-foreground": "#ffffff",
22
+ border: "#e2e8f0",
23
+ ring: "#2563eb",
24
+ },
25
+ backgrounds: {
26
+ transparent: "transparent",
27
+ background: "#ffffff",
28
+ card: "#ffffff",
29
+ popover: "#ffffff",
30
+ primary: "#2563eb",
31
+ secondary: "#f1f5f9",
32
+ muted: "#f1f5f9",
33
+ accent: "#f1f5f9",
34
+ destructive: "#ef4444",
35
+ input: "#ffffff",
36
+ },
37
+ spacing: {
38
+ none: "0",
39
+ xs: "4px",
40
+ sm: "8px",
41
+ md: "16px",
42
+ lg: "24px",
43
+ xl: "32px",
44
+ "2xl": "48px",
45
+ },
46
+ typography: {
47
+ sizes: {
48
+ xs: "12px",
49
+ sm: "14px",
50
+ base: "16px",
51
+ lg: "18px",
52
+ xl: "20px",
53
+ "2xl": "24px",
54
+ },
55
+ weights: {
56
+ normal: 400,
57
+ medium: 500,
58
+ semibold: 600,
59
+ bold: 700,
60
+ },
61
+ },
62
+ radius: {
63
+ none: "0",
64
+ sm: "4px",
65
+ md: "6px",
66
+ lg: "8px",
67
+ xl: "12px",
68
+ full: "9999px",
69
+ },
70
+ shadows: {
71
+ none: "none",
72
+ sm: "0 1px 2px 0 rgb(0 0 0 / 0.05)",
73
+ md: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
74
+ lg: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",
75
+ xl: "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)",
76
+ },
77
+ }
@@ -0,0 +1,76 @@
1
+ # src/tokens/defaults.yaml
2
+ # Default shadcn design tokens for prev-cli
3
+ # Users can override these in their project's tokens.yaml
4
+
5
+ # Semantic color tokens
6
+ colors:
7
+ foreground: "#0f172a"
8
+ card-foreground: "#0f172a"
9
+ popover-foreground: "#0f172a"
10
+ primary: "#2563eb"
11
+ primary-foreground: "#ffffff"
12
+ secondary: "#64748b"
13
+ secondary-foreground: "#0f172a"
14
+ muted: "#94a3b8"
15
+ muted-foreground: "#64748b"
16
+ accent: "#2563eb"
17
+ accent-foreground: "#ffffff"
18
+ destructive: "#ef4444"
19
+ destructive-foreground: "#ffffff"
20
+ border: "#e2e8f0"
21
+ ring: "#2563eb"
22
+
23
+ # Background tokens
24
+ backgrounds:
25
+ transparent: transparent
26
+ background: "#ffffff"
27
+ card: "#ffffff"
28
+ popover: "#ffffff"
29
+ primary: "#2563eb"
30
+ secondary: "#f1f5f9"
31
+ muted: "#f1f5f9"
32
+ accent: "#f1f5f9"
33
+ destructive: "#ef4444"
34
+ input: "#ffffff"
35
+
36
+ # Spacing scale
37
+ spacing:
38
+ none: "0"
39
+ xs: "4px"
40
+ sm: "8px"
41
+ md: "16px"
42
+ lg: "24px"
43
+ xl: "32px"
44
+ 2xl: "48px"
45
+
46
+ # Typography
47
+ typography:
48
+ sizes:
49
+ xs: "12px"
50
+ sm: "14px"
51
+ base: "16px"
52
+ lg: "18px"
53
+ xl: "20px"
54
+ 2xl: "24px"
55
+ weights:
56
+ normal: 400
57
+ medium: 500
58
+ semibold: 600
59
+ bold: 700
60
+
61
+ # Border radius scale
62
+ radius:
63
+ none: "0"
64
+ sm: "4px"
65
+ md: "6px"
66
+ lg: "8px"
67
+ xl: "12px"
68
+ full: "9999px"
69
+
70
+ # Shadow scale
71
+ shadows:
72
+ none: none
73
+ sm: "0 1px 2px 0 rgb(0 0 0 / 0.05)"
74
+ md: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)"
75
+ lg: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)"
76
+ xl: "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)"