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.
- package/AGENTS.md +350 -0
- package/README.md +279 -0
- package/components.json +18 -0
- package/package.json +73 -0
- package/scripts/download-mobbin-reference-images.mjs +112 -0
- package/scripts/lib/host-config.example.sh +48 -0
- package/scripts/lib/host-config.sh +25 -0
- package/scripts/lib/resolve-host-root.mjs +18 -0
- package/scripts/lib/source-path.sh +49 -0
- package/scripts/link-source-db.sh +128 -0
- package/scripts/link-source.sh +40 -0
- package/scripts/publish-package.sh +147 -0
- package/scripts/share-prototype.sh +279 -0
- package/scripts/sync-from-source.sh +74 -0
- package/scripts/verify-prototype-component-ids.mjs +151 -0
- package/scripts/verify-prototype-preview-states.mjs +135 -0
- package/src/app.ts +4 -0
- package/src/components/platform-ui/code-block.tsx +21 -0
- package/src/components/platform-ui/controls-panel-options.tsx +71 -0
- package/src/components/platform-ui/controls-panel-select.tsx +438 -0
- package/src/components/platform-ui/empty-state.tsx +15 -0
- package/src/components/platform-ui/icon-button.tsx +18 -0
- package/src/components/platform-ui/panel-select.tsx +123 -0
- package/src/components/platform-ui/panel.tsx +97 -0
- package/src/components/platform-ui/review-dropdown-menu.tsx +35 -0
- package/src/components/platform-ui/sidebar.tsx +293 -0
- package/src/components/platform-ui/toolbar-icon-button.tsx +34 -0
- package/src/components/platform-ui/toolbar.tsx +37 -0
- package/src/components/prototype-gallery-client.tsx +57 -0
- package/src/components/prototype-provider.tsx +20 -0
- package/src/components/prototypes/mini-pill-label.tsx +37 -0
- package/src/components/prototypes/prototype-change-log-panel.tsx +207 -0
- package/src/components/prototypes/prototype-comment-provider.tsx +61 -0
- package/src/components/prototypes/prototype-comment-toolbar.tsx +65 -0
- package/src/components/prototypes/prototype-control.tsx +31 -0
- package/src/components/prototypes/prototype-controls.tsx +553 -0
- package/src/components/prototypes/prototype-create-exploration-modal.tsx +117 -0
- package/src/components/prototypes/prototype-create-state-modal.tsx +117 -0
- package/src/components/prototypes/prototype-default-state-map-page.tsx +66 -0
- package/src/components/prototypes/prototype-design-brief-modal.tsx +128 -0
- package/src/components/prototypes/prototype-design-brief-panel.tsx +525 -0
- package/src/components/prototypes/prototype-design-context-panel.tsx +65 -0
- package/src/components/prototypes/prototype-floating-pill.module.scss +1341 -0
- package/src/components/prototypes/prototype-mobbin-gallery.tsx +330 -0
- package/src/components/prototypes/prototype-reference-docs.tsx +222 -0
- package/src/components/prototypes/prototype-review-chrome.module.scss +630 -0
- package/src/components/prototypes/prototype-review-chrome.tsx +1241 -0
- package/src/components/prototypes/prototype-review-dropdown-menu-item.tsx +1 -0
- package/src/components/prototypes/prototype-review-dropdown-menu-separator.tsx +1 -0
- package/src/components/prototypes/prototype-review-panel-menu-item.tsx +1 -0
- package/src/components/prototypes/prototype-review-panel-menu-separator.tsx +1 -0
- package/src/components/prototypes/prototype-review-panel-select.tsx +4 -0
- package/src/components/prototypes/prototype-review-sidebar.tsx +167 -0
- package/src/components/prototypes/prototype-share-link-restore.tsx +75 -0
- package/src/components/prototypes/prototype-share-mode.tsx +206 -0
- package/src/components/prototypes/prototype-spec-panel-content.tsx +538 -0
- package/src/components/prototypes/prototype-state-canvas-overlay.module.scss +336 -0
- package/src/components/prototypes/prototype-state-canvas-overlay.tsx +1023 -0
- package/src/components/prototypes/prototype-state-canvas.tsx +38 -0
- package/src/components/prototypes/prototype-state-screenshot-preview.tsx +47 -0
- package/src/components/prototypes/prototype-target.tsx +120 -0
- package/src/components/prototypes/prototype-tweak-highlight.tsx +132 -0
- package/src/components/prototypes/prototype-tweaks.tsx +48 -0
- package/src/components/prototypes/prototype-variant-explorer.tsx +347 -0
- package/src/components/prototypes/prototype-variant-rationale-panel.tsx +49 -0
- package/src/components/prototypes/prototype-variant-tabs.tsx +207 -0
- package/src/components/prototypes/prototype-vercel-preview-modal.tsx +120 -0
- package/src/components/prototypes/state-map-annotation-tooltip.module.scss +89 -0
- package/src/components/prototypes/state-map-annotation-tooltip.tsx +137 -0
- package/src/components/prototypes/state-map-wireframe-highlight.module.scss +25 -0
- package/src/components/prototypes/state-map-wireframe-highlight.tsx +82 -0
- package/src/components/shell/prototype-gallery-shell.tsx +79 -0
- package/src/components/shell/prototype-screenshot-capture.tsx +45 -0
- package/src/components/shell/prototype-shell.module.scss +194 -0
- package/src/components/shell/prototype-shell.tsx +160 -0
- package/src/components/shell/prototype-state-screenshot-capture.tsx +68 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/dialog.module.scss +81 -0
- package/src/components/ui/dialog.tsx +127 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/option-select.tsx +81 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/slider.tsx +28 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/toggle-group.tsx +80 -0
- package/src/components/ui/tooltip.tsx +32 -0
- package/src/config/create-prototype-gallery-page.tsx +65 -0
- package/src/config/create-prototype-host-app.tsx +121 -0
- package/src/config/create-prototype-page.tsx +41 -0
- package/src/config/create-prototype-site-layout.tsx +28 -0
- package/src/config/define-prototype-config.ts +7 -0
- package/src/config/prototype-icon.tsx +25 -0
- package/src/config/prototype-site-layout-client.tsx +35 -0
- package/src/config/with-prototype.ts +20 -0
- package/src/index.ts +303 -0
- package/src/lib/pr-split/build-pr-agent-prompt.ts +68 -0
- package/src/lib/pr-split/build-pr-split-prototype-url.ts +12 -0
- package/src/lib/pr-split/pr-split-highlight.ts +6 -0
- package/src/lib/pr-split/pr-split-types.ts +58 -0
- package/src/lib/pr-split/resolve-source-preview-url.ts +45 -0
- package/src/lib/pr-split/use-pr-vercel-previews.ts +69 -0
- package/src/lib/prototype-comments/core/annotation-channel.ts +41 -0
- package/src/lib/prototype-comments/core/annotation-status.ts +60 -0
- package/src/lib/prototype-comments/core/annotation-target.ts +567 -0
- package/src/lib/prototype-comments/core/capture-theme.ts +41 -0
- package/src/lib/prototype-comments/core/capture.ts +11 -0
- package/src/lib/prototype-comments/core/comment-capture-blocked.ts +47 -0
- package/src/lib/prototype-comments/core/comment-threads.ts +52 -0
- package/src/lib/prototype-comments/core/element-identification.ts +615 -0
- package/src/lib/prototype-comments/core/export.ts +35 -0
- package/src/lib/prototype-comments/core/freeze-animations.ts +266 -0
- package/src/lib/prototype-comments/core/identify-element.ts +76 -0
- package/src/lib/prototype-comments/core/normalize-viewport.ts +35 -0
- package/src/lib/prototype-comments/core/react-detection.ts +704 -0
- package/src/lib/prototype-comments/core/restore.ts +7 -0
- package/src/lib/prototype-comments/core/screenshot.ts +428 -0
- package/src/lib/prototype-comments/core/source-location.ts +906 -0
- package/src/lib/prototype-comments/core/storage.ts +80 -0
- package/src/lib/prototype-comments/core/types.ts +79 -0
- package/src/lib/prototype-comments/core/validation.ts +17 -0
- package/src/lib/prototype-comments/hooks/useLayoutShift.ts +61 -0
- package/src/lib/prototype-comments/index.ts +58 -0
- package/src/lib/prototype-comments/react/CommentCaptureToolbar.tsx +1398 -0
- package/src/lib/prototype-comments/react/CommentProvider.tsx +298 -0
- package/src/lib/prototype-comments/scss.d.ts +10 -0
- package/src/lib/prototype-comments/ui/CommentAnnotationOverlay.tsx +253 -0
- package/src/lib/prototype-comments/ui/CommentDetailPage.tsx +56 -0
- package/src/lib/prototype-comments/ui/CommentHeaderCount.tsx +28 -0
- package/src/lib/prototype-comments/ui/CommentThread.tsx +331 -0
- package/src/lib/prototype-comments/ui/CommentsGrid.tsx +780 -0
- package/src/lib/prototype-comments/ui/CommentsListPage.tsx +25 -0
- package/src/lib/prototype-comments/ui/CommentsSidebar.tsx +77 -0
- package/src/lib/prototype-comments/ui/annotation-marker/index.tsx +200 -0
- package/src/lib/prototype-comments/ui/annotation-marker/styles.module.scss +194 -0
- package/src/lib/prototype-comments/ui/annotation-popup/index.tsx +335 -0
- package/src/lib/prototype-comments/ui/annotation-popup/styles.module.scss +469 -0
- package/src/lib/prototype-comments/ui/capture-styles.module.scss +2225 -0
- package/src/lib/prototype-comments/ui/comment-highlight.ts +48 -0
- package/src/lib/prototype-comments/ui/comment-thread.module.scss +203 -0
- package/src/lib/prototype-comments/ui/comments-grid-card.module.scss +253 -0
- package/src/lib/prototype-comments/ui/comments-sidebar.module.scss +123 -0
- package/src/lib/prototype-comments/ui/icon-transitions.module.scss +51 -0
- package/src/lib/prototype-comments/ui/icons.tsx +1086 -0
- package/src/lib/prototype-comments/ui/is-comment-detail-path.ts +7 -0
- package/src/lib/prototypes/capture-prototype-viewport.ts +32 -0
- package/src/lib/prototypes/changelog-meta-context.tsx +162 -0
- package/src/lib/prototypes/changelog-meta-storage.ts +62 -0
- package/src/lib/prototypes/changelog-meta.ts +95 -0
- package/src/lib/prototypes/create-prototype-registry.ts +74 -0
- package/src/lib/prototypes/create-state-screenshot-thumbnail.ts +46 -0
- package/src/lib/prototypes/design-exploration-default-storage.ts +68 -0
- package/src/lib/prototypes/design-exploration-default.ts +51 -0
- package/src/lib/prototypes/design-exploration-types.ts +203 -0
- package/src/lib/prototypes/prototype-comment-registry.tsx +145 -0
- package/src/lib/prototypes/prototype-comment-review-bridge.tsx +40 -0
- package/src/lib/prototypes/prototype-comment-review-state.ts +57 -0
- package/src/lib/prototypes/prototype-component-registry.ts +40 -0
- package/src/lib/prototypes/prototype-config-types.ts +34 -0
- package/src/lib/prototypes/prototype-preview-state-registry.ts +443 -0
- package/src/lib/prototypes/prototype-preview-state-types.ts +89 -0
- package/src/lib/prototypes/prototype-review-context.tsx +905 -0
- package/src/lib/prototypes/prototype-review-url.ts +134 -0
- package/src/lib/prototypes/prototype-share-link.ts +175 -0
- package/src/lib/prototypes/prototype-state-canvas-constants.ts +118 -0
- package/src/lib/prototypes/prototype-state-canvas-types.ts +159 -0
- package/src/lib/prototypes/prototype-state-screenshot.ts +39 -0
- package/src/lib/prototypes/prototype-tweak-registry.ts +96 -0
- package/src/lib/prototypes/prototype-tweak-types.ts +31 -0
- package/src/lib/prototypes/reference-docs.ts +137 -0
- package/src/lib/prototypes/screenshot-manifest.ts +33 -0
- package/src/lib/prototypes/screenshot-src.ts +7 -0
- package/src/lib/prototypes/share-command.ts +8 -0
- package/src/lib/prototypes/state-map-annotation.ts +70 -0
- package/src/lib/prototypes/use-design-exploration-default.ts +114 -0
- package/src/lib/prototypes/use-persisted-local-state.ts +140 -0
- package/src/lib/prototypes/use-prototype-comments.ts +63 -0
- package/src/lib/prototypes/use-prototype-mobile-viewport-frame.ts +46 -0
- package/src/lib/prototypes/use-prototype-state-screenshot-src.ts +71 -0
- package/src/lib/prototypes/use-prototype-tool-theme.tsx +115 -0
- package/src/lib/prototypes/use-prototype-viewport-frame.ts +34 -0
- package/src/lib/prototypes/use-register-prototype-reference-docs.ts +16 -0
- package/src/lib/prototypes/use-snap-corner-position.ts +417 -0
- package/src/lib/prototypes/use-toolbar-hover-panel.ts +98 -0
- package/src/lib/prototypes/variant-set-lucide-icon.tsx +55 -0
- package/src/lib/tool-portal.ts +33 -0
- package/src/lib/use-copy-to-clipboard.ts +81 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/vercel-preview/parse-vercel-github-comment.ts +81 -0
- package/src/server/create-host-api-route.ts +100 -0
- package/src/server/create-prototype-api-routes.ts +428 -0
- package/src/server/gallery/create-gallery-api.ts +56 -0
- package/src/server/gallery/folders-route.ts +156 -0
- package/src/server/gallery/gallery-env.ts +39 -0
- package/src/server/gallery/gallery-route.ts +150 -0
- package/src/server/gallery/gallery-types.ts +15 -0
- package/src/server/gallery/image-route.ts +59 -0
- package/src/server/gallery/item-route.ts +92 -0
- package/src/server/pr-vercel-preview-route.ts +162 -0
- package/src/server/redis/client.ts +55 -0
- package/src/server/redis/prototype-changelog-meta.ts +31 -0
- package/src/server/redis/prototype-comments.ts +92 -0
- package/src/server/redis/prototype-design-exploration-default.ts +32 -0
- package/src/server.ts +51 -0
- package/src/styles/globals.css +624 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
import { getElementClasses } from "./element-identification";
|
|
2
|
+
|
|
3
|
+
export const PROTOTYPE_TARGET_ATTR = "data-prototype-target";
|
|
4
|
+
const PROTOTYPE_SCREENSHOT_ROOT = "[data-prototype-screenshot]";
|
|
5
|
+
|
|
6
|
+
export type AnnotationPositionAnchor = "viewport" | "element";
|
|
7
|
+
|
|
8
|
+
export type AnnotationTargetFields = {
|
|
9
|
+
/** Full id from data-prototype-target */
|
|
10
|
+
targetId?: string;
|
|
11
|
+
boundingBox?: { x: number; y: number; width: number; height: number };
|
|
12
|
+
isFixed?: boolean;
|
|
13
|
+
/** % within the target element (0–100). Legacy annotations use viewport width % for x. */
|
|
14
|
+
x?: number;
|
|
15
|
+
/** % within the target element (0–100), or legacy absolute px from document/viewport top. */
|
|
16
|
+
y?: number;
|
|
17
|
+
/** New annotations anchor the marker relative to the clicked element. */
|
|
18
|
+
positionAnchor?: AnnotationPositionAnchor;
|
|
19
|
+
element?: string;
|
|
20
|
+
cssClasses?: string;
|
|
21
|
+
nearbyText?: string;
|
|
22
|
+
selectedText?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type AnnotationTargetOptions = {
|
|
26
|
+
visibleEventTypeIds?: number[];
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type Box = { x: number; y: number; width: number; height: number };
|
|
30
|
+
|
|
31
|
+
export function getCommentsSidebarWidth(): number {
|
|
32
|
+
const raw = getComputedStyle(document.documentElement).getPropertyValue(
|
|
33
|
+
"--comments-sidebar-width",
|
|
34
|
+
);
|
|
35
|
+
const width = parseFloat(raw) || 400;
|
|
36
|
+
return Math.min(width, window.innerWidth);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getCommentsSidebarInset(): number {
|
|
40
|
+
if (!document.documentElement.hasAttribute("data-comments-sidebar-open")) return 0;
|
|
41
|
+
// Inline sidebar already shrinks the prototype content area — no coordinate offset needed.
|
|
42
|
+
if (document.documentElement.hasAttribute("data-comments-sidebar-inline")) return 0;
|
|
43
|
+
return getCommentsSidebarWidth();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function deepElementFromPoint(x: number, y: number): HTMLElement | null {
|
|
47
|
+
let element = document.elementFromPoint(x, y) as HTMLElement | null;
|
|
48
|
+
if (!element) return null;
|
|
49
|
+
|
|
50
|
+
while (element?.shadowRoot) {
|
|
51
|
+
const deeper = element.shadowRoot.elementFromPoint(x, y) as HTMLElement | null;
|
|
52
|
+
if (!deeper || deeper === element) break;
|
|
53
|
+
element = deeper;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return element;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getStoredViewportBox(
|
|
60
|
+
annotation: AnnotationTargetFields,
|
|
61
|
+
scrollY: number,
|
|
62
|
+
): Box | null {
|
|
63
|
+
if (!annotation.boundingBox) return null;
|
|
64
|
+
|
|
65
|
+
const bb = annotation.boundingBox;
|
|
66
|
+
return {
|
|
67
|
+
x: bb.x,
|
|
68
|
+
y: annotation.isFixed ? bb.y : bb.y - scrollY,
|
|
69
|
+
width: bb.width,
|
|
70
|
+
height: bb.height,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getBoxOverlapRatio(a: Box, b: Box): number {
|
|
75
|
+
const left = Math.max(a.x, b.x);
|
|
76
|
+
const top = Math.max(a.y, b.y);
|
|
77
|
+
const right = Math.min(a.x + a.width, b.x + b.width);
|
|
78
|
+
const bottom = Math.min(a.y + a.height, b.y + b.height);
|
|
79
|
+
if (right <= left || bottom <= top) return 0;
|
|
80
|
+
|
|
81
|
+
const intersection = (right - left) * (bottom - top);
|
|
82
|
+
const minArea = Math.min(a.width * a.height, b.width * b.height);
|
|
83
|
+
if (minArea <= 0) return 0;
|
|
84
|
+
|
|
85
|
+
return intersection / minArea;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function boxesOverlap(a: Box, b: Box, minRatio = 0.3): boolean {
|
|
89
|
+
return getBoxOverlapRatio(a, b) >= minRatio;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isElementVisible(element: HTMLElement): boolean {
|
|
93
|
+
if (!document.contains(element)) return false;
|
|
94
|
+
if (element.hidden) return false;
|
|
95
|
+
|
|
96
|
+
const rect = element.getBoundingClientRect();
|
|
97
|
+
if (rect.width < 1 || rect.height < 1) return false;
|
|
98
|
+
|
|
99
|
+
const style = window.getComputedStyle(element);
|
|
100
|
+
if (style.display === "none" || style.visibility === "hidden") return false;
|
|
101
|
+
if (parseFloat(style.opacity) === 0) return false;
|
|
102
|
+
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isEventTypeVisible(
|
|
107
|
+
element: HTMLElement,
|
|
108
|
+
visibleEventTypeIds?: number[],
|
|
109
|
+
): boolean {
|
|
110
|
+
if (!visibleEventTypeIds) return true;
|
|
111
|
+
|
|
112
|
+
const container = element.closest("[data-event-type-id]");
|
|
113
|
+
if (!container) return true;
|
|
114
|
+
|
|
115
|
+
const id = Number(container.getAttribute("data-event-type-id"));
|
|
116
|
+
if (Number.isNaN(id)) return true;
|
|
117
|
+
|
|
118
|
+
return visibleEventTypeIds.includes(id);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function classesMatch(storedClasses: string | undefined, element: HTMLElement): boolean {
|
|
122
|
+
if (!storedClasses) return true;
|
|
123
|
+
|
|
124
|
+
const expected = storedClasses
|
|
125
|
+
.split(/,\s*/)
|
|
126
|
+
.map((value) => value.trim())
|
|
127
|
+
.filter(Boolean);
|
|
128
|
+
if (expected.length === 0) return true;
|
|
129
|
+
|
|
130
|
+
const actual = getElementClasses(element)
|
|
131
|
+
.split(/,\s*/)
|
|
132
|
+
.map((value) => value.trim())
|
|
133
|
+
.filter(Boolean);
|
|
134
|
+
|
|
135
|
+
return expected.some((className) => actual.includes(className));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function elementLabelMatches(storedElement: string | undefined, element: HTMLElement): boolean {
|
|
139
|
+
if (!storedElement) return true;
|
|
140
|
+
|
|
141
|
+
const stored = storedElement.toLowerCase();
|
|
142
|
+
const text = element.textContent?.trim().toLowerCase() ?? "";
|
|
143
|
+
const quoted = stored.match(/"([^"]+)"/)?.[1];
|
|
144
|
+
|
|
145
|
+
if (quoted && text.includes(quoted)) return true;
|
|
146
|
+
|
|
147
|
+
if (stored.includes("input") && element.tagName.toLowerCase() === "input") return true;
|
|
148
|
+
if (stored.includes("button") && element.tagName.toLowerCase() === "button") return true;
|
|
149
|
+
if (stored.includes("link") && element.tagName.toLowerCase() === "a") return true;
|
|
150
|
+
|
|
151
|
+
const tag = element.tagName.toLowerCase();
|
|
152
|
+
if (stored.startsWith(tag)) return true;
|
|
153
|
+
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function nearbyTextMatches(
|
|
158
|
+
nearbyText: string | undefined,
|
|
159
|
+
selectedText: string | undefined,
|
|
160
|
+
element: HTMLElement,
|
|
161
|
+
): boolean {
|
|
162
|
+
if (!nearbyText && !selectedText) return true;
|
|
163
|
+
|
|
164
|
+
const haystack = element.textContent?.trim().toLowerCase() ?? "";
|
|
165
|
+
if (selectedText && haystack.includes(selectedText.trim().toLowerCase())) return true;
|
|
166
|
+
if (nearbyText && haystack.includes(nearbyText.trim().toLowerCase().slice(0, 40))) {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function isAnnotationAgentRoot(element: HTMLElement): boolean {
|
|
174
|
+
return Boolean(element.closest("[data-prototype-comment-root]") || element.closest("[data-annotation-marker]"));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function getElementsAtPoint(x: number, y: number): HTMLElement[] {
|
|
178
|
+
const hits: HTMLElement[] = [];
|
|
179
|
+
const seen = new Set<HTMLElement>();
|
|
180
|
+
|
|
181
|
+
const add = (element: HTMLElement | null) => {
|
|
182
|
+
if (!element || seen.has(element) || isAnnotationAgentRoot(element)) return;
|
|
183
|
+
seen.add(element);
|
|
184
|
+
hits.push(element);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
add(deepElementFromPoint(x, y));
|
|
188
|
+
|
|
189
|
+
for (const node of document.elementsFromPoint(x, y)) {
|
|
190
|
+
if (!(node instanceof HTMLElement) || isAnnotationAgentRoot(node)) continue;
|
|
191
|
+
add(node);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return hits;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function collectCandidates(hits: HTMLElement[]): HTMLElement[] {
|
|
198
|
+
const candidates: HTMLElement[] = [];
|
|
199
|
+
const seen = new Set<HTMLElement>();
|
|
200
|
+
|
|
201
|
+
for (const hit of hits) {
|
|
202
|
+
let current: HTMLElement | null = hit;
|
|
203
|
+
|
|
204
|
+
for (let depth = 0; depth < 12 && current; depth += 1) {
|
|
205
|
+
if (!seen.has(current)) {
|
|
206
|
+
seen.add(current);
|
|
207
|
+
candidates.push(current);
|
|
208
|
+
}
|
|
209
|
+
current = current.parentElement;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return candidates;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function hasIdentityMatch(
|
|
217
|
+
annotation: AnnotationTargetFields,
|
|
218
|
+
element: HTMLElement,
|
|
219
|
+
): boolean {
|
|
220
|
+
return (
|
|
221
|
+
classesMatch(annotation.cssClasses, element) &&
|
|
222
|
+
elementLabelMatches(annotation.element, element) &&
|
|
223
|
+
nearbyTextMatches(annotation.nearbyText, annotation.selectedText, element)
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function getCandidateBoxScore(
|
|
228
|
+
storedViewportBox: Box,
|
|
229
|
+
element: HTMLElement,
|
|
230
|
+
annotation: AnnotationTargetFields,
|
|
231
|
+
): number {
|
|
232
|
+
const rect = element.getBoundingClientRect();
|
|
233
|
+
const candidateBox = {
|
|
234
|
+
x: rect.left,
|
|
235
|
+
y: rect.top,
|
|
236
|
+
width: rect.width,
|
|
237
|
+
height: rect.height,
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
const overlap = getBoxOverlapRatio(storedViewportBox, candidateBox);
|
|
241
|
+
if (overlap < 0.3) return -1;
|
|
242
|
+
|
|
243
|
+
const widthDiff =
|
|
244
|
+
Math.abs(candidateBox.width - storedViewportBox.width) /
|
|
245
|
+
Math.max(storedViewportBox.width, 1);
|
|
246
|
+
const heightDiff =
|
|
247
|
+
Math.abs(candidateBox.height - storedViewportBox.height) /
|
|
248
|
+
Math.max(storedViewportBox.height, 1);
|
|
249
|
+
const sizeScore = Math.max(0, 1 - (widthDiff + heightDiff) / 2);
|
|
250
|
+
|
|
251
|
+
const storedCenterX = storedViewportBox.x + storedViewportBox.width / 2;
|
|
252
|
+
const storedCenterY = storedViewportBox.y + storedViewportBox.height / 2;
|
|
253
|
+
const candidateCenterX = candidateBox.x + candidateBox.width / 2;
|
|
254
|
+
const candidateCenterY = candidateBox.y + candidateBox.height / 2;
|
|
255
|
+
const positionPenalty =
|
|
256
|
+
(Math.abs(candidateCenterX - storedCenterX) +
|
|
257
|
+
Math.abs(candidateCenterY - storedCenterY)) /
|
|
258
|
+
Math.max(storedViewportBox.width + storedViewportBox.height, 1);
|
|
259
|
+
const positionScore = Math.max(0, 1 - positionPenalty);
|
|
260
|
+
|
|
261
|
+
let score = overlap * 0.35 + sizeScore * 0.5 + positionScore * 0.15;
|
|
262
|
+
if (hasIdentityMatch(annotation, element)) score += 0.35;
|
|
263
|
+
|
|
264
|
+
return score;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function isValidAnnotationTarget(
|
|
268
|
+
element: HTMLElement,
|
|
269
|
+
annotation: AnnotationTargetFields,
|
|
270
|
+
storedViewportBox: Box,
|
|
271
|
+
options?: AnnotationTargetOptions,
|
|
272
|
+
): boolean {
|
|
273
|
+
if (isAnnotationAgentRoot(element)) return false;
|
|
274
|
+
if (!isElementVisible(element)) return false;
|
|
275
|
+
if (!isEventTypeVisible(element, options?.visibleEventTypeIds)) return false;
|
|
276
|
+
|
|
277
|
+
const rect = element.getBoundingClientRect();
|
|
278
|
+
const candidateBox = {
|
|
279
|
+
x: rect.left,
|
|
280
|
+
y: rect.top,
|
|
281
|
+
width: rect.width,
|
|
282
|
+
height: rect.height,
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
if (!boxesOverlap(storedViewportBox, candidateBox)) return false;
|
|
286
|
+
|
|
287
|
+
const widthRatio = rect.width / storedViewportBox.width;
|
|
288
|
+
const heightRatio = rect.height / storedViewportBox.height;
|
|
289
|
+
if (widthRatio < 0.45 || heightRatio < 0.45) return false;
|
|
290
|
+
|
|
291
|
+
if (hasIdentityMatch(annotation, element)) return true;
|
|
292
|
+
|
|
293
|
+
// Reject ancestors that are much larger than the stored target unless overlap is very tight.
|
|
294
|
+
if (widthRatio > 1.15 && heightRatio > 1.15) {
|
|
295
|
+
return boxesOverlap(storedViewportBox, candidateBox, 0.85);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Fall back to a tighter overlap when identity metadata is sparse.
|
|
299
|
+
return boxesOverlap(storedViewportBox, candidateBox, 0.55);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function pickBestCandidate(
|
|
303
|
+
annotation: AnnotationTargetFields,
|
|
304
|
+
storedViewportBox: Box,
|
|
305
|
+
centerX: number,
|
|
306
|
+
options?: AnnotationTargetOptions,
|
|
307
|
+
): HTMLElement | null {
|
|
308
|
+
const centerY = storedViewportBox.y + storedViewportBox.height / 2;
|
|
309
|
+
const hits = getElementsAtPoint(centerX, centerY);
|
|
310
|
+
if (hits.length === 0) return null;
|
|
311
|
+
|
|
312
|
+
let best: HTMLElement | null = null;
|
|
313
|
+
let bestScore = -1;
|
|
314
|
+
|
|
315
|
+
for (const candidate of collectCandidates(hits)) {
|
|
316
|
+
if (!isValidAnnotationTarget(candidate, annotation, storedViewportBox, options)) {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const score = getCandidateBoxScore(storedViewportBox, candidate, annotation);
|
|
321
|
+
if (score > bestScore) {
|
|
322
|
+
bestScore = score;
|
|
323
|
+
best = candidate;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return best;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function getPrototypeTargetElement(
|
|
331
|
+
element: HTMLElement | null,
|
|
332
|
+
): HTMLElement | null {
|
|
333
|
+
if (!element) return null;
|
|
334
|
+
const anchor = element.closest(`[${PROTOTYPE_TARGET_ATTR}]`);
|
|
335
|
+
return anchor instanceof HTMLElement ? anchor : null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export function resolveAnnotationTargetById(
|
|
339
|
+
targetId: string,
|
|
340
|
+
): HTMLElement | null {
|
|
341
|
+
const root = document.querySelector(PROTOTYPE_SCREENSHOT_ROOT);
|
|
342
|
+
if (!root) return null;
|
|
343
|
+
|
|
344
|
+
const el = root.querySelector(
|
|
345
|
+
`[${PROTOTYPE_TARGET_ATTR}="${CSS.escape(targetId)}"]`,
|
|
346
|
+
);
|
|
347
|
+
if (!(el instanceof HTMLElement)) return null;
|
|
348
|
+
if (!isElementVisible(el)) return null;
|
|
349
|
+
return el;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function getPrototypeScrollContainer(): HTMLElement | null {
|
|
353
|
+
const root = document.querySelector(PROTOTYPE_SCREENSHOT_ROOT);
|
|
354
|
+
if (!root) return null;
|
|
355
|
+
|
|
356
|
+
const labeled = root.querySelector(
|
|
357
|
+
`[${PROTOTYPE_TARGET_ATTR}$="scroll-container"]`,
|
|
358
|
+
);
|
|
359
|
+
if (labeled instanceof HTMLElement) {
|
|
360
|
+
return labeled;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return (
|
|
364
|
+
root.querySelector(".overflow-y-auto") ??
|
|
365
|
+
(root instanceof HTMLElement ? root : null)
|
|
366
|
+
) as HTMLElement | null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function resolveAnnotationTargetElement(
|
|
370
|
+
annotation: AnnotationTargetFields,
|
|
371
|
+
scrollY: number,
|
|
372
|
+
options?: AnnotationTargetOptions,
|
|
373
|
+
): HTMLElement | null {
|
|
374
|
+
if (annotation.targetId) {
|
|
375
|
+
const byId = resolveAnnotationTargetById(annotation.targetId);
|
|
376
|
+
if (byId) return byId;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (!annotation.boundingBox) return null;
|
|
380
|
+
|
|
381
|
+
const storedViewportBox = getStoredViewportBox(annotation, scrollY);
|
|
382
|
+
if (!storedViewportBox) return null;
|
|
383
|
+
|
|
384
|
+
const centerX = annotation.boundingBox.x + annotation.boundingBox.width / 2;
|
|
385
|
+
const sidebarInset = getCommentsSidebarInset();
|
|
386
|
+
|
|
387
|
+
// Coordinates were captured before the comments sidebar shifted the layout.
|
|
388
|
+
const centerXs =
|
|
389
|
+
sidebarInset > 0 ? [centerX - sidebarInset, centerX] : [centerX];
|
|
390
|
+
|
|
391
|
+
let best: HTMLElement | null = null;
|
|
392
|
+
let bestScore = -1;
|
|
393
|
+
|
|
394
|
+
for (const x of centerXs) {
|
|
395
|
+
const candidate = pickBestCandidate(annotation, storedViewportBox, x, options);
|
|
396
|
+
if (!candidate) continue;
|
|
397
|
+
|
|
398
|
+
const score = getCandidateBoxScore(storedViewportBox, candidate, annotation);
|
|
399
|
+
if (score > bestScore) {
|
|
400
|
+
bestScore = score;
|
|
401
|
+
best = candidate;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (!best && sidebarInset === 0) {
|
|
406
|
+
return pickBestCandidate(
|
|
407
|
+
annotation,
|
|
408
|
+
storedViewportBox,
|
|
409
|
+
centerX + getCommentsSidebarWidth(),
|
|
410
|
+
options,
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return best;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function isAnnotationTargetMounted(
|
|
418
|
+
annotation: AnnotationTargetFields,
|
|
419
|
+
scrollY: number,
|
|
420
|
+
options?: AnnotationTargetOptions,
|
|
421
|
+
): boolean {
|
|
422
|
+
const element = resolveAnnotationTargetElement(annotation, scrollY, options);
|
|
423
|
+
return element !== null && document.contains(element) && isElementVisible(element);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export function usesElementRelativePosition(
|
|
427
|
+
annotation: AnnotationTargetFields,
|
|
428
|
+
): boolean {
|
|
429
|
+
return annotation.positionAnchor === "element";
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function computeElementRelativeMarkerPosition(
|
|
433
|
+
clientX: number,
|
|
434
|
+
clientY: number,
|
|
435
|
+
rect: Pick<DOMRect, "left" | "top" | "width" | "height">,
|
|
436
|
+
): { x: number; y: number } {
|
|
437
|
+
const width = Math.max(rect.width, 1);
|
|
438
|
+
const height = Math.max(rect.height, 1);
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
x: ((clientX - rect.left) / width) * 100,
|
|
442
|
+
y: ((clientY - rect.top) / height) * 100,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
export function computeElementCenterMarkerPosition(): { x: number; y: number } {
|
|
447
|
+
return { x: 50, y: 50 };
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export function getAnnotationClickOffset(
|
|
451
|
+
annotation: AnnotationTargetFields,
|
|
452
|
+
targetRect?: Pick<DOMRect, "width" | "height">,
|
|
453
|
+
): { offsetX: number; offsetY: number } | null {
|
|
454
|
+
if (!annotation.boundingBox || annotation.x === undefined || annotation.y === undefined) {
|
|
455
|
+
return null;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const bb = annotation.boundingBox;
|
|
459
|
+
|
|
460
|
+
if (usesElementRelativePosition(annotation)) {
|
|
461
|
+
const width = Math.max(targetRect?.width ?? bb.width, 1);
|
|
462
|
+
const height = Math.max(targetRect?.height ?? bb.height, 1);
|
|
463
|
+
return {
|
|
464
|
+
offsetX: (annotation.x / 100) * width,
|
|
465
|
+
offsetY: (annotation.y / 100) * height,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
offsetX: (annotation.x / 100) * window.innerWidth - bb.x,
|
|
471
|
+
offsetY: annotation.y - bb.y,
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function getStoredElementRelativeMarkerPosition(
|
|
476
|
+
annotation: AnnotationTargetFields & { x: number; y: number },
|
|
477
|
+
): { left: string; top: number } | null {
|
|
478
|
+
if (!annotation.boundingBox || !usesElementRelativePosition(annotation)) {
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const bb = annotation.boundingBox;
|
|
483
|
+
return {
|
|
484
|
+
left: `${bb.x + (annotation.x / 100) * bb.width}px`,
|
|
485
|
+
top: bb.y + (annotation.y / 100) * bb.height,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export function getMarkerViewportX(
|
|
490
|
+
positionStyle: { left: string; top: number } | undefined,
|
|
491
|
+
fallbackXPercent: number,
|
|
492
|
+
): number {
|
|
493
|
+
if (positionStyle?.left.endsWith("px")) {
|
|
494
|
+
return parseFloat(positionStyle.left);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (positionStyle?.left.endsWith("%")) {
|
|
498
|
+
return (parseFloat(positionStyle.left) / 100) * window.innerWidth;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
return (fallbackXPercent / 100) * window.innerWidth;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
export function getAnnotationMarkerPosition(
|
|
505
|
+
annotation: AnnotationTargetFields & { x: number; y: number },
|
|
506
|
+
targetElement: HTMLElement | null,
|
|
507
|
+
scrollY: number,
|
|
508
|
+
): { left: string; top: number } {
|
|
509
|
+
const fallback =
|
|
510
|
+
getStoredElementRelativeMarkerPosition(annotation) ?? {
|
|
511
|
+
left: `${annotation.x}%`,
|
|
512
|
+
top: annotation.y,
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
if (!targetElement || !document.contains(targetElement) || !annotation.boundingBox) {
|
|
516
|
+
return fallback;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const rect = targetElement.getBoundingClientRect();
|
|
520
|
+
const offset = getAnnotationClickOffset(annotation, rect);
|
|
521
|
+
if (!offset) return fallback;
|
|
522
|
+
|
|
523
|
+
const markerViewportX = rect.left + offset.offsetX;
|
|
524
|
+
const markerViewportY = rect.top + offset.offsetY;
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
left: `${markerViewportX}px`,
|
|
528
|
+
top: annotation.isFixed ? markerViewportY : markerViewportY + scrollY,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export function getMarkerViewportPosition(
|
|
533
|
+
annotation: AnnotationTargetFields & { x: number; y: number },
|
|
534
|
+
scrollY: number,
|
|
535
|
+
targetElement?: HTMLElement | null,
|
|
536
|
+
): { x: number; y: number } {
|
|
537
|
+
const positionStyle = getAnnotationMarkerPosition(
|
|
538
|
+
annotation,
|
|
539
|
+
targetElement ?? null,
|
|
540
|
+
scrollY,
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
return {
|
|
544
|
+
x: getMarkerViewportX(positionStyle, annotation.x),
|
|
545
|
+
y: annotation.isFixed ? positionStyle.top : positionStyle.top - scrollY,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
export function getAnnotationOutlineBox(
|
|
550
|
+
annotation: AnnotationTargetFields & { boundingBox?: { x: number; y: number; width: number; height: number } },
|
|
551
|
+
targetElement: HTMLElement | null,
|
|
552
|
+
scrollY: number,
|
|
553
|
+
) {
|
|
554
|
+
if (targetElement && document.contains(targetElement) && isElementVisible(targetElement)) {
|
|
555
|
+
const rect = targetElement.getBoundingClientRect();
|
|
556
|
+
return { x: rect.left, y: rect.top, width: rect.width, height: rect.height };
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (!annotation.boundingBox) return null;
|
|
560
|
+
|
|
561
|
+
return {
|
|
562
|
+
x: annotation.boundingBox.x,
|
|
563
|
+
y: annotation.isFixed ? annotation.boundingBox.y : annotation.boundingBox.y - scrollY,
|
|
564
|
+
width: annotation.boundingBox.width,
|
|
565
|
+
height: annotation.boundingBox.height,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const COLOR_OPTIONS = [
|
|
2
|
+
{ id: "indigo", label: "Indigo", srgb: "#6155F5", p3: "color(display-p3 0.38 0.33 0.96)" },
|
|
3
|
+
{ id: "blue", label: "Blue", srgb: "#0088FF", p3: "color(display-p3 0.00 0.53 1.00)" },
|
|
4
|
+
{ id: "cyan", label: "Cyan", srgb: "#00C3D0", p3: "color(display-p3 0.00 0.76 0.82)" },
|
|
5
|
+
{ id: "green", label: "Green", srgb: "#34C759", p3: "color(display-p3 0.20 0.78 0.35)" },
|
|
6
|
+
{ id: "yellow", label: "Yellow", srgb: "#FFCC00", p3: "color(display-p3 1.00 0.80 0.00)" },
|
|
7
|
+
{ id: "orange", label: "Orange", srgb: "#FF8D28", p3: "color(display-p3 1.00 0.55 0.16)" },
|
|
8
|
+
{ id: "red", label: "Red", srgb: "#FF383C", p3: "color(display-p3 1.00 0.22 0.24)" },
|
|
9
|
+
] as const;
|
|
10
|
+
|
|
11
|
+
export function injectCaptureColorTokens() {
|
|
12
|
+
if (typeof document === "undefined") return;
|
|
13
|
+
if (document.getElementById("prototype-comments-color-tokens")) return;
|
|
14
|
+
|
|
15
|
+
const style = document.createElement("style");
|
|
16
|
+
style.id = "prototype-comments-color-tokens";
|
|
17
|
+
style.textContent = [
|
|
18
|
+
...COLOR_OPTIONS.map(
|
|
19
|
+
(c) => `
|
|
20
|
+
[data-prototype-comment-accent="${c.id}"] {
|
|
21
|
+
--prototype-comment-color-accent: ${c.srgb};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@supports (color: color(display-p3 0 0 0)) {
|
|
25
|
+
[data-prototype-comment-accent="${c.id}"] {
|
|
26
|
+
--prototype-comment-color-accent: ${c.p3};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
`,
|
|
30
|
+
),
|
|
31
|
+
`:root {
|
|
32
|
+
${COLOR_OPTIONS.map((c) => `--prototype-comment-color-${c.id}: ${c.srgb};`).join("\n")}
|
|
33
|
+
}`,
|
|
34
|
+
`@supports (color: color(display-p3 0 0 0)) {
|
|
35
|
+
:root {
|
|
36
|
+
${COLOR_OPTIONS.map((c) => `--prototype-comment-color-${c.id}: ${c.p3};`).join("\n")}
|
|
37
|
+
}
|
|
38
|
+
}`,
|
|
39
|
+
].join("\n");
|
|
40
|
+
document.head.appendChild(style);
|
|
41
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { CommentInteractionState } from "./types";
|
|
2
|
+
|
|
3
|
+
export function captureInteractionState<TState>(
|
|
4
|
+
live: TState,
|
|
5
|
+
): CommentInteractionState<TState> {
|
|
6
|
+
return structuredClone({
|
|
7
|
+
...live,
|
|
8
|
+
capturedAt: new Date().toISOString(),
|
|
9
|
+
route: typeof window !== "undefined" ? window.location.pathname : "/",
|
|
10
|
+
});
|
|
11
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { closestCrossingShadow } from "./element-identification";
|
|
2
|
+
|
|
3
|
+
/** DOM regions where comment capture must not attach (plugin chrome, not prototype UI). */
|
|
4
|
+
export const COMMENT_CAPTURE_BLOCKED_SELECTORS = [
|
|
5
|
+
"[data-feedback-toolbar]",
|
|
6
|
+
"[data-comment-capture-toolbar]",
|
|
7
|
+
"[data-prototype-review-trigger]",
|
|
8
|
+
"[data-prototype-review-footer]",
|
|
9
|
+
"[data-prototype-review-sidebar]",
|
|
10
|
+
"[data-prototype-review-panel]",
|
|
11
|
+
"[data-comments-sidebar]",
|
|
12
|
+
"#prototype-comments-sidebar-root",
|
|
13
|
+
"[data-annotation-popup]",
|
|
14
|
+
"[data-annotation-marker]",
|
|
15
|
+
"[data-prototype-share-hint]",
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
export function isCommentCaptureBlockedTarget(target: HTMLElement): boolean {
|
|
19
|
+
return COMMENT_CAPTURE_BLOCKED_SELECTORS.some(
|
|
20
|
+
(selector) => closestCrossingShadow(target, selector) != null,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** CSS :not() chain for descendants that should not receive comment-mode crosshair. */
|
|
25
|
+
export function buildCommentCaptureCursorExclusionCss(): string {
|
|
26
|
+
return COMMENT_CAPTURE_BLOCKED_SELECTORS.flatMap((selector) => [
|
|
27
|
+
`:not(${selector})`,
|
|
28
|
+
`:not(${selector} *)`,
|
|
29
|
+
]).join("");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Injected while comment/share capture is active. */
|
|
33
|
+
export function buildCommentCaptureCursorStyles(): string {
|
|
34
|
+
const exclusions = buildCommentCaptureCursorExclusionCss();
|
|
35
|
+
const blockedCursorReset = COMMENT_CAPTURE_BLOCKED_SELECTORS.map(
|
|
36
|
+
(selector) => `${selector}, ${selector} *`,
|
|
37
|
+
).join(",\n");
|
|
38
|
+
|
|
39
|
+
return `
|
|
40
|
+
body ${exclusions}:not([data-prototype-comment-root]):not([data-prototype-comment-root] *) {
|
|
41
|
+
cursor: crosshair !important;
|
|
42
|
+
}
|
|
43
|
+
${blockedCursorReset} {
|
|
44
|
+
cursor: auto !important;
|
|
45
|
+
}
|
|
46
|
+
`;
|
|
47
|
+
}
|