proto-plugin 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +350 -0
- package/README.md +279 -0
- package/components.json +18 -0
- package/package.json +73 -0
- package/scripts/download-mobbin-reference-images.mjs +112 -0
- package/scripts/lib/host-config.example.sh +48 -0
- package/scripts/lib/host-config.sh +25 -0
- package/scripts/lib/resolve-host-root.mjs +18 -0
- package/scripts/lib/source-path.sh +49 -0
- package/scripts/link-source-db.sh +128 -0
- package/scripts/link-source.sh +40 -0
- package/scripts/publish-package.sh +147 -0
- package/scripts/share-prototype.sh +279 -0
- package/scripts/sync-from-source.sh +74 -0
- package/scripts/verify-prototype-component-ids.mjs +151 -0
- package/scripts/verify-prototype-preview-states.mjs +135 -0
- package/src/app.ts +4 -0
- package/src/components/platform-ui/code-block.tsx +21 -0
- package/src/components/platform-ui/controls-panel-options.tsx +71 -0
- package/src/components/platform-ui/controls-panel-select.tsx +438 -0
- package/src/components/platform-ui/empty-state.tsx +15 -0
- package/src/components/platform-ui/icon-button.tsx +18 -0
- package/src/components/platform-ui/panel-select.tsx +123 -0
- package/src/components/platform-ui/panel.tsx +97 -0
- package/src/components/platform-ui/review-dropdown-menu.tsx +35 -0
- package/src/components/platform-ui/sidebar.tsx +293 -0
- package/src/components/platform-ui/toolbar-icon-button.tsx +34 -0
- package/src/components/platform-ui/toolbar.tsx +37 -0
- package/src/components/prototype-gallery-client.tsx +57 -0
- package/src/components/prototype-provider.tsx +20 -0
- package/src/components/prototypes/mini-pill-label.tsx +37 -0
- package/src/components/prototypes/prototype-change-log-panel.tsx +207 -0
- package/src/components/prototypes/prototype-comment-provider.tsx +61 -0
- package/src/components/prototypes/prototype-comment-toolbar.tsx +65 -0
- package/src/components/prototypes/prototype-control.tsx +31 -0
- package/src/components/prototypes/prototype-controls.tsx +553 -0
- package/src/components/prototypes/prototype-create-exploration-modal.tsx +117 -0
- package/src/components/prototypes/prototype-create-state-modal.tsx +117 -0
- package/src/components/prototypes/prototype-default-state-map-page.tsx +66 -0
- package/src/components/prototypes/prototype-design-brief-modal.tsx +128 -0
- package/src/components/prototypes/prototype-design-brief-panel.tsx +525 -0
- package/src/components/prototypes/prototype-design-context-panel.tsx +65 -0
- package/src/components/prototypes/prototype-floating-pill.module.scss +1341 -0
- package/src/components/prototypes/prototype-mobbin-gallery.tsx +330 -0
- package/src/components/prototypes/prototype-reference-docs.tsx +222 -0
- package/src/components/prototypes/prototype-review-chrome.module.scss +630 -0
- package/src/components/prototypes/prototype-review-chrome.tsx +1241 -0
- package/src/components/prototypes/prototype-review-dropdown-menu-item.tsx +1 -0
- package/src/components/prototypes/prototype-review-dropdown-menu-separator.tsx +1 -0
- package/src/components/prototypes/prototype-review-panel-menu-item.tsx +1 -0
- package/src/components/prototypes/prototype-review-panel-menu-separator.tsx +1 -0
- package/src/components/prototypes/prototype-review-panel-select.tsx +4 -0
- package/src/components/prototypes/prototype-review-sidebar.tsx +167 -0
- package/src/components/prototypes/prototype-share-link-restore.tsx +75 -0
- package/src/components/prototypes/prototype-share-mode.tsx +206 -0
- package/src/components/prototypes/prototype-spec-panel-content.tsx +538 -0
- package/src/components/prototypes/prototype-state-canvas-overlay.module.scss +336 -0
- package/src/components/prototypes/prototype-state-canvas-overlay.tsx +1023 -0
- package/src/components/prototypes/prototype-state-canvas.tsx +38 -0
- package/src/components/prototypes/prototype-state-screenshot-preview.tsx +47 -0
- package/src/components/prototypes/prototype-target.tsx +120 -0
- package/src/components/prototypes/prototype-tweak-highlight.tsx +132 -0
- package/src/components/prototypes/prototype-tweaks.tsx +48 -0
- package/src/components/prototypes/prototype-variant-explorer.tsx +347 -0
- package/src/components/prototypes/prototype-variant-rationale-panel.tsx +49 -0
- package/src/components/prototypes/prototype-variant-tabs.tsx +207 -0
- package/src/components/prototypes/prototype-vercel-preview-modal.tsx +120 -0
- package/src/components/prototypes/state-map-annotation-tooltip.module.scss +89 -0
- package/src/components/prototypes/state-map-annotation-tooltip.tsx +137 -0
- package/src/components/prototypes/state-map-wireframe-highlight.module.scss +25 -0
- package/src/components/prototypes/state-map-wireframe-highlight.tsx +82 -0
- package/src/components/shell/prototype-gallery-shell.tsx +79 -0
- package/src/components/shell/prototype-screenshot-capture.tsx +45 -0
- package/src/components/shell/prototype-shell.module.scss +194 -0
- package/src/components/shell/prototype-shell.tsx +160 -0
- package/src/components/shell/prototype-state-screenshot-capture.tsx +68 -0
- package/src/components/ui/button.tsx +58 -0
- package/src/components/ui/dialog.module.scss +81 -0
- package/src/components/ui/dialog.tsx +127 -0
- package/src/components/ui/dropdown-menu.tsx +200 -0
- package/src/components/ui/input.tsx +22 -0
- package/src/components/ui/label.tsx +26 -0
- package/src/components/ui/option-select.tsx +81 -0
- package/src/components/ui/scroll-area.tsx +48 -0
- package/src/components/ui/separator.tsx +31 -0
- package/src/components/ui/slider.tsx +28 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/tabs.tsx +55 -0
- package/src/components/ui/toggle-group.tsx +80 -0
- package/src/components/ui/tooltip.tsx +32 -0
- package/src/config/create-prototype-gallery-page.tsx +65 -0
- package/src/config/create-prototype-host-app.tsx +121 -0
- package/src/config/create-prototype-page.tsx +41 -0
- package/src/config/create-prototype-site-layout.tsx +28 -0
- package/src/config/define-prototype-config.ts +7 -0
- package/src/config/prototype-icon.tsx +25 -0
- package/src/config/prototype-site-layout-client.tsx +35 -0
- package/src/config/with-prototype.ts +20 -0
- package/src/index.ts +303 -0
- package/src/lib/pr-split/build-pr-agent-prompt.ts +68 -0
- package/src/lib/pr-split/build-pr-split-prototype-url.ts +12 -0
- package/src/lib/pr-split/pr-split-highlight.ts +6 -0
- package/src/lib/pr-split/pr-split-types.ts +58 -0
- package/src/lib/pr-split/resolve-source-preview-url.ts +45 -0
- package/src/lib/pr-split/use-pr-vercel-previews.ts +69 -0
- package/src/lib/prototype-comments/core/annotation-channel.ts +41 -0
- package/src/lib/prototype-comments/core/annotation-status.ts +60 -0
- package/src/lib/prototype-comments/core/annotation-target.ts +567 -0
- package/src/lib/prototype-comments/core/capture-theme.ts +41 -0
- package/src/lib/prototype-comments/core/capture.ts +11 -0
- package/src/lib/prototype-comments/core/comment-capture-blocked.ts +47 -0
- package/src/lib/prototype-comments/core/comment-threads.ts +52 -0
- package/src/lib/prototype-comments/core/element-identification.ts +615 -0
- package/src/lib/prototype-comments/core/export.ts +35 -0
- package/src/lib/prototype-comments/core/freeze-animations.ts +266 -0
- package/src/lib/prototype-comments/core/identify-element.ts +76 -0
- package/src/lib/prototype-comments/core/normalize-viewport.ts +35 -0
- package/src/lib/prototype-comments/core/react-detection.ts +704 -0
- package/src/lib/prototype-comments/core/restore.ts +7 -0
- package/src/lib/prototype-comments/core/screenshot.ts +428 -0
- package/src/lib/prototype-comments/core/source-location.ts +906 -0
- package/src/lib/prototype-comments/core/storage.ts +80 -0
- package/src/lib/prototype-comments/core/types.ts +79 -0
- package/src/lib/prototype-comments/core/validation.ts +17 -0
- package/src/lib/prototype-comments/hooks/useLayoutShift.ts +61 -0
- package/src/lib/prototype-comments/index.ts +58 -0
- package/src/lib/prototype-comments/react/CommentCaptureToolbar.tsx +1398 -0
- package/src/lib/prototype-comments/react/CommentProvider.tsx +298 -0
- package/src/lib/prototype-comments/scss.d.ts +10 -0
- package/src/lib/prototype-comments/ui/CommentAnnotationOverlay.tsx +253 -0
- package/src/lib/prototype-comments/ui/CommentDetailPage.tsx +56 -0
- package/src/lib/prototype-comments/ui/CommentHeaderCount.tsx +28 -0
- package/src/lib/prototype-comments/ui/CommentThread.tsx +331 -0
- package/src/lib/prototype-comments/ui/CommentsGrid.tsx +780 -0
- package/src/lib/prototype-comments/ui/CommentsListPage.tsx +25 -0
- package/src/lib/prototype-comments/ui/CommentsSidebar.tsx +77 -0
- package/src/lib/prototype-comments/ui/annotation-marker/index.tsx +200 -0
- package/src/lib/prototype-comments/ui/annotation-marker/styles.module.scss +194 -0
- package/src/lib/prototype-comments/ui/annotation-popup/index.tsx +335 -0
- package/src/lib/prototype-comments/ui/annotation-popup/styles.module.scss +469 -0
- package/src/lib/prototype-comments/ui/capture-styles.module.scss +2225 -0
- package/src/lib/prototype-comments/ui/comment-highlight.ts +48 -0
- package/src/lib/prototype-comments/ui/comment-thread.module.scss +203 -0
- package/src/lib/prototype-comments/ui/comments-grid-card.module.scss +253 -0
- package/src/lib/prototype-comments/ui/comments-sidebar.module.scss +123 -0
- package/src/lib/prototype-comments/ui/icon-transitions.module.scss +51 -0
- package/src/lib/prototype-comments/ui/icons.tsx +1086 -0
- package/src/lib/prototype-comments/ui/is-comment-detail-path.ts +7 -0
- package/src/lib/prototypes/capture-prototype-viewport.ts +32 -0
- package/src/lib/prototypes/changelog-meta-context.tsx +162 -0
- package/src/lib/prototypes/changelog-meta-storage.ts +62 -0
- package/src/lib/prototypes/changelog-meta.ts +95 -0
- package/src/lib/prototypes/create-prototype-registry.ts +74 -0
- package/src/lib/prototypes/create-state-screenshot-thumbnail.ts +46 -0
- package/src/lib/prototypes/design-exploration-default-storage.ts +68 -0
- package/src/lib/prototypes/design-exploration-default.ts +51 -0
- package/src/lib/prototypes/design-exploration-types.ts +203 -0
- package/src/lib/prototypes/prototype-comment-registry.tsx +145 -0
- package/src/lib/prototypes/prototype-comment-review-bridge.tsx +40 -0
- package/src/lib/prototypes/prototype-comment-review-state.ts +57 -0
- package/src/lib/prototypes/prototype-component-registry.ts +40 -0
- package/src/lib/prototypes/prototype-config-types.ts +34 -0
- package/src/lib/prototypes/prototype-preview-state-registry.ts +443 -0
- package/src/lib/prototypes/prototype-preview-state-types.ts +89 -0
- package/src/lib/prototypes/prototype-review-context.tsx +905 -0
- package/src/lib/prototypes/prototype-review-url.ts +134 -0
- package/src/lib/prototypes/prototype-share-link.ts +175 -0
- package/src/lib/prototypes/prototype-state-canvas-constants.ts +118 -0
- package/src/lib/prototypes/prototype-state-canvas-types.ts +159 -0
- package/src/lib/prototypes/prototype-state-screenshot.ts +39 -0
- package/src/lib/prototypes/prototype-tweak-registry.ts +96 -0
- package/src/lib/prototypes/prototype-tweak-types.ts +31 -0
- package/src/lib/prototypes/reference-docs.ts +137 -0
- package/src/lib/prototypes/screenshot-manifest.ts +33 -0
- package/src/lib/prototypes/screenshot-src.ts +7 -0
- package/src/lib/prototypes/share-command.ts +8 -0
- package/src/lib/prototypes/state-map-annotation.ts +70 -0
- package/src/lib/prototypes/use-design-exploration-default.ts +114 -0
- package/src/lib/prototypes/use-persisted-local-state.ts +140 -0
- package/src/lib/prototypes/use-prototype-comments.ts +63 -0
- package/src/lib/prototypes/use-prototype-mobile-viewport-frame.ts +46 -0
- package/src/lib/prototypes/use-prototype-state-screenshot-src.ts +71 -0
- package/src/lib/prototypes/use-prototype-tool-theme.tsx +115 -0
- package/src/lib/prototypes/use-prototype-viewport-frame.ts +34 -0
- package/src/lib/prototypes/use-register-prototype-reference-docs.ts +16 -0
- package/src/lib/prototypes/use-snap-corner-position.ts +417 -0
- package/src/lib/prototypes/use-toolbar-hover-panel.ts +98 -0
- package/src/lib/prototypes/variant-set-lucide-icon.tsx +55 -0
- package/src/lib/tool-portal.ts +33 -0
- package/src/lib/use-copy-to-clipboard.ts +81 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/vercel-preview/parse-vercel-github-comment.ts +81 -0
- package/src/server/create-host-api-route.ts +100 -0
- package/src/server/create-prototype-api-routes.ts +428 -0
- package/src/server/gallery/create-gallery-api.ts +56 -0
- package/src/server/gallery/folders-route.ts +156 -0
- package/src/server/gallery/gallery-env.ts +39 -0
- package/src/server/gallery/gallery-route.ts +150 -0
- package/src/server/gallery/gallery-types.ts +15 -0
- package/src/server/gallery/image-route.ts +59 -0
- package/src/server/gallery/item-route.ts +92 -0
- package/src/server/pr-vercel-preview-route.ts +162 -0
- package/src/server/redis/client.ts +55 -0
- package/src/server/redis/prototype-changelog-meta.ts +31 -0
- package/src/server/redis/prototype-comments.ts +92 -0
- package/src/server/redis/prototype-design-exploration-default.ts +32 -0
- package/src/server.ts +51 -0
- package/src/styles/globals.css +624 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { PrototypeTweaksRegistration } from "@prototype/lib/prototypes/prototype-tweak-types";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
|
|
4
|
+
type PrototypeTweakStore = {
|
|
5
|
+
registration: PrototypeTweaksRegistration | null;
|
|
6
|
+
listeners: Set<() => void>;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
declare global {
|
|
10
|
+
interface Window {
|
|
11
|
+
__prototypeTweakStore?: PrototypeTweakStore;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getStore(): PrototypeTweakStore {
|
|
16
|
+
if (typeof window === "undefined") {
|
|
17
|
+
return { registration: null, listeners: new Set() };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!window.__prototypeTweakStore) {
|
|
21
|
+
window.__prototypeTweakStore = {
|
|
22
|
+
registration: null,
|
|
23
|
+
listeners: new Set(),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return window.__prototypeTweakStore;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function emitChange() {
|
|
31
|
+
for (const listener of getStore().listeners) {
|
|
32
|
+
listener();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function registerPrototypeTweaks(
|
|
37
|
+
nextRegistration: PrototypeTweaksRegistration | null,
|
|
38
|
+
) {
|
|
39
|
+
const store = getStore();
|
|
40
|
+
store.registration = nextRegistration;
|
|
41
|
+
emitChange();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getPrototypeTweaksRegistration() {
|
|
45
|
+
return getStore().registration;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function subscribe(listener: () => void) {
|
|
49
|
+
const store = getStore();
|
|
50
|
+
store.listeners.add(listener);
|
|
51
|
+
return () => {
|
|
52
|
+
store.listeners.delete(listener);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function subscribePrototypeTweaks(listener: () => void) {
|
|
57
|
+
return subscribe(listener);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function usePrototypeTweaksRegistration() {
|
|
61
|
+
const [registration, setRegistration] =
|
|
62
|
+
useState<PrototypeTweaksRegistration | null>(() =>
|
|
63
|
+
typeof window === "undefined" ? null : getPrototypeTweaksRegistration(),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
setRegistration(getPrototypeTweaksRegistration());
|
|
68
|
+
return subscribe(() => {
|
|
69
|
+
setRegistration(getPrototypeTweaksRegistration());
|
|
70
|
+
});
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
return registration;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function focusPrototypeTweak(tweakId: string) {
|
|
77
|
+
const activeRegistration = getPrototypeTweaksRegistration();
|
|
78
|
+
const tweak = activeRegistration?.items.find((item) => item.id === tweakId);
|
|
79
|
+
if (!tweak) return null;
|
|
80
|
+
|
|
81
|
+
activeRegistration?.navigateTo(tweakId);
|
|
82
|
+
|
|
83
|
+
window.requestAnimationFrame(() => {
|
|
84
|
+
window.requestAnimationFrame(() => {
|
|
85
|
+
const screenshotRoot = document.querySelector(
|
|
86
|
+
"[data-prototype-screenshot]",
|
|
87
|
+
);
|
|
88
|
+
const target = screenshotRoot?.querySelector<HTMLElement>(
|
|
89
|
+
`[data-prototype-target$=".${tweak.targetId}"]`,
|
|
90
|
+
);
|
|
91
|
+
target?.scrollIntoView({ behavior: "smooth", block: "center" });
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return tweak.targetId;
|
|
96
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const PROTOTYPE_TWEAK_CATEGORY_LABELS = {
|
|
2
|
+
state: "State",
|
|
3
|
+
variants: "Variants",
|
|
4
|
+
} as const;
|
|
5
|
+
|
|
6
|
+
export type PrototypeTweakCategory = keyof typeof PROTOTYPE_TWEAK_CATEGORY_LABELS;
|
|
7
|
+
|
|
8
|
+
export const PROTOTYPE_TWEAK_CATEGORY_ORDER: PrototypeTweakCategory[] = [
|
|
9
|
+
"state",
|
|
10
|
+
"variants",
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
export type PrototypeTweakDefinition = {
|
|
14
|
+
id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
targetId: string;
|
|
17
|
+
category: PrototypeTweakCategory;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type PrototypeTweaksRegistration = {
|
|
21
|
+
items: PrototypeTweakDefinition[];
|
|
22
|
+
navigateTo: (id: string) => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function groupPrototypeTweaksByCategory(items: PrototypeTweakDefinition[]) {
|
|
26
|
+
return PROTOTYPE_TWEAK_CATEGORY_ORDER.map((category) => ({
|
|
27
|
+
category,
|
|
28
|
+
label: PROTOTYPE_TWEAK_CATEGORY_LABELS[category],
|
|
29
|
+
items: items.filter((item) => item.category === category),
|
|
30
|
+
})).filter((group) => group.items.length > 0);
|
|
31
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
export type PrototypeReferenceDoc = {
|
|
2
|
+
name: string;
|
|
3
|
+
link: string;
|
|
4
|
+
content: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function defaultReferenceDocsConfigPath(slug: string): string {
|
|
8
|
+
return `src/prototypes/${slug}/reference-docs.ts`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isValidReferenceDoc(value: unknown): value is PrototypeReferenceDoc {
|
|
12
|
+
if (!value || typeof value !== "object") return false;
|
|
13
|
+
|
|
14
|
+
const doc = value as Partial<PrototypeReferenceDoc>;
|
|
15
|
+
|
|
16
|
+
if (typeof doc.name !== "string" || !doc.name.trim()) return false;
|
|
17
|
+
if (typeof doc.link !== "string" || !doc.link.trim()) return false;
|
|
18
|
+
if (typeof doc.content !== "string") return false;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const url = new URL(normalizeReferenceDocLink(doc.link));
|
|
22
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function normalizeReferenceDocLink(link: string): string {
|
|
29
|
+
const trimmed = link.trim();
|
|
30
|
+
if (/^https?:\/\//i.test(trimmed)) {
|
|
31
|
+
return trimmed;
|
|
32
|
+
}
|
|
33
|
+
return `https://${trimmed}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isValidReferenceDocInput(
|
|
37
|
+
name: string,
|
|
38
|
+
link: string,
|
|
39
|
+
content: string,
|
|
40
|
+
): boolean {
|
|
41
|
+
if (!name.trim() || !link.trim() || !content.trim()) return false;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const url = new URL(normalizeReferenceDocLink(link));
|
|
45
|
+
return url.protocol === "http:" || url.protocol === "https:";
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function buildAddReferenceDocPrompt({
|
|
52
|
+
slug,
|
|
53
|
+
name,
|
|
54
|
+
link,
|
|
55
|
+
content,
|
|
56
|
+
existingDocs = [],
|
|
57
|
+
configFilePath,
|
|
58
|
+
origin = "http://localhost:3003",
|
|
59
|
+
}: {
|
|
60
|
+
slug: string;
|
|
61
|
+
name: string;
|
|
62
|
+
link: string;
|
|
63
|
+
content: string;
|
|
64
|
+
existingDocs?: PrototypeReferenceDoc[];
|
|
65
|
+
configFilePath?: string;
|
|
66
|
+
origin?: string;
|
|
67
|
+
}): string {
|
|
68
|
+
const target = configFilePath ?? defaultReferenceDocsConfigPath(slug);
|
|
69
|
+
const prototypeUrl = `${origin}/prototypes/${slug}`;
|
|
70
|
+
const trimmedName = name.trim();
|
|
71
|
+
const trimmedLink = normalizeReferenceDocLink(link);
|
|
72
|
+
const trimmedContent = content.trim();
|
|
73
|
+
|
|
74
|
+
const lines = [
|
|
75
|
+
`Add a reference doc to the "${slug}" prototype.`,
|
|
76
|
+
"",
|
|
77
|
+
"## Doc",
|
|
78
|
+
`Name: ${trimmedName}`,
|
|
79
|
+
`Link: ${trimmedLink}`,
|
|
80
|
+
"",
|
|
81
|
+
"Content:",
|
|
82
|
+
trimmedContent,
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
if (existingDocs.length > 0) {
|
|
86
|
+
lines.push("", "## Existing reference docs");
|
|
87
|
+
for (const doc of existingDocs) {
|
|
88
|
+
lines.push(`- ${doc.name} (${doc.link})`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
lines.push(
|
|
93
|
+
"",
|
|
94
|
+
"## Prototype",
|
|
95
|
+
`- Slug: \`${slug}\``,
|
|
96
|
+
`- Preview: ${prototypeUrl}`,
|
|
97
|
+
"",
|
|
98
|
+
"## Instructions",
|
|
99
|
+
`1. Create or update \`${target}\` and export \`REFERENCE_DOCS\` as a \`PrototypeReferenceDoc[]\` array.`,
|
|
100
|
+
"2. Prepend the new doc to the top of the array so the newest reference appears first.",
|
|
101
|
+
"3. Each entry must include `name`, `link`, and `content`.",
|
|
102
|
+
"4. Wire the docs on the prototype page with `useRegisterPrototypeReferenceDocs(REFERENCE_DOCS)` from `prototype`.",
|
|
103
|
+
"5. Do not store reference docs in KV or localStorage.",
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
return lines.join("\n");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function buildAddReferenceDocCopyText({
|
|
110
|
+
slug,
|
|
111
|
+
name,
|
|
112
|
+
link,
|
|
113
|
+
content,
|
|
114
|
+
existingDocs = [],
|
|
115
|
+
configFilePath,
|
|
116
|
+
origin = "http://localhost:3003",
|
|
117
|
+
}: {
|
|
118
|
+
slug: string;
|
|
119
|
+
name: string;
|
|
120
|
+
link: string;
|
|
121
|
+
content: string;
|
|
122
|
+
existingDocs?: PrototypeReferenceDoc[];
|
|
123
|
+
configFilePath?: string;
|
|
124
|
+
origin?: string;
|
|
125
|
+
}): string {
|
|
126
|
+
const prompt = buildAddReferenceDocPrompt({
|
|
127
|
+
slug,
|
|
128
|
+
name,
|
|
129
|
+
link,
|
|
130
|
+
content,
|
|
131
|
+
existingDocs,
|
|
132
|
+
configFilePath,
|
|
133
|
+
origin,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
return `${content.trim()}\n\n---\n\n${prompt}`;
|
|
137
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
type ScreenshotManifest = Record<string, number>;
|
|
5
|
+
|
|
6
|
+
const MANIFEST_PATH = path.join(
|
|
7
|
+
process.cwd(),
|
|
8
|
+
"public/prototypes/screenshots/manifest.json",
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
export async function readScreenshotManifest(): Promise<ScreenshotManifest> {
|
|
12
|
+
try {
|
|
13
|
+
const raw = await readFile(MANIFEST_PATH, "utf-8");
|
|
14
|
+
return JSON.parse(raw) as ScreenshotManifest;
|
|
15
|
+
} catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function touchScreenshotManifestKey(key: string): Promise<number> {
|
|
21
|
+
const manifest = await readScreenshotManifest();
|
|
22
|
+
const version = Date.now();
|
|
23
|
+
manifest[key] = version;
|
|
24
|
+
|
|
25
|
+
await mkdir(path.dirname(MANIFEST_PATH), { recursive: true });
|
|
26
|
+
await writeFile(MANIFEST_PATH, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
27
|
+
|
|
28
|
+
return version;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function touchScreenshotManifest(slug: string): Promise<number> {
|
|
32
|
+
return touchScreenshotManifestKey(slug);
|
|
33
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function getPrototypeShareCommand(slug: string): string {
|
|
2
|
+
return `pnpm share-prototype ${slug}`;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function getPrototypeSlugFromPathname(pathname: string): string | null {
|
|
6
|
+
const match = pathname.match(/^\/prototypes\/([^/]+)/);
|
|
7
|
+
return match?.[1] ?? null;
|
|
8
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export type ParsedStateMapBullet = {
|
|
2
|
+
text: string;
|
|
3
|
+
index: number;
|
|
4
|
+
highlightId?: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type ParsedStateMapAnnotation = {
|
|
8
|
+
lead: string;
|
|
9
|
+
bullets: ParsedStateMapBullet[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const BULLET_LINE = /^\s*[•*-]\s+(.+)$/;
|
|
13
|
+
const HIGHLIGHT_SUFFIX = /\s+@([a-z0-9-]+)$/i;
|
|
14
|
+
|
|
15
|
+
export function parseStateMapAnnotation(text: string): ParsedStateMapAnnotation {
|
|
16
|
+
const lines = text.split("\n");
|
|
17
|
+
const leadLines: string[] = [];
|
|
18
|
+
const bullets: ParsedStateMapBullet[] = [];
|
|
19
|
+
let bulletIndex = 0;
|
|
20
|
+
|
|
21
|
+
for (const line of lines) {
|
|
22
|
+
const trimmed = line.trim();
|
|
23
|
+
if (!trimmed) {
|
|
24
|
+
if (leadLines.length > 0 && bullets.length === 0) {
|
|
25
|
+
leadLines.push("");
|
|
26
|
+
}
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const bulletMatch = trimmed.match(BULLET_LINE);
|
|
31
|
+
if (bulletMatch) {
|
|
32
|
+
let body = bulletMatch[1]!.trim();
|
|
33
|
+
let highlightId: string | undefined;
|
|
34
|
+
const highlightMatch = body.match(HIGHLIGHT_SUFFIX);
|
|
35
|
+
if (highlightMatch) {
|
|
36
|
+
highlightId = highlightMatch[1]!.toLowerCase();
|
|
37
|
+
body = body.slice(0, highlightMatch.index).trim();
|
|
38
|
+
}
|
|
39
|
+
bullets.push({ text: body, index: bulletIndex, highlightId });
|
|
40
|
+
bulletIndex += 1;
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (bullets.length === 0) {
|
|
45
|
+
leadLines.push(trimmed);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { lead: leadLines.join("\n").trim(), bullets };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function resolveBulletHighlightId(
|
|
53
|
+
bullet: ParsedStateMapBullet,
|
|
54
|
+
highlightRegions?: readonly string[],
|
|
55
|
+
): string | null {
|
|
56
|
+
if (bullet.highlightId) return bullet.highlightId;
|
|
57
|
+
return highlightRegions?.[bullet.index] ?? null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function stateMapAnnotationHasInteractiveBullets(
|
|
61
|
+
text: string,
|
|
62
|
+
highlightRegions?: readonly string[],
|
|
63
|
+
): boolean {
|
|
64
|
+
const { bullets } = parseStateMapAnnotation(text);
|
|
65
|
+
if (bullets.length === 0) return false;
|
|
66
|
+
if (!highlightRegions?.length) {
|
|
67
|
+
return bullets.some((bullet) => bullet.highlightId);
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createDesignExplorationDefaultStorageAdapter } from "@prototype/lib/prototypes/design-exploration-default-storage";
|
|
4
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
5
|
+
|
|
6
|
+
type UseDesignExplorationDefaultOptions<TVariant extends string> = {
|
|
7
|
+
slug: string | null;
|
|
8
|
+
storageKeyPrefix: string;
|
|
9
|
+
validValues: readonly TVariant[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function useDesignExplorationDefault<TVariant extends string>({
|
|
13
|
+
slug,
|
|
14
|
+
storageKeyPrefix,
|
|
15
|
+
validValues,
|
|
16
|
+
}: UseDesignExplorationDefaultOptions<TVariant>) {
|
|
17
|
+
const [persistedDefault, setPersistedDefault] = useState<TVariant | null>(null);
|
|
18
|
+
const [ready, setReady] = useState(false);
|
|
19
|
+
const [saving, setSaving] = useState(false);
|
|
20
|
+
const [error, setError] = useState<string | null>(null);
|
|
21
|
+
|
|
22
|
+
const storage = useMemo(() => {
|
|
23
|
+
if (!slug) return null;
|
|
24
|
+
return createDesignExplorationDefaultStorageAdapter(slug, storageKeyPrefix);
|
|
25
|
+
}, [slug, storageKeyPrefix]);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (!storage) {
|
|
29
|
+
setReady(true);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let cancelled = false;
|
|
34
|
+
setReady(false);
|
|
35
|
+
setError(null);
|
|
36
|
+
|
|
37
|
+
async function load() {
|
|
38
|
+
const activeStorage = storage;
|
|
39
|
+
if (!activeStorage) return;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const record = await activeStorage.load();
|
|
43
|
+
if (cancelled) return;
|
|
44
|
+
|
|
45
|
+
if (
|
|
46
|
+
record &&
|
|
47
|
+
validValues.includes(record.value as TVariant)
|
|
48
|
+
) {
|
|
49
|
+
setPersistedDefault(record.value as TVariant);
|
|
50
|
+
} else {
|
|
51
|
+
setPersistedDefault(null);
|
|
52
|
+
}
|
|
53
|
+
} catch (loadError) {
|
|
54
|
+
if (!cancelled) {
|
|
55
|
+
setError(
|
|
56
|
+
loadError instanceof Error
|
|
57
|
+
? loadError.message
|
|
58
|
+
: "Failed to load design default variant.",
|
|
59
|
+
);
|
|
60
|
+
setPersistedDefault(null);
|
|
61
|
+
}
|
|
62
|
+
} finally {
|
|
63
|
+
if (!cancelled) {
|
|
64
|
+
setReady(true);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
void load();
|
|
70
|
+
|
|
71
|
+
return () => {
|
|
72
|
+
cancelled = true;
|
|
73
|
+
};
|
|
74
|
+
}, [storage, validValues]);
|
|
75
|
+
|
|
76
|
+
const effectiveDefault = persistedDefault ?? validValues[0];
|
|
77
|
+
|
|
78
|
+
const setDefaultVariant = useCallback(
|
|
79
|
+
async (value: TVariant) => {
|
|
80
|
+
if (!validValues.includes(value)) return;
|
|
81
|
+
|
|
82
|
+
setPersistedDefault(value);
|
|
83
|
+
|
|
84
|
+
if (!storage) return;
|
|
85
|
+
|
|
86
|
+
setSaving(true);
|
|
87
|
+
setError(null);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
await storage.save({
|
|
91
|
+
value,
|
|
92
|
+
updatedAt: new Date().toISOString(),
|
|
93
|
+
});
|
|
94
|
+
} catch (saveError) {
|
|
95
|
+
setError(
|
|
96
|
+
saveError instanceof Error
|
|
97
|
+
? saveError.message
|
|
98
|
+
: "Failed to save design default variant.",
|
|
99
|
+
);
|
|
100
|
+
} finally {
|
|
101
|
+
setSaving(false);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
[storage, validValues],
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
effectiveDefault,
|
|
109
|
+
setDefaultVariant,
|
|
110
|
+
ready,
|
|
111
|
+
saving,
|
|
112
|
+
error,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
hasPendingCommentRestore,
|
|
5
|
+
readShareCommentParam,
|
|
6
|
+
readShareLinkParams,
|
|
7
|
+
} from "@prototype/lib/prototypes/prototype-share-link";
|
|
8
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
9
|
+
|
|
10
|
+
export function prototypeLiveStateStorageKey(slug: string): string {
|
|
11
|
+
return `prototype-live-state:${slug}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function prototypeReviewPreferenceKey(
|
|
15
|
+
slug: string,
|
|
16
|
+
preference: string,
|
|
17
|
+
): string {
|
|
18
|
+
return `prototype-review:${slug}:${preference}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function readPersistedPrototypeLiveState<T>(
|
|
22
|
+
slug: string,
|
|
23
|
+
revive?: (parsed: unknown) => T | null,
|
|
24
|
+
): T | null {
|
|
25
|
+
if (typeof window === "undefined") return null;
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
29
|
+
if (
|
|
30
|
+
readShareLinkParams(searchParams) ||
|
|
31
|
+
readShareCommentParam(searchParams) ||
|
|
32
|
+
hasPendingCommentRestore(slug)
|
|
33
|
+
) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const raw = localStorage.getItem(prototypeLiveStateStorageKey(slug));
|
|
38
|
+
if (!raw) return null;
|
|
39
|
+
|
|
40
|
+
const parsed: unknown = JSON.parse(raw);
|
|
41
|
+
return revive ? revive(parsed) : (parsed as T);
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function writePersistedPrototypeLiveState(
|
|
48
|
+
slug: string,
|
|
49
|
+
state: unknown,
|
|
50
|
+
): void {
|
|
51
|
+
if (typeof window === "undefined") return;
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
localStorage.setItem(
|
|
55
|
+
prototypeLiveStateStorageKey(slug),
|
|
56
|
+
JSON.stringify(state),
|
|
57
|
+
);
|
|
58
|
+
} catch {
|
|
59
|
+
// Ignore localStorage write failures in prototype preview.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Persists prototype live state to localStorage on change (skips share-link loads). */
|
|
64
|
+
export function usePersistPrototypeLiveState<T extends Record<string, unknown>>(
|
|
65
|
+
slug: string,
|
|
66
|
+
state: T,
|
|
67
|
+
enabled = true,
|
|
68
|
+
): void {
|
|
69
|
+
const hasHydratedRef = useRef(false);
|
|
70
|
+
const serializedState = JSON.stringify(state);
|
|
71
|
+
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
if (!enabled) return;
|
|
74
|
+
|
|
75
|
+
if (!hasHydratedRef.current) {
|
|
76
|
+
hasHydratedRef.current = true;
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
writePersistedPrototypeLiveState(slug, JSON.parse(serializedState) as T);
|
|
81
|
+
}, [enabled, serializedState, slug]);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function usePersistedLocalString(storageKey: string, defaultValue: string) {
|
|
85
|
+
const [value, setValue] = useState(defaultValue);
|
|
86
|
+
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
try {
|
|
89
|
+
const stored = localStorage.getItem(storageKey);
|
|
90
|
+
if (stored != null) {
|
|
91
|
+
setValue(stored);
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
// Ignore localStorage read failures in prototype preview.
|
|
95
|
+
}
|
|
96
|
+
}, [storageKey]);
|
|
97
|
+
|
|
98
|
+
const updateValue = useCallback(
|
|
99
|
+
(nextValue: string) => {
|
|
100
|
+
setValue(nextValue);
|
|
101
|
+
try {
|
|
102
|
+
localStorage.setItem(storageKey, nextValue);
|
|
103
|
+
} catch {
|
|
104
|
+
// Ignore localStorage write failures in prototype preview.
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
[storageKey],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return { value, updateValue };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function usePersistedLocalBoolean(storageKey: string, defaultValue: boolean) {
|
|
114
|
+
const [value, setValue] = useState(defaultValue);
|
|
115
|
+
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
try {
|
|
118
|
+
const stored = localStorage.getItem(storageKey);
|
|
119
|
+
if (stored != null) {
|
|
120
|
+
setValue(stored === "true");
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
// Ignore localStorage read failures in prototype preview.
|
|
124
|
+
}
|
|
125
|
+
}, [storageKey]);
|
|
126
|
+
|
|
127
|
+
const updateValue = useCallback(
|
|
128
|
+
(nextValue: boolean) => {
|
|
129
|
+
setValue(nextValue);
|
|
130
|
+
try {
|
|
131
|
+
localStorage.setItem(storageKey, String(nextValue));
|
|
132
|
+
} catch {
|
|
133
|
+
// Ignore localStorage write failures in prototype preview.
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
[storageKey],
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
return { value, updateValue };
|
|
140
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useLayoutEffect, useRef } from "react";
|
|
4
|
+
|
|
5
|
+
import type { AnnotationTargetOptions } from "@prototype/lib/prototype-comments/core/annotation-target";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
usePrototypeCommentRegistry,
|
|
9
|
+
type PrototypeCommentHandlers,
|
|
10
|
+
} from "./prototype-comment-registry";
|
|
11
|
+
|
|
12
|
+
type UsePrototypeCommentsOptions = {
|
|
13
|
+
resolveTargetOptions?: () => AnnotationTargetOptions;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Register live state snapshot/restore handlers for the prototype comment system.
|
|
18
|
+
* Call from each prototype that wants controls/filter state frozen in annotations.
|
|
19
|
+
*/
|
|
20
|
+
export function usePrototypeComments<TLive extends Record<string, unknown>>(
|
|
21
|
+
live: TLive,
|
|
22
|
+
onRestore: (live: TLive) => void,
|
|
23
|
+
options?: UsePrototypeCommentsOptions,
|
|
24
|
+
) {
|
|
25
|
+
const { register, unregister, notifyLiveStateChange } =
|
|
26
|
+
usePrototypeCommentRegistry();
|
|
27
|
+
const liveRef = useRef(live);
|
|
28
|
+
const onRestoreRef = useRef(onRestore);
|
|
29
|
+
const resolveTargetOptionsRef = useRef(options?.resolveTargetOptions);
|
|
30
|
+
|
|
31
|
+
liveRef.current = live;
|
|
32
|
+
onRestoreRef.current = onRestore;
|
|
33
|
+
resolveTargetOptionsRef.current = options?.resolveTargetOptions;
|
|
34
|
+
|
|
35
|
+
const lastSerializedRef = useRef<string | null>(null);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const serialized = JSON.stringify(live);
|
|
38
|
+
if (
|
|
39
|
+
lastSerializedRef.current !== null &&
|
|
40
|
+
lastSerializedRef.current !== serialized
|
|
41
|
+
) {
|
|
42
|
+
notifyLiveStateChange();
|
|
43
|
+
}
|
|
44
|
+
lastSerializedRef.current = serialized;
|
|
45
|
+
}, [live, notifyLiveStateChange]);
|
|
46
|
+
|
|
47
|
+
useLayoutEffect(() => {
|
|
48
|
+
const handlers: PrototypeCommentHandlers = {
|
|
49
|
+
getLiveState: () => liveRef.current,
|
|
50
|
+
onRestore: (restored) => {
|
|
51
|
+
onRestoreRef.current(restored as TLive);
|
|
52
|
+
},
|
|
53
|
+
...(resolveTargetOptionsRef.current
|
|
54
|
+
? {
|
|
55
|
+
resolveTargetOptions: () =>
|
|
56
|
+
resolveTargetOptionsRef.current?.() ?? {},
|
|
57
|
+
}
|
|
58
|
+
: {}),
|
|
59
|
+
};
|
|
60
|
+
register(handlers);
|
|
61
|
+
return () => unregister(handlers);
|
|
62
|
+
}, [register, unregister]);
|
|
63
|
+
}
|