proto-plugin 0.1.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 (209) hide show
  1. package/AGENTS.md +350 -0
  2. package/README.md +279 -0
  3. package/components.json +18 -0
  4. package/package.json +73 -0
  5. package/scripts/download-mobbin-reference-images.mjs +112 -0
  6. package/scripts/lib/host-config.example.sh +48 -0
  7. package/scripts/lib/host-config.sh +25 -0
  8. package/scripts/lib/resolve-host-root.mjs +18 -0
  9. package/scripts/lib/source-path.sh +49 -0
  10. package/scripts/link-source-db.sh +128 -0
  11. package/scripts/link-source.sh +40 -0
  12. package/scripts/publish-package.sh +147 -0
  13. package/scripts/share-prototype.sh +279 -0
  14. package/scripts/sync-from-source.sh +74 -0
  15. package/scripts/verify-prototype-component-ids.mjs +151 -0
  16. package/scripts/verify-prototype-preview-states.mjs +135 -0
  17. package/src/app.ts +4 -0
  18. package/src/components/platform-ui/code-block.tsx +21 -0
  19. package/src/components/platform-ui/controls-panel-options.tsx +71 -0
  20. package/src/components/platform-ui/controls-panel-select.tsx +438 -0
  21. package/src/components/platform-ui/empty-state.tsx +15 -0
  22. package/src/components/platform-ui/icon-button.tsx +18 -0
  23. package/src/components/platform-ui/panel-select.tsx +123 -0
  24. package/src/components/platform-ui/panel.tsx +97 -0
  25. package/src/components/platform-ui/review-dropdown-menu.tsx +35 -0
  26. package/src/components/platform-ui/sidebar.tsx +293 -0
  27. package/src/components/platform-ui/toolbar-icon-button.tsx +34 -0
  28. package/src/components/platform-ui/toolbar.tsx +37 -0
  29. package/src/components/prototype-gallery-client.tsx +57 -0
  30. package/src/components/prototype-provider.tsx +20 -0
  31. package/src/components/prototypes/mini-pill-label.tsx +37 -0
  32. package/src/components/prototypes/prototype-change-log-panel.tsx +207 -0
  33. package/src/components/prototypes/prototype-comment-provider.tsx +61 -0
  34. package/src/components/prototypes/prototype-comment-toolbar.tsx +65 -0
  35. package/src/components/prototypes/prototype-control.tsx +31 -0
  36. package/src/components/prototypes/prototype-controls.tsx +553 -0
  37. package/src/components/prototypes/prototype-create-exploration-modal.tsx +117 -0
  38. package/src/components/prototypes/prototype-create-state-modal.tsx +117 -0
  39. package/src/components/prototypes/prototype-default-state-map-page.tsx +66 -0
  40. package/src/components/prototypes/prototype-design-brief-modal.tsx +128 -0
  41. package/src/components/prototypes/prototype-design-brief-panel.tsx +525 -0
  42. package/src/components/prototypes/prototype-design-context-panel.tsx +65 -0
  43. package/src/components/prototypes/prototype-floating-pill.module.scss +1341 -0
  44. package/src/components/prototypes/prototype-mobbin-gallery.tsx +330 -0
  45. package/src/components/prototypes/prototype-reference-docs.tsx +222 -0
  46. package/src/components/prototypes/prototype-review-chrome.module.scss +630 -0
  47. package/src/components/prototypes/prototype-review-chrome.tsx +1241 -0
  48. package/src/components/prototypes/prototype-review-dropdown-menu-item.tsx +1 -0
  49. package/src/components/prototypes/prototype-review-dropdown-menu-separator.tsx +1 -0
  50. package/src/components/prototypes/prototype-review-panel-menu-item.tsx +1 -0
  51. package/src/components/prototypes/prototype-review-panel-menu-separator.tsx +1 -0
  52. package/src/components/prototypes/prototype-review-panel-select.tsx +4 -0
  53. package/src/components/prototypes/prototype-review-sidebar.tsx +167 -0
  54. package/src/components/prototypes/prototype-share-link-restore.tsx +75 -0
  55. package/src/components/prototypes/prototype-share-mode.tsx +206 -0
  56. package/src/components/prototypes/prototype-spec-panel-content.tsx +538 -0
  57. package/src/components/prototypes/prototype-state-canvas-overlay.module.scss +336 -0
  58. package/src/components/prototypes/prototype-state-canvas-overlay.tsx +1023 -0
  59. package/src/components/prototypes/prototype-state-canvas.tsx +38 -0
  60. package/src/components/prototypes/prototype-state-screenshot-preview.tsx +47 -0
  61. package/src/components/prototypes/prototype-target.tsx +120 -0
  62. package/src/components/prototypes/prototype-tweak-highlight.tsx +132 -0
  63. package/src/components/prototypes/prototype-tweaks.tsx +48 -0
  64. package/src/components/prototypes/prototype-variant-explorer.tsx +347 -0
  65. package/src/components/prototypes/prototype-variant-rationale-panel.tsx +49 -0
  66. package/src/components/prototypes/prototype-variant-tabs.tsx +207 -0
  67. package/src/components/prototypes/prototype-vercel-preview-modal.tsx +120 -0
  68. package/src/components/prototypes/state-map-annotation-tooltip.module.scss +89 -0
  69. package/src/components/prototypes/state-map-annotation-tooltip.tsx +137 -0
  70. package/src/components/prototypes/state-map-wireframe-highlight.module.scss +25 -0
  71. package/src/components/prototypes/state-map-wireframe-highlight.tsx +82 -0
  72. package/src/components/shell/prototype-gallery-shell.tsx +79 -0
  73. package/src/components/shell/prototype-screenshot-capture.tsx +45 -0
  74. package/src/components/shell/prototype-shell.module.scss +194 -0
  75. package/src/components/shell/prototype-shell.tsx +160 -0
  76. package/src/components/shell/prototype-state-screenshot-capture.tsx +68 -0
  77. package/src/components/ui/button.tsx +58 -0
  78. package/src/components/ui/dialog.module.scss +81 -0
  79. package/src/components/ui/dialog.tsx +127 -0
  80. package/src/components/ui/dropdown-menu.tsx +200 -0
  81. package/src/components/ui/input.tsx +22 -0
  82. package/src/components/ui/label.tsx +26 -0
  83. package/src/components/ui/option-select.tsx +81 -0
  84. package/src/components/ui/scroll-area.tsx +48 -0
  85. package/src/components/ui/separator.tsx +31 -0
  86. package/src/components/ui/slider.tsx +28 -0
  87. package/src/components/ui/switch.tsx +29 -0
  88. package/src/components/ui/tabs.tsx +55 -0
  89. package/src/components/ui/toggle-group.tsx +80 -0
  90. package/src/components/ui/tooltip.tsx +32 -0
  91. package/src/config/create-prototype-gallery-page.tsx +65 -0
  92. package/src/config/create-prototype-host-app.tsx +121 -0
  93. package/src/config/create-prototype-page.tsx +41 -0
  94. package/src/config/create-prototype-site-layout.tsx +28 -0
  95. package/src/config/define-prototype-config.ts +7 -0
  96. package/src/config/prototype-icon.tsx +25 -0
  97. package/src/config/prototype-site-layout-client.tsx +35 -0
  98. package/src/config/with-prototype.ts +20 -0
  99. package/src/index.ts +303 -0
  100. package/src/lib/pr-split/build-pr-agent-prompt.ts +68 -0
  101. package/src/lib/pr-split/build-pr-split-prototype-url.ts +12 -0
  102. package/src/lib/pr-split/pr-split-highlight.ts +6 -0
  103. package/src/lib/pr-split/pr-split-types.ts +58 -0
  104. package/src/lib/pr-split/resolve-source-preview-url.ts +45 -0
  105. package/src/lib/pr-split/use-pr-vercel-previews.ts +69 -0
  106. package/src/lib/prototype-comments/core/annotation-channel.ts +41 -0
  107. package/src/lib/prototype-comments/core/annotation-status.ts +60 -0
  108. package/src/lib/prototype-comments/core/annotation-target.ts +567 -0
  109. package/src/lib/prototype-comments/core/capture-theme.ts +41 -0
  110. package/src/lib/prototype-comments/core/capture.ts +11 -0
  111. package/src/lib/prototype-comments/core/comment-capture-blocked.ts +47 -0
  112. package/src/lib/prototype-comments/core/comment-threads.ts +52 -0
  113. package/src/lib/prototype-comments/core/element-identification.ts +615 -0
  114. package/src/lib/prototype-comments/core/export.ts +35 -0
  115. package/src/lib/prototype-comments/core/freeze-animations.ts +266 -0
  116. package/src/lib/prototype-comments/core/identify-element.ts +76 -0
  117. package/src/lib/prototype-comments/core/normalize-viewport.ts +35 -0
  118. package/src/lib/prototype-comments/core/react-detection.ts +704 -0
  119. package/src/lib/prototype-comments/core/restore.ts +7 -0
  120. package/src/lib/prototype-comments/core/screenshot.ts +428 -0
  121. package/src/lib/prototype-comments/core/source-location.ts +906 -0
  122. package/src/lib/prototype-comments/core/storage.ts +80 -0
  123. package/src/lib/prototype-comments/core/types.ts +79 -0
  124. package/src/lib/prototype-comments/core/validation.ts +17 -0
  125. package/src/lib/prototype-comments/hooks/useLayoutShift.ts +61 -0
  126. package/src/lib/prototype-comments/index.ts +58 -0
  127. package/src/lib/prototype-comments/react/CommentCaptureToolbar.tsx +1398 -0
  128. package/src/lib/prototype-comments/react/CommentProvider.tsx +298 -0
  129. package/src/lib/prototype-comments/scss.d.ts +10 -0
  130. package/src/lib/prototype-comments/ui/CommentAnnotationOverlay.tsx +253 -0
  131. package/src/lib/prototype-comments/ui/CommentDetailPage.tsx +56 -0
  132. package/src/lib/prototype-comments/ui/CommentHeaderCount.tsx +28 -0
  133. package/src/lib/prototype-comments/ui/CommentThread.tsx +331 -0
  134. package/src/lib/prototype-comments/ui/CommentsGrid.tsx +780 -0
  135. package/src/lib/prototype-comments/ui/CommentsListPage.tsx +25 -0
  136. package/src/lib/prototype-comments/ui/CommentsSidebar.tsx +77 -0
  137. package/src/lib/prototype-comments/ui/annotation-marker/index.tsx +200 -0
  138. package/src/lib/prototype-comments/ui/annotation-marker/styles.module.scss +194 -0
  139. package/src/lib/prototype-comments/ui/annotation-popup/index.tsx +335 -0
  140. package/src/lib/prototype-comments/ui/annotation-popup/styles.module.scss +469 -0
  141. package/src/lib/prototype-comments/ui/capture-styles.module.scss +2225 -0
  142. package/src/lib/prototype-comments/ui/comment-highlight.ts +48 -0
  143. package/src/lib/prototype-comments/ui/comment-thread.module.scss +203 -0
  144. package/src/lib/prototype-comments/ui/comments-grid-card.module.scss +253 -0
  145. package/src/lib/prototype-comments/ui/comments-sidebar.module.scss +123 -0
  146. package/src/lib/prototype-comments/ui/icon-transitions.module.scss +51 -0
  147. package/src/lib/prototype-comments/ui/icons.tsx +1086 -0
  148. package/src/lib/prototype-comments/ui/is-comment-detail-path.ts +7 -0
  149. package/src/lib/prototypes/capture-prototype-viewport.ts +32 -0
  150. package/src/lib/prototypes/changelog-meta-context.tsx +162 -0
  151. package/src/lib/prototypes/changelog-meta-storage.ts +62 -0
  152. package/src/lib/prototypes/changelog-meta.ts +95 -0
  153. package/src/lib/prototypes/create-prototype-registry.ts +74 -0
  154. package/src/lib/prototypes/create-state-screenshot-thumbnail.ts +46 -0
  155. package/src/lib/prototypes/design-exploration-default-storage.ts +68 -0
  156. package/src/lib/prototypes/design-exploration-default.ts +51 -0
  157. package/src/lib/prototypes/design-exploration-types.ts +203 -0
  158. package/src/lib/prototypes/prototype-comment-registry.tsx +145 -0
  159. package/src/lib/prototypes/prototype-comment-review-bridge.tsx +40 -0
  160. package/src/lib/prototypes/prototype-comment-review-state.ts +57 -0
  161. package/src/lib/prototypes/prototype-component-registry.ts +40 -0
  162. package/src/lib/prototypes/prototype-config-types.ts +34 -0
  163. package/src/lib/prototypes/prototype-preview-state-registry.ts +443 -0
  164. package/src/lib/prototypes/prototype-preview-state-types.ts +89 -0
  165. package/src/lib/prototypes/prototype-review-context.tsx +905 -0
  166. package/src/lib/prototypes/prototype-review-url.ts +134 -0
  167. package/src/lib/prototypes/prototype-share-link.ts +175 -0
  168. package/src/lib/prototypes/prototype-state-canvas-constants.ts +118 -0
  169. package/src/lib/prototypes/prototype-state-canvas-types.ts +159 -0
  170. package/src/lib/prototypes/prototype-state-screenshot.ts +39 -0
  171. package/src/lib/prototypes/prototype-tweak-registry.ts +96 -0
  172. package/src/lib/prototypes/prototype-tweak-types.ts +31 -0
  173. package/src/lib/prototypes/reference-docs.ts +137 -0
  174. package/src/lib/prototypes/screenshot-manifest.ts +33 -0
  175. package/src/lib/prototypes/screenshot-src.ts +7 -0
  176. package/src/lib/prototypes/share-command.ts +8 -0
  177. package/src/lib/prototypes/state-map-annotation.ts +70 -0
  178. package/src/lib/prototypes/use-design-exploration-default.ts +114 -0
  179. package/src/lib/prototypes/use-persisted-local-state.ts +140 -0
  180. package/src/lib/prototypes/use-prototype-comments.ts +63 -0
  181. package/src/lib/prototypes/use-prototype-mobile-viewport-frame.ts +46 -0
  182. package/src/lib/prototypes/use-prototype-state-screenshot-src.ts +71 -0
  183. package/src/lib/prototypes/use-prototype-tool-theme.tsx +115 -0
  184. package/src/lib/prototypes/use-prototype-viewport-frame.ts +34 -0
  185. package/src/lib/prototypes/use-register-prototype-reference-docs.ts +16 -0
  186. package/src/lib/prototypes/use-snap-corner-position.ts +417 -0
  187. package/src/lib/prototypes/use-toolbar-hover-panel.ts +98 -0
  188. package/src/lib/prototypes/variant-set-lucide-icon.tsx +55 -0
  189. package/src/lib/tool-portal.ts +33 -0
  190. package/src/lib/use-copy-to-clipboard.ts +81 -0
  191. package/src/lib/utils.ts +6 -0
  192. package/src/lib/vercel-preview/parse-vercel-github-comment.ts +81 -0
  193. package/src/server/create-host-api-route.ts +100 -0
  194. package/src/server/create-prototype-api-routes.ts +428 -0
  195. package/src/server/gallery/create-gallery-api.ts +56 -0
  196. package/src/server/gallery/folders-route.ts +156 -0
  197. package/src/server/gallery/gallery-env.ts +39 -0
  198. package/src/server/gallery/gallery-route.ts +150 -0
  199. package/src/server/gallery/gallery-types.ts +15 -0
  200. package/src/server/gallery/image-route.ts +59 -0
  201. package/src/server/gallery/item-route.ts +92 -0
  202. package/src/server/pr-vercel-preview-route.ts +162 -0
  203. package/src/server/redis/client.ts +55 -0
  204. package/src/server/redis/prototype-changelog-meta.ts +31 -0
  205. package/src/server/redis/prototype-comments.ts +92 -0
  206. package/src/server/redis/prototype-design-exploration-default.ts +32 -0
  207. package/src/server.ts +51 -0
  208. package/src/styles/globals.css +624 -0
  209. package/tsconfig.json +19 -0
