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
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// src/preview-runtime/build.ts
|
|
2
2
|
// Production build for previews - pre-bundles React/TSX at build time
|
|
3
3
|
|
|
4
|
-
import { build } from 'esbuild'
|
|
5
4
|
import type { PreviewConfig } from './types'
|
|
5
|
+
import { mkdtempSync, writeFileSync, rmSync, existsSync, mkdirSync } from 'fs'
|
|
6
|
+
import { join, dirname } from 'path'
|
|
7
|
+
import { tmpdir } from 'os'
|
|
6
8
|
|
|
7
9
|
export interface PreviewBuildResult {
|
|
8
10
|
html: string
|
|
@@ -11,18 +13,10 @@ export interface PreviewBuildResult {
|
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* Build a preview into a standalone HTML file for production
|
|
14
|
-
* Uses
|
|
16
|
+
* Uses Bun.build (native) to bundle at build time
|
|
15
17
|
*/
|
|
16
18
|
export async function buildPreviewHtml(config: PreviewConfig): Promise<PreviewBuildResult> {
|
|
17
19
|
try {
|
|
18
|
-
// Build virtual filesystem
|
|
19
|
-
const virtualFs: Record<string, { contents: string; loader: string }> = {}
|
|
20
|
-
for (const file of config.files) {
|
|
21
|
-
const ext = file.path.split('.').pop()?.toLowerCase()
|
|
22
|
-
const loader = ext === 'css' ? 'css' : ext === 'json' ? 'json' : ext || 'tsx'
|
|
23
|
-
virtualFs[file.path] = { contents: file.content, loader }
|
|
24
|
-
}
|
|
25
|
-
|
|
26
20
|
// Find entry and check if it exports default
|
|
27
21
|
const entryFile = config.files.find(f => f.path === config.entry)
|
|
28
22
|
if (!entryFile) {
|
|
@@ -43,83 +37,82 @@ export async function buildPreviewHtml(config: PreviewConfig): Promise<PreviewBu
|
|
|
43
37
|
import './${config.entry}'
|
|
44
38
|
`
|
|
45
39
|
|
|
46
|
-
//
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
build
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// CSS: convert to JS that injects styles
|
|
99
|
-
if (file.loader === 'css') {
|
|
100
|
-
const css = file.contents.replace(/`/g, '\\`').replace(/\$/g, '\\$')
|
|
40
|
+
// Write all files to temp directory
|
|
41
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'prev-build-'))
|
|
42
|
+
const entryPath = join(tempDir, '__entry.tsx')
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
writeFileSync(entryPath, entryCode)
|
|
46
|
+
|
|
47
|
+
// Write virtual files to temp dir
|
|
48
|
+
for (const file of config.files) {
|
|
49
|
+
const targetPath = join(tempDir, file.path)
|
|
50
|
+
const dir = dirname(targetPath)
|
|
51
|
+
if (!existsSync(dir)) {
|
|
52
|
+
mkdirSync(dir, { recursive: true })
|
|
53
|
+
}
|
|
54
|
+
writeFileSync(targetPath, file.content)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Bundle with Bun.build
|
|
58
|
+
const result = await Bun.build({
|
|
59
|
+
entrypoints: [entryPath],
|
|
60
|
+
format: 'esm',
|
|
61
|
+
target: 'browser',
|
|
62
|
+
minify: true,
|
|
63
|
+
jsx: { runtime: 'automatic', importSource: 'react' },
|
|
64
|
+
define: {
|
|
65
|
+
'process.env.NODE_ENV': '"production"',
|
|
66
|
+
},
|
|
67
|
+
plugins: [{
|
|
68
|
+
name: 'preview-externals',
|
|
69
|
+
setup(build) {
|
|
70
|
+
// External: React from CDN
|
|
71
|
+
build.onResolve({ filter: /^react(-dom)?(\/.*)?$/ }, args => {
|
|
72
|
+
const parts = args.path.split('/')
|
|
73
|
+
const pkg = parts[0]
|
|
74
|
+
const subpath = parts.slice(1).join('/')
|
|
75
|
+
const url = subpath
|
|
76
|
+
? `https://esm.sh/${pkg}@18/${subpath}`
|
|
77
|
+
: `https://esm.sh/${pkg}@18`
|
|
78
|
+
return { path: url, external: true }
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
// Auto-resolve npm packages via esm.sh
|
|
82
|
+
build.onResolve({ filter: /^[^./]/ }, args => {
|
|
83
|
+
if (args.path.startsWith('https://')) return undefined
|
|
84
|
+
return { path: `https://esm.sh/${args.path}`, external: true }
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// CSS: convert to JS that injects styles
|
|
88
|
+
build.onLoad({ filter: /\.css$/ }, args => {
|
|
89
|
+
const content = Bun.file(args.path)
|
|
90
|
+
return content.text().then(css => {
|
|
91
|
+
const escaped = css.replace(/`/g, '\\`').replace(/\$/g, '\\$')
|
|
101
92
|
return {
|
|
102
93
|
contents: `
|
|
103
94
|
const style = document.createElement('style');
|
|
104
|
-
style.textContent = \`${
|
|
95
|
+
style.textContent = \`${escaped}\`;
|
|
105
96
|
document.head.appendChild(style);
|
|
106
97
|
`,
|
|
107
98
|
loader: 'js',
|
|
108
99
|
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
},
|
|
103
|
+
}],
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
if (!result.success) {
|
|
107
|
+
const errors = result.logs.filter(l => l.level === 'error').map(l => l.message).join('; ')
|
|
108
|
+
return { html: '', error: errors || 'Build failed' }
|
|
109
|
+
}
|
|
117
110
|
|
|
118
|
-
|
|
119
|
-
|
|
111
|
+
const jsFile = result.outputs.find(f => f.path.endsWith('.js')) || result.outputs[0]
|
|
112
|
+
const jsCode = jsFile ? await jsFile.text() : ''
|
|
120
113
|
|
|
121
|
-
|
|
122
|
-
|
|
114
|
+
// Generate standalone HTML
|
|
115
|
+
const html = `<!DOCTYPE html>
|
|
123
116
|
<html lang="en">
|
|
124
117
|
<head>
|
|
125
118
|
<meta charset="UTF-8">
|
|
@@ -137,7 +130,10 @@ export async function buildPreviewHtml(config: PreviewConfig): Promise<PreviewBu
|
|
|
137
130
|
</body>
|
|
138
131
|
</html>`
|
|
139
132
|
|
|
140
|
-
|
|
133
|
+
return { html }
|
|
134
|
+
} finally {
|
|
135
|
+
rmSync(tempDir, { recursive: true, force: true })
|
|
136
|
+
}
|
|
141
137
|
} catch (err) {
|
|
142
138
|
return {
|
|
143
139
|
html: '',
|
|
@@ -50,18 +50,22 @@
|
|
|
50
50
|
// Fast preview runtime - uses server-side bundled code
|
|
51
51
|
const params = new URLSearchParams(window.location.search)
|
|
52
52
|
const src = params.get('src')
|
|
53
|
+
const state = params.get('state')
|
|
53
54
|
|
|
54
55
|
if (!src) {
|
|
55
56
|
document.getElementById('root').innerHTML = '<div class="preview-error">No preview source specified</div>'
|
|
56
57
|
} else {
|
|
57
58
|
const startTime = performance.now()
|
|
59
|
+
const bundleUrl = state
|
|
60
|
+
? `/_preview-bundle/${src}?state=${encodeURIComponent(state)}`
|
|
61
|
+
: `/_preview-bundle/${src}`
|
|
58
62
|
|
|
59
63
|
try {
|
|
60
64
|
// Pre-fetch tokens, JSX, and bundle in parallel
|
|
61
65
|
const [tokensRes, jsxModule, bundleResponse] = await Promise.all([
|
|
62
66
|
fetch('/_prev/tokens.json').then(r => r.json()).catch(() => null),
|
|
63
67
|
import(`${window.location.origin}/_prev/jsx.js`),
|
|
64
|
-
fetch(
|
|
68
|
+
fetch(bundleUrl)
|
|
65
69
|
])
|
|
66
70
|
|
|
67
71
|
// Set tokens
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { test, expect, describe } from 'bun:test'
|
|
2
|
+
import { REGION_BRIDGE_SCRIPT } from './region-bridge'
|
|
3
|
+
|
|
4
|
+
describe('REGION_BRIDGE_SCRIPT', () => {
|
|
5
|
+
test('is valid JavaScript (parses without error)', () => {
|
|
6
|
+
// Bun.Transpiler will throw on syntax errors
|
|
7
|
+
const transpiler = new Bun.Transpiler({ loader: 'js' })
|
|
8
|
+
expect(() => transpiler.transformSync(REGION_BRIDGE_SCRIPT)).not.toThrow()
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test('contains click handler for [data-region]', () => {
|
|
12
|
+
expect(REGION_BRIDGE_SCRIPT).toContain('data-region')
|
|
13
|
+
expect(REGION_BRIDGE_SCRIPT).toContain('click')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('contains highlight-regions listener', () => {
|
|
17
|
+
expect(REGION_BRIDGE_SCRIPT).toContain('highlight-regions')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
test('posts region-click message to parent', () => {
|
|
21
|
+
expect(REGION_BRIDGE_SCRIPT).toContain('region-click')
|
|
22
|
+
expect(REGION_BRIDGE_SCRIPT).toContain('postMessage')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
test('reports region-rects with bounding rect data', () => {
|
|
26
|
+
expect(REGION_BRIDGE_SCRIPT).toContain('region-rects')
|
|
27
|
+
expect(REGION_BRIDGE_SCRIPT).toContain('getBoundingClientRect')
|
|
28
|
+
expect(REGION_BRIDGE_SCRIPT).toContain('reportRegionRects')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('sends empty region-rects when regions are cleared', () => {
|
|
32
|
+
// When highlight-regions is sent with empty array, bridge should report empty rects
|
|
33
|
+
expect(REGION_BRIDGE_SCRIPT).toContain("{ type: 'region-rects', rects: [] }")
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
test('debounces rect reporting on scroll and resize', () => {
|
|
37
|
+
expect(REGION_BRIDGE_SCRIPT).toContain('scroll')
|
|
38
|
+
expect(REGION_BRIDGE_SCRIPT).toContain('resize')
|
|
39
|
+
expect(REGION_BRIDGE_SCRIPT).toContain('debouncedReport')
|
|
40
|
+
})
|
|
41
|
+
})
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Bridge script injected into preview iframes to enable region interactivity.
|
|
2
|
+
// Handles: click events on [data-region] elements → postMessage to parent
|
|
3
|
+
// highlight-regions message from parent → visual overlay on regions
|
|
4
|
+
|
|
5
|
+
export const REGION_BRIDGE_SCRIPT = `
|
|
6
|
+
(function() {
|
|
7
|
+
// Click handler: delegate clicks on [data-region] elements
|
|
8
|
+
document.addEventListener('click', function(e) {
|
|
9
|
+
var el = e.target;
|
|
10
|
+
while (el && el !== document.body) {
|
|
11
|
+
if (el.getAttribute && el.getAttribute('data-region')) {
|
|
12
|
+
e.preventDefault();
|
|
13
|
+
e.stopPropagation();
|
|
14
|
+
window.parent.postMessage({
|
|
15
|
+
type: 'region-click',
|
|
16
|
+
region: el.getAttribute('data-region')
|
|
17
|
+
}, '*');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
el = el.parentElement;
|
|
21
|
+
}
|
|
22
|
+
}, true);
|
|
23
|
+
|
|
24
|
+
// Highlight handler: parent sends list of region names to highlight
|
|
25
|
+
window.addEventListener('message', function(e) {
|
|
26
|
+
if (!e.data || e.data.type !== 'highlight-regions') return;
|
|
27
|
+
|
|
28
|
+
// Remove existing highlights
|
|
29
|
+
var existing = document.querySelectorAll('[data-region-highlight]');
|
|
30
|
+
for (var i = 0; i < existing.length; i++) {
|
|
31
|
+
existing[i].removeAttribute('data-region-highlight');
|
|
32
|
+
existing[i].style.cursor = '';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
var regions = e.data.regions || [];
|
|
36
|
+
if (regions.length === 0) {
|
|
37
|
+
// Report empty rects when cleared
|
|
38
|
+
window.parent.postMessage({ type: 'region-rects', rects: [] }, '*');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for (var j = 0; j < regions.length; j++) {
|
|
43
|
+
var els = document.querySelectorAll('[data-region="' + regions[j] + '"]');
|
|
44
|
+
for (var k = 0; k < els.length; k++) {
|
|
45
|
+
els[k].setAttribute('data-region-highlight', 'true');
|
|
46
|
+
els[k].style.cursor = 'pointer';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Report bounding rects to parent
|
|
51
|
+
reportRegionRects();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Measure and report region rects to parent
|
|
55
|
+
function reportRegionRects() {
|
|
56
|
+
var highlighted = document.querySelectorAll('[data-region-highlight]');
|
|
57
|
+
var rects = [];
|
|
58
|
+
for (var i = 0; i < highlighted.length; i++) {
|
|
59
|
+
var el = highlighted[i];
|
|
60
|
+
var rect = el.getBoundingClientRect();
|
|
61
|
+
rects.push({
|
|
62
|
+
name: el.getAttribute('data-region') || '',
|
|
63
|
+
x: rect.left,
|
|
64
|
+
y: rect.top,
|
|
65
|
+
width: rect.width,
|
|
66
|
+
height: rect.height
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
window.parent.postMessage({ type: 'region-rects', rects: rects }, '*');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Debounced re-report on scroll/resize
|
|
73
|
+
var debounceTimer = null;
|
|
74
|
+
function debouncedReport() {
|
|
75
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
76
|
+
debounceTimer = setTimeout(function() {
|
|
77
|
+
var highlighted = document.querySelectorAll('[data-region-highlight]');
|
|
78
|
+
if (highlighted.length > 0) reportRegionRects();
|
|
79
|
+
}, 100);
|
|
80
|
+
}
|
|
81
|
+
window.addEventListener('scroll', debouncedReport, true);
|
|
82
|
+
window.addEventListener('resize', debouncedReport);
|
|
83
|
+
|
|
84
|
+
// Token override handler: parent sends CSS overrides to inject
|
|
85
|
+
window.addEventListener('message', function(e) {
|
|
86
|
+
if (!e.data || e.data.type !== 'token-overrides') return;
|
|
87
|
+
var styleId = 'prev-token-overrides';
|
|
88
|
+
var existing = document.getElementById(styleId);
|
|
89
|
+
if (e.data.css) {
|
|
90
|
+
if (!existing) {
|
|
91
|
+
existing = document.createElement('style');
|
|
92
|
+
existing.id = styleId;
|
|
93
|
+
document.head.appendChild(existing);
|
|
94
|
+
}
|
|
95
|
+
existing.textContent = e.data.css;
|
|
96
|
+
} else if (existing) {
|
|
97
|
+
existing.remove();
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
})();
|
|
101
|
+
`
|
|
@@ -27,3 +27,9 @@ export type PreviewMessage =
|
|
|
27
27
|
| { type: 'ready' }
|
|
28
28
|
| { type: 'built'; result: BuildResult }
|
|
29
29
|
| { type: 'error'; error: string }
|
|
30
|
+
// Region interactivity (flow previews)
|
|
31
|
+
| { type: 'region-click'; region: string }
|
|
32
|
+
| { type: 'highlight-regions'; regions: string[] }
|
|
33
|
+
| { type: 'region-rects'; rects: Array<{ name: string; x: number; y: number; width: number; height: number }> }
|
|
34
|
+
// Token override injection
|
|
35
|
+
| { type: 'token-overrides'; css: string }
|
|
@@ -1,9 +1,27 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { join, dirname } from 'path'
|
|
2
|
+
import { mkdtempSync, writeFileSync, rmSync, existsSync, readFileSync } from 'fs'
|
|
3
3
|
import { fileURLToPath } from 'url'
|
|
4
|
+
import { tmpdir } from 'os'
|
|
4
5
|
|
|
5
|
-
//
|
|
6
|
-
|
|
6
|
+
// Find CLI root for module resolution (React is our dependency)
|
|
7
|
+
function findCliRoot(): string {
|
|
8
|
+
let dir = dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
for (let i = 0; i < 10; i++) {
|
|
10
|
+
const pkgPath = join(dir, 'package.json')
|
|
11
|
+
if (existsSync(pkgPath)) {
|
|
12
|
+
try {
|
|
13
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
|
|
14
|
+
if (pkg.name === 'prev-cli') return dir
|
|
15
|
+
} catch {}
|
|
16
|
+
}
|
|
17
|
+
const parent = dirname(dir)
|
|
18
|
+
if (parent === dir) break
|
|
19
|
+
dir = parent
|
|
20
|
+
}
|
|
21
|
+
return dirname(dirname(fileURLToPath(import.meta.url)))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const cliRoot = findCliRoot()
|
|
7
25
|
|
|
8
26
|
export interface VendorBundleResult {
|
|
9
27
|
success: boolean
|
|
@@ -18,6 +36,7 @@ export async function buildVendorBundle(): Promise<VendorBundleResult> {
|
|
|
18
36
|
import * as ReactDOM from 'react-dom'
|
|
19
37
|
import { createRoot } from 'react-dom/client'
|
|
20
38
|
export { jsx, jsxs, Fragment } from 'react/jsx-runtime'
|
|
39
|
+
export { jsxDEV } from 'react/jsx-dev-runtime'
|
|
21
40
|
export { React, ReactDOM, createRoot }
|
|
22
41
|
// Re-export React hooks as named exports (preview code imports them directly)
|
|
23
42
|
export {
|
|
@@ -31,26 +50,200 @@ export async function buildVendorBundle(): Promise<VendorBundleResult> {
|
|
|
31
50
|
export default React
|
|
32
51
|
`
|
|
33
52
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
// Write temp file in CLI root so React can be resolved from node_modules
|
|
54
|
+
const tempDir = mkdtempSync(join(cliRoot, '.tmp-vendor-'))
|
|
55
|
+
const entryPath = join(tempDir, 'entry.ts')
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
writeFileSync(entryPath, entryCode)
|
|
59
|
+
|
|
60
|
+
const result = await Bun.build({
|
|
61
|
+
entrypoints: [entryPath],
|
|
62
|
+
format: 'esm',
|
|
63
|
+
target: 'browser',
|
|
64
|
+
minify: true,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
if (!result.success) {
|
|
68
|
+
const errors = result.logs.filter(l => l.level === 'error').map(l => l.message).join('; ')
|
|
69
|
+
return { success: false, code: '', error: errors || 'Build failed' }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const jsFile = result.outputs.find(f => f.path.endsWith('.js')) || result.outputs[0]
|
|
73
|
+
if (!jsFile) {
|
|
74
|
+
return { success: false, code: '', error: 'No output generated' }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { success: true, code: await jsFile.text() }
|
|
78
|
+
} finally {
|
|
79
|
+
rmSync(tempDir, { recursive: true, force: true })
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
return {
|
|
83
|
+
success: false,
|
|
84
|
+
code: '',
|
|
85
|
+
error: err instanceof Error ? err.message : String(err),
|
|
51
86
|
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Build @prev/jsx bundle for static preview builds
|
|
92
|
+
* React is externalized, so temp dir location doesn't matter for resolution
|
|
93
|
+
*/
|
|
94
|
+
export async function buildJsxBundle(vendorPath: string): Promise<VendorBundleResult> {
|
|
95
|
+
try {
|
|
96
|
+
const minimalJsx = `
|
|
97
|
+
import * as React from 'react'
|
|
52
98
|
|
|
53
|
-
|
|
99
|
+
// Default token values for standalone preview rendering
|
|
100
|
+
const defaultTokens = {
|
|
101
|
+
background: {
|
|
102
|
+
primary: '#3b82f6',
|
|
103
|
+
secondary: '#f1f5f9',
|
|
104
|
+
destructive: '#ef4444',
|
|
105
|
+
muted: '#f1f5f9',
|
|
106
|
+
accent: '#f1f5f9',
|
|
107
|
+
transparent: 'transparent',
|
|
108
|
+
},
|
|
109
|
+
color: {
|
|
110
|
+
'primary-foreground': '#ffffff',
|
|
111
|
+
'secondary-foreground': '#0f172a',
|
|
112
|
+
'destructive-foreground': '#ffffff',
|
|
113
|
+
'muted-foreground': '#64748b',
|
|
114
|
+
'accent-foreground': '#0f172a',
|
|
115
|
+
foreground: '#0f172a',
|
|
116
|
+
},
|
|
117
|
+
spacing: {
|
|
118
|
+
xs: '4px',
|
|
119
|
+
sm: '8px',
|
|
120
|
+
md: '12px',
|
|
121
|
+
lg: '16px',
|
|
122
|
+
xl: '24px',
|
|
123
|
+
},
|
|
124
|
+
radius: {
|
|
125
|
+
none: '0',
|
|
126
|
+
sm: '4px',
|
|
127
|
+
md: '6px',
|
|
128
|
+
lg: '8px',
|
|
129
|
+
full: '9999px',
|
|
130
|
+
},
|
|
131
|
+
'typography.size': {
|
|
132
|
+
xs: '12px',
|
|
133
|
+
sm: '14px',
|
|
134
|
+
base: '16px',
|
|
135
|
+
lg: '18px',
|
|
136
|
+
xl: '20px',
|
|
137
|
+
},
|
|
138
|
+
'typography.weight': {
|
|
139
|
+
normal: '400',
|
|
140
|
+
medium: '500',
|
|
141
|
+
semibold: '600',
|
|
142
|
+
bold: '700',
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Token resolution
|
|
147
|
+
let tokensConfig = null
|
|
148
|
+
export function setTokensConfig(config) { tokensConfig = config }
|
|
149
|
+
|
|
150
|
+
function resolveToken(category, token) {
|
|
151
|
+
// Check custom config first, then defaults
|
|
152
|
+
const config = tokensConfig || defaultTokens
|
|
153
|
+
const cat = config[category]
|
|
154
|
+
return cat?.[token] ?? token
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// VNode type
|
|
158
|
+
export class VNode {
|
|
159
|
+
constructor(type, props, children) {
|
|
160
|
+
this.type = type
|
|
161
|
+
this.props = props || {}
|
|
162
|
+
this.children = children || []
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Primitives - return VNodes
|
|
167
|
+
export function Box(props) { return new VNode('Box', props, props.children ? [props.children] : []) }
|
|
168
|
+
export function Text(props) { return new VNode('Text', props, props.children ? [props.children] : []) }
|
|
169
|
+
export function Col(props) { return new VNode('Col', props, props.children || []) }
|
|
170
|
+
export function Row(props) { return new VNode('Row', props, props.children || []) }
|
|
171
|
+
export function Spacer(props) { return new VNode('Spacer', props, []) }
|
|
172
|
+
export function Slot(props) { return new VNode('Slot', props, []) }
|
|
173
|
+
export function Icon(props) { return new VNode('Icon', props, []) }
|
|
174
|
+
export function Image(props) { return new VNode('Image', props, []) }
|
|
175
|
+
export const Fragment = React.Fragment
|
|
176
|
+
|
|
177
|
+
// Convert VNode to React element
|
|
178
|
+
export function toReact(vnode) {
|
|
179
|
+
if (!vnode || typeof vnode !== 'object') return vnode
|
|
180
|
+
if (!(vnode instanceof VNode)) return vnode
|
|
181
|
+
|
|
182
|
+
const { type, props, children } = vnode
|
|
183
|
+
const style = {}
|
|
184
|
+
|
|
185
|
+
// Map props to styles
|
|
186
|
+
if (props.bg) style.backgroundColor = resolveToken('background', props.bg)
|
|
187
|
+
if (props.padding) style.padding = resolveToken('spacing', props.padding)
|
|
188
|
+
if (props.radius) style.borderRadius = resolveToken('radius', props.radius)
|
|
189
|
+
if (props.color) style.color = resolveToken('color', props.color)
|
|
190
|
+
if (props.size) style.fontSize = resolveToken('typography.size', props.size)
|
|
191
|
+
if (props.weight) style.fontWeight = resolveToken('typography.weight', props.weight)
|
|
192
|
+
if (props.gap) style.gap = resolveToken('spacing', props.gap)
|
|
193
|
+
|
|
194
|
+
// Layout types
|
|
195
|
+
if (type === 'Col') { style.display = 'flex'; style.flexDirection = 'column' }
|
|
196
|
+
if (type === 'Row') { style.display = 'flex'; style.flexDirection = 'row' }
|
|
197
|
+
if (type === 'Spacer') { style.flex = 1 }
|
|
198
|
+
|
|
199
|
+
const childElements = children.map(c => toReact(c))
|
|
200
|
+
|
|
201
|
+
return React.createElement('div', { style }, ...childElements)
|
|
202
|
+
}
|
|
203
|
+
`
|
|
204
|
+
|
|
205
|
+
// React is externalized via plugin, so temp dir location doesn't matter
|
|
206
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'prev-jsx-'))
|
|
207
|
+
const entryPath = join(tempDir, 'entry.ts')
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
writeFileSync(entryPath, minimalJsx)
|
|
211
|
+
|
|
212
|
+
const result = await Bun.build({
|
|
213
|
+
entrypoints: [entryPath],
|
|
214
|
+
format: 'esm',
|
|
215
|
+
target: 'browser',
|
|
216
|
+
minify: true,
|
|
217
|
+
plugins: [
|
|
218
|
+
{
|
|
219
|
+
name: 'jsx-externals',
|
|
220
|
+
setup(build) {
|
|
221
|
+
build.onResolve({ filter: /^react(-dom)?(\/.*)?$/ }, () => {
|
|
222
|
+
return { path: vendorPath, external: true }
|
|
223
|
+
})
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
],
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
if (!result.success) {
|
|
230
|
+
const errors = result.logs.filter(l => l.level === 'error').map(l => l.message).join('; ')
|
|
231
|
+
return { success: false, code: '', error: errors || 'Build failed' }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const jsFile = result.outputs.find(f => f.path.endsWith('.js')) || result.outputs[0]
|
|
235
|
+
if (!jsFile) {
|
|
236
|
+
return { success: false, code: '', error: 'No output generated' }
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Fix bare specifiers — Bun.build keeps original import paths for externals
|
|
240
|
+
let code = await jsFile.text()
|
|
241
|
+
code = code.replace(/from\s*["']react["']/g, `from"${vendorPath}"`)
|
|
242
|
+
|
|
243
|
+
return { success: true, code }
|
|
244
|
+
} finally {
|
|
245
|
+
rmSync(tempDir, { recursive: true, force: true })
|
|
246
|
+
}
|
|
54
247
|
} catch (err) {
|
|
55
248
|
return {
|
|
56
249
|
success: false,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!-- c3-generated: c3-601, c3-602, c3-603, c3-604 -->
|
|
2
|
+
# Template Primitives (WIP)
|
|
3
|
+
|
|
4
|
+
Before modifying this code, read:
|
|
5
|
+
- `.c3/c3-6-primitives/c3-601-types.md` - Type definitions
|
|
6
|
+
- `.c3/c3-6-primitives/c3-602-parser.md` - Template parser
|
|
7
|
+
- `.c3/c3-6-primitives/c3-603-template-parser.md` - Syntax parser
|
|
8
|
+
- `.c3/c3-6-primitives/c3-604-template-renderer.md` - Output renderer
|
|
9
|
+
|
|
10
|
+
Primitive types: `col`, `row`, `box`, `spacer`, `slot`, `text`, `icon`, `image`
|
|
11
|
+
|
|
12
|
+
Key files:
|
|
13
|
+
- `types.ts` - PrimitiveType, Primitive, props interfaces
|
|
14
|
+
- `parser.ts` - Main template parser
|
|
15
|
+
- `template-parser.ts` - Template syntax parsing
|
|
16
|
+
- `template-renderer.ts` - Template to output
|
|
17
|
+
<!-- end-c3-generated -->
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<!-- c3-generated: c3-301, c3-302, c3-303, c3-304, c3-305 -->
|
|
2
|
+
# Theme Components
|
|
3
|
+
|
|
4
|
+
Before modifying this code, read:
|
|
5
|
+
- `.c3/c3-3-theme/c3-301-entry.md` - React app entry & routing
|
|
6
|
+
- `.c3/c3-3-theme/c3-302-layout.md` - Page layout structure
|
|
7
|
+
- `.c3/c3-3-theme/c3-303-mdx-provider.md` - MDX component mapping
|
|
8
|
+
- `.c3/c3-3-theme/c3-304-toolbar.md` - Navigation toolbar
|
|
9
|
+
- `.c3/c3-3-theme/c3-305-sidebar.md` - Sidebar navigation
|
|
10
|
+
- Patterns: `ref-theming`
|
|
11
|
+
|
|
12
|
+
Key files:
|
|
13
|
+
- `entry.tsx` - TanStack Router setup, virtual module imports
|
|
14
|
+
- `Layout.tsx` - Main layout with sidebar/toolbar
|
|
15
|
+
- `mdx-components.tsx` - MDX component overrides
|
|
16
|
+
- `Toolbar.tsx` - Top navigation bar
|
|
17
|
+
- `TOCPanel.tsx` - Table of contents
|
|
18
|
+
|
|
19
|
+
Full refs: `.c3/refs/ref-theming.md`
|
|
20
|
+
<!-- end-c3-generated -->
|