@open-slide/core 1.0.5 → 1.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.
Files changed (40) hide show
  1. package/dist/{build-CoON6kTb.js → build-DSqSio-T.js} +1 -1
  2. package/dist/cli/bin.js +3 -3
  3. package/dist/{config-D2y1AXaN.d.ts → config-C7vMYzFD.d.ts} +1 -1
  4. package/dist/{config-Bxtztw-H.js → config-KdiYeWtK.js} +114 -1
  5. package/dist/{dev-IezNC17X.js → dev-B_GVbr11.js} +1 -1
  6. package/dist/index.d.ts +2 -2
  7. package/dist/locale/index.d.ts +1 -1
  8. package/dist/locale/index.js +40 -4
  9. package/dist/{preview-BwYjtENY.js → preview-D_mxhj7w.js} +1 -1
  10. package/dist/{types-BVvl_xup.d.ts → types-DYgVpIGo.d.ts} +9 -0
  11. package/dist/vite/index.d.ts +2 -2
  12. package/dist/vite/index.js +1 -1
  13. package/package.json +5 -1
  14. package/src/app/components/inspector/image-crop-dialog.tsx +168 -0
  15. package/src/app/components/inspector/inspect-overlay.tsx +96 -19
  16. package/src/app/components/inspector/inspector-panel.tsx +46 -13
  17. package/src/app/components/inspector/inspector-provider.tsx +83 -1
  18. package/src/app/components/inspector/save-bar.tsx +0 -3
  19. package/src/app/components/player.tsx +22 -26
  20. package/src/app/components/present/overview-grid.tsx +0 -5
  21. package/src/app/components/present/use-idle.ts +6 -4
  22. package/src/app/components/present/use-presenter-channel.ts +3 -10
  23. package/src/app/components/sidebar/folder-item.tsx +0 -2
  24. package/src/app/components/sidebar/icon-picker.tsx +0 -3
  25. package/src/app/components/slide-canvas.tsx +1 -10
  26. package/src/app/components/style-panel/design-provider.tsx +15 -6
  27. package/src/app/components/style-panel/style-panel.tsx +23 -11
  28. package/src/app/components/thumbnail-rail.tsx +220 -53
  29. package/src/app/lib/design-presets.ts +94 -0
  30. package/src/app/lib/export-html.ts +1 -9
  31. package/src/app/lib/export-pdf.ts +0 -5
  32. package/src/app/lib/print-ready.ts +0 -4
  33. package/src/app/lib/sdk.ts +1 -2
  34. package/src/app/routes/presenter.tsx +27 -24
  35. package/src/app/routes/slide.tsx +53 -1
  36. package/src/locale/en.ts +9 -0
  37. package/src/locale/ja.ts +9 -0
  38. package/src/locale/types.ts +9 -0
  39. package/src/locale/zh-cn.ts +9 -0
  40. package/src/locale/zh-tw.ts +9 -0
