proto-plugin 0.1.0 → 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 (91) hide show
  1. package/AGENTS.md +308 -29
  2. package/README.md +55 -13
  3. package/package.json +1 -1
  4. package/scripts/lib/host-config.example.sh +1 -1
  5. package/scripts/publish-package.sh +41 -0
  6. package/scripts/verify-prototype-preview-states.mjs +57 -0
  7. package/src/components/platform-ui/controls-panel-select.tsx +4 -3
  8. package/src/components/platform-ui/prototype-tool-dialog.tsx +117 -0
  9. package/src/components/platform-ui/sidebar.tsx +34 -16
  10. package/src/components/prototype-gallery-client.tsx +77 -37
  11. package/src/components/prototypes/design-exploration-variant-preview-shell.tsx +43 -0
  12. package/src/components/prototypes/prototype-brief-field.tsx +33 -0
  13. package/src/components/prototypes/prototype-change-log-panel.tsx +4 -0
  14. package/src/components/prototypes/prototype-comment-storage-empty-state.tsx +41 -0
  15. package/src/components/prototypes/prototype-create-exploration-modal.tsx +41 -64
  16. package/src/components/prototypes/prototype-create-prototype-modal.tsx +102 -0
  17. package/src/components/prototypes/prototype-create-state-modal.tsx +31 -70
  18. package/src/components/prototypes/prototype-default-state-map-page.tsx +4 -4
  19. package/src/components/prototypes/prototype-design-brief-modal.tsx +10 -10
  20. package/src/components/prototypes/prototype-design-brief-panel.tsx +261 -231
  21. package/src/components/prototypes/prototype-floating-pill.module.scss +16 -9
  22. package/src/components/prototypes/prototype-mobbin-gallery.tsx +18 -2
  23. package/src/components/prototypes/prototype-review-sidebar.tsx +11 -28
  24. package/src/components/prototypes/prototype-spec-panel-content.tsx +5 -5
  25. package/src/components/prototypes/prototype-spec-panel-empty-state.tsx +53 -0
  26. package/src/components/prototypes/prototype-state-canvas-overlay.module.scss +20 -47
  27. package/src/components/prototypes/prototype-state-canvas-overlay.tsx +4 -42
  28. package/src/components/prototypes/prototype-variant-explorer.tsx +32 -78
  29. package/src/components/prototypes/prototype-vercel-preview-modal.tsx +4 -3
  30. package/src/components/prototypes/state-map-annotation-tooltip.module.scss +7 -7
  31. package/src/components/shell/prototype-component-library-empty-state.tsx +59 -0
  32. package/src/components/shell/prototype-gallery-nav.tsx +124 -0
  33. package/src/components/shell/prototype-gallery-shell.tsx +45 -13
  34. package/src/components/shell/prototype-link-source-modal.tsx +100 -0
  35. package/src/components/ui/button.tsx +3 -1
  36. package/src/components/ui/dialog.tsx +43 -4
  37. package/src/config/create-prototype-component-library-page.tsx +74 -0
  38. package/src/config/create-prototype-gallery-page.tsx +26 -23
  39. package/src/config/create-prototype-host-app.tsx +3 -15
  40. package/src/config/create-prototype-site-layout.tsx +14 -1
  41. package/src/config/create-prototype-state-map-page.tsx +44 -0
  42. package/src/config/prototype-site-layout-client.tsx +20 -6
  43. package/src/config/with-prototype.ts +21 -0
  44. package/src/index.ts +54 -10
  45. package/src/lib/copy-text-with-attachments.ts +35 -0
  46. package/src/lib/format-source-directory-name.ts +12 -0
  47. package/src/lib/platform-ui/sidebar-interactive-cursor.ts +19 -0
  48. package/src/lib/pr-split/build-pr-split-plan-prompt.ts +81 -0
  49. package/src/lib/pr-split/pr-split-highlight.ts +3 -0
  50. package/src/lib/prototype-comments/core/annotation-target.ts +12 -3
  51. package/src/lib/prototype-comments/core/comment-capture-blocked.ts +24 -0
  52. package/src/lib/prototype-comments/core/screenshot.ts +68 -31
  53. package/src/lib/prototype-comments/core/storage.ts +3 -6
  54. package/src/lib/prototype-comments/core/types.ts +2 -0
  55. package/src/lib/prototype-comments/hooks/useLayoutShift.ts +9 -2
  56. package/src/lib/prototype-comments/react/CommentCaptureToolbar.tsx +45 -10
  57. package/src/lib/prototype-comments/react/CommentProvider.tsx +19 -0
  58. package/src/lib/prototype-comments/ui/CommentModeToggleButton.tsx +34 -0
  59. package/src/lib/prototype-comments/ui/CommentsGrid.tsx +8 -16
  60. package/src/lib/prototype-comments/ui/CommentsSidebar.tsx +5 -19
  61. package/src/lib/prototype-comments/ui/capture-styles.module.scss +5 -1
  62. package/src/lib/prototype-comments/ui/comments-grid-card.module.scss +0 -7
  63. package/src/lib/prototypes/build-design-exploration-renderers.ts +44 -0
  64. package/src/lib/prototypes/comment-storage-setup-prompt.ts +124 -0
  65. package/src/lib/prototypes/create-prototype-prompt.ts +96 -0
  66. package/src/lib/prototypes/create-prototype-registry.ts +5 -0
  67. package/src/lib/prototypes/design-exploration-types.ts +261 -10
  68. package/src/lib/prototypes/link-source-prompt.ts +93 -0
  69. package/src/lib/prototypes/populate-component-library-prompt.ts +64 -0
  70. package/src/lib/prototypes/prototype-config-types.ts +2 -0
  71. package/src/lib/prototypes/prototype-layout-classes.ts +19 -0
  72. package/src/lib/prototypes/prototype-preview-state-registry.ts +5 -2
  73. package/src/lib/prototypes/prototype-review-context.tsx +4 -4
  74. package/src/lib/prototypes/prototype-state-canvas-constants.ts +1 -1
  75. package/src/lib/prototypes/prototype-state-canvas-types.ts +0 -2
  76. package/src/lib/prototypes/prototype-storage-status.ts +31 -0
  77. package/src/lib/prototypes/sort-prototypes-by-creation-date.ts +43 -0
  78. package/src/lib/prototypes/use-prototype-tool-theme.tsx +1 -1
  79. package/src/lib/prototypes/use-toolbar-hover-panel.ts +12 -1
  80. package/src/lib/prototypes/variant-set-lucide-icon.tsx +24 -0
  81. package/src/lib/tool-portal.ts +24 -0
  82. package/src/lib/use-copy-to-clipboard.ts +7 -13
  83. package/src/server/create-host-api-route.ts +7 -16
  84. package/src/server/create-prototype-api-routes.ts +0 -115
  85. package/src/server/prototype-storage-status-route.ts +29 -0
  86. package/src/server.ts +0 -8
  87. package/src/styles/globals.css +359 -71
  88. package/src/lib/prototypes/design-exploration-default-storage.ts +0 -68
  89. package/src/lib/prototypes/design-exploration-default.ts +0 -51
  90. package/src/lib/prototypes/use-design-exploration-default.ts +0 -114
  91. package/src/server/redis/prototype-design-exploration-default.ts +0 -32
