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.
Files changed (109) hide show
  1. package/AGENTS.md +308 -29
  2. package/README.md +48 -6
  3. package/package.json +2 -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 +59 -55
  13. package/src/components/prototypes/prototype-comment-provider.tsx +0 -1
  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-chrome.tsx +21 -2
  24. package/src/components/prototypes/prototype-review-sidebar.tsx +7 -37
  25. package/src/components/prototypes/prototype-spec-panel-content.tsx +5 -5
  26. package/src/components/prototypes/prototype-spec-panel-empty-state.tsx +53 -0
  27. package/src/components/prototypes/prototype-state-canvas-overlay.module.scss +20 -47
  28. package/src/components/prototypes/prototype-state-canvas-overlay.tsx +4 -42
  29. package/src/components/prototypes/prototype-variant-explorer.tsx +32 -78
  30. package/src/components/prototypes/prototype-vercel-preview-modal.tsx +4 -3
  31. package/src/components/prototypes/state-map-annotation-tooltip.module.scss +7 -7
  32. package/src/components/shell/prototype-component-library-empty-state.tsx +59 -0
  33. package/src/components/shell/prototype-gallery-nav.tsx +124 -0
  34. package/src/components/shell/prototype-gallery-shell.tsx +45 -13
  35. package/src/components/shell/prototype-link-source-modal.tsx +100 -0
  36. package/src/components/shell/prototype-plugin-update-popup.tsx +74 -0
  37. package/src/components/ui/button.tsx +4 -2
  38. package/src/components/ui/dialog.tsx +44 -5
  39. package/src/components/ui/dropdown-menu.tsx +2 -2
  40. package/src/components/ui/input.tsx +1 -1
  41. package/src/components/ui/label.tsx +1 -1
  42. package/src/components/ui/option-select.tsx +3 -3
  43. package/src/components/ui/scroll-area.tsx +1 -1
  44. package/src/components/ui/separator.tsx +1 -1
  45. package/src/components/ui/slider.tsx +1 -1
  46. package/src/components/ui/switch.tsx +1 -1
  47. package/src/components/ui/tabs.tsx +1 -1
  48. package/src/components/ui/toggle-group.tsx +1 -1
  49. package/src/components/ui/tooltip.tsx +2 -2
  50. package/src/config/create-prototype-component-library-page.tsx +74 -0
  51. package/src/config/create-prototype-gallery-page.tsx +26 -23
  52. package/src/config/create-prototype-host-app.tsx +3 -15
  53. package/src/config/create-prototype-site-layout.tsx +14 -1
  54. package/src/config/create-prototype-state-map-page.tsx +44 -0
  55. package/src/config/prototype-site-layout-client.tsx +23 -6
  56. package/src/config/with-prototype.ts +54 -0
  57. package/src/index.ts +71 -11
  58. package/src/lib/copy-text-with-attachments.ts +35 -0
  59. package/src/lib/format-source-directory-name.ts +12 -0
  60. package/src/lib/platform-ui/sidebar-interactive-cursor.ts +19 -0
  61. package/src/lib/pr-split/build-pr-split-plan-prompt.ts +81 -0
  62. package/src/lib/pr-split/pr-split-highlight.ts +3 -0
  63. package/src/lib/prototype-comments/core/annotation-target.ts +12 -3
  64. package/src/lib/prototype-comments/core/comment-capture-blocked.ts +24 -0
  65. package/src/lib/prototype-comments/core/screenshot.ts +68 -31
  66. package/src/lib/prototype-comments/core/storage.ts +3 -6
  67. package/src/lib/prototype-comments/core/types.ts +2 -0
  68. package/src/lib/prototype-comments/hooks/useLayoutShift.ts +9 -2
  69. package/src/lib/prototype-comments/react/CommentCaptureToolbar.tsx +45 -10
  70. package/src/lib/prototype-comments/react/CommentProvider.tsx +19 -0
  71. package/src/lib/prototype-comments/ui/CommentModeToggleButton.tsx +34 -0
  72. package/src/lib/prototype-comments/ui/CommentsGrid.tsx +8 -16
  73. package/src/lib/prototype-comments/ui/CommentsSidebar.tsx +5 -19
  74. package/src/lib/prototype-comments/ui/capture-styles.module.scss +5 -1
  75. package/src/lib/prototype-comments/ui/comments-grid-card.module.scss +0 -7
  76. package/src/lib/prototypes/build-design-exploration-renderers.ts +44 -0
  77. package/src/lib/prototypes/changelog-description-prompt.ts +58 -0
  78. package/src/lib/prototypes/comment-storage-setup-prompt.ts +124 -0
  79. package/src/lib/prototypes/create-prototype-prompt.ts +96 -0
  80. package/src/lib/prototypes/create-prototype-registry.ts +5 -0
  81. package/src/lib/prototypes/design-exploration-types.ts +261 -10
  82. package/src/lib/prototypes/link-source-prompt.ts +93 -0
  83. package/src/lib/prototypes/populate-component-library-prompt.ts +64 -0
  84. package/src/lib/prototypes/proto-plugin-version.ts +19 -0
  85. package/src/lib/prototypes/prototype-config-types.ts +2 -0
  86. package/src/lib/prototypes/prototype-layout-classes.ts +19 -0
  87. package/src/lib/prototypes/prototype-preview-state-registry.ts +5 -2
  88. package/src/lib/prototypes/prototype-review-context.tsx +4 -4
  89. package/src/lib/prototypes/prototype-state-canvas-constants.ts +1 -1
  90. package/src/lib/prototypes/prototype-state-canvas-types.ts +0 -2
  91. package/src/lib/prototypes/prototype-storage-status.ts +31 -0
  92. package/src/lib/prototypes/sort-prototypes-by-creation-date.ts +43 -0
  93. package/src/lib/prototypes/upgrade-proto-plugin-prompt.ts +40 -0
  94. package/src/lib/prototypes/use-proto-plugin-version-check.ts +67 -0
  95. package/src/lib/prototypes/use-prototype-tool-theme.tsx +1 -1
  96. package/src/lib/prototypes/use-toolbar-hover-panel.ts +12 -1
  97. package/src/lib/prototypes/variant-set-lucide-icon.tsx +24 -0
  98. package/src/lib/tool-portal.ts +24 -0
  99. package/src/lib/use-copy-to-clipboard.ts +7 -13
  100. package/src/server/create-host-api-route.ts +14 -16
  101. package/src/server/create-prototype-api-routes.ts +0 -115
  102. package/src/server/proto-plugin-version-route.ts +145 -0
  103. package/src/server/prototype-storage-status-route.ts +29 -0
  104. package/src/server.ts +2 -8
  105. package/src/styles/globals.css +357 -69
  106. package/src/lib/prototypes/design-exploration-default-storage.ts +0 -68
  107. package/src/lib/prototypes/design-exploration-default.ts +0 -51
  108. package/src/lib/prototypes/use-design-exploration-default.ts +0 -114
  109. package/src/server/redis/prototype-design-exploration-default.ts +0 -32
