prev-cli 0.24.15 → 0.24.18

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 (42) hide show
  1. package/dist/cli.d.ts +1 -1
  2. package/dist/cli.js +4466 -1681
  3. package/dist/jsx/adapters/html.d.ts +15 -0
  4. package/dist/jsx/adapters/react.d.ts +26 -0
  5. package/dist/jsx/define-component.d.ts +68 -0
  6. package/dist/jsx/index.d.ts +7 -0
  7. package/dist/jsx/jsx-runtime.d.ts +34 -0
  8. package/dist/jsx/migrate.d.ts +24 -0
  9. package/dist/jsx/schemas/index.d.ts +2 -0
  10. package/dist/jsx/schemas/primitives.d.ts +355 -0
  11. package/dist/jsx/schemas/tokens.d.ts +79 -0
  12. package/dist/jsx/validation.d.ts +28 -0
  13. package/dist/jsx/vnode.d.ts +58 -0
  14. package/dist/migrate.d.ts +18 -0
  15. package/dist/primitives/index.d.ts +5 -0
  16. package/dist/primitives/migrate.d.ts +25 -0
  17. package/dist/primitives/parser.d.ts +32 -0
  18. package/dist/primitives/template-parser.d.ts +33 -0
  19. package/dist/primitives/template-renderer.d.ts +19 -0
  20. package/dist/primitives/types.d.ts +164 -0
  21. package/dist/renderers/html/index.d.ts +6 -0
  22. package/dist/renderers/index.d.ts +5 -0
  23. package/dist/renderers/react/index.d.ts +6 -0
  24. package/dist/renderers/registry.d.ts +41 -0
  25. package/dist/renderers/render.d.ts +35 -0
  26. package/dist/renderers/types.d.ts +188 -0
  27. package/dist/tokens/defaults.d.ts +2 -0
  28. package/dist/tokens/resolver.d.ts +67 -0
  29. package/dist/tokens/utils.d.ts +15 -0
  30. package/dist/tokens/validation.d.ts +36 -0
  31. package/dist/typecheck/index.d.ts +24 -0
  32. package/dist/ui/button.d.ts +1 -1
  33. package/dist/validators/index.d.ts +59 -0
  34. package/dist/validators/schema-validator.d.ts +27 -0
  35. package/dist/validators/semantic-validator.d.ts +47 -0
  36. package/dist/vite/plugins/tokens-plugin.d.ts +2 -0
  37. package/package.json +8 -4
  38. package/src/preview-runtime/fast-template.html +115 -0
  39. package/src/preview-runtime/template.html +50 -8
  40. package/src/preview-runtime/vendors.ts +9 -0
  41. package/src/theme/entry.tsx +153 -12
  42. package/src/theme/previews/TokensPage.tsx +328 -0
