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
|
@@ -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
|
+
}
|
|
@@ -4,22 +4,23 @@ import { filterAnnotationsByChannel } from "@prototype/lib/prototype-comments/co
|
|
|
4
4
|
import { useCommentStore } from "@prototype/lib/prototype-comments/react/CommentProvider";
|
|
5
5
|
import { useCommentCaptureBridgeOptional } from "@prototype/lib/prototype-comments/react/CommentCaptureToolbar";
|
|
6
6
|
import { CommentsGrid } from "@prototype/lib/prototype-comments/ui/CommentsGrid";
|
|
7
|
-
import {
|
|
7
|
+
import { buildChangelogDescriptionCopyText } from "@prototype/lib/prototypes/changelog-description-prompt";
|
|
8
8
|
import { usePrototypeReview } from "@prototype/lib/prototypes/prototype-review-context";
|
|
9
|
+
import {
|
|
10
|
+
prototypeReviewPreferenceKey,
|
|
11
|
+
usePersistedLocalString,
|
|
12
|
+
} from "@prototype/lib/prototypes/use-persisted-local-state";
|
|
9
13
|
import { ToolbarIconButton } from "@prototype/components/platform-ui/toolbar-icon-button";
|
|
14
|
+
import { Button } from "@prototype/components/ui/button";
|
|
10
15
|
import { Label } from "@prototype/components/ui/label";
|
|
11
16
|
import { Switch } from "@prototype/components/ui/switch";
|
|
12
|
-
import
|
|
17
|
+
import useCopyToClipboard from "@prototype/lib/use-copy-to-clipboard";
|
|
18
|
+
import { Plus } from "lucide-react";
|
|
13
19
|
import { cn } from "@prototype/lib/utils";
|
|
14
|
-
import {
|
|
15
|
-
useCallback,
|
|
16
|
-
useEffect,
|
|
17
|
-
useMemo,
|
|
18
|
-
useState,
|
|
19
|
-
type ChangeEvent,
|
|
20
|
-
} from "react";
|
|
20
|
+
import { useMemo } 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,64 +106,65 @@ export function PrototypeChangeLogPanel({
|
|
|
105
106
|
updateAnnotation,
|
|
106
107
|
storageError,
|
|
107
108
|
storageReady,
|
|
109
|
+
storageConfigured,
|
|
108
110
|
} = useCommentStore();
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
const review = usePrototypeReview();
|
|
112
|
+
const { value: description, updateValue: updateDescription } =
|
|
113
|
+
usePersistedLocalString(
|
|
114
|
+
prototypeReviewPreferenceKey(review.slug, "changelog-description-brief"),
|
|
115
|
+
"",
|
|
116
|
+
);
|
|
117
|
+
const {
|
|
118
|
+
copy: copyDescriptionPrompt,
|
|
119
|
+
icon: copyDescriptionIcon,
|
|
120
|
+
isCopied: isDescriptionCopied,
|
|
121
|
+
} = useCopyToClipboard();
|
|
112
122
|
const changelogAnnotations = useMemo(
|
|
113
123
|
() => filterAnnotationsByChannel(annotations, "changelog"),
|
|
114
124
|
[annotations],
|
|
115
125
|
);
|
|
126
|
+
const canCopyDescription = description.trim().length > 0;
|
|
116
127
|
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
setDraftOverview(overview);
|
|
120
|
-
}
|
|
121
|
-
}, [metaReady, overview]);
|
|
122
|
-
|
|
123
|
-
const isOverviewDirty = draftOverview !== overview;
|
|
128
|
+
const handleCopyDescriptionPrompt = () => {
|
|
129
|
+
if (!canCopyDescription) return;
|
|
124
130
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
setDraftOverview(event.target.value);
|
|
128
|
-
},
|
|
129
|
-
[],
|
|
130
|
-
);
|
|
131
|
+
const origin =
|
|
132
|
+
typeof window !== "undefined" ? window.location.origin : undefined;
|
|
131
133
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
134
|
+
copyDescriptionPrompt(
|
|
135
|
+
buildChangelogDescriptionCopyText({
|
|
136
|
+
slug: review.slug,
|
|
137
|
+
description,
|
|
138
|
+
origin,
|
|
139
|
+
}),
|
|
140
|
+
);
|
|
141
|
+
};
|
|
135
142
|
|
|
136
143
|
return (
|
|
137
144
|
<div className="flex flex-col">
|
|
138
145
|
<section className={CHANGELOG_SECTION_CLASS}>
|
|
139
|
-
<
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
placeholder="Summarize what changed and why…"
|
|
162
|
-
aria-label="Description"
|
|
163
|
-
className={CHANGELOG_OVERVIEW_FIELD_CLASS}
|
|
164
|
-
/>
|
|
165
|
-
)}
|
|
146
|
+
<span className={CHANGELOG_SECTION_TITLE_CLASS}>Description</span>
|
|
147
|
+
<textarea
|
|
148
|
+
id="prototype-changelog-overview"
|
|
149
|
+
value={description}
|
|
150
|
+
onChange={(event) => updateDescription(event.target.value)}
|
|
151
|
+
rows={3}
|
|
152
|
+
placeholder="Summarize what changed and why…"
|
|
153
|
+
aria-label="Description"
|
|
154
|
+
className={CHANGELOG_OVERVIEW_FIELD_CLASS}
|
|
155
|
+
/>
|
|
156
|
+
<Button
|
|
157
|
+
type="button"
|
|
158
|
+
variant="chrome"
|
|
159
|
+
size="sm"
|
|
160
|
+
className="h-8 gap-1.5 self-start"
|
|
161
|
+
disabled={!canCopyDescription}
|
|
162
|
+
onClick={handleCopyDescriptionPrompt}
|
|
163
|
+
aria-label="Copy description prompt"
|
|
164
|
+
>
|
|
165
|
+
{isDescriptionCopied ? "Copied prompt" : "Copy prompt"}
|
|
166
|
+
{copyDescriptionIcon}
|
|
167
|
+
</Button>
|
|
166
168
|
</section>
|
|
167
169
|
|
|
168
170
|
<PrototypeReferenceDocs
|
|
@@ -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
|
) : (
|
|
@@ -4,7 +4,6 @@ import type { ReactNode } from "react";
|
|
|
4
4
|
|
|
5
5
|
import { PrototypeTargetIdProvider } from "@prototype/components/prototypes/prototype-target";
|
|
6
6
|
import { CommentProvider } from "@prototype/lib/prototype-comments/react/CommentProvider";
|
|
7
|
-
import { ChangelogMetaProvider } from "@prototype/lib/prototypes/changelog-meta-context";
|
|
8
7
|
import type { PrototypeComponentRegistry } from "@prototype/lib/prototypes/prototype-component-registry";
|
|
9
8
|
import { getPrototypeComponentRegistryForSlug } from "@prototype/lib/prototypes/create-prototype-registry";
|
|
10
9
|
import {
|
|
@@ -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
|
+
}
|