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,780 @@
1
+ "use client";
2
+
3
+ import {
4
+ Tooltip,
5
+ TooltipContent,
6
+ TooltipProvider,
7
+ TooltipTrigger,
8
+ } from "@prototype/components/ui/tooltip";
9
+ import {
10
+ useEffect,
11
+ useRef,
12
+ useState,
13
+ type CSSProperties,
14
+ type MouseEvent,
15
+ type ReactNode,
16
+ } from "react";
17
+ import {
18
+ isAnnotationResolved,
19
+ sortAnnotationsForDisplay,
20
+ } from "../core/annotation-status";
21
+ import { getRepliesForParent } from "../core/comment-threads";
22
+ import type { CommentAnnotation } from "../core/types";
23
+ import { useCommentStoreOptional } from "../react/CommentProvider";
24
+ import { CommentThread } from "./CommentThread";
25
+ import { usePrototypeToolTheme } from "@prototype/lib/prototypes/use-prototype-tool-theme";
26
+ import {
27
+ getCaptureAspectRatio,
28
+ getScreenshotObjectPosition,
29
+ getViewportBoundingBox,
30
+ } from "./comment-highlight";
31
+ import {
32
+ PR_TARGET_HIGHLIGHT_BORDER,
33
+ PR_TARGET_HIGHLIGHT_FILL,
34
+ } from "@prototype/lib/pr-split/pr-split-highlight";
35
+ import cardStyles from "./comments-grid-card.module.scss";
36
+ import { cn } from "@prototype/lib/utils";
37
+
38
+ function reviewSidebarCardBorder(
39
+ selected: boolean,
40
+ hovered: boolean,
41
+ ): string {
42
+ if (selected) {
43
+ return PR_TARGET_HIGHLIGHT_BORDER;
44
+ }
45
+ if (hovered) {
46
+ return "var(--border-medium)";
47
+ }
48
+ return "var(--border-solid)";
49
+ }
50
+
51
+ const s = {
52
+ emptyState: {
53
+ display: "flex",
54
+ flexDirection: "column" as const,
55
+ alignItems: "center",
56
+ justifyContent: "center",
57
+ padding: "48px 24px",
58
+ textAlign: "center" as const,
59
+ color: "var(--text-tertiary)",
60
+ fontSize: "14px",
61
+ gap: "8px",
62
+ },
63
+ emptyIcon: {
64
+ width: "40px",
65
+ height: "40px",
66
+ opacity: 0.45,
67
+ marginBottom: "4px",
68
+ color: "var(--text-secondary)",
69
+ },
70
+ grid: (layout: "grid" | "sidebar"): CSSProperties => ({
71
+ display: "grid",
72
+ gridTemplateColumns:
73
+ layout === "sidebar" ? "minmax(0, 1fr)" : "repeat(2, minmax(0, 1fr))",
74
+ gap: layout === "sidebar" ? "12px" : "16px",
75
+ padding: layout === "sidebar" ? "0" : "4px 2px",
76
+ }),
77
+ card: (
78
+ hovered: boolean,
79
+ selected: boolean,
80
+ resolved: boolean,
81
+ layout: "grid" | "sidebar" = "grid",
82
+ ): CSSProperties => ({
83
+ display: "flex",
84
+ flexDirection: "column",
85
+ borderRadius: layout === "sidebar" ? "0.5rem" : "6px",
86
+ overflow: "hidden",
87
+ border: `1px solid ${
88
+ selected
89
+ ? PR_TARGET_HIGHLIGHT_BORDER
90
+ : hovered
91
+ ? "var(--border-medium)"
92
+ : "var(--border-solid)"
93
+ }`,
94
+ background: "var(--bg-main)",
95
+ cursor: "pointer",
96
+ transition: "border-color 0.15s ease, background 0.15s ease, opacity 0.15s ease",
97
+ position: "relative",
98
+ opacity: resolved ? 0.72 : 1,
99
+ }),
100
+ screenshot: {
101
+ position: "relative" as const,
102
+ overflow: "hidden",
103
+ background: "var(--bg-subtle)",
104
+ borderBottom: "1px solid var(--border-solid)",
105
+ },
106
+ screenshotImg: {
107
+ display: "block",
108
+ width: "100%",
109
+ height: "100%",
110
+ objectFit: "cover" as const,
111
+ objectPosition: "top",
112
+ },
113
+ noScreenshot: {
114
+ display: "flex",
115
+ alignItems: "center",
116
+ justifyContent: "center",
117
+ aspectRatio: "16 / 9",
118
+ color: "var(--text-tertiary)",
119
+ fontSize: "13px",
120
+ },
121
+ highlight: (
122
+ isMultiSelect: boolean,
123
+ x: number,
124
+ y: number,
125
+ w: number,
126
+ h: number,
127
+ vw: number,
128
+ vh: number,
129
+ ): CSSProperties => ({
130
+ position: "absolute",
131
+ left: `${(x / vw) * 100}%`,
132
+ top: `${(y / vh) * 100}%`,
133
+ width: `${(w / vw) * 100}%`,
134
+ height: `${(h / vh) * 100}%`,
135
+ boxSizing: "border-box",
136
+ borderRadius: "4px",
137
+ border: isMultiSelect
138
+ ? "2px dashed rgba(34,197,94,0.8)"
139
+ : `1.5px solid ${PR_TARGET_HIGHLIGHT_BORDER}`,
140
+ background: isMultiSelect
141
+ ? "rgba(34,197,94,0.12)"
142
+ : PR_TARGET_HIGHLIGHT_FILL,
143
+ pointerEvents: "none",
144
+ }),
145
+ content: {
146
+ padding: "10px 12px 12px",
147
+ display: "flex",
148
+ flexDirection: "column" as const,
149
+ gap: "6px",
150
+ flex: 1,
151
+ },
152
+ comment: (resolved: boolean): CSSProperties => ({
153
+ fontSize: "14px",
154
+ lineHeight: 1.5,
155
+ color: resolved ? "var(--text-secondary)" : "var(--text-primary)",
156
+ margin: 0,
157
+ display: "-webkit-box" as unknown as "block",
158
+ WebkitBoxOrient: "vertical" as const,
159
+ WebkitLineClamp: 3,
160
+ overflow: "hidden",
161
+ textDecoration: resolved ? "line-through" : "none",
162
+ }),
163
+ resolvedBadge: {
164
+ alignSelf: "flex-start",
165
+ fontSize: "11px",
166
+ fontWeight: 500,
167
+ lineHeight: 1,
168
+ color: "var(--success)",
169
+ background: "color-mix(in srgb, var(--success) 12%, transparent)",
170
+ borderRadius: "4px",
171
+ padding: "4px 6px",
172
+ },
173
+ actions: (visible: boolean, resolved: boolean): CSSProperties => ({
174
+ position: "absolute",
175
+ top: "8px",
176
+ right: "8px",
177
+ display: "flex",
178
+ gap: "4px",
179
+ opacity: visible || resolved ? 1 : 0,
180
+ transition: "opacity 0.15s",
181
+ pointerEvents: visible || resolved ? "auto" : "none",
182
+ }),
183
+ actionBtn: (variant?: "danger" | "resolve" | "resolved"): CSSProperties => ({
184
+ width: "28px",
185
+ height: "28px",
186
+ borderRadius: "calc(var(--radius) - 2px)",
187
+ border: "1px solid var(--tool-chrome-border)",
188
+ background: "var(--tool-chrome-bg)",
189
+ color:
190
+ variant === "danger"
191
+ ? "var(--tool-chrome-pink)"
192
+ : variant === "resolved"
193
+ ? "#22c55e"
194
+ : "var(--tool-chrome-text-muted)",
195
+ cursor: "pointer",
196
+ display: "flex",
197
+ alignItems: "center",
198
+ justifyContent: "center",
199
+ padding: 0,
200
+ boxShadow: "var(--shadow-sm)",
201
+ }),
202
+ editInput: {
203
+ fontSize: "14px",
204
+ lineHeight: 1.5,
205
+ color: "var(--text-primary)",
206
+ border: "1px solid var(--border-medium)",
207
+ borderRadius: "6px",
208
+ padding: "6px 8px",
209
+ outline: "none",
210
+ resize: "none" as const,
211
+ width: "100%",
212
+ fontFamily: "inherit",
213
+ boxSizing: "border-box" as const,
214
+ background: "var(--bg-main)",
215
+ },
216
+ };
217
+
218
+ function PencilIcon() {
219
+ return (
220
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
221
+ <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
222
+ <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
223
+ </svg>
224
+ );
225
+ }
226
+
227
+ function TrashIcon() {
228
+ return (
229
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
230
+ <polyline points="3 6 5 6 21 6" />
231
+ <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
232
+ <path d="M10 11v6M14 11v6" />
233
+ <path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
234
+ </svg>
235
+ );
236
+ }
237
+
238
+ function CopyIcon() {
239
+ return (
240
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
241
+ <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
242
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
243
+ </svg>
244
+ );
245
+ }
246
+
247
+ function CopiedIcon() {
248
+ return (
249
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
250
+ <polyline points="20 6 9 17 4 12" />
251
+ </svg>
252
+ );
253
+ }
254
+
255
+ function copyText(text: string): void {
256
+ if (navigator.clipboard) {
257
+ navigator.clipboard.writeText(text).catch(() => copyTextFallback(text));
258
+ } else {
259
+ copyTextFallback(text);
260
+ }
261
+ }
262
+
263
+ function copyTextFallback(text: string): void {
264
+ const ta = document.createElement("textarea");
265
+ ta.value = text;
266
+ ta.style.cssText = "position:fixed;opacity:0;pointer-events:none";
267
+ document.body.appendChild(ta);
268
+ ta.focus();
269
+ ta.select();
270
+ document.execCommand("copy");
271
+ document.body.removeChild(ta);
272
+ }
273
+
274
+ function buildCopyPrompt(
275
+ annotation: CommentAnnotation<unknown>,
276
+ prototypeSlug?: string,
277
+ allAnnotations?: readonly CommentAnnotation<unknown>[],
278
+ ): string {
279
+ const lines: string[] = [];
280
+ lines.push(`Fix the following UI feedback:`);
281
+ if (prototypeSlug) {
282
+ lines.push(`Prototype: ${prototypeSlug}`);
283
+ lines.push(`Work in: src/prototypes/${prototypeSlug}/`);
284
+ }
285
+ lines.push(`\nComment: "${annotation.comment}"`);
286
+ if (allAnnotations) {
287
+ const replies = getRepliesForParent(allAnnotations, annotation.id);
288
+ for (const reply of replies) {
289
+ lines.push(`Reply: "${reply.comment}"`);
290
+ }
291
+ }
292
+ if (annotation.element) lines.push(`Element: ${annotation.element}`);
293
+ if (annotation.sourceFile) lines.push(`Source file: ${annotation.sourceFile}`);
294
+ if (annotation.reactComponents) lines.push(`React components: ${annotation.reactComponents}`);
295
+ if (annotation.elementPath) lines.push(`Element path: ${annotation.elementPath}`);
296
+ if (annotation.cssClasses) lines.push(`CSS classes: ${annotation.cssClasses}`);
297
+ if (annotation.nearbyText) lines.push(`Nearby text: ${annotation.nearbyText}`);
298
+ return lines.join("\n");
299
+ }
300
+
301
+ function CheckIcon() {
302
+ return (
303
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
304
+ <polyline points="20 6 9 17 4 12" />
305
+ </svg>
306
+ );
307
+ }
308
+
309
+ function ResolveIcon() {
310
+ return (
311
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
312
+ <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
313
+ <polyline points="22 4 12 14.01 9 11.01" />
314
+ </svg>
315
+ );
316
+ }
317
+
318
+ const ACTION_TOOLTIP_DELAY_MS = 500;
319
+
320
+ type CommentActionButtonProps = {
321
+ label: string;
322
+ onClick: (event: MouseEvent<HTMLButtonElement>) => void;
323
+ variant?: "danger" | "resolve" | "resolved";
324
+ compact?: boolean;
325
+ children: ReactNode;
326
+ };
327
+
328
+ function CommentActionButton({
329
+ label,
330
+ onClick,
331
+ variant,
332
+ compact = false,
333
+ children,
334
+ }: CommentActionButtonProps) {
335
+ return (
336
+ <Tooltip>
337
+ <TooltipTrigger asChild>
338
+ <button
339
+ style={s.actionBtn(variant)}
340
+ className={cn(compact && cardStyles.actionBtnCompact)}
341
+ onClick={onClick}
342
+ type="button"
343
+ aria-label={label}
344
+ >
345
+ {children}
346
+ </button>
347
+ </TooltipTrigger>
348
+ <TooltipContent side="top">{label}</TooltipContent>
349
+ </Tooltip>
350
+ );
351
+ }
352
+
353
+ type CommentCardProps<TState> = {
354
+ annotation: CommentAnnotation<TState>;
355
+ onSelect?: (id: string) => void;
356
+ linkBasePath?: string;
357
+ onDelete: (id: string) => void;
358
+ onUpdateComment: (id: string, comment: string) => void;
359
+ onResolve?: (id: string) => void;
360
+ showResolveActions?: boolean;
361
+ showCopyPrompt?: boolean;
362
+ showReplies?: boolean;
363
+ selected?: boolean;
364
+ prototypeSlug?: string;
365
+ layout?: "grid" | "sidebar";
366
+ expandComment?: boolean;
367
+ };
368
+
369
+ function CommentCard<TState>({
370
+ annotation,
371
+ onSelect,
372
+ linkBasePath,
373
+ onDelete,
374
+ onUpdateComment,
375
+ onResolve,
376
+ showResolveActions = true,
377
+ showCopyPrompt = true,
378
+ showReplies = true,
379
+ selected = false,
380
+ prototypeSlug,
381
+ layout = "grid",
382
+ expandComment = false,
383
+ }: CommentCardProps<TState>) {
384
+ const commentStore = useCommentStoreOptional();
385
+ const { useLightTheme } = usePrototypeToolTheme();
386
+ const allAnnotations = commentStore?.annotations;
387
+ const cardRef = useRef<HTMLDivElement>(null);
388
+ const [hovered, setHovered] = useState(false);
389
+ const [editing, setEditing] = useState(false);
390
+ const [draft, setDraft] = useState(annotation.comment);
391
+ const [copied, setCopied] = useState(false);
392
+ const isResolved = showResolveActions && isAnnotationResolved(annotation);
393
+ const isCondensed = isResolved && !editing;
394
+ const replyCount =
395
+ showReplies && allAnnotations
396
+ ? getRepliesForParent(allAnnotations, annotation.id).length
397
+ : 0;
398
+ const showActions = hovered && !editing;
399
+
400
+ useEffect(() => {
401
+ if (selected && cardRef.current) {
402
+ cardRef.current.scrollIntoView({ block: "nearest", behavior: "smooth" });
403
+ }
404
+ }, [selected]);
405
+
406
+ const viewportBox = getViewportBoundingBox(annotation);
407
+ const viewport = annotation.captureViewport;
408
+ const isSidebarLayout = layout === "sidebar";
409
+ const aspectRatio = getCaptureAspectRatio(annotation);
410
+ const screenshotObjectPosition = getScreenshotObjectPosition(annotation);
411
+ const showResolvedBadge =
412
+ showResolveActions && isResolved && !isCondensed;
413
+
414
+ function handleSave() {
415
+ if (draft.trim()) onUpdateComment(annotation.id, draft.trim());
416
+ setEditing(false);
417
+ }
418
+
419
+ function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
420
+ if (e.key === "Enter" && !e.shiftKey) {
421
+ e.preventDefault();
422
+ handleSave();
423
+ }
424
+ if (e.key === "Escape") {
425
+ setDraft(annotation.comment);
426
+ setEditing(false);
427
+ }
428
+ }
429
+
430
+ function handleSelect() {
431
+ if (linkBasePath) {
432
+ window.open(`${linkBasePath}/${annotation.id}`, "_blank", "noopener,noreferrer");
433
+ return;
434
+ }
435
+ onSelect?.(annotation.id);
436
+ }
437
+
438
+ function renderActionButtons(compact: boolean) {
439
+ return (
440
+ <>
441
+ {editing ? (
442
+ <CommentActionButton
443
+ label="Save"
444
+ onClick={handleSave}
445
+ compact={compact}
446
+ >
447
+ <CheckIcon />
448
+ </CommentActionButton>
449
+ ) : (
450
+ <>
451
+ {showResolveActions && onResolve ? (
452
+ <CommentActionButton
453
+ label={isResolved ? "Unresolve comment" : "Resolve comment"}
454
+ variant={isResolved ? "resolved" : "resolve"}
455
+ compact={compact}
456
+ onClick={(e) => {
457
+ e.stopPropagation();
458
+ onResolve(annotation.id);
459
+ }}
460
+ >
461
+ <ResolveIcon />
462
+ </CommentActionButton>
463
+ ) : null}
464
+ <span className={cardStyles.secondaryAction}>
465
+ <CommentActionButton
466
+ label="Edit comment"
467
+ compact={compact}
468
+ onClick={(e) => {
469
+ e.stopPropagation();
470
+ setDraft(annotation.comment);
471
+ setEditing(true);
472
+ }}
473
+ >
474
+ <PencilIcon />
475
+ </CommentActionButton>
476
+ </span>
477
+ {showCopyPrompt ? (
478
+ <span className={cardStyles.secondaryAction}>
479
+ <CommentActionButton
480
+ label="Copy as agent prompt"
481
+ variant={copied ? "resolve" : undefined}
482
+ compact={compact}
483
+ onClick={(e) => {
484
+ e.stopPropagation();
485
+ copyText(
486
+ buildCopyPrompt(
487
+ annotation as CommentAnnotation<unknown>,
488
+ prototypeSlug,
489
+ allAnnotations as CommentAnnotation<unknown>[] | undefined,
490
+ ),
491
+ );
492
+ setCopied(true);
493
+ setTimeout(() => setCopied(false), 1500);
494
+ }}
495
+ >
496
+ {copied ? <CopiedIcon /> : <CopyIcon />}
497
+ </CommentActionButton>
498
+ </span>
499
+ ) : null}
500
+ </>
501
+ )}
502
+ <CommentActionButton
503
+ label="Delete comment"
504
+ variant="danger"
505
+ compact={compact}
506
+ onClick={(e) => {
507
+ e.stopPropagation();
508
+ onDelete(annotation.id);
509
+ }}
510
+ >
511
+ <TrashIcon />
512
+ </CommentActionButton>
513
+ </>
514
+ );
515
+ }
516
+
517
+ const showActionBar = showActions || isCondensed;
518
+
519
+ const actionButtons = (
520
+ <TooltipProvider delayDuration={ACTION_TOOLTIP_DELAY_MS}>
521
+ <div
522
+ className={cardStyles.actions}
523
+ style={s.actions(showActionBar, isCondensed || isResolved)}
524
+ onClick={(e) => e.stopPropagation()}
525
+ >
526
+ {renderActionButtons(isCondensed)}
527
+ </div>
528
+ </TooltipProvider>
529
+ );
530
+
531
+ return (
532
+ <div
533
+ ref={cardRef}
534
+ className={cn(
535
+ "tool-review-card",
536
+ cardStyles.card,
537
+ isSidebarLayout && cardStyles.cardSidebar,
538
+ isSidebarLayout && "rounded-md text-[var(--tool-chrome-text)]",
539
+ isCondensed && cardStyles.cardCondensed,
540
+ isResolved && cardStyles.cardResolved,
541
+ )}
542
+ style={
543
+ isSidebarLayout
544
+ ? {
545
+ border: `1px solid ${reviewSidebarCardBorder(selected, hovered)}`,
546
+ background: "var(--bg-main)",
547
+ transition:
548
+ "border-color 0.15s ease, background 0.15s ease, opacity 0.15s ease, box-shadow 0.15s ease",
549
+ }
550
+ : {
551
+ border: `1px solid ${
552
+ selected
553
+ ? PR_TARGET_HIGHLIGHT_BORDER
554
+ : hovered
555
+ ? "var(--border-medium)"
556
+ : "var(--border-solid)"
557
+ }`,
558
+ background: "var(--bg-main)",
559
+ }
560
+ }
561
+ aria-current={selected ? "true" : undefined}
562
+ onMouseEnter={() => setHovered(true)}
563
+ onMouseLeave={() => setHovered(false)}
564
+ onClick={() => !editing && handleSelect()}
565
+ role="button"
566
+ tabIndex={0}
567
+ onKeyDown={(e) => {
568
+ if (editing) return;
569
+ if (e.key === "Enter") {
570
+ handleSelect();
571
+ return;
572
+ }
573
+ if (
574
+ selected &&
575
+ (e.key === "ArrowUp" || e.key === "ArrowDown")
576
+ ) {
577
+ e.preventDefault();
578
+ }
579
+ }}
580
+ >
581
+ {actionButtons}
582
+
583
+ <div className={cardStyles.screenshotCollapse}>
584
+ <div className={cardStyles.screenshotInner}>
585
+ <div
586
+ className={cardStyles.screenshot}
587
+ style={
588
+ isSidebarLayout
589
+ ? undefined
590
+ : { aspectRatio: aspectRatio ?? "16 / 9" }
591
+ }
592
+ >
593
+ {annotation.screenshot ? (
594
+ <>
595
+ <img
596
+ src={annotation.screenshot}
597
+ alt={annotation.comment}
598
+ className={
599
+ isSidebarLayout ? cardStyles.screenshotImgSidebar : undefined
600
+ }
601
+ style={
602
+ isSidebarLayout
603
+ ? { objectPosition: screenshotObjectPosition }
604
+ : {
605
+ ...s.screenshotImg,
606
+ objectPosition: screenshotObjectPosition,
607
+ }
608
+ }
609
+ />
610
+ {viewportBox && viewport && (
611
+ <div
612
+ aria-hidden
613
+ style={s.highlight(
614
+ !!annotation.isMultiSelect,
615
+ viewportBox.x,
616
+ viewportBox.y,
617
+ viewportBox.width,
618
+ viewportBox.height,
619
+ viewport.width,
620
+ viewport.height,
621
+ )}
622
+ />
623
+ )}
624
+ </>
625
+ ) : (
626
+ <div style={s.noScreenshot}>No screenshot</div>
627
+ )}
628
+ </div>
629
+ </div>
630
+ </div>
631
+
632
+ <div className={cardStyles.body}>
633
+ <span className={cardStyles.statusIcon} aria-hidden>
634
+ <ResolveIcon />
635
+ </span>
636
+
637
+ <div className={cardStyles.bodyMain}>
638
+ <div
639
+ className={cn(
640
+ cardStyles.resolvedBadgeCollapse,
641
+ showResolvedBadge && cardStyles.resolvedBadgeCollapseExpanded,
642
+ )}
643
+ >
644
+ <div className={cardStyles.resolvedBadgeInner}>
645
+ {showResolvedBadge ? (
646
+ <span style={s.resolvedBadge}>Resolved</span>
647
+ ) : null}
648
+ </div>
649
+ </div>
650
+
651
+ {editing ? (
652
+ <textarea
653
+ autoFocus
654
+ value={draft}
655
+ onChange={(e) => setDraft(e.target.value)}
656
+ onKeyDown={handleKeyDown}
657
+ onBlur={handleSave}
658
+ onClick={(e) => e.stopPropagation()}
659
+ rows={3}
660
+ style={s.editInput}
661
+ />
662
+ ) : (
663
+ <p
664
+ className={cn(
665
+ cardStyles.comment,
666
+ expandComment && cardStyles.commentExpanded,
667
+ )}
668
+ style={
669
+ !expandComment && isResolved && !isCondensed
670
+ ? s.comment(true)
671
+ : undefined
672
+ }
673
+ >
674
+ {annotation.comment}
675
+ </p>
676
+ )}
677
+
678
+ {replyCount > 0 ? (
679
+ <span
680
+ className={cn(
681
+ cardStyles.replyCount,
682
+ cardStyles.replyCountVisible,
683
+ )}
684
+ >
685
+ {replyCount} {replyCount === 1 ? "reply" : "replies"}
686
+ </span>
687
+ ) : null}
688
+
689
+ {showReplies ? (
690
+ <div className={cardStyles.threadCollapse}>
691
+ <div className={cardStyles.threadInner}>
692
+ <CommentThread
693
+ rootAnnotation={annotation}
694
+ dark={!useLightTheme}
695
+ />
696
+ </div>
697
+ </div>
698
+ ) : null}
699
+ </div>
700
+ </div>
701
+ </div>
702
+ );
703
+ }
704
+
705
+ type CommentsGridProps<TState> = {
706
+ annotations: CommentAnnotation<TState>[];
707
+ onSelect?: (id: string) => void;
708
+ linkBasePath?: string;
709
+ layout?: "grid" | "sidebar";
710
+ onDelete: (id: string) => void;
711
+ onUpdateComment: (id: string, comment: string) => void;
712
+ onResolve?: (id: string) => void;
713
+ showResolveActions?: boolean;
714
+ showCopyPrompt?: boolean;
715
+ showReplies?: boolean;
716
+ emptyStateTitle?: string;
717
+ emptyStateAction?: ReactNode;
718
+ selectedId?: string | null;
719
+ sortOrder?: "newest" | "oldest";
720
+ expandComment?: boolean;
721
+ };
722
+
723
+ export function CommentsGrid<TState>({
724
+ annotations,
725
+ onSelect,
726
+ linkBasePath,
727
+ layout = "grid",
728
+ onDelete,
729
+ onUpdateComment,
730
+ onResolve,
731
+ showResolveActions = true,
732
+ showCopyPrompt = true,
733
+ showReplies = true,
734
+ emptyStateTitle = "No comments yet",
735
+ emptyStateAction,
736
+ selectedId,
737
+ sortOrder = "newest",
738
+ expandComment = false,
739
+ }: CommentsGridProps<TState>) {
740
+ const commentStore = useCommentStoreOptional();
741
+ const prototypeSlug = commentStore?.slug;
742
+ const sorted = sortAnnotationsForDisplay(annotations, sortOrder);
743
+
744
+ if (sorted.length === 0) {
745
+ return (
746
+ <div style={s.emptyState}>
747
+ <svg style={s.emptyIcon} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
748
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" />
749
+ </svg>
750
+ <span style={{ fontWeight: 500, color: "var(--text-secondary)" }}>
751
+ {emptyStateTitle}
752
+ </span>
753
+ {emptyStateAction}
754
+ </div>
755
+ );
756
+ }
757
+
758
+ return (
759
+ <div style={s.grid(layout)}>
760
+ {sorted.map((annotation) => (
761
+ <CommentCard
762
+ key={annotation.id}
763
+ annotation={annotation}
764
+ layout={layout}
765
+ onSelect={onSelect}
766
+ linkBasePath={linkBasePath}
767
+ onDelete={onDelete}
768
+ onUpdateComment={onUpdateComment}
769
+ onResolve={onResolve}
770
+ showResolveActions={showResolveActions}
771
+ showCopyPrompt={showCopyPrompt}
772
+ showReplies={showReplies}
773
+ selected={selectedId === annotation.id}
774
+ prototypeSlug={prototypeSlug}
775
+ expandComment={expandComment}
776
+ />
777
+ ))}
778
+ </div>
779
+ );
780
+ }