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
@@ -0,0 +1,50 @@
1
+ import { useState, useCallback } from 'react'
2
+ import { storage } from '../storage'
3
+ import type { ApprovalStatus, StatusEntry, AuditLogEntry, UserIdentity } from '../types'
4
+
5
+ const DEFAULT_ENTRY: StatusEntry = {
6
+ previewName: '',
7
+ status: 'draft',
8
+ updatedAt: '',
9
+ updatedBy: '',
10
+ }
11
+
12
+ export function useApprovalStatus(previewName: string) {
13
+ const [entry, setEntry] = useState<StatusEntry>(
14
+ () => storage.get<StatusEntry>(`status:${previewName}`) ?? { ...DEFAULT_ENTRY, previewName }
15
+ )
16
+
17
+ const changeStatus = useCallback((newStatus: ApprovalStatus) => {
18
+ const user = storage.get<UserIdentity>('user')
19
+ const now = new Date().toISOString()
20
+
21
+ // Record audit log
22
+ const audit: AuditLogEntry = {
23
+ previewName,
24
+ from: entry.status,
25
+ to: newStatus,
26
+ changedBy: user?.name || 'Anonymous',
27
+ changedAt: now,
28
+ }
29
+ storage.set(`audit:${previewName}:${Date.now()}`, audit)
30
+
31
+ // Update status
32
+ const updated: StatusEntry = {
33
+ previewName,
34
+ status: newStatus,
35
+ updatedAt: now,
36
+ updatedBy: user?.name || 'Anonymous',
37
+ }
38
+ storage.set(`status:${previewName}`, updated)
39
+ setEntry(updated)
40
+ }, [previewName, entry.status])
41
+
42
+ const getAuditLog = useCallback((): AuditLogEntry[] => {
43
+ return storage
44
+ .list(`audit:${previewName}:`)
45
+ .map(k => storage.get<AuditLogEntry>(k))
46
+ .filter(Boolean) as AuditLogEntry[]
47
+ }, [previewName])
48
+
49
+ return { status: entry.status, changeStatus, entry, getAuditLog }
50
+ }
@@ -0,0 +1,147 @@
1
+ import { useState, useCallback, useRef } from 'react'
2
+ import { storage } from '../storage'
3
+ import type { Snapshot } from '../types'
4
+
5
+ const MAX_SNAPSHOTS = 20
6
+
7
+ export function useSnapshots(previewName?: string) {
8
+ const capturingRef = useRef(false)
9
+ const loadAll = (): Snapshot[] => {
10
+ const all = storage
11
+ .list('snapshots:')
12
+ .map(k => storage.get<Snapshot>(k))
13
+ .filter(Boolean) as Snapshot[]
14
+
15
+ // Sort newest first
16
+ all.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
17
+
18
+ return previewName
19
+ ? all.filter(s => s.previewName === previewName)
20
+ : all
21
+ }
22
+
23
+ const [snapshots, setSnapshots] = useState<Snapshot[]>(loadAll)
24
+
25
+ const refresh = useCallback(() => {
26
+ setSnapshots(loadAll())
27
+ }, [previewName])
28
+
29
+ const captureSnapshot = useCallback(
30
+ async (
31
+ iframeRef: React.RefObject<HTMLIFrameElement | null>,
32
+ metadata: {
33
+ previewName: string
34
+ stateOrStep: string
35
+ viewport: string
36
+ label?: string
37
+ },
38
+ ) => {
39
+ if (capturingRef.current) return
40
+ capturingRef.current = true
41
+
42
+ const id = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
43
+ const now = new Date().toISOString()
44
+
45
+ let screenshotDataUrl = ''
46
+
47
+ try {
48
+ const iframe = iframeRef.current
49
+ if (iframe?.contentDocument) {
50
+ const doc = iframe.contentDocument
51
+ const serializer = new XMLSerializer()
52
+ const html = serializer.serializeToString(doc)
53
+ const width = iframe.clientWidth || 800
54
+ const height = iframe.clientHeight || 600
55
+
56
+ // Draw via SVG foreignObject onto canvas
57
+ const svg = `
58
+ <svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
59
+ <foreignObject width="100%" height="100%">
60
+ ${html}
61
+ </foreignObject>
62
+ </svg>
63
+ `
64
+
65
+ const canvas = document.createElement('canvas')
66
+ canvas.width = width
67
+ canvas.height = height
68
+ const ctx = canvas.getContext('2d')
69
+ const img = new Image()
70
+ const blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' })
71
+ const url = URL.createObjectURL(blob)
72
+
73
+ try {
74
+ img.src = url
75
+ await img.decode()
76
+ ctx?.drawImage(img, 0, 0)
77
+ screenshotDataUrl = canvas.toDataURL('image/png')
78
+ } catch {
79
+ // tainted canvas or draw failure
80
+ } finally {
81
+ URL.revokeObjectURL(url)
82
+ }
83
+ }
84
+ } catch {
85
+ // cross-origin or other access error
86
+ }
87
+
88
+ // Fallback placeholder if capture failed
89
+ if (!screenshotDataUrl) {
90
+ const w = 320
91
+ const h = 200
92
+ const placeholderSvg = `
93
+ <svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}">
94
+ <rect width="${w}" height="${h}" fill="#f0f0f0" rx="8"/>
95
+ <text x="50%" y="45%" text-anchor="middle" fill="#999" font-size="14" font-family="system-ui">
96
+ ${metadata.previewName}
97
+ </text>
98
+ <text x="50%" y="60%" text-anchor="middle" fill="#bbb" font-size="11" font-family="system-ui">
99
+ Cross-origin snapshot
100
+ </text>
101
+ </svg>
102
+ `
103
+ screenshotDataUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(placeholderSvg.trim())}`
104
+ }
105
+
106
+ const snapshot: Snapshot = {
107
+ id,
108
+ previewName: metadata.previewName,
109
+ stateOrStep: metadata.stateOrStep,
110
+ viewport: metadata.viewport,
111
+ screenshotDataUrl,
112
+ createdAt: now,
113
+ label: metadata.label,
114
+ }
115
+
116
+ storage.set(`snapshots:${id}`, snapshot)
117
+
118
+ // Enforce cap: delete oldest beyond MAX_SNAPSHOTS
119
+ const allKeys = storage.list('snapshots:')
120
+ if (allKeys.length > MAX_SNAPSHOTS) {
121
+ const allSnapshots = allKeys
122
+ .map(k => storage.get<Snapshot>(k))
123
+ .filter(Boolean) as Snapshot[]
124
+ allSnapshots.sort((a, b) => a.createdAt.localeCompare(b.createdAt))
125
+
126
+ const toDelete = allSnapshots.slice(0, allSnapshots.length - MAX_SNAPSHOTS)
127
+ for (const s of toDelete) {
128
+ storage.remove(`snapshots:${s.id}`)
129
+ }
130
+ }
131
+
132
+ setSnapshots(loadAll())
133
+ capturingRef.current = false
134
+ },
135
+ [previewName],
136
+ )
137
+
138
+ const deleteSnapshot = useCallback(
139
+ (id: string) => {
140
+ storage.remove(`snapshots:${id}`)
141
+ setSnapshots(loadAll())
142
+ },
143
+ [previewName],
144
+ )
145
+
146
+ return { snapshots, captureSnapshot, deleteSnapshot, refresh }
147
+ }
@@ -0,0 +1,26 @@
1
+ import { useState, useCallback } from 'react'
2
+ import { storage } from '../storage'
3
+
4
+ export function useStorage<T>(key: string, initial: T): [T, (v: T) => void] {
5
+ const [value, setValue] = useState<T>(() => storage.get<T>(key) ?? initial)
6
+
7
+ const update = useCallback((v: T) => {
8
+ storage.set(key, v)
9
+ setValue(v)
10
+ }, [key])
11
+
12
+ return [value, update]
13
+ }
14
+
15
+ export function useStorageList<T>(prefix: string): [T[], () => void] {
16
+ const load = () =>
17
+ storage.list(prefix).map(k => storage.get<T>(k)).filter(Boolean) as T[]
18
+
19
+ const [items, setItems] = useState<T[]>(load)
20
+
21
+ const refresh = useCallback(() => {
22
+ setItems(load())
23
+ }, [prefix])
24
+
25
+ return [items, refresh]
26
+ }
@@ -0,0 +1,56 @@
1
+ import { useState, useCallback } from 'react'
2
+ import { storage } from '../storage'
3
+ import type { TokenOverride } from '../types'
4
+
5
+ const STORAGE_KEY = 'token-overrides'
6
+
7
+ export function useTokenOverrides() {
8
+ const [overrides, setOverrides] = useState<TokenOverride[]>(
9
+ () => storage.get<TokenOverride[]>(STORAGE_KEY) ?? []
10
+ )
11
+
12
+ const persist = (next: TokenOverride[]) => {
13
+ storage.set(STORAGE_KEY, next)
14
+ setOverrides(next)
15
+ }
16
+
17
+ const setOverride = useCallback(
18
+ (category: string, name: string, originalValue: string, newValue: string) => {
19
+ setOverrides(prev => {
20
+ const idx = prev.findIndex(o => o.category === category && o.name === name)
21
+ const entry: TokenOverride = { category, name, originalValue, overrideValue: newValue }
22
+ const next = idx >= 0
23
+ ? prev.map((o, i) => (i === idx ? entry : o))
24
+ : [...prev, entry]
25
+ storage.set(STORAGE_KEY, next)
26
+ return next
27
+ })
28
+ },
29
+ [],
30
+ )
31
+
32
+ const removeOverride = useCallback(
33
+ (category: string, name: string) => {
34
+ setOverrides(prev => {
35
+ const next = prev.filter(o => !(o.category === category && o.name === name))
36
+ storage.set(STORAGE_KEY, next)
37
+ return next
38
+ })
39
+ },
40
+ [],
41
+ )
42
+
43
+ const resetAll = useCallback(() => {
44
+ persist([])
45
+ }, [])
46
+
47
+ const toCssOverrides = useCallback((): string => {
48
+ if (overrides.length === 0) return ''
49
+ const declarations = overrides
50
+ .map(o => ` --prev-token-${o.category}-${o.name}: ${o.overrideValue};`)
51
+ .join('\n')
52
+ return `:root {\n${declarations}\n}`
53
+ }, [overrides])
54
+
55
+ return { overrides, setOverride, removeOverride, resetAll, toCssOverrides }
56
+ }
@@ -0,0 +1,23 @@
1
+ import { useState, useCallback } from 'react'
2
+ import { storage } from '../storage'
3
+
4
+ export type Viewport = 'mobile' | 'tablet' | 'desktop'
5
+
6
+ export const VIEWPORT_WIDTHS: Record<Viewport, number> = {
7
+ mobile: 375,
8
+ tablet: 768,
9
+ desktop: 1280,
10
+ }
11
+
12
+ export function useViewport(): [Viewport, (v: Viewport) => void] {
13
+ const [viewport, setViewportState] = useState<Viewport>(
14
+ () => storage.get<Viewport>('viewport-pref') ?? 'desktop'
15
+ )
16
+
17
+ const setViewport = useCallback((v: Viewport) => {
18
+ storage.set('viewport-pref', v)
19
+ setViewportState(v)
20
+ }, [])
21
+
22
+ return [viewport, setViewport]
23
+ }
@@ -92,13 +92,51 @@ export function IconSprite() {
92
92
  <symbol id="icon-arrow-left" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
93
93
  <path d="M19 12H5M12 19l-7-7 7-7" />
94
94
  </symbol>
95
+
96
+ {/* Pin (map pin) */}
97
+ <symbol id="icon-pin" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
98
+ <path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7z" />
99
+ <circle cx="12" cy="9" r="2.5" />
100
+ </symbol>
101
+
102
+ {/* Camera */}
103
+ <symbol id="icon-camera" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
104
+ <path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z" />
105
+ <circle cx="12" cy="13" r="4" />
106
+ </symbol>
107
+
108
+ {/* Check Circle */}
109
+ <symbol id="icon-check-circle" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
110
+ <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
111
+ <path d="M22 4L12 14.01l-3-3" />
112
+ </symbol>
113
+
114
+ {/* Palette */}
115
+ <symbol id="icon-palette" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
116
+ <circle cx="13.5" cy="6.5" r="0.5" fill="currentColor" />
117
+ <circle cx="17.5" cy="10.5" r="0.5" fill="currentColor" />
118
+ <circle cx="8.5" cy="7.5" r="0.5" fill="currentColor" />
119
+ <circle cx="6.5" cy="12.5" r="0.5" fill="currentColor" />
120
+ <path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.93 0 1.5-.67 1.5-1.5 0-.39-.15-.74-.39-1.04-.23-.29-.38-.63-.38-1.02 0-.83.67-1.5 1.5-1.5H16c3.31 0 6-2.69 6-6 0-5.17-4.49-8.94-10-8.94z" />
121
+ </symbol>
122
+
123
+ {/* Map / Flow diagram */}
124
+ <symbol id="icon-map" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
125
+ <path d="M1 6v16l7-4 8 4 7-4V2l-7 4-8-4-7 4z" />
126
+ <path d="M8 2v16M16 6v16" />
127
+ </symbol>
128
+
129
+ {/* Message Circle */}
130
+ <symbol id="icon-message-circle" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
131
+ <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z" />
132
+ </symbol>
95
133
  </svg>
96
134
  )
97
135
  }
98
136
 
99
137
  // Icon component that references symbols from the sprite
100
138
  interface IconProps {
101
- name: 'menu' | 'grid' | 'sun' | 'moon' | 'maximize' | 'minimize' | 'x' | 'chevron-right' | 'file' | 'folder' | 'mobile' | 'tablet' | 'desktop' | 'sliders' | 'loader' | 'arrow-left'
139
+ name: 'menu' | 'grid' | 'sun' | 'moon' | 'maximize' | 'minimize' | 'x' | 'chevron-right' | 'file' | 'folder' | 'mobile' | 'tablet' | 'desktop' | 'sliders' | 'loader' | 'arrow-left' | 'pin' | 'camera' | 'check-circle' | 'palette' | 'map' | 'message-circle'
102
140
  size?: number
103
141
  className?: string
104
142
  style?: React.CSSProperties
@@ -0,0 +1,18 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Documentation</title>
7
+ <!-- Preconnect to Google Fonts for faster loading -->
8
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
+ <!-- Preload critical fonts -->
11
+ <link rel="preload" href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700&family=IBM+Plex+Mono:wght@400;500&display=swap" as="style" />
12
+ <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700&family=IBM+Plex+Mono:wght@400;500&display=swap" />
13
+ </head>
14
+ <body>
15
+ <div id="root"></div>
16
+ <script type="module" src="./entry.tsx"></script>
17
+ </body>
18
+ </html>
@@ -33,7 +33,7 @@ function routeExists(href: string): boolean {
33
33
  // Custom link component that validates internal links and uses router
34
34
  function MdxLink({ href, children, ...props }: React.AnchorHTMLAttributes<HTMLAnchorElement>) {
35
35
  const isInternal = isInternalLink(href || '')
36
- const isDev = import.meta.env?.DEV ?? false
36
+ const isDev = import.meta.env.DEV ?? false
37
37
 
38
38
  // For internal links, use TanStack Router's Link
39
39
  if (isInternal && href) {
@@ -0,0 +1,285 @@
1
+ import React, { useState, useRef, useEffect } from 'react'
2
+ import type { AnnotationCategory } from '../types'
3
+ import { useAnnotations } from '../hooks/useAnnotations'
4
+ import { AnnotationPin } from './AnnotationPin'
5
+ import { AnnotationThread } from './AnnotationThread'
6
+ import { Icon } from '../icons'
7
+
8
+ const CATEGORY_COLORS: Record<AnnotationCategory, string> = {
9
+ bug: 'oklch(0.65 0.20 25)',
10
+ copy: 'oklch(0.65 0.15 250)',
11
+ design: 'oklch(0.65 0.18 310)',
12
+ general: 'oklch(0.65 0.10 85)',
13
+ }
14
+
15
+ const CATEGORIES: AnnotationCategory[] = ['bug', 'copy', 'design', 'general']
16
+
17
+ interface AnnotationLayerProps {
18
+ previewName: string
19
+ stateOrStep: string
20
+ enabled: boolean
21
+ children: React.ReactNode
22
+ }
23
+
24
+ interface PendingPin {
25
+ x: number
26
+ y: number
27
+ }
28
+
29
+ export function AnnotationLayer({ previewName, stateOrStep, enabled, children }: AnnotationLayerProps) {
30
+ const { annotations, createAnnotation, addComment, resolveAnnotation, deleteAnnotation } =
31
+ useAnnotations(previewName, stateOrStep)
32
+ const [activeId, setActiveId] = useState<string | null>(null)
33
+ const [pending, setPending] = useState<PendingPin | null>(null)
34
+ const [pendingCategory, setPendingCategory] = useState<AnnotationCategory>('general')
35
+ const [pendingText, setPendingText] = useState('')
36
+ const containerRef = useRef<HTMLDivElement>(null)
37
+ const formRef = useRef<HTMLDivElement>(null)
38
+
39
+ // Filter annotations for the current state/step
40
+ const visible = annotations.filter(a => a.stateOrStep === stateOrStep)
41
+
42
+ // Close on outside click
43
+ useEffect(() => {
44
+ if (!activeId && !pending) return
45
+ const handler = (e: MouseEvent) => {
46
+ if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
47
+ setActiveId(null)
48
+ setPending(null)
49
+ }
50
+ }
51
+ document.addEventListener('mousedown', handler)
52
+ return () => document.removeEventListener('mousedown', handler)
53
+ }, [activeId, pending])
54
+
55
+ const handleOverlayClick = (e: React.MouseEvent<HTMLDivElement>) => {
56
+ if (!enabled) return
57
+ const rect = e.currentTarget.getBoundingClientRect()
58
+ const x = ((e.clientX - rect.left) / rect.width) * 100
59
+ const y = ((e.clientY - rect.top) / rect.height) * 100
60
+ setActiveId(null)
61
+ setPending({ x, y })
62
+ setPendingCategory('general')
63
+ setPendingText('')
64
+ }
65
+
66
+ const handleCreate = () => {
67
+ const text = pendingText.trim()
68
+ if (!text || !pending) return
69
+ createAnnotation(pending.x, pending.y, pendingCategory, text)
70
+ setPending(null)
71
+ setPendingText('')
72
+ }
73
+
74
+ return (
75
+ <div ref={containerRef} style={{ position: 'relative' }}>
76
+ {children}
77
+
78
+ {/* Clickable overlay when annotation mode is enabled */}
79
+ {enabled && (
80
+ <div
81
+ onClick={handleOverlayClick}
82
+ style={{
83
+ position: 'absolute',
84
+ inset: 0,
85
+ cursor: 'crosshair',
86
+ zIndex: 15,
87
+ }}
88
+ />
89
+ )}
90
+
91
+ {/* Existing pins */}
92
+ {visible.map((a, i) => (
93
+ <AnnotationPin
94
+ key={a.id}
95
+ x={a.x}
96
+ y={a.y}
97
+ index={i + 1}
98
+ category={a.category}
99
+ resolved={a.resolved}
100
+ isActive={activeId === a.id}
101
+ onClick={() => {
102
+ setActiveId(activeId === a.id ? null : a.id)
103
+ setPending(null)
104
+ }}
105
+ />
106
+ ))}
107
+
108
+ {/* Active thread */}
109
+ {activeId && (() => {
110
+ const annotation = visible.find(a => a.id === activeId)
111
+ if (!annotation) return null
112
+ return (
113
+ <AnnotationThread
114
+ annotation={annotation}
115
+ onAddComment={(text) => addComment(annotation.id, text)}
116
+ onResolve={() => resolveAnnotation(annotation.id)}
117
+ onDelete={() => {
118
+ deleteAnnotation(annotation.id)
119
+ setActiveId(null)
120
+ }}
121
+ onClose={() => setActiveId(null)}
122
+ />
123
+ )
124
+ })()}
125
+
126
+ {/* New annotation form */}
127
+ {pending && (
128
+ <div
129
+ ref={formRef}
130
+ onClick={(e) => e.stopPropagation()}
131
+ style={{
132
+ position: 'absolute',
133
+ left: `${pending.x}%`,
134
+ top: `${pending.y}%`,
135
+ transform: 'translate(12px, -12px)',
136
+ width: '240px',
137
+ backgroundColor: 'var(--fd-card)',
138
+ borderRadius: '12px',
139
+ boxShadow: '0 12px 40px -8px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.06)',
140
+ zIndex: 30,
141
+ overflow: 'hidden',
142
+ fontSize: '13px',
143
+ }}
144
+ >
145
+ {/* Category selector */}
146
+ <div style={{
147
+ padding: '10px 12px',
148
+ borderBottom: '1px solid var(--fd-border)',
149
+ backgroundColor: 'var(--fd-muted)',
150
+ }}>
151
+ <div style={{
152
+ fontSize: '11px',
153
+ fontWeight: 600,
154
+ color: 'var(--fd-muted-foreground)',
155
+ textTransform: 'uppercase',
156
+ letterSpacing: '0.04em',
157
+ marginBottom: '8px',
158
+ }}>
159
+ Category
160
+ </div>
161
+ <div style={{ display: 'flex', gap: '6px' }}>
162
+ {CATEGORIES.map(cat => (
163
+ <button
164
+ key={cat}
165
+ onClick={() => setPendingCategory(cat)}
166
+ title={cat}
167
+ style={{
168
+ width: '28px',
169
+ height: '28px',
170
+ borderRadius: '50%',
171
+ border: pendingCategory === cat
172
+ ? '2px solid var(--fd-foreground)'
173
+ : '2px solid transparent',
174
+ backgroundColor: CATEGORY_COLORS[cat],
175
+ cursor: 'pointer',
176
+ padding: 0,
177
+ boxShadow: pendingCategory === cat
178
+ ? `0 0 0 2px var(--fd-card), 0 0 0 4px ${CATEGORY_COLORS[cat]}`
179
+ : 'none',
180
+ transition: 'box-shadow 0.15s ease',
181
+ }}
182
+ />
183
+ ))}
184
+ </div>
185
+ </div>
186
+
187
+ {/* Comment input */}
188
+ <div style={{ padding: '10px 12px' }}>
189
+ <textarea
190
+ value={pendingText}
191
+ onChange={e => setPendingText(e.target.value)}
192
+ onKeyDown={e => {
193
+ if (e.key === 'Enter' && !e.shiftKey) {
194
+ e.preventDefault()
195
+ handleCreate()
196
+ }
197
+ }}
198
+ placeholder="Describe the issue..."
199
+ autoFocus
200
+ rows={3}
201
+ style={{
202
+ width: '100%',
203
+ padding: '8px 10px',
204
+ fontSize: '12px',
205
+ border: '1px solid var(--fd-border)',
206
+ borderRadius: '6px',
207
+ backgroundColor: 'var(--fd-background)',
208
+ color: 'var(--fd-foreground)',
209
+ outline: 'none',
210
+ resize: 'vertical',
211
+ fontFamily: 'inherit',
212
+ boxSizing: 'border-box',
213
+ }}
214
+ />
215
+ </div>
216
+
217
+ {/* Actions */}
218
+ <div style={{
219
+ padding: '8px 12px',
220
+ borderTop: '1px solid var(--fd-border)',
221
+ display: 'flex',
222
+ gap: '6px',
223
+ justifyContent: 'flex-end',
224
+ }}>
225
+ <button
226
+ onClick={() => setPending(null)}
227
+ style={{
228
+ padding: '5px 10px',
229
+ fontSize: '11px',
230
+ fontWeight: 500,
231
+ border: '1px solid var(--fd-border)',
232
+ borderRadius: '6px',
233
+ backgroundColor: 'var(--fd-background)',
234
+ color: 'var(--fd-muted-foreground)',
235
+ cursor: 'pointer',
236
+ }}
237
+ >
238
+ Cancel
239
+ </button>
240
+ <button
241
+ onClick={handleCreate}
242
+ style={{
243
+ padding: '5px 12px',
244
+ fontSize: '11px',
245
+ fontWeight: 600,
246
+ border: 'none',
247
+ borderRadius: '6px',
248
+ backgroundColor: 'var(--fd-primary)',
249
+ color: 'var(--fd-primary-foreground)',
250
+ cursor: 'pointer',
251
+ opacity: pendingText.trim() ? 1 : 0.5,
252
+ }}
253
+ disabled={!pendingText.trim()}
254
+ >
255
+ Add Pin
256
+ </button>
257
+ </div>
258
+ </div>
259
+ )}
260
+
261
+ {/* Pending pin marker */}
262
+ {pending && (
263
+ <div style={{
264
+ position: 'absolute',
265
+ left: `${pending.x}%`,
266
+ top: `${pending.y}%`,
267
+ transform: 'translate(-50%, -50%)',
268
+ width: '24px',
269
+ height: '24px',
270
+ borderRadius: '50%',
271
+ border: '2px solid white',
272
+ backgroundColor: CATEGORY_COLORS[pendingCategory],
273
+ boxShadow: `0 0 0 3px ${CATEGORY_COLORS[pendingCategory]}, 0 2px 8px rgba(0, 0, 0, 0.3)`,
274
+ zIndex: 22,
275
+ pointerEvents: 'none',
276
+ display: 'flex',
277
+ alignItems: 'center',
278
+ justifyContent: 'center',
279
+ }}>
280
+ <Icon name="pin" size={14} style={{ color: 'white' }} />
281
+ </div>
282
+ )}
283
+ </div>
284
+ )
285
+ }