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.
Files changed (90) hide show
  1. package/AGENTS.md +308 -29
  2. package/README.md +48 -6
  3. package/package.json +1 -1
  4. package/scripts/lib/host-config.example.sh +1 -1
  5. package/scripts/verify-prototype-preview-states.mjs +57 -0
  6. package/src/components/platform-ui/controls-panel-select.tsx +4 -3
  7. package/src/components/platform-ui/prototype-tool-dialog.tsx +117 -0
  8. package/src/components/platform-ui/sidebar.tsx +34 -16
  9. package/src/components/prototype-gallery-client.tsx +77 -37
  10. package/src/components/prototypes/design-exploration-variant-preview-shell.tsx +43 -0
  11. package/src/components/prototypes/prototype-brief-field.tsx +33 -0
  12. package/src/components/prototypes/prototype-change-log-panel.tsx +4 -0
  13. package/src/components/prototypes/prototype-comment-storage-empty-state.tsx +41 -0
  14. package/src/components/prototypes/prototype-create-exploration-modal.tsx +41 -64
  15. package/src/components/prototypes/prototype-create-prototype-modal.tsx +102 -0
  16. package/src/components/prototypes/prototype-create-state-modal.tsx +31 -70
  17. package/src/components/prototypes/prototype-default-state-map-page.tsx +4 -4
  18. package/src/components/prototypes/prototype-design-brief-modal.tsx +10 -10
  19. package/src/components/prototypes/prototype-design-brief-panel.tsx +261 -231
  20. package/src/components/prototypes/prototype-floating-pill.module.scss +16 -9
  21. package/src/components/prototypes/prototype-mobbin-gallery.tsx +18 -2
  22. package/src/components/prototypes/prototype-review-sidebar.tsx +11 -28
  23. package/src/components/prototypes/prototype-spec-panel-content.tsx +5 -5
  24. package/src/components/prototypes/prototype-spec-panel-empty-state.tsx +53 -0
  25. package/src/components/prototypes/prototype-state-canvas-overlay.module.scss +20 -47
  26. package/src/components/prototypes/prototype-state-canvas-overlay.tsx +4 -42
  27. package/src/components/prototypes/prototype-variant-explorer.tsx +32 -78
  28. package/src/components/prototypes/prototype-vercel-preview-modal.tsx +4 -3
  29. package/src/components/prototypes/state-map-annotation-tooltip.module.scss +7 -7
  30. package/src/components/shell/prototype-component-library-empty-state.tsx +59 -0
  31. package/src/components/shell/prototype-gallery-nav.tsx +124 -0
  32. package/src/components/shell/prototype-gallery-shell.tsx +45 -13
  33. package/src/components/shell/prototype-link-source-modal.tsx +100 -0
  34. package/src/components/ui/button.tsx +3 -1
  35. package/src/components/ui/dialog.tsx +43 -4
  36. package/src/config/create-prototype-component-library-page.tsx +74 -0
  37. package/src/config/create-prototype-gallery-page.tsx +26 -23
  38. package/src/config/create-prototype-host-app.tsx +3 -15
  39. package/src/config/create-prototype-site-layout.tsx +14 -1
  40. package/src/config/create-prototype-state-map-page.tsx +44 -0
  41. package/src/config/prototype-site-layout-client.tsx +20 -6
  42. package/src/config/with-prototype.ts +21 -0
  43. package/src/index.ts +54 -10
  44. package/src/lib/copy-text-with-attachments.ts +35 -0
  45. package/src/lib/format-source-directory-name.ts +12 -0
  46. package/src/lib/platform-ui/sidebar-interactive-cursor.ts +19 -0
  47. package/src/lib/pr-split/build-pr-split-plan-prompt.ts +81 -0
  48. package/src/lib/pr-split/pr-split-highlight.ts +3 -0
  49. package/src/lib/prototype-comments/core/annotation-target.ts +12 -3
  50. package/src/lib/prototype-comments/core/comment-capture-blocked.ts +24 -0
  51. package/src/lib/prototype-comments/core/screenshot.ts +68 -31
  52. package/src/lib/prototype-comments/core/storage.ts +3 -6
  53. package/src/lib/prototype-comments/core/types.ts +2 -0
  54. package/src/lib/prototype-comments/hooks/useLayoutShift.ts +9 -2
  55. package/src/lib/prototype-comments/react/CommentCaptureToolbar.tsx +45 -10
  56. package/src/lib/prototype-comments/react/CommentProvider.tsx +19 -0
  57. package/src/lib/prototype-comments/ui/CommentModeToggleButton.tsx +34 -0
  58. package/src/lib/prototype-comments/ui/CommentsGrid.tsx +8 -16
  59. package/src/lib/prototype-comments/ui/CommentsSidebar.tsx +5 -19
  60. package/src/lib/prototype-comments/ui/capture-styles.module.scss +5 -1
  61. package/src/lib/prototype-comments/ui/comments-grid-card.module.scss +0 -7
  62. package/src/lib/prototypes/build-design-exploration-renderers.ts +44 -0
  63. package/src/lib/prototypes/comment-storage-setup-prompt.ts +124 -0
  64. package/src/lib/prototypes/create-prototype-prompt.ts +96 -0
  65. package/src/lib/prototypes/create-prototype-registry.ts +5 -0
  66. package/src/lib/prototypes/design-exploration-types.ts +261 -10
  67. package/src/lib/prototypes/link-source-prompt.ts +93 -0
  68. package/src/lib/prototypes/populate-component-library-prompt.ts +64 -0
  69. package/src/lib/prototypes/prototype-config-types.ts +2 -0
  70. package/src/lib/prototypes/prototype-layout-classes.ts +19 -0
  71. package/src/lib/prototypes/prototype-preview-state-registry.ts +5 -2
  72. package/src/lib/prototypes/prototype-review-context.tsx +4 -4
  73. package/src/lib/prototypes/prototype-state-canvas-constants.ts +1 -1
  74. package/src/lib/prototypes/prototype-state-canvas-types.ts +0 -2
  75. package/src/lib/prototypes/prototype-storage-status.ts +31 -0
  76. package/src/lib/prototypes/sort-prototypes-by-creation-date.ts +43 -0
  77. package/src/lib/prototypes/use-prototype-tool-theme.tsx +1 -1
  78. package/src/lib/prototypes/use-toolbar-hover-panel.ts +12 -1
  79. package/src/lib/prototypes/variant-set-lucide-icon.tsx +24 -0
  80. package/src/lib/tool-portal.ts +24 -0
  81. package/src/lib/use-copy-to-clipboard.ts +7 -13
  82. package/src/server/create-host-api-route.ts +7 -16
  83. package/src/server/create-prototype-api-routes.ts +0 -115
  84. package/src/server/prototype-storage-status-route.ts +29 -0
  85. package/src/server.ts +0 -8
  86. package/src/styles/globals.css +359 -71
  87. package/src/lib/prototypes/design-exploration-default-storage.ts +0 -68
  88. package/src/lib/prototypes/design-exploration-default.ts +0 -51
  89. package/src/lib/prototypes/use-design-exploration-default.ts +0 -114
  90. 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 || !review) return;
