create-imagine 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/LICENSE +21 -0
- package/README.md +40 -0
- package/bin/create-imagine.js +407 -0
- package/package.json +19 -0
- package/templates/blank/README.md +96 -0
- package/templates/blank/gitignore +6 -0
- package/templates/blank/index.html +13 -0
- package/templates/blank/package.json +42 -0
- package/templates/blank/projects/example/figures/hello-world.tsx +45 -0
- package/templates/blank/projects/example/manifest.ts +44 -0
- package/templates/blank/projects/example/project.ts +16 -0
- package/templates/blank/projects/example/props.json +4 -0
- package/templates/blank/scripts/list.ts +46 -0
- package/templates/blank/scripts/projects.ts +43 -0
- package/templates/blank/scripts/render.ts +288 -0
- package/templates/blank/scripts/server.ts +117 -0
- package/templates/blank/src/core/__tests__/controls.test.ts +14 -0
- package/templates/blank/src/core/__tests__/manifest.test.ts +45 -0
- package/templates/blank/src/core/__tests__/props.test.ts +9 -0
- package/templates/blank/src/core/controls.ts +14 -0
- package/templates/blank/src/core/manifest.ts +134 -0
- package/templates/blank/src/core/props.ts +7 -0
- package/templates/blank/src/framework/Figure.tsx +34 -0
- package/templates/blank/src/framework/__tests__/sizing.test.ts +29 -0
- package/templates/blank/src/framework/charts/Axes.tsx +103 -0
- package/templates/blank/src/framework/charts/GridLines.tsx +59 -0
- package/templates/blank/src/framework/charts/Legend.tsx +31 -0
- package/templates/blank/src/framework/charts/Series.tsx +50 -0
- package/templates/blank/src/framework/charts/scales.ts +19 -0
- package/templates/blank/src/framework/diagrams/primitives.tsx +134 -0
- package/templates/blank/src/framework/layout/PanelGrid.tsx +60 -0
- package/templates/blank/src/framework/math/MathSvg.tsx +35 -0
- package/templates/blank/src/framework/math/mathjax.ts +64 -0
- package/templates/blank/src/framework/sizing.ts +28 -0
- package/templates/blank/src/framework/theme.ts +35 -0
- package/templates/blank/src/framework/types.ts +42 -0
- package/templates/blank/src/main.tsx +11 -0
- package/templates/blank/src/studio/StudioApp.tsx +130 -0
- package/templates/blank/src/studio/StudioRoot.tsx +14 -0
- package/templates/blank/src/studio/base64url.ts +8 -0
- package/templates/blank/src/studio/figureLoader.ts +30 -0
- package/templates/blank/src/studio/projectLoader.ts +40 -0
- package/templates/blank/src/studio/propsApi.ts +26 -0
- package/templates/blank/src/studio/routes/FigureView.tsx +365 -0
- package/templates/blank/src/studio/routes/ProjectHome.tsx +107 -0
- package/templates/blank/src/studio/routes/ProjectsHome.tsx +63 -0
- package/templates/blank/src/studio/routes/RenderView.tsx +123 -0
- package/templates/blank/src/studio/studio.css +540 -0
- package/templates/blank/src/studio/useProjectProps.ts +129 -0
- package/templates/blank/src/vite-env.d.ts +2 -0
- package/templates/blank/tsconfig.json +20 -0
- package/templates/blank/vite.config.ts +82 -0
- package/templates/blank/vitest.config.ts +8 -0
- package/templates/example/README.md +96 -0
- package/templates/example/gitignore +6 -0
- package/templates/example/index.html +13 -0
- package/templates/example/package.json +42 -0
- package/templates/example/projects/example/figures/ai-agent-architecture.tsx +133 -0
- package/templates/example/projects/example/figures/equation.tsx +29 -0
- package/templates/example/projects/example/figures/hello-world.tsx +45 -0
- package/templates/example/projects/example/figures/line-chart.tsx +80 -0
- package/templates/example/projects/example/figures/multi-panel.tsx +51 -0
- package/templates/example/projects/example/figures/pipeline-diagram.tsx +51 -0
- package/templates/example/projects/example/manifest.ts +161 -0
- package/templates/example/projects/example/project.ts +31 -0
- package/templates/example/projects/example/props.json +10 -0
- package/templates/example/public/projects/example/previews/ai-agent-architecture--default.png +0 -0
- package/templates/example/public/projects/example/previews/equation--default.png +0 -0
- package/templates/example/public/projects/example/previews/hello-world--default.png +0 -0
- package/templates/example/public/projects/example/previews/line-chart--default.png +0 -0
- package/templates/example/public/projects/example/previews/multi-panel--default.png +0 -0
- package/templates/example/public/projects/example/previews/pipeline-diagram--default.png +0 -0
- package/templates/example/scripts/list.ts +46 -0
- package/templates/example/scripts/projects.ts +43 -0
- package/templates/example/scripts/render.ts +288 -0
- package/templates/example/scripts/server.ts +117 -0
- package/templates/example/src/core/__tests__/controls.test.ts +14 -0
- package/templates/example/src/core/__tests__/manifest.test.ts +45 -0
- package/templates/example/src/core/__tests__/props.test.ts +9 -0
- package/templates/example/src/core/controls.ts +14 -0
- package/templates/example/src/core/manifest.ts +134 -0
- package/templates/example/src/core/props.ts +7 -0
- package/templates/example/src/framework/Figure.tsx +34 -0
- package/templates/example/src/framework/__tests__/sizing.test.ts +29 -0
- package/templates/example/src/framework/charts/Axes.tsx +103 -0
- package/templates/example/src/framework/charts/GridLines.tsx +59 -0
- package/templates/example/src/framework/charts/Legend.tsx +31 -0
- package/templates/example/src/framework/charts/Series.tsx +50 -0
- package/templates/example/src/framework/charts/scales.ts +19 -0
- package/templates/example/src/framework/diagrams/primitives.tsx +134 -0
- package/templates/example/src/framework/layout/PanelGrid.tsx +60 -0
- package/templates/example/src/framework/math/MathSvg.tsx +35 -0
- package/templates/example/src/framework/math/mathjax.ts +64 -0
- package/templates/example/src/framework/sizing.ts +28 -0
- package/templates/example/src/framework/theme.ts +35 -0
- package/templates/example/src/framework/types.ts +42 -0
- package/templates/example/src/main.tsx +11 -0
- package/templates/example/src/studio/StudioApp.tsx +130 -0
- package/templates/example/src/studio/StudioRoot.tsx +14 -0
- package/templates/example/src/studio/base64url.ts +8 -0
- package/templates/example/src/studio/figureLoader.ts +30 -0
- package/templates/example/src/studio/projectLoader.ts +40 -0
- package/templates/example/src/studio/propsApi.ts +26 -0
- package/templates/example/src/studio/routes/FigureView.tsx +365 -0
- package/templates/example/src/studio/routes/ProjectHome.tsx +107 -0
- package/templates/example/src/studio/routes/ProjectsHome.tsx +63 -0
- package/templates/example/src/studio/routes/RenderView.tsx +123 -0
- package/templates/example/src/studio/studio.css +540 -0
- package/templates/example/src/studio/useProjectProps.ts +129 -0
- package/templates/example/src/vite-env.d.ts +2 -0
- package/templates/example/tsconfig.json +20 -0
- package/templates/example/vite.config.ts +82 -0
- package/templates/example/vitest.config.ts +8 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
type MathJaxGlobal = {
|
|
2
|
+
startup?: { promise?: Promise<unknown> };
|
|
3
|
+
tex2svgPromise?: (tex: string, options?: unknown) => Promise<unknown>;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
declare global {
|
|
7
|
+
interface Window {
|
|
8
|
+
MathJax?: MathJaxGlobal;
|
|
9
|
+
__IMAGINE_MATH_PENDING__?: number;
|
|
10
|
+
__IMAGINE_MATH_WAITERS__?: Array<() => void>;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let loadPromise: Promise<void> | null = null;
|
|
15
|
+
|
|
16
|
+
export async function loadMathJax(): Promise<void> {
|
|
17
|
+
if (typeof window === 'undefined') return;
|
|
18
|
+
if (window.MathJax?.tex2svgPromise) return;
|
|
19
|
+
if (loadPromise) return loadPromise;
|
|
20
|
+
|
|
21
|
+
loadPromise = new Promise<void>((resolve, reject) => {
|
|
22
|
+
const url = import.meta.env.VITE_MATHJAX_URL ?? 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js';
|
|
23
|
+
|
|
24
|
+
window.MathJax = {
|
|
25
|
+
...window.MathJax,
|
|
26
|
+
svg: { ...(window.MathJax as any)?.svg, fontCache: 'global' }
|
|
27
|
+
} as MathJaxGlobal;
|
|
28
|
+
|
|
29
|
+
const script = document.createElement('script');
|
|
30
|
+
script.async = true;
|
|
31
|
+
script.src = url;
|
|
32
|
+
script.addEventListener('load', () => resolve());
|
|
33
|
+
script.addEventListener('error', () => reject(new Error(`Failed to load MathJax from ${url}`)));
|
|
34
|
+
document.head.appendChild(script);
|
|
35
|
+
}).then(async () => {
|
|
36
|
+
await window.MathJax?.startup?.promise?.catch(() => undefined);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return loadPromise;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function trackMathTask(task: Promise<unknown>): void {
|
|
43
|
+
if (typeof window === 'undefined') return;
|
|
44
|
+
window.__IMAGINE_MATH_PENDING__ = (window.__IMAGINE_MATH_PENDING__ ?? 0) + 1;
|
|
45
|
+
|
|
46
|
+
task.finally(() => {
|
|
47
|
+
window.__IMAGINE_MATH_PENDING__ = Math.max(0, (window.__IMAGINE_MATH_PENDING__ ?? 1) - 1);
|
|
48
|
+
if (window.__IMAGINE_MATH_PENDING__ === 0 && window.__IMAGINE_MATH_WAITERS__?.length) {
|
|
49
|
+
const waiters = window.__IMAGINE_MATH_WAITERS__;
|
|
50
|
+
window.__IMAGINE_MATH_WAITERS__ = [];
|
|
51
|
+
waiters.forEach((w) => w());
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function waitForMathTasks(): Promise<void> {
|
|
57
|
+
if (typeof window === 'undefined') return Promise.resolve();
|
|
58
|
+
if ((window.__IMAGINE_MATH_PENDING__ ?? 0) === 0) return Promise.resolve();
|
|
59
|
+
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
window.__IMAGINE_MATH_WAITERS__ = window.__IMAGINE_MATH_WAITERS__ ?? [];
|
|
62
|
+
window.__IMAGINE_MATH_WAITERS__.push(resolve);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { FigureSize, ResolvedSize } from './types';
|
|
2
|
+
|
|
3
|
+
export const MM_PER_INCH = 25.4;
|
|
4
|
+
|
|
5
|
+
export function mmToPx(mm: number, dpi: number): number {
|
|
6
|
+
return Math.round((mm / MM_PER_INCH) * dpi);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function pxToMm(px: number, dpi: number): number {
|
|
10
|
+
return (px / dpi) * MM_PER_INCH;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function resolveSize(size: FigureSize): ResolvedSize {
|
|
14
|
+
if (size.unit === 'px') {
|
|
15
|
+
return { width: size.width, height: size.height, source: size };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const width = mmToPx(size.width, size.dpi);
|
|
19
|
+
const height = mmToPx(size.height, size.dpi);
|
|
20
|
+
return {
|
|
21
|
+
width,
|
|
22
|
+
height,
|
|
23
|
+
dpi: size.dpi,
|
|
24
|
+
mm: { width: size.width, height: size.height },
|
|
25
|
+
source: size
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const theme = {
|
|
2
|
+
fontFamily:
|
|
3
|
+
"ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, 'Noto Sans', 'Liberation Sans', sans-serif",
|
|
4
|
+
monoFontFamily:
|
|
5
|
+
"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
|
6
|
+
colors: {
|
|
7
|
+
text: '#111827',
|
|
8
|
+
subtleText: '#4B5563',
|
|
9
|
+
grid: '#E5E7EB',
|
|
10
|
+
axis: '#111827',
|
|
11
|
+
bg: '#FFFFFF',
|
|
12
|
+
panel: '#F9FAFB',
|
|
13
|
+
blue: '#2563EB',
|
|
14
|
+
teal: '#0F766E',
|
|
15
|
+
orange: '#C2410C',
|
|
16
|
+
red: '#B91C1C'
|
|
17
|
+
},
|
|
18
|
+
strokes: {
|
|
19
|
+
thin: 1,
|
|
20
|
+
normal: 1.5,
|
|
21
|
+
thick: 2.5
|
|
22
|
+
},
|
|
23
|
+
radii: {
|
|
24
|
+
sm: 6,
|
|
25
|
+
md: 10
|
|
26
|
+
},
|
|
27
|
+
spacing: {
|
|
28
|
+
xs: 4,
|
|
29
|
+
sm: 8,
|
|
30
|
+
md: 12,
|
|
31
|
+
lg: 16,
|
|
32
|
+
xl: 24
|
|
33
|
+
}
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export type FigureId = string;
|
|
4
|
+
|
|
5
|
+
export type FigureSize =
|
|
6
|
+
| { unit: 'px'; width: number; height: number }
|
|
7
|
+
| { unit: 'mm'; width: number; height: number; dpi: number };
|
|
8
|
+
|
|
9
|
+
export type FigureVariant = {
|
|
10
|
+
id: string;
|
|
11
|
+
title?: string;
|
|
12
|
+
props?: unknown;
|
|
13
|
+
size?: FigureSize;
|
|
14
|
+
background?: 'white' | 'transparent';
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type FigureManifestItem = {
|
|
18
|
+
id: FigureId;
|
|
19
|
+
title: string;
|
|
20
|
+
moduleKey: string;
|
|
21
|
+
size: FigureSize;
|
|
22
|
+
variants: FigureVariant[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type FigureComponentBaseProps = {
|
|
26
|
+
width: number;
|
|
27
|
+
height: number;
|
|
28
|
+
background?: 'white' | 'transparent';
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type FigureComponent<Props extends Record<string, unknown> = Record<string, unknown>> = (
|
|
32
|
+
props: FigureComponentBaseProps & Props
|
|
33
|
+
) => ReactNode;
|
|
34
|
+
|
|
35
|
+
export type ResolvedSize = {
|
|
36
|
+
width: number;
|
|
37
|
+
height: number;
|
|
38
|
+
dpi?: number;
|
|
39
|
+
mm?: { width: number; height: number };
|
|
40
|
+
source: FigureSize;
|
|
41
|
+
};
|
|
42
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import ReactDOM from 'react-dom/client';
|
|
3
|
+
import { StudioRoot } from './studio/StudioRoot';
|
|
4
|
+
import './studio/studio.css';
|
|
5
|
+
|
|
6
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
7
|
+
<React.StrictMode>
|
|
8
|
+
<StudioRoot />
|
|
9
|
+
</React.StrictMode>
|
|
10
|
+
);
|
|
11
|
+
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { Link, Route, Routes, useMatch, useNavigate } from 'react-router-dom';
|
|
3
|
+
import type { ProjectDefinition } from '../core/manifest';
|
|
4
|
+
import { loadAllProjects } from './projectLoader';
|
|
5
|
+
import { ProjectsHome } from './routes/ProjectsHome';
|
|
6
|
+
import { ProjectHome } from './routes/ProjectHome';
|
|
7
|
+
import { FigureView } from './routes/FigureView';
|
|
8
|
+
|
|
9
|
+
function useActiveProjectId(): string | null {
|
|
10
|
+
const match = useMatch('/project/:projectId/*');
|
|
11
|
+
return match?.params.projectId ?? null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function useActiveFigureId(): string | null {
|
|
15
|
+
const match = useMatch('/project/:projectId/figure/:figureId/*');
|
|
16
|
+
return match?.params.figureId ?? null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function StudioApp() {
|
|
20
|
+
const navigate = useNavigate();
|
|
21
|
+
const activeId = useActiveFigureId();
|
|
22
|
+
const activeProjectId = useActiveProjectId();
|
|
23
|
+
const [query, setQuery] = useState('');
|
|
24
|
+
const [projects, setProjects] = useState<ProjectDefinition[] | null>(null);
|
|
25
|
+
const [projectsError, setProjectsError] = useState<string | null>(null);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
loadAllProjects().then(setProjects, (err) => setProjectsError(String(err?.message ?? err)));
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
const activeProject = useMemo(() => {
|
|
32
|
+
if (!projects || !activeProjectId) return null;
|
|
33
|
+
return projects.find((p) => p.id === activeProjectId) ?? null;
|
|
34
|
+
}, [projects, activeProjectId]);
|
|
35
|
+
|
|
36
|
+
const filteredFigures = useMemo(() => {
|
|
37
|
+
if (!activeProject) return [];
|
|
38
|
+
const q = query.trim().toLowerCase();
|
|
39
|
+
if (!q) return activeProject.figures;
|
|
40
|
+
return activeProject.figures.filter((f) => f.id.toLowerCase().includes(q) || f.title.toLowerCase().includes(q));
|
|
41
|
+
}, [activeProject, query]);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="studio">
|
|
45
|
+
<aside className="sidebar">
|
|
46
|
+
<div className="sidebarHeader">
|
|
47
|
+
<div className="sidebarTitleRow">
|
|
48
|
+
<div className="sidebarTitle">Imagine Studio</div>
|
|
49
|
+
<button className="btn btnSmall" onClick={() => navigate('/')}>
|
|
50
|
+
Home
|
|
51
|
+
</button>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<div className="sidebarSection">
|
|
56
|
+
<div className="sidebarSectionTitle">Projects</div>
|
|
57
|
+
<nav className="projectList" aria-label="Projects">
|
|
58
|
+
{projects?.map((p) => {
|
|
59
|
+
const active = activeProjectId === p.id;
|
|
60
|
+
return (
|
|
61
|
+
<Link key={p.id} className={`projectItem ${active ? 'active' : ''}`} to={`/project/${encodeURIComponent(p.id)}`}>
|
|
62
|
+
<div className="projectItemTitle">{p.title}</div>
|
|
63
|
+
<div className="projectItemMeta">
|
|
64
|
+
<span className="mono">{p.id}</span>
|
|
65
|
+
<span className="dot">•</span>
|
|
66
|
+
<span>{p.figures.length} figure{p.figures.length === 1 ? '' : 's'}</span>
|
|
67
|
+
</div>
|
|
68
|
+
</Link>
|
|
69
|
+
);
|
|
70
|
+
})}
|
|
71
|
+
{projectsError ? <div className="sidebarError">{projectsError}</div> : null}
|
|
72
|
+
{!projects && !projectsError ? <div className="sidebarHint">Loading…</div> : null}
|
|
73
|
+
</nav>
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
{activeProject ? (
|
|
77
|
+
<div className="sidebarSection">
|
|
78
|
+
<div className="sidebarSectionTitle">Figures</div>
|
|
79
|
+
<input
|
|
80
|
+
className="search"
|
|
81
|
+
placeholder={`Search in ${activeProject.title}…`}
|
|
82
|
+
value={query}
|
|
83
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
84
|
+
/>
|
|
85
|
+
<nav className="figureList" aria-label="Figures">
|
|
86
|
+
{filteredFigures.map((f) => {
|
|
87
|
+
const active = activeId === f.id;
|
|
88
|
+
return (
|
|
89
|
+
<Link
|
|
90
|
+
key={f.id}
|
|
91
|
+
className={`figureItem ${active ? 'active' : ''}`}
|
|
92
|
+
to={`/project/${encodeURIComponent(activeProject.id)}/figure/${encodeURIComponent(f.id)}`}
|
|
93
|
+
>
|
|
94
|
+
<div className="figureItemTitle">{f.title}</div>
|
|
95
|
+
<div className="figureItemMeta">
|
|
96
|
+
<span className="mono">{f.id}</span>
|
|
97
|
+
<span className="dot">•</span>
|
|
98
|
+
<span>
|
|
99
|
+
{f.variants.length} variant{f.variants.length === 1 ? '' : 's'}
|
|
100
|
+
</span>
|
|
101
|
+
</div>
|
|
102
|
+
</Link>
|
|
103
|
+
);
|
|
104
|
+
})}
|
|
105
|
+
</nav>
|
|
106
|
+
</div>
|
|
107
|
+
) : null}
|
|
108
|
+
</aside>
|
|
109
|
+
|
|
110
|
+
<main className="main">
|
|
111
|
+
<Routes>
|
|
112
|
+
<Route path="/" element={<ProjectsHome />} />
|
|
113
|
+
<Route path="/project/:projectId" element={<ProjectHome />} />
|
|
114
|
+
<Route path="/project/:projectId/figure/:figureId/:variantId?" element={<FigureView />} />
|
|
115
|
+
<Route
|
|
116
|
+
path="*"
|
|
117
|
+
element={
|
|
118
|
+
<div className="empty">
|
|
119
|
+
<div className="emptyTitle">Not found</div>
|
|
120
|
+
<div className="emptyBody">
|
|
121
|
+
Go back to <Link to="/">home</Link>.
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
}
|
|
125
|
+
/>
|
|
126
|
+
</Routes>
|
|
127
|
+
</main>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { HashRouter, Route, Routes } from 'react-router-dom';
|
|
2
|
+
import { RenderView } from './routes/RenderView';
|
|
3
|
+
import { StudioApp } from './StudioApp';
|
|
4
|
+
|
|
5
|
+
export function StudioRoot() {
|
|
6
|
+
return (
|
|
7
|
+
<HashRouter>
|
|
8
|
+
<Routes>
|
|
9
|
+
<Route path="/render/:projectId/:figureId/:variantId?" element={<RenderView />} />
|
|
10
|
+
<Route path="/*" element={<StudioApp />} />
|
|
11
|
+
</Routes>
|
|
12
|
+
</HashRouter>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function base64UrlDecodeToString(input: string): string {
|
|
2
|
+
const normalized = input.replace(/-/g, '+').replace(/_/g, '/');
|
|
3
|
+
const padded = normalized + '='.repeat((4 - (normalized.length % 4)) % 4);
|
|
4
|
+
const binary = atob(padded);
|
|
5
|
+
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
|
|
6
|
+
return new TextDecoder().decode(bytes);
|
|
7
|
+
}
|
|
8
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ComponentType } from 'react';
|
|
2
|
+
|
|
3
|
+
type FigureModule = { default: ComponentType<any> };
|
|
4
|
+
|
|
5
|
+
const modules = import.meta.glob<FigureModule>('../../projects/*/figures/*.tsx');
|
|
6
|
+
|
|
7
|
+
const byProject: Record<string, Record<string, () => Promise<FigureModule>>> = {};
|
|
8
|
+
for (const [modulePath, loader] of Object.entries(modules)) {
|
|
9
|
+
const match = modulePath.match(/projects\/([^/]+)\/figures\/([^/]+)\.tsx$/);
|
|
10
|
+
if (!match) continue;
|
|
11
|
+
const projectId = match[1]!;
|
|
12
|
+
const moduleKey = match[2]!;
|
|
13
|
+
byProject[projectId] = byProject[projectId] ?? {};
|
|
14
|
+
byProject[projectId]![moduleKey] = loader;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function availableFigureModuleKeys(projectId: string): string[] {
|
|
18
|
+
return Object.keys(byProject[projectId] ?? {}).sort();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function loadFigureComponent(projectId: string, moduleKey: string): Promise<ComponentType<any>> {
|
|
22
|
+
const loader = byProject[projectId]?.[moduleKey];
|
|
23
|
+
if (!loader) {
|
|
24
|
+
throw new Error(
|
|
25
|
+
`Unknown moduleKey "${moduleKey}" for project "${projectId}". Available: ${availableFigureModuleKeys(projectId).join(', ')}`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
const mod = await loader();
|
|
29
|
+
return mod.default;
|
|
30
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ProjectDefinition } from '../core/manifest';
|
|
2
|
+
|
|
3
|
+
type ProjectModule = { default: ProjectDefinition };
|
|
4
|
+
|
|
5
|
+
const modules = import.meta.glob<ProjectModule>('../../projects/*/project.ts');
|
|
6
|
+
|
|
7
|
+
const byId: Record<string, () => Promise<ProjectModule>> = Object.fromEntries(
|
|
8
|
+
Object.entries(modules)
|
|
9
|
+
.map(([modulePath, loader]) => {
|
|
10
|
+
const match = modulePath.match(/projects\/([^/]+)\/project\.ts$/);
|
|
11
|
+
if (!match) return null;
|
|
12
|
+
return [match[1]!, loader] as const;
|
|
13
|
+
})
|
|
14
|
+
.filter((x): x is readonly [string, () => Promise<ProjectModule>] => Boolean(x))
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const cache = new Map<string, Promise<ProjectDefinition>>();
|
|
18
|
+
|
|
19
|
+
export function availableProjectIds(): string[] {
|
|
20
|
+
return Object.keys(byId).sort();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function loadProject(projectId: string): Promise<ProjectDefinition> {
|
|
24
|
+
const loader = byId[projectId];
|
|
25
|
+
if (!loader) throw new Error(`Unknown projectId "${projectId}". Available: ${availableProjectIds().join(', ')}`);
|
|
26
|
+
|
|
27
|
+
const cached = cache.get(projectId);
|
|
28
|
+
if (cached) return cached;
|
|
29
|
+
|
|
30
|
+
const promise = loader().then((m) => m.default);
|
|
31
|
+
cache.set(projectId, promise);
|
|
32
|
+
return promise;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function loadAllProjects(): Promise<ProjectDefinition[]> {
|
|
36
|
+
const ids = availableProjectIds();
|
|
37
|
+
const projects = await Promise.all(ids.map((id) => loadProject(id)));
|
|
38
|
+
return projects.slice().sort((a, b) => a.title.localeCompare(b.title));
|
|
39
|
+
}
|
|
40
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { emptyPropsFile, validatePropsFileV1, type PropsFileV1 } from '../core/manifest';
|
|
2
|
+
|
|
3
|
+
function propsUrl(projectId: string): string {
|
|
4
|
+
const u = new URL('/__imagine/props', window.location.origin);
|
|
5
|
+
u.searchParams.set('projectId', projectId);
|
|
6
|
+
return u.toString();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function fetchPropsFile(projectId: string): Promise<PropsFileV1> {
|
|
10
|
+
const res = await fetch(propsUrl(projectId), { method: 'GET' });
|
|
11
|
+
if (!res.ok) throw new Error(`GET props failed: ${res.status} ${res.statusText}`);
|
|
12
|
+
const json = await res.json();
|
|
13
|
+
return validatePropsFileV1(json);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function savePropsFile(projectId: string, file: PropsFileV1): Promise<void> {
|
|
17
|
+
const res = await fetch(propsUrl(projectId), {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: { 'content-type': 'application/json' },
|
|
20
|
+
body: JSON.stringify(file)
|
|
21
|
+
});
|
|
22
|
+
if (!res.ok) throw new Error(`POST props failed: ${res.status} ${res.statusText}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { emptyPropsFile };
|
|
26
|
+
|