prev-cli 0.24.20 → 0.25.0
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.
- package/dist/cli.js +2006 -1714
- package/dist/previews/components/cart-item/index.d.ts +5 -0
- package/dist/previews/components/price-tag/index.d.ts +6 -0
- package/dist/previews/screens/cart/empty.d.ts +1 -0
- package/dist/previews/screens/cart/index.d.ts +1 -0
- package/dist/previews/screens/payment/error.d.ts +1 -0
- package/dist/previews/screens/payment/index.d.ts +1 -0
- package/dist/previews/screens/payment/processing.d.ts +1 -0
- package/dist/previews/screens/receipt/index.d.ts +1 -0
- package/dist/previews/shared/data.d.ts +30 -0
- package/dist/src/content/config-parser.d.ts +30 -0
- package/dist/src/content/flow-verifier.d.ts +21 -0
- package/dist/src/content/preview-types.d.ts +288 -0
- package/dist/{vite → src/content}/previews.d.ts +3 -11
- package/dist/{preview-runtime → src/preview-runtime}/build-optimized.d.ts +2 -0
- package/dist/{preview-runtime → src/preview-runtime}/build.d.ts +1 -1
- package/dist/src/preview-runtime/region-bridge.d.ts +1 -0
- package/dist/{preview-runtime → src/preview-runtime}/types.d.ts +18 -0
- package/dist/src/preview-runtime/vendors.d.ts +11 -0
- package/dist/{renderers → src/renderers}/index.d.ts +1 -1
- package/dist/{renderers → src/renderers}/types.d.ts +3 -31
- package/dist/src/server/build.d.ts +6 -0
- package/dist/src/server/dev.d.ts +13 -0
- package/dist/src/server/plugins/aliases.d.ts +5 -0
- package/dist/src/server/plugins/mdx.d.ts +5 -0
- package/dist/src/server/plugins/virtual-modules.d.ts +8 -0
- package/dist/src/server/preview.d.ts +10 -0
- package/dist/src/server/routes/component-bundle.d.ts +1 -0
- package/dist/src/server/routes/jsx-bundle.d.ts +3 -0
- package/dist/src/server/routes/og-image.d.ts +15 -0
- package/dist/src/server/routes/preview-bundle.d.ts +1 -0
- package/dist/src/server/routes/preview-config.d.ts +1 -0
- package/dist/src/server/routes/tokens.d.ts +1 -0
- package/dist/{vite → src/server}/start.d.ts +5 -2
- package/dist/{ui → src/ui}/button.d.ts +1 -1
- package/dist/{validators → src/validators}/index.d.ts +0 -5
- package/dist/{validators → src/validators}/semantic-validator.d.ts +2 -3
- package/package.json +8 -11
- package/src/jsx/CLAUDE.md +18 -0
- package/src/jsx/jsx-runtime.ts +1 -1
- package/src/preview-runtime/CLAUDE.md +21 -0
- package/src/preview-runtime/build-optimized.ts +189 -73
- package/src/preview-runtime/build.ts +75 -79
- package/src/preview-runtime/fast-template.html +5 -1
- package/src/preview-runtime/region-bridge.test.ts +41 -0
- package/src/preview-runtime/region-bridge.ts +101 -0
- package/src/preview-runtime/types.ts +6 -0
- package/src/preview-runtime/vendors.ts +215 -22
- package/src/primitives/CLAUDE.md +17 -0
- package/src/theme/CLAUDE.md +20 -0
- package/src/theme/Preview.tsx +10 -4
- package/src/theme/Toolbar.tsx +2 -2
- package/src/theme/entry.tsx +247 -121
- package/src/theme/hooks/useAnnotations.ts +77 -0
- package/src/theme/hooks/useApprovalStatus.ts +50 -0
- package/src/theme/hooks/useSnapshots.ts +147 -0
- package/src/theme/hooks/useStorage.ts +26 -0
- package/src/theme/hooks/useTokenOverrides.ts +56 -0
- package/src/theme/hooks/useViewport.ts +23 -0
- package/src/theme/icons.tsx +39 -1
- package/src/theme/index.html +18 -0
- package/src/theme/mdx-components.tsx +1 -1
- package/src/theme/previews/AnnotationLayer.tsx +285 -0
- package/src/theme/previews/AnnotationPin.tsx +61 -0
- package/src/theme/previews/AnnotationThread.tsx +257 -0
- package/src/theme/previews/CLAUDE.md +18 -0
- package/src/theme/previews/ComponentPreview.tsx +487 -107
- package/src/theme/previews/FlowDiagram.tsx +111 -0
- package/src/theme/previews/FlowPreview.tsx +938 -174
- package/src/theme/previews/PreviewRouter.tsx +1 -4
- package/src/theme/previews/ScreenPreview.tsx +515 -175
- package/src/theme/previews/SnapshotButton.tsx +68 -0
- package/src/theme/previews/SnapshotCompare.tsx +216 -0
- package/src/theme/previews/SnapshotPanel.tsx +274 -0
- package/src/theme/previews/StatusBadge.tsx +66 -0
- package/src/theme/previews/StatusDropdown.tsx +158 -0
- package/src/theme/previews/TokenPlayground.tsx +438 -0
- package/src/theme/previews/ViewportControls.tsx +67 -0
- package/src/theme/previews/flow-diagram.test.ts +141 -0
- package/src/theme/previews/flow-diagram.ts +109 -0
- package/src/theme/previews/flow-navigation.test.ts +90 -0
- package/src/theme/previews/flow-navigation.ts +47 -0
- package/src/theme/previews/machines/derived.test.ts +225 -0
- package/src/theme/previews/machines/derived.ts +73 -0
- package/src/theme/previews/machines/flow-machine.test.ts +379 -0
- package/src/theme/previews/machines/flow-machine.ts +207 -0
- package/src/theme/previews/machines/screen-machine.test.ts +149 -0
- package/src/theme/previews/machines/screen-machine.ts +76 -0
- package/src/theme/previews/stores/flow-store.test.ts +157 -0
- package/src/theme/previews/stores/flow-store.ts +49 -0
- package/src/theme/previews/stores/screen-store.test.ts +68 -0
- package/src/theme/previews/stores/screen-store.ts +33 -0
- package/src/theme/storage.test.ts +97 -0
- package/src/theme/storage.ts +71 -0
- package/src/theme/styles.css +296 -25
- package/src/theme/types.ts +64 -0
- package/src/tokens/CLAUDE.md +16 -0
- package/src/tokens/resolver.ts +1 -1
- package/dist/preview-runtime/vendors.d.ts +0 -6
- package/dist/vite/config-parser.d.ts +0 -13
- package/dist/vite/config.d.ts +0 -12
- package/dist/vite/plugins/config-plugin.d.ts +0 -3
- package/dist/vite/plugins/debug-plugin.d.ts +0 -3
- package/dist/vite/plugins/entry-plugin.d.ts +0 -2
- package/dist/vite/plugins/fumadocs-plugin.d.ts +0 -9
- package/dist/vite/plugins/pages-plugin.d.ts +0 -5
- package/dist/vite/plugins/previews-plugin.d.ts +0 -2
- package/dist/vite/plugins/tokens-plugin.d.ts +0 -2
- package/dist/vite/preview-types.d.ts +0 -70
- package/src/theme/previews/AtlasPreview.tsx +0 -528
- package/dist/{cli.d.ts → src/cli.d.ts} +0 -0
- package/dist/{config → src/config}/index.d.ts +0 -0
- package/dist/{config → src/config}/loader.d.ts +0 -0
- package/dist/{config → src/config}/schema.d.ts +0 -0
- package/dist/{vite → src/content}/pages.d.ts +0 -0
- package/dist/{jsx → src/jsx}/adapters/html.d.ts +0 -0
- package/dist/{jsx → src/jsx}/adapters/react.d.ts +0 -0
- package/dist/{jsx → src/jsx}/define-component.d.ts +0 -0
- package/dist/{jsx → src/jsx}/index.d.ts +0 -0
- package/dist/{jsx → src/jsx}/jsx-runtime.d.ts +0 -0
- package/dist/{jsx → src/jsx}/migrate.d.ts +0 -0
- package/dist/{jsx → src/jsx}/schemas/index.d.ts +0 -0
- package/dist/{jsx → src/jsx}/schemas/primitives.d.ts +10 -10
- package/dist/{jsx → src/jsx}/schemas/tokens.d.ts +3 -3
- /package/dist/{jsx → src/jsx}/validation.d.ts +0 -0
- /package/dist/{jsx → src/jsx}/vnode.d.ts +0 -0
- /package/dist/{migrate.d.ts → src/migrate.d.ts} +0 -0
- /package/dist/{preview-runtime → src/preview-runtime}/tailwind.d.ts +0 -0
- /package/dist/{primitives → src/primitives}/index.d.ts +0 -0
- /package/dist/{primitives → src/primitives}/migrate.d.ts +0 -0
- /package/dist/{primitives → src/primitives}/parser.d.ts +0 -0
- /package/dist/{primitives → src/primitives}/template-parser.d.ts +0 -0
- /package/dist/{primitives → src/primitives}/template-renderer.d.ts +0 -0
- /package/dist/{primitives → src/primitives}/types.d.ts +0 -0
- /package/dist/{renderers → src/renderers}/html/index.d.ts +0 -0
- /package/dist/{renderers → src/renderers}/react/index.d.ts +0 -0
- /package/dist/{renderers → src/renderers}/registry.d.ts +0 -0
- /package/dist/{renderers → src/renderers}/render.d.ts +0 -0
- /package/dist/{tokens → src/tokens}/defaults.d.ts +0 -0
- /package/dist/{tokens → src/tokens}/resolver.d.ts +0 -0
- /package/dist/{tokens → src/tokens}/utils.d.ts +0 -0
- /package/dist/{tokens → src/tokens}/validation.d.ts +0 -0
- /package/dist/{typecheck → src/typecheck}/index.d.ts +0 -0
- /package/dist/{ui → src/ui}/card.d.ts +0 -0
- /package/dist/{ui → src/ui}/index.d.ts +0 -0
- /package/dist/{ui → src/ui}/utils.d.ts +0 -0
- /package/dist/{utils → src/utils}/cache.d.ts +0 -0
- /package/dist/{utils → src/utils}/debug.d.ts +0 -0
- /package/dist/{utils → src/utils}/port.d.ts +0 -0
- /package/dist/{validators → src/validators}/schema-validator.d.ts +0 -0
package/src/theme/Preview.tsx
CHANGED
|
@@ -10,6 +10,7 @@ interface PreviewProps {
|
|
|
10
10
|
title?: string
|
|
11
11
|
mode?: 'wasm' | 'legacy'
|
|
12
12
|
showHeader?: boolean // Show full header with back button and devtools
|
|
13
|
+
state?: string | null // Optional state name for screen previews
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
type DeviceMode = 'mobile' | 'tablet' | 'desktop'
|
|
@@ -20,7 +21,7 @@ const DEVICE_WIDTHS: Record<DeviceMode, number | '100%'> = {
|
|
|
20
21
|
desktop: '100%',
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
export function Preview({ src, height = 400, title, mode = 'wasm', showHeader = false }: PreviewProps) {
|
|
24
|
+
export function Preview({ src, height = 400, title, mode = 'wasm', showHeader = false, state = null }: PreviewProps) {
|
|
24
25
|
const [isFullscreen, setIsFullscreen] = useState(false)
|
|
25
26
|
// Default to 'mobile' device mode on mobile viewports to match user's actual environment
|
|
26
27
|
const [deviceMode, setDeviceMode] = useState<DeviceMode>(() => {
|
|
@@ -42,14 +43,19 @@ export function Preview({ src, height = 400, title, mode = 'wasm', showHeader =
|
|
|
42
43
|
|
|
43
44
|
// In production, always use pre-built static previews
|
|
44
45
|
// In dev, use WASM runtime for live bundling
|
|
45
|
-
const isDev = import.meta.env
|
|
46
|
+
const isDev = import.meta.env.DEV ?? false
|
|
46
47
|
const effectiveMode = isDev ? mode : 'legacy'
|
|
47
48
|
|
|
48
49
|
// Get base URL for proper subpath deployment support
|
|
49
|
-
const baseUrl = (import.meta.env
|
|
50
|
+
const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/$/, '')
|
|
50
51
|
|
|
51
52
|
// URL depends on mode - wasm mode needs src param, legacy uses pre-built files
|
|
52
|
-
|
|
53
|
+
// Include state parameter if provided (for screen previews with multiple states)
|
|
54
|
+
const stateParam = state ? `&state=${state}` : ''
|
|
55
|
+
const stateUrlPart = state ? `?state=${state}` : ''
|
|
56
|
+
const previewUrl = effectiveMode === 'wasm'
|
|
57
|
+
? `/_preview-runtime?src=${src}${stateParam}`
|
|
58
|
+
: `${baseUrl}/_preview/${src}/${stateUrlPart}`
|
|
53
59
|
const displayTitle = title || src
|
|
54
60
|
|
|
55
61
|
// Calculate current width
|
package/src/theme/Toolbar.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useState, useRef, useEffect } from 'react'
|
|
2
2
|
import { Link, useLocation } from '@tanstack/react-router'
|
|
3
3
|
import type { PageTree } from 'fumadocs-core/server'
|
|
4
|
-
import {
|
|
4
|
+
import { previewUnits } from 'virtual:prev-previews'
|
|
5
5
|
import { Icon } from './icons'
|
|
6
6
|
import { useDevTools } from './DevToolsContext'
|
|
7
7
|
import './Toolbar.css'
|
|
@@ -76,7 +76,7 @@ export function Toolbar({ tree, onThemeToggle, onWidthToggle, isDark, isFullWidt
|
|
|
76
76
|
<Icon name="menu" size={18} />
|
|
77
77
|
</button>
|
|
78
78
|
|
|
79
|
-
{
|
|
79
|
+
{previewUnits && previewUnits.length > 0 && (
|
|
80
80
|
<Link to="/previews" className={`toolbar-btn ${isOnPreviews ? 'active' : ''}`} title="Previews">
|
|
81
81
|
<Icon name="grid" size={18} />
|
|
82
82
|
</Link>
|
package/src/theme/entry.tsx
CHANGED
|
@@ -15,8 +15,8 @@ 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 {
|
|
19
|
-
import type { PreviewUnit, PreviewType } from '../
|
|
18
|
+
import { previewUnits } from 'virtual:prev-previews'
|
|
19
|
+
import type { PreviewUnit, PreviewType } from '../content/preview-types'
|
|
20
20
|
import { Preview } from './Preview'
|
|
21
21
|
import { TokensPage } from './previews/TokensPage'
|
|
22
22
|
import { useDiagrams } from './diagrams'
|
|
@@ -24,6 +24,9 @@ import { Layout } from './Layout'
|
|
|
24
24
|
import { MetadataBlock } from './MetadataBlock'
|
|
25
25
|
import { mdxComponents } from './mdx-components'
|
|
26
26
|
import { DevToolsProvider } from './DevToolsContext'
|
|
27
|
+
import { StatusBadge } from './previews/StatusBadge'
|
|
28
|
+
import { useApprovalStatus } from './hooks/useApprovalStatus'
|
|
29
|
+
import { SnapshotCompare } from './previews/SnapshotCompare'
|
|
27
30
|
import './styles.css'
|
|
28
31
|
|
|
29
32
|
// PageTree types (simplified from fumadocs-core)
|
|
@@ -110,15 +113,10 @@ const CATEGORY_META: Record<PreviewType, { label: string; icon: string; descript
|
|
|
110
113
|
icon: '⇢',
|
|
111
114
|
description: 'Multi-step user journeys',
|
|
112
115
|
},
|
|
113
|
-
atlas: {
|
|
114
|
-
label: 'Atlas',
|
|
115
|
-
icon: '◎',
|
|
116
|
-
description: 'Information architecture maps',
|
|
117
|
-
},
|
|
118
116
|
}
|
|
119
117
|
|
|
120
118
|
// Category display order
|
|
121
|
-
const CATEGORY_ORDER: PreviewType[] = ['component', 'screen', 'flow'
|
|
119
|
+
const CATEGORY_ORDER: PreviewType[] = ['component', 'screen', 'flow']
|
|
122
120
|
|
|
123
121
|
// Group previews by type
|
|
124
122
|
function groupByType(units: PreviewUnit[]): Map<PreviewType, PreviewUnit[]> {
|
|
@@ -130,50 +128,55 @@ function groupByType(units: PreviewUnit[]): Map<PreviewType, PreviewUnit[]> {
|
|
|
130
128
|
return grouped
|
|
131
129
|
}
|
|
132
130
|
|
|
133
|
-
// Category section component
|
|
131
|
+
// Category section component with carousel navigation
|
|
134
132
|
function CategorySection({ type, units }: { type: PreviewType; units: PreviewUnit[] }) {
|
|
135
133
|
const meta = CATEGORY_META[type]
|
|
134
|
+
const scrollRef = React.useRef<HTMLDivElement>(null)
|
|
135
|
+
|
|
136
|
+
const scroll = (direction: 'left' | 'right') => {
|
|
137
|
+
const container = scrollRef.current
|
|
138
|
+
if (!container) return
|
|
139
|
+
const cardWidth = 280 + 16 // card width + gap
|
|
140
|
+
const scrollAmount = direction === 'left' ? -cardWidth : cardWidth
|
|
141
|
+
container.scrollBy({ left: scrollAmount, behavior: 'smooth' })
|
|
142
|
+
}
|
|
136
143
|
|
|
137
144
|
return (
|
|
138
|
-
<section
|
|
139
|
-
<div
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
}}>
|
|
145
|
+
<section className="category-section">
|
|
146
|
+
<div className="category-header">
|
|
147
|
+
<span className="category-icon">{meta.icon}</span>
|
|
148
|
+
<div className="category-info">
|
|
149
|
+
<h2 className="category-title">
|
|
155
150
|
{meta.label}
|
|
156
|
-
<span
|
|
157
|
-
marginLeft: '8px',
|
|
158
|
-
fontSize: '14px',
|
|
159
|
-
fontWeight: '400',
|
|
160
|
-
color: 'var(--fd-muted-foreground)',
|
|
161
|
-
}}>
|
|
162
|
-
({units.length})
|
|
163
|
-
</span>
|
|
151
|
+
<span className="category-count">({units.length})</span>
|
|
164
152
|
</h2>
|
|
165
|
-
<p
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
153
|
+
<p className="category-description">{meta.description}</p>
|
|
154
|
+
</div>
|
|
155
|
+
<div className="category-nav">
|
|
156
|
+
<button
|
|
157
|
+
type="button"
|
|
158
|
+
className="category-nav-btn"
|
|
159
|
+
onClick={() => scroll('left')}
|
|
160
|
+
aria-label="Previous"
|
|
161
|
+
>
|
|
162
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
163
|
+
<path d="M15 18l-6-6 6-6" />
|
|
164
|
+
</svg>
|
|
165
|
+
</button>
|
|
166
|
+
<button
|
|
167
|
+
type="button"
|
|
168
|
+
className="category-nav-btn"
|
|
169
|
+
onClick={() => scroll('right')}
|
|
170
|
+
aria-label="Next"
|
|
171
|
+
>
|
|
172
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
|
173
|
+
<path d="M9 18l6-6-6-6" />
|
|
174
|
+
</svg>
|
|
175
|
+
</button>
|
|
172
176
|
</div>
|
|
173
177
|
</div>
|
|
174
|
-
<div className="previews-grid">
|
|
178
|
+
<div className="previews-grid" ref={scrollRef}>
|
|
175
179
|
{units.map((unit) => {
|
|
176
|
-
// Extract full path name from route (e.g., "/_preview/components/button" -> "components/button")
|
|
177
180
|
const fullName = unit.route.replace(/^\/_preview\//, '')
|
|
178
181
|
return (
|
|
179
182
|
<PreviewCard
|
|
@@ -181,6 +184,7 @@ function CategorySection({ type, units }: { type: PreviewType; units: PreviewUni
|
|
|
181
184
|
name={fullName}
|
|
182
185
|
title={unit.config?.title}
|
|
183
186
|
status={unit.config?.status}
|
|
187
|
+
type={unit.type}
|
|
184
188
|
/>
|
|
185
189
|
)
|
|
186
190
|
})}
|
|
@@ -191,21 +195,9 @@ function CategorySection({ type, units }: { type: PreviewType; units: PreviewUni
|
|
|
191
195
|
|
|
192
196
|
// Previews catalog - Storybook-like gallery with categorized sections
|
|
193
197
|
function PreviewsCatalog() {
|
|
194
|
-
// Use previewUnits for categorization, fall back to legacy previews
|
|
195
198
|
const units = previewUnits || []
|
|
196
|
-
|
|
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) {
|
|
199
|
+
|
|
200
|
+
if (units.length === 0) {
|
|
209
201
|
return (
|
|
210
202
|
<div style={{ padding: '40px 20px', textAlign: 'center' }}>
|
|
211
203
|
<h1 style={{ fontSize: '24px', fontWeight: 'bold', marginBottom: '16px' }}>
|
|
@@ -227,19 +219,16 @@ function PreviewsCatalog() {
|
|
|
227
219
|
)
|
|
228
220
|
}
|
|
229
221
|
|
|
230
|
-
const grouped = groupByType(
|
|
231
|
-
const totalCount =
|
|
222
|
+
const grouped = groupByType(units)
|
|
223
|
+
const totalCount = units.length
|
|
232
224
|
|
|
233
225
|
return (
|
|
234
226
|
<div className="previews-catalog">
|
|
235
227
|
{/* Header */}
|
|
236
|
-
<div
|
|
237
|
-
<h1
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
<p style={{ color: 'var(--fd-muted-foreground)', margin: 0 }}>
|
|
241
|
-
{totalCount} preview{totalCount !== 1 ? 's' : ''} across {grouped.size} categor{grouped.size !== 1 ? 'ies' : 'y'}.
|
|
242
|
-
Click any preview to open it.
|
|
228
|
+
<div className="catalog-header">
|
|
229
|
+
<h1 className="catalog-title">Previews</h1>
|
|
230
|
+
<p className="catalog-subtitle">
|
|
231
|
+
{totalCount} preview{totalCount !== 1 ? 's' : ''} across {grouped.size} categor{grouped.size !== 1 ? 'ies' : 'y'}
|
|
243
232
|
</p>
|
|
244
233
|
</div>
|
|
245
234
|
|
|
@@ -250,19 +239,11 @@ function PreviewsCatalog() {
|
|
|
250
239
|
return <CategorySection key={type} type={type} units={units} />
|
|
251
240
|
})}
|
|
252
241
|
|
|
253
|
-
{/* Tip */}
|
|
254
|
-
<div
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
border: '1px solid var(--fd-border)',
|
|
259
|
-
borderRadius: '10px',
|
|
260
|
-
}}>
|
|
261
|
-
<p style={{ margin: 0, fontSize: '14px', color: 'var(--fd-muted-foreground)' }}>
|
|
262
|
-
<strong style={{ color: 'var(--fd-foreground)' }}>Tip:</strong> Embed any preview in your MDX docs with{' '}
|
|
263
|
-
<code style={{ backgroundColor: 'var(--fd-accent)', padding: '2px 6px', borderRadius: '4px', fontFamily: 'var(--fd-font-mono)' }}>
|
|
264
|
-
{'<Preview src="name" />'}
|
|
265
|
-
</code>
|
|
242
|
+
{/* Tip - hidden on mobile */}
|
|
243
|
+
<div className="catalog-tip">
|
|
244
|
+
<p>
|
|
245
|
+
<strong>Tip:</strong> Embed any preview in your MDX docs with{' '}
|
|
246
|
+
<code>{'<Preview src="name" />'}</code>
|
|
266
247
|
</p>
|
|
267
248
|
</div>
|
|
268
249
|
</div>
|
|
@@ -272,38 +253,36 @@ function PreviewsCatalog() {
|
|
|
272
253
|
// Individual preview card - clickable thumbnail with WASM preview communication
|
|
273
254
|
import type { PreviewConfig, PreviewMessage } from '../preview-runtime/types'
|
|
274
255
|
|
|
275
|
-
function PreviewCard({ name, title, status }: { name: string; title?: string; status?: 'draft' | 'stable' | 'deprecated' }) {
|
|
256
|
+
function PreviewCard({ name, title, status, type }: { name: string; title?: string; status?: 'draft' | 'stable' | 'deprecated'; type?: PreviewType }) {
|
|
257
|
+
const { status: approvalStatus } = useApprovalStatus(name)
|
|
258
|
+
// Config-only types (flow) have no JS entry point — show a styled placeholder instead of iframe
|
|
259
|
+
const isConfigOnly = type === 'flow'
|
|
260
|
+
|
|
276
261
|
const iframeRef = React.useRef<HTMLIFrameElement>(null)
|
|
277
|
-
|
|
262
|
+
// In production, start as loaded since static files are fast
|
|
263
|
+
const isDev = import.meta.env.DEV ?? false
|
|
264
|
+
const [isLoaded, setIsLoaded] = React.useState(!isDev || isConfigOnly)
|
|
278
265
|
const [loadError, setLoadError] = React.useState(false)
|
|
279
266
|
|
|
280
|
-
|
|
281
|
-
const
|
|
282
|
-
const baseUrl = (import.meta.env?.BASE_URL ?? '/').replace(/\/$/, '')
|
|
283
|
-
const previewUrl = isDev ? `/_preview-runtime?src=${name}` : `${baseUrl}/_preview/${name}/`
|
|
267
|
+
const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/$/, '')
|
|
268
|
+
const previewUrl = isConfigOnly ? '' : (isDev ? `/_preview-runtime?src=${name}` : `${baseUrl}/_preview/${name}/`)
|
|
284
269
|
|
|
285
|
-
// Timeout for loading -
|
|
270
|
+
// Timeout for loading - only needed in dev mode
|
|
286
271
|
React.useEffect(() => {
|
|
272
|
+
if (!isDev || isConfigOnly) return
|
|
273
|
+
|
|
287
274
|
const timeout = setTimeout(() => {
|
|
288
275
|
if (!isLoaded) {
|
|
289
276
|
setLoadError(true)
|
|
290
277
|
}
|
|
291
|
-
}, 5000)
|
|
278
|
+
}, 5000)
|
|
292
279
|
|
|
293
280
|
return () => clearTimeout(timeout)
|
|
294
|
-
}, [isLoaded])
|
|
281
|
+
}, [isLoaded, isDev, isConfigOnly])
|
|
295
282
|
|
|
296
283
|
// Set up WASM preview communication for thumbnail (dev mode only)
|
|
297
284
|
React.useEffect(() => {
|
|
298
|
-
if (!isDev)
|
|
299
|
-
// In production, just mark as loaded when iframe loads
|
|
300
|
-
const iframe = iframeRef.current
|
|
301
|
-
if (iframe) {
|
|
302
|
-
iframe.onload = () => setIsLoaded(true)
|
|
303
|
-
iframe.onerror = () => setLoadError(true)
|
|
304
|
-
}
|
|
305
|
-
return
|
|
306
|
-
}
|
|
285
|
+
if (!isDev || isConfigOnly) return
|
|
307
286
|
|
|
308
287
|
const iframe = iframeRef.current
|
|
309
288
|
if (!iframe) return
|
|
@@ -346,35 +325,46 @@ function PreviewCard({ name, title, status }: { name: string; title?: string; st
|
|
|
346
325
|
<Link to={`/previews/${name}`} className="preview-card">
|
|
347
326
|
{/* Thumbnail preview */}
|
|
348
327
|
<div className="preview-card-thumbnail">
|
|
349
|
-
{
|
|
350
|
-
|
|
351
|
-
<div className="preview-card-
|
|
352
|
-
<
|
|
353
|
-
|
|
354
|
-
)}
|
|
355
|
-
{/* Error/timeout placeholder */}
|
|
356
|
-
{loadError && (
|
|
357
|
-
<div className="preview-card-placeholder">
|
|
358
|
-
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
359
|
-
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
360
|
-
<path d="M9 9l6 6m0-6l-6 6" />
|
|
361
|
-
</svg>
|
|
362
|
-
<span>Preview</span>
|
|
328
|
+
{isConfigOnly ? (
|
|
329
|
+
/* Config-only types: styled placeholder with category icon */
|
|
330
|
+
<div className="preview-card-placeholder" style={{ opacity: 1 }}>
|
|
331
|
+
<span style={{ fontSize: '32px' }}>⇢</span>
|
|
332
|
+
<span>Flow</span>
|
|
363
333
|
</div>
|
|
334
|
+
) : (
|
|
335
|
+
<>
|
|
336
|
+
{/* Loading state */}
|
|
337
|
+
{!isLoaded && !loadError && (
|
|
338
|
+
<div className="preview-card-loading">
|
|
339
|
+
<div className="preview-card-spinner" />
|
|
340
|
+
</div>
|
|
341
|
+
)}
|
|
342
|
+
{/* Error/timeout placeholder */}
|
|
343
|
+
{loadError && (
|
|
344
|
+
<div className="preview-card-placeholder">
|
|
345
|
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
346
|
+
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
347
|
+
<path d="M9 9l6 6m0-6l-6 6" />
|
|
348
|
+
</svg>
|
|
349
|
+
<span>Preview</span>
|
|
350
|
+
</div>
|
|
351
|
+
)}
|
|
352
|
+
<iframe
|
|
353
|
+
ref={iframeRef}
|
|
354
|
+
src={previewUrl}
|
|
355
|
+
className="preview-card-iframe"
|
|
356
|
+
style={{ opacity: isLoaded && !loadError ? 1 : 0 }}
|
|
357
|
+
title={name}
|
|
358
|
+
loading="lazy"
|
|
359
|
+
/>
|
|
360
|
+
</>
|
|
364
361
|
)}
|
|
365
|
-
<iframe
|
|
366
|
-
ref={iframeRef}
|
|
367
|
-
src={previewUrl}
|
|
368
|
-
className="preview-card-iframe"
|
|
369
|
-
style={{ opacity: isLoaded && !loadError ? 1 : 0 }}
|
|
370
|
-
title={name}
|
|
371
|
-
loading="lazy"
|
|
372
|
-
/>
|
|
373
362
|
</div>
|
|
374
363
|
{/* Card footer */}
|
|
375
364
|
<div className="preview-card-footer">
|
|
376
365
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
|
377
366
|
<h3 className="preview-card-title">{title || name}</h3>
|
|
367
|
+
<StatusBadge status={approvalStatus} compact />
|
|
378
368
|
{status && status !== 'stable' && (
|
|
379
369
|
<span style={{
|
|
380
370
|
fontSize: '10px',
|
|
@@ -396,18 +386,132 @@ function PreviewCard({ name, title, status }: { name: string; title?: string; st
|
|
|
396
386
|
}
|
|
397
387
|
|
|
398
388
|
// Individual preview page - full view with devtools in toolbar
|
|
389
|
+
// Uses specialized component for flow, generic Preview for components/screens
|
|
390
|
+
import { FlowPreview } from './previews/FlowPreview'
|
|
391
|
+
|
|
392
|
+
// Standalone preview embed (for iframe thumbnails in production)
|
|
393
|
+
// Renders just the preview content without Layout wrapper
|
|
394
|
+
function PreviewEmbed() {
|
|
395
|
+
const params = useParams({ strict: false })
|
|
396
|
+
const name = (params as any)['_splat'] || (params as any)['*'] || ''
|
|
397
|
+
|
|
398
|
+
// In production, always use pre-built static files
|
|
399
|
+
const isDev = import.meta.env.DEV ?? false
|
|
400
|
+
const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/$/, '')
|
|
401
|
+
|
|
402
|
+
// Parse type from name (e.g., "flows/checkout" -> type="flow", unitName="checkout")
|
|
403
|
+
const match = name.match(/^(components|screens|flows)\/(.+)$/)
|
|
404
|
+
if (!match) {
|
|
405
|
+
// Fallback for legacy preview paths without type prefix
|
|
406
|
+
const previewUrl = isDev ? `/_preview-runtime?src=${name}` : `${baseUrl}/_preview/${name}/`
|
|
407
|
+
return (
|
|
408
|
+
<iframe
|
|
409
|
+
src={previewUrl}
|
|
410
|
+
style={{ width: '100%', height: '100vh', border: 'none' }}
|
|
411
|
+
title={name}
|
|
412
|
+
/>
|
|
413
|
+
)
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const [, typeFolder, unitName] = match
|
|
417
|
+
const type = typeFolder === 'flows' ? 'flow' : typeFolder.slice(0, -1)
|
|
418
|
+
|
|
419
|
+
// Find the preview unit
|
|
420
|
+
const unit = previewUnits.find(u => u.type === type && u.name === unitName)
|
|
421
|
+
|
|
422
|
+
if (!unit) {
|
|
423
|
+
return (
|
|
424
|
+
<div style={{ padding: '32px', textAlign: 'center', color: '#888' }}>
|
|
425
|
+
Preview not found: {name}
|
|
426
|
+
</div>
|
|
427
|
+
)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// For flows, render the specialized component
|
|
431
|
+
if (unit.type === 'flow') {
|
|
432
|
+
return <FlowPreview unit={unit} />
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// For components and screens, use iframe to load static HTML
|
|
436
|
+
const previewUrl = isDev ? `/_preview-runtime?src=${name}` : `${baseUrl}/_preview/${name}/`
|
|
437
|
+
return (
|
|
438
|
+
<iframe
|
|
439
|
+
src={previewUrl}
|
|
440
|
+
style={{ width: '100%', height: '100vh', border: 'none' }}
|
|
441
|
+
title={name}
|
|
442
|
+
/>
|
|
443
|
+
)
|
|
444
|
+
}
|
|
445
|
+
|
|
399
446
|
function PreviewPage() {
|
|
400
447
|
const params = useParams({ strict: false })
|
|
448
|
+
const location = useLocation()
|
|
401
449
|
// Splat param captures the full path after /previews/
|
|
402
450
|
const name = (params as any)['_splat'] || (params as any)['*'] || params.name as string
|
|
403
451
|
|
|
452
|
+
// Read deep link params from URL search
|
|
453
|
+
const searchParams = new URLSearchParams(location.search)
|
|
454
|
+
const urlState = searchParams.get('state')
|
|
455
|
+
const urlStep = searchParams.get('step')
|
|
456
|
+
|
|
457
|
+
const [selectedState, setSelectedState] = React.useState<string | null>(urlState)
|
|
458
|
+
|
|
404
459
|
if (!name) {
|
|
405
460
|
return <Navigate to="/previews" />
|
|
406
461
|
}
|
|
407
462
|
|
|
463
|
+
// Find the preview unit to determine the type
|
|
464
|
+
const unit = previewUnits.find(u => {
|
|
465
|
+
// Match by route suffix (e.g., "flows/onboarding" matches route "/_preview/flows/onboarding")
|
|
466
|
+
return u.route.endsWith(`/${name}`) || `${u.type}s/${u.name}` === name
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
// For flows, use the specialized component
|
|
470
|
+
if (unit?.type === 'flow') {
|
|
471
|
+
return (
|
|
472
|
+
<div className="preview-detail-page">
|
|
473
|
+
<FlowPreview unit={unit} initialStep={urlStep || undefined} />
|
|
474
|
+
</div>
|
|
475
|
+
)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Get states for screens
|
|
479
|
+
const states = unit?.files?.states || []
|
|
480
|
+
const hasStates = states.length > 0
|
|
481
|
+
|
|
482
|
+
// For components and screens, use the generic Preview with iframe
|
|
408
483
|
return (
|
|
409
484
|
<div className="preview-detail-page">
|
|
410
|
-
|
|
485
|
+
{/* State selector for screens with multiple states */}
|
|
486
|
+
{hasStates && (
|
|
487
|
+
<div className="preview-state-selector">
|
|
488
|
+
<span className="preview-state-label">State:</span>
|
|
489
|
+
<div className="preview-state-buttons">
|
|
490
|
+
<button
|
|
491
|
+
type="button"
|
|
492
|
+
className={`preview-state-btn ${selectedState === null ? 'active' : ''}`}
|
|
493
|
+
onClick={() => setSelectedState(null)}
|
|
494
|
+
>
|
|
495
|
+
Default
|
|
496
|
+
</button>
|
|
497
|
+
{states.map((stateFile: string) => {
|
|
498
|
+
const stateName = stateFile.replace(/\.(tsx|jsx)$/, '')
|
|
499
|
+
const isSelected = selectedState === stateName
|
|
500
|
+
return (
|
|
501
|
+
<button
|
|
502
|
+
key={stateFile}
|
|
503
|
+
type="button"
|
|
504
|
+
className={`preview-state-btn ${isSelected ? 'active' : ''}`}
|
|
505
|
+
onClick={() => setSelectedState(stateName)}
|
|
506
|
+
>
|
|
507
|
+
{stateName.replace(/[-_]/g, ' ')}
|
|
508
|
+
</button>
|
|
509
|
+
)
|
|
510
|
+
})}
|
|
511
|
+
</div>
|
|
512
|
+
</div>
|
|
513
|
+
)}
|
|
514
|
+
<Preview src={name} state={selectedState} height="100%" showHeader />
|
|
411
515
|
</div>
|
|
412
516
|
)
|
|
413
517
|
}
|
|
@@ -469,6 +573,14 @@ const tokensRoute = createRoute({
|
|
|
469
573
|
component: TokensPage,
|
|
470
574
|
})
|
|
471
575
|
|
|
576
|
+
// Standalone preview embed route (handles /_preview/flows/checkout, /_preview/atlas/app, etc.)
|
|
577
|
+
// This is used when thumbnails load as iframes - the 404.html serves the SPA which matches this route
|
|
578
|
+
const previewEmbedRoute = createRoute({
|
|
579
|
+
getParentRoute: () => rootRoute,
|
|
580
|
+
path: '/_preview/$',
|
|
581
|
+
component: PreviewEmbed,
|
|
582
|
+
})
|
|
583
|
+
|
|
472
584
|
// Check if we have an index page (route '/')
|
|
473
585
|
const hasIndexPage = pages.some((page: { route: string }) => page.route === '/')
|
|
474
586
|
const firstPage = pages[0] as { route: string; file: string; title?: string; description?: string; frontmatter?: Record<string, unknown> } | undefined
|
|
@@ -502,20 +614,34 @@ function NotFoundPage() {
|
|
|
502
614
|
}
|
|
503
615
|
|
|
504
616
|
// Create router with notFoundRoute
|
|
617
|
+
// Snapshot compare route
|
|
618
|
+
function ComparePageComponent() {
|
|
619
|
+
const params = new URLSearchParams(window.location.search)
|
|
620
|
+
return <SnapshotCompare leftId={params.get('left') || undefined} rightId={params.get('right') || undefined} />
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const compareRoute = createRoute({
|
|
624
|
+
getParentRoute: () => previewsLayoutRoute,
|
|
625
|
+
path: '_compare',
|
|
626
|
+
component: ComparePageComponent,
|
|
627
|
+
})
|
|
628
|
+
|
|
505
629
|
// Previews routes: layout with catalog (index) and detail (splat) children
|
|
506
630
|
const previewsRouteWithChildren = previewsLayoutRoute.addChildren([
|
|
507
631
|
previewsCatalogRoute,
|
|
632
|
+
compareRoute,
|
|
508
633
|
previewDetailRoute,
|
|
509
634
|
])
|
|
510
635
|
|
|
511
636
|
const routeTree = rootRoute.addChildren([
|
|
512
637
|
previewsRouteWithChildren,
|
|
513
638
|
tokensRoute,
|
|
639
|
+
previewEmbedRoute,
|
|
514
640
|
...(indexRedirectRoute ? [indexRedirectRoute] : []),
|
|
515
641
|
...pageRoutes,
|
|
516
642
|
])
|
|
517
643
|
// Get base path for subpath deployments (e.g., GitHub Pages)
|
|
518
|
-
const basepath = (import.meta.env
|
|
644
|
+
const basepath = (import.meta.env.BASE_URL ?? '/').replace(/\/$/, '') || '/'
|
|
519
645
|
|
|
520
646
|
const router = createRouter({
|
|
521
647
|
routeTree,
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useState, useCallback } from 'react'
|
|
2
|
+
import { storage } from '../storage'
|
|
3
|
+
import type { Annotation, AnnotationComment, AnnotationCategory, UserIdentity } from '../types'
|
|
4
|
+
|
|
5
|
+
export function useAnnotations(previewName: string, stateOrStep: string) {
|
|
6
|
+
const storageKey = `annotations:${previewName}`
|
|
7
|
+
|
|
8
|
+
const [annotations, setAnnotations] = useState<Annotation[]>(
|
|
9
|
+
() => storage.get<Annotation[]>(storageKey) ?? []
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
const persist = (updated: Annotation[]) => {
|
|
13
|
+
storage.set(storageKey, updated)
|
|
14
|
+
setAnnotations(updated)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const getUser = (): string => {
|
|
18
|
+
const user = storage.get<UserIdentity>('user')
|
|
19
|
+
return user?.name || 'Anonymous'
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const createAnnotation = useCallback((
|
|
23
|
+
x: number,
|
|
24
|
+
y: number,
|
|
25
|
+
category: AnnotationCategory,
|
|
26
|
+
text: string,
|
|
27
|
+
) => {
|
|
28
|
+
const now = new Date().toISOString()
|
|
29
|
+
const annotation: Annotation = {
|
|
30
|
+
id: crypto.randomUUID(),
|
|
31
|
+
previewName,
|
|
32
|
+
stateOrStep,
|
|
33
|
+
x,
|
|
34
|
+
y,
|
|
35
|
+
category,
|
|
36
|
+
resolved: false,
|
|
37
|
+
createdAt: now,
|
|
38
|
+
comments: [{
|
|
39
|
+
id: crypto.randomUUID(),
|
|
40
|
+
author: getUser(),
|
|
41
|
+
text,
|
|
42
|
+
createdAt: now,
|
|
43
|
+
}],
|
|
44
|
+
}
|
|
45
|
+
const current = storage.get<Annotation[]>(storageKey) ?? []
|
|
46
|
+
persist([...current, annotation])
|
|
47
|
+
}, [previewName, stateOrStep, storageKey])
|
|
48
|
+
|
|
49
|
+
const addComment = useCallback((annotationId: string, text: string) => {
|
|
50
|
+
const current = storage.get<Annotation[]>(storageKey) ?? []
|
|
51
|
+
const comment: AnnotationComment = {
|
|
52
|
+
id: crypto.randomUUID(),
|
|
53
|
+
author: getUser(),
|
|
54
|
+
text,
|
|
55
|
+
createdAt: new Date().toISOString(),
|
|
56
|
+
}
|
|
57
|
+
persist(current.map(a =>
|
|
58
|
+
a.id === annotationId
|
|
59
|
+
? { ...a, comments: [...a.comments, comment] }
|
|
60
|
+
: a
|
|
61
|
+
))
|
|
62
|
+
}, [storageKey])
|
|
63
|
+
|
|
64
|
+
const resolveAnnotation = useCallback((annotationId: string) => {
|
|
65
|
+
const current = storage.get<Annotation[]>(storageKey) ?? []
|
|
66
|
+
persist(current.map(a =>
|
|
67
|
+
a.id === annotationId ? { ...a, resolved: !a.resolved } : a
|
|
68
|
+
))
|
|
69
|
+
}, [storageKey])
|
|
70
|
+
|
|
71
|
+
const deleteAnnotation = useCallback((annotationId: string) => {
|
|
72
|
+
const current = storage.get<Annotation[]>(storageKey) ?? []
|
|
73
|
+
persist(current.filter(a => a.id !== annotationId))
|
|
74
|
+
}, [storageKey])
|
|
75
|
+
|
|
76
|
+
return { annotations, createAnnotation, addComment, resolveAnnotation, deleteAnnotation }
|
|
77
|
+
}
|