@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.
- package/dist/{build-CoON6kTb.js → build-DSqSio-T.js} +1 -1
- package/dist/cli/bin.js +3 -3
- package/dist/{config-D2y1AXaN.d.ts → config-C7vMYzFD.d.ts} +1 -1
- package/dist/{config-Bxtztw-H.js → config-KdiYeWtK.js} +114 -1
- package/dist/{dev-IezNC17X.js → dev-B_GVbr11.js} +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/locale/index.d.ts +1 -1
- package/dist/locale/index.js +40 -4
- package/dist/{preview-BwYjtENY.js → preview-D_mxhj7w.js} +1 -1
- package/dist/{types-BVvl_xup.d.ts → types-DYgVpIGo.d.ts} +9 -0
- package/dist/vite/index.d.ts +2 -2
- package/dist/vite/index.js +1 -1
- package/package.json +5 -1
- package/src/app/components/inspector/image-crop-dialog.tsx +168 -0
- package/src/app/components/inspector/inspect-overlay.tsx +96 -19
- package/src/app/components/inspector/inspector-panel.tsx +46 -13
- package/src/app/components/inspector/inspector-provider.tsx +83 -1
- package/src/app/components/inspector/save-bar.tsx +0 -3
- package/src/app/components/player.tsx +22 -26
- package/src/app/components/present/overview-grid.tsx +0 -5
- package/src/app/components/present/use-idle.ts +6 -4
- package/src/app/components/present/use-presenter-channel.ts +3 -10
- package/src/app/components/sidebar/folder-item.tsx +0 -2
- package/src/app/components/sidebar/icon-picker.tsx +0 -3
- package/src/app/components/slide-canvas.tsx +1 -10
- package/src/app/components/style-panel/design-provider.tsx +15 -6
- package/src/app/components/style-panel/style-panel.tsx +23 -11
- package/src/app/components/thumbnail-rail.tsx +220 -53
- package/src/app/lib/design-presets.ts +94 -0
- package/src/app/lib/export-html.ts +1 -9
- package/src/app/lib/export-pdf.ts +0 -5
- package/src/app/lib/print-ready.ts +0 -4
- package/src/app/lib/sdk.ts +1 -2
- package/src/app/routes/presenter.tsx +27 -24
- package/src/app/routes/slide.tsx +53 -1
- package/src/locale/en.ts +9 -0
- package/src/locale/ja.ts +9 -0
- package/src/locale/types.ts +9 -0
- package/src/locale/zh-cn.ts +9 -0
- 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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
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()) {
|
package/src/app/lib/sdk.ts
CHANGED
|
@@ -11,8 +11,7 @@ export type SlideModule = {
|
|
|
11
11
|
default: Page[];
|
|
12
12
|
meta?: SlideMeta;
|
|
13
13
|
design?: DesignSystem;
|
|
14
|
-
// Index-aligned with `default`.
|
|
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
|
|