@@ -15,8 +15,10 @@ import {
15
15
  import { MDXProvider } from '@mdx-js/react'
16
16
  import { pages, sidebar } from 'virtual:prev-pages'
17
17
  import { pageModules } from 'virtual:prev-page-modules'
18
- import { previews } from 'virtual:prev-previews'
18
+ import { previews, previewUnits } from 'virtual:prev-previews'
19
+ import type { PreviewUnit, PreviewType } from '../vite/preview-types'
19
20
  import { Preview } from './Preview'
21
+ import { TokensPage } from './previews/TokensPage'
20
22
  import { useDiagrams } from './diagrams'
21
23
  import { Layout } from './Layout'
22
24
  import { MetadataBlock } from './MetadataBlock'
@@ -91,9 +93,119 @@ function PageWrapper({ Component, meta }: { Component: React.ComponentType; meta
91
93
  )
92
94
  }
93
95
 
94
- // Previews catalog - Storybook-like gallery with clickable cards
96
+ // Category metadata for display
97
+ const CATEGORY_META: Record<PreviewType, { label: string; icon: string; description: string }> = {
98
+ component: {
99
+ label: 'Components',
100
+ icon: '⬡',
101
+ description: 'Reusable UI building blocks',
102
+ },
103
+ screen: {
104
+ label: 'Screens',
105
+ icon: '▣',
106
+ description: 'Full page layouts and views',
107
+ },
108
+ flow: {
109
+ label: 'Flows',
110
+ icon: '⇢',
111
+ description: 'Multi-step user journeys',
112
+ },
113
+ atlas: {
114
+ label: 'Atlas',
115
+ icon: '◎',
116
+ description: 'Information architecture maps',
117
+ },
118
+ }
119
+
120
+ // Category display order
121
+ const CATEGORY_ORDER: PreviewType[] = ['component', 'screen', 'flow', 'atlas']
122
+
123
+ // Group previews by type
124
+ function groupByType(units: PreviewUnit[]): Map<PreviewType, PreviewUnit[]> {
125
+ const grouped = new Map<PreviewType, PreviewUnit[]>()
126
+ for (const unit of units) {
127
+ const existing = grouped.get(unit.type) || []
128
+ grouped.set(unit.type, [...existing, unit])
129
+ }
130
+ return grouped
131
+ }
132
+
133
+ // Category section component
134
+ function CategorySection({ type, units }: { type: PreviewType; units: PreviewUnit[] }) {
135
+ const meta = CATEGORY_META[type]
136
+
137
+ return (
138
+ <section style={{ marginBottom: '32px' }}>
139
+ <div style={{
140
+ display: 'flex',
141
+ alignItems: 'center',
142
+ gap: '10px',
143
+ marginBottom: '16px',
144
+ paddingBottom: '12px',
145
+ borderBottom: '1px solid var(--fd-border)',
146
+ }}>
147
+ <span style={{ fontSize: '20px' }}>{meta.icon}</span>
148
+ <div>
149
+ <h2 style={{
150
+ fontSize: '18px',
151
+ fontWeight: '600',
152
+ margin: 0,
153
+ color: 'var(--fd-foreground)',
154
+ }}>
155
+ {meta.label}
156
+ <span style={{
157
+ marginLeft: '8px',
158
+ fontSize: '14px',
159
+ fontWeight: '400',
160
+ color: 'var(--fd-muted-foreground)',
161
+ }}>
162
+ ({units.length})
163
+ </span>
164
+ </h2>
165
+ <p style={{
166
+ fontSize: '13px',
167
+ color: 'var(--fd-muted-foreground)',
168
+ margin: 0,
169
+ }}>
170
+ {meta.description}
171
+ </p>
172
+ </div>
173
+ </div>
174
+ <div className="previews-grid">
175
+ {units.map((unit) => {
176
+ // Extract full path name from route (e.g., "/_preview/components/button" -> "components/button")
177
+ const fullName = unit.route.replace(/^\/_preview\//, '')
178
+ return (
179
+ <PreviewCard
180
+ key={fullName}
181
+ name={fullName}
182
+ title={unit.config?.title}
183
+ status={unit.config?.status}
184
+ />
185
+ )
186
+ })}
187
+ </div>
188
+ </section>
189
+ )
190
+ }
191
+
192
+ // Previews catalog - Storybook-like gallery with categorized sections
95
193
  function PreviewsCatalog() {
96
- if (!previews || previews.length === 0) {
194
+ // Use previewUnits for categorization, fall back to legacy previews
195
+ const units = previewUnits || []
196
+ const legacyPreviews = previews || []
197
+
198
+ // If no units but have legacy previews, convert them
199
+ const allUnits: PreviewUnit[] = units.length > 0 ? units : legacyPreviews.map((p: { name: string; route: string }) => ({
200
+ type: 'component' as PreviewType,
201
+ name: p.name,
202
+ path: '',
203
+ route: p.route,
204
+ config: null,
205
+ files: { index: '' },
206
+ }))
207
+
208
+ if (allUnits.length === 0) {
97
209
  return (
98
210
  <div style={{ padding: '40px 20px', textAlign: 'center' }}>
99
211
  <h1 style={{ fontSize: '24px', fontWeight: 'bold', marginBottom: '16px' }}>
@@ -115,24 +227,30 @@ function PreviewsCatalog() {
115
227
  )
116
228
  }
117
229
 
230
+ const grouped = groupByType(allUnits)
231
+ const totalCount = allUnits.length
232
+
118
233
  return (
119
234
  <div className="previews-catalog">
120
- <div style={{ marginBottom: '24px' }}>
235
+ {/* Header */}
236
+ <div style={{ marginBottom: '32px' }}>
121
237
  <h1 style={{ fontSize: '28px', fontWeight: 'bold', marginBottom: '8px' }}>
122
238
  Previews
123
239
  </h1>
124
240
  <p style={{ color: 'var(--fd-muted-foreground)', margin: 0 }}>
125
- {previews.length} component preview{previews.length !== 1 ? 's' : ''} available.
241
+ {totalCount} preview{totalCount !== 1 ? 's' : ''} across {grouped.size} categor{grouped.size !== 1 ? 'ies' : 'y'}.
126
242
  Click any preview to open it.
127
243
  </p>
128
244
  </div>
129
245
 
130
- <div className="previews-grid">
131
- {previews.map((preview: { name: string; route: string }) => (
132
- <PreviewCard key={preview.name} name={preview.name} />
133
- ))}
134
- </div>
246
+ {/* Categorized sections */}
247
+ {CATEGORY_ORDER.map((type) => {
248
+ const units = grouped.get(type)
249
+ if (!units || units.length === 0) return null
250
+ return <CategorySection key={type} type={type} units={units} />
251
+ })}
135
252
 
253
+ {/* Tip */}
136
254
  <div style={{
137
255
  marginTop: '32px',
138
256
  padding: '14px 16px',
@@ -154,7 +272,7 @@ function PreviewsCatalog() {
154
272
  // Individual preview card - clickable thumbnail with WASM preview communication
155
273
  import type { PreviewConfig, PreviewMessage } from '../preview-runtime/types'
156
274
 
157
- function PreviewCard({ name }: { name: string }) {
275
+ function PreviewCard({ name, title, status }: { name: string; title?: string; status?: 'draft' | 'stable' | 'deprecated' }) {
158
276
  const iframeRef = React.useRef<HTMLIFrameElement>(null)
159
277
  const [isLoaded, setIsLoaded] = React.useState(false)
160
278
  const [loadError, setLoadError] = React.useState(false)
@@ -255,7 +373,22 @@ function PreviewCard({ name }: { name: string }) {
255
373
  </div>
256
374
  {/* Card footer */}
257
375
  <div className="preview-card-footer">
258
- <h3 className="preview-card-title">{name}</h3>
376
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
377
+ <h3 className="preview-card-title">{title || name}</h3>
378
+ {status && status !== 'stable' && (
379
+ <span style={{
380
+ fontSize: '10px',
381
+ fontWeight: '500',
382
+ padding: '2px 6px',
383
+ borderRadius: '4px',
384
+ backgroundColor: status === 'draft' ? 'var(--fd-warning-background, #fef3c7)' : 'var(--fd-muted)',
385
+ color: status === 'draft' ? 'var(--fd-warning-foreground, #92400e)' : 'var(--fd-muted-foreground)',
386
+ textTransform: 'uppercase',
387
+ }}>
388
+ {status}
389
+ </span>
390
+ )}
391
+ </div>
259
392
  <code className="preview-card-path">previews/{name}/</code>
260
393
  </div>
261
394
  </Link>
@@ -329,6 +462,13 @@ const previewDetailRoute = createRoute({
329
462
  component: PreviewPage,
330
463
  })
331
464
 
465
+ // Tokens showcase route (design system tokens reference)
466
+ const tokensRoute = createRoute({
467
+ getParentRoute: () => rootRoute,
468
+ path: '/_prev/tokens',
469
+ component: TokensPage,
470
+ })
471
+
332
472
  // Check if we have an index page (route '/')
333
473
  const hasIndexPage = pages.some((page: { route: string }) => page.route === '/')
334
474
  const firstPage = pages[0] as { route: string; file: string; title?: string; description?: string; frontmatter?: Record<string, unknown> } | undefined
@@ -370,6 +510,7 @@ const previewsRouteWithChildren = previewsLayoutRoute.addChildren([
370
510
 
371
511
  const routeTree = rootRoute.addChildren([
372
512
  previewsRouteWithChildren,
513
+ tokensRoute,
373
514
  ...(indexRedirectRoute ? [indexRedirectRoute] : []),
374
515
  ...pageRoutes,
375
516
  ])
@@ -0,0 +1,328 @@
1
+ // src/theme/previews/TokensPage.tsx
2
+ // Visual showcase page for design tokens at /_prev/tokens
3
+ import React, { useState } from 'react'
4
+ import { tokens } from 'virtual:prev-tokens'
5
+
6
+ interface TokenRowProps {
7
+ name: string
8
+ value: string | number
9
+ preview?: React.ReactNode
10
+ category: string
11
+ }
12
+
13
+ function TokenRow({ name, value, preview, category }: TokenRowProps) {
14
+ const [copied, setCopied] = useState(false)
15
+
16
+ const copyToken = () => {
17
+ const prompt = `Use the ${category} token "${name}" (value: ${value})`
18
+ navigator.clipboard.writeText(prompt)
19
+ setCopied(true)
20
+ setTimeout(() => setCopied(false), 2000)
21
+ }
22
+
23
+ return (
24
+ <div
25
+ data-token={name}
26
+ style={{
27
+ display: 'flex',
28
+ alignItems: 'center',
29
+ gap: '16px',
30
+ padding: '12px 16px',
31
+ borderBottom: '1px solid var(--fd-border)',
32
+ }}
33
+ >
34
+ {/* Preview swatch */}
35
+ <div style={{ width: '48px', height: '48px', flexShrink: 0 }}>
36
+ {preview}
37
+ </div>
38
+
39
+ {/* Token info */}
40
+ <div style={{ flex: 1 }}>
41
+ <div style={{ fontWeight: 500, color: 'var(--fd-foreground)' }}>{name}</div>
42
+ <div data-value style={{ fontSize: '14px', color: 'var(--fd-muted-foreground)', fontFamily: 'monospace' }}>
43
+ {String(value)}
44
+ </div>
45
+ </div>
46
+
47
+ {/* Copy button */}
48
+ <button
49
+ data-copy
50
+ onClick={copyToken}
51
+ style={{
52
+ padding: '6px 12px',
53
+ fontSize: '12px',
54
+ border: '1px solid var(--fd-border)',
55
+ borderRadius: '4px',
56
+ background: copied ? 'var(--fd-accent)' : 'var(--fd-background)',
57
+ color: copied ? 'var(--fd-accent-foreground)' : 'var(--fd-foreground)',
58
+ cursor: 'pointer',
59
+ }}
60
+ >
61
+ {copied ? 'Copied!' : 'Copy'}
62
+ </button>
63
+ </div>
64
+ )
65
+ }
66
+
67
+ function ColorSwatch({ color }: { color: string }) {
68
+ return (
69
+ <div style={{
70
+ width: '48px',
71
+ height: '48px',
72
+ backgroundColor: color,
73
+ borderRadius: '6px',
74
+ border: '1px solid var(--fd-border)',
75
+ }} />
76
+ )
77
+ }
78
+
79
+ function SpacingSwatch({ size }: { size: string }) {
80
+ return (
81
+ <div style={{
82
+ display: 'flex',
83
+ alignItems: 'center',
84
+ justifyContent: 'center',
85
+ width: '48px',
86
+ height: '48px',
87
+ }}>
88
+ <div style={{
89
+ width: size,
90
+ height: size,
91
+ backgroundColor: 'var(--fd-primary)',
92
+ borderRadius: '2px',
93
+ }} />
94
+ </div>
95
+ )
96
+ }
97
+
98
+ function RadiusSwatch({ radius }: { radius: string }) {
99
+ return (
100
+ <div style={{
101
+ width: '48px',
102
+ height: '48px',
103
+ backgroundColor: 'var(--fd-primary)',
104
+ borderRadius: radius,
105
+ }} />
106
+ )
107
+ }
108
+
109
+ function ShadowSwatch({ shadow }: { shadow: string }) {
110
+ return (
111
+ <div style={{
112
+ width: '48px',
113
+ height: '48px',
114
+ backgroundColor: 'var(--fd-background)',
115
+ borderRadius: '6px',
116
+ boxShadow: shadow,
117
+ border: shadow === 'none' ? '1px solid var(--fd-border)' : 'none',
118
+ }} />
119
+ )
120
+ }
121
+
122
+ interface SectionProps {
123
+ title: string
124
+ children: React.ReactNode
125
+ }
126
+
127
+ function Section({ title, children }: SectionProps) {
128
+ return (
129
+ <section data-section={title.toLowerCase()} style={{ marginBottom: '32px' }}>
130
+ <h2 style={{
131
+ fontSize: '20px',
132
+ fontWeight: 600,
133
+ color: 'var(--fd-foreground)',
134
+ marginBottom: '16px',
135
+ paddingBottom: '8px',
136
+ borderBottom: '2px solid var(--fd-border)',
137
+ }}>
138
+ {title}
139
+ </h2>
140
+ <div style={{
141
+ border: '1px solid var(--fd-border)',
142
+ borderRadius: '8px',
143
+ overflow: 'hidden',
144
+ }}>
145
+ {children}
146
+ </div>
147
+ </section>
148
+ )
149
+ }
150
+
151
+ export function TokensPage() {
152
+ const [copyAllStatus, setCopyAllStatus] = useState<string>('Copy All as Prompt')
153
+
154
+ // Tokens are pre-resolved at build time via virtual:prev-tokens
155
+
156
+ const copyAllAsPrompt = () => {
157
+ const lines = [
158
+ '# Design Tokens',
159
+ '',
160
+ '## Colors',
161
+ ...Object.entries(tokens.colors).map(([name, value]) => `- ${name}: ${value}`),
162
+ '',
163
+ '## Backgrounds',
164
+ ...Object.entries(tokens.backgrounds).map(([name, value]) => `- ${name}: ${value}`),
165
+ '',
166
+ '## Spacing',
167
+ ...Object.entries(tokens.spacing).map(([name, value]) => `- ${name}: ${value}`),
168
+ '',
169
+ '## Typography Sizes',
170
+ ...Object.entries(tokens.typography.sizes).map(([name, value]) => `- ${name}: ${value}`),
171
+ '',
172
+ '## Typography Weights',
173
+ ...Object.entries(tokens.typography.weights).map(([name, value]) => `- ${name}: ${value}`),
174
+ '',
175
+ '## Border Radius',
176
+ ...Object.entries(tokens.radius).map(([name, value]) => `- ${name}: ${value}`),
177
+ '',
178
+ '## Shadows',
179
+ ...Object.entries(tokens.shadows).map(([name, value]) => `- ${name}: ${value}`),
180
+ ]
181
+
182
+ navigator.clipboard.writeText(lines.join('\n'))
183
+ setCopyAllStatus('Copied!')
184
+ setTimeout(() => setCopyAllStatus('Copy All as Prompt'), 2000)
185
+ }
186
+
187
+ return (
188
+ <div style={{
189
+ maxWidth: '800px',
190
+ margin: '0 auto',
191
+ padding: '32px 24px',
192
+ }}>
193
+ {/* Header */}
194
+ <div style={{
195
+ display: 'flex',
196
+ justifyContent: 'space-between',
197
+ alignItems: 'center',
198
+ marginBottom: '32px',
199
+ }}>
200
+ <h1 style={{
201
+ fontSize: '28px',
202
+ fontWeight: 700,
203
+ color: 'var(--fd-foreground)',
204
+ margin: 0,
205
+ }}>
206
+ Design Tokens
207
+ </h1>
208
+ <button
209
+ data-copy-all
210
+ onClick={copyAllAsPrompt}
211
+ style={{
212
+ padding: '10px 20px',
213
+ fontSize: '14px',
214
+ fontWeight: 500,
215
+ border: '1px solid var(--fd-primary)',
216
+ borderRadius: '6px',
217
+ background: 'var(--fd-primary)',
218
+ color: 'var(--fd-primary-foreground)',
219
+ cursor: 'pointer',
220
+ }}
221
+ >
222
+ {copyAllStatus}
223
+ </button>
224
+ </div>
225
+
226
+ {/* Colors Section */}
227
+ <Section title="Colors">
228
+ {Object.entries(tokens.colors as Record<string, string>).map(([name, value]) => (
229
+ <TokenRow
230
+ key={name}
231
+ name={name}
232
+ value={value}
233
+ category="color"
234
+ preview={<ColorSwatch color={value} />}
235
+ />
236
+ ))}
237
+ </Section>
238
+
239
+ {/* Backgrounds Section */}
240
+ <Section title="Backgrounds">
241
+ {Object.entries(tokens.backgrounds as Record<string, string>).map(([name, value]) => (
242
+ <TokenRow
243
+ key={name}
244
+ name={name}
245
+ value={value}
246
+ category="background"
247
+ preview={<ColorSwatch color={value} />}
248
+ />
249
+ ))}
250
+ </Section>
251
+
252
+ {/* Spacing Section */}
253
+ <Section title="Spacing">
254
+ {Object.entries(tokens.spacing as Record<string, string>).map(([name, value]) => (
255
+ <TokenRow
256
+ key={name}
257
+ name={name}
258
+ value={value}
259
+ category="spacing"
260
+ preview={<SpacingSwatch size={value} />}
261
+ />
262
+ ))}
263
+ </Section>
264
+
265
+ {/* Typography Section */}
266
+ <Section title="Typography">
267
+ <div style={{ padding: '12px 16px', backgroundColor: 'var(--fd-muted)', fontSize: '12px', fontWeight: 500, color: 'var(--fd-muted-foreground)' }}>
268
+ Sizes
269
+ </div>
270
+ {Object.entries(tokens.typography.sizes as Record<string, string>).map(([name, value]) => (
271
+ <TokenRow
272
+ key={name}
273
+ name={name}
274
+ value={value}
275
+ category="typography size"
276
+ preview={
277
+ <div style={{ fontSize: value, lineHeight: 1, color: 'var(--fd-foreground)' }}>
278
+ Aa
279
+ </div>
280
+ }
281
+ />
282
+ ))}
283
+ <div style={{ padding: '12px 16px', backgroundColor: 'var(--fd-muted)', fontSize: '12px', fontWeight: 500, color: 'var(--fd-muted-foreground)' }}>
284
+ Weights
285
+ </div>
286
+ {Object.entries(tokens.typography.weights as Record<string, number>).map(([name, value]) => (
287
+ <TokenRow
288
+ key={name}
289
+ name={name}
290
+ value={value}
291
+ category="typography weight"
292
+ preview={
293
+ <div style={{ fontWeight: value, fontSize: '24px', color: 'var(--fd-foreground)' }}>
294
+ Aa
295
+ </div>
296
+ }
297
+ />
298
+ ))}
299
+ </Section>
300
+
301
+ {/* Radius Section */}
302
+ <Section title="Radius">
303
+ {Object.entries(tokens.radius as Record<string, string>).map(([name, value]) => (
304
+ <TokenRow
305
+ key={name}
306
+ name={name}
307
+ value={value}
308
+ category="radius"
309
+ preview={<RadiusSwatch radius={value} />}
310
+ />
311
+ ))}
312
+ </Section>
313
+
314
+ {/* Shadows Section */}
315
+ <Section title="Shadows">
316
+ {Object.entries(tokens.shadows as Record<string, string>).map(([name, value]) => (
317
+ <TokenRow
318
+ key={name}
319
+ name={name}
320
+ value={value}
321
+ category="shadow"
322
+ preview={<ShadowSwatch shadow={value} />}
323
+ />
324
+ ))}
325
+ </Section>
326
+ </div>
327
+ )
328
+ }