@@ -0,0 +1,64 @@
1
+ const DEFAULT_SYNC_CONFIG_PATH = "prototype.sync.config.sh";
2
+ const DEFAULT_COMPONENT_LIBRARY_PAGE_PATH = "src/app/component-library/page.tsx";
3
+
4
+ export function buildPopulateComponentLibraryPrompt({
5
+ sourcePath,
6
+ syncConfigPath = DEFAULT_SYNC_CONFIG_PATH,
7
+ componentLibraryPagePath = DEFAULT_COMPONENT_LIBRARY_PAGE_PATH,
8
+ origin = "http://localhost:3003",
9
+ }: {
10
+ sourcePath?: string;
11
+ syncConfigPath?: string;
12
+ componentLibraryPagePath?: string;
13
+ origin?: string;
14
+ } = {}): string {
15
+ const sourceLine = sourcePath?.trim()
16
+ ? `- Source path: \`${sourcePath.trim()}\` (also readable via the \`source/\` symlink after \`pnpm link-source\`)`
17
+ : "- Source: set `SOURCE_PATH` in `.env.local`, run `pnpm link-source`, then read via the `source/` symlink";
18
+
19
+ const lines = [
20
+ "Populate the Component Library for this prototype host app.",
21
+ "",
22
+ "## Goal",
23
+ "The Component Library page is empty. Inspect the linked source application and extract all base styles and UI building blocks into this host repo.",
24
+ "",
25
+ "**Be generous — when in doubt, port more rather than less.** Prefer syncing whole directories and shared primitives over cherry-picking a minimal subset.",
26
+ "",
27
+ "## Source",
28
+ sourceLine,
29
+ "- Read the source app thoroughly before deciding what to skip.",
30
+ "",
31
+ "## What to extract (prioritize breadth)",
32
+ "1. Design tokens and global styles (`globals.css`, fonts, CSS variables, theme layers)",
33
+ "2. All shadcn/base UI primitives under `src/components/ui/`",
34
+ "3. Shared layout primitives, form patterns, badges, cards, tables, dialogs, and other reusable chrome",
35
+ "4. Shared utilities used by UI (`src/lib/utils.ts`, `cn` helpers, formatters, small hooks)",
36
+ "5. Composite components reused across multiple product surfaces",
37
+ "6. If the source organizes design-system pieces elsewhere (monorepo packages, shared folders), trace what product pages actually import and replicate those pieces here",
38
+ "",
39
+ "## How to sync",
40
+ "1. Ensure `SOURCE_PATH` is set in `.env.local` and run `pnpm link-source`.",
41
+ `2. Update \`${syncConfigPath}\` — add paths to \`SYNC_FILES\`, \`SYNC_DIRS\`, and/or \`SYNC_GLOBS\`. Prefer syncing whole directories (e.g. \`src/components/ui\`) over individual files.`,
42
+ "3. Run `pnpm sync-from-source`.",
43
+ "4. Re-apply any entries listed in `SYNC_LOCAL_EXTENSIONS` if this host customizes beyond source.",
44
+ "",
45
+ "## Component Library page",
46
+ `After syncing, wire live previews on \`${componentLibraryPagePath}\` via \`createPrototypeComponentLibraryPage\` so each primitive and composite is visible with common variants and states.`,
47
+ "",
48
+ `Gallery: ${origin}/component-library`,
49
+ "",
50
+ "## Rules",
51
+ "- Follow AGENTS.md — no runtime imports from `source/`; replicate into the host tree.",
52
+ "- Preserve semantic tokens from `globals.css`; avoid hardcoded hex.",
53
+ "- Confirm components render correctly on `/component-library` before finishing.",
54
+ ];
55
+
56
+ return lines.join("\n");
57
+ }
58
+
59
+ export function buildPopulateComponentLibraryCopyText(
60
+ options: Parameters<typeof buildPopulateComponentLibraryPrompt>[0] = {},
61
+ ): string {
62
+ const prompt = buildPopulateComponentLibraryPrompt(options);
63
+ return prompt;
64
+ }
@@ -0,0 +1,19 @@
1
+ export type ProtoPluginVersionStatus = {
2
+ installed: string | null;
3
+ latest: string | null;
4
+ updateAvailable: boolean;
5
+ isWorkspaceLink: boolean;
6
+ repositoryUrl?: string;
7
+ };
8
+
9
+ export const PROTOTYPE_PLUGIN_VERSION_PATH = "/api/proto-plugin/version";
10
+
11
+ export async function fetchProtoPluginVersionStatus(): Promise<ProtoPluginVersionStatus> {
12
+ const response = await fetch(PROTOTYPE_PLUGIN_VERSION_PATH);
13
+
14
+ if (!response.ok) {
15
+ throw new Error("Failed to check proto-plugin version.");
16
+ }
17
+
18
+ return (await response.json()) as ProtoPluginVersionStatus;
19
+ }
@@ -9,6 +9,8 @@ export type PrototypeDefinition = {
9
9
  screenshot: string;
10
10
  component: ComponentType;
11
11
  componentRegistry: PrototypeComponentRegistry;
12
+ /** Full-page state map at `/prototypes/<slug>/states`. Omit to show the default empty map. */
13
+ stateMapComponent?: ComponentType;
12
14
  };
