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,7 @@
|
|
|
1
|
+
export function isCanvasPath(pathname: string, basePath = "/canvas"): boolean {
|
|
2
|
+
return pathname === basePath || pathname.startsWith(`${basePath}/`);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function isCanvasDetailPath(pathname: string, basePath = "/canvas"): boolean {
|
|
6
|
+
return pathname !== basePath && pathname.startsWith(`${basePath}/`);
|
|
7
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { toPng } from "html-to-image";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_CAPTURE_DELAY_MS = 600;
|
|
4
|
+
|
|
5
|
+
export async function capturePrototypeViewportPng(
|
|
6
|
+
delayMs = DEFAULT_CAPTURE_DELAY_MS,
|
|
7
|
+
): Promise<string | null> {
|
|
8
|
+
const root = document.querySelector<HTMLElement>("[data-prototype-screenshot]");
|
|
9
|
+
if (!root) return null;
|
|
10
|
+
|
|
11
|
+
await document.fonts.ready;
|
|
12
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
13
|
+
|
|
14
|
+
const pixelRatio = Math.max(2, window.devicePixelRatio || 1);
|
|
15
|
+
|
|
16
|
+
return toPng(root, {
|
|
17
|
+
pixelRatio,
|
|
18
|
+
cacheBust: true,
|
|
19
|
+
width: root.clientWidth,
|
|
20
|
+
height: root.clientHeight,
|
|
21
|
+
filter: (node) => {
|
|
22
|
+
if (!(node instanceof HTMLElement)) return true;
|
|
23
|
+
if (node.dataset.prototypeControls !== undefined) return false;
|
|
24
|
+
if (node.dataset.prototypeCommentRoot !== undefined) return false;
|
|
25
|
+
if (node.closest("[data-prototype-comment-root]")) return false;
|
|
26
|
+
if (node.dataset.commentCaptureToolbar !== undefined) return false;
|
|
27
|
+
if (node.dataset.commentsSidebar !== undefined) return false;
|
|
28
|
+
if (node.dataset.prototypeStateCanvas !== undefined) return false;
|
|
29
|
+
return true;
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createChangelogMetaStorageAdapter } from "@prototype/lib/prototypes/changelog-meta-storage";
|
|
4
|
+
import {
|
|
5
|
+
EMPTY_CHANGELOG_META,
|
|
6
|
+
mergeLegacyChangelogMeta,
|
|
7
|
+
type PrototypeChangelogMeta,
|
|
8
|
+
} from "@prototype/lib/prototypes/changelog-meta";
|
|
9
|
+
import { prototypeReviewPreferenceKey } from "@prototype/lib/prototypes/use-persisted-local-state";
|
|
10
|
+
import {
|
|
11
|
+
createContext,
|
|
12
|
+
useCallback,
|
|
13
|
+
useContext,
|
|
14
|
+
useEffect,
|
|
15
|
+
useMemo,
|
|
16
|
+
useRef,
|
|
17
|
+
useState,
|
|
18
|
+
type ReactNode,
|
|
19
|
+
} from "react";
|
|
20
|
+
|
|
21
|
+
type ChangelogMetaStore = {
|
|
22
|
+
slug: string;
|
|
23
|
+
overview: string;
|
|
24
|
+
ready: boolean;
|
|
25
|
+
error: string | null;
|
|
26
|
+
setOverview: (overview: string) => void;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const ChangelogMetaContext = createContext<ChangelogMetaStore | null>(null);
|
|
30
|
+
|
|
31
|
+
const SAVE_DEBOUNCE_MS = 400;
|
|
32
|
+
|
|
33
|
+
type ChangelogMetaProviderProps = {
|
|
34
|
+
slug: string;
|
|
35
|
+
children: ReactNode;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export function ChangelogMetaProvider({
|
|
39
|
+
slug,
|
|
40
|
+
children,
|
|
41
|
+
}: ChangelogMetaProviderProps) {
|
|
42
|
+
const [meta, setMeta] = useState<PrototypeChangelogMeta>(EMPTY_CHANGELOG_META);
|
|
43
|
+
const [ready, setReady] = useState(false);
|
|
44
|
+
const [error, setError] = useState<string | null>(null);
|
|
45
|
+
const skipSaveRef = useRef(true);
|
|
46
|
+
const legacyOverviewKey = prototypeReviewPreferenceKey(
|
|
47
|
+
slug,
|
|
48
|
+
"changelog-overview",
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const storage = useMemo(
|
|
52
|
+
() => createChangelogMetaStorageAdapter(slug),
|
|
53
|
+
[slug],
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
let cancelled = false;
|
|
58
|
+
skipSaveRef.current = true;
|
|
59
|
+
|
|
60
|
+
async function load() {
|
|
61
|
+
setReady(false);
|
|
62
|
+
setError(null);
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const remote = await storage.load();
|
|
66
|
+
if (cancelled) return;
|
|
67
|
+
|
|
68
|
+
const merged = mergeLegacyChangelogMeta(
|
|
69
|
+
remote,
|
|
70
|
+
slug,
|
|
71
|
+
legacyOverviewKey,
|
|
72
|
+
);
|
|
73
|
+
setMeta(merged);
|
|
74
|
+
|
|
75
|
+
const migratedFromLegacy = merged.overview !== remote.overview;
|
|
76
|
+
|
|
77
|
+
if (migratedFromLegacy) {
|
|
78
|
+
await storage.save(merged);
|
|
79
|
+
}
|
|
80
|
+
} catch (loadError) {
|
|
81
|
+
if (!cancelled) {
|
|
82
|
+
setError(
|
|
83
|
+
loadError instanceof Error
|
|
84
|
+
? loadError.message
|
|
85
|
+
: "Failed to load change log.",
|
|
86
|
+
);
|
|
87
|
+
setMeta(EMPTY_CHANGELOG_META);
|
|
88
|
+
}
|
|
89
|
+
} finally {
|
|
90
|
+
if (!cancelled) {
|
|
91
|
+
setReady(true);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
void load();
|
|
97
|
+
|
|
98
|
+
return () => {
|
|
99
|
+
cancelled = true;
|
|
100
|
+
};
|
|
101
|
+
}, [legacyOverviewKey, slug, storage]);
|
|
102
|
+
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
if (!ready) return;
|
|
105
|
+
|
|
106
|
+
if (skipSaveRef.current) {
|
|
107
|
+
skipSaveRef.current = false;
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const timeout = window.setTimeout(() => {
|
|
112
|
+
void storage.save(meta).catch((saveError) => {
|
|
113
|
+
setError(
|
|
114
|
+
saveError instanceof Error
|
|
115
|
+
? saveError.message
|
|
116
|
+
: "Failed to save change log.",
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
}, SAVE_DEBOUNCE_MS);
|
|
120
|
+
|
|
121
|
+
return () => {
|
|
122
|
+
window.clearTimeout(timeout);
|
|
123
|
+
};
|
|
124
|
+
}, [meta, ready, storage]);
|
|
125
|
+
|
|
126
|
+
const setOverview = useCallback((overview: string) => {
|
|
127
|
+
setMeta((current) => ({
|
|
128
|
+
...current,
|
|
129
|
+
overview,
|
|
130
|
+
updatedAt: new Date().toISOString(),
|
|
131
|
+
}));
|
|
132
|
+
}, []);
|
|
133
|
+
|
|
134
|
+
const value = useMemo(
|
|
135
|
+
(): ChangelogMetaStore => ({
|
|
136
|
+
slug,
|
|
137
|
+
overview: meta.overview,
|
|
138
|
+
ready,
|
|
139
|
+
error,
|
|
140
|
+
setOverview,
|
|
141
|
+
}),
|
|
142
|
+
[slug, meta.overview, ready, error, setOverview],
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<ChangelogMetaContext.Provider value={value}>
|
|
147
|
+
{children}
|
|
148
|
+
</ChangelogMetaContext.Provider>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export function useChangelogMeta(): ChangelogMetaStore {
|
|
153
|
+
const context = useContext(ChangelogMetaContext);
|
|
154
|
+
if (!context) {
|
|
155
|
+
throw new Error("useChangelogMeta must be used within ChangelogMetaProvider");
|
|
156
|
+
}
|
|
157
|
+
return context;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function useChangelogMetaOptional(): ChangelogMetaStore | null {
|
|
161
|
+
return useContext(ChangelogMetaContext);
|
|
162
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseChangelogMeta,
|
|
3
|
+
type PrototypeChangelogMeta,
|
|
4
|
+
} from "./changelog-meta";
|
|
5
|
+
|
|
6
|
+
export class ChangelogMetaStorageError extends Error {
|
|
7
|
+
constructor(message: string) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "ChangelogMetaStorageError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createChangelogMetaStorageAdapter(slug: string) {
|
|
14
|
+
const url = `/api/prototypes/${encodeURIComponent(slug)}/changelog-meta`;
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
async load(): Promise<PrototypeChangelogMeta> {
|
|
18
|
+
const response = await fetch(url);
|
|
19
|
+
|
|
20
|
+
if (response.status === 503) {
|
|
21
|
+
throw new ChangelogMetaStorageError(
|
|
22
|
+
"Change log storage is not configured. Set Upstash Redis env vars.",
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
const body = (await response.json().catch(() => null)) as {
|
|
28
|
+
error?: string;
|
|
29
|
+
} | null;
|
|
30
|
+
throw new ChangelogMetaStorageError(
|
|
31
|
+
body?.error ?? "Failed to load change log overview.",
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const parsed: unknown = await response.json();
|
|
36
|
+
return parseChangelogMeta(parsed);
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
async save(meta: PrototypeChangelogMeta): Promise<void> {
|
|
40
|
+
const response = await fetch(url, {
|
|
41
|
+
method: "PUT",
|
|
42
|
+
headers: { "Content-Type": "application/json" },
|
|
43
|
+
body: JSON.stringify(meta),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (response.status === 503) {
|
|
47
|
+
throw new ChangelogMetaStorageError(
|
|
48
|
+
"Change log storage is not configured. Set Upstash Redis env vars.",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
const body = (await response.json().catch(() => null)) as {
|
|
54
|
+
error?: string;
|
|
55
|
+
} | null;
|
|
56
|
+
throw new ChangelogMetaStorageError(
|
|
57
|
+
body?.error ?? "Failed to save change log overview.",
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export type PrototypeChangelogMeta = {
|
|
2
|
+
overview: string;
|
|
3
|
+
updatedAt: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export function prototypeChangelogMetaRedisKey(slug: string): string {
|
|
7
|
+
return `prototype-changelog-meta:${slug}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function parseChangelogMeta(value: unknown): PrototypeChangelogMeta {
|
|
11
|
+
if (!value || typeof value !== "object") {
|
|
12
|
+
return EMPTY_CHANGELOG_META;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const meta = value as Partial<PrototypeChangelogMeta>;
|
|
16
|
+
|
|
17
|
+
if (
|
|
18
|
+
typeof meta.overview !== "string" ||
|
|
19
|
+
typeof meta.updatedAt !== "string" ||
|
|
20
|
+
Number.isNaN(Date.parse(meta.updatedAt))
|
|
21
|
+
) {
|
|
22
|
+
return EMPTY_CHANGELOG_META;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
overview: meta.overview,
|
|
27
|
+
updatedAt: meta.updatedAt,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function isValidChangelogMeta(value: unknown): value is PrototypeChangelogMeta {
|
|
32
|
+
if (!value || typeof value !== "object") return false;
|
|
33
|
+
|
|
34
|
+
const meta = value as Partial<PrototypeChangelogMeta>;
|
|
35
|
+
|
|
36
|
+
if (typeof meta.overview !== "string") return false;
|
|
37
|
+
if (
|
|
38
|
+
typeof meta.updatedAt !== "string" ||
|
|
39
|
+
Number.isNaN(Date.parse(meta.updatedAt))
|
|
40
|
+
) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function normalizeChangelogMetaPayload(
|
|
48
|
+
value: unknown,
|
|
49
|
+
): PrototypeChangelogMeta | null {
|
|
50
|
+
if (!isValidChangelogMeta(value)) return null;
|
|
51
|
+
|
|
52
|
+
const meta = value as Partial<PrototypeChangelogMeta>;
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
overview: meta.overview ?? "",
|
|
56
|
+
updatedAt: meta.updatedAt ?? new Date().toISOString(),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export const EMPTY_CHANGELOG_META: PrototypeChangelogMeta = {
|
|
61
|
+
overview: "",
|
|
62
|
+
updatedAt: new Date(0).toISOString(),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/** One-time merge from legacy browser storage when Redis has no saved meta yet. */
|
|
66
|
+
export function mergeLegacyChangelogMeta(
|
|
67
|
+
remote: PrototypeChangelogMeta,
|
|
68
|
+
_slug: string,
|
|
69
|
+
legacyOverviewKey: string,
|
|
70
|
+
): PrototypeChangelogMeta {
|
|
71
|
+
if (typeof window === "undefined") return remote;
|
|
72
|
+
|
|
73
|
+
const hasRemoteContent = remote.overview.trim().length > 0;
|
|
74
|
+
if (hasRemoteContent) return remote;
|
|
75
|
+
|
|
76
|
+
let overview = remote.overview;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const legacyOverview = localStorage.getItem(legacyOverviewKey);
|
|
80
|
+
if (legacyOverview?.trim()) {
|
|
81
|
+
overview = legacyOverview;
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
// ignore
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (overview === remote.overview) {
|
|
88
|
+
return remote;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
overview,
|
|
93
|
+
updatedAt: new Date().toISOString(),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PrototypeConfig,
|
|
3
|
+
PrototypeMetadata,
|
|
4
|
+
} from "./prototype-config-types";
|
|
5
|
+
import type { PrototypeComponentRegistry } from "./prototype-component-registry";
|
|
6
|
+
|
|
7
|
+
let activeConfig: PrototypeConfig | null = null;
|
|
8
|
+
|
|
9
|
+
export function setPrototypeConfig(config: PrototypeConfig): void {
|
|
10
|
+
activeConfig = config;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function requireConfig(): PrototypeConfig {
|
|
14
|
+
if (!activeConfig) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
"Prototype config is not set. Import prototype.config.ts and call setPrototypeConfig() or createPrototypeRegistry() first.",
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
return activeConfig;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function createPrototypeRegistry(config: PrototypeConfig) {
|
|
23
|
+
setPrototypeConfig(config);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
getAllPrototypes(): PrototypeMetadata[] {
|
|
27
|
+
return requireConfig().prototypes.map(
|
|
28
|
+
({ slug, title, screenshot }) => ({ slug, title, screenshot }),
|
|
29
|
+
);
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
getPrototype(slug: string): PrototypeMetadata | undefined {
|
|
33
|
+
const entry = requireConfig().prototypes.find((prototype) => prototype.slug === slug);
|
|
34
|
+
if (!entry) return undefined;
|
|
35
|
+
return { slug: entry.slug, title: entry.title, screenshot: entry.screenshot };
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
getPrototypeComponent(slug: string) {
|
|
39
|
+
return requireConfig().prototypes.find((prototype) => prototype.slug === slug)
|
|
40
|
+
?.component;
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
getPrototypeComponentRegistryForSlug(
|
|
44
|
+
slug: string,
|
|
45
|
+
): PrototypeComponentRegistry | undefined {
|
|
46
|
+
return requireConfig().prototypes.find((prototype) => prototype.slug === slug)
|
|
47
|
+
?.componentRegistry;
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getPrototypeComponentRegistryForSlug(
|
|
53
|
+
slug: string,
|
|
54
|
+
): PrototypeComponentRegistry | undefined {
|
|
55
|
+
return requireConfig().prototypes.find((prototype) => prototype.slug === slug)
|
|
56
|
+
?.componentRegistry;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getAllPrototypes(): PrototypeMetadata[] {
|
|
60
|
+
return requireConfig().prototypes.map(({ slug, title, screenshot }) => ({
|
|
61
|
+
slug,
|
|
62
|
+
title,
|
|
63
|
+
screenshot,
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function getPrototype(slug: string): PrototypeMetadata | undefined {
|
|
68
|
+
return getAllPrototypes().find((prototype) => prototype.slug === slug);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function getPrototypeComponent(slug: string) {
|
|
72
|
+
return requireConfig().prototypes.find((prototype) => prototype.slug === slug)
|
|
73
|
+
?.component;
|
|
74
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
function loadImage(dataUrl: string): Promise<HTMLImageElement> {
|
|
2
|
+
return new Promise((resolve, reject) => {
|
|
3
|
+
const img = new Image();
|
|
4
|
+
img.onload = () => resolve(img);
|
|
5
|
+
img.onerror = () => reject(new Error("Failed to load screenshot image"));
|
|
6
|
+
img.src = dataUrl;
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Crop and resize a viewport capture to match state-map preview (object-cover, top). */
|
|
11
|
+
export async function createStateScreenshotThumbnail(
|
|
12
|
+
sourceDataUrl: string,
|
|
13
|
+
targetWidth: number,
|
|
14
|
+
targetHeight: number,
|
|
15
|
+
): Promise<string | null> {
|
|
16
|
+
try {
|
|
17
|
+
const img = await loadImage(sourceDataUrl);
|
|
18
|
+
const canvas = document.createElement("canvas");
|
|
19
|
+
canvas.width = targetWidth;
|
|
20
|
+
canvas.height = targetHeight;
|
|
21
|
+
|
|
22
|
+
const ctx = canvas.getContext("2d");
|
|
23
|
+
if (!ctx) return null;
|
|
24
|
+
|
|
25
|
+
const sourceAspect = img.width / img.height;
|
|
26
|
+
const targetAspect = targetWidth / targetHeight;
|
|
27
|
+
|
|
28
|
+
let sx = 0;
|
|
29
|
+
let sy = 0;
|
|
30
|
+
let sw = img.width;
|
|
31
|
+
let sh = img.height;
|
|
32
|
+
|
|
33
|
+
if (sourceAspect > targetAspect) {
|
|
34
|
+
sw = img.height * targetAspect;
|
|
35
|
+
sx = (img.width - sw) / 2;
|
|
36
|
+
} else {
|
|
37
|
+
sh = img.width / targetAspect;
|
|
38
|
+
sy = 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
ctx.drawImage(img, sx, sy, sw, sh, 0, 0, targetWidth, targetHeight);
|
|
42
|
+
return canvas.toDataURL("image/png");
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {
|
|
2
|
+
parseDesignExplorationDefault,
|
|
3
|
+
type DesignExplorationDefaultRecord,
|
|
4
|
+
} from "./design-exploration-default";
|
|
5
|
+
|
|
6
|
+
export class DesignExplorationDefaultStorageError extends Error {
|
|
7
|
+
constructor(message: string) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "DesignExplorationDefaultStorageError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createDesignExplorationDefaultStorageAdapter(
|
|
14
|
+
slug: string,
|
|
15
|
+
storageKeyPrefix: string,
|
|
16
|
+
) {
|
|
17
|
+
const params = new URLSearchParams({ key: storageKeyPrefix });
|
|
18
|
+
const url = `/api/prototypes/${encodeURIComponent(slug)}/design-exploration-default?${params}`;
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
async load(): Promise<DesignExplorationDefaultRecord | null> {
|
|
22
|
+
const response = await fetch(url);
|
|
23
|
+
|
|
24
|
+
if (response.status === 503) {
|
|
25
|
+
throw new DesignExplorationDefaultStorageError(
|
|
26
|
+
"Design default storage is not configured. Set Upstash Redis env vars.",
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
const body = (await response.json().catch(() => null)) as {
|
|
32
|
+
error?: string;
|
|
33
|
+
} | null;
|
|
34
|
+
throw new DesignExplorationDefaultStorageError(
|
|
35
|
+
body?.error ?? "Failed to load design default variant.",
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const parsed: unknown = await response.json();
|
|
40
|
+
if (parsed == null) return null;
|
|
41
|
+
|
|
42
|
+
return parseDesignExplorationDefault(parsed);
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
async save(record: DesignExplorationDefaultRecord): Promise<void> {
|
|
46
|
+
const response = await fetch(url, {
|
|
47
|
+
method: "PUT",
|
|
48
|
+
headers: { "Content-Type": "application/json" },
|
|
49
|
+
body: JSON.stringify(record),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (response.status === 503) {
|
|
53
|
+
throw new DesignExplorationDefaultStorageError(
|
|
54
|
+
"Design default storage is not configured. Set Upstash Redis env vars.",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
const body = (await response.json().catch(() => null)) as {
|
|
60
|
+
error?: string;
|
|
61
|
+
} | null;
|
|
62
|
+
throw new DesignExplorationDefaultStorageError(
|
|
63
|
+
body?.error ?? "Failed to save design default variant.",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export type DesignExplorationDefaultRecord = {
|
|
2
|
+
value: string;
|
|
3
|
+
updatedAt: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export function prototypeDesignExplorationDefaultRedisKey(
|
|
7
|
+
storageKeyPrefix: string,
|
|
8
|
+
): string {
|
|
9
|
+
return `prototype-design-default:${storageKeyPrefix}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function parseDesignExplorationDefault(
|
|
13
|
+
value: unknown,
|
|
14
|
+
): DesignExplorationDefaultRecord | null {
|
|
15
|
+
if (!value || typeof value !== "object") return null;
|
|
16
|
+
|
|
17
|
+
const record = value as Partial<DesignExplorationDefaultRecord>;
|
|
18
|
+
|
|
19
|
+
if (typeof record.value !== "string" || !record.value.trim()) return null;
|
|
20
|
+
if (
|
|
21
|
+
typeof record.updatedAt !== "string" ||
|
|
22
|
+
Number.isNaN(Date.parse(record.updatedAt))
|
|
23
|
+
) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
value: record.value,
|
|
29
|
+
updatedAt: record.updatedAt,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isValidDesignExplorationDefaultRecord(
|
|
34
|
+
value: unknown,
|
|
35
|
+
): value is DesignExplorationDefaultRecord {
|
|
36
|
+
return parseDesignExplorationDefault(value) != null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function normalizeDesignExplorationDefaultPayload(
|
|
40
|
+
value: unknown,
|
|
41
|
+
validValues?: readonly string[],
|
|
42
|
+
): DesignExplorationDefaultRecord | null {
|
|
43
|
+
const parsed = parseDesignExplorationDefault(value);
|
|
44
|
+
if (!parsed) return null;
|
|
45
|
+
|
|
46
|
+
if (validValues && !validValues.includes(parsed.value)) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return parsed;
|
|
51
|
+
}
|