proto-plugin 0.1.1 → 0.1.2
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 +1 -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 +4 -0
- 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-sidebar.tsx +11 -28
- 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/ui/button.tsx +3 -1
- package/src/components/ui/dialog.tsx +43 -4
- 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 +20 -6
- package/src/config/with-prototype.ts +21 -0
- package/src/index.ts +54 -10
- 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/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/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/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 +7 -16
- package/src/server/create-prototype-api-routes.ts +0 -115
- package/src/server/prototype-storage-status-route.ts +29 -0
- package/src/server.ts +0 -8
- package/src/styles/globals.css +359 -71
- 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
|
@@ -116,21 +116,22 @@ export function ControlsPanelSelect<T extends string>({
|
|
|
116
116
|
const isMiniPill = appearance === "miniPill";
|
|
117
117
|
const isMenuList = appearance === "menuList";
|
|
118
118
|
const review = usePrototypeReviewOptional();
|
|
119
|
+
const registerPreviewStates = review?.registerPreviewStates;
|
|
119
120
|
const { commentTheme } = usePrototypeToolTheme();
|
|
120
121
|
const triggerLabel = isMiniPill ? (miniPillLabel ?? activeLabel) : activeLabel;
|
|
121
122
|
const [open, setOpen] = useState(false);
|
|
122
123
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
123
124
|
|
|
124
125
|
useEffect(() => {
|
|
125
|
-
if (!isMenuList || !
|
|
126
|
+
if (!isMenuList || !registerPreviewStates) return;
|
|
126
127
|
|
|
127
|
-
|
|
128
|
+
registerPreviewStates(
|
|
128
129
|
getSelectableOptions(options).map((option) => ({
|
|
129
130
|
id: option.value,
|
|
130
131
|
label: option.label,
|
|
131
132
|
})),
|
|
132
133
|
);
|
|
133
|
-
}, [isMenuList, options,
|
|
134
|
+
}, [isMenuList, options, registerPreviewStates]);
|
|
134
135
|
|
|
135
136
|
const selectSubmenuEntry = useCallback(
|
|
136
137
|
(
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Dialog,
|
|
5
|
+
DialogClose,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogTitle,
|
|
8
|
+
} from "@prototype/components/ui/dialog";
|
|
9
|
+
import { cn } from "@prototype/lib/utils";
|
|
10
|
+
import { X } from "lucide-react";
|
|
11
|
+
import type { ReactNode } from "react";
|
|
12
|
+
|
|
13
|
+
export const PROTOTYPE_TOOL_MODAL_CLOSE_CLASS =
|
|
14
|
+
"text-muted-foreground hover:text-foreground inline-flex size-8 shrink-0 items-center justify-center rounded-sm border-0 bg-transparent p-0 opacity-70 transition-opacity duration-200 ease hover:opacity-100 outline-none focus:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-0 disabled:pointer-events-none disabled:opacity-40 disabled:hover:opacity-40 disabled:hover:text-muted-foreground motion-reduce:transition-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4";
|
|
15
|
+
|
|
16
|
+
export const PROTOTYPE_TOOL_MODAL_TEXTAREA_CLASS =
|
|
17
|
+
"field-sizing-content min-h-[min(50vh,20rem)] w-full resize-y border-0 bg-transparent px-0 py-0 text-sm leading-relaxed text-foreground shadow-none outline-none placeholder:text-muted-foreground focus-visible:ring-0";
|
|
18
|
+
|
|
19
|
+
const MODAL_SIZE_CLASS = {
|
|
20
|
+
md: "w-[min(42rem,calc(100vw-2rem))] max-w-[min(42rem,calc(100vw-2rem))] sm:max-w-[min(42rem,calc(100vw-2rem))]",
|
|
21
|
+
wide: "w-[min(96rem,calc(100vw-2rem))] max-w-[min(96rem,calc(100vw-2rem))] sm:max-w-[min(96rem,calc(100vw-2rem))]",
|
|
22
|
+
} as const;
|
|
23
|
+
|
|
24
|
+
function PrototypeToolDialogSeparator() {
|
|
25
|
+
return (
|
|
26
|
+
<div
|
|
27
|
+
className="tool-dialog-separator h-px w-full shrink-0"
|
|
28
|
+
role="separator"
|
|
29
|
+
aria-hidden
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type PrototypeToolDialogProps = {
|
|
35
|
+
open: boolean;
|
|
36
|
+
onOpenChange: (open: boolean) => void;
|
|
37
|
+
title: ReactNode;
|
|
38
|
+
description?: ReactNode;
|
|
39
|
+
children: ReactNode;
|
|
40
|
+
footer?: ReactNode;
|
|
41
|
+
footerClassName?: string;
|
|
42
|
+
headerActions?: ReactNode;
|
|
43
|
+
size?: keyof typeof MODAL_SIZE_CLASS;
|
|
44
|
+
bodyClassName?: string;
|
|
45
|
+
className?: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export function PrototypeToolDialog({
|
|
49
|
+
open,
|
|
50
|
+
onOpenChange,
|
|
51
|
+
title,
|
|
52
|
+
description,
|
|
53
|
+
children,
|
|
54
|
+
footer,
|
|
55
|
+
footerClassName,
|
|
56
|
+
headerActions,
|
|
57
|
+
size = "md",
|
|
58
|
+
bodyClassName,
|
|
59
|
+
className,
|
|
60
|
+
}: PrototypeToolDialogProps) {
|
|
61
|
+
return (
|
|
62
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
63
|
+
<DialogContent
|
|
64
|
+
portalScope="tool"
|
|
65
|
+
showCloseButton={false}
|
|
66
|
+
className={cn(
|
|
67
|
+
"tool-dialog-surface flex max-h-[min(92svh,calc(100%-2rem))] flex-col gap-0 overflow-hidden border-0 p-0",
|
|
68
|
+
MODAL_SIZE_CLASS[size],
|
|
69
|
+
className,
|
|
70
|
+
)}
|
|
71
|
+
>
|
|
72
|
+
<div className="flex items-start justify-between gap-3 px-5 py-4">
|
|
73
|
+
<div className="flex min-w-0 flex-col gap-1">
|
|
74
|
+
<DialogTitle className="text-base font-medium text-foreground">
|
|
75
|
+
{title}
|
|
76
|
+
</DialogTitle>
|
|
77
|
+
{description ? (
|
|
78
|
+
<p className="text-sm text-muted-foreground">{description}</p>
|
|
79
|
+
) : null}
|
|
80
|
+
</div>
|
|
81
|
+
<div className="flex shrink-0 items-center gap-0.5">
|
|
82
|
+
{headerActions}
|
|
83
|
+
<DialogClose className={PROTOTYPE_TOOL_MODAL_CLOSE_CLASS}>
|
|
84
|
+
<X />
|
|
85
|
+
<span className="sr-only">Close</span>
|
|
86
|
+
</DialogClose>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<PrototypeToolDialogSeparator />
|
|
91
|
+
|
|
92
|
+
<div
|
|
93
|
+
className={cn(
|
|
94
|
+
"flex min-h-0 flex-1 flex-col overflow-y-auto px-5 py-4",
|
|
95
|
+
bodyClassName,
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
{children}
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{footer ? (
|
|
102
|
+
<>
|
|
103
|
+
<PrototypeToolDialogSeparator />
|
|
104
|
+
<div
|
|
105
|
+
className={cn(
|
|
106
|
+
"flex items-center gap-2 px-5 py-3",
|
|
107
|
+
footerClassName,
|
|
108
|
+
)}
|
|
109
|
+
>
|
|
110
|
+
{footer}
|
|
111
|
+
</div>
|
|
112
|
+
</>
|
|
113
|
+
) : null}
|
|
114
|
+
</DialogContent>
|
|
115
|
+
</Dialog>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
@@ -20,11 +20,22 @@ export function clampSidebarWidth(width: number) {
|
|
|
20
20
|
return Math.min(MAX_SIDEBAR_WIDTH, Math.max(MIN_SIDEBAR_WIDTH, width));
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/** e.g. `data-comments-sidebar` + `"open"` → `data-comments-sidebar-open` */
|
|
24
|
+
export function getSidebarStateAttribute(
|
|
25
|
+
dataAttribute: string,
|
|
26
|
+
suffix: "open" | "inline" | "resizing",
|
|
27
|
+
): string {
|
|
28
|
+
const base = dataAttribute.startsWith("data-")
|
|
29
|
+
? dataAttribute.slice(5)
|
|
30
|
+
: dataAttribute;
|
|
31
|
+
return `data-${base}-${suffix}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
23
34
|
const SIDEBAR_TITLE_CLASS =
|
|
24
|
-
"font-display truncate text-sm font-semibold text-[var(--tool-chrome-
|
|
35
|
+
"font-display truncate text-sm font-semibold text-[var(--tool-chrome-text-heading)]";
|
|
25
36
|
|
|
26
37
|
export const SIDEBAR_ICON_BUTTON_CLASS =
|
|
27
|
-
"text-[var(--tool-chrome-icon)] hover:bg-[var(--tool-chrome-gray-highlight)] hover:text-[var(--tool-chrome-icon-hover)] focus:outline-none focus-visible:ring-0 focus-visible:ring-offset-0";
|
|
38
|
+
"cursor-pointer text-[var(--tool-chrome-icon)] hover:bg-[var(--tool-chrome-gray-highlight)] hover:text-[var(--tool-chrome-icon-hover)] focus:outline-none focus-visible:ring-0 focus-visible:ring-offset-0";
|
|
28
39
|
|
|
29
40
|
type SidebarProps = {
|
|
30
41
|
open: boolean;
|
|
@@ -59,6 +70,9 @@ export function Sidebar({
|
|
|
59
70
|
className,
|
|
60
71
|
overlay = false,
|
|
61
72
|
}: SidebarProps) {
|
|
73
|
+
const openAttr = getSidebarStateAttribute(dataAttribute, "open");
|
|
74
|
+
const inlineAttr = getSidebarStateAttribute(dataAttribute, "inline");
|
|
75
|
+
const resizingAttr = getSidebarStateAttribute(dataAttribute, "resizing");
|
|
62
76
|
const [sidebarWidth, setSidebarWidth] = useState(DEFAULT_SIDEBAR_WIDTH);
|
|
63
77
|
const [isResizing, setIsResizing] = useState(false);
|
|
64
78
|
const resizeStartRef = useRef<{ startX: number; startWidth: number } | null>(
|
|
@@ -80,7 +94,9 @@ export function Sidebar({
|
|
|
80
94
|
);
|
|
81
95
|
|
|
82
96
|
useEffect(() => {
|
|
83
|
-
|
|
97
|
+
if (!overlay) {
|
|
98
|
+
document.documentElement.setAttribute(inlineAttr, "");
|
|
99
|
+
}
|
|
84
100
|
document.documentElement.style.setProperty(
|
|
85
101
|
widthCssVar,
|
|
86
102
|
`${DEFAULT_SIDEBAR_WIDTH}px`,
|
|
@@ -88,22 +104,19 @@ export function Sidebar({
|
|
|
88
104
|
|
|
89
105
|
return () => {
|
|
90
106
|
document.documentElement.style.removeProperty(widthCssVar);
|
|
91
|
-
document.documentElement.removeAttribute(
|
|
92
|
-
document.documentElement.removeAttribute(
|
|
93
|
-
document.documentElement.removeAttribute(
|
|
107
|
+
document.documentElement.removeAttribute(openAttr);
|
|
108
|
+
document.documentElement.removeAttribute(resizingAttr);
|
|
109
|
+
document.documentElement.removeAttribute(inlineAttr);
|
|
94
110
|
};
|
|
95
|
-
}, [widthCssVar]);
|
|
111
|
+
}, [inlineAttr, openAttr, overlay, resizingAttr, widthCssVar]);
|
|
96
112
|
|
|
97
113
|
useEffect(() => {
|
|
98
|
-
document.documentElement.toggleAttribute(
|
|
99
|
-
}, [open]);
|
|
114
|
+
document.documentElement.toggleAttribute(openAttr, open);
|
|
115
|
+
}, [open, openAttr]);
|
|
100
116
|
|
|
101
117
|
useEffect(() => {
|
|
102
|
-
document.documentElement.toggleAttribute(
|
|
103
|
-
|
|
104
|
-
isResizing,
|
|
105
|
-
);
|
|
106
|
-
}, [isResizing]);
|
|
118
|
+
document.documentElement.toggleAttribute(resizingAttr, isResizing);
|
|
119
|
+
}, [isResizing, resizingAttr]);
|
|
107
120
|
|
|
108
121
|
const handleResizeStart = useCallback(
|
|
109
122
|
(event: ReactPointerEvent<HTMLDivElement>) => {
|
|
@@ -152,7 +165,7 @@ export function Sidebar({
|
|
|
152
165
|
: { width: open ? sidebarWidth : 0 }
|
|
153
166
|
}
|
|
154
167
|
className={cn(
|
|
155
|
-
"flex h-full shrink-0 flex-col overflow-hidden bg-[var(--tool-chrome-ground)] text-[var(--tool-chrome-
|
|
168
|
+
"flex h-full shrink-0 flex-col overflow-hidden bg-[var(--tool-chrome-ground)] text-[var(--tool-chrome-text)]",
|
|
156
169
|
isResizing && "select-none transition-none",
|
|
157
170
|
overlay
|
|
158
171
|
? "absolute inset-y-0 right-0 shadow-2xl transition-transform duration-200 ease-out"
|
|
@@ -226,13 +239,18 @@ type SidebarResizeHandleProps = {
|
|
|
226
239
|
export function SidebarResizeHandle({
|
|
227
240
|
open,
|
|
228
241
|
sidebarWidth,
|
|
242
|
+
isResizing,
|
|
229
243
|
onResizeStart,
|
|
230
244
|
onResizeMove,
|
|
231
245
|
onResizeEnd,
|
|
232
246
|
}: SidebarResizeHandleProps) {
|
|
233
247
|
return (
|
|
234
248
|
<div
|
|
235
|
-
|
|
249
|
+
data-resizing={isResizing ? "" : undefined}
|
|
250
|
+
className={cn(
|
|
251
|
+
"absolute top-0 left-[-4px] z-[1] h-full w-2 cursor-col-resize touch-none before:absolute before:top-0 before:left-[3px] before:h-full before:w-0.5 before:bg-transparent before:transition-colors hover:before:bg-border data-resizing:before:bg-border",
|
|
252
|
+
isResizing && "select-none",
|
|
253
|
+
)}
|
|
236
254
|
onPointerDown={onResizeStart}
|
|
237
255
|
onPointerMove={onResizeMove}
|
|
238
256
|
onPointerUp={onResizeEnd}
|
|
@@ -1,57 +1,97 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import Link from "next/link";
|
|
5
|
-
|
|
3
|
+
import { PrototypeCreatePrototypeModal } from "@prototype/components/prototypes/prototype-create-prototype-modal";
|
|
6
4
|
import type { PrototypeMetadata } from "@prototype/lib/prototypes/prototype-config-types";
|
|
7
5
|
import { screenshotSrc } from "@prototype/lib/prototypes/screenshot-src";
|
|
8
6
|
import { cn } from "@prototype/lib/utils";
|
|
7
|
+
import { Plus } from "lucide-react";
|
|
8
|
+
import Image from "next/image";
|
|
9
|
+
import Link from "next/link";
|
|
10
|
+
import { useState } from "react";
|
|
9
11
|
|
|
10
12
|
type PrototypeGalleryClientProps = {
|
|
11
13
|
prototypes: PrototypeMetadata[];
|
|
12
14
|
screenshotVersions: Record<string, number>;
|
|
13
15
|
};
|
|
14
16
|
|
|
17
|
+
const prototypeGalleryTitleClassName =
|
|
18
|
+
"mt-2 text-sm text-[var(--tool-chrome-text-muted)] transition-colors duration-300 ease-out group-hover:text-[var(--tool-chrome-text-heading)]";
|
|
19
|
+
|
|
20
|
+
function PrototypeGalleryAddCard({ onClick }: { onClick: () => void }) {
|
|
21
|
+
return (
|
|
22
|
+
<button
|
|
23
|
+
type="button"
|
|
24
|
+
onClick={onClick}
|
|
25
|
+
aria-label="New prototype"
|
|
26
|
+
className="group block w-full cursor-pointer text-left"
|
|
27
|
+
>
|
|
28
|
+
<div
|
|
29
|
+
className={cn(
|
|
30
|
+
"rounded-xl",
|
|
31
|
+
"shadow-[var(--tool-review-card-shadow)]",
|
|
32
|
+
"translate-y-0 transition-[translate,box-shadow] duration-300 ease-out will-change-[translate]",
|
|
33
|
+
"group-hover:-translate-y-1 group-hover:shadow-[var(--tool-review-card-shadow-hover)]",
|
|
34
|
+
)}
|
|
35
|
+
>
|
|
36
|
+
<div className="relative flex aspect-video items-center justify-center overflow-hidden rounded-xl border border-dashed border-[var(--tool-chrome-border)] bg-[var(--tool-chrome-surface)] text-[var(--tool-chrome-text-muted)] transition-colors duration-300 ease-out group-hover:text-[var(--tool-chrome-text-heading)]">
|
|
37
|
+
<Plus size={20} strokeWidth={2} aria-hidden />
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
<p className={cn(prototypeGalleryTitleClassName, "invisible")} aria-hidden>
|
|
41
|
+
|
|
42
|
+
</p>
|
|
43
|
+
</button>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
15
47
|
export function PrototypeGalleryClient({
|
|
16
48
|
prototypes,
|
|
17
49
|
screenshotVersions,
|
|
18
50
|
}: PrototypeGalleryClientProps) {
|
|
51
|
+
const [createModalOpen, setCreateModalOpen] = useState(false);
|
|
52
|
+
|
|
19
53
|
return (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
className={cn(
|
|
29
|
-
"rounded-xl",
|
|
30
|
-
"shadow-[0_1px_3px_0_rgb(0_0_0/0.06)]",
|
|
31
|
-
"translate-y-0 transition-[translate,box-shadow] duration-300 ease-out will-change-[translate]",
|
|
32
|
-
"group-hover:-translate-y-1 group-hover:shadow-[0_4px_12px_-4px_rgb(0_0_0/0.10)]",
|
|
33
|
-
)}
|
|
54
|
+
<>
|
|
55
|
+
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
56
|
+
<PrototypeGalleryAddCard onClick={() => setCreateModalOpen(true)} />
|
|
57
|
+
{prototypes.map((prototype) => (
|
|
58
|
+
<Link
|
|
59
|
+
key={prototype.slug}
|
|
60
|
+
href={`/prototypes/${prototype.slug}`}
|
|
61
|
+
className="group"
|
|
34
62
|
>
|
|
35
|
-
<div
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
63
|
+
<div
|
|
64
|
+
className={cn(
|
|
65
|
+
"rounded-xl",
|
|
66
|
+
"shadow-[var(--tool-review-card-shadow)]",
|
|
67
|
+
"translate-y-0 transition-[translate,box-shadow] duration-300 ease-out will-change-[translate]",
|
|
68
|
+
"group-hover:-translate-y-1 group-hover:shadow-[var(--tool-review-card-shadow-hover)]",
|
|
69
|
+
)}
|
|
70
|
+
>
|
|
71
|
+
<div className="relative aspect-video overflow-hidden rounded-xl border border-[var(--tool-chrome-border)] bg-[var(--tool-chrome-surface)]">
|
|
72
|
+
<Image
|
|
73
|
+
src={screenshotSrc(
|
|
74
|
+
prototype.screenshot,
|
|
75
|
+
screenshotVersions[prototype.slug],
|
|
76
|
+
)}
|
|
77
|
+
alt={prototype.title}
|
|
78
|
+
width={1600}
|
|
79
|
+
height={900}
|
|
80
|
+
quality={90}
|
|
81
|
+
sizes="(min-width: 1024px) 33vw, (min-width: 768px) 50vw, 100vw"
|
|
82
|
+
className="h-full w-full object-cover object-top"
|
|
83
|
+
/>
|
|
84
|
+
</div>
|
|
48
85
|
</div>
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
86
|
+
<p className={prototypeGalleryTitleClassName}>{prototype.title}</p>
|
|
87
|
+
</Link>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
|
|
91
|
+
<PrototypeCreatePrototypeModal
|
|
92
|
+
open={createModalOpen}
|
|
93
|
+
onOpenChange={setCreateModalOpen}
|
|
94
|
+
/>
|
|
95
|
+
</>
|
|
56
96
|
);
|
|
57
97
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@prototype/lib/utils";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
|
|
6
|
+
export type DesignExplorationVariantPreviewLayout = "inline" | "overlay";
|
|
7
|
+
|
|
8
|
+
export type DesignExplorationVariantPreviewShellProps = {
|
|
9
|
+
/** Use `overlay` when the variant uses absolute positioning on the live page. */
|
|
10
|
+
layout?: DesignExplorationVariantPreviewLayout;
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
className?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Hosts a variant inside the design-brief preview card. Overlay layouts get a
|
|
17
|
+
* positioned shell so absolutely placed controls remain visible in the sidebar.
|
|
18
|
+
*/
|
|
19
|
+
export function DesignExplorationVariantPreviewShell({
|
|
20
|
+
layout = "inline",
|
|
21
|
+
children,
|
|
22
|
+
className,
|
|
23
|
+
}: DesignExplorationVariantPreviewShellProps) {
|
|
24
|
+
const isOverlay = layout === "overlay";
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
className={cn(
|
|
29
|
+
"relative bg-white",
|
|
30
|
+
isOverlay ? "h-full w-full" : "w-full px-2 py-3",
|
|
31
|
+
className,
|
|
32
|
+
)}
|
|
33
|
+
>
|
|
34
|
+
{isOverlay ? (
|
|
35
|
+
<div
|
|
36
|
+
className="from-muted/25 to-muted/5 pointer-events-none absolute inset-0 bg-gradient-to-b"
|
|
37
|
+
aria-hidden
|
|
38
|
+
/>
|
|
39
|
+
) : null}
|
|
40
|
+
{children}
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { cn } from "@prototype/lib/utils";
|
|
4
|
+
import type { ComponentPropsWithoutRef } from "react";
|
|
5
|
+
|
|
6
|
+
/** Shared border/focus styling for prototype brief inputs (gallery modals + review sidebar). */
|
|
7
|
+
export const PROTOTYPE_BRIEF_FIELD_BORDER_CLASS =
|
|
8
|
+
"border border-border shadow-none outline-none transition-colors focus-visible:border-muted-foreground/40 focus-visible:ring-1 focus-visible:ring-muted-foreground/20 focus-visible:ring-offset-0";
|
|
9
|
+
|
|
10
|
+
const BRIEF_TEXTAREA_BASE_CLASS =
|
|
11
|
+
"field-sizing-content w-full resize-y rounded-md bg-transparent px-3 py-2 text-sm leading-relaxed text-foreground placeholder:text-muted-foreground";
|
|
12
|
+
|
|
13
|
+
export type PrototypeBriefTextareaProps = ComponentPropsWithoutRef<"textarea"> & {
|
|
14
|
+
minHeightClass?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function PrototypeBriefTextarea({
|
|
18
|
+
className,
|
|
19
|
+
minHeightClass = "min-h-[min(40vh,14rem)]",
|
|
20
|
+
...props
|
|
21
|
+
}: PrototypeBriefTextareaProps) {
|
|
22
|
+
return (
|
|
23
|
+
<textarea
|
|
24
|
+
className={cn(
|
|
25
|
+
BRIEF_TEXTAREA_BASE_CLASS,
|
|
26
|
+
minHeightClass,
|
|
27
|
+
PROTOTYPE_BRIEF_FIELD_BORDER_CLASS,
|
|
28
|
+
className,
|
|
29
|
+
)}
|
|
30
|
+
{...props}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
} from "react";
|
|
21
21
|
|
|
22
22
|
import { PrototypeReferenceDocs } from "./prototype-reference-docs";
|
|
23
|
+
import { PrototypeCommentStorageEmptyState } from "./prototype-comment-storage-empty-state";
|
|
23
24
|
|
|
24
25
|
const CHANGELOG_SECTION_TITLE_CLASS = "tool-section-label";
|
|
25
26
|
|
|
@@ -105,6 +106,7 @@ export function PrototypeChangeLogPanel({
|
|
|
105
106
|
updateAnnotation,
|
|
106
107
|
storageError,
|
|
107
108
|
storageReady,
|
|
109
|
+
storageConfigured,
|
|
108
110
|
} = useCommentStore();
|
|
109
111
|
const { overview, ready: metaReady, error: metaError, setOverview } =
|
|
110
112
|
useChangelogMeta();
|
|
@@ -182,6 +184,8 @@ export function PrototypeChangeLogPanel({
|
|
|
182
184
|
</div>
|
|
183
185
|
{!storageReady ? (
|
|
184
186
|
<p className="text-muted-foreground text-xs">Loading changes…</p>
|
|
187
|
+
) : storageConfigured === false ? (
|
|
188
|
+
<PrototypeCommentStorageEmptyState message={storageError ?? undefined} />
|
|
185
189
|
) : storageError ? (
|
|
186
190
|
<p className="text-destructive text-xs">{storageError}</p>
|
|
187
191
|
) : (
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Button } from "@prototype/components/ui/button";
|
|
4
|
+
import {
|
|
5
|
+
buildCommentStorageSetupCopyText,
|
|
6
|
+
COMMENT_STORAGE_NOT_CONFIGURED_MESSAGE,
|
|
7
|
+
} from "@prototype/lib/prototypes/comment-storage-setup-prompt";
|
|
8
|
+
import useCopyToClipboard from "@prototype/lib/use-copy-to-clipboard";
|
|
9
|
+
|
|
10
|
+
type PrototypeCommentStorageEmptyStateProps = {
|
|
11
|
+
message?: string;
|
|
12
|
+
vercelProjectName?: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function PrototypeCommentStorageEmptyState({
|
|
16
|
+
message = COMMENT_STORAGE_NOT_CONFIGURED_MESSAGE,
|
|
17
|
+
vercelProjectName,
|
|
18
|
+
}: PrototypeCommentStorageEmptyStateProps) {
|
|
19
|
+
const { copy, icon, isCopied } = useCopyToClipboard();
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="px-1 py-2">
|
|
23
|
+
<p className="text-sm text-muted-foreground">{message}</p>
|
|
24
|
+
<div className="mt-3">
|
|
25
|
+
<Button
|
|
26
|
+
type="button"
|
|
27
|
+
variant="outline"
|
|
28
|
+
size="sm"
|
|
29
|
+
className="h-8 cursor-pointer gap-1.5"
|
|
30
|
+
onClick={() => {
|
|
31
|
+
copy(buildCommentStorageSetupCopyText({ vercelProjectName }));
|
|
32
|
+
}}
|
|
33
|
+
aria-label="Copy comment storage setup prompt"
|
|
34
|
+
>
|
|
35
|
+
{isCopied ? "Copied prompt" : "Copy setup prompt"}
|
|
36
|
+
{icon}
|
|
37
|
+
</Button>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -1,22 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
DialogTitle,
|
|
8
|
-
} from "@prototype/components/ui/dialog";
|
|
3
|
+
import { PrototypeToolDialog } from "@prototype/components/platform-ui/prototype-tool-dialog";
|
|
4
|
+
import { PrototypeBriefTextarea } from "@prototype/components/prototypes/prototype-brief-field";
|
|
5
|
+
import { Button } from "@prototype/components/ui/button";
|
|
6
|
+
import { Switch } from "@prototype/components/ui/switch";
|
|
9
7
|
import { buildCreateDesignExplorationCopyText } from "@prototype/lib/prototypes/design-exploration-types";
|
|
10
8
|
import useCopyToClipboard from "@prototype/lib/use-copy-to-clipboard";
|
|
11
|
-
import { X } from "lucide-react";
|
|
12
9
|
import { useEffect, useState } from "react";
|
|
13
10
|
|
|
14
|
-
const MODAL_CHROME_BUTTON_CLASS =
|
|
15
|
-
"text-muted-foreground hover:text-foreground inline-flex size-8 shrink-0 items-center justify-center rounded-sm border-0 bg-transparent p-0 opacity-70 transition-opacity duration-200 ease hover:opacity-100 outline-none focus:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-0 disabled:pointer-events-none disabled:opacity-40 disabled:hover:opacity-40 disabled:hover:text-muted-foreground motion-reduce:transition-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4";
|
|
16
|
-
|
|
17
|
-
const BRIEF_TEXTAREA_CLASS =
|
|
18
|
-
"field-sizing-content min-h-[min(50vh,20rem)] w-full resize-y border-0 bg-transparent px-0 py-0 text-sm leading-relaxed text-foreground shadow-none outline-none placeholder:text-muted-foreground focus-visible:ring-0";
|
|
19
|
-
|
|
20
11
|
export type PrototypeCreateExplorationModalProps = {
|
|
21
12
|
open: boolean;
|
|
22
13
|
onOpenChange: (open: boolean) => void;
|
|
@@ -31,6 +22,7 @@ export function PrototypeCreateExplorationModal({
|
|
|
31
22
|
existingExplorations,
|
|
32
23
|
}: PrototypeCreateExplorationModalProps) {
|
|
33
24
|
const [brief, setBrief] = useState("");
|
|
25
|
+
const [includeMobbin, setIncludeMobbin] = useState(true);
|
|
34
26
|
const { copy, icon, isCopied } = useCopyToClipboard();
|
|
35
27
|
const canCopy = brief.trim().length > 0;
|
|
36
28
|
|
|
@@ -51,67 +43,52 @@ export function PrototypeCreateExplorationModal({
|
|
|
51
43
|
existingExplorations,
|
|
52
44
|
origin,
|
|
53
45
|
briefText: brief,
|
|
46
|
+
includeMobbin,
|
|
54
47
|
}),
|
|
55
48
|
);
|
|
56
49
|
};
|
|
57
50
|
|
|
58
51
|
return (
|
|
59
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
</
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
<div
|
|
81
|
-
className="h-px w-full shrink-0 bg-border"
|
|
82
|
-
role="separator"
|
|
83
|
-
aria-hidden
|
|
84
|
-
/>
|
|
85
|
-
|
|
86
|
-
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto px-5 py-4">
|
|
87
|
-
<textarea
|
|
88
|
-
value={brief}
|
|
89
|
-
onChange={(event) => setBrief(event.target.value)}
|
|
90
|
-
placeholder="e.g. Give me a settings page with a profile picture, name, plan, and bio"
|
|
91
|
-
aria-label="Exploration brief"
|
|
92
|
-
className={BRIEF_TEXTAREA_CLASS}
|
|
93
|
-
/>
|
|
94
|
-
</div>
|
|
95
|
-
|
|
96
|
-
<div
|
|
97
|
-
className="h-px w-full shrink-0 bg-border"
|
|
98
|
-
role="separator"
|
|
99
|
-
aria-hidden
|
|
100
|
-
/>
|
|
101
|
-
|
|
102
|
-
<div className="flex items-center justify-end gap-2 px-5 py-4">
|
|
103
|
-
<button
|
|
52
|
+
<PrototypeToolDialog
|
|
53
|
+
open={open}
|
|
54
|
+
onOpenChange={onOpenChange}
|
|
55
|
+
title="New exploration"
|
|
56
|
+
description="Describe what you're trying to build, and we'll give you a few different versions."
|
|
57
|
+
footerClassName="justify-between gap-4"
|
|
58
|
+
footer={
|
|
59
|
+
<>
|
|
60
|
+
<label className="flex cursor-pointer items-center gap-2">
|
|
61
|
+
<Switch
|
|
62
|
+
checked={includeMobbin}
|
|
63
|
+
onCheckedChange={setIncludeMobbin}
|
|
64
|
+
aria-label="Gather references from Mobbin"
|
|
65
|
+
className="data-[state=checked]:bg-foreground"
|
|
66
|
+
/>
|
|
67
|
+
<span className="text-sm text-muted-foreground">
|
|
68
|
+
Gather references from Mobbin
|
|
69
|
+
</span>
|
|
70
|
+
</label>
|
|
71
|
+
<Button
|
|
104
72
|
type="button"
|
|
73
|
+
variant="chrome"
|
|
74
|
+
size="sm"
|
|
75
|
+
className="h-8 gap-1.5"
|
|
105
76
|
disabled={!canCopy}
|
|
106
77
|
onClick={handleCopy}
|
|
107
|
-
className="inline-flex h-9 items-center gap-2 rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors duration-200 ease hover:bg-primary-dark disabled:pointer-events-none disabled:opacity-50"
|
|
108
78
|
aria-label="Copy exploration brief and prompt"
|
|
109
79
|
>
|
|
110
|
-
{isCopied ? "Copied" : "Copy prompt"}
|
|
80
|
+
{isCopied ? "Copied prompt" : "Copy prompt"}
|
|
111
81
|
{icon}
|
|
112
|
-
</
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
82
|
+
</Button>
|
|
83
|
+
</>
|
|
84
|
+
}
|
|
85
|
+
>
|
|
86
|
+
<PrototypeBriefTextarea
|
|
87
|
+
value={brief}
|
|
88
|
+
onChange={(event) => setBrief(event.target.value)}
|
|
89
|
+
placeholder="e.g. Give me a settings page with a profile picture, name, plan, and bio"
|
|
90
|
+
aria-label="Exploration brief"
|
|
91
|
+
/>
|
|
92
|
+
</PrototypeToolDialog>
|
|
116
93
|
);
|
|
117
94
|
}
|