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
@@ -4,19 +4,16 @@ import useCopyToClipboard from "@prototype/lib/use-copy-to-clipboard";
4
4
  import { PrototypeControlGroup } from "@prototype/components/prototypes/prototype-control";
5
5
  import { PrototypeDesignBriefModal } from "@prototype/components/prototypes/prototype-design-brief-modal";
6
6
  import { PrototypeDesignBriefPanel } from "@prototype/components/prototypes/prototype-design-brief-panel";
7
- import {
8
- PrototypeComponent,
9
- usePrototypeSlug,
10
- } from "@prototype/components/prototypes/prototype-target";
7
+ import { PrototypeComponent } from "@prototype/components/prototypes/prototype-target";
11
8
  import { PrototypeVariantTabs } from "@prototype/components/prototypes/prototype-variant-tabs";
12
9
  import {
13
10
  buildBriefOverviewCopyText,
11
+ getDesignExplorationDisplayOptions,
14
12
  type DesignExplorationConfig,
15
13
  } from "@prototype/lib/prototypes/design-exploration-types";
16
14
  import { usePrototypeReviewOptional } from "@prototype/lib/prototypes/prototype-review-context";
17
- import { useDesignExplorationDefault } from "@prototype/lib/prototypes/use-design-exploration-default";
18
15
  import { usePersistedLocalString } from "@prototype/lib/prototypes/use-persisted-local-state";
19
- import { ChevronDown, ChevronUp, LayoutGrid } from "lucide-react";
16
+ import { LayoutGrid } from "lucide-react";
20
17
  import { useEffect, useMemo, useState } from "react";
21
18
 
22
19
  const OVERVIEW_CONTROL_BUTTON_CLASS = "tool-toolbar-button";
@@ -35,6 +32,12 @@ type PrototypeVariantExplorerProps<TVariant extends string> =
35
32
  * design explorations available in the review sidebar.
36
33
  */
37
34
  registerOnly?: boolean;
35
+ /**
36
+ * When true, skips rendering the active variant inline on the page while
37
+ * still using `renderers` in the design-brief sidebar/modal. Use when the
38
+ * live page already reflects the selected variant (e.g. a full-page canvas).
39
+ */
40
+ hideInlinePreview?: boolean;
38
41
  /** When set, registers this exploration in the review sidebar. */