126
+ if (!isMenuList || !registerPreviewStates) return;
126
127
 
127
- review.registerPreviewStates(
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, review]);
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-icon-hover)]";
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
- document.documentElement.setAttribute("data-platform-sidebar-inline", "");
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("data-platform-sidebar-open");
92
- document.documentElement.removeAttribute("data-platform-sidebar-resizing");
93
- document.documentElement.removeAttribute("data-platform-sidebar-inline");
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("data-platform-sidebar-open", open);
99
- }, [open]);
114
+ document.documentElement.toggleAttribute(openAttr, open);
115
+ }, [open, openAttr]);
100
116
 
101
117
  useEffect(() => {
102
- document.documentElement.toggleAttribute(
103
- "data-platform-sidebar-resizing",
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-icon)]",
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
- className="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-platform-sidebar-resizing]_&]:before:bg-border"
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 Image from "next/image";
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
+ &nbsp;
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
- <div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
21
- {prototypes.map((prototype) => (
22
- <Link
23
- key={prototype.slug}
24
- href={`/prototypes/${prototype.slug}`}
25
- className="group"
26
- >
27
- <div
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 className="border-border bg-background relative aspect-video overflow-hidden rounded-xl border">
36
- <Image
37
- src={screenshotSrc(
38
- prototype.screenshot,
39
- screenshotVersions[prototype.slug],
40
- )}
41
- alt={prototype.title}
42
- width={1600}
43
- height={900}
44
- quality={90}
45
- sizes="(min-width: 1024px) 33vw, (min-width: 768px) 50vw, 100vw"
46
- className="h-full w-full object-cover object-top"
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
- </div>
50
- <p className="text-sm text-muted-foreground group-hover:text-foreground mt-2 transition-colors duration-300 ease-out">
51
- {prototype.title}
52
- </p>
53
- </Link>
54
- ))}
55
- </div>
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
- Dialog,
5
- DialogClose,
6
- DialogContent,
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
- <Dialog open={open} onOpenChange={onOpenChange}>
60
- <DialogContent
61
- showCloseButton={false}
62
- className="tool-dialog-surface flex max-h-[min(92svh,calc(100%-2rem))] w-[min(42rem,calc(100vw-2rem))] max-w-[min(42rem,calc(100vw-2rem))] flex-col gap-0 overflow-hidden border-0 p-0 sm:max-w-[min(42rem,calc(100vw-2rem))]"
63
- >
64
- <div className="flex items-start justify-between gap-3 px-5 py-4">
65
- <div className="flex min-w-0 flex-col gap-1">
66
- <DialogTitle className="text-base font-medium text-foreground">
67
- New exploration
68
- </DialogTitle>
69
- <p className="text-sm text-muted-foreground">
70
- Describe what you&apos;re trying to build, and we&apos;ll give you
71
- a few different versions.
72
- </p>
73
- </div>
74
- <DialogClose className={MODAL_CHROME_BUTTON_CLASS}>
75
- <X />
76
- <span className="sr-only">Close</span>
77
- </DialogClose>
78
- </div>
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
- </button>
113
- </div>
114
- </DialogContent>
115
- </Dialog>
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
  }