@@ -8,6 +8,8 @@ REGISTRY="https://registry.npmjs.org"
8
8
  DRY_RUN=0
9
9
  TAG="latest"
10
10
  SKIP_VERIFY=0
11
+ SKIP_BUMP=0
12
+ BUMP="patch"
11
13
  OTP=""
12
14
 
13
15
  usage() {
@@ -21,6 +23,8 @@ Options:
21
23
  --tag <tag> npm dist-tag (default: latest)
22
24
  --otp <code> One-time password from your authenticator app
23
25
  --skip-verify Skip pre-publish verification scripts
26
+ --no-bump Publish package.json version as-is (fail if already published)
27
+ --bump <level> Bump level when current version exists on npm: patch, minor, major (default: patch)
24
28
  -h, --help Show this help message
25
29
 
26
30
  Examples:
@@ -47,6 +51,21 @@ while [[ $# -gt 0 ]]; do
47
51
  SKIP_VERIFY=1
48
52
  shift
49
53
  ;;
54
+ --no-bump)
55
+ SKIP_BUMP=1
56
+ shift
57
+ ;;
58
+ --bump)
59
+ BUMP="${2:?Missing value for --bump}"
60
+ case "$BUMP" in
61
+ patch | minor | major) ;;
62
+ *)
63
+ echo "Invalid --bump value: $BUMP (expected patch, minor, or major)" >&2
64
+ exit 1
65
+ ;;
66
+ esac
67
+ shift 2
68
+ ;;
50
69
  --otp)
51
70
  OTP="${2:?Missing value for --otp}"
52
71
  shift 2
@@ -99,6 +118,25 @@ fi
99
118
  PACKAGE_NAME="$(node -pe "require('./package.json').name")"
100
119
  PACKAGE_VERSION="$(node -pe "require('./package.json').version")"
101
120
 
