@sanity-labs/slides 0.0.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/README.md +241 -0
- package/SKILL.md +119 -0
- package/dist/cli.d.ts +38 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +386 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/components.d.ts +179 -0
- package/dist/core/components.d.ts.map +1 -0
- package/dist/core/components.js +40 -0
- package/dist/core/components.js.map +1 -0
- package/dist/core/fake-runtime.d.ts +138 -0
- package/dist/core/fake-runtime.d.ts.map +1 -0
- package/dist/core/fake-runtime.js +210 -0
- package/dist/core/fake-runtime.js.map +1 -0
- package/dist/core/font-resolver.d.ts +28 -0
- package/dist/core/font-resolver.d.ts.map +1 -0
- package/dist/core/font-resolver.js +30 -0
- package/dist/core/font-resolver.js.map +1 -0
- package/dist/core/geometry.d.ts +71 -0
- package/dist/core/geometry.d.ts.map +1 -0
- package/dist/core/geometry.js +44 -0
- package/dist/core/geometry.js.map +1 -0
- package/dist/core/index.d.ts +19 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +20 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/manifest.d.ts +123 -0
- package/dist/core/manifest.d.ts.map +1 -0
- package/dist/core/manifest.js +43 -0
- package/dist/core/manifest.js.map +1 -0
- package/dist/core/op-translator-pptx.d.ts +150 -0
- package/dist/core/op-translator-pptx.d.ts.map +1 -0
- package/dist/core/op-translator-pptx.js +245 -0
- package/dist/core/op-translator-pptx.js.map +1 -0
- package/dist/core/pptx-runtime.d.ts +103 -0
- package/dist/core/pptx-runtime.d.ts.map +1 -0
- package/dist/core/pptx-runtime.js +405 -0
- package/dist/core/pptx-runtime.js.map +1 -0
- package/dist/core/reconciler.d.ts +113 -0
- package/dist/core/reconciler.d.ts.map +1 -0
- package/dist/core/reconciler.js +453 -0
- package/dist/core/reconciler.js.map +1 -0
- package/dist/core/runtime.d.ts +161 -0
- package/dist/core/runtime.d.ts.map +1 -0
- package/dist/core/runtime.js +11 -0
- package/dist/core/runtime.js.map +1 -0
- package/dist/core/template.d.ts +32 -0
- package/dist/core/template.d.ts.map +1 -0
- package/dist/core/template.js +3 -0
- package/dist/core/template.js.map +1 -0
- package/dist/dev/auto-examples.d.ts +6 -0
- package/dist/dev/auto-examples.d.ts.map +1 -0
- package/dist/dev/auto-examples.js +79 -0
- package/dist/dev/auto-examples.js.map +1 -0
- package/dist/dev/bin/slides-dev.d.ts +3 -0
- package/dist/dev/bin/slides-dev.d.ts.map +1 -0
- package/dist/dev/bin/slides-dev.js +87 -0
- package/dist/dev/bin/slides-dev.js.map +1 -0
- package/dist/dev/bin/slides-dev.mjs +24 -0
- package/dist/dev/compose-deck.d.ts +18 -0
- package/dist/dev/compose-deck.d.ts.map +1 -0
- package/dist/dev/compose-deck.js +19 -0
- package/dist/dev/compose-deck.js.map +1 -0
- package/dist/dev/deck-viewer.d.ts +19 -0
- package/dist/dev/deck-viewer.d.ts.map +1 -0
- package/dist/dev/deck-viewer.js +237 -0
- package/dist/dev/deck-viewer.js.map +1 -0
- package/dist/dev/dev-server/client/entry.d.ts +2 -0
- package/dist/dev/dev-server/client/entry.d.ts.map +1 -0
- package/dist/dev/dev-server/client/entry.js +12 -0
- package/dist/dev/dev-server/client/entry.js.map +1 -0
- package/dist/dev/dev-server/output.d.ts +8 -0
- package/dist/dev/dev-server/output.d.ts.map +1 -0
- package/dist/dev/dev-server/output.js +32 -0
- package/dist/dev/dev-server/output.js.map +1 -0
- package/dist/dev/dev-server/server-only-stub.d.ts +7 -0
- package/dist/dev/dev-server/server-only-stub.d.ts.map +1 -0
- package/dist/dev/dev-server/server-only-stub.js +12 -0
- package/dist/dev/dev-server/server-only-stub.js.map +1 -0
- package/dist/dev/dev-server/start.d.ts +14 -0
- package/dist/dev/dev-server/start.d.ts.map +1 -0
- package/dist/dev/dev-server/start.js +135 -0
- package/dist/dev/dev-server/start.js.map +1 -0
- package/dist/dev/index.d.ts +5 -0
- package/dist/dev/index.d.ts.map +1 -0
- package/dist/dev/index.js +5 -0
- package/dist/dev/index.js.map +1 -0
- package/dist/dev/lib/cn.d.ts +3 -0
- package/dist/dev/lib/cn.d.ts.map +1 -0
- package/dist/dev/lib/cn.js +3 -0
- package/dist/dev/lib/cn.js.map +1 -0
- package/dist/dev/slide-canvas.d.ts +12 -0
- package/dist/dev/slide-canvas.d.ts.map +1 -0
- package/dist/dev/slide-canvas.js +123 -0
- package/dist/dev/slide-canvas.js.map +1 -0
- package/dist/dev/styles.css +37 -0
- package/dist/dev/ui/icon-button.d.ts +12 -0
- package/dist/dev/ui/icon-button.d.ts.map +1 -0
- package/dist/dev/ui/icon-button.js +6 -0
- package/dist/dev/ui/icon-button.js.map +1 -0
- package/dist/dev/ui/kbd.d.ts +6 -0
- package/dist/dev/ui/kbd.d.ts.map +1 -0
- package/dist/dev/ui/kbd.js +4 -0
- package/dist/dev/ui/kbd.js.map +1 -0
- package/dist/dev/ui/text-button.d.ts +10 -0
- package/dist/dev/ui/text-button.d.ts.map +1 -0
- package/dist/dev/ui/text-button.js +6 -0
- package/dist/dev/ui/text-button.js.map +1 -0
- package/dist/dev/url-state.d.ts +7 -0
- package/dist/dev/url-state.d.ts.map +1 -0
- package/dist/dev/url-state.js +13 -0
- package/dist/dev/url-state.js.map +1 -0
- package/dist/dev/use-keyboard-nav.d.ts +17 -0
- package/dist/dev/use-keyboard-nav.d.ts.map +1 -0
- package/dist/dev/use-keyboard-nav.js +53 -0
- package/dist/dev/use-keyboard-nav.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/errors.d.ts +57 -0
- package/dist/mcp/errors.d.ts.map +1 -0
- package/dist/mcp/errors.js +44 -0
- package/dist/mcp/errors.js.map +1 -0
- package/dist/mcp/index.d.ts +29 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +29 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/naming.d.ts +37 -0
- package/dist/mcp/naming.d.ts.map +1 -0
- package/dist/mcp/naming.js +43 -0
- package/dist/mcp/naming.js.map +1 -0
- package/dist/mcp/render.d.ts +45 -0
- package/dist/mcp/render.d.ts.map +1 -0
- package/dist/mcp/render.js +77 -0
- package/dist/mcp/render.js.map +1 -0
- package/dist/mcp/schema.d.ts +54 -0
- package/dist/mcp/schema.d.ts.map +1 -0
- package/dist/mcp/schema.js +55 -0
- package/dist/mcp/schema.js.map +1 -0
- package/dist/mcp/server.d.ts +63 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +196 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/scaffold/index.d.ts +39 -0
- package/dist/scaffold/index.d.ts.map +1 -0
- package/dist/scaffold/index.js +84 -0
- package/dist/scaffold/index.js.map +1 -0
- package/dist/scaffold/template-base/README.md +134 -0
- package/dist/scaffold/template-base/_gitignore +4 -0
- package/dist/scaffold/template-base/package.json +35 -0
- package/dist/scaffold/template-base/src/components/Cover.tsx +30 -0
- package/dist/scaffold/template-base/src/index.ts +27 -0
- package/dist/scaffold/template-base/src/preview.tsx +9 -0
- package/dist/scaffold/template-base/tsconfig.build.json +10 -0
- package/dist/scaffold/template-base/tsconfig.json +18 -0
- package/package.json +164 -0
- package/src/__tests__/fixtures/test-template/index.tsx +77 -0
- package/src/__tests__/pptx-mcp.test.ts +85 -0
- package/src/__tests__/pptx-smoke.test.ts +45 -0
- package/src/__tests__/preview.test.ts +28 -0
- package/src/cli.ts +426 -0
- package/src/core/__snapshots__/reconciler.test.ts.snap +320 -0
- package/src/core/components.test.ts +57 -0
- package/src/core/components.ts +196 -0
- package/src/core/fake-runtime.test.ts +174 -0
- package/src/core/fake-runtime.ts +302 -0
- package/src/core/font-resolver.ts +46 -0
- package/src/core/geometry.test.ts +58 -0
- package/src/core/geometry.ts +91 -0
- package/src/core/index.ts +69 -0
- package/src/core/manifest.test.ts +33 -0
- package/src/core/manifest.ts +150 -0
- package/src/core/op-translator-pptx.test.ts +204 -0
- package/src/core/op-translator-pptx.ts +365 -0
- package/src/core/pptx-runtime.test.ts +137 -0
- package/src/core/pptx-runtime.ts +504 -0
- package/src/core/reconciler.test.ts +644 -0
- package/src/core/reconciler.ts +603 -0
- package/src/core/runtime.ts +150 -0
- package/src/core/template.test.ts +136 -0
- package/src/core/template.ts +37 -0
- package/src/dev/auto-examples.ts +89 -0
- package/src/dev/bin/slides-dev.mjs +24 -0
- package/src/dev/bin/slides-dev.ts +101 -0
- package/src/dev/compose-deck.test.ts +68 -0
- package/src/dev/compose-deck.ts +40 -0
- package/src/dev/deck-viewer.tsx +677 -0
- package/src/dev/dev-server/client/entry.tsx +15 -0
- package/src/dev/dev-server/client/index.html +24 -0
- package/src/dev/dev-server/output.ts +37 -0
- package/src/dev/dev-server/server-only-stub.ts +12 -0
- package/src/dev/dev-server/start.ts +155 -0
- package/src/dev/index.ts +4 -0
- package/src/dev/lib/cn.ts +3 -0
- package/src/dev/slide-canvas.test.tsx +66 -0
- package/src/dev/slide-canvas.tsx +170 -0
- package/src/dev/styles.css +37 -0
- package/src/dev/ui/icon-button.tsx +31 -0
- package/src/dev/ui/kbd.tsx +20 -0
- package/src/dev/ui/text-button.tsx +31 -0
- package/src/dev/url-state.test.ts +22 -0
- package/src/dev/url-state.ts +17 -0
- package/src/dev/use-keyboard-nav.ts +64 -0
- package/src/index.ts +17 -0
- package/src/mcp/errors.test.ts +51 -0
- package/src/mcp/errors.ts +76 -0
- package/src/mcp/index.ts +45 -0
- package/src/mcp/naming.test.ts +39 -0
- package/src/mcp/naming.ts +49 -0
- package/src/mcp/render.ts +110 -0
- package/src/mcp/schema.test.ts +86 -0
- package/src/mcp/schema.ts +93 -0
- package/src/mcp/server.test.ts +309 -0
- package/src/mcp/server.ts +276 -0
- package/src/scaffold/index.ts +102 -0
- package/src/scaffold/template-base/README.md +134 -0
- package/src/scaffold/template-base/_gitignore +4 -0
- package/src/scaffold/template-base/package.json +35 -0
- package/src/scaffold/template-base/src/components/Cover.tsx +30 -0
- package/src/scaffold/template-base/src/index.ts +27 -0
- package/src/scaffold/template-base/src/preview.tsx +9 -0
- package/src/scaffold/template-base/tsconfig.build.json +10 -0
- package/src/scaffold/template-base/tsconfig.json +18 -0
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
CircleAlert,
|
|
5
|
+
Fullscreen,
|
|
6
|
+
Menu,
|
|
7
|
+
Minus,
|
|
8
|
+
Plus,
|
|
9
|
+
Command,
|
|
10
|
+
Download,
|
|
11
|
+
Loader2,
|
|
12
|
+
TriangleAlert,
|
|
13
|
+
} from 'lucide-react';
|
|
14
|
+
import {
|
|
15
|
+
TransformWrapper,
|
|
16
|
+
TransformComponent,
|
|
17
|
+
type ReactZoomPanPinchRef,
|
|
18
|
+
} from 'react-zoom-pan-pinch';
|
|
19
|
+
import type { FakeDeck } from '../core/fake-runtime.js';
|
|
20
|
+
import type { Template } from '../core/template.js';
|
|
21
|
+
import { composeDeck, type ComposedDeck } from './compose-deck.js';
|
|
22
|
+
import { deriveAutoPreview } from './auto-examples.js';
|
|
23
|
+
import { SlideCanvas } from './slide-canvas.js';
|
|
24
|
+
import { SHORTCUT_LIST, useKeyboardNav } from './use-keyboard-nav.js';
|
|
25
|
+
import {
|
|
26
|
+
DEFAULT_URL_STATE,
|
|
27
|
+
parseUrlState,
|
|
28
|
+
serializeUrlState,
|
|
29
|
+
type ViewerUrlState,
|
|
30
|
+
} from './url-state.js';
|
|
31
|
+
import { cn } from './lib/cn.js';
|
|
32
|
+
import { IconButton } from './ui/icon-button.js';
|
|
33
|
+
import { TextButton } from './ui/text-button.js';
|
|
34
|
+
import { Kbd } from './ui/kbd.js';
|
|
35
|
+
|
|
36
|
+
const NAV_VISIBLE_KEY = 'slides-dev:nav-visible';
|
|
37
|
+
const THUMB_W = 228;
|
|
38
|
+
const MIN_SCALE = 0.1;
|
|
39
|
+
const MAX_SCALE = 5;
|
|
40
|
+
const ZOOM_STEP = 0.2;
|
|
41
|
+
const ANIMATION_MS = 200;
|
|
42
|
+
const VIEWPORT_PADDING = 48;
|
|
43
|
+
|
|
44
|
+
export type DeckViewerProps = {
|
|
45
|
+
readonly template: Template;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
type AsyncDeckState =
|
|
49
|
+
| { status: 'pending' }
|
|
50
|
+
| { status: 'error'; error: Error }
|
|
51
|
+
| { status: 'ready'; composed: ComposedDeck };
|
|
52
|
+
|
|
53
|
+
export const DeckViewer = ({ template }: DeckViewerProps): ReactElement => {
|
|
54
|
+
const [url, setUrl] = useUrlState();
|
|
55
|
+
const [navVisible, setNavVisible] = usePersistedBool(NAV_VISIBLE_KEY, true);
|
|
56
|
+
const [showShortcuts, setShowShortcuts] = useState(false);
|
|
57
|
+
const [scale, setScale] = useState(1);
|
|
58
|
+
const [autoFit, setAutoFit] = useState(true);
|
|
59
|
+
|
|
60
|
+
const wrapperRef = useRef<ReactZoomPanPinchRef | null>(null);
|
|
61
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
62
|
+
|
|
63
|
+
const async = useDeckCompose(template);
|
|
64
|
+
const slideCount = async.status === 'ready' ? async.composed.deck.slideOrder.length : 0;
|
|
65
|
+
|
|
66
|
+
const setSlide = useCallback(
|
|
67
|
+
(n: number) => setUrl({ slide: Math.max(0, Math.min(n, slideCount - 1)) }),
|
|
68
|
+
[setUrl, slideCount],
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const fitToViewport = useCallback(() => {
|
|
72
|
+
const wrapper = wrapperRef.current;
|
|
73
|
+
const container = containerRef.current;
|
|
74
|
+
if (!wrapper || !container) return;
|
|
75
|
+
const fit = computeFitScale(container, template.canvas);
|
|
76
|
+
const x = (container.clientWidth - template.canvas.w * fit) / 2;
|
|
77
|
+
const y = (container.clientHeight - template.canvas.h * fit) / 2;
|
|
78
|
+
wrapper.setTransform(x, y, fit, ANIMATION_MS, 'easeOut');
|
|
79
|
+
setAutoFit(true);
|
|
80
|
+
}, [template.canvas]);
|
|
81
|
+
|
|
82
|
+
const zoomBy = useCallback((dir: 1 | -1) => {
|
|
83
|
+
const wrapper = wrapperRef.current;
|
|
84
|
+
if (!wrapper) return;
|
|
85
|
+
setAutoFit(false);
|
|
86
|
+
if (dir === 1) wrapper.zoomIn(ZOOM_STEP, ANIMATION_MS, 'easeOut');
|
|
87
|
+
else wrapper.zoomOut(ZOOM_STEP, ANIMATION_MS, 'easeOut');
|
|
88
|
+
}, []);
|
|
89
|
+
|
|
90
|
+
useDebugHandle({ template, async });
|
|
91
|
+
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
const container = containerRef.current;
|
|
94
|
+
if (!container || !autoFit) return;
|
|
95
|
+
let raf = 0;
|
|
96
|
+
const ro = new ResizeObserver(() => {
|
|
97
|
+
cancelAnimationFrame(raf);
|
|
98
|
+
raf = requestAnimationFrame(() => fitToViewport());
|
|
99
|
+
});
|
|
100
|
+
ro.observe(container);
|
|
101
|
+
return () => {
|
|
102
|
+
ro.disconnect();
|
|
103
|
+
cancelAnimationFrame(raf);
|
|
104
|
+
};
|
|
105
|
+
}, [autoFit, fitToViewport]);
|
|
106
|
+
useKeyboardNav({
|
|
107
|
+
onNext: () => setSlide(url.slide + 1),
|
|
108
|
+
onPrev: () => setSlide(url.slide - 1),
|
|
109
|
+
onFirst: () => setSlide(0),
|
|
110
|
+
onLast: () => setSlide(slideCount - 1),
|
|
111
|
+
onZoomIn: () => zoomBy(1),
|
|
112
|
+
onZoomOut: () => zoomBy(-1),
|
|
113
|
+
onZoomFit: fitToViewport,
|
|
114
|
+
onToggleNav: () => setNavVisible((v) => !v),
|
|
115
|
+
onShowHelp: () => setShowShortcuts((v) => !v),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div className="flex h-dvh flex-col antialiased isolate">
|
|
120
|
+
<TopBar
|
|
121
|
+
template={template}
|
|
122
|
+
navVisible={navVisible}
|
|
123
|
+
toggleNav={() => setNavVisible((v) => !v)}
|
|
124
|
+
slideIndex={url.slide}
|
|
125
|
+
slideCount={slideCount}
|
|
126
|
+
showShortcuts={showShortcuts}
|
|
127
|
+
toggleShortcuts={() => setShowShortcuts((v) => !v)}
|
|
128
|
+
/>
|
|
129
|
+
<div className="flex min-h-0 flex-1">
|
|
130
|
+
<NavRailContainer visible={navVisible}>
|
|
131
|
+
<NavRail template={template} async={async} slideIndex={url.slide} onSelect={setSlide} />
|
|
132
|
+
</NavRailContainer>
|
|
133
|
+
<Viewport
|
|
134
|
+
template={template}
|
|
135
|
+
async={async}
|
|
136
|
+
slideIndex={url.slide}
|
|
137
|
+
containerRef={containerRef}
|
|
138
|
+
wrapperRef={wrapperRef}
|
|
139
|
+
onTransform={(_, state) => setScale(state.scale)}
|
|
140
|
+
onUserZoom={() => setAutoFit(false)}
|
|
141
|
+
onClampSlide={setSlide}
|
|
142
|
+
/>
|
|
143
|
+
</div>
|
|
144
|
+
<FloatingZoom
|
|
145
|
+
scale={scale}
|
|
146
|
+
isFit={autoFit}
|
|
147
|
+
onFit={fitToViewport}
|
|
148
|
+
onZoomIn={() => zoomBy(1)}
|
|
149
|
+
onZoomOut={() => zoomBy(-1)}
|
|
150
|
+
/>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const computeFitScale = (container: HTMLElement, canvas: { w: number; h: number }): number => {
|
|
156
|
+
const w = container.clientWidth - VIEWPORT_PADDING * 2;
|
|
157
|
+
const h = container.clientHeight - VIEWPORT_PADDING * 2;
|
|
158
|
+
return Math.max(MIN_SCALE, Math.min(w / canvas.w, h / canvas.h));
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const useUrlState = (): [ViewerUrlState, (next: ViewerUrlState) => void] => {
|
|
162
|
+
const [state, setState] = useState<ViewerUrlState>(() =>
|
|
163
|
+
typeof window === 'undefined' ? DEFAULT_URL_STATE : parseUrlState(window.location.hash),
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
const onHashChange = () => setState(parseUrlState(window.location.hash));
|
|
168
|
+
window.addEventListener('hashchange', onHashChange);
|
|
169
|
+
return () => window.removeEventListener('hashchange', onHashChange);
|
|
170
|
+
}, []);
|
|
171
|
+
|
|
172
|
+
const setUrl = useCallback((next: ViewerUrlState) => {
|
|
173
|
+
setState(next);
|
|
174
|
+
const serialized = serializeUrlState(next);
|
|
175
|
+
if (typeof window !== 'undefined' && window.location.hash !== serialized) {
|
|
176
|
+
window.history.replaceState(null, '', serialized || window.location.pathname);
|
|
177
|
+
}
|
|
178
|
+
}, []);
|
|
179
|
+
|
|
180
|
+
return [state, setUrl];
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const usePersistedBool = (
|
|
184
|
+
key: string,
|
|
185
|
+
fallback: boolean,
|
|
186
|
+
): [boolean, (next: boolean | ((prev: boolean) => boolean)) => void] => {
|
|
187
|
+
const [state, setState] = useState<boolean>(() => {
|
|
188
|
+
if (typeof window === 'undefined') return fallback;
|
|
189
|
+
const stored = window.localStorage.getItem(key);
|
|
190
|
+
return stored === null ? fallback : stored === 'true';
|
|
191
|
+
});
|
|
192
|
+
const set = useCallback(
|
|
193
|
+
(next: boolean | ((prev: boolean) => boolean)) => {
|
|
194
|
+
setState((prev) => {
|
|
195
|
+
const resolved = typeof next === 'function' ? next(prev) : next;
|
|
196
|
+
if (typeof window !== 'undefined') window.localStorage.setItem(key, String(resolved));
|
|
197
|
+
return resolved;
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
[key],
|
|
201
|
+
);
|
|
202
|
+
return [state, set];
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const useDeckCompose = (template: Template): AsyncDeckState => {
|
|
206
|
+
const [state, setState] = useState<AsyncDeckState>({ status: 'pending' });
|
|
207
|
+
useEffect(() => {
|
|
208
|
+
let cancelled = false;
|
|
209
|
+
setState({ status: 'pending' });
|
|
210
|
+
(async () => {
|
|
211
|
+
try {
|
|
212
|
+
const tree = template.preview ? template.preview() : deriveAutoPreview(template);
|
|
213
|
+
const composed = await composeDeck({ tree, template });
|
|
214
|
+
if (!cancelled) setState({ status: 'ready', composed });
|
|
215
|
+
} catch (err) {
|
|
216
|
+
if (!cancelled) {
|
|
217
|
+
setState({
|
|
218
|
+
status: 'error',
|
|
219
|
+
error: err instanceof Error ? err : new Error(String(err)),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
})();
|
|
224
|
+
return () => {
|
|
225
|
+
cancelled = true;
|
|
226
|
+
};
|
|
227
|
+
}, [template]);
|
|
228
|
+
return state;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
declare global {
|
|
232
|
+
interface Window {
|
|
233
|
+
__slides?: {
|
|
234
|
+
template: Template;
|
|
235
|
+
deck?: FakeDeck;
|
|
236
|
+
ops?: ComposedDeck['ops'];
|
|
237
|
+
manifest?: ComposedDeck['manifest'];
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const useDebugHandle = ({
|
|
243
|
+
template,
|
|
244
|
+
async,
|
|
245
|
+
}: {
|
|
246
|
+
template: Template;
|
|
247
|
+
async: AsyncDeckState;
|
|
248
|
+
}): void => {
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
window.__slides = {
|
|
251
|
+
template,
|
|
252
|
+
...(async.status === 'ready'
|
|
253
|
+
? {
|
|
254
|
+
deck: async.composed.deck,
|
|
255
|
+
ops: async.composed.ops,
|
|
256
|
+
manifest: async.composed.manifest,
|
|
257
|
+
}
|
|
258
|
+
: {}),
|
|
259
|
+
};
|
|
260
|
+
}, [template, async]);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const TopBar = ({
|
|
264
|
+
template,
|
|
265
|
+
navVisible,
|
|
266
|
+
toggleNav,
|
|
267
|
+
slideIndex,
|
|
268
|
+
slideCount,
|
|
269
|
+
showShortcuts,
|
|
270
|
+
toggleShortcuts,
|
|
271
|
+
}: {
|
|
272
|
+
template: Template;
|
|
273
|
+
navVisible: boolean;
|
|
274
|
+
toggleNav: () => void;
|
|
275
|
+
slideIndex: number;
|
|
276
|
+
slideCount: number;
|
|
277
|
+
showShortcuts: boolean;
|
|
278
|
+
toggleShortcuts: () => void;
|
|
279
|
+
}): ReactElement => (
|
|
280
|
+
<header className="flex shrink-0 items-center justify-between gap-3 border-b border-border bg-paper px-3 py-2">
|
|
281
|
+
<div className="flex min-w-0 items-center gap-3">
|
|
282
|
+
<IconButton
|
|
283
|
+
icon={<Menu className="size-4" />}
|
|
284
|
+
label={navVisible ? 'Hide navigation' : 'Show navigation'}
|
|
285
|
+
selected={navVisible}
|
|
286
|
+
onClick={toggleNav}
|
|
287
|
+
/>
|
|
288
|
+
<h1 className="inline-flex shrink-0 items-center rounded-md bg-ink/5 px-2 py-1 text-xs font-medium text-ink">
|
|
289
|
+
{template.name}
|
|
290
|
+
</h1>
|
|
291
|
+
{slideCount > 0 && (
|
|
292
|
+
<span className="shrink-0 text-xs font-medium text-text-muted tabular-nums">
|
|
293
|
+
Slide {slideIndex + 1} of {slideCount}
|
|
294
|
+
</span>
|
|
295
|
+
)}
|
|
296
|
+
</div>
|
|
297
|
+
<div className="flex items-center gap-1">
|
|
298
|
+
<ShortcutsDropdown open={showShortcuts} onToggle={toggleShortcuts} />
|
|
299
|
+
<ExportButton templateName={template.name} />
|
|
300
|
+
</div>
|
|
301
|
+
</header>
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const NavRailContainer = ({
|
|
305
|
+
visible,
|
|
306
|
+
children,
|
|
307
|
+
}: {
|
|
308
|
+
visible: boolean;
|
|
309
|
+
children: ReactNode;
|
|
310
|
+
}): ReactElement => (
|
|
311
|
+
<div
|
|
312
|
+
inert={!visible}
|
|
313
|
+
aria-hidden={!visible}
|
|
314
|
+
className={cn(
|
|
315
|
+
'h-full shrink-0 overflow-hidden transition-[width] duration-300 ease-out',
|
|
316
|
+
visible ? 'w-[280px]' : 'w-0',
|
|
317
|
+
)}
|
|
318
|
+
>
|
|
319
|
+
{children}
|
|
320
|
+
</div>
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
const NavRail = ({
|
|
324
|
+
template,
|
|
325
|
+
async,
|
|
326
|
+
slideIndex,
|
|
327
|
+
onSelect,
|
|
328
|
+
}: {
|
|
329
|
+
template: Template;
|
|
330
|
+
async: AsyncDeckState;
|
|
331
|
+
slideIndex: number;
|
|
332
|
+
onSelect: (i: number) => void;
|
|
333
|
+
}): ReactElement => (
|
|
334
|
+
<nav
|
|
335
|
+
aria-label="Slides"
|
|
336
|
+
className="h-full w-[280px] shrink-0 overflow-y-auto border-r border-border bg-paper py-5 pr-4 pl-1.5"
|
|
337
|
+
>
|
|
338
|
+
{async.status !== 'ready' ? (
|
|
339
|
+
<div className="text-sm text-text-muted">Loading…</div>
|
|
340
|
+
) : (
|
|
341
|
+
<ul role="list" className="flex flex-col gap-5">
|
|
342
|
+
{async.composed.deck.slideOrder.map((slideId, i) => {
|
|
343
|
+
const slide = async.composed.deck.slides.get(slideId);
|
|
344
|
+
if (!slide) return null;
|
|
345
|
+
const isActive = i === slideIndex;
|
|
346
|
+
const thumbH = template.canvas.h * (THUMB_W / template.canvas.w);
|
|
347
|
+
return (
|
|
348
|
+
<li key={slideId}>
|
|
349
|
+
<button
|
|
350
|
+
type="button"
|
|
351
|
+
onClick={() => onSelect(i)}
|
|
352
|
+
aria-current={isActive ? 'true' : undefined}
|
|
353
|
+
aria-label={`Slide ${i + 1}`}
|
|
354
|
+
className="group flex w-full items-start gap-4 focus:outline-none focus-visible:outline-none"
|
|
355
|
+
>
|
|
356
|
+
<span
|
|
357
|
+
className={cn(
|
|
358
|
+
'w-7 shrink-0 pt-0.5 text-right text-sm font-medium tabular-nums',
|
|
359
|
+
isActive ? 'text-ink' : 'text-text-muted',
|
|
360
|
+
)}
|
|
361
|
+
>
|
|
362
|
+
{i + 1}
|
|
363
|
+
</span>
|
|
364
|
+
<div
|
|
365
|
+
className={cn(
|
|
366
|
+
'overflow-hidden rounded-lg bg-surface transition-shadow',
|
|
367
|
+
isActive
|
|
368
|
+
? 'ring-3 ring-focus'
|
|
369
|
+
: 'ring-1 ring-border group-hover:ring-border-strong group-focus-visible:ring-3 group-focus-visible:ring-focus',
|
|
370
|
+
)}
|
|
371
|
+
style={{ width: THUMB_W, height: thumbH }}
|
|
372
|
+
>
|
|
373
|
+
<div
|
|
374
|
+
className="pointer-events-none origin-top-left"
|
|
375
|
+
style={{ transform: `scale(${THUMB_W / template.canvas.w})` }}
|
|
376
|
+
>
|
|
377
|
+
<SlideCanvas
|
|
378
|
+
slide={slide}
|
|
379
|
+
deck={async.composed.deck}
|
|
380
|
+
canvas={template.canvas}
|
|
381
|
+
/>
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
</button>
|
|
385
|
+
</li>
|
|
386
|
+
);
|
|
387
|
+
})}
|
|
388
|
+
</ul>
|
|
389
|
+
)}
|
|
390
|
+
</nav>
|
|
391
|
+
);
|
|
392
|
+
|
|
393
|
+
const Viewport = ({
|
|
394
|
+
template,
|
|
395
|
+
async,
|
|
396
|
+
slideIndex,
|
|
397
|
+
containerRef,
|
|
398
|
+
onUserZoom,
|
|
399
|
+
wrapperRef,
|
|
400
|
+
onTransform,
|
|
401
|
+
onClampSlide,
|
|
402
|
+
}: {
|
|
403
|
+
template: Template;
|
|
404
|
+
async: AsyncDeckState;
|
|
405
|
+
slideIndex: number;
|
|
406
|
+
containerRef: React.RefObject<HTMLDivElement | null>;
|
|
407
|
+
wrapperRef: React.MutableRefObject<ReactZoomPanPinchRef | null>;
|
|
408
|
+
onTransform: (ref: ReactZoomPanPinchRef, state: { scale: number }) => void;
|
|
409
|
+
onUserZoom: () => void;
|
|
410
|
+
onClampSlide: (i: number) => void;
|
|
411
|
+
}): ReactElement => (
|
|
412
|
+
<main ref={containerRef} className="relative min-w-0 flex-1 overflow-hidden">
|
|
413
|
+
{async.status === 'pending' && (
|
|
414
|
+
<Center>
|
|
415
|
+
<ViewerStatus icon={<Spinner />} message="Composing deck…" />
|
|
416
|
+
</Center>
|
|
417
|
+
)}
|
|
418
|
+
{async.status === 'error' && (
|
|
419
|
+
<Center>
|
|
420
|
+
<ErrorPanel error={async.error} />
|
|
421
|
+
</Center>
|
|
422
|
+
)}
|
|
423
|
+
{async.status === 'ready' && (
|
|
424
|
+
<ActiveSlide
|
|
425
|
+
template={template}
|
|
426
|
+
deck={async.composed.deck}
|
|
427
|
+
slideIndex={slideIndex}
|
|
428
|
+
containerEl={containerRef.current}
|
|
429
|
+
wrapperRef={wrapperRef}
|
|
430
|
+
onTransform={onTransform}
|
|
431
|
+
onUserZoom={onUserZoom}
|
|
432
|
+
onClampSlide={onClampSlide}
|
|
433
|
+
/>
|
|
434
|
+
)}
|
|
435
|
+
</main>
|
|
436
|
+
);
|
|
437
|
+
|
|
438
|
+
const ActiveSlide = ({
|
|
439
|
+
template,
|
|
440
|
+
deck,
|
|
441
|
+
slideIndex,
|
|
442
|
+
containerEl,
|
|
443
|
+
wrapperRef,
|
|
444
|
+
onTransform,
|
|
445
|
+
onUserZoom,
|
|
446
|
+
onClampSlide,
|
|
447
|
+
}: {
|
|
448
|
+
template: Template;
|
|
449
|
+
deck: FakeDeck;
|
|
450
|
+
slideIndex: number;
|
|
451
|
+
containerEl: HTMLDivElement | null;
|
|
452
|
+
wrapperRef: React.MutableRefObject<ReactZoomPanPinchRef | null>;
|
|
453
|
+
onTransform: (ref: ReactZoomPanPinchRef, state: { scale: number }) => void;
|
|
454
|
+
onUserZoom: () => void;
|
|
455
|
+
onClampSlide: (i: number) => void;
|
|
456
|
+
}): ReactElement | null => {
|
|
457
|
+
const count = deck.slideOrder.length;
|
|
458
|
+
const clamped = Math.max(0, Math.min(slideIndex, count - 1));
|
|
459
|
+
|
|
460
|
+
useEffect(() => {
|
|
461
|
+
if (clamped !== slideIndex) onClampSlide(clamped);
|
|
462
|
+
}, [clamped, slideIndex, onClampSlide]);
|
|
463
|
+
|
|
464
|
+
if (count === 0) {
|
|
465
|
+
return (
|
|
466
|
+
<Center>
|
|
467
|
+
<ViewerStatus
|
|
468
|
+
icon={<TriangleAlert className="size-4" />}
|
|
469
|
+
message="This template renders zero slides."
|
|
470
|
+
/>
|
|
471
|
+
</Center>
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const slideId = deck.slideOrder[clamped];
|
|
476
|
+
const slide = slideId ? deck.slides.get(slideId) : undefined;
|
|
477
|
+
if (!slide) return <Center>Slide not found.</Center>;
|
|
478
|
+
|
|
479
|
+
const initialScale = containerEl ? computeFitScale(containerEl, template.canvas) : 1;
|
|
480
|
+
|
|
481
|
+
return (
|
|
482
|
+
<TransformWrapper
|
|
483
|
+
key={clamped}
|
|
484
|
+
ref={wrapperRef}
|
|
485
|
+
initialScale={initialScale}
|
|
486
|
+
minScale={MIN_SCALE}
|
|
487
|
+
maxScale={MAX_SCALE}
|
|
488
|
+
centerOnInit
|
|
489
|
+
limitToBounds={false}
|
|
490
|
+
doubleClick={{ disabled: true }}
|
|
491
|
+
wheel={{ step: 0.04 }}
|
|
492
|
+
onTransform={onTransform}
|
|
493
|
+
onWheelStart={onUserZoom}
|
|
494
|
+
onPinchStart={onUserZoom}
|
|
495
|
+
onZoomStart={onUserZoom}
|
|
496
|
+
>
|
|
497
|
+
<TransformComponent
|
|
498
|
+
wrapperClass="!w-full !h-full !cursor-grab active:!cursor-grabbing"
|
|
499
|
+
contentClass="!shadow-[0_4px_32px_rgb(0_0_0_/_0.12)] ring-1 ring-black/5 rounded"
|
|
500
|
+
>
|
|
501
|
+
<div style={{ width: template.canvas.w, height: template.canvas.h }}>
|
|
502
|
+
<SlideCanvas slide={slide} deck={deck} canvas={template.canvas} />
|
|
503
|
+
</div>
|
|
504
|
+
</TransformComponent>
|
|
505
|
+
</TransformWrapper>
|
|
506
|
+
);
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
const FloatingZoom = ({
|
|
510
|
+
scale,
|
|
511
|
+
isFit,
|
|
512
|
+
onFit,
|
|
513
|
+
onZoomIn,
|
|
514
|
+
onZoomOut,
|
|
515
|
+
}: {
|
|
516
|
+
scale: number;
|
|
517
|
+
isFit: boolean;
|
|
518
|
+
onFit: () => void;
|
|
519
|
+
onZoomIn: () => void;
|
|
520
|
+
onZoomOut: () => void;
|
|
521
|
+
}): ReactElement => (
|
|
522
|
+
<div className="pointer-events-none fixed right-4 bottom-4 z-10 flex items-center gap-1 rounded-full bg-paper p-1 shadow-[0_6px_24px_rgb(0_0_0_/_0.1),_0_2px_6px_rgb(0_0_0_/_0.06)] ring-1 ring-border [&_*]:pointer-events-auto">
|
|
523
|
+
<IconButton icon={<Minus className="size-4" />} label="Zoom out" onClick={onZoomOut} />
|
|
524
|
+
<button
|
|
525
|
+
type="button"
|
|
526
|
+
onDoubleClick={onFit}
|
|
527
|
+
aria-label="Reset zoom to fit"
|
|
528
|
+
title="Double-click to fit"
|
|
529
|
+
className="min-w-12 cursor-default rounded-md px-1 py-2 text-center text-xs font-medium text-text-muted tabular-nums select-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus"
|
|
530
|
+
>
|
|
531
|
+
{isFit ? 'Auto' : `${Math.round(scale * 100)}%`}
|
|
532
|
+
</button>
|
|
533
|
+
<IconButton icon={<Plus className="size-4" />} label="Zoom in" onClick={onZoomIn} />
|
|
534
|
+
<IconButton icon={<Fullscreen className="size-4" />} label="Fit to viewport" onClick={onFit} />
|
|
535
|
+
</div>
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
const ShortcutsDropdown = ({
|
|
539
|
+
open,
|
|
540
|
+
onToggle,
|
|
541
|
+
}: {
|
|
542
|
+
open: boolean;
|
|
543
|
+
onToggle: () => void;
|
|
544
|
+
}): ReactElement => {
|
|
545
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
546
|
+
|
|
547
|
+
useEffect(() => {
|
|
548
|
+
if (!open) return;
|
|
549
|
+
const onClick = (e: MouseEvent) => {
|
|
550
|
+
if (ref.current && !ref.current.contains(e.target as Node)) onToggle();
|
|
551
|
+
};
|
|
552
|
+
window.addEventListener('mousedown', onClick);
|
|
553
|
+
return () => window.removeEventListener('mousedown', onClick);
|
|
554
|
+
}, [open, onToggle]);
|
|
555
|
+
|
|
556
|
+
return (
|
|
557
|
+
<div ref={ref} className="relative">
|
|
558
|
+
<TextButton
|
|
559
|
+
icon={<Command className="size-4" />}
|
|
560
|
+
selected={open}
|
|
561
|
+
onClick={onToggle}
|
|
562
|
+
aria-haspopup="dialog"
|
|
563
|
+
aria-expanded={open}
|
|
564
|
+
>
|
|
565
|
+
Shortcuts
|
|
566
|
+
</TextButton>
|
|
567
|
+
{open && (
|
|
568
|
+
<div
|
|
569
|
+
role="dialog"
|
|
570
|
+
aria-label="Keyboard shortcuts"
|
|
571
|
+
className="absolute top-full right-0 z-20 mt-2 w-72 rounded-xl bg-paper p-5 shadow-[0_12px_40px_rgb(0_0_0_/_0.12),_0_4px_12px_rgb(0_0_0_/_0.08)] ring-1 ring-border"
|
|
572
|
+
>
|
|
573
|
+
<h2 className="mb-4 text-xs font-semibold tracking-wide text-text-muted uppercase">
|
|
574
|
+
Keyboard shortcuts
|
|
575
|
+
</h2>
|
|
576
|
+
<ul role="list" className="flex flex-col gap-3">
|
|
577
|
+
{SHORTCUT_LIST.map((s) => (
|
|
578
|
+
<li key={s.label} className="flex items-center justify-between gap-3 text-xs">
|
|
579
|
+
<span className="text-ink">{s.label}</span>
|
|
580
|
+
<span className="flex shrink-0 items-center gap-1.5">
|
|
581
|
+
{s.keys.split(' ').map((token, i) => (
|
|
582
|
+
<ShortcutToken key={i} token={token} />
|
|
583
|
+
))}
|
|
584
|
+
</span>
|
|
585
|
+
</li>
|
|
586
|
+
))}
|
|
587
|
+
</ul>
|
|
588
|
+
</div>
|
|
589
|
+
)}
|
|
590
|
+
</div>
|
|
591
|
+
);
|
|
592
|
+
};
|
|
593
|
+
|
|
594
|
+
const ExportButton = ({ templateName }: { templateName: string }): ReactElement => {
|
|
595
|
+
const [exporting, setExporting] = useState(false);
|
|
596
|
+
|
|
597
|
+
const handleExport = async () => {
|
|
598
|
+
setExporting(true);
|
|
599
|
+
try {
|
|
600
|
+
const res = await fetch('/api/export.pptx');
|
|
601
|
+
if (!res.ok) throw new Error(await res.text());
|
|
602
|
+
const blob = await res.blob();
|
|
603
|
+
const url = URL.createObjectURL(blob);
|
|
604
|
+
const a = document.createElement('a');
|
|
605
|
+
a.href = url;
|
|
606
|
+
a.download = `${templateName}.pptx`;
|
|
607
|
+
a.click();
|
|
608
|
+
URL.revokeObjectURL(url);
|
|
609
|
+
} catch (err) {
|
|
610
|
+
console.error('Export failed:', err);
|
|
611
|
+
} finally {
|
|
612
|
+
setExporting(false);
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
return (
|
|
617
|
+
<button
|
|
618
|
+
type="button"
|
|
619
|
+
onClick={handleExport}
|
|
620
|
+
disabled={exporting}
|
|
621
|
+
className="inline-flex h-8 shrink-0 cursor-pointer items-center gap-1.5 rounded-full bg-brand px-3 pl-2.5 text-xs font-medium text-paper transition-colors hover:bg-brand/85 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-focus disabled:cursor-not-allowed"
|
|
622
|
+
>
|
|
623
|
+
<span className="relative inline-flex size-4 shrink-0 items-center justify-center">
|
|
624
|
+
<Download
|
|
625
|
+
className={cn(
|
|
626
|
+
'absolute inset-0 size-4 transition-all duration-200',
|
|
627
|
+
exporting ? 'scale-50 opacity-0' : 'scale-100 opacity-100',
|
|
628
|
+
)}
|
|
629
|
+
/>
|
|
630
|
+
<Loader2
|
|
631
|
+
className={cn(
|
|
632
|
+
'absolute inset-0 size-4 animate-spin transition-all duration-200',
|
|
633
|
+
exporting ? 'scale-100 opacity-100' : 'scale-50 opacity-0',
|
|
634
|
+
)}
|
|
635
|
+
/>
|
|
636
|
+
</span>
|
|
637
|
+
Export
|
|
638
|
+
</button>
|
|
639
|
+
);
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
const SHORTCUT_SEPARATORS = new Set(['/', '·', '•']);
|
|
643
|
+
|
|
644
|
+
const ShortcutToken = ({ token }: { token: string }) =>
|
|
645
|
+
SHORTCUT_SEPARATORS.has(token) ? (
|
|
646
|
+
<span className="text-xs text-text-muted">{token}</span>
|
|
647
|
+
) : (
|
|
648
|
+
<Kbd>{token}</Kbd>
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
const Center = ({ children }: { children: ReactNode }) => (
|
|
652
|
+
<div className="flex h-full w-full items-center justify-center">{children}</div>
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
const ViewerStatus = ({ icon, message }: { icon: ReactNode; message: string }) => (
|
|
656
|
+
<div className="flex items-center gap-3 text-sm text-text-muted">
|
|
657
|
+
<span className="text-text-muted">{icon}</span>
|
|
658
|
+
<span>{message}</span>
|
|
659
|
+
</div>
|
|
660
|
+
);
|
|
661
|
+
|
|
662
|
+
const ErrorPanel = ({ error }: { error: Error }) => (
|
|
663
|
+
<div className="max-w-xl rounded-lg border border-red-200 bg-red-50 p-4">
|
|
664
|
+
<div className="flex items-center gap-2 text-red-700">
|
|
665
|
+
<CircleAlert className="size-4 shrink-0" />
|
|
666
|
+
<h2 className="text-sm font-semibold">{error.name}</h2>
|
|
667
|
+
</div>
|
|
668
|
+
<pre className="mt-2 font-mono text-sm whitespace-pre-wrap text-red-800">{error.message}</pre>
|
|
669
|
+
</div>
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
const Spinner = () => (
|
|
673
|
+
<span
|
|
674
|
+
aria-hidden="true"
|
|
675
|
+
className="inline-block size-4 animate-spin rounded-full border-2 border-border border-t-ink"
|
|
676
|
+
/>
|
|
677
|
+
);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import '../../styles.css';
|
|
2
|
+
import { StrictMode } from 'react';
|
|
3
|
+
import { createRoot } from 'react-dom/client';
|
|
4
|
+
import { DeckViewer } from '../../deck-viewer.js';
|
|
5
|
+
// @ts-expect-error -- resolved at runtime by the dev-server Vite plugin
|
|
6
|
+
import { template } from 'virtual:slides-dev/template';
|
|
7
|
+
|
|
8
|
+
const node = document.getElementById('root');
|
|
9
|
+
if (!node) throw new Error('slides-dev: missing #root in index.html');
|
|
10
|
+
|
|
11
|
+
createRoot(node).render(
|
|
12
|
+
<StrictMode>
|
|
13
|
+
<DeckViewer template={template} />
|
|
14
|
+
</StrictMode>,
|
|
15
|
+
);
|