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,317 @@
1
+ // src/primitives/migrate.test.ts
2
+ import { test, expect, describe } from 'bun:test'
3
+ import { migrateLayout, migrateScreenConfig } from './migrate'
4
+
5
+ describe('migrateLayout', () => {
6
+ describe('ComponentRef', () => {
7
+ test('migrates ComponentRef to string ref', () => {
8
+ const result = migrateLayout({
9
+ type: 'ComponentRef',
10
+ ref: 'components/button',
11
+ })
12
+ expect(result.success).toBe(true)
13
+ expect(result.template?.root).toBe('components/button')
14
+ })
15
+
16
+ test('warns about ComponentRef with props', () => {
17
+ const result = migrateLayout({
18
+ type: 'ComponentRef',
19
+ ref: 'components/button',
20
+ props: { disabled: true },
21
+ })
22
+ expect(result.success).toBe(true)
23
+ expect(result.warnings).toHaveLength(1)
24
+ expect(result.warnings[0]).toContain('props')
25
+ })
26
+ })
27
+
28
+ describe('Slot', () => {
29
+ test('migrates Slot to $slot primitive', () => {
30
+ const result = migrateLayout({
31
+ type: 'Slot',
32
+ name: 'main',
33
+ })
34
+ expect(result.success).toBe(true)
35
+ expect(result.template?.root).toBe('$slot(main)')
36
+ })
37
+
38
+ test('extracts slot default content', () => {
39
+ const result = migrateLayout({
40
+ type: 'Slot',
41
+ name: 'content',
42
+ default: [
43
+ {
44
+ type: 'ComponentRef',
45
+ ref: 'components/placeholder',
46
+ },
47
+ ],
48
+ })
49
+ expect(result.success).toBe(true)
50
+ expect(result.template?.root).toBe('$slot(content)')
51
+ expect(result.slots?.content?.default).toBe('components/placeholder')
52
+ })
53
+ })
54
+
55
+ describe('Stack/VStack', () => {
56
+ test('migrates Stack to $col', () => {
57
+ const result = migrateLayout({
58
+ type: 'Stack',
59
+ })
60
+ expect(result.success).toBe(true)
61
+ expect(result.template?.root).toBe('$col')
62
+ })
63
+
64
+ test('migrates VStack to $col', () => {
65
+ const result = migrateLayout({
66
+ type: 'VStack',
67
+ })
68
+ expect(result.success).toBe(true)
69
+ expect(result.template?.root).toBe('$col')
70
+ })
71
+
72
+ test('migrates Stack with gap', () => {
73
+ const result = migrateLayout({
74
+ type: 'Stack',
75
+ gap: 'lg',
76
+ })
77
+ expect(result.success).toBe(true)
78
+ expect(result.template?.root).toBe('$col(gap:lg)')
79
+ })
80
+
81
+ test('migrates Stack with CSS gap value', () => {
82
+ const result = migrateLayout({
83
+ type: 'Stack',
84
+ gap: '16px',
85
+ })
86
+ expect(result.success).toBe(true)
87
+ expect(result.template?.root).toBe('$col(gap:md)')
88
+ })
89
+
90
+ test('migrates Stack with children', () => {
91
+ const result = migrateLayout({
92
+ type: 'Stack',
93
+ children: [
94
+ { type: 'ComponentRef', ref: 'components/header' },
95
+ { type: 'ComponentRef', ref: 'components/footer' },
96
+ ],
97
+ })
98
+ expect(result.success).toBe(true)
99
+ const root = result.template?.root
100
+ expect(typeof root).toBe('object')
101
+ if (typeof root === 'object' && root !== null && 'children' in root) {
102
+ expect(Object.keys(root.children || {})).toHaveLength(2)
103
+ }
104
+ })
105
+ })
106
+
107
+ describe('HStack', () => {
108
+ test('migrates HStack to $row', () => {
109
+ const result = migrateLayout({
110
+ type: 'HStack',
111
+ })
112
+ expect(result.success).toBe(true)
113
+ expect(result.template?.root).toBe('$row')
114
+ })
115
+
116
+ test('migrates HStack with gap and align', () => {
117
+ const result = migrateLayout({
118
+ type: 'HStack',
119
+ gap: 'sm',
120
+ align: 'center',
121
+ })
122
+ expect(result.success).toBe(true)
123
+ expect(result.template?.root).toContain('$row')
124
+ expect(result.template?.root).toContain('gap:sm')
125
+ expect(result.template?.root).toContain('align:center')
126
+ })
127
+ })
128
+
129
+ describe('Box/Container', () => {
130
+ test('migrates Box to $box', () => {
131
+ const result = migrateLayout({
132
+ type: 'Box',
133
+ })
134
+ expect(result.success).toBe(true)
135
+ expect(result.template?.root).toBe('$box')
136
+ })
137
+
138
+ test('migrates Container to $box', () => {
139
+ const result = migrateLayout({
140
+ type: 'Container',
141
+ })
142
+ expect(result.success).toBe(true)
143
+ expect(result.template?.root).toBe('$box')
144
+ })
145
+
146
+ test('migrates Box with padding', () => {
147
+ const result = migrateLayout({
148
+ type: 'Box',
149
+ padding: 'lg',
150
+ })
151
+ expect(result.success).toBe(true)
152
+ expect(result.template?.root).toBe('$box(padding:lg)')
153
+ })
154
+
155
+ test('migrates Box with bg', () => {
156
+ const result = migrateLayout({
157
+ type: 'Box',
158
+ bg: 'surface',
159
+ })
160
+ expect(result.success).toBe(true)
161
+ expect(result.template?.root).toBe('$box(bg:surface)')
162
+ })
163
+
164
+ test('migrates Box with radius', () => {
165
+ const result = migrateLayout({
166
+ type: 'Box',
167
+ radius: 'md',
168
+ })
169
+ expect(result.success).toBe(true)
170
+ expect(result.template?.root).toBe('$box(radius:md)')
171
+ })
172
+ })
173
+
174
+ describe('Spacer', () => {
175
+ test('migrates Spacer without size', () => {
176
+ const result = migrateLayout({
177
+ type: 'Spacer',
178
+ })
179
+ expect(result.success).toBe(true)
180
+ expect(result.template?.root).toBe('$spacer')
181
+ })
182
+
183
+ test('migrates Spacer with token size', () => {
184
+ const result = migrateLayout({
185
+ type: 'Spacer',
186
+ size: 'xl',
187
+ })
188
+ expect(result.success).toBe(true)
189
+ expect(result.template?.root).toBe('$spacer(xl)')
190
+ })
191
+ })
192
+
193
+ describe('Text', () => {
194
+ test('migrates Text with content', () => {
195
+ const result = migrateLayout({
196
+ type: 'Text',
197
+ content: 'label',
198
+ })
199
+ expect(result.success).toBe(true)
200
+ expect(result.template?.root).toBe('$text(label)')
201
+ })
202
+
203
+ test('migrates Text with literal children', () => {
204
+ const result = migrateLayout({
205
+ type: 'Text',
206
+ children: 'Hello World',
207
+ })
208
+ expect(result.success).toBe(true)
209
+ expect(result.template?.root).toBe('$text("Hello World")')
210
+ })
211
+
212
+ test('migrates Text with size and weight', () => {
213
+ const result = migrateLayout({
214
+ type: 'Text',
215
+ content: 'title',
216
+ size: 'lg',
217
+ weight: 'bold',
218
+ })
219
+ expect(result.success).toBe(true)
220
+ expect(result.template?.root).toContain('$text')
221
+ expect(result.template?.root).toContain('size:lg')
222
+ expect(result.template?.root).toContain('weight:bold')
223
+ })
224
+ })
225
+
226
+ describe('multiple root nodes', () => {
227
+ test('wraps multiple nodes in $col', () => {
228
+ const result = migrateLayout([
229
+ { type: 'ComponentRef', ref: 'components/header' },
230
+ { type: 'ComponentRef', ref: 'components/footer' },
231
+ ])
232
+ expect(result.success).toBe(true)
233
+ const root = result.template?.root
234
+ expect(typeof root).toBe('object')
235
+ if (typeof root === 'object' && root !== null && 'type' in root) {
236
+ expect(root.type).toBe('$col')
237
+ }
238
+ })
239
+ })
240
+
241
+ describe('complex nested layout', () => {
242
+ test('migrates complex layout', () => {
243
+ const result = migrateLayout({
244
+ type: 'Stack',
245
+ gap: 'lg',
246
+ children: [
247
+ { type: 'ComponentRef', ref: 'components/header' },
248
+ {
249
+ type: 'HStack',
250
+ gap: 'md',
251
+ children: [
252
+ { type: 'ComponentRef', ref: 'components/sidebar' },
253
+ {
254
+ type: 'Box',
255
+ padding: 'lg',
256
+ children: [{ type: 'Slot', name: 'main' }],
257
+ },
258
+ ],
259
+ },
260
+ { type: 'ComponentRef', ref: 'components/footer' },
261
+ ],
262
+ })
263
+ expect(result.success).toBe(true)
264
+ expect(result.errors).toHaveLength(0)
265
+ })
266
+ })
267
+
268
+ describe('error handling', () => {
269
+ test('rejects null layout', () => {
270
+ const result = migrateLayout(null)
271
+ expect(result.success).toBe(false)
272
+ expect(result.errors[0]).toContain('must be an object')
273
+ })
274
+
275
+ test('rejects non-object layout', () => {
276
+ const result = migrateLayout('invalid')
277
+ expect(result.success).toBe(false)
278
+ })
279
+
280
+ test('warns about unknown node types', () => {
281
+ const result = migrateLayout({
282
+ type: 'CustomComponent',
283
+ })
284
+ expect(result.success).toBe(true) // Still succeeds with warning
285
+ expect(result.warnings).toHaveLength(1)
286
+ expect(result.warnings[0]).toContain('Unknown node type')
287
+ })
288
+ })
289
+ })
290
+
291
+ describe('migrateScreenConfig', () => {
292
+ test('migrates react layout by default', () => {
293
+ const result = migrateScreenConfig({
294
+ react: {
295
+ type: 'ComponentRef',
296
+ ref: 'components/home',
297
+ },
298
+ html: {
299
+ type: 'ComponentRef',
300
+ ref: 'components/home-html',
301
+ },
302
+ })
303
+ expect(result.success).toBe(true)
304
+ expect(result.template?.root).toBe('components/home')
305
+ })
306
+
307
+ test('falls back to first renderer if no react', () => {
308
+ const result = migrateScreenConfig({
309
+ svelte: {
310
+ type: 'ComponentRef',
311
+ ref: 'components/home-svelte',
312
+ },
313
+ })
314
+ expect(result.success).toBe(true)
315
+ expect(result.template?.root).toBe('components/home-svelte')
316
+ })
317
+ })
@@ -0,0 +1,364 @@
1
+ // src/primitives/migrate.ts
2
+ // Migration tool to convert layoutByRenderer to template format
3
+
4
+ import type { Template, TemplateNode, Slots } from './types'
5
+
6
+ /**
7
+ * Legacy layout node types from layoutByRenderer
8
+ */
9
+ interface LegacyLayoutNode {
10
+ type: string
11
+ ref?: string
12
+ name?: string
13
+ props?: Record<string, unknown>
14
+ children?: LegacyLayoutNode[]
15
+ gap?: string
16
+ align?: string
17
+ padding?: string
18
+ direction?: string
19
+ default?: LegacyLayoutNode[]
20
+ [key: string]: unknown
21
+ }
22
+
23
+ /**
24
+ * Migration result
25
+ */
26
+ export interface MigrationResult {
27
+ success: boolean
28
+ template?: Template
29
+ slots?: Slots
30
+ errors: string[]
31
+ warnings: string[]
32
+ }
33
+
34
+ /**
35
+ * Migrate a layoutByRenderer tree to template format
36
+ *
37
+ * Converts:
38
+ * - Stack/HStack/VStack to $col/$row
39
+ * - Box/Container to $box
40
+ * - ComponentRef to component refs
41
+ * - Slot to $slot
42
+ */
43
+ export function migrateLayout(
44
+ layout: unknown,
45
+ _rendererName: string = 'react'
46
+ ): MigrationResult {
47
+ const errors: string[] = []
48
+ const warnings: string[] = []
49
+ const slots: Slots = {}
50
+
51
+ if (!layout || typeof layout !== 'object') {
52
+ return {
53
+ success: false,
54
+ errors: ['Layout must be an object or array'],
55
+ warnings: [],
56
+ }
57
+ }
58
+
59
+ try {
60
+ // Handle array at root level
61
+ const layoutNodes = Array.isArray(layout) ? layout : [layout]
62
+
63
+ // If single node, use it directly as root
64
+ // If multiple nodes, wrap in $col
65
+ let root: TemplateNode
66
+
67
+ if (layoutNodes.length === 1) {
68
+ root = migrateSingleNode(layoutNodes[0] as LegacyLayoutNode, 'root', slots, errors, warnings)
69
+ } else {
70
+ // Multiple root nodes - wrap in $col
71
+ const children: Record<string, TemplateNode> = {}
72
+ layoutNodes.forEach((node, index) => {
73
+ const nodeId = `child-${index}`
74
+ children[nodeId] = migrateSingleNode(node as LegacyLayoutNode, nodeId, slots, errors, warnings)
75
+ })
76
+
77
+ root = {
78
+ type: '$col',
79
+ children,
80
+ }
81
+ }
82
+
83
+ const template: Template = { root }
84
+
85
+ return {
86
+ success: errors.length === 0,
87
+ template,
88
+ slots: Object.keys(slots).length > 0 ? slots : undefined,
89
+ errors,
90
+ warnings,
91
+ }
92
+ } catch (err) {
93
+ return {
94
+ success: false,
95
+ errors: [`Migration failed: ${err instanceof Error ? err.message : String(err)}`],
96
+ warnings,
97
+ }
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Migrate a single layout node
103
+ */
104
+ function migrateSingleNode(
105
+ node: LegacyLayoutNode,
106
+ nodeId: string,
107
+ slots: Slots,
108
+ errors: string[],
109
+ warnings: string[]
110
+ ): TemplateNode {
111
+ if (!node || typeof node !== 'object') {
112
+ errors.push(`Invalid node at ${nodeId}`)
113
+ return `<!-- invalid: ${nodeId} -->`
114
+ }
115
+
116
+ const nodeType = node.type
117
+
118
+ // ComponentRef -> component ref string
119
+ if (nodeType === 'ComponentRef' && node.ref) {
120
+ if (node.props && Object.keys(node.props).length > 0) {
121
+ warnings.push(`ComponentRef at ${nodeId} has props that cannot be migrated to template format`)
122
+ }
123
+ return node.ref
124
+ }
125
+
126
+ // Slot -> $slot
127
+ if (nodeType === 'Slot' && node.name) {
128
+ // Track slot defaults for migration
129
+ if (node.default && Array.isArray(node.default) && node.default.length > 0) {
130
+ // Convert default content to slot mapping
131
+ const defaultContent = migrateSingleNode(node.default[0], `${nodeId}-default`, slots, errors, warnings)
132
+ if (typeof defaultContent === 'string') {
133
+ if (!slots[node.name]) {
134
+ slots[node.name] = {}
135
+ }
136
+ slots[node.name].default = defaultContent
137
+ } else {
138
+ warnings.push(`Slot "${node.name}" default content is complex; manual migration required`)
139
+ }
140
+ }
141
+ return `$slot(${node.name})`
142
+ }
143
+
144
+ // Stack/VStack -> $col
145
+ if (nodeType === 'Stack' || nodeType === 'VStack') {
146
+ const props = buildLayoutProps(node, false)
147
+ const primitive = props ? `$col(${props})` : '$col'
148
+
149
+ if (node.children && Array.isArray(node.children) && node.children.length > 0) {
150
+ const children: Record<string, TemplateNode> = {}
151
+ node.children.forEach((child, index) => {
152
+ const childId = `${nodeId}-${index}`
153
+ children[childId] = migrateSingleNode(child, childId, slots, errors, warnings)
154
+ })
155
+ return { type: primitive, children }
156
+ }
157
+
158
+ return primitive
159
+ }
160
+
161
+ // HStack -> $row
162
+ if (nodeType === 'HStack') {
163
+ const props = buildLayoutProps(node, true)
164
+ const primitive = props ? `$row(${props})` : '$row'
165
+
166
+ if (node.children && Array.isArray(node.children) && node.children.length > 0) {
167
+ const children: Record<string, TemplateNode> = {}
168
+ node.children.forEach((child, index) => {
169
+ const childId = `${nodeId}-${index}`
170
+ children[childId] = migrateSingleNode(child, childId, slots, errors, warnings)
171
+ })
172
+ return { type: primitive, children }
173
+ }
174
+
175
+ return primitive
176
+ }
177
+
178
+ // Box/Container -> $box
179
+ if (nodeType === 'Box' || nodeType === 'Container') {
180
+ const props = buildBoxProps(node)
181
+ const primitive = props ? `$box(${props})` : '$box'
182
+
183
+ if (node.children && Array.isArray(node.children) && node.children.length > 0) {
184
+ const children: Record<string, TemplateNode> = {}
185
+ node.children.forEach((child, index) => {
186
+ const childId = `${nodeId}-${index}`
187
+ children[childId] = migrateSingleNode(child, childId, slots, errors, warnings)
188
+ })
189
+ return { type: primitive, children }
190
+ }
191
+
192
+ return primitive
193
+ }
194
+
195
+ // Spacer -> $spacer
196
+ if (nodeType === 'Spacer') {
197
+ if (node.size) {
198
+ const tokenSize = mapToToken(node.size as string, 'spacing')
199
+ return tokenSize ? `$spacer(${tokenSize})` : '$spacer'
200
+ }
201
+ return '$spacer'
202
+ }
203
+
204
+ // Text -> $text
205
+ if (nodeType === 'Text') {
206
+ const props: string[] = []
207
+ if (node.content) {
208
+ props.push(String(node.content))
209
+ } else if (node.children && typeof node.children === 'string') {
210
+ props.push(`"${node.children}"`)
211
+ }
212
+ if (node.size) {
213
+ const tokenSize = mapToToken(node.size as string, 'size')
214
+ if (tokenSize) props.push(`size:${tokenSize}`)
215
+ }
216
+ if (node.weight) {
217
+ const tokenWeight = mapToToken(node.weight as string, 'weight')
218
+ if (tokenWeight) props.push(`weight:${tokenWeight}`)
219
+ }
220
+ return props.length > 0 ? `$text(${props.join(' ')})` : '$text(content)'
221
+ }
222
+
223
+ // Unknown type - warn and convert to comment
224
+ warnings.push(`Unknown node type "${nodeType}" at ${nodeId}; manual migration required`)
225
+ return `<!-- unknown: ${nodeType} -->`
226
+ }
227
+
228
+ /**
229
+ * Build layout props string for $col/$row
230
+ */
231
+ function buildLayoutProps(node: LegacyLayoutNode, _isRow: boolean): string {
232
+ const props: string[] = []
233
+
234
+ if (node.gap) {
235
+ const tokenGap = mapToToken(node.gap, 'spacing')
236
+ if (tokenGap) props.push(`gap:${tokenGap}`)
237
+ }
238
+
239
+ if (node.align) {
240
+ const tokenAlign = mapToToken(node.align, 'align')
241
+ if (tokenAlign) props.push(`align:${tokenAlign}`)
242
+ }
243
+
244
+ if (node.padding) {
245
+ const tokenPadding = mapToToken(node.padding, 'spacing')
246
+ if (tokenPadding) props.push(`padding:${tokenPadding}`)
247
+ }
248
+
249
+ return props.join(' ')
250
+ }
251
+
252
+ /**
253
+ * Build box props string for $box
254
+ */
255
+ function buildBoxProps(node: LegacyLayoutNode): string {
256
+ const props: string[] = []
257
+
258
+ if (node.padding) {
259
+ const tokenPadding = mapToToken(node.padding as string, 'spacing')
260
+ if (tokenPadding) props.push(`padding:${tokenPadding}`)
261
+ }
262
+
263
+ if (node.bg || node.background) {
264
+ const bg = (node.bg || node.background) as string
265
+ const tokenBg = mapToToken(bg, 'background')
266
+ if (tokenBg) props.push(`bg:${tokenBg}`)
267
+ }
268
+
269
+ if (node.radius || node.borderRadius) {
270
+ const radius = (node.radius || node.borderRadius) as string
271
+ const tokenRadius = mapToToken(radius, 'radius')
272
+ if (tokenRadius) props.push(`radius:${tokenRadius}`)
273
+ }
274
+
275
+ return props.join(' ')
276
+ }
277
+
278
+ /**
279
+ * Map CSS values to design tokens
280
+ */
281
+ function mapToToken(
282
+ value: string | number,
283
+ type: 'spacing' | 'align' | 'background' | 'radius' | 'size' | 'weight'
284
+ ): string | null {
285
+ const str = String(value).toLowerCase().trim()
286
+
287
+ // Already a token name
288
+ const tokenNames: Record<string, string[]> = {
289
+ spacing: ['none', 'xs', 'sm', 'md', 'lg', 'xl', '2xl'],
290
+ align: ['start', 'center', 'end', 'stretch', 'between'],
291
+ background: ['transparent', 'surface', 'muted', 'accent'],
292
+ radius: ['none', 'sm', 'md', 'lg', 'full'],
293
+ size: ['xs', 'sm', 'md', 'lg', 'xl'],
294
+ weight: ['normal', 'medium', 'bold'],
295
+ }
296
+
297
+ if (tokenNames[type]?.includes(str)) {
298
+ return str
299
+ }
300
+
301
+ // Map common CSS values to tokens
302
+ const spacingMap: Record<string, string> = {
303
+ '0': 'none',
304
+ '0px': 'none',
305
+ '4px': 'xs',
306
+ '0.25rem': 'xs',
307
+ '8px': 'sm',
308
+ '0.5rem': 'sm',
309
+ '16px': 'md',
310
+ '1rem': 'md',
311
+ '24px': 'lg',
312
+ '1.5rem': 'lg',
313
+ '32px': 'xl',
314
+ '2rem': 'xl',
315
+ '48px': '2xl',
316
+ '3rem': '2xl',
317
+ }
318
+
319
+ const alignMap: Record<string, string> = {
320
+ 'flex-start': 'start',
321
+ 'flex-end': 'end',
322
+ 'space-between': 'between',
323
+ }
324
+
325
+ const radiusMap: Record<string, string> = {
326
+ '0': 'none',
327
+ '4px': 'sm',
328
+ '0.25rem': 'sm',
329
+ '8px': 'md',
330
+ '0.5rem': 'md',
331
+ '16px': 'lg',
332
+ '1rem': 'lg',
333
+ '9999px': 'full',
334
+ '50%': 'full',
335
+ }
336
+
337
+ if (type === 'spacing' && spacingMap[str]) {
338
+ return spacingMap[str]
339
+ }
340
+
341
+ if (type === 'align' && alignMap[str]) {
342
+ return alignMap[str]
343
+ }
344
+
345
+ if (type === 'radius' && radiusMap[str]) {
346
+ return radiusMap[str]
347
+ }
348
+
349
+ // Can't map - return null
350
+ return null
351
+ }
352
+
353
+ /**
354
+ * Migrate all layouts from a screen config
355
+ */
356
+ export function migrateScreenConfig(
357
+ layoutByRenderer: Record<string, unknown>
358
+ ): MigrationResult {
359
+ // Prefer 'react' layout, fall back to first available
360
+ const rendererName = 'react' in layoutByRenderer ? 'react' : Object.keys(layoutByRenderer)[0]
361
+ const layout = layoutByRenderer[rendererName]
362
+
363
+ return migrateLayout(layout, rendererName)
364
+ }