@@ -0,0 +1,538 @@
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useState, type ReactNode } from "react";
4
+ import {
5
+ Check,
6
+ ChevronDown,
7
+ Clock,
8
+ Copy,
9
+ GitBranch,
10
+ GitMerge,
11
+ Github,
12
+ Monitor,
13
+ } from "lucide-react";
14
+
15
+ import { PrototypeComponent } from "./prototype-target";
16
+ import { resolveSourcePreviewUrl } from "../../lib/pr-split/resolve-source-preview-url";
17
+ import { PR_TARGET_HIGHLIGHT_BORDER } from "../../lib/pr-split/pr-split-highlight";
18
+ import type { PrSplitEntry } from "../../lib/pr-split/pr-split-types";
19
+ import { usePrVercelPreviews } from "../../lib/pr-split/use-pr-vercel-previews";
20
+ import type { VercelPreviewFromPr } from "../../lib/vercel-preview/parse-vercel-github-comment";
21
+
22
+ function reviewSidebarCardBorder(
23
+ selected: boolean,
24
+ hovered: boolean,
25
+ ): string {
26
+ if (selected) {
27
+ return PR_TARGET_HIGHLIGHT_BORDER;
28
+ }
29
+ if (hovered) {
30
+ return "var(--border-medium)";
31
+ }
32
+ return "var(--border-solid)";
33
+ }
34
+
35
+ function CopyPrPrompt<TWireframeId extends string, TLiveState>({
36
+ entry,
37
+ buildAgentPrompt,
38
+ }: {
39
+ entry: PrSplitEntry<TWireframeId, TLiveState>;
40
+ buildAgentPrompt: (
41
+ entry: PrSplitEntry<TWireframeId, TLiveState>,
42
+ origin?: string,
43
+ ) => string;
44
+ }) {
45
+ const [copied, setCopied] = useState(false);
46
+
47
+ const handleCopy = (e: React.MouseEvent) => {
48
+ e.stopPropagation();
49
+ const origin =
50
+ typeof window !== "undefined" ? window.location.origin : undefined;
51
+ const prompt = buildAgentPrompt(
52
+ entry,
53
+ origin ?? "http://localhost:3003",
54
+ );
55
+ navigator.clipboard.writeText(prompt).then(() => {
56
+ setCopied(true);
57
+ setTimeout(() => setCopied(false), 1800);
58
+ });
59
+ };
60
+
61
+ return (
62
+ <button
63
+ type="button"
64
+ aria-label="Copy PR prompt"
65
+ onClick={handleCopy}
66
+ className="inline-flex size-[13px] shrink-0 items-center justify-center text-muted-foreground/60 transition-colors duration-200 ease hover:text-muted-foreground"
67
+ >
68
+ {copied ? (
69
+ <Check className="size-3 text-[var(--text-primary)]" />
70
+ ) : (
71
+ <Copy className="size-3" />
72
+ )}
73
+ </button>
74
+ );
75
+ }
76
+
77
+ function LinearIcon({ className }: { className?: string }) {
78
+ return (
79
+ <svg
80
+ viewBox="0 0 16 16"
81
+ fill="none"
82
+ stroke="currentColor"
83
+ strokeWidth="1.75"
84
+ strokeLinecap="round"
85
+ aria-hidden
86
+ className={className}
87
+ >
88
+ <path d="M2.5 13.5 6.5 9.5" />
89
+ <path d="M6.5 13.5 13.5 6.5" />
90
+ <path d="M2.5 6.5 9.5 2.5" />
91
+ </svg>
92
+ );
93
+ }
94
+
95
+ function CopyLinearLink({ linearUrl }: { linearUrl: string }) {
96
+ const [copied, setCopied] = useState(false);
97
+
98
+ const handleCopy = (e: React.MouseEvent) => {
99
+ e.stopPropagation();
100
+ navigator.clipboard.writeText(linearUrl).then(() => {
101
+ setCopied(true);
102
+ setTimeout(() => setCopied(false), 1800);
103
+ });
104
+ };
105
+
106
+ return (
107
+ <button
108
+ type="button"
109
+ aria-label={`Copy Linear issue URL: ${linearUrl}`}
110
+ onClick={handleCopy}
111
+ className="group inline-flex h-6 shrink-0 items-center gap-1 rounded-md border border-[var(--border-solid)] bg-[var(--bg-subtle)] px-2 text-[11px] text-[var(--text-secondary)] transition-colors duration-150 ease hover:bg-[var(--bg-layered)] hover:text-[var(--text-primary)]"
112
+ >
113
+ <LinearIcon className="size-3 shrink-0 text-[var(--text-tertiary)] transition-colors duration-150 ease group-hover:text-[var(--text-secondary)]" />
114
+ Linear
115
+ {copied ? (
116
+ <Check className="size-3 text-[var(--text-primary)]" aria-hidden />
117
+ ) : (
118
+ <Copy className="size-3 text-[var(--text-tertiary)] transition-colors duration-150 ease group-hover:text-[var(--text-secondary)]" aria-hidden />
119
+ )}
120
+ </button>
121
+ );
122
+ }
123
+
124
+ function CopyBranch({ branch }: { branch: string }) {
125
+ const [copied, setCopied] = useState(false);
126
+
127
+ const handleCopy = (e: React.MouseEvent) => {
128
+ e.stopPropagation();
129
+ navigator.clipboard.writeText(branch).then(() => {
130
+ setCopied(true);
131
+ setTimeout(() => setCopied(false), 1800);
132
+ });
133
+ };
134
+
135
+ return (
136
+ <button
137
+ type="button"
138
+ aria-label={`Copy branch name: ${branch}`}
139
+ onClick={handleCopy}
140
+ className="group inline-flex h-6 shrink-0 items-center gap-1 rounded-md border border-[var(--border-solid)] bg-[var(--bg-subtle)] px-2 text-[11px] text-[var(--text-secondary)] transition-colors duration-150 ease hover:bg-[var(--bg-layered)] hover:text-[var(--text-primary)]"
141
+ >
142
+ <GitBranch
143
+ className="size-3 shrink-0 text-[var(--text-tertiary)] transition-colors duration-150 ease group-hover:text-[var(--text-secondary)]"
144
+ aria-hidden
145
+ />
146
+ Branch
147
+ {copied ? (
148
+ <Check className="size-3 text-[var(--text-primary)]" aria-hidden />
149
+ ) : (
150
+ <Copy className="size-3 text-[var(--text-tertiary)] transition-colors duration-150 ease group-hover:text-[var(--text-secondary)]" aria-hidden />
151
+ )}
152
+ </button>
153
+ );
154
+ }
155
+
156
+ function CopyPrLink({ prUrl }: { prUrl: string }) {
157
+ const [copied, setCopied] = useState(false);
158
+
159
+ const handleCopy = (e: React.MouseEvent) => {
160
+ e.stopPropagation();
161
+ navigator.clipboard.writeText(prUrl).then(() => {
162
+ setCopied(true);
163
+ setTimeout(() => setCopied(false), 1800);
164
+ });
165
+ };
166
+
167
+ return (
168
+ <button
169
+ type="button"
170
+ aria-label={`Copy pull request URL: ${prUrl}`}
171
+ onClick={handleCopy}
172
+ className="group inline-flex h-6 shrink-0 items-center gap-1 rounded-md border border-[var(--border-solid)] bg-[var(--bg-subtle)] px-2 text-[11px] text-[var(--text-secondary)] transition-colors duration-150 ease hover:bg-[var(--bg-layered)] hover:text-[var(--text-primary)]"
173
+ >
174
+ <Github
175
+ className="size-3 shrink-0 text-[var(--text-tertiary)] transition-colors duration-150 ease group-hover:text-[var(--text-secondary)]"
176
+ aria-hidden
177
+ />
178
+ PR
179
+ {copied ? (
180
+ <Check className="size-3 text-[var(--text-primary)]" aria-hidden />
181
+ ) : (
182
+ <Copy className="size-3 text-[var(--text-tertiary)] transition-colors duration-150 ease group-hover:text-[var(--text-secondary)]" aria-hidden />
183
+ )}
184
+ </button>
185
+ );
186
+ }
187
+
188
+ function CopyPreviewLink<TWireframeId extends string, TLiveState>({
189
+ entry,
190
+ previewCache,
191
+ previewLoading,
192
+ defaultPreviewPath,
193
+ vercelProjectName,
194
+ }: {
195
+ entry: PrSplitEntry<TWireframeId, TLiveState>;
196
+ previewCache: Map<string, VercelPreviewFromPr>;
197
+ previewLoading: boolean;
198
+ defaultPreviewPath?: string;
199
+ vercelProjectName?: string;
200
+ }) {
201
+ const [copied, setCopied] = useState(false);
202
+ const cached = entry.prUrl ? previewCache.get(entry.prUrl) : undefined;
203
+ const resolved = resolveSourcePreviewUrl(cached ?? null, entry, {
204
+ defaultPreviewPath: defaultPreviewPath ?? "/",
205
+ vercelProjectName: vercelProjectName ?? "",
206
+ });
207
+ const previewUrl = resolved?.previewUrl ?? null;
208
+
209
+ const handleCopy = (e: React.MouseEvent) => {
210
+ e.stopPropagation();
211
+ if (!previewUrl) return;
212
+ navigator.clipboard.writeText(previewUrl).then(() => {
213
+ setCopied(true);
214
+ setTimeout(() => setCopied(false), 1800);
215
+ });
216
+ };
217
+
218
+ return (
219
+ <button
220
+ type="button"
221
+ disabled={!previewUrl}
222
+ aria-label={
223
+ previewUrl
224
+ ? `Copy preview URL: ${previewUrl}`
225
+ : previewLoading
226
+ ? "Loading preview URL"
227
+ : "Preview unavailable"
228
+ }
229
+ onClick={handleCopy}
230
+ className="group inline-flex h-6 shrink-0 items-center gap-1 rounded-md border border-[var(--border-solid)] bg-[var(--bg-subtle)] px-2 text-[11px] text-[var(--text-secondary)] transition-colors duration-150 ease hover:bg-[var(--bg-layered)] hover:text-[var(--text-primary)] disabled:cursor-not-allowed disabled:opacity-60"
231
+ >
232
+ <Monitor
233
+ className="size-3 shrink-0 text-[var(--text-tertiary)] transition-colors duration-150 ease group-hover:text-[var(--text-secondary)] group-disabled:opacity-60"
234
+ aria-hidden
235
+ />
236
+ Preview
237
+ {copied ? (
238
+ <Check className="size-3 text-[var(--text-primary)]" aria-hidden />
239
+ ) : (
240
+ <Copy className="size-3 text-[var(--text-tertiary)] transition-colors duration-150 ease group-hover:text-[var(--text-secondary)] group-disabled:opacity-60" aria-hidden />
241
+ )}
242
+ </button>
243
+ );
244
+ }
245
+
246
+ function PrStatusLink({
247
+ prUrl,
248
+ status,
249
+ }: {
250
+ prUrl: string;
251
+ status: "merged" | "approved" | "in-review";
252
+ }) {
253
+ const config = {
254
+ merged: {
255
+ label: "Merged",
256
+ className: "text-[#8957e5]",
257
+ icon: <GitMerge className="size-3.5" aria-hidden />,
258
+ },
259
+ approved: {
260
+ label: "Approved",
261
+ className: "text-green-600",
262
+ icon: <Check className="size-3.5" aria-hidden />,
263
+ },
264
+ "in-review": {
265
+ label: "In Review",
266
+ className: "text-[var(--color-active)]",
267
+ icon: <Clock className="size-3.5" aria-hidden />,
268
+ },
269
+ }[status];
270
+
271
+ return (
272
+ <a
273
+ href={prUrl}
274
+ target="_blank"
275
+ rel="noopener noreferrer"
276
+ aria-label={`Open pull request (${config.label})`}
277
+ onClick={(e) => e.stopPropagation()}
278
+ className={`inline-flex shrink-0 items-center gap-1.5 transition-opacity duration-200 ease hover:opacity-80 ${config.className}`}
279
+ >
280
+ {config.icon}
281
+ <span className="text-[12px] font-medium">{config.label}</span>
282
+ </a>
283
+ );
284
+ }
285
+
286
+ function PrPreviewCard<TWireframeId extends string, TLiveState>({
287
+ entry,
288
+ selected,
289
+ onSelect,
290
+ previewCache,
291
+ previewLoading,
292
+ renderWireframe,
293
+ buildAgentPrompt,
294
+ defaultPreviewPath,
295
+ vercelProjectName,
296
+ }: {
297
+ entry: PrSplitEntry<TWireframeId, TLiveState>;
298
+ selected: boolean;
299
+ onSelect: (entry: PrSplitEntry<TWireframeId, TLiveState>) => void;
300
+ previewCache: Map<string, VercelPreviewFromPr>;
301
+ previewLoading: boolean;
302
+ renderWireframe: (wireframeId: TWireframeId) => ReactNode;
303
+ buildAgentPrompt: (
304
+ entry: PrSplitEntry<TWireframeId, TLiveState>,
305
+ origin?: string,
306
+ ) => string;
307
+ defaultPreviewPath?: string;
308
+ vercelProjectName?: string;
309
+ }) {
310
+ const [hovered, setHovered] = useState(false);
311
+ const isMerged = Boolean(entry.merged);
312
+ const [expanded, setExpanded] = useState(!isMerged);
313
+
314
+ useEffect(() => {
315
+ if (selected) setExpanded(true);
316
+ }, [selected]);
317
+
318
+ const collapseStyle = {
319
+ display: "grid",
320
+ gridTemplateRows: expanded ? "1fr" : "0fr",
321
+ transition: "grid-template-rows 0.25s cubic-bezier(.215, .61, .355, 1)",
322
+ } as const;
323
+
324
+ const collapsedMerged = isMerged && !expanded;
325
+
326
+ return (
327
+ <button
328
+ type="button"
329
+ data-pr-order={entry.order}
330
+ aria-current={selected ? "true" : undefined}
331
+ onClick={() => {
332
+ if (collapsedMerged) {
333
+ setExpanded(true);
334
+ return;
335
+ }
336
+ onSelect(entry);
337
+ }}
338
+ onMouseEnter={() => setHovered(true)}
339
+ onMouseLeave={() => setHovered(false)}
340
+ className="tool-review-card flex w-full flex-col overflow-hidden text-left"
341
+ style={{
342
+ borderRadius: 6,
343
+ border: `1px solid ${reviewSidebarCardBorder(selected, hovered)}`,
344
+ background: collapsedMerged ? "var(--bg-subtle)" : "var(--bg-main)",
345
+ opacity: collapsedMerged ? 0.55 : 1,
346
+ transition:
347
+ "border-color 0.15s ease, background 0.15s ease, opacity 0.15s ease, box-shadow 0.15s ease",
348
+ }}
349
+ >
350
+ <div style={collapseStyle}>
351
+ <div className="overflow-hidden">
352
+ <div className="border-b border-[var(--border-solid)]">
353
+ {renderWireframe(entry.wireframeId)}
354
+ </div>
355
+ </div>
356
+ </div>
357
+ <div className="flex flex-col px-3 pb-3.5 pt-3">
358
+ <div className="flex items-center justify-between gap-3">
359
+ <div className="inline-flex min-w-0 items-center gap-2.5">
360
+ <span className="text-[13px] font-medium text-[var(--text-primary)]">
361
+ PR {entry.order} · {entry.title}
362
+ </span>
363
+ {!entry.prUrl ? (
364
+ <CopyPrPrompt entry={entry} buildAgentPrompt={buildAgentPrompt} />
365
+ ) : null}
366
+ {entry.merged && entry.prUrl ? (
367
+ <PrStatusLink prUrl={entry.prUrl} status="merged" />
368
+ ) : entry.approved && entry.prUrl ? (
369
+ <PrStatusLink prUrl={entry.prUrl} status="approved" />
370
+ ) : entry.prUrl ? (
371
+ <PrStatusLink prUrl={entry.prUrl} status="in-review" />
372
+ ) : null}
373
+ </div>
374
+ <div className="flex shrink-0 items-center gap-1.5">
375
+ <span className="text-[11px] text-[var(--text-tertiary)]">
376
+ {entry.size}
377
+ </span>
378
+ {isMerged ? (
379
+ <span
380
+ role="button"
381
+ tabIndex={0}
382
+ aria-label={expanded ? "Collapse PR" : "Expand PR"}
383
+ aria-expanded={expanded}
384
+ onClick={(e) => {
385
+ e.stopPropagation();
386
+ setExpanded((v) => !v);
387
+ }}
388
+ onKeyDown={(e) => {
389
+ if (e.key === "Enter" || e.key === " ") {
390
+ e.preventDefault();
391
+ e.stopPropagation();
392
+ setExpanded((v) => !v);
393
+ }
394
+ }}
395
+ className="inline-flex size-5 items-center justify-center rounded text-[var(--text-tertiary)] transition-colors duration-200 ease hover:text-[var(--text-secondary)]"
396
+ >
397
+ <ChevronDown
398
+ className="size-4 transition-transform duration-200 ease"
399
+ style={{ transform: expanded ? "rotate(180deg)" : "rotate(0deg)" }}
400
+ aria-hidden
401
+ />
402
+ </span>
403
+ ) : null}
404
+ </div>
405
+ </div>
406
+ <div style={collapseStyle}>
407
+ <div className="overflow-hidden">
408
+ <div className="flex flex-col gap-3 pt-3">
409
+ <p className="text-[12px] leading-normal text-[var(--text-secondary)]">
410
+ {entry.description}
411
+ </p>
412
+ {entry.analyticsNotes && entry.analyticsNotes.length > 0 && (
413
+ <div className="border-l-2 border-[var(--border-faint)] pl-3">
414
+ <p className="text-[10px] font-medium uppercase tracking-wide text-[var(--text-tertiary)]">
415
+ Event logging
416
+ </p>
417
+ <ol className="mt-1 flex list-decimal flex-col gap-0.5 pl-4">
418
+ {entry.analyticsNotes.map((note) => (
419
+ <li
420
+ key={note}
421
+ className="text-[11px] leading-snug text-[var(--text-secondary)]"
422
+ >
423
+ {note}
424
+ </li>
425
+ ))}
426
+ </ol>
427
+ </div>
428
+ )}
429
+ {entry.prUrl ||
430
+ entry.branch ||
431
+ entry.linearUrl ||
432
+ entry.vercelPreviewHost ? (
433
+ <div className="flex flex-wrap items-center gap-2">
434
+ {entry.prUrl ? <CopyPrLink prUrl={entry.prUrl} /> : null}
435
+ {entry.branch ? <CopyBranch branch={entry.branch} /> : null}
436
+ {entry.linearUrl ? (
437
+ <CopyLinearLink linearUrl={entry.linearUrl} />
438
+ ) : null}
439
+ {entry.prUrl || entry.vercelPreviewHost ? (
440
+ <CopyPreviewLink
441
+ entry={entry}
442
+ previewCache={previewCache}
443
+ previewLoading={previewLoading}
444
+ defaultPreviewPath={defaultPreviewPath}
445
+ vercelProjectName={vercelProjectName}
446
+ />
447
+ ) : null}
448
+ </div>
449
+ ) : null}
450
+ </div>
451
+ </div>
452
+ </div>
453
+ </div>
454
+ </button>
455
+ );
456
+ }
457
+
458
+ export type PrototypeSpecPanelContentProps<
459
+ TWireframeId extends string = string,
460
+ TLiveState = unknown,
461
+ > = {
462
+ entries: PrSplitEntry<TWireframeId, TLiveState>[];
463
+ renderWireframe: (wireframeId: TWireframeId) => ReactNode;
464
+ buildAgentPrompt: (
465
+ entry: PrSplitEntry<TWireframeId, TLiveState>,
466
+ origin?: string,
467
+ ) => string;
468
+ onPrNavigate?: (entry: PrSplitEntry<TWireframeId, TLiveState>) => void;
469
+ selectedPrOrder?: number | null;
470
+ previewApiPath?: string;
471
+ defaultPreviewPath?: string;
472
+ vercelProjectName?: string;
473
+ };
474
+
475
+ export function PrototypeSpecPanelContent<
476
+ TWireframeId extends string = string,
477
+ TLiveState = unknown,
478
+ >({
479
+ entries,
480
+ renderWireframe,
481
+ buildAgentPrompt,
482
+ onPrNavigate,
483
+ selectedPrOrder,
484
+ previewApiPath,
485
+ defaultPreviewPath,
486
+ vercelProjectName,
487
+ }: PrototypeSpecPanelContentProps<TWireframeId, TLiveState>) {
488
+ const prUrlsWithLinks = useMemo(
489
+ () =>
490
+ entries.map((entry) => entry.prUrl).filter((url): url is string =>
491
+ Boolean(url),
492
+ ),
493
+ [entries],
494
+ );
495
+
496
+ const { previewCache, previewLoading } = usePrVercelPreviews(
497
+ prUrlsWithLinks,
498
+ previewApiPath,
499
+ );
500
+
501
+ useEffect(() => {
502
+ if (selectedPrOrder == null) return;
503
+ const timer = window.setTimeout(() => {
504
+ const el = document.querySelector<HTMLElement>(
505
+ `[data-pr-order="${selectedPrOrder}"]`,
506
+ );
507
+ el?.scrollIntoView({ behavior: "smooth", block: "nearest" });
508
+ }, 120);
509
+ return () => clearTimeout(timer);
510
+ }, [selectedPrOrder]);
511
+
512
+ return (
513
+ <PrototypeComponent
514
+ id="spec-panel-content"
515
+ className="flex flex-col gap-4 pb-6"
516
+ >
517
+ <p className="text-[13px] leading-relaxed text-[var(--text-secondary)]">
518
+ Click a PR to preview the change and highlight the relevant component.
519
+ </p>
520
+ <div className="flex flex-col gap-4">
521
+ {entries.map((entry) => (
522
+ <PrPreviewCard
523
+ key={entry.order}
524
+ entry={entry}
525
+ selected={entry.order === selectedPrOrder}
526
+ onSelect={(selected) => onPrNavigate?.(selected)}
527
+ previewCache={previewCache}
528
+ previewLoading={previewLoading}
529
+ renderWireframe={renderWireframe}
530
+ buildAgentPrompt={buildAgentPrompt}
531
+ defaultPreviewPath={defaultPreviewPath}
532
+ vercelProjectName={vercelProjectName}
533
+ />
534
+ ))}
535
+ </div>
536
+ </PrototypeComponent>
537
+ );
538
+ }