prev-cli 0.24.19 → 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.
Files changed (150) hide show
  1. package/dist/cli.js +2006 -1703
  2. package/dist/previews/components/cart-item/index.d.ts +5 -0
  3. package/dist/previews/components/price-tag/index.d.ts +6 -0
  4. package/dist/previews/screens/cart/empty.d.ts +1 -0
  5. package/dist/previews/screens/cart/index.d.ts +1 -0
  6. package/dist/previews/screens/payment/error.d.ts +1 -0
  7. package/dist/previews/screens/payment/index.d.ts +1 -0
  8. package/dist/previews/screens/payment/processing.d.ts +1 -0
  9. package/dist/previews/screens/receipt/index.d.ts +1 -0
  10. package/dist/previews/shared/data.d.ts +30 -0
  11. package/dist/src/content/config-parser.d.ts +30 -0
  12. package/dist/src/content/flow-verifier.d.ts +21 -0
  13. package/dist/src/content/preview-types.d.ts +288 -0
  14. package/dist/{vite → src/content}/previews.d.ts +3 -11
  15. package/dist/{preview-runtime → src/preview-runtime}/build-optimized.d.ts +2 -0
  16. package/dist/{preview-runtime → src/preview-runtime}/build.d.ts +1 -1
  17. package/dist/src/preview-runtime/region-bridge.d.ts +1 -0
  18. package/dist/{preview-runtime → src/preview-runtime}/types.d.ts +18 -0
  19. package/dist/src/preview-runtime/vendors.d.ts +11 -0
  20. package/dist/{renderers → src/renderers}/index.d.ts +1 -1
  21. package/dist/{renderers → src/renderers}/types.d.ts +3 -31
  22. package/dist/src/server/build.d.ts +6 -0
  23. package/dist/src/server/dev.d.ts +13 -0
  24. package/dist/src/server/plugins/aliases.d.ts +5 -0
  25. package/dist/src/server/plugins/mdx.d.ts +5 -0
  26. package/dist/src/server/plugins/virtual-modules.d.ts +8 -0
  27. package/dist/src/server/preview.d.ts +10 -0
  28. package/dist/src/server/routes/component-bundle.d.ts +1 -0
  29. package/dist/src/server/routes/jsx-bundle.d.ts +3 -0
  30. package/dist/src/server/routes/og-image.d.ts +15 -0
  31. package/dist/src/server/routes/preview-bundle.d.ts +1 -0
  32. package/dist/src/server/routes/preview-config.d.ts +1 -0
  33. package/dist/src/server/routes/tokens.d.ts +1 -0
  34. package/dist/{vite → src/server}/start.d.ts +5 -2
  35. package/dist/{ui → src/ui}/button.d.ts +1 -1
  36. package/dist/{validators → src/validators}/index.d.ts +0 -5
  37. package/dist/{validators → src/validators}/semantic-validator.d.ts +2 -3
  38. package/package.json +8 -11
  39. package/src/jsx/CLAUDE.md +18 -0
  40. package/src/jsx/jsx-runtime.ts +1 -1
  41. package/src/preview-runtime/CLAUDE.md +21 -0
  42. package/src/preview-runtime/build-optimized.ts +189 -73
  43. package/src/preview-runtime/build.ts +75 -79
  44. package/src/preview-runtime/fast-template.html +5 -1
  45. package/src/preview-runtime/region-bridge.test.ts +41 -0
  46. package/src/preview-runtime/region-bridge.ts +101 -0
  47. package/src/preview-runtime/types.ts +6 -0
  48. package/src/preview-runtime/vendors.ts +215 -22
  49. package/src/primitives/CLAUDE.md +17 -0
  50. package/src/theme/CLAUDE.md +20 -0
  51. package/src/theme/Preview.tsx +10 -4
  52. package/src/theme/Toolbar.tsx +2 -2
  53. package/src/theme/entry.tsx +247 -121
  54. package/src/theme/hooks/useAnnotations.ts +77 -0
  55. package/src/theme/hooks/useApprovalStatus.ts +50 -0
  56. package/src/theme/hooks/useSnapshots.ts +147 -0
  57. package/src/theme/hooks/useStorage.ts +26 -0
  58. package/src/theme/hooks/useTokenOverrides.ts +56 -0
  59. package/src/theme/hooks/useViewport.ts +23 -0
  60. package/src/theme/icons.tsx +39 -1
  61. package/src/theme/index.html +18 -0
  62. package/src/theme/mdx-components.tsx +1 -1
  63. package/src/theme/previews/AnnotationLayer.tsx +285 -0
  64. package/src/theme/previews/AnnotationPin.tsx +61 -0
  65. package/src/theme/previews/AnnotationThread.tsx +257 -0
  66. package/src/theme/previews/CLAUDE.md +18 -0
  67. package/src/theme/previews/ComponentPreview.tsx +487 -107
  68. package/src/theme/previews/FlowDiagram.tsx +111 -0
  69. package/src/theme/previews/FlowPreview.tsx +938 -174
  70. package/src/theme/previews/PreviewRouter.tsx +1 -4
  71. package/src/theme/previews/ScreenPreview.tsx +515 -175
  72. package/src/theme/previews/SnapshotButton.tsx +68 -0
  73. package/src/theme/previews/SnapshotCompare.tsx +216 -0
  74. package/src/theme/previews/SnapshotPanel.tsx +274 -0
  75. package/src/theme/previews/StatusBadge.tsx +66 -0
  76. package/src/theme/previews/StatusDropdown.tsx +158 -0
  77. package/src/theme/previews/TokenPlayground.tsx +438 -0
  78. package/src/theme/previews/ViewportControls.tsx +67 -0
  79. package/src/theme/previews/flow-diagram.test.ts +141 -0
  80. package/src/theme/previews/flow-diagram.ts +109 -0
  81. package/src/theme/previews/flow-navigation.test.ts +90 -0
  82. package/src/theme/previews/flow-navigation.ts +47 -0
  83. package/src/theme/previews/machines/derived.test.ts +225 -0
  84. package/src/theme/previews/machines/derived.ts +73 -0
  85. package/src/theme/previews/machines/flow-machine.test.ts +379 -0
  86. package/src/theme/previews/machines/flow-machine.ts +207 -0
  87. package/src/theme/previews/machines/screen-machine.test.ts +149 -0
  88. package/src/theme/previews/machines/screen-machine.ts +76 -0
  89. package/src/theme/previews/stores/flow-store.test.ts +157 -0
  90. package/src/theme/previews/stores/flow-store.ts +49 -0
  91. package/src/theme/previews/stores/screen-store.test.ts +68 -0
  92. package/src/theme/previews/stores/screen-store.ts +33 -0
  93. package/src/theme/storage.test.ts +97 -0
  94. package/src/theme/storage.ts +71 -0
  95. package/src/theme/styles.css +296 -25
  96. package/src/theme/types.ts +64 -0
  97. package/src/tokens/CLAUDE.md +16 -0
  98. package/src/tokens/resolver.ts +1 -1
  99. package/dist/preview-runtime/vendors.d.ts +0 -6
  100. package/dist/vite/config-parser.d.ts +0 -13
  101. package/dist/vite/config.d.ts +0 -12
  102. package/dist/vite/plugins/config-plugin.d.ts +0 -3
  103. package/dist/vite/plugins/debug-plugin.d.ts +0 -3
  104. package/dist/vite/plugins/entry-plugin.d.ts +0 -2
  105. package/dist/vite/plugins/fumadocs-plugin.d.ts +0 -9
  106. package/dist/vite/plugins/pages-plugin.d.ts +0 -5
  107. package/dist/vite/plugins/previews-plugin.d.ts +0 -2
  108. package/dist/vite/plugins/tokens-plugin.d.ts +0 -2
  109. package/dist/vite/preview-types.d.ts +0 -70
  110. package/src/theme/previews/AtlasPreview.tsx +0 -528
  111. package/dist/{cli.d.ts → src/cli.d.ts} +0 -0
  112. package/dist/{config → src/config}/index.d.ts +0 -0
  113. package/dist/{config → src/config}/loader.d.ts +0 -0
  114. package/dist/{config → src/config}/schema.d.ts +0 -0
  115. package/dist/{vite → src/content}/pages.d.ts +0 -0
  116. package/dist/{jsx → src/jsx}/adapters/html.d.ts +0 -0
  117. package/dist/{jsx → src/jsx}/adapters/react.d.ts +0 -0
  118. package/dist/{jsx → src/jsx}/define-component.d.ts +0 -0
  119. package/dist/{jsx → src/jsx}/index.d.ts +0 -0
  120. package/dist/{jsx → src/jsx}/jsx-runtime.d.ts +0 -0
  121. package/dist/{jsx → src/jsx}/migrate.d.ts +0 -0
  122. package/dist/{jsx → src/jsx}/schemas/index.d.ts +0 -0
  123. package/dist/{jsx → src/jsx}/schemas/primitives.d.ts +10 -10
  124. package/dist/{jsx → src/jsx}/schemas/tokens.d.ts +3 -3
  125. /package/dist/{jsx → src/jsx}/validation.d.ts +0 -0
  126. /package/dist/{jsx → src/jsx}/vnode.d.ts +0 -0
  127. /package/dist/{migrate.d.ts → src/migrate.d.ts} +0 -0
  128. /package/dist/{preview-runtime → src/preview-runtime}/tailwind.d.ts +0 -0
  129. /package/dist/{primitives → src/primitives}/index.d.ts +0 -0
  130. /package/dist/{primitives → src/primitives}/migrate.d.ts +0 -0
  131. /package/dist/{primitives → src/primitives}/parser.d.ts +0 -0
  132. /package/dist/{primitives → src/primitives}/template-parser.d.ts +0 -0
  133. /package/dist/{primitives → src/primitives}/template-renderer.d.ts +0 -0
  134. /package/dist/{primitives → src/primitives}/types.d.ts +0 -0
  135. /package/dist/{renderers → src/renderers}/html/index.d.ts +0 -0
  136. /package/dist/{renderers → src/renderers}/react/index.d.ts +0 -0
  137. /package/dist/{renderers → src/renderers}/registry.d.ts +0 -0
  138. /package/dist/{renderers → src/renderers}/render.d.ts +0 -0
  139. /package/dist/{tokens → src/tokens}/defaults.d.ts +0 -0
  140. /package/dist/{tokens → src/tokens}/resolver.d.ts +0 -0
  141. /package/dist/{tokens → src/tokens}/utils.d.ts +0 -0
  142. /package/dist/{tokens → src/tokens}/validation.d.ts +0 -0
  143. /package/dist/{typecheck → src/typecheck}/index.d.ts +0 -0
  144. /package/dist/{ui → src/ui}/card.d.ts +0 -0
  145. /package/dist/{ui → src/ui}/index.d.ts +0 -0
  146. /package/dist/{ui → src/ui}/utils.d.ts +0 -0
  147. /package/dist/{utils → src/utils}/cache.d.ts +0 -0
  148. /package/dist/{utils → src/utils}/debug.d.ts +0 -0
  149. /package/dist/{utils → src/utils}/port.d.ts +0 -0
  150. /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 esbuild (native) to bundle at build time
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
- // Bundle with esbuild
47
- const result = await build({
48
- stdin: {
49
- contents: entryCode,
50
- loader: 'tsx',
51
- resolveDir: '/',
52
- },
53
- bundle: true,
54
- write: false,
55
- format: 'esm',
56
- jsx: 'automatic',
57
- jsxImportSource: 'react',
58
- target: 'es2020',
59
- minify: true,
60
- plugins: [{
61
- name: 'virtual-fs',
62
- setup(build) {
63
- // External: React from CDN
64
- build.onResolve({ filter: /^react(-dom)?(\/.*)?$/ }, args => {
65
- const parts = args.path.split('/')
66
- const pkg = parts[0]
67
- const subpath = parts.slice(1).join('/')
68
- const url = subpath
69
- ? `https://esm.sh/${pkg}@18/${subpath}`
70
- : `https://esm.sh/${pkg}@18`
71
- return { path: url, external: true }
72
- })
73
-
74
- // Auto-resolve npm packages via esm.sh
75
- build.onResolve({ filter: /^[^./]/ }, args => {
76
- if (args.path.startsWith('https://')) return
77
- return { path: `https://esm.sh/${args.path}`, external: true }
78
- })
79
-
80
- // Resolve relative imports
81
- build.onResolve({ filter: /^\./ }, args => {
82
- let resolved = args.path.replace(/^\.\//, '')
83
- if (!resolved.includes('.')) {
84
- for (const ext of ['.tsx', '.ts', '.jsx', '.js', '.css']) {
85
- if (virtualFs[resolved + ext]) {
86
- resolved = resolved + ext
87
- break
88
- }
89
- }
90
- }
91
- return { path: resolved, namespace: 'virtual' }
92
- })
93
-
94
- // Load from virtual filesystem
95
- build.onLoad({ filter: /.*/, namespace: 'virtual' }, args => {
96
- const file = virtualFs[args.path]
97
- if (file) {
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 = \`${css}\`;
95
+ style.textContent = \`${escaped}\`;
105
96
  document.head.appendChild(style);
106
97
  `,
107
98
  loader: 'js',
108
99
  }
109
- }
110
- return { contents: file.contents, loader: file.loader as any }
111
- }
112
- return { contents: '', loader: 'empty' }
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
- const jsFile = result.outputFiles.find(f => f.path.endsWith('.js')) || result.outputFiles[0]
119
- const jsCode = jsFile?.text || ''
111
+ const jsFile = result.outputs.find(f => f.path.endsWith('.js')) || result.outputs[0]
112
+ const jsCode = jsFile ? await jsFile.text() : ''
120
113
 
121
- // Generate standalone HTML
122
- const html = `<!DOCTYPE html>
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
- return { html }
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(`/_preview-bundle/${src}`)
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 { build } from 'esbuild'
2
- import { dirname } from 'path'
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
- // Resolve from CLI's location, not user's project (React is our dependency)
6
- const __dirname = dirname(fileURLToPath(import.meta.url))
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
- const result = await build({
35
- stdin: {
36
- contents: entryCode,
37
- loader: 'ts',
38
- resolveDir: __dirname, // Resolve React from CLI's node_modules
39
- },
40
- bundle: true,
41
- write: false,
42
- format: 'esm',
43
- target: 'es2020',
44
- minify: true,
45
- })
46
-
47
- // Select JS output file explicitly (in case sourcemaps are added later)
48
- const jsFile = result.outputFiles?.find(f => f.path.endsWith('.js')) || result.outputFiles?.[0]
49
- if (!jsFile) {
50
- return { success: false, code: '', error: 'No output generated' }
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
- return { success: true, code: jsFile.text }
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 -->