proto-plugin 0.1.1 → 0.1.3
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 +308 -29
- package/README.md +48 -6
- package/package.json +2 -1
- package/scripts/lib/host-config.example.sh +1 -1
- package/scripts/verify-prototype-preview-states.mjs +57 -0
- package/src/components/platform-ui/controls-panel-select.tsx +4 -3
- package/src/components/platform-ui/prototype-tool-dialog.tsx +117 -0
- package/src/components/platform-ui/sidebar.tsx +34 -16
- package/src/components/prototype-gallery-client.tsx +77 -37
- package/src/components/prototypes/design-exploration-variant-preview-shell.tsx +43 -0
- package/src/components/prototypes/prototype-brief-field.tsx +33 -0
- package/src/components/prototypes/prototype-change-log-panel.tsx +59 -55
- package/src/components/prototypes/prototype-comment-provider.tsx +0 -1
- package/src/components/prototypes/prototype-comment-storage-empty-state.tsx +41 -0
- package/src/components/prototypes/prototype-create-exploration-modal.tsx +41 -64
- package/src/components/prototypes/prototype-create-prototype-modal.tsx +102 -0
- package/src/components/prototypes/prototype-create-state-modal.tsx +31 -70
- package/src/components/prototypes/prototype-default-state-map-page.tsx +4 -4
- package/src/components/prototypes/prototype-design-brief-modal.tsx +10 -10
- package/src/components/prototypes/prototype-design-brief-panel.tsx +261 -231
- package/src/components/prototypes/prototype-floating-pill.module.scss +16 -9
- package/src/components/prototypes/prototype-mobbin-gallery.tsx +18 -2
- package/src/components/prototypes/prototype-review-chrome.tsx +21 -2
- package/src/components/prototypes/prototype-review-sidebar.tsx +7 -37
- package/src/components/prototypes/prototype-spec-panel-content.tsx +5 -5
- package/src/components/prototypes/prototype-spec-panel-empty-state.tsx +53 -0
- package/src/components/prototypes/prototype-state-canvas-overlay.module.scss +20 -47
- package/src/components/prototypes/prototype-state-canvas-overlay.tsx +4 -42
- package/src/components/prototypes/prototype-variant-explorer.tsx +32 -78
- package/src/components/prototypes/prototype-vercel-preview-modal.tsx +4 -3
- package/src/components/prototypes/state-map-annotation-tooltip.module.scss +7 -7
- package/src/components/shell/prototype-component-library-empty-state.tsx +59 -0
- package/src/components/shell/prototype-gallery-nav.tsx +124 -0
- package/src/components/shell/prototype-gallery-shell.tsx +45 -13
- package/src/components/shell/prototype-link-source-modal.tsx +100 -0
- package/src/components/shell/prototype-plugin-update-popup.tsx +74 -0
- package/src/components/ui/button.tsx +4 -2
- package/src/components/ui/dialog.tsx +44 -5
- package/src/components/ui/dropdown-menu.tsx +2 -2
- package/src/components/ui/input.tsx +1 -1
- package/src/components/ui/label.tsx +1 -1
- package/src/components/ui/option-select.tsx +3 -3
- package/src/components/ui/scroll-area.tsx +1 -1
- package/src/components/ui/separator.tsx +1 -1
- package/src/components/ui/slider.tsx +1 -1
- package/src/components/ui/switch.tsx +1 -1
- package/src/components/ui/tabs.tsx +1 -1
- package/src/components/ui/toggle-group.tsx +1 -1
- package/src/components/ui/tooltip.tsx +2 -2
- package/src/config/create-prototype-component-library-page.tsx +74 -0
- package/src/config/create-prototype-gallery-page.tsx +26 -23
- package/src/config/create-prototype-host-app.tsx +3 -15
- package/src/config/create-prototype-site-layout.tsx +14 -1
- package/src/config/create-prototype-state-map-page.tsx +44 -0
- package/src/config/prototype-site-layout-client.tsx +23 -6
- package/src/config/with-prototype.ts +54 -0
- package/src/index.ts +71 -11
- package/src/lib/copy-text-with-attachments.ts +35 -0
- package/src/lib/format-source-directory-name.ts +12 -0
- package/src/lib/platform-ui/sidebar-interactive-cursor.ts +19 -0
- package/src/lib/pr-split/build-pr-split-plan-prompt.ts +81 -0
- package/src/lib/pr-split/pr-split-highlight.ts +3 -0
- package/src/lib/prototype-comments/core/annotation-target.ts +12 -3
- package/src/lib/prototype-comments/core/comment-capture-blocked.ts +24 -0
- package/src/lib/prototype-comments/core/screenshot.ts +68 -31
- package/src/lib/prototype-comments/core/storage.ts +3 -6
- package/src/lib/prototype-comments/core/types.ts +2 -0
- package/src/lib/prototype-comments/hooks/useLayoutShift.ts +9 -2
- package/src/lib/prototype-comments/react/CommentCaptureToolbar.tsx +45 -10
- package/src/lib/prototype-comments/react/CommentProvider.tsx +19 -0
- package/src/lib/prototype-comments/ui/CommentModeToggleButton.tsx +34 -0
- package/src/lib/prototype-comments/ui/CommentsGrid.tsx +8 -16
- package/src/lib/prototype-comments/ui/CommentsSidebar.tsx +5 -19
- package/src/lib/prototype-comments/ui/capture-styles.module.scss +5 -1
- package/src/lib/prototype-comments/ui/comments-grid-card.module.scss +0 -7
- package/src/lib/prototypes/build-design-exploration-renderers.ts +44 -0
- package/src/lib/prototypes/changelog-description-prompt.ts +58 -0
- package/src/lib/prototypes/comment-storage-setup-prompt.ts +124 -0
- package/src/lib/prototypes/create-prototype-prompt.ts +96 -0
- package/src/lib/prototypes/create-prototype-registry.ts +5 -0
- package/src/lib/prototypes/design-exploration-types.ts +261 -10
- package/src/lib/prototypes/link-source-prompt.ts +93 -0
- package/src/lib/prototypes/populate-component-library-prompt.ts +64 -0
- package/src/lib/prototypes/proto-plugin-version.ts +19 -0
- package/src/lib/prototypes/prototype-config-types.ts +2 -0
- package/src/lib/prototypes/prototype-layout-classes.ts +19 -0
- package/src/lib/prototypes/prototype-preview-state-registry.ts +5 -2
- package/src/lib/prototypes/prototype-review-context.tsx +4 -4
- package/src/lib/prototypes/prototype-state-canvas-constants.ts +1 -1
- package/src/lib/prototypes/prototype-state-canvas-types.ts +0 -2
- package/src/lib/prototypes/prototype-storage-status.ts +31 -0
- package/src/lib/prototypes/sort-prototypes-by-creation-date.ts +43 -0
- package/src/lib/prototypes/upgrade-proto-plugin-prompt.ts +40 -0
- package/src/lib/prototypes/use-proto-plugin-version-check.ts +67 -0
- package/src/lib/prototypes/use-prototype-tool-theme.tsx +1 -1
- package/src/lib/prototypes/use-toolbar-hover-panel.ts +12 -1
- package/src/lib/prototypes/variant-set-lucide-icon.tsx +24 -0
- package/src/lib/tool-portal.ts +24 -0
- package/src/lib/use-copy-to-clipboard.ts +7 -13
- package/src/server/create-host-api-route.ts +14 -16
- package/src/server/create-prototype-api-routes.ts +0 -115
- package/src/server/proto-plugin-version-route.ts +145 -0
- package/src/server/prototype-storage-status-route.ts +29 -0
- package/src/server.ts +2 -8
- package/src/styles/globals.css +357 -69
- package/src/lib/prototypes/design-exploration-default-storage.ts +0 -68
- package/src/lib/prototypes/design-exploration-default.ts +0 -51
- package/src/lib/prototypes/use-design-exploration-default.ts +0 -114
- package/src/server/redis/prototype-design-exploration-default.ts +0 -32
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
// Cache the import result so we only try once
|
|
12
12
|
let _domCaptureModule: {
|
|
13
13
|
domToCanvas: (node: Node, options?: Record<string, unknown>) => Promise<HTMLCanvasElement>;
|
|
14
|
+
domToJpeg: (node: Node, options?: Record<string, unknown>) => Promise<string>;
|
|
14
15
|
} | null | undefined; // null = tried and failed, undefined = not tried yet
|
|
15
16
|
|
|
16
17
|
async function getDomCapture() {
|
|
@@ -36,6 +37,10 @@ import {
|
|
|
36
37
|
getCommentsSidebarInset,
|
|
37
38
|
getPrototypeScrollContainer,
|
|
38
39
|
} from "./annotation-target";
|
|
40
|
+
import {
|
|
41
|
+
getPrototypeScreenshotRoot,
|
|
42
|
+
PROTOTYPE_COMMENTS_SIDEBAR_ROOT_ID,
|
|
43
|
+
} from "@prototype/lib/tool-portal";
|
|
39
44
|
import type { CaptureViewport } from "./types";
|
|
40
45
|
|
|
41
46
|
type CaptureUiRestore = {
|
|
@@ -43,7 +48,7 @@ type CaptureUiRestore = {
|
|
|
43
48
|
};
|
|
44
49
|
|
|
45
50
|
/**
|
|
46
|
-
* Hide feedback overlays
|
|
51
|
+
* Hide feedback overlays and the comments sidebar during capture.
|
|
47
52
|
*/
|
|
48
53
|
async function prepareDomForCapture(): Promise<CaptureUiRestore> {
|
|
49
54
|
// Map ensures each element is hidden/restored once. The review toolbar matches
|
|
@@ -52,7 +57,6 @@ async function prepareDomForCapture(): Promise<CaptureUiRestore> {
|
|
|
52
57
|
|
|
53
58
|
const hideElement = (el: Element) => {
|
|
54
59
|
const html = el as HTMLElement;
|
|
55
|
-
if (html.closest("[data-comments-sidebar]")) return;
|
|
56
60
|
if (hidden.has(html)) return;
|
|
57
61
|
hidden.set(html, html.style.visibility);
|
|
58
62
|
html.style.visibility = "hidden";
|
|
@@ -66,18 +70,16 @@ async function prepareDomForCapture(): Promise<CaptureUiRestore> {
|
|
|
66
70
|
"[data-annotation-marker]",
|
|
67
71
|
"[data-annotation-popup]",
|
|
68
72
|
"[data-comment-capture-overlay]",
|
|
73
|
+
`#${PROTOTYPE_COMMENTS_SIDEBAR_ROOT_ID}`,
|
|
74
|
+
"[data-comments-sidebar]",
|
|
75
|
+
"[data-prototype-review-sidebar]",
|
|
69
76
|
];
|
|
70
77
|
|
|
71
78
|
for (const selector of captureUiSelectors) {
|
|
72
79
|
document.querySelectorAll(selector).forEach(hideElement);
|
|
73
80
|
}
|
|
74
81
|
|
|
75
|
-
|
|
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
|
-
});
|
|
82
|
+
document.querySelectorAll("[data-prototype-comment-root]").forEach(hideElement);
|
|
81
83
|
|
|
82
84
|
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
|
83
85
|
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
|
|
@@ -111,6 +113,31 @@ function cropSidebarFromCanvas(
|
|
|
111
113
|
return cropped;
|
|
112
114
|
}
|
|
113
115
|
|
|
116
|
+
function drawCanvasPreservingAspect(
|
|
117
|
+
ctx: CanvasRenderingContext2D,
|
|
118
|
+
source: HTMLCanvasElement,
|
|
119
|
+
destWidth: number,
|
|
120
|
+
destHeight: number,
|
|
121
|
+
) {
|
|
122
|
+
const targetAspect = destWidth / destHeight;
|
|
123
|
+
const sourceAspect = source.width / source.height;
|
|
124
|
+
|
|
125
|
+
let sx = 0;
|
|
126
|
+
let sy = 0;
|
|
127
|
+
let sw = source.width;
|
|
128
|
+
let sh = source.height;
|
|
129
|
+
|
|
130
|
+
if (sourceAspect > targetAspect) {
|
|
131
|
+
sw = source.height * targetAspect;
|
|
132
|
+
sx = (source.width - sw) / 2;
|
|
133
|
+
} else if (sourceAspect < targetAspect) {
|
|
134
|
+
sh = source.width / targetAspect;
|
|
135
|
+
sy = 0;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
ctx.drawImage(source, sx, sy, sw, sh, 0, 0, destWidth, destHeight);
|
|
139
|
+
}
|
|
140
|
+
|
|
114
141
|
export type ViewportScreenshotResult = {
|
|
115
142
|
dataUrl: string;
|
|
116
143
|
captureViewport: CaptureViewport;
|
|
@@ -129,33 +156,49 @@ export async function captureViewportScreenshot(
|
|
|
129
156
|
return null;
|
|
130
157
|
}
|
|
131
158
|
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
const isInlineSidebar = document.documentElement.hasAttribute("data-comments-sidebar-inline");
|
|
159
|
+
const screenshotRoot = getPrototypeScreenshotRoot();
|
|
160
|
+
const sidebarInset = screenshotRoot ? 0 : getCommentsSidebarInset();
|
|
135
161
|
const { restore } = await prepareDomForCapture();
|
|
136
162
|
|
|
137
163
|
try {
|
|
138
|
-
const captureTarget =
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const targetRect = captureTarget.getBoundingClientRect();
|
|
143
|
-
const viewportWidth = isInlineSidebar && inlineContent instanceof HTMLElement
|
|
144
|
-
? targetRect.width
|
|
164
|
+
const captureTarget = screenshotRoot ?? document.documentElement;
|
|
165
|
+
const viewportWidth = screenshotRoot
|
|
166
|
+
? screenshotRoot.clientWidth
|
|
145
167
|
: window.innerWidth;
|
|
146
|
-
const viewportHeight =
|
|
147
|
-
?
|
|
168
|
+
const viewportHeight = screenshotRoot
|
|
169
|
+
? screenshotRoot.clientHeight
|
|
148
170
|
: window.innerHeight;
|
|
149
171
|
const contentWidth = Math.max(1, viewportWidth - sidebarInset);
|
|
150
172
|
const maxWidth = 1200;
|
|
151
|
-
const scale = Math.min(1, maxWidth /
|
|
152
|
-
const
|
|
173
|
+
const scale = Math.min(1, maxWidth / contentWidth);
|
|
174
|
+
const scrollContainer = getPrototypeScrollContainer();
|
|
175
|
+
const captureViewport: CaptureViewport = {
|
|
176
|
+
width: screenshotRoot ? viewportWidth : contentWidth,
|
|
177
|
+
height: viewportHeight,
|
|
178
|
+
scrollY: window.scrollY,
|
|
179
|
+
scrollTop: scrollContainer?.scrollTop ?? 0,
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
if (screenshotRoot) {
|
|
183
|
+
const dataUrl = await mod.domToJpeg(captureTarget, {
|
|
184
|
+
backgroundColor: "#ffffff",
|
|
185
|
+
timeout: 5000,
|
|
186
|
+
width: viewportWidth,
|
|
187
|
+
height: viewportHeight,
|
|
188
|
+
quality,
|
|
189
|
+
scale,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return { dataUrl, captureViewport };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const outW = Math.round(contentWidth * scale);
|
|
153
196
|
const outH = Math.round(viewportHeight * scale);
|
|
154
197
|
|
|
155
198
|
const domCanvas = await mod.domToCanvas(captureTarget, {
|
|
156
199
|
backgroundColor: "#ffffff",
|
|
157
200
|
timeout: 5000,
|
|
158
|
-
width:
|
|
201
|
+
width: window.innerWidth,
|
|
159
202
|
height: viewportHeight,
|
|
160
203
|
});
|
|
161
204
|
|
|
@@ -165,18 +208,12 @@ export async function captureViewportScreenshot(
|
|
|
165
208
|
const ctx = canvas.getContext("2d");
|
|
166
209
|
if (!ctx) return null;
|
|
167
210
|
|
|
168
|
-
ctx
|
|
211
|
+
drawCanvasPreservingAspect(ctx, domCanvas, outW, outH);
|
|
169
212
|
|
|
170
213
|
const cropped = cropSidebarFromCanvas(canvas, sidebarInset, scale);
|
|
171
|
-
const scrollContainer = getPrototypeScrollContainer();
|
|
172
214
|
return {
|
|
173
215
|
dataUrl: cropped.toDataURL("image/jpeg", quality),
|
|
174
|
-
captureViewport
|
|
175
|
-
width: contentWidth,
|
|
176
|
-
height: viewportHeight,
|
|
177
|
-
scrollY: window.scrollY,
|
|
178
|
-
scrollTop: scrollContainer?.scrollTop ?? 0,
|
|
179
|
-
},
|
|
216
|
+
captureViewport,
|
|
180
217
|
};
|
|
181
218
|
} catch (err) {
|
|
182
219
|
console.warn("[prototype-comments] Viewport capture failed:", err);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { COMMENT_STORAGE_NOT_CONFIGURED_MESSAGE } from "@prototype/lib/prototypes/comment-storage-setup-prompt";
|
|
1
2
|
import { isValidAnnotation } from "./validation";
|
|
2
3
|
|
|
3
4
|
export interface CommentStorageAdapter<T extends { id: string }> {
|
|
@@ -30,9 +31,7 @@ export function createRemoteStorageAdapter<T extends { id: string }>(
|
|
|
30
31
|
);
|
|
31
32
|
|
|
32
33
|
if (response.status === 503) {
|
|
33
|
-
throw new CommentStorageError(
|
|
34
|
-
"Comment storage is not configured. Set Upstash Redis env vars.",
|
|
35
|
-
);
|
|
34
|
+
throw new CommentStorageError(COMMENT_STORAGE_NOT_CONFIGURED_MESSAGE);
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
if (!response.ok) {
|
|
@@ -48,9 +47,7 @@ export function createRemoteStorageAdapter<T extends { id: string }>(
|
|
|
48
47
|
const response = await fetch(listUrl);
|
|
49
48
|
|
|
50
49
|
if (response.status === 503) {
|
|
51
|
-
throw new CommentStorageError(
|
|
52
|
-
"Comment storage is not configured. Set Upstash Redis env vars.",
|
|
53
|
-
);
|
|
50
|
+
throw new CommentStorageError(COMMENT_STORAGE_NOT_CONFIGURED_MESSAGE);
|
|
54
51
|
}
|
|
55
52
|
|
|
56
53
|
if (!response.ok) {
|
|
@@ -66,6 +66,8 @@ export type CommentStore<TState> = {
|
|
|
66
66
|
slug: string;
|
|
67
67
|
annotations: CommentAnnotation<TState>[];
|
|
68
68
|
storageReady: boolean;
|
|
69
|
+
/** False when server env vars are missing; null while checking. */
|
|
70
|
+
storageConfigured: boolean | null;
|
|
69
71
|
storageError: string | null;
|
|
70
72
|
addAnnotation: (
|
|
71
73
|
fields: Omit<CommentAnnotation<TState>, "interactionState">,
|
|
@@ -17,7 +17,8 @@ export function useLayoutShift(): number {
|
|
|
17
17
|
for (const mutation of mutations) {
|
|
18
18
|
if (
|
|
19
19
|
mutation.type === "attributes" &&
|
|
20
|
-
mutation.attributeName === "data-comments-sidebar-open"
|
|
20
|
+
(mutation.attributeName === "data-comments-sidebar-open" ||
|
|
21
|
+
mutation.attributeName === "data-prototype-review-sidebar-open")
|
|
21
22
|
) {
|
|
22
23
|
bump();
|
|
23
24
|
break;
|
|
@@ -26,7 +27,13 @@ export function useLayoutShift(): number {
|
|
|
26
27
|
});
|
|
27
28
|
mutationObserver.observe(document.documentElement, {
|
|
28
29
|
attributes: true,
|
|
29
|
-
attributeFilter: [
|
|
30
|
+
attributeFilter: [
|
|
31
|
+
"data-comments-sidebar-open",
|
|
32
|
+
"data-prototype-review-sidebar-open",
|
|
33
|
+
"data-comments-sidebar-resizing",
|
|
34
|
+
"data-prototype-review-sidebar-resizing",
|
|
35
|
+
"style",
|
|
36
|
+
],
|
|
30
37
|
});
|
|
31
38
|
|
|
32
39
|
const handleTransitionEnd = (event: TransitionEvent) => {
|
|
@@ -53,6 +53,7 @@ import { getRootAnnotations, resolveThreadRootId } from "../core/comment-threads
|
|
|
53
53
|
import {
|
|
54
54
|
buildCommentCaptureCursorStyles,
|
|
55
55
|
isCommentCaptureBlockedTarget,
|
|
56
|
+
isCommentModeChromeInteraction,
|
|
56
57
|
} from "../core/comment-capture-blocked";
|
|
57
58
|
import { closestCrossingShadow } from "../core/element-identification";
|
|
58
59
|
import { injectCaptureColorTokens } from "../core/capture-theme";
|
|
@@ -209,6 +210,13 @@ export function CommentCaptureToolbar<TState = unknown>({
|
|
|
209
210
|
const [scrollY, setScrollY] = useState(0);
|
|
210
211
|
|
|
211
212
|
const isFeedbackActive = isGiveAMomentMode;
|
|
213
|
+
const isChangelogModeActive =
|
|
214
|
+
isGiveAMomentMode &&
|
|
215
|
+
integratedReview &&
|
|
216
|
+
review?.captureChannel === "changelog";
|
|
217
|
+
const isCommentModeActive =
|
|
218
|
+
isGiveAMomentMode &&
|
|
219
|
+
(!integratedReview || review?.captureChannel === "comment");
|
|
212
220
|
const annotationTargetOptions = useMemo(() => {
|
|
213
221
|
if (typeof resolveTargetOptions === "function") {
|
|
214
222
|
return resolveTargetOptions();
|
|
@@ -354,6 +362,18 @@ export function CommentCaptureToolbar<TState = unknown>({
|
|
|
354
362
|
setIsGiveAMomentMode((active) => !active);
|
|
355
363
|
}, [integratedReview, review]);
|
|
356
364
|
|
|
365
|
+
const exitCommentMode = useCallback(() => {
|
|
366
|
+
setPendingMultiSelectElements([]);
|
|
367
|
+
setPendingAnnotation(null);
|
|
368
|
+
setPendingExiting(false);
|
|
369
|
+
setHoverInfo(null);
|
|
370
|
+
setIsGiveAMomentMode(false);
|
|
371
|
+
modifiersHeldRef.current = { cmd: false, shift: false };
|
|
372
|
+
if (document.activeElement instanceof HTMLElement) {
|
|
373
|
+
document.activeElement.blur();
|
|
374
|
+
}
|
|
375
|
+
}, []);
|
|
376
|
+
|
|
357
377
|
const cancelAnnotation = useCallback(() => {
|
|
358
378
|
setPendingExiting(true);
|
|
359
379
|
originalSetTimeout(() => {
|
|
@@ -983,6 +1003,28 @@ export function CommentCaptureToolbar<TState = unknown>({
|
|
|
983
1003
|
createMultiSelectPendingAnnotation,
|
|
984
1004
|
]);
|
|
985
1005
|
|
|
1006
|
+
useEffect(() => {
|
|
1007
|
+
if (!isCommentModeActive) return;
|
|
1008
|
+
|
|
1009
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
1010
|
+
if (event.key !== "Escape") return;
|
|
1011
|
+
exitCommentMode();
|
|
1012
|
+
};
|
|
1013
|
+
|
|
1014
|
+
const handlePointerDown = (event: PointerEvent) => {
|
|
1015
|
+
const target = (event.composedPath()[0] || event.target) as HTMLElement;
|
|
1016
|
+
if (!isCommentModeChromeInteraction(target)) return;
|
|
1017
|
+
exitCommentMode();
|
|
1018
|
+
};
|
|
1019
|
+
|
|
1020
|
+
document.addEventListener("keydown", handleKeyDown, true);
|
|
1021
|
+
document.addEventListener("pointerdown", handlePointerDown, true);
|
|
1022
|
+
return () => {
|
|
1023
|
+
document.removeEventListener("keydown", handleKeyDown, true);
|
|
1024
|
+
document.removeEventListener("pointerdown", handlePointerDown, true);
|
|
1025
|
+
};
|
|
1026
|
+
}, [exitCommentMode, isCommentModeActive]);
|
|
1027
|
+
|
|
986
1028
|
useEffect(() => {
|
|
987
1029
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
988
1030
|
const isTyping =
|
|
@@ -1000,7 +1042,7 @@ export function CommentCaptureToolbar<TState = unknown>({
|
|
|
1000
1042
|
return;
|
|
1001
1043
|
}
|
|
1002
1044
|
if (pendingAnnotation) return;
|
|
1003
|
-
if (
|
|
1045
|
+
if (isChangelogModeActive) {
|
|
1004
1046
|
setIsGiveAMomentMode(false);
|
|
1005
1047
|
e.preventDefault();
|
|
1006
1048
|
if (document.activeElement instanceof HTMLElement) {
|
|
@@ -1028,7 +1070,7 @@ export function CommentCaptureToolbar<TState = unknown>({
|
|
|
1028
1070
|
document.addEventListener("keydown", handleKeyDown);
|
|
1029
1071
|
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
1030
1072
|
}, [
|
|
1031
|
-
|
|
1073
|
+
isChangelogModeActive,
|
|
1032
1074
|
pendingAnnotation,
|
|
1033
1075
|
pendingMultiSelectElements.length,
|
|
1034
1076
|
activateToolbar,
|
|
@@ -1052,14 +1094,6 @@ export function CommentCaptureToolbar<TState = unknown>({
|
|
|
1052
1094
|
}
|
|
1053
1095
|
}, [isActive, integratedReview, prototypeSlug]);
|
|
1054
1096
|
|
|
1055
|
-
const isChangelogModeActive =
|
|
1056
|
-
isGiveAMomentMode &&
|
|
1057
|
-
integratedReview &&
|
|
1058
|
-
review.captureChannel === "changelog";
|
|
1059
|
-
const isCommentModeActive =
|
|
1060
|
-
isGiveAMomentMode &&
|
|
1061
|
-
(!integratedReview || review.captureChannel === "comment");
|
|
1062
|
-
|
|
1063
1097
|
const bridgeValue = useMemo<CommentCaptureBridgeValue>(
|
|
1064
1098
|
() => ({
|
|
1065
1099
|
onSelect: handleCommentSelect,
|
|
@@ -1175,6 +1209,7 @@ export function CommentCaptureToolbar<TState = unknown>({
|
|
|
1175
1209
|
e.stopPropagation();
|
|
1176
1210
|
setIsGiveAMomentMode((active) => !active);
|
|
1177
1211
|
}}
|
|
1212
|
+
data-prototype-comment-mode-toggle
|
|
1178
1213
|
data-active={isGiveAMomentMode}
|
|
1179
1214
|
aria-label={
|
|
1180
1215
|
isGiveAMomentMode
|
|
@@ -15,6 +15,8 @@ import { captureInteractionState } from "../core/capture";
|
|
|
15
15
|
import { getRepliesForParent } from "../core/comment-threads";
|
|
16
16
|
import { commentToLiveState } from "../core/restore";
|
|
17
17
|
import { normalizeAnnotationViewport } from "../core/normalize-viewport";
|
|
18
|
+
import { COMMENT_STORAGE_NOT_CONFIGURED_MESSAGE } from "@prototype/lib/prototypes/comment-storage-setup-prompt";
|
|
19
|
+
import { fetchPrototypeStorageStatus, isPrototypeStorageFullyConfigured } from "@prototype/lib/prototypes/prototype-storage-status";
|
|
18
20
|
import { createRemoteStorageAdapter } from "../core/storage";
|
|
19
21
|
import type { CommentAnnotation, CommentStore } from "../core/types";
|
|
20
22
|
|
|
@@ -36,6 +38,7 @@ export function CommentProvider<TState>({
|
|
|
36
38
|
}: CommentProviderProps<TState>) {
|
|
37
39
|
const [annotations, setAnnotations] = useState<CommentAnnotation<TState>[]>([]);
|
|
38
40
|
const [storageReady, setStorageReady] = useState(false);
|
|
41
|
+
const [storageConfigured, setStorageConfigured] = useState<boolean | null>(null);
|
|
39
42
|
const [storageError, setStorageError] = useState<string | null>(null);
|
|
40
43
|
|
|
41
44
|
const getLiveStateRef = useRef(getLiveState);
|
|
@@ -70,8 +73,21 @@ export function CommentProvider<TState>({
|
|
|
70
73
|
async function loadAnnotations() {
|
|
71
74
|
setStorageReady(false);
|
|
72
75
|
setStorageError(null);
|
|
76
|
+
setStorageConfigured(null);
|
|
73
77
|
|
|
74
78
|
try {
|
|
79
|
+
const status = await fetchPrototypeStorageStatus();
|
|
80
|
+
if (cancelled) return;
|
|
81
|
+
|
|
82
|
+
const fullyConfigured = isPrototypeStorageFullyConfigured(status);
|
|
83
|
+
setStorageConfigured(fullyConfigured);
|
|
84
|
+
|
|
85
|
+
if (!fullyConfigured) {
|
|
86
|
+
setStorageError(COMMENT_STORAGE_NOT_CONFIGURED_MESSAGE);
|
|
87
|
+
setAnnotations([]);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
75
91
|
const stored = await storage.load();
|
|
76
92
|
if (cancelled) return;
|
|
77
93
|
setAnnotations(stored);
|
|
@@ -83,6 +99,7 @@ export function CommentProvider<TState>({
|
|
|
83
99
|
: "Failed to load comments.";
|
|
84
100
|
setStorageError(message);
|
|
85
101
|
setAnnotations([]);
|
|
102
|
+
setStorageConfigured((current) => current ?? true);
|
|
86
103
|
} finally {
|
|
87
104
|
if (!cancelled) setStorageReady(true);
|
|
88
105
|
}
|
|
@@ -254,6 +271,7 @@ export function CommentProvider<TState>({
|
|
|
254
271
|
slug,
|
|
255
272
|
annotations,
|
|
256
273
|
storageReady,
|
|
274
|
+
storageConfigured,
|
|
257
275
|
storageError,
|
|
258
276
|
addAnnotation,
|
|
259
277
|
addReply,
|
|
@@ -267,6 +285,7 @@ export function CommentProvider<TState>({
|
|
|
267
285
|
slug,
|
|
268
286
|
annotations,
|
|
269
287
|
storageReady,
|
|
288
|
+
storageConfigured,
|
|
270
289
|
storageError,
|
|
271
290
|
addAnnotation,
|
|
272
291
|
addReply,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { IconButton } from "@prototype/components/platform-ui/icon-button";
|
|
4
|
+
import { cn } from "@prototype/lib/utils";
|
|
5
|
+
import { MessageSquarePlus } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
type CommentModeToggleButtonProps = {
|
|
8
|
+
isActive: boolean;
|
|
9
|
+
onClick: () => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function CommentModeToggleButton({
|
|
13
|
+
isActive,
|
|
14
|
+
onClick,
|
|
15
|
+
}: CommentModeToggleButtonProps) {
|
|
16
|
+
return (
|
|
17
|
+
<IconButton
|
|
18
|
+
type="button"
|
|
19
|
+
variant="chrome"
|
|
20
|
+
size="icon"
|
|
21
|
+
onClick={onClick}
|
|
22
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
23
|
+
data-prototype-comment-mode-toggle
|
|
24
|
+
aria-label={isActive ? "Exit comment mode" : "Add comment"}
|
|
25
|
+
aria-pressed={isActive}
|
|
26
|
+
className={cn(
|
|
27
|
+
"size-8 shrink-0",
|
|
28
|
+
isActive && "bg-accent text-accent-foreground hover:bg-accent/90 hover:text-accent-foreground",
|
|
29
|
+
)}
|
|
30
|
+
>
|
|
31
|
+
<MessageSquarePlus size={16} strokeWidth={2} />
|
|
32
|
+
</IconButton>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -584,28 +584,20 @@ function CommentCard<TState>({
|
|
|
584
584
|
<div className={cardStyles.screenshotInner}>
|
|
585
585
|
<div
|
|
586
586
|
className={cardStyles.screenshot}
|
|
587
|
-
style={
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
}
|
|
587
|
+
style={{
|
|
588
|
+
aspectRatio: aspectRatio ?? "16 / 9",
|
|
589
|
+
...(isSidebarLayout ? { maxHeight: 220 } : {}),
|
|
590
|
+
}}
|
|
592
591
|
>
|
|
593
592
|
{annotation.screenshot ? (
|
|
594
593
|
<>
|
|
595
594
|
<img
|
|
596
595
|
src={annotation.screenshot}
|
|
597
596
|
alt={annotation.comment}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
isSidebarLayout
|
|
603
|
-
? { objectPosition: screenshotObjectPosition }
|
|
604
|
-
: {
|
|
605
|
-
...s.screenshotImg,
|
|
606
|
-
objectPosition: screenshotObjectPosition,
|
|
607
|
-
}
|
|
608
|
-
}
|
|
597
|
+
style={{
|
|
598
|
+
...s.screenshotImg,
|
|
599
|
+
objectPosition: screenshotObjectPosition,
|
|
600
|
+
}}
|
|
609
601
|
/>
|
|
610
602
|
{viewportBox && viewport && (
|
|
611
603
|
<div
|
|
@@ -3,14 +3,9 @@
|
|
|
3
3
|
import { countUnresolvedAnnotations } from "../core/annotation-status";
|
|
4
4
|
import { useCommentStore } from "../react/CommentProvider";
|
|
5
5
|
import { CommentHeaderCount } from "./CommentHeaderCount";
|
|
6
|
+
import { CommentModeToggleButton } from "./CommentModeToggleButton";
|
|
6
7
|
import { CommentsGrid } from "./CommentsGrid";
|
|
7
|
-
import {
|
|
8
|
-
SIDEBAR_ICON_BUTTON_CLASS,
|
|
9
|
-
Sidebar,
|
|
10
|
-
} from "@prototype/components/platform-ui/sidebar";
|
|
11
|
-
import { IconButton } from "@prototype/components/platform-ui/icon-button";
|
|
12
|
-
import { cn } from "@prototype/lib/utils";
|
|
13
|
-
import { MessageSquarePlus } from "lucide-react";
|
|
8
|
+
import { Sidebar } from "@prototype/components/platform-ui/sidebar";
|
|
14
9
|
|
|
15
10
|
type CommentsSidebarProps = {
|
|
16
11
|
open: boolean;
|
|
@@ -47,19 +42,10 @@ export function CommentsSidebar({
|
|
|
47
42
|
widthCssVar="--comments-sidebar-width"
|
|
48
43
|
headerActions={
|
|
49
44
|
onToggleCommentMode ? (
|
|
50
|
-
<
|
|
51
|
-
|
|
45
|
+
<CommentModeToggleButton
|
|
46
|
+
isActive={isCommentModeActive}
|
|
52
47
|
onClick={onToggleCommentMode}
|
|
53
|
-
|
|
54
|
-
aria-label={isCommentModeActive ? "Exit comment mode" : "Add comment"}
|
|
55
|
-
aria-pressed={isCommentModeActive}
|
|
56
|
-
className={cn(
|
|
57
|
-
SIDEBAR_ICON_BUTTON_CLASS,
|
|
58
|
-
isCommentModeActive && "bg-accent text-accent-foreground",
|
|
59
|
-
)}
|
|
60
|
-
>
|
|
61
|
-
<MessageSquarePlus size={16} strokeWidth={2} />
|
|
62
|
-
</IconButton>
|
|
48
|
+
/>
|
|
63
49
|
) : null
|
|
64
50
|
}
|
|
65
51
|
>
|
|
@@ -870,9 +870,13 @@
|
|
|
870
870
|
}
|
|
871
871
|
|
|
872
872
|
:global(html[data-comments-sidebar-open]) .markersLayer,
|
|
873
|
+
:global(html[data-prototype-review-sidebar-open]) .markersLayer,
|
|
873
874
|
:global(html[data-comments-sidebar-open]) .fixedMarkersLayer,
|
|
875
|
+
:global(html[data-prototype-review-sidebar-open]) .fixedMarkersLayer,
|
|
874
876
|
:global(html[data-comments-sidebar-open]) .overlay,
|
|
875
|
-
:global(html[data-
|
|
877
|
+
:global(html[data-prototype-review-sidebar-open]) .overlay,
|
|
878
|
+
:global(html[data-comments-sidebar-open]) .targetHighlightLayer,
|
|
879
|
+
:global(html[data-prototype-review-sidebar-open]) .targetHighlightLayer {
|
|
876
880
|
right: var(--comments-sidebar-width, 400px);
|
|
877
881
|
}
|
|
878
882
|
|
|
@@ -42,13 +42,6 @@
|
|
|
42
42
|
transition: border-color 0.2s cubic-bezier(0.215, 0.61, 0.355, 1);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
.screenshotImgSidebar {
|
|
46
|
-
display: block;
|
|
47
|
-
width: 100%;
|
|
48
|
-
height: auto;
|
|
49
|
-
max-height: 220px;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
45
|
.cardCondensed .screenshot {
|
|
53
46
|
border-bottom-color: transparent;
|
|
54
47
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
DesignExplorationBaselineOption,
|
|
5
|
+
DesignExplorationVariantOption,
|
|
6
|
+
} from "./design-exploration-types";
|
|
7
|
+
import { getDesignExplorationDisplayOptions } from "./design-exploration-types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Builds a renderer entry for every design exploration option. Prefer this over
|
|
11
|
+
* hand-rolled `Object.fromEntries` maps so each variant gets a preview and new
|
|
12
|
+
* options cannot silently return `null`.
|
|
13
|
+
*/
|
|
14
|
+
export function buildDesignExplorationRenderers<TVariant extends string>(
|
|
15
|
+
options: readonly DesignExplorationVariantOption<TVariant>[],
|
|
16
|
+
renderVariant: (variant: TVariant) => ReactNode,
|
|
17
|
+
baseline?: DesignExplorationBaselineOption<TVariant>,
|
|
18
|
+
): Record<TVariant, () => ReactNode> {
|
|
19
|
+
const displayOptions = baseline
|
|
20
|
+
? getDesignExplorationDisplayOptions([...options], baseline)
|
|
21
|
+
: options;
|
|
22
|
+
const renderers = {} as Record<TVariant, () => ReactNode>;
|
|
23
|
+
|
|
24
|
+
for (const option of displayOptions) {
|
|
25
|
+
const variant = option.value as TVariant;
|
|
26
|
+
renderers[variant] = () => {
|
|
27
|
+
const node = renderVariant(variant);
|
|
28
|
+
|
|
29
|
+
if (process.env.NODE_ENV !== "production" && isEmptyRendererOutput(node)) {
|
|
30
|
+
console.warn(
|
|
31
|
+
`[prototype] Design exploration renderer for "${variant}" returned no preview content.`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return node;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return renderers;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isEmptyRendererOutput(node: ReactNode): boolean {
|
|
43
|
+
return node == null || node === false;
|
|
44
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export function buildChangelogDescriptionPrompt({
|
|
2
|
+
slug,
|
|
3
|
+
description,
|
|
4
|
+
origin = "http://localhost:3003",
|
|
5
|
+
}: {
|
|
6
|
+
slug: string;
|
|
7
|
+
description: string;
|
|
8
|
+
origin?: string;
|
|
9
|
+
}): string {
|
|
10
|
+
const prototypeUrl = `${origin}/prototypes/${slug}`;
|
|
11
|
+
const trimmed = description.trim();
|
|
12
|
+
|
|
13
|
+
const lines = [
|
|
14
|
+
`Update the "${slug}" prototype to match the following change log overview.`,
|
|
15
|
+
"",
|
|
16
|
+
"## Description",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
if (trimmed) {
|
|
20
|
+
lines.push(trimmed);
|
|
21
|
+
} else {
|
|
22
|
+
lines.push(
|
|
23
|
+
"Summarize what changed in this prototype and why:",
|
|
24
|
+
"- What changed:",
|
|
25
|
+
"- Why:",
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
lines.push(
|
|
30
|
+
"",
|
|
31
|
+
"## Prototype",
|
|
32
|
+
`- Slug: \`${slug}\``,
|
|
33
|
+
`- Preview: ${prototypeUrl}`,
|
|
34
|
+
`- Work in: \`src/prototypes/${slug}/\``,
|
|
35
|
+
"",
|
|
36
|
+
"## Instructions",
|
|
37
|
+
"1. Review the overview and anchored changelog entries in the prototype review sidebar.",
|
|
38
|
+
"2. Apply any missing updates so the prototype matches the described changes.",
|
|
39
|
+
"3. Do not persist this overview in KV — it is prompt-only.",
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
return lines.join("\n");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function buildChangelogDescriptionCopyText({
|
|
46
|
+
slug,
|
|
47
|
+
description,
|
|
48
|
+
origin,
|
|
49
|
+
}: {
|
|
50
|
+
slug: string;
|
|
51
|
+
description: string;
|
|
52
|
+
origin?: string;
|
|
53
|
+
}): string {
|
|
54
|
+
const prompt = buildChangelogDescriptionPrompt({ slug, description, origin });
|
|
55
|
+
const trimmed = description.trim();
|
|
56
|
+
if (!trimmed) return prompt;
|
|
57
|
+
return `${trimmed}\n\n---\n\n${prompt}`;
|
|
58
|
+
}
|