@@ -11,6 +11,7 @@ import {
11
11
  import { toast } from 'sonner';
12
12
  import { useHistory } from '@/components/history-provider';
13
13
  import { type DesignSystem, defaultDesign, designToCssVars } from '../../lib/design';
14
+ import { shuffleDesign } from '../../lib/design-presets';
14
15
  import { useDesign as useDesignFetch } from './use-design';
15
16
 
16
17
  type DesignCtx = {
@@ -26,6 +27,7 @@ type DesignCtx = {
26
27
  commit: () => Promise<void>;
27
28
  discard: () => void;
28
29
  resetToDefaults: () => void;
30
+ shuffle: () => void;
29
31
  };
30
32
 
31
33
  const Ctx = createContext<DesignCtx | null>(null);
@@ -48,7 +50,6 @@ export function DesignProvider({ slideId, children }: { slideId: string; childre
48
50
  const draftRef = useRef<DesignSystem | null>(null);
49
51
  draftRef.current = draft;
50
52
 
51
- // Re-seed draft whenever the saved design changes (slide switch, post-save HMR).
52
53
  useEffect(() => {
53
54
  if (design) setDraft(clone(design));
54
55
  }, [design]);
@@ -99,11 +100,18 @@ export function DesignProvider({ slideId, children }: { slideId: string; childre
99
100
  });
100
101
  }, [history]);
101
102
 
102
- // Live-preview overlay: rendered only while there are unsaved changes so the
103
- // canvas reflects the draft instantly, before any file write. SlideCanvas
104
- // emits its own CSS variables inline on the canvas root (so home thumbnails,
105
- // player, and exports work without any extra plumbing). Inline styles win
106
- // against external rules, so the overlay must use `!important` to override.
103
+ const shuffle = useCallback(() => {
104
+ const prev = draftRef.current;
105
+ const next = clone(shuffleDesign(prev));
106
+ setDraft(next);
107
+ history.record({
108
+ undo: () => setDraft(prev),
109
+ redo: () => setDraft(next),
110
+ });
111
+ }, [history]);
112
+
113
+ // SlideCanvas emits its design vars inline on the canvas root, so a draft
114
+ // overlay must use `!important` to outrank those inline styles.
107
115
  const previewCss = useMemo(() => {
108
116
  if (!dirty || !draft) return '';
109
117
  const lines = Object.entries(designToCssVars(draft))
@@ -125,6 +133,7 @@ export function DesignProvider({ slideId, children }: { slideId: string; childre
125
133
  commit,
126
134
  discard,
127
135
  resetToDefaults,
136
+ shuffle,
128
137
  };
129
138
 
130
139
  return (
@@ -1,4 +1,4 @@
1
- import { Palette, X } from 'lucide-react';
1
+ import { Palette, Shuffle, X } from 'lucide-react';
2
2
  import { useEffect, useState } from 'react';
3
3
  import { Field, NumberField, Section } from '@/components/panel/panel-fields';
4
4
  import { PanelShell, usePanelMount } from '@/components/panel/panel-shell';
@@ -28,7 +28,7 @@ type DesignPanelProps = {
28
28
  };
29
29
 
30
30
  export function DesignPanel({ open, onClose }: DesignPanelProps) {
31
- const { draft, exists, warning, loaded, dirty, update } = useDesignPanelState();
31
+ const { draft, exists, warning, loaded, dirty, update, shuffle } = useDesignPanelState();
32
32
  const { mounted, animVisible } = usePanelMount(open);
33
33
  const t = useLocale();
34
34
 
@@ -60,15 +60,27 @@ export function DesignPanel({ open, onClose }: DesignPanelProps) {
60
60
  />
61
61
  )}
62
62
  </div>
63
- <Button
64
- variant="ghost"
65
- size="icon-sm"
66
- className="text-muted-foreground hover:text-foreground"
67
- onClick={onClose}
68
- aria-label={t.stylePanel.closePanelAria}
69
- >
70
- <X className="size-3.5" />
71
- </Button>
63
+ <div className="flex items-center gap-0.5">
64
+ <Button
65
+ variant="ghost"
66
+ size="icon-sm"
67
+ className="text-muted-foreground hover:text-foreground"
68
+ onClick={shuffle}
69
+ aria-label={t.stylePanel.shuffleAria}
70
+ title={t.stylePanel.shuffleTitle}
71
+ >
72
+ <Shuffle className="size-3.5" />
73
+ </Button>
74
+ <Button
75
+ variant="ghost"
76
+ size="icon-sm"
77
+ className="text-muted-foreground hover:text-foreground"
78
+ onClick={onClose}
79
+ aria-label={t.stylePanel.closePanelAria}
80
+ >
81
+ <X className="size-3.5" />
82
+ </Button>
83
+ </div>
72
84
  </>
73
85
  }
74
86
  banner={
@@ -1,3 +1,19 @@
1
+ import {
2
+ closestCenter,
3
+ DndContext,
4
+ type DragEndEvent,
5
+ KeyboardSensor,
6
+ PointerSensor,
7
+ useSensor,
8
+ useSensors,
9
+ } from '@dnd-kit/core';
10
+ import {
11
+ SortableContext,
12
+ sortableKeyboardCoordinates,
13
+ useSortable,
14
+ verticalListSortingStrategy,
15
+ } from '@dnd-kit/sortable';
16
+ import { CSS } from '@dnd-kit/utilities';
1
17
  import { useEffect, useRef } from 'react';
2
18
  import { ScrollArea } from '@/components/ui/scroll-area';
3
19
  import { format, useLocale } from '@/lib/use-locale';
@@ -14,6 +30,7 @@ type Props = {
14
30
  design?: DesignSystem;
15
31
  current: number;
16
32
  onSelect: (index: number) => void;
33
+ onReorder?: (from: number, to: number) => void;
17
34
  orientation?: Orientation;
18
35
  };
19
36
 
@@ -25,6 +42,7 @@ export function ThumbnailRail({
25
42
  design,
26
43
  current,
27
44
  onSelect,
45
+ onReorder,
28
46
  orientation = 'vertical',
29
47
  }: Props) {
30
48
  const activeRef = useRef<HTMLButtonElement | null>(null);
@@ -93,61 +111,210 @@ export function ThumbnailRail({
93
111
 
94
112
  const scale = VERTICAL_THUMB_WIDTH / CANVAS_WIDTH;
95
113
  const height = CANVAS_HEIGHT * scale;
114
+
115
+ const renderThumb = (PageComp: Page, i: number) => {
116
+ const active = i === current;
117
+ const inner = (
118
+ <ThumbContents
119
+ index={i}
120
+ active={active}
121
+ page={PageComp}
122
+ design={design}
123
+ scale={scale}
124
+ height={height}
125
+ />
126
+ );
127
+
128
+ if (onReorder) {
129
+ return (
130
+ <SortableThumb
131
+ key={i}
132
+ index={i}
133
+ active={active}
134
+ activeRef={active ? activeRef : undefined}
135
+ onSelect={() => onSelect(i)}
136
+ ariaLabel={format(t.thumbnailRail.goToPageAria, { n: i + 1 })}
137
+ >
138
+ {inner}
139
+ </SortableThumb>
140
+ );
141
+ }
142
+
143
+ return (
144
+ <button
145
+ key={i}
146
+ type="button"
147
+ ref={active ? activeRef : undefined}
148
+ onClick={() => onSelect(i)}
149
+ aria-label={format(t.thumbnailRail.goToPageAria, { n: i + 1 })}
150
+ aria-current={active ? 'true' : undefined}
151
+ className={thumbButtonClass(active)}
152
+ >
153
+ {inner}
154
+ </button>
155
+ );
156
+ };
157
+
158
+ const list = (
159
+ <aside className="flex flex-col gap-2 px-3 py-3">
160
+ <div className="flex items-baseline justify-between px-1 pb-1">
161
+ <span className="eyebrow">{t.thumbnailRail.pages}</span>
162
+ <span className="folio">{pages.length.toString().padStart(2, '0')}</span>
163
+ </div>
164
+ {pages.map(renderThumb)}
165
+ </aside>
166
+ );
167
+
168
+ if (!onReorder) {
169
+ return <ScrollArea className="h-full border-r border-hairline bg-sidebar">{list}</ScrollArea>;
170
+ }
171
+
96
172
  return (
97
173
  <ScrollArea className="h-full border-r border-hairline bg-sidebar">
98
- <aside className="flex flex-col gap-2 px-3 py-3">
99
- <div className="flex items-baseline justify-between px-1 pb-1">
100
- <span className="eyebrow">{t.thumbnailRail.pages}</span>
101
- <span className="folio">{pages.length.toString().padStart(2, '0')}</span>
102
- </div>
103
- {pages.map((PageComp, i) => {
104
- const active = i === current;
105
- return (
106
- <button
107
- // biome-ignore lint/suspicious/noArrayIndexKey: pages list is render-stable
108
- key={i}
109
- type="button"
110
- ref={active ? activeRef : undefined}
111
- onClick={() => onSelect(i)}
112
- aria-label={`Go to page ${i + 1}`}
113
- aria-current={active ? 'true' : undefined}
114
- className={cn(
115
- 'group/thumb flex items-start gap-2.5 rounded-[6px] p-1.5 text-left motion-safe:transition-colors',
116
- 'hover:bg-muted/60',
117
- active && 'bg-muted',
118
- )}
119
- >
120
- <span
121
- className={cn(
122
- 'mt-1.5 w-7 shrink-0 text-right font-mono text-[10px] font-medium tracking-[0.06em] tabular-nums uppercase',
123
- active ? 'text-brand' : 'text-muted-foreground/70',
124
- )}
125
- >
126
- {(i + 1).toString().padStart(2, '0')}
127
- </span>
128
- <div
129
- className={cn(
130
- 'relative shrink-0 overflow-hidden rounded-[4px] border bg-card motion-safe:transition-all',
131
- active
132
- ? 'border-brand shadow-[0_0_0_1px_var(--brand)]'
133
- : 'border-hairline group-hover/thumb:border-foreground/25',
134
- )}
135
- style={{ width: VERTICAL_THUMB_WIDTH, height }}
136
- >
137
- <SlideCanvas scale={scale} center={false} flat freezeMotion design={design}>
138
- <PageComp />
139
- </SlideCanvas>
140
- {active && (
141
- <span
142
- aria-hidden
143
- className="pointer-events-none absolute inset-y-0 left-0 w-[2px] bg-brand"
144
- />
145
- )}
146
- </div>
147
- </button>
148
- );
149
- })}
150
- </aside>
174
+ <SortableRail pages={pages} onReorder={onReorder}>
175
+ {list}
176
+ </SortableRail>
151
177
  </ScrollArea>
152
178
  );
153
179
  }
180
+
181
+ function thumbButtonClass(active: boolean): string {
182
+ return cn(
183
+ 'group/thumb flex w-full items-start gap-2.5 rounded-[6px] p-1.5 text-left motion-safe:transition-colors',
184
+ 'hover:bg-muted/60',
185
+ active && 'bg-muted',
186
+ );
187
+ }
188
+
189
+ function ThumbContents({
190
+ index,
191
+ active,
192
+ page: PageComp,
193
+ design,
194
+ scale,
195
+ height,
196
+ }: {
197
+ index: number;
198
+ active: boolean;
199
+ page: Page;
200
+ design?: DesignSystem;
201
+ scale: number;
202
+ height: number;
203
+ }) {
204
+ return (
205
+ <>
206
+ <span
207
+ className={cn(
208
+ 'mt-1.5 w-7 shrink-0 text-right font-mono text-[10px] font-medium tracking-[0.06em] tabular-nums uppercase',
209
+ active ? 'text-brand' : 'text-muted-foreground/70',
210
+ )}
211
+ >
212
+ {(index + 1).toString().padStart(2, '0')}
213
+ </span>
214
+ <div
215
+ className={cn(
216
+ 'relative shrink-0 overflow-hidden rounded-[4px] border bg-card motion-safe:transition-all',
217
+ active
218
+ ? 'border-brand shadow-[0_0_0_1px_var(--brand)]'
219
+ : 'border-hairline group-hover/thumb:border-foreground/25',
220
+ )}
221
+ style={{ width: VERTICAL_THUMB_WIDTH, height }}
222
+ >
223
+ <SlideCanvas scale={scale} center={false} flat freezeMotion design={design}>
224
+ <PageComp />
225
+ </SlideCanvas>
226
+ {active && (
227
+ <span
228
+ aria-hidden
229
+ className="pointer-events-none absolute inset-y-0 left-0 w-[2px] bg-brand"
230
+ />
231
+ )}
232
+ </div>
233
+ </>
234
+ );
235
+ }
236
+
237
+ function SortableRail({
238
+ pages,
239
+ onReorder,
240
+ children,
241
+ }: {
242
+ pages: Page[];
243
+ onReorder: (from: number, to: number) => void;
244
+ children: React.ReactNode;
245
+ }) {
246
+ const sensors = useSensors(
247
+ useSensor(PointerSensor, { activationConstraint: { distance: 5 } }),
248
+ useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
249
+ );
250
+
251
+ const items = pages.map((_, i) => i + 1);
252
+
253
+ const handleDragEnd = (event: DragEndEvent) => {
254
+ const { active, over } = event;
255
+ if (!over || active.id === over.id) return;
256
+ const from = (active.id as number) - 1;
257
+ const to = (over.id as number) - 1;
258
+ if (from < 0 || to < 0 || from === to) return;
259
+ onReorder(from, to);
260
+ };
261
+
262
+ return (
263
+ <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
264
+ <SortableContext items={items} strategy={verticalListSortingStrategy}>
265
+ {children}
266
+ </SortableContext>
267
+ </DndContext>
268
+ );
269
+ }
270
+
271
+ function SortableThumb({
272
+ index,
273
+ active,
274
+ activeRef,
275
+ onSelect,
276
+ ariaLabel,
277
+ children,
278
+ }: {
279
+ index: number;
280
+ active: boolean;
281
+ activeRef: React.MutableRefObject<HTMLButtonElement | null> | undefined;
282
+ onSelect: () => void;
283
+ ariaLabel: string;
284
+ children: React.ReactNode;
285
+ }) {
286
+ const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
287
+ id: index + 1,
288
+ });
289
+
290
+ const setRef = (node: HTMLButtonElement | null) => {
291
+ setNodeRef(node);
292
+ if (activeRef) activeRef.current = node;
293
+ };
294
+
295
+ const yOnlyTransform = transform ? { ...transform, x: 0 } : transform;
296
+
297
+ return (
298
+ <button
299
+ ref={setRef}
300
+ type="button"
301
+ onClick={onSelect}
302
+ aria-label={ariaLabel}
303
+ aria-current={active ? 'true' : undefined}
304
+ style={{
305
+ transform: CSS.Transform.toString(yOnlyTransform),
306
+ transition,
307
+ touchAction: 'none',
308
+ }}
309
+ className={cn(
310
+ thumbButtonClass(active),
311
+ 'cursor-grab active:cursor-grabbing',
312
+ isDragging && 'z-10 opacity-60 shadow-edge ring-1 ring-brand',
313
+ )}
314
+ {...attributes}
315
+ {...listeners}
316
+ >
317
+ {children}
318
+ </button>
319
+ );
320
+ }
@@ -0,0 +1,94 @@
1
+ import { type DesignSystem, defaultDesign } from './design';
2
+
3
+ const SANS_SYSTEM = '-apple-system, BlinkMacSystemFont, "Inter", system-ui, sans-serif';
4
+ const SANS_INTER = '"Inter", system-ui, sans-serif';
5
+ const SANS_HELV = '"Helvetica Neue", Helvetica, Arial, sans-serif';
6
+ const SERIF_GEORGIA = 'Georgia, "Times New Roman", serif';
7
+ const SERIF_TIMES = '"Times New Roman", Times, serif';
8
+ const MONO_SF = '"SF Mono", "JetBrains Mono", Menlo, monospace';
9
+
10
+ export const designPresets: DesignSystem[] = [
11
+ defaultDesign,
12
+ {
13
+ palette: { bg: '#0f1115', text: '#f5f3ee', accent: '#7cc4ff' },
14
+ fonts: { display: SERIF_GEORGIA, body: SANS_SYSTEM },
15
+ typeScale: { hero: 192, body: 32 },
16
+ radius: 6,
17
+ },
18
+ {
19
+ palette: { bg: '#eef1f4', text: '#1c2733', accent: '#ff6a5b' },
20
+ fonts: { display: SANS_HELV, body: SANS_SYSTEM },
21
+ typeScale: { hero: 156, body: 30 },
22
+ radius: 8,
23
+ },
24
+ {
25
+ palette: { bg: '#fdf6e3', text: '#073642', accent: '#b58900' },
26
+ fonts: { display: SERIF_GEORGIA, body: SANS_INTER },
27
+ typeScale: { hero: 144, body: 28 },
28
+ radius: 14,
29
+ },
30
+ {
31
+ palette: { bg: '#ede2cc', text: '#3a2a1a', accent: '#2f6e3a' },
32
+ fonts: { display: SERIF_TIMES, body: SERIF_GEORGIA },
33
+ typeScale: { hero: 168, body: 32 },
34
+ radius: 4,
35
+ },
36
+ {
37
+ palette: { bg: '#ffffff', text: '#0a0a0a', accent: '#e11d48' },
38
+ fonts: { display: SANS_HELV, body: SANS_HELV },
39
+ typeScale: { hero: 200, body: 28 },
40
+ radius: 0,
41
+ },
42
+ {
43
+ palette: { bg: '#fde9d9', text: '#3a1f3d', accent: '#f97316' },
44
+ fonts: { display: SERIF_GEORGIA, body: SANS_SYSTEM },
45
+ typeScale: { hero: 184, body: 36 },
46
+ radius: 24,
47
+ },
48
+ {
49
+ palette: { bg: '#e9f5ee', text: '#0f3324', accent: '#ec4899' },
50
+ fonts: { display: SANS_INTER, body: SANS_INTER },
51
+ typeScale: { hero: 160, body: 32 },
52
+ radius: 16,
53
+ },
54
+ {
55
+ palette: { bg: '#0a0a0a', text: '#f3edd9', accent: '#eab308' },
56
+ fonts: { display: SERIF_GEORGIA, body: SANS_HELV },
57
+ typeScale: { hero: 200, body: 32 },
58
+ radius: 2,
59
+ },
60
+ {
61
+ palette: { bg: '#ece2f5', text: '#2a1c4a', accent: '#facc15' },
62
+ fonts: { display: SERIF_GEORGIA, body: SANS_SYSTEM },
63
+ typeScale: { hero: 168, body: 34 },
64
+ radius: 20,
65
+ },
66
+ {
67
+ palette: { bg: '#101418', text: '#a7f3d0', accent: '#fbbf24' },
68
+ fonts: { display: MONO_SF, body: MONO_SF },
69
+ typeScale: { hero: 144, body: 24 },
70
+ radius: 4,
71
+ },
72
+ {
73
+ palette: { bg: '#fafafa', text: '#0a0a0a', accent: '#facc15' },
74
+ fonts: { display: SANS_HELV, body: SANS_HELV },
75
+ typeScale: { hero: 220, body: 32 },
76
+ radius: 0,
77
+ },
78
+ ];
79
+
80
+ function pickRandom(): DesignSystem {
81
+ const idx = Math.floor(Math.random() * designPresets.length);
82
+ return designPresets[idx] ?? defaultDesign;
83
+ }
84
+
85
+ export function shuffleDesign(current?: DesignSystem | null): DesignSystem {
86
+ if (designPresets.length === 0) return defaultDesign;
87
+ if (designPresets.length === 1) return designPresets[0] ?? defaultDesign;
88
+ const currentJson = current ? JSON.stringify(current) : null;
89
+ for (let i = 0; i < 8; i++) {
90
+ const pick = pickRandom();
91
+ if (JSON.stringify(pick) !== currentJson) return pick;
92
+ }
93
+ return pickRandom();
94
+ }
@@ -1,8 +1,3 @@
1
- // Exports a slide as a standalone HTML file (or a .zip bundle if the slide
2
- // references bundled assets). The export is a static snapshot of each page's
3
- // post-mount DOM — runtime interactivity (useState click handlers, timers,
4
- // etc.) is captured at snapshot time only.
5
-
6
1
  import { createElement } from 'react';
7
2
  import { createRoot } from 'react-dom/client';
8
3
  import { designToCssVars } from './design';
@@ -39,9 +34,7 @@ export async function exportSlideAsHtml(slide: SlideModule, slideId: string): Pr
39
34
  const buf = new Uint8Array(await res.arrayBuffer());
40
35
  const name = uniqueAssetName(absolute, usedNames);
41
36
  assets.set(url, { name, bytes: buf });
42
- } catch {
43
- // ignore unreachable assets
44
- }
37
+ } catch {}
45
38
  }
46
39
 
47
40
  const rewrittenPages = pagesHtml.map((html) => rewriteUrls(html, assets, 'html'));
@@ -175,7 +168,6 @@ function looksLikeAsset(url: string): boolean {
175
168
  if (url.startsWith('mailto:') || url.startsWith('javascript:')) return false;
176
169
  const abs = toAbsolute(url);
177
170
  if (!abs) return false;
178
- // Same-origin only: we can only fetch local assets.
179
171
  try {
180
172
  const u = new URL(abs);
181
173
  if (u.origin !== window.location.origin) return false;
@@ -1,8 +1,3 @@
1
- // Exports a slide as a PDF via the browser's native print engine.
2
- // Each page in `slide.default` becomes one PDF page at 1920×1080.
3
- // Text stays selectable and inline SVG remains vector — `window.print()`
4
- // preserves both. The user picks "Save as PDF" in the print dialog.
5
-
6
1
  import { createElement } from 'react';
7
2
  import { createRoot, type Root } from 'react-dom/client';
8
3
  import { designToCssVars } from './design';
@@ -1,6 +1,3 @@
1
- // Helpers used by the PDF export flow to wait for the page to settle before
2
- // invoking window.print(). Browser-only — no Node / headless dependency.
3
-
4
1
  const DEFAULT_WAITFOR_TIMEOUT_MS = 10_000;
5
2
 
6
3
  export async function waitForFonts(): Promise<void> {
@@ -38,7 +35,6 @@ export async function waitForDataWaitfor(
38
35
  );
39
36
  }
40
37
 
41
- /** Returns true if `frame` has no running finite-iteration animations. */
42
38
  export function isFrameAnimationSettled(frame: Element): boolean {
43
39
  if (typeof document.getAnimations !== 'function') return true;
44
40
  for (const anim of document.getAnimations()) {
@@ -11,8 +11,7 @@ export type SlideModule = {
11
11
  default: Page[];
12
12
  meta?: SlideMeta;
13
13
  design?: DesignSystem;
14
- // Index-aligned with `default`. Each entry is the speaker note for the
15
- // page at the same position. Used by Presenter View only.
14
+ // Index-aligned with `default`.
16
15
  notes?: (string | undefined)[];
17
16
  };
18
17