39
42
  variantSet?: {
40
43
  id: string;
@@ -49,34 +52,26 @@ export function PrototypeVariantExplorer<TVariant extends string>({
49
52
  variant,
50
53
  onVariantChange,
51
54
  options,
52
- rationale,
53
55
  renderers,
56
+ baseline,
54
57
  brief,
55
58
  context,
56
59
  mobbin,
57
60
  variantsSection,
61
+ defaultVariant,
58
62
  variantTabAriaLabel = "variant",
59
63
  mobbinGalleryId = "mobbin-inspiration-gallery",
60
64
  wrapRoot = true,
61
65
  registerOnly = false,
66
+ hideInlinePreview = false,
62
67
  briefConfigFilePath,
63
68
  variantSet,
64
69
  }: PrototypeVariantExplorerProps<TVariant>) {
65
70
  const review = usePrototypeReviewOptional();
66
- const slug = usePrototypeSlug();
67
- const validValues = useMemo(
68
- () => options.map((option) => option.value),
69
- [options],
70
- );
71
- const {
72
- effectiveDefault,
73
- setDefaultVariant,
74
- } = useDesignExplorationDefault({
75
- slug,
76
- storageKeyPrefix,
77
- validValues,
78
- });
79
- const [notesExpanded, setNotesExpanded] = useState(false);
71
+ // Default is stored in the codebase config — not KV. "Make default" copies a
72
+ // prompt to update `defaultVariant` in the exploration config file.
73
+ const effectiveDefault = defaultVariant ?? options[0]?.value;
74
+ const displayOptions = getDesignExplorationDisplayOptions(options, baseline);
80
75
  const [overviewOpen, setOverviewOpen] = useState(false);
81
76
  const { copy: copyOverview, icon: copyIcon } = useCopyToClipboard();
82
77
  const { value: overviewTitle } = usePersistedLocalString(
@@ -88,9 +83,7 @@ export function PrototypeVariantExplorer<TVariant extends string>({
88
83
  brief.descriptionDefault,
89
84
  );
90
85
 
91
- const fallbackVariant = options[0]?.value;
92
- const activeRationale =
93
- rationale[variant] ?? (fallbackVariant ? rationale[fallbackVariant] : undefined);
86
+ const fallbackVariant = displayOptions[0]?.value;
94
87
  const ActiveVariantRenderer =
95
88
  renderers[variant] ?? (fallbackVariant ? renderers[fallbackVariant] : null);
96
89
  const hideInlineControls = Boolean(review);
@@ -107,11 +100,12 @@ export function PrototypeVariantExplorer<TVariant extends string>({
107
100
  variant,
108
101
  onVariantChange,
109
102
  options,
110
- rationale,
111
103
  renderers,
104
+ baseline,
112
105
  defaultVariantValue: effectiveDefault,
113
- onSetDefaultVariant: setDefaultVariant,
114
106
  briefConfigFilePath,
107
+ explorationLabel: variantSet?.label ?? brief.titleDefault,
108
+ explorationId: variantSet?.id,
115
109
  }),
116
110
  [
117
111
  brief,
@@ -123,11 +117,12 @@ export function PrototypeVariantExplorer<TVariant extends string>({
123
117
  mobbinGalleryId,
124
118
  onVariantChange,
125
119
  options,
126
- rationale,
127
120
  renderers,
128
- setDefaultVariant,
121
+ baseline,
129
122
  storageKeyPrefix,
130
123
  variant,
124
+ variantSet?.id,
125
+ variantSet?.label,
131
126
  variantsSection,
132
127
  ],
133
128
  );
@@ -151,8 +146,9 @@ export function PrototypeVariantExplorer<TVariant extends string>({
151
146
  overviewTitle,
152
147
  overviewDescription,
153
148
  options.map((option) => option.value).join(","),
149
+ baseline.value,
154
150
  ].join("|"),
155
- [variant, effectiveDefault, overviewTitle, overviewDescription, options],
151
+ [variant, effectiveDefault, overviewTitle, overviewDescription, options, baseline.value],
156
152
  );
157
153
 
158
154
  const variantSetId = variantSet?.id ?? null;
@@ -205,7 +201,7 @@ export function PrototypeVariantExplorer<TVariant extends string>({
205
201
  <div className="flex flex-wrap items-center gap-2">
206
202
  <PrototypeVariantTabs
207
203
  idPrefix={variantTabsIdPrefix}
208
- options={options}
204
+ options={displayOptions}
209
205
  value={variant}
210
206
  onValueChange={onVariantChange}
211
207
  keyboardScopeActive={!overviewOpen}
@@ -242,65 +238,21 @@ export function PrototypeVariantExplorer<TVariant extends string>({
242
238
  <LayoutGrid className="size-3.5" />
243
239
  </button>
244
240
  </PrototypeComponent>
245
-
246
- <button
247
- type="button"
248
- className={OVERVIEW_CONTROL_BUTTON_CLASS}
249
- onClick={() => setNotesExpanded((expanded) => !expanded)}
250
- aria-expanded={notesExpanded}
251
- aria-controls={`${componentIdPrefix}-notes`}
252
- aria-label={notesExpanded ? "Hide notes" : "Show notes"}
253
- >
254
- {notesExpanded ? (
255
- <ChevronUp className="size-3.5" />
256
- ) : (
257
- <ChevronDown className="size-3.5" />
258
- )}
259
- </button>
260
241
  </div>
261
242
  </div>
262
-
263
- {notesExpanded && activeRationale ? (
264
- <PrototypeComponent
265
- id={`${componentIdPrefix}.rationale`}
266
- className="border-border bg-muted absolute right-0 top-full z-10 mt-2 max-w-md rounded-lg border px-3 py-2.5 text-left"
267
- >
268
- <div
269
- id={`${componentIdPrefix}-notes`}
270
- className="flex flex-col gap-2"
271
- >
272
- <p className="text-sm leading-relaxed">
273
- <span className="text-foreground font-medium">Good</span>
274
- {" · "}
275
- <span className="text-muted-foreground">
276
- {activeRationale.good}
277
- </span>
278
- </p>
279
- <p className="text-sm leading-relaxed">
280
- <span className="text-foreground font-medium">Bad</span>
281
- {" · "}
282
- <span className="text-muted-foreground">
283
- {activeRationale.bad}
284
- </span>
285
- </p>
286
- </div>
287
- </PrototypeComponent>
288
- ) : null}
289
243
  </div>
290
244
  </PrototypeControlGroup>
291
245
  </div>
292
246
  ),
293
247
  [
294
- activeRationale,
295
248
  brief.descriptionDefault,
296
249
  brief.titleDefault,
297
250
  componentIdPrefix,
298
251
  copyIcon,
299
252
  copyOverview,
300
253
  effectiveDefault,
301
- notesExpanded,
302
254
  onVariantChange,
303
- options,
255
+ displayOptions,
304
256
  overviewDescription,
305
257
  overviewOpen,
306
258
  overviewTitle,
@@ -334,14 +286,16 @@ export function PrototypeVariantExplorer<TVariant extends string>({
334
286
  variant={variant}
335
287
  onVariantChange={onVariantChange}
336
288
  options={options}
337
- rationale={rationale}
338
289
  renderers={renderers}
290
+ baseline={baseline}
339
291
  defaultVariantValue={effectiveDefault}
340
- onSetDefaultVariant={setDefaultVariant}
292
+ briefConfigFilePath={briefConfigFilePath}
341
293
  />
342
294
  ) : null}
343
295
 
344
- {!registerOnly && ActiveVariantRenderer ? ActiveVariantRenderer() : null}
296
+ {!registerOnly && !hideInlinePreview && ActiveVariantRenderer
297
+ ? ActiveVariantRenderer()
298
+ : null}
345
299
  </div>
346
300
  );
347
301
  }
@@ -6,11 +6,11 @@ import {
6
6
  DialogContent,
7
7
  DialogTitle,
8
8
  } from "@prototype/components/ui/dialog";
9
+ import { PROTOTYPE_TOOL_MODAL_CLOSE_CLASS } from "@prototype/components/platform-ui/prototype-tool-dialog";
9
10
  import { ExternalLink, Loader2, X } from "lucide-react";
10
11
  import { useEffect, useState } from "react";
11
12
 
12
- const MODAL_CHROME_BUTTON_CLASS =
13
- "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 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";
13
+ const MODAL_CHROME_BUTTON_CLASS = PROTOTYPE_TOOL_MODAL_CLOSE_CLASS;
14
14
 
15
15
  export type PrototypeVercelPreviewModalProps = {
16
16
  open: boolean;
@@ -44,8 +44,9 @@ export function PrototypeVercelPreviewModal({
44
44
  return (
45
45
  <Dialog open={open} onOpenChange={onOpenChange}>
46
46
  <DialogContent
47
+ portalScope="tool"
47
48
  showCloseButton={false}
48
- className="flex h-[min(92svh,calc(100%-2rem))] max-h-[min(92svh,calc(100%-2rem))] w-[min(96rem,calc(100vw-2rem))] max-w-[min(96rem,calc(100vw-2rem))] flex-col gap-0 overflow-hidden p-0 sm:max-w-[min(96rem,calc(100vw-2rem))]"
49
+ className="tool-dialog-surface flex h-[min(92svh,calc(100%-2rem))] max-h-[min(92svh,calc(100%-2rem))] w-[min(96rem,calc(100vw-2rem))] max-w-[min(96rem,calc(100vw-2rem))] flex-col gap-0 overflow-hidden border-0 p-0 sm:max-w-[min(96rem,calc(100vw-2rem))]"
49
50
  >
50
51
  <div className="flex items-center justify-between gap-3 border-b border-border px-4 py-3">
51
52
  <DialogTitle className="min-w-0 truncate text-sm font-medium">
@@ -1,5 +1,5 @@
1
- $wireframe-bg: #ffffff;
2
- $wireframe-border: #edebeb;
1
+ $wireframe-bg: var(--tool-chrome-surface);
2
+ $wireframe-border: var(--tool-chrome-border);
3
3
  $enter-duration: 0.12s;
4
4
  $enter-ease: cubic-bezier(0.215, 0.61, 0.355, 1);
5
5
 
@@ -38,7 +38,7 @@ $enter-ease: cubic-bezier(0.215, 0.61, 0.355, 1);
38
38
  margin: 0;
39
39
  font-size: 0.6875rem;
40
40
  line-height: 1.45;
41
- color: var(--color-gray-500);
41
+ color: var(--tool-chrome-text-muted);
42
42
  white-space: pre-wrap;
43
43
  }
44
44
 
@@ -54,13 +54,13 @@ $enter-ease: cubic-bezier(0.215, 0.61, 0.355, 1);
54
54
  padding: 0.1875rem 0 0.1875rem 0.875rem;
55
55
  font-size: 0.6875rem;
56
56
  line-height: 1.45;
57
- color: var(--color-gray-500);
57
+ color: var(--tool-chrome-text-muted);
58
58
 
59
59
  &::before {
60
60
  content: "•";
61
61
  position: absolute;
62
62
  left: 0;
63
- color: var(--color-gray-400);
63
+ color: var(--tool-chrome-icon);
64
64
  }
65
65
 
66
66
  & + & {
@@ -78,12 +78,12 @@ $enter-ease: cubic-bezier(0.215, 0.61, 0.355, 1);
78
78
  @media (hover: hover) and (pointer: fine) {
79
79
  &:hover {
80
80
  background: color-mix(in srgb, var(--tool-chrome-blue) 6%, $wireframe-bg);
81
- color: var(--color-gray-700);
81
+ color: var(--tool-chrome-text-heading);
82
82
  }
83
83
  }
84
84
  }
85
85
 
86
86
  .bulletItemActive {
87
87
  background: color-mix(in srgb, var(--tool-chrome-blue) 8%, $wireframe-bg);
88
- color: var(--color-gray-700);
88
+ color: var(--tool-chrome-text-heading);
89
89
  }
@@ -0,0 +1,59 @@
1
+ "use client";
2
+
3
+ import { Button } from "@prototype/components/ui/button";
4
+ import { buildPopulateComponentLibraryCopyText } from "@prototype/lib/prototypes/populate-component-library-prompt";
5
+ import useCopyToClipboard from "@prototype/lib/use-copy-to-clipboard";
6
+
7
+ type PrototypeComponentLibraryEmptyStateProps = {
8
+ sourcePath?: string;
9
+ syncConfigPath?: string;
10
+ componentLibraryPagePath?: string;
11
+ };
12
+
13
+ export function PrototypeComponentLibraryEmptyState({
14
+ sourcePath,
15
+ syncConfigPath,
16
+ componentLibraryPagePath,
17
+ }: PrototypeComponentLibraryEmptyStateProps) {
18
+ const { copy, icon, isCopied } = useCopyToClipboard();
19
+
20
+ const handleCopy = () => {
21
+ const origin =
22
+ typeof window !== "undefined" ? window.location.origin : undefined;
23
+
24
+ copy(
25
+ buildPopulateComponentLibraryCopyText({
26
+ sourcePath,
27
+ syncConfigPath,
28
+ componentLibraryPagePath,
29
+ origin,
30
+ }),
31
+ );
32
+ };
33
+
34
+ return (
35
+ <div className="max-w-2xl rounded-xl border border-dashed border-[var(--tool-chrome-border)] bg-[var(--tool-chrome-surface-muted)] px-6 py-8 text-[var(--tool-chrome-text-muted)]">
36
+ <p className="text-sm font-medium text-[var(--tool-chrome-text-heading)]">
37
+ No components yet
38
+ </p>
39
+ <p className="mt-2 text-sm leading-relaxed">
40
+ Copy the prompt below into your agent. It will inspect the linked source
41
+ app and port over base styles, UI primitives, and shared building
42
+ blocks — err on the side of syncing more rather than less.
43
+ </p>
44
+ <div className="mt-5 flex items-center gap-3">
45
+ <Button
46
+ type="button"
47
+ variant="outline"
48
+ size="sm"
49
+ className="h-8 gap-1.5 border-[var(--tool-chrome-border-strong)] text-[var(--tool-chrome-text-heading)] hover:text-[var(--tool-chrome-text-heading)]"
50
+ onClick={handleCopy}
51
+ aria-label="Copy component library population prompt"
52
+ >
53
+ {isCopied ? "Copied prompt" : "Copy prompt"}
54
+ {icon}
55
+ </Button>
56
+ </div>
57
+ </div>
58
+ );
59
+ }
@@ -0,0 +1,124 @@
1
+ "use client";
2
+
3
+ import { IconButton } from "@prototype/components/platform-ui/icon-button";
4
+ import { Button } from "@prototype/components/ui/button";
5
+ import { PrototypeLinkSourceModal } from "@prototype/components/shell/prototype-link-source-modal";
6
+ import { cn } from "@prototype/lib/utils";
7
+ import { usePrototypeToolTheme } from "@prototype/lib/prototypes/use-prototype-tool-theme";
8
+ import { Moon, Pencil, Shapes, Sun, LayoutDashboard } from "lucide-react";
9
+ import Link from "next/link";
10
+ import { usePathname } from "next/navigation";
11
+ import { useState } from "react";
12
+
13
+ const GALLERY_NAV_ITEMS = [
14
+ {
15
+ href: "/",
16
+ label: "Prototypes",
17
+ icon: Shapes,
18
+ },
19
+ {
20
+ href: "/component-library",
21
+ label: "Component Library",
22
+ icon: LayoutDashboard,
23
+ },
24
+ ] as const;
25
+
26
+ type PrototypeGalleryNavProps = {
27
+ sourceDirectoryName?: string;
28
+ sourcePath?: string;
29
+ };
30
+
31
+ export function PrototypeGalleryNav({
32
+ sourceDirectoryName,
33
+ sourcePath,
34
+ }: PrototypeGalleryNavProps) {
35
+ const pathname = usePathname();
36
+ const { theme, toggleTheme } = usePrototypeToolTheme();
37
+ const [linkSourceOpen, setLinkSourceOpen] = useState(false);
38
+
39
+ return (
40
+ <aside className="border-[var(--tool-chrome-border)] bg-[var(--bg-ground)] flex h-full min-h-0 w-56 shrink-0 flex-col self-stretch overflow-hidden border-r">
41
+ <nav aria-label="Gallery" className="flex flex-col gap-1 p-3">
42
+ {GALLERY_NAV_ITEMS.map(({ href, label, icon: Icon }) => {
43
+ const isActive =
44
+ href === "/"
45
+ ? pathname === "/"
46
+ : pathname === href || pathname.startsWith(`${href}/`);
47
+
48
+ return (
49
+ <Link
50
+ key={href}
51
+ href={href}
52
+ aria-current={isActive ? "page" : undefined}
53
+ className={cn(
54
+ "flex items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium transition-colors",
55
+ isActive
56
+ ? "bg-[var(--tool-chrome-gray-highlight)] text-[var(--tool-chrome-text-heading)]"
57
+ : "text-[var(--tool-chrome-text-muted)] hover:bg-[var(--tool-chrome-gray-highlight)] hover:text-[var(--tool-chrome-text-heading)]",
58
+ )}
59
+ >
60
+ <Icon className="size-4 shrink-0" aria-hidden />
61
+ {label}
62
+ </Link>
63
+ );
64
+ })}
65
+ </nav>
66
+
67
+ <div className="border-[var(--tool-chrome-border)] mt-auto border-t p-3">
68
+ <div className="flex min-w-0 items-center gap-2">
69
+ <IconButton
70
+ type="button"
71
+ variant="ghost"
72
+ size="icon"
73
+ onClick={toggleTheme}
74
+ aria-label={
75
+ theme === "light" ? "Switch to dark mode" : "Switch to light mode"
76
+ }
77
+ className="size-8 shrink-0 text-[var(--tool-chrome-text-muted)] hover:text-[var(--tool-chrome-text-heading)]"
78
+ >
79
+ {theme === "light" ? (
80
+ <Moon size={16} strokeWidth={2} aria-hidden />
81
+ ) : (
82
+ <Sun size={16} strokeWidth={2} aria-hidden />
83
+ )}
84
+ </IconButton>
85
+ {sourceDirectoryName ? (
86
+ <>
87
+ <span
88
+ className="min-w-0 flex-1 truncate text-xs text-[var(--tool-chrome-text-muted)]"
89
+ title={sourcePath ?? sourceDirectoryName}
90
+ >
91
+ {sourceDirectoryName}
92
+ </span>
93
+ <IconButton
94
+ type="button"
95
+ variant="ghost"
96
+ size="icon"
97
+ onClick={() => setLinkSourceOpen(true)}
98
+ aria-label="Change linked source app"
99
+ className="size-7 shrink-0 text-[var(--tool-chrome-text-muted)] hover:text-[var(--tool-chrome-text-heading)]"
100
+ >
101
+ <Pencil size={14} strokeWidth={2} aria-hidden />
102
+ </IconButton>
103
+ </>
104
+ ) : (
105
+ <Button
106
+ type="button"
107
+ variant="outline"
108
+ onClick={() => setLinkSourceOpen(true)}
109
+ className="h-9 min-w-0 flex-1 justify-center rounded-lg text-sm font-medium"
110
+ >
111
+ Link source
112
+ </Button>
113
+ )}
114
+ </div>
115
+ </div>
116
+
117
+ <PrototypeLinkSourceModal
118
+ open={linkSourceOpen}
119
+ onOpenChange={setLinkSourceOpen}
120
+ currentSourcePath={sourcePath}
121
+ />
122
+ </aside>
123
+ );
124
+ }
@@ -1,33 +1,64 @@
1
1
  "use client";
2
2
 
3
3
  import { PrototypeProvider } from "@prototype/components/prototype-provider";
4
+ import { PrototypeGalleryNav } from "@prototype/components/shell/prototype-gallery-nav";
4
5
  import { cn } from "@prototype/lib/utils";
5
6
  import type { ReactNode } from "react";
6
7
 
7
8
  type PrototypeGalleryShellProps = {
8
9
  children: ReactNode;
9
10
  footer?: ReactNode;
11
+ sourceDirectoryName?: string;
12
+ sourcePath?: string;
10
13
  };
11
14
 
12
15
  export function PrototypeGalleryShell({
13
16
  children,
14
17
  footer,
18
+ sourceDirectoryName,
19
+ sourcePath,
15
20
  }: PrototypeGalleryShellProps) {
16
21
  return (
17
- <PrototypeProvider className="bg-[var(--bg-ground)] flex h-svh w-full flex-col overflow-hidden">
22
+ <PrototypeProvider className="bg-[var(--bg-main)] flex h-full min-h-0 w-full flex-1 flex-col overflow-hidden">
18
23
  {footer ? (
19
- <div className="border-border flex shrink-0 items-center justify-end border-b px-10 py-3 lg:px-16">
24
+ <div className="border-[var(--tool-chrome-border)] bg-[var(--bg-ground)] flex shrink-0 items-center justify-end border-b px-10 py-3 lg:px-16">
20
25
  {footer}
21
26
  </div>
22
27
  ) : null}
23
28
 
24
- <main className="bg-background min-h-0 flex-1 overflow-y-auto">
25
- {children}
26
- </main>
29
+ <div className="flex h-full min-h-0 flex-1 overflow-hidden">
30
+ <PrototypeGalleryNav
31
+ sourceDirectoryName={sourceDirectoryName}
32
+ sourcePath={sourcePath}
33
+ />
34
+ <main className="bg-[var(--bg-main)] flex min-h-0 flex-1 flex-col overflow-hidden">
35
+ {children}
36
+ </main>
37
+ </div>
27
38
  </PrototypeProvider>
28
39
  );
29
40
  }
30
41
 
42
+ type PrototypeGalleryPageLayoutProps = {
43
+ header: ReactNode;
44
+ children: ReactNode;
45
+ };
46
+
47
+ /** Gallery pages: header stays fixed; only body scrolls. */
48
+ export function PrototypeGalleryPageLayout({
49
+ header,
50
+ children,
51
+ }: PrototypeGalleryPageLayoutProps) {
52
+ return (
53
+ <div className="flex h-full min-h-0 flex-1 flex-col overflow-hidden">
54
+ {header}
55
+ <div className="bg-[var(--bg-main)] min-h-0 flex-1 overflow-y-auto overscroll-y-contain">
56
+ <div className="h-fit w-full">{children}</div>
57
+ </div>
58
+ </div>
59
+ );
60
+ }
61
+
31
62
  type PrototypeGalleryHeaderProps = {
32
63
  eyebrow?: string;
33
64
  title?: string;
@@ -44,17 +75,17 @@ export function PrototypeGalleryHeader({
44
75
  return (
45
76
  <div
46
77
  className={cn(
47
- "border-border -mx-10 mb-8 border-b px-10 py-6 lg:-mx-16 lg:px-16",
78
+ "border-[var(--tool-chrome-border)] bg-[var(--bg-ground)] shrink-0 border-b px-10 py-6 lg:px-16",
48
79
  className,
49
80
  )}
50
81
  >
51
82
  {eyebrow ? (
52
- <p className="text-xs text-muted-foreground">{eyebrow}</p>
83
+ <p className="text-xs text-[var(--tool-chrome-text-muted)]">{eyebrow}</p>
53
84
  ) : null}
54
- <h1 className="text-foreground mt-0 text-2xl font-medium tracking-tight">
85
+ <h1 className="mt-0 text-2xl font-medium tracking-tight text-[var(--tool-chrome-text-heading)]">
55
86
  {title}
56
87
  </h1>
57
- <p className="text-muted-foreground mt-2 max-w-2xl text-sm leading-relaxed">
88
+ <p className="mt-2 max-w-2xl text-sm leading-relaxed text-[var(--tool-chrome-text-muted)]">
58
89
  {description}
59
90
  </p>
60
91
  </div>
@@ -70,10 +101,11 @@ export function PrototypeGalleryPage({
70
101
  }) {
71
102
  return (
72
103
  <PrototypeGalleryShell footer={footer}>
73
- <div className="px-10 lg:px-16">
74
- {header ?? <PrototypeGalleryHeader />}
75
- {children}
76
- </div>
104
+ <PrototypeGalleryPageLayout
105
+ header={header ?? <PrototypeGalleryHeader className="mb-0" />}
106
+ >
107
+ <div className="px-10 pt-8 pb-16 lg:px-16">{children}</div>
108
+ </PrototypeGalleryPageLayout>
77
109
  </PrototypeGalleryShell>
78
110
  );
79
111
  }