13
15
 
14
16
  export type PrototypeExtraRoute = {
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Canonical Tailwind fill classes for prototype pages inside #prototype-viewport.
3
+ * The preview shell only auto-sizes direct children of .viewportPage — every nested
4
+ * layer must repeat this chain or content pins to a corner and clips.
5
+ *
6
+ * @see packages/prototype/AGENTS.md — "Prototype viewport fill"
7
+ */
8
+
9
+ /** `PrototypeComponent` with id="scroll-container" */
10
+ export const PROTOTYPE_SCROLL_CONTAINER_CLASS =
11
+ "flex h-full min-h-0 flex-1 flex-col overflow-hidden";
12
+
13
+ /** `PrototypeComponent` with id="page" */
14
+ export const PROTOTYPE_PAGE_CLASS =
15
+ "relative flex h-full min-h-0 flex-1 flex-col overflow-hidden";
16
+
17
+ /** Step/view roots, AnimatePresence wrappers, motion.div page shells */
18
+ export const PROTOTYPE_VIEW_SHELL_CLASS =
19
+ "flex h-full min-h-0 w-full flex-1 flex-col overflow-hidden";
@@ -212,8 +212,11 @@ export function buildPreviewStateCanvasLayout<T extends string>(
212
212
  };
213
213
  }
214
214
 
215
- function resolvePreviewStateRegistryLayout<TPrimary extends string>(
216
- registry: PrototypePreviewStateRegistry<TPrimary>,
215
+ function resolvePreviewStateRegistryLayout<
216
+ TPrimary extends string,
217
+ TVariant extends string = never,
218
+ >(
219
+ registry: PrototypePreviewStateRegistry<TPrimary, TVariant>,
217
220
  ): {
218
221
  states: ResolvedPrototypePreviewStateDefinition<TPrimary>[];
219
222
  sections: PrototypeStateCanvasSection[];
@@ -732,8 +732,8 @@ export function PrototypeReviewProvider({
732
732
  showProdReference,
733
733
  ]);
734
734
 
735
- const value = useMemo(
736
- () => ({
735
+ const value = useMemo(() => {
736
+ return {
737
737
  slug,
738
738
  open,
739
739
  sidebarPanel,
@@ -806,8 +806,8 @@ export function PrototypeReviewProvider({
806
806
  viewportLayout,
807
807
  setViewportLayout,
808
808
  toggleViewportLayout,
809
- }),
810
- [
809
+ };
810
+ }, [
811
811
  slug,
812
812
  open,
813
813
  sidebarPanel,
@@ -85,7 +85,7 @@ export function getStateCanvasNodeHeight(
85
85
  const count = callouts?.length ?? 0;
86
86
  if (count === 0) return bodyHeight;
87
87
 
88
- const calloutsHeight = callouts.reduce((sum, callout, index) => {
88
+ const calloutsHeight = (callouts ?? []).reduce((sum, callout, index) => {
89
89
  const gap = index > 0 ? PROTOTYPE_STATE_NODE_CALLOUT_GAP : 0;
90
90
  return sum + gap + getStateCanvasCalloutHeight(callout);
91
91
  }, 0);
@@ -75,8 +75,6 @@ export type StateMapHighlightRect = {
75
75
  };
76
76
 
77
77
  export type PrototypeStateCanvasConfig<T extends string = string> = {
78
- /** When omitted, no node appears selected — the map is a stateless picker. */
79
- activeStateId?: T;
80
78
  onStateSelect: (id: T) => void;
81
79
  nodes: PrototypeStateCanvasNode<T>[];
82
80
  edges: PrototypeStateCanvasEdge<T>[];
@@ -0,0 +1,31 @@
1
+ export type PrototypeStorageStatusSlice = {
2
+ configured: boolean;
3
+ missing: string[];
4
+ };
5
+
6
+ export type PrototypeStorageStatus = {
7
+ comments: PrototypeStorageStatusSlice;
8
+ gallery: {
9
+ kvConfigured: boolean;
10
+ storageConfigured: boolean;
11
+ missing: string[];
12
+ };
13
+ };
14
+
15
+ export const PROTOTYPE_STORAGE_STATUS_PATH = "/api/prototypes/storage-status";
16
+
17
+ export async function fetchPrototypeStorageStatus(): Promise<PrototypeStorageStatus> {
18
+ const response = await fetch(PROTOTYPE_STORAGE_STATUS_PATH);
19
+
20
+ if (!response.ok) {
21
+ throw new Error("Failed to check prototype storage status.");
22
+ }
23
+
24
+ return (await response.json()) as PrototypeStorageStatus;
25
+ }
26
+
27
+ export function isPrototypeStorageFullyConfigured(
28
+ status: PrototypeStorageStatus,
29
+ ): boolean {
30
+ return status.comments.configured && status.gallery.storageConfigured;
31
+ }
@@ -0,0 +1,43 @@
1
+ import { stat } from "fs/promises";
2
+ import path from "path";
3
+
4
+ import type { PrototypeMetadata } from "./prototype-config-types";
5
+
6
+ async function getPrototypeCreationMs(
7
+ slug: string,
8
+ configIndex: number,
9
+ ): Promise<number> {
10
+ try {
11
+ const stats = await stat(
12
+ path.join(process.cwd(), "src/prototypes", slug),
13
+ );
14
+ if (stats.birthtimeMs > 0) return stats.birthtimeMs;
15
+ } catch {
16
+ // Prototype directory may not exist in some host layouts.
17
+ }
18
+
19
+ // Config order is append-only — use registration index when birthtime is unavailable.
20
+ return configIndex;
21
+ }
22
+
23
+ export async function sortPrototypesByCreationDate(
24
+ prototypes: PrototypeMetadata[],
25
+ ): Promise<PrototypeMetadata[]> {
26
+ const creationMsBySlug = new Map<string, number>();
27
+
28
+ await Promise.all(
29
+ prototypes.map(async (prototype, configIndex) => {
30
+ creationMsBySlug.set(
31
+ prototype.slug,
32
+ await getPrototypeCreationMs(prototype.slug, configIndex),
33
+ );
34
+ }),
35
+ );
36
+
37
+ return [...prototypes].sort((a, b) => {
38
+ const creationDiff =
39
+ (creationMsBySlug.get(b.slug) ?? 0) - (creationMsBySlug.get(a.slug) ?? 0);
40
+ if (creationDiff !== 0) return creationDiff;
41
+ return a.title.localeCompare(b.title);
42
+ });
43
+ }
@@ -0,0 +1,40 @@
1
+ export type UpgradeProtoPluginPromptOptions = {
2
+ installed: string;
3
+ latest: string;
4
+ };
5
+
6
+ export function buildUpgradeProtoPluginPrompt({
7
+ installed,
8
+ latest,
9
+ }: UpgradeProtoPluginPromptOptions): string {
10
+ return [
11
+ `Upgrade \`proto-plugin\` in this host app from \`${installed}\` to \`${latest}\`. **Do every step yourself** — the user should not run commands manually.`,
12
+ "",
13
+ "## Your job",
14
+ `Update the dependency, restart the dev server if needed, and confirm the gallery and at least one prototype still work on \`${latest}\`.`,
15
+ "",
16
+ "## Versions",
17
+ `- Installed: \`${installed}\``,
18
+ `- Latest: \`${latest}\``,
19
+ "",
20
+ "## Steps",
21
+ "1. Detect the package manager from the lockfile (`pnpm-lock.yaml`, `package-lock.json`, or `yarn.lock`).",
22
+ "2. Update the dependency:",
23
+ ` - pnpm: \`pnpm update proto-plugin\` or \`pnpm add proto-plugin@${latest}\``,
24
+ ` - npm: \`npm install proto-plugin@${latest}\``,
25
+ ` - yarn: \`yarn add proto-plugin@${latest}\``,
26
+ "3. Restart the dev server if it is already running.",
27
+ `4. Skim \`node_modules/proto-plugin/AGENTS.md\` (or the package changelog on npm) for breaking changes since \`${installed}\`.`,
28
+ "5. Open `/` and one prototype preview — confirm the gallery and review chrome load without errors.",
29
+ "",
30
+ "## Notes",
31
+ "- Do not modify prototype source under `src/prototypes/` unless AGENTS.md or the changelog requires migration steps for this upgrade.",
32
+ "- If the update fails (peer dependency conflicts, build errors), diagnose and fix before stopping.",
33
+ ].join("\n");
34
+ }
35
+
36
+ export function buildUpgradeProtoPluginCopyText(
37
+ options: UpgradeProtoPluginPromptOptions,
38
+ ): string {
39
+ return buildUpgradeProtoPluginPrompt(options);
40
+ }
@@ -0,0 +1,67 @@
1
+ "use client";
2
+
3
+ import {
4
+ fetchProtoPluginVersionStatus,
5
+ type ProtoPluginVersionStatus,
6
+ } from "@prototype/lib/prototypes/proto-plugin-version";
7
+ import { usePersistedLocalString } from "@prototype/lib/prototypes/use-persisted-local-state";
8
+ import { useCallback, useEffect, useMemo, useState } from "react";
9
+
10
+ const DISMISSED_VERSION_KEY = "prototype-review:global:dismissed-plugin-version";
11
+
12
+ export function useProtoPluginVersionCheck() {
13
+ const [status, setStatus] = useState<ProtoPluginVersionStatus | null>(null);
14
+ const [isLoading, setIsLoading] = useState(true);
15
+ const { value: dismissedVersion, updateValue: setDismissedVersion } =
16
+ usePersistedLocalString(DISMISSED_VERSION_KEY, "");
17
+
18
+ useEffect(() => {
19
+ let cancelled = false;
20
+
21
+ void (async () => {
22
+ try {
23
+ const nextStatus = await fetchProtoPluginVersionStatus();
24
+ if (!cancelled) {
25
+ setStatus(nextStatus);
26
+ }
27
+ } catch {
28
+ if (!cancelled) {
29
+ setStatus(null);
30
+ }
31
+ } finally {
32
+ if (!cancelled) {
33
+ setIsLoading(false);
34
+ }
35
+ }
36
+ })();
37
+
38
+ return () => {
39
+ cancelled = true;
40
+ };
41
+ }, []);
42
+
43
+ const dismiss = useCallback(() => {
44
+ if (status?.latest) {
45
+ setDismissedVersion(status.latest);
46
+ }
47
+ }, [setDismissedVersion, status?.latest]);
48
+
49
+ const shouldShowPopup = useMemo(() => {
50
+ if (isLoading || !status?.updateAvailable || !status.latest || !status.installed) {
51
+ return false;
52
+ }
53
+
54
+ if (!dismissedVersion) {
55
+ return true;
56
+ }
57
+
58
+ return dismissedVersion !== status.latest;
59
+ }, [dismissedVersion, isLoading, status]);
60
+
61
+ return {
62
+ status,
63
+ shouldShowPopup,
64
+ dismiss,
65
+ isLoading,
66
+ };
67
+ }
@@ -28,7 +28,7 @@ type PrototypeToolThemeContextValue = {
28
28
  const PrototypeToolThemeContext =
29
29
  createContext<PrototypeToolThemeContextValue | null>(null);
30
30
 
31
- const DEFAULT_THEME: PrototypeToolTheme = "dark";
31
+ const DEFAULT_THEME: PrototypeToolTheme = "light";
32
32
 
33
33
  function normalizeTheme(value: string): PrototypeToolTheme {
34
34
  return value === "light" ? "light" : "dark";
@@ -8,12 +8,21 @@ import type {
8
8
 
9
9
  export const TOOLBAR_PANEL_HOVER_OPEN_DELAY_MS = 100;
10
10
  export const TOOLBAR_PANEL_HOVER_CLOSE_DELAY_MS = 150;
11
+ export const TOOLBAR_HOVER_CAPABLE_MEDIA_QUERY =
12
+ "(hover: hover) and (pointer: fine)";
13
+
14
+ export function supportsToolbarHover() {
15
+ if (typeof window === "undefined") return true;
16
+ return window.matchMedia(TOOLBAR_HOVER_CAPABLE_MEDIA_QUERY).matches;
17
+ }
11
18
 
12
19
  export function preventToolbarHoverButtonActivation(
13
20
  event:
14
21
  | ReactMouseEvent<HTMLButtonElement>
15
22
  | ReactPointerEvent<HTMLButtonElement>,
16
23
  ) {
24
+ if (!supportsToolbarHover()) return;
25
+
17
26
  event.preventDefault();
18
27
  event.stopPropagation();
19
28
  }
@@ -59,7 +68,7 @@ export function useToolbarHoverPanel({
59
68
 
60
69
  const scheduleOpen = useCallback(
61
70
  (event: ReactPointerEvent<HTMLElement>) => {
62
- if (event.pointerType === "touch") return;
71
+ if (!supportsToolbarHover() || event.pointerType === "touch") return;
63
72
 
64
73
  clearCloseTimer();
65
74
  clearOpenTimer();
@@ -71,6 +80,8 @@ export function useToolbarHoverPanel({
71
80
  );
72
81
 
73
82
  const scheduleClose = useCallback(() => {
83
+ if (!supportsToolbarHover()) return;
84
+
74
85
  clearOpenTimer();
75
86
  clearCloseTimer();
76
87
  closeTimerRef.current = setTimeout(() => {
@@ -1,14 +1,24 @@
1
1
  import type { LucideIcon, LucideProps } from "lucide-react";
2
2
  import {
3
3
  ArrowDownUp,
4
+ BadgeCheck,
4
5
  CalendarDays,
5
6
  Clock,
6
7
  FilterX,
8
+ Hexagon,
7
9
  Inbox,
8
10
  LayoutGrid,
11
+ LayoutList,
12
+ LayoutTemplate,
13
+ PanelsTopLeft,
14
+ MessageSquareText,
15
+ Palette,
9
16
  PanelBottom,
17
+ PanelBottomOpen,
18
+ Send,
10
19
  Shapes,
11
20
  Smartphone,
21
+ Sparkles,
12
22
  Table,
13
23
  } from "lucide-react";
14
24
 
@@ -19,6 +29,20 @@ type VariantSetIconRule = {
19
29
 
20
30
  /** First match wins — order rules from most specific to broadest. */
21
31
  const VARIANT_SET_ICON_RULES: VariantSetIconRule[] = [
32
+ { test: /event-types-page-layout|event types page layout/i, icon: PanelsTopLeft },
33
+ // proto-partner-page explorations
34
+ { test: /creator-attribution|creator attribution/i, icon: BadgeCheck },
35
+ { test: /submit-modal|submit modal/i, icon: Send },
36
+ { test: /mobile-panel-motion|mobile panel motion/i, icon: PanelBottomOpen },
37
+ { test: /mobile-picker-layout|mobile picker layout/i, icon: LayoutList },
38
+ {
39
+ test: /invite-follow-up-shape-layout|follow-up shape layout/i,
40
+ icon: LayoutTemplate,
41
+ },
42
+ { test: /invite-animations|invite animations/i, icon: Sparkles },
43
+ { test: /shape-color-picker|shape & color picker/i, icon: Palette },
44
+ { test: /proto-shapes|proto shapes/i, icon: Hexagon },
45
+ { test: /invite-copy|invite copy/i, icon: MessageSquareText },
22
46
  { test: /retention|expir|before/i, icon: Clock },
23
47
  { test: /calendar|date.?picker|date-range/i, icon: CalendarDays },
24
48
  { test: /mobile/i, icon: Smartphone },
@@ -8,6 +8,9 @@ export const PROTOTYPE_VIEWPORT_ID = "prototype-viewport";
8
8
 
9
9
  export const PROTOTYPE_SCREENSHOT_ATTR = "data-prototype-screenshot";
10
10
 
11
+ /** Synced source-app previews (component library, etc.) — fixed product tokens, not tool theme. */
12
+ export const PROTOTYPE_SOURCE_SURFACE_ATTR = "data-prototype-source-surface";
13
+
11
14
  export const PROTOTYPE_COMMENTS_SIDEBAR_ROOT_ID =
12
15
  "prototype-comments-sidebar-root";
13
16
 
@@ -16,6 +19,27 @@ export function getPrototypePortalContainer(): HTMLElement | undefined {
16
19
  return document.getElementById(PROTOTYPE_ROOT_ID) ?? undefined;
17
20
  }
18
21
 
22
+ /** Dialogs/overlays inside prototype content — scoped to the preview viewport, not the review sidebar. */
23
+ export function getPrototypeDialogPortalContainer(): HTMLElement | undefined {
24
+ if (typeof document === "undefined") return undefined;
25
+ return (
26
+ document.getElementById(PROTOTYPE_VIEWPORT_ID) ??
27
+ document.getElementById(PROTOTYPE_PREVIEW_STAGE_ID) ??
28
+ document.getElementById(PROTOTYPE_ROOT_ID) ??
29
+ undefined
30
+ );
31
+ }
32
+
33
+ /** Tool chrome modals (prompt copy, link source, etc.) — outside the screenshot surface so dark/light tool theme applies. */
34
+ export function getPrototypeToolDialogPortalContainer(): HTMLElement | undefined {
35
+ if (typeof document === "undefined") return undefined;
36
+ return (
37
+ document.getElementById(PROTOTYPE_ROOT_ID) ??
38
+ document.getElementById(PROTOTYPE_PREVIEW_STAGE_ID) ??
39
+ undefined
40
+ );
41
+ }
42
+
19
43
  export function getPrototypePreviewStage(): HTMLElement | undefined {
20
44
  if (typeof document === "undefined") return undefined;
21
45
  return document.getElementById(PROTOTYPE_PREVIEW_STAGE_ID) ?? undefined;
@@ -1,9 +1,9 @@
1
1
  "use client";
2
2
 
3
+ import { copyTextWithAttachments } from "@prototype/lib/copy-text-with-attachments";
3
4
  import { cn } from "@prototype/lib/utils";
4
5
  import { CheckIcon, CopyIcon, SaveIcon } from "lucide-react";
5
6
  import { createElement, useCallback, useRef, useState } from "react";
6
- import { toast } from "sonner";
7
7
 
8
8
  type CopyState = "idle" | "scaling-out" | "copied" | "reverting-out" | null;
9
9
 
@@ -25,7 +25,7 @@ export default function useCopyToClipboard({
25
25
  const isCopied = state === "copied" || state === "reverting-out";
26
26
 
27
27
  const copy = useCallback(
28
- (text: string) => {
28
+ async (text: string) => {
29
29
  clearTimeout(timerRef.current);
30
30
  setState("idle");
31
31
 
@@ -43,17 +43,11 @@ export default function useCopyToClipboard({
43
43
  timerRef.current = setTimeout(revert, cooldownTime);
44
44
  };
45
45
 
46
- navigator.clipboard
47
- .writeText(text)
48
- .catch(() => {
49
- toast.error("Copy error", {
50
- description: "Failed to copy value to clipboard",
51
- });
52
- })
53
- .finally(() => {
54
- setState("scaling-out");
55
- timerRef.current = setTimeout(copied, SCALE_OUT_MS);
56
- });
46
+ const didCopy = await copyTextWithAttachments(text);
47
+ if (!didCopy) return;
48
+
49
+ setState("scaling-out");
50
+ timerRef.current = setTimeout(copied, SCALE_OUT_MS);
57
51
  },
58
52
  [cooldownTime],
59
53
  );
@@ -6,10 +6,11 @@ import {
6
6
  createPrototypeChangelogMetaRoute,
7
7
  createPrototypeCommentIdRoute,
8
8
  createPrototypeCommentsRoute,
9
- createPrototypeDesignExplorationDefaultRoute,
10
9
  createPrototypeScreenshotRoute,
11
10
  } from "./create-prototype-api-routes";
12
11
  import { createGalleryApi } from "./gallery/create-gallery-api";
12
+ import { GET as getPrototypeStorageStatus } from "./prototype-storage-status-route";
13
+ import { GET as getProtoPluginVersion } from "./proto-plugin-version-route";
13
14
  import { GET as getPrVercelPreview } from "./pr-vercel-preview-route";
14
15
 
15
16
  type ApiRouteContext = {
@@ -20,8 +21,6 @@ export function createPrototypeApiRoute(_config: PrototypeConfig) {
20
21
  const commentsRoute = createPrototypeCommentsRoute(_config);
21
22
  const commentIdRoute = createPrototypeCommentIdRoute(_config);
22
23
  const changelogMetaRoute = createPrototypeChangelogMetaRoute(_config);
23
- const designExplorationDefaultRoute =
24
- createPrototypeDesignExplorationDefaultRoute(_config);
25
24
  const screenshotRoute = createPrototypeScreenshotRoute(_config);
26
25
  const galleryApi =
27
26
  _config.gallery === false ? null : createGalleryApi(_config.gallery);
@@ -44,6 +43,18 @@ export function createPrototypeApiRoute(_config: PrototypeConfig) {
44
43
  }
45
44
  }
46
45
 
46
+ if (path[0] === "proto-plugin" && path.length === 2 && path[1] === "version") {
47
+ if (method === "GET") {
48
+ return getProtoPluginVersion();
49
+ }
50
+ }
51
+
52
+ if (path[0] === "prototypes" && path.length === 2 && path[1] === "storage-status") {
53
+ if (method === "GET") {
54
+ return getPrototypeStorageStatus();
55
+ }
56
+ }
57
+
47
58
  if (path[0] === "prototypes" && path.length >= 2) {
48
59
  const slug = path[1]!;
49
60
 
@@ -72,19 +83,6 @@ export function createPrototypeApiRoute(_config: PrototypeConfig) {
72
83
  }
73
84
  }
74
85
 
75
- if (path.length === 3 && path[2] === "design-exploration-default") {
76
- if (method === "GET") {
77
- return designExplorationDefaultRoute.GET(request, {
78
- params: Promise.resolve({ slug }),
79
- });
80
- }
81
- if (method === "PUT") {
82
- return designExplorationDefaultRoute.PUT(request, {
83
- params: Promise.resolve({ slug }),
84
- });
85
- }
86
- }
87
-
88
86
  if (path.length === 3 && path[2] === "screenshot" && method === "POST") {
89
87
  return screenshotRoute.POST(request, { params: Promise.resolve({ slug }) });
90
88
  }