121
+ version_is_published() {
122
+ npm view "${PACKAGE_NAME}@${1}" version --registry "$REGISTRY" >/dev/null 2>&1
123
+ }
124
+
125
+ ensure_unpublished_version() {
126
+ local bump_level="$1"
127
+ while version_is_published "$(node -pe "require('./package.json').version")"; do
128
+ local current
129
+ current="$(node -pe "require('./package.json').version")"
130
+ echo "Version ${current} is already on npm; bumping ${bump_level}..."
131
+ npm version "$bump_level" --no-git-tag-version
132
+ done
133
+ }
134
+
135
+ if [[ "$SKIP_BUMP" -eq 0 ]]; then
136
+ ensure_unpublished_version "$BUMP"
137
+ PACKAGE_VERSION="$(node -pe "require('./package.json').version")"
138
+ fi
139
+
102
140
  echo "Publishing ${PACKAGE_NAME}@${PACKAGE_VERSION}"
103
141
  echo "Registry: ${REGISTRY}"
104
142
  echo "Access: public (unscoped)"
@@ -144,4 +182,7 @@ if [[ "$DRY_RUN" -eq 0 ]]; then
144
182
  echo "Install with:"
145
183
  echo " npm install ${PACKAGE_NAME}@${PACKAGE_VERSION}"
146
184
  echo " pnpm add ${PACKAGE_NAME}@${PACKAGE_VERSION}"
185
+ if [[ "$SKIP_BUMP" -eq 0 ]]; then
186
+ echo "Commit the version bump in package.json when ready."
187
+ fi
147
188
  fi
@@ -16,6 +16,40 @@ import {
16
16
  const toolRoot = resolveToolRoot(import.meta.url);
17
17
  const repoRoot = resolveHostRoot(toolRoot);
18
18
  const prototypesDir = path.join(repoRoot, "src/prototypes");
19
+ const prototypeConfigPath = path.join(repoRoot, "prototype.config.ts");
20
+
21
+ function readPrototypeConfigSource() {
22
+ if (!fs.existsSync(prototypeConfigPath)) {
23
+ return null;
24
+ }
25
+ return fs.readFileSync(prototypeConfigPath, "utf8");
26
+ }
27
+
28
+ function prototypeConfigBlockHasStateMapComponent(slug, configSource) {
29
+ const slugMarker = `slug: "${slug}"`;
30
+ const slugIndex = configSource.indexOf(slugMarker);
31
+ if (slugIndex === -1) return false;
32
+
33
+ const nextSlugIndex = configSource.indexOf('slug: "', slugIndex + slugMarker.length);
34
+ const block =
35
+ nextSlugIndex === -1
36
+ ? configSource.slice(slugIndex)
37
+ : configSource.slice(slugIndex, nextSlugIndex);
38
+
39
+ return block.includes("stateMapComponent");
40
+ }
41
+
42
+ function findStateMapPageFiles(slug) {
43
+ const stateMapPagePath = path.join(prototypesDir, slug, "state-map-page.tsx");
44
+ return fs.existsSync(stateMapPagePath) ? [stateMapPagePath] : [];
45
+ }
46
+
47
+ function indexUsesStateCanvasConfig(slug) {
48
+ const indexPath = path.join(prototypesDir, slug, "index.tsx");
49
+ if (!fs.existsSync(indexPath)) return false;
50
+ const source = fs.readFileSync(indexPath, "utf8");
51
+ return source.includes("setStateCanvasConfig");
52
+ }
19
53
 
20
54
  function listPrototypeSlugs() {
21
55
  return fs
@@ -126,6 +160,29 @@ for (const slug of listPrototypeSlugs()) {
126
160
  failed = true;
127
161
  }
128
162
  }
163
+
164
+ const stateMapPageFiles = findStateMapPageFiles(slug);
165
+ if (stateMapPageFiles.length === 0) {
166
+ console.error(
167
+ `[${slug}] Has *-preview-states.ts but no state-map-page.tsx. Add src/prototypes/${slug}/state-map-page.tsx and register stateMapComponent in prototype.config.ts.`,
168
+ );
169
+ failed = true;
170
+ }
171
+
172
+ const configSource = readPrototypeConfigSource();
173
+ if (configSource && !prototypeConfigBlockHasStateMapComponent(slug, configSource)) {
174
+ console.error(
175
+ `[${slug}] Has *-preview-states.ts but prototype.config.ts is missing stateMapComponent for this slug.`,
176
+ );
177
+ failed = true;
178
+ }
179
+
180
+ if (!indexUsesStateCanvasConfig(slug)) {
181
+ console.error(
182
+ `[${slug}] Has *-preview-states.ts but index.tsx does not wire review.setStateCanvasConfig(...).`,
183
+ );
184
+ failed = true;
185
+ }
129
186
  }
130
187
 
131
188
  if (failed) {
@@ -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
+ }