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,428 @@
1
+ // =============================================================================
2
+ // Drawing Screenshots
3
+ // =============================================================================
4
+ //
5
+ // Captures a DOM region with drawing strokes composited on top.
6
+ //
7
+ // Uses `modern-screenshot` (optional peer dep) for DOM-to-image capture.
8
+ // If not installed, falls back to stroke-only canvas capture.
9
+ //
10
+
11
+ // Cache the import result so we only try once
12
+ let _domCaptureModule: {
13
+ domToCanvas: (node: Node, options?: Record<string, unknown>) => Promise<HTMLCanvasElement>;
14
+ } | null | undefined; // null = tried and failed, undefined = not tried yet
15
+
16
+ async function getDomCapture() {
17
+ if (_domCaptureModule !== undefined) return _domCaptureModule;
18
+ try {
19
+ _domCaptureModule = await import("modern-screenshot");
20
+ return _domCaptureModule;
21
+ } catch {
22
+ _domCaptureModule = null;
23
+ return null;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Check whether DOM capture is available (modern-screenshot is installed).
29
+ * Returns a cached result after the first check.
30
+ */
31
+ export async function isDomCaptureAvailable(): Promise<boolean> {
32
+ return (await getDomCapture()) !== null;
33
+ }
34
+
35
+ import {
36
+ getCommentsSidebarInset,
37
+ getPrototypeScrollContainer,
38
+ } from "./annotation-target";
39
+ import type { CaptureViewport } from "./types";
40
+
41
+ type CaptureUiRestore = {
42
+ restore: () => void;
43
+ };
44
+
45
+ /**
46
+ * Hide feedback overlays during capture while leaving the comments sidebar untouched.
47
+ */
48
+ async function prepareDomForCapture(): Promise<CaptureUiRestore> {
49
+ // Map ensures each element is hidden/restored once. The review toolbar matches
50
+ // both `[data-prototype-review-trigger]` and `[data-prototype-comment-root]`.
51
+ const hidden = new Map<HTMLElement, string>();
52
+
53
+ const hideElement = (el: Element) => {
54
+ const html = el as HTMLElement;
55
+ if (html.closest("[data-comments-sidebar]")) return;
56
+ if (hidden.has(html)) return;
57
+ hidden.set(html, html.style.visibility);
58
+ html.style.visibility = "hidden";
59
+ };
60
+
61
+ const captureUiSelectors = [
62
+ "[data-feedback-toolbar]",
63
+ "[data-comment-capture-toolbar]",
64
+ "[data-prototype-review-trigger]",
65
+ "[data-prototype-controls]",
66
+ "[data-annotation-marker]",
67
+ "[data-annotation-popup]",
68
+ "[data-comment-capture-overlay]",
69
+ ];
70
+
71
+ for (const selector of captureUiSelectors) {
72
+ document.querySelectorAll(selector).forEach(hideElement);
73
+ }
74
+
75
+ // Hide standalone overlay roots (e.g. restore overlay), but keep the comments sidebar visible
76
+ // so cropSidebarFromCanvas can still trim it from the captured image.
77
+ document.querySelectorAll("[data-prototype-comment-root]").forEach((root) => {
78
+ if (root.querySelector("[data-comments-sidebar]")) return;
79
+ hideElement(root);
80
+ });
81
+
82
+ await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
83
+ await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
84
+
85
+ return {
86
+ restore: () => {
87
+ hidden.forEach((visibility, el) => {
88
+ el.style.visibility = visibility;
89
+ });
90
+ },
91
+ };
92
+ }
93
+
94
+ function cropSidebarFromCanvas(
95
+ source: HTMLCanvasElement,
96
+ sidebarInset: number,
97
+ scale: number,
98
+ ): HTMLCanvasElement {
99
+ if (sidebarInset <= 0) return source;
100
+
101
+ const croppedWidth = Math.max(1, source.width - Math.round(sidebarInset * scale));
102
+ if (croppedWidth >= source.width) return source;
103
+
104
+ const cropped = document.createElement("canvas");
105
+ cropped.width = croppedWidth;
106
+ cropped.height = source.height;
107
+ const ctx = cropped.getContext("2d");
108
+ if (!ctx) return source;
109
+
110
+ ctx.drawImage(source, 0, 0, croppedWidth, source.height, 0, 0, croppedWidth, source.height);
111
+ return cropped;
112
+ }
113
+
114
+ export type ViewportScreenshotResult = {
115
+ dataUrl: string;
116
+ captureViewport: CaptureViewport;
117
+ };
118
+
119
+ /**
120
+ * Capture the current viewport as a JPEG data URL.
121
+ * Hides feedback overlays during capture and crops the comments sidebar from the result.
122
+ */
123
+ export async function captureViewportScreenshot(
124
+ quality = 0.85,
125
+ ): Promise<ViewportScreenshotResult | null> {
126
+ const mod = await getDomCapture();
127
+ if (!mod) {
128
+ console.warn("[prototype-comments] modern-screenshot is not installed; viewport capture skipped.");
129
+ return null;
130
+ }
131
+
132
+ const sidebarInset = getCommentsSidebarInset();
133
+ const inlineContent = document.querySelector("[data-prototype-screenshot]");
134
+ const isInlineSidebar = document.documentElement.hasAttribute("data-comments-sidebar-inline");
135
+ const { restore } = await prepareDomForCapture();
136
+
137
+ try {
138
+ const captureTarget = isInlineSidebar && inlineContent instanceof HTMLElement
139
+ ? inlineContent
140
+ : document.documentElement;
141
+
142
+ const targetRect = captureTarget.getBoundingClientRect();
143
+ const viewportWidth = isInlineSidebar && inlineContent instanceof HTMLElement
144
+ ? targetRect.width
145
+ : window.innerWidth;
146
+ const viewportHeight = isInlineSidebar && inlineContent instanceof HTMLElement
147
+ ? targetRect.height
148
+ : window.innerHeight;
149
+ const contentWidth = Math.max(1, viewportWidth - sidebarInset);
150
+ const maxWidth = 1200;
151
+ const scale = Math.min(1, maxWidth / viewportWidth);
152
+ const outW = Math.round(viewportWidth * scale);
153
+ const outH = Math.round(viewportHeight * scale);
154
+
155
+ const domCanvas = await mod.domToCanvas(captureTarget, {
156
+ backgroundColor: "#ffffff",
157
+ timeout: 5000,
158
+ width: viewportWidth,
159
+ height: viewportHeight,
160
+ });
161
+
162
+ const canvas = document.createElement("canvas");
163
+ canvas.width = outW;
164
+ canvas.height = outH;
165
+ const ctx = canvas.getContext("2d");
166
+ if (!ctx) return null;
167
+
168
+ ctx.drawImage(domCanvas, 0, 0, outW, outH);
169
+
170
+ const cropped = cropSidebarFromCanvas(canvas, sidebarInset, scale);
171
+ const scrollContainer = getPrototypeScrollContainer();
172
+ return {
173
+ dataUrl: cropped.toDataURL("image/jpeg", quality),
174
+ captureViewport: {
175
+ width: contentWidth,
176
+ height: viewportHeight,
177
+ scrollY: window.scrollY,
178
+ scrollTop: scrollContainer?.scrollTop ?? 0,
179
+ },
180
+ };
181
+ } catch (err) {
182
+ console.warn("[prototype-comments] Viewport capture failed:", err);
183
+ return null;
184
+ } finally {
185
+ restore();
186
+ }
187
+ }
188
+
189
+
190
+ // ---------------------------------------------------------------------------
191
+ // Find capture target element
192
+ // ---------------------------------------------------------------------------
193
+
194
+ /**
195
+ * Find the smallest DOM element that covers a given viewport region.
196
+ * Uses elementsFromPoint to get all elements at the region center,
197
+ * then picks the smallest one that fully contains the capture area.
198
+ */
199
+ function findCaptureTarget(
200
+ captureX: number,
201
+ captureY: number,
202
+ captureW: number,
203
+ captureH: number,
204
+ ): HTMLElement {
205
+ const cx = captureX + captureW / 2;
206
+ const cy = captureY + captureH / 2;
207
+
208
+ // elementsFromPoint returns elements from most specific (smallest) to least
209
+ const elements = document.elementsFromPoint(cx, cy);
210
+
211
+ for (const el of elements) {
212
+ if (!(el instanceof HTMLElement)) continue;
213
+ // Skip prototype comment UI
214
+ if (el.hasAttribute("data-prototype-comment-root")) continue;
215
+ if (el.closest?.("[data-prototype-comment-root]")) continue;
216
+ if (el.hasAttribute("data-comments-sidebar")) continue;
217
+ if (el.closest?.("[data-comments-sidebar]")) continue;
218
+ if (el.tagName === "CANVAS") continue;
219
+ // Skip html/body — we want something more specific
220
+ if (el === document.documentElement || el === document.body) continue;
221
+
222
+ const rect = el.getBoundingClientRect();
223
+ // Accept elements that cover at least ~90% of the capture region
224
+ if (
225
+ rect.left <= captureX + captureW * 0.1 &&
226
+ rect.top <= captureY + captureH * 0.1 &&
227
+ rect.right >= captureX + captureW * 0.9 &&
228
+ rect.bottom >= captureY + captureH * 0.9
229
+ ) {
230
+ return el;
231
+ }
232
+ }
233
+
234
+ return document.body;
235
+ }
236
+
237
+ // ---------------------------------------------------------------------------
238
+ // DOM capture (modern-screenshot)
239
+ // ---------------------------------------------------------------------------
240
+
241
+ /**
242
+ * Capture a viewport region as a JPEG data URL using DOM-to-image.
243
+ * Composites drawing strokes on top.
244
+ *
245
+ * Returns null if modern-screenshot is not installed or capture fails.
246
+ */
247
+ export async function captureDomRegion(
248
+ regionX: number,
249
+ regionY: number,
250
+ regionW: number,
251
+ regionH: number,
252
+ strokes: Array<{
253
+ points: Array<{ x: number; y: number }>;
254
+ color: string;
255
+ fixed: boolean;
256
+ }>,
257
+ padding = 32,
258
+ quality = 0.85,
259
+ ): Promise<string | null> {
260
+ const mod = await getDomCapture();
261
+ if (!mod) return null;
262
+
263
+ // Region to capture in viewport coords
264
+ const captureX = Math.max(0, regionX - padding);
265
+ const captureY = Math.max(0, regionY - padding);
266
+ const captureW = regionW + padding * 2;
267
+ const captureH = regionH + padding * 2;
268
+
269
+ // Output size (capped)
270
+ const maxDim = 600;
271
+ const outScale = Math.min(1, maxDim / Math.max(captureW, captureH));
272
+ const outW = Math.round(captureW * outScale);
273
+ const outH = Math.round(captureH * outScale);
274
+ if (outW < 1 || outH < 1) return null;
275
+
276
+ const { restore } = await prepareDomForCapture();
277
+
278
+ try {
279
+ const target = findCaptureTarget(captureX, captureY, captureW, captureH);
280
+ const targetRect = target.getBoundingClientRect();
281
+
282
+ // Render the target element at 1:1 CSS pixel scale
283
+ const domCanvas = await mod.domToCanvas(target, {
284
+ backgroundColor: "#ffffff",
285
+ timeout: 5000,
286
+ });
287
+
288
+ // domToCanvas renders the element's full scrollable content.
289
+ // We need to map our viewport capture region to the domCanvas coords.
290
+ //
291
+ // For non-scrollable elements: domCanvas size ≈ targetRect size
292
+ // For scrollable elements: domCanvas size ≈ scrollWidth × scrollHeight
293
+ //
294
+ // The offset within the canvas depends on whether the target has scrolled content.
295
+ // Use the ratio of canvas size to actual element dimensions.
296
+ const ratioX = domCanvas.width / (target.scrollWidth || targetRect.width);
297
+ const ratioY = domCanvas.height / (target.scrollHeight || targetRect.height);
298
+
299
+ // Convert viewport offset to element-content offset
300
+ // targetRect.top is viewport-relative; for scrolled elements we need to add scrollTop
301
+ const scrollLeft = target === document.body ? window.scrollX : target.scrollLeft;
302
+ const scrollTop = target === document.body ? window.scrollY : target.scrollTop;
303
+
304
+ const elContentX = (captureX - targetRect.left + scrollLeft) * ratioX;
305
+ const elContentY = (captureY - targetRect.top + scrollTop) * ratioY;
306
+ const cropW = captureW * ratioX;
307
+ const cropH = captureH * ratioY;
308
+
309
+ // Create output canvas and crop
310
+ const canvas = document.createElement("canvas");
311
+ canvas.width = outW;
312
+ canvas.height = outH;
313
+ const ctx = canvas.getContext("2d");
314
+ if (!ctx) return null;
315
+
316
+ // White background in case crop extends beyond domCanvas
317
+ ctx.fillStyle = "#ffffff";
318
+ ctx.fillRect(0, 0, outW, outH);
319
+
320
+ ctx.drawImage(
321
+ domCanvas,
322
+ elContentX, elContentY, cropW, cropH,
323
+ 0, 0, outW, outH,
324
+ );
325
+
326
+ // Composite drawing strokes on top
327
+ drawStrokesOnCanvas(ctx, strokes, captureX, captureY, outScale);
328
+
329
+ return canvas.toDataURL("image/jpeg", quality);
330
+ } catch (err) {
331
+ console.warn("[prototype-comments] DOM capture failed:", err);
332
+ return null;
333
+ } finally {
334
+ restore();
335
+ }
336
+ }
337
+
338
+ // ---------------------------------------------------------------------------
339
+ // Stroke-only fallback
340
+ // ---------------------------------------------------------------------------
341
+
342
+ /**
343
+ * Capture drawing strokes as a PNG data URL (fallback when DOM capture
344
+ * isn't available). Renders strokes on a light background.
345
+ */
346
+ export function captureDrawingStrokes(
347
+ regionX: number,
348
+ regionY: number,
349
+ regionW: number,
350
+ regionH: number,
351
+ strokes: Array<{
352
+ points: Array<{ x: number; y: number }>;
353
+ color: string;
354
+ fixed: boolean;
355
+ }>,
356
+ padding = 32,
357
+ ): string | null {
358
+ try {
359
+ const captureX = Math.max(0, regionX - padding);
360
+ const captureY = Math.max(0, regionY - padding);
361
+ const captureW = regionW + padding * 2;
362
+ const captureH = regionH + padding * 2;
363
+
364
+ const maxDim = 400;
365
+ const scale = Math.min(1, maxDim / Math.max(captureW, captureH));
366
+ const outW = Math.round(captureW * scale);
367
+ const outH = Math.round(captureH * scale);
368
+
369
+ if (outW < 1 || outH < 1) return null;
370
+
371
+ const canvas = document.createElement("canvas");
372
+ canvas.width = outW;
373
+ canvas.height = outH;
374
+ const ctx = canvas.getContext("2d");
375
+ if (!ctx) return null;
376
+
377
+ ctx.fillStyle = "rgba(255, 255, 255, 0.85)";
378
+ ctx.fillRect(0, 0, outW, outH);
379
+
380
+ drawStrokesOnCanvas(ctx, strokes, captureX, captureY, scale);
381
+
382
+ return canvas.toDataURL("image/png");
383
+ } catch (err) {
384
+ console.warn("[prototype-comments] Stroke capture failed:", err);
385
+ return null;
386
+ }
387
+ }
388
+
389
+ // ---------------------------------------------------------------------------
390
+ // Shared: draw strokes onto a canvas
391
+ // ---------------------------------------------------------------------------
392
+
393
+ function drawStrokesOnCanvas(
394
+ ctx: CanvasRenderingContext2D,
395
+ strokes: Array<{
396
+ points: Array<{ x: number; y: number }>;
397
+ color: string;
398
+ fixed: boolean;
399
+ }>,
400
+ originX: number,
401
+ originY: number,
402
+ scale: number,
403
+ ) {
404
+ const scrollY = window.scrollY;
405
+ for (const stroke of strokes) {
406
+ if (stroke.points.length < 2) continue;
407
+
408
+ ctx.save();
409
+ ctx.strokeStyle = stroke.color;
410
+ ctx.lineWidth = Math.max(2, 2.5 * scale);
411
+ ctx.lineCap = "round";
412
+ ctx.lineJoin = "round";
413
+
414
+ ctx.beginPath();
415
+ for (let i = 0; i < stroke.points.length; i++) {
416
+ const p = stroke.points[i];
417
+ const vx = p.x;
418
+ const vy = stroke.fixed ? p.y : p.y - scrollY;
419
+ const cx = (vx - originX) * scale;
420
+ const cy = (vy - originY) * scale;
421
+
422
+ if (i === 0) ctx.moveTo(cx, cy);
423
+ else ctx.lineTo(cx, cy);
424
+ }
425
+ ctx.stroke();
426
+ ctx.restore();
427
+ }
428
+ }