@open-slide/core 1.0.5 → 1.0.6
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-4wOJF1l4.js} +1 -1
- package/dist/cli/bin.js +3 -3
- package/dist/{config-Bxtztw-H.js → config-evLWCV1-.js} +5 -1
- package/dist/{dev-IezNC17X.js → dev-BUr0S-Ij.js} +1 -1
- package/dist/{preview-BwYjtENY.js → preview-DP_gIphz.js} +1 -1
- package/dist/vite/index.js +1 -1
- package/package.json +1 -1
- package/src/app/components/inspector/inspect-overlay.tsx +79 -17
- package/src/app/components/inspector/save-bar.tsx +0 -3
- package/src/app/components/player.tsx +7 -25
- package/src/app/components/present/overview-grid.tsx +0 -5
- 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 +2 -6
- 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/dist/cli/bin.js
CHANGED
|
@@ -57,15 +57,15 @@ async function run(argv) {
|
|
|
57
57
|
program.name("open-slide").description("Author slides — we handle the Vite/React stack.").version(version, "-v, --version", "print version").helpOption("-h, --help", "show help").showHelpAfterError(chalk.dim("(run `open-slide --help` for usage)"));
|
|
58
58
|
program.command("dev").description("Start the dev server").addOption(new Option("-p, --port <port>", "port to listen on").argParser(parsePort)).addOption(new Option("--host [host]", "expose on the network (optional host)")).option("--open", "open the browser on start").option("--no-skills-check", "skip the built-in skills drift check").action(async (flags) => {
|
|
59
59
|
if (flags.skillsCheck !== false) await runSkillsDriftCheck(resolveBuiltinSkillsDir());
|
|
60
|
-
const { dev } = await import("../dev-
|
|
60
|
+
const { dev } = await import("../dev-BUr0S-Ij.js");
|
|
61
61
|
await dev(flags);
|
|
62
62
|
});
|
|
63
63
|
program.command("build").description("Build a static site").option("--out-dir <dir>", "output directory (defaults to `dist`)").action(async (flags) => {
|
|
64
|
-
const { build } = await import("../build-
|
|
64
|
+
const { build } = await import("../build-4wOJF1l4.js");
|
|
65
65
|
await build(flags);
|
|
66
66
|
});
|
|
67
67
|
program.command("preview").description("Preview the production build").addOption(new Option("-p, --port <port>", "port to listen on").argParser(parsePort)).addOption(new Option("--host [host]", "expose on the network (optional host)")).option("--open", "open the browser on start").action(async (flags) => {
|
|
68
|
-
const { preview } = await import("../preview-
|
|
68
|
+
const { preview } = await import("../preview-DP_gIphz.js");
|
|
69
69
|
await preview(flags);
|
|
70
70
|
});
|
|
71
71
|
program.command("sync:skills").description("Sync built-in skills from @open-slide/core into this workspace").option("--dry-run", "show what would change without writing").action(async (flags) => {
|
|
@@ -1962,7 +1962,7 @@ function toId(absFile, slidesRoot) {
|
|
|
1962
1962
|
function generateSlidesModule(files, slidesRoot, isDev) {
|
|
1963
1963
|
const entries = files.map((abs) => {
|
|
1964
1964
|
const id = toId(abs, slidesRoot);
|
|
1965
|
-
const importPath = isDev ? `/@fs
|
|
1965
|
+
const importPath = isDev ? `/@fs/${abs.replace(/^\/+/, "")}` : abs;
|
|
1966
1966
|
return {
|
|
1967
1967
|
id,
|
|
1968
1968
|
importPath
|
|
@@ -2130,6 +2130,10 @@ async function createViteConfig(opts) {
|
|
|
2130
2130
|
optimizeDeps: {
|
|
2131
2131
|
entries: [path.join(APP_ROOT, "main.tsx")],
|
|
2132
2132
|
include: [
|
|
2133
|
+
"react",
|
|
2134
|
+
"react-dom",
|
|
2135
|
+
"react-dom/client",
|
|
2136
|
+
"next-themes",
|
|
2133
2137
|
"react-router-dom",
|
|
2134
2138
|
"radix-ui",
|
|
2135
2139
|
"lucide-react",
|
package/dist/vite/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
2
|
+
import { PANEL_TRANSITION_MS } from '@/components/panel/panel-shell';
|
|
2
3
|
import { findSlideSource, type SlideSourceHit } from '@/lib/inspector/fiber';
|
|
3
4
|
import { useInspector } from './inspector-provider';
|
|
4
5
|
|
|
5
|
-
type Highlight = {
|
|
6
|
+
type Highlight = { hit: SlideSourceHit };
|
|
6
7
|
|
|
7
8
|
type RelRect = { left: number; top: number; width: number; height: number };
|
|
8
9
|
|
|
9
10
|
const FRAME_FADE_MS = 150;
|
|
10
11
|
const FRAME_MORPH_MS = 180;
|
|
12
|
+
const LAYOUT_TRACK_MS = PANEL_TRANSITION_MS + FRAME_MORPH_MS;
|
|
11
13
|
|
|
12
14
|
export function InspectOverlay() {
|
|
13
15
|
const { active, slideId, selected, setSelected, cancel } = useInspector();
|
|
@@ -33,7 +35,7 @@ export function InspectOverlay() {
|
|
|
33
35
|
if (!el) return setHover(null);
|
|
34
36
|
const hit = findSlideSource(el, slideId, { hostOnly: true });
|
|
35
37
|
if (!hit) return setHover(null);
|
|
36
|
-
setHover({
|
|
38
|
+
setHover({ hit });
|
|
37
39
|
};
|
|
38
40
|
|
|
39
41
|
const onClick = (e: MouseEvent) => {
|
|
@@ -45,7 +47,7 @@ export function InspectOverlay() {
|
|
|
45
47
|
e.preventDefault();
|
|
46
48
|
e.stopPropagation();
|
|
47
49
|
setSelected({ line: hit.line, column: hit.column, anchor: hit.anchor });
|
|
48
|
-
setHover({
|
|
50
|
+
setHover({ hit });
|
|
49
51
|
};
|
|
50
52
|
|
|
51
53
|
window.addEventListener('pointermove', onMove, true);
|
|
@@ -64,7 +66,7 @@ export function InspectOverlay() {
|
|
|
64
66
|
overlayRef={overlayRef}
|
|
65
67
|
// Pin to the selection so the highlight tracks what the panel
|
|
66
68
|
// is editing even after the cursor moves away.
|
|
67
|
-
|
|
69
|
+
targetAnchor={selected?.anchor ?? hover?.hit.anchor ?? null}
|
|
68
70
|
/>
|
|
69
71
|
);
|
|
70
72
|
}
|
|
@@ -72,26 +74,77 @@ export function InspectOverlay() {
|
|
|
72
74
|
function FrameOverlay({
|
|
73
75
|
active,
|
|
74
76
|
overlayRef,
|
|
75
|
-
|
|
77
|
+
targetAnchor,
|
|
76
78
|
}: {
|
|
77
79
|
active: boolean;
|
|
78
80
|
overlayRef: React.RefObject<HTMLDivElement>;
|
|
79
|
-
|
|
81
|
+
targetAnchor: HTMLElement | null;
|
|
80
82
|
}) {
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
const [rect, setRect] = useState<RelRect | null>(null);
|
|
84
|
+
const [hasTarget, setHasTarget] = useState(false);
|
|
85
|
+
|
|
86
|
+
const measure = useCallback(() => {
|
|
87
|
+
const overlay = overlayRef.current;
|
|
88
|
+
if (!active || !targetAnchor?.isConnected || !overlay) {
|
|
89
|
+
setHasTarget(false);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const targetRect = targetAnchor.getBoundingClientRect();
|
|
94
|
+
const overlayRect = overlay.getBoundingClientRect();
|
|
95
|
+
const next = {
|
|
89
96
|
left: targetRect.left - overlayRect.left,
|
|
90
97
|
top: targetRect.top - overlayRect.top,
|
|
91
98
|
width: targetRect.width,
|
|
92
99
|
height: targetRect.height,
|
|
93
100
|
};
|
|
94
|
-
|
|
101
|
+
|
|
102
|
+
setHasTarget(true);
|
|
103
|
+
setRect((prev) => (sameRect(prev, next) ? prev : next));
|
|
104
|
+
}, [active, overlayRef, targetAnchor]);
|
|
105
|
+
|
|
106
|
+
useLayoutEffect(() => {
|
|
107
|
+
measure();
|
|
108
|
+
}, [measure]);
|
|
109
|
+
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (!active) {
|
|
112
|
+
setHasTarget(false);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
let scheduled = 0;
|
|
117
|
+
let tracking = 0;
|
|
118
|
+
const scheduleMeasure = () => {
|
|
119
|
+
cancelAnimationFrame(scheduled);
|
|
120
|
+
scheduled = requestAnimationFrame(measure);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const resizeObserver = new ResizeObserver(scheduleMeasure);
|
|
124
|
+
const root = document.querySelector<HTMLElement>('[data-inspector-root]');
|
|
125
|
+
if (root) resizeObserver.observe(root);
|
|
126
|
+
if (overlayRef.current) resizeObserver.observe(overlayRef.current);
|
|
127
|
+
if (targetAnchor) resizeObserver.observe(targetAnchor);
|
|
128
|
+
|
|
129
|
+
const stopAt = performance.now() + LAYOUT_TRACK_MS;
|
|
130
|
+
const trackPanelTransition = () => {
|
|
131
|
+
measure();
|
|
132
|
+
if (performance.now() < stopAt) tracking = requestAnimationFrame(trackPanelTransition);
|
|
133
|
+
};
|
|
134
|
+
tracking = requestAnimationFrame(trackPanelTransition);
|
|
135
|
+
|
|
136
|
+
window.addEventListener('resize', scheduleMeasure, true);
|
|
137
|
+
window.addEventListener('scroll', scheduleMeasure, true);
|
|
138
|
+
return () => {
|
|
139
|
+
resizeObserver.disconnect();
|
|
140
|
+
cancelAnimationFrame(scheduled);
|
|
141
|
+
cancelAnimationFrame(tracking);
|
|
142
|
+
window.removeEventListener('resize', scheduleMeasure, true);
|
|
143
|
+
window.removeEventListener('scroll', scheduleMeasure, true);
|
|
144
|
+
};
|
|
145
|
+
}, [active, measure, overlayRef, targetAnchor]);
|
|
146
|
+
|
|
147
|
+
const visible = !!(active && hasTarget && rect);
|
|
95
148
|
|
|
96
149
|
// First render after appearing: snap to the new rect (no transition).
|
|
97
150
|
// Subsequent rect changes in the same visible session: animate.
|
|
@@ -106,7 +159,6 @@ function FrameOverlay({
|
|
|
106
159
|
}, [visible]);
|
|
107
160
|
|
|
108
161
|
if (!active) return null;
|
|
109
|
-
const rect = lastRectRef.current;
|
|
110
162
|
const transition = morph
|
|
111
163
|
? `left ${FRAME_MORPH_MS}ms ease-out, top ${FRAME_MORPH_MS}ms ease-out, ` +
|
|
112
164
|
`width ${FRAME_MORPH_MS}ms ease-out, height ${FRAME_MORPH_MS}ms ease-out, ` +
|
|
@@ -134,6 +186,16 @@ function FrameOverlay({
|
|
|
134
186
|
);
|
|
135
187
|
}
|
|
136
188
|
|
|
189
|
+
function sameRect(a: RelRect | null, b: RelRect): boolean {
|
|
190
|
+
return (
|
|
191
|
+
!!a &&
|
|
192
|
+
Math.abs(a.left - b.left) < 0.5 &&
|
|
193
|
+
Math.abs(a.top - b.top) < 0.5 &&
|
|
194
|
+
Math.abs(a.width - b.width) < 0.5 &&
|
|
195
|
+
Math.abs(a.height - b.height) < 0.5
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
137
199
|
function pickElement(x: number, y: number): HTMLElement | null {
|
|
138
200
|
const stack = document.elementsFromPoint(x, y);
|
|
139
201
|
for (const el of stack) {
|
|
@@ -4,9 +4,6 @@ import { useDesignPanelState } from '@/components/style-panel/design-provider';
|
|
|
4
4
|
import { format, plural, useLocale } from '@/lib/use-locale';
|
|
5
5
|
import { useInspector } from './inspector-provider';
|
|
6
6
|
|
|
7
|
-
// Single save card for both inspector edits and design-token edits.
|
|
8
|
-
// Counts the design draft as one unit; the user sees one combined
|
|
9
|
-
// "N unsaved changes" pill. Save/Discard fan out to both providers.
|
|
10
7
|
export function SaveBar() {
|
|
11
8
|
const insp = useInspector();
|
|
12
9
|
const design = useDesignPanelState();
|
|
@@ -21,9 +21,6 @@ import { useTouchSwipe } from './present/use-touch-swipe';
|
|
|
21
21
|
import { SlideCanvas } from './slide-canvas';
|
|
22
22
|
|
|
23
23
|
const IDLE_HIDE_MS = 2000;
|
|
24
|
-
// Bottom band of the viewport that reveals the control bar + progress bar.
|
|
25
|
-
// Generous enough to feel forgiving with a trackpad, tight enough not to
|
|
26
|
-
// flash on incidental cursor moves.
|
|
27
24
|
const BAR_HOTZONE_PX = 160;
|
|
28
25
|
|
|
29
26
|
type Props = {
|
|
@@ -33,15 +30,7 @@ type Props = {
|
|
|
33
30
|
onIndexChange: (index: number) => void;
|
|
34
31
|
onExit: () => void;
|
|
35
32
|
allowExit?: boolean;
|
|
36
|
-
/**
|
|
37
|
-
* When true, render the full presenter chrome (control bar, progress bar,
|
|
38
|
-
* overview, blackout, laser pointer, jump-to-slide, help overlay, and
|
|
39
|
-
* the BroadcastChannel sync that powers Presenter View). Defaults to
|
|
40
|
-
* false so the static HTML export and any other minimal embeddings stay
|
|
41
|
-
* untouched.
|
|
42
|
-
*/
|
|
43
33
|
controls?: boolean;
|
|
44
|
-
/** Optional id used to namespace the BroadcastChannel for Presenter View. */
|
|
45
34
|
slideId?: string;
|
|
46
35
|
};
|
|
47
36
|
|
|
@@ -56,16 +45,15 @@ export function Player({
|
|
|
56
45
|
slideId,
|
|
57
46
|
}: Props) {
|
|
58
47
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
59
|
-
// Mirrored as state so
|
|
60
|
-
// (tooltips, popovers — the body is outside the fullscreen
|
|
61
|
-
//
|
|
48
|
+
// Mirrored as state so descendants portaling *into* the player subtree
|
|
49
|
+
// (tooltips, popovers — the body is outside the fullscreen tree) re-render
|
|
50
|
+
// once the node mounts.
|
|
62
51
|
const [rootEl, setRootEl] = useState<HTMLDivElement | null>(null);
|
|
63
52
|
const setRoot = useCallback((el: HTMLDivElement | null) => {
|
|
64
53
|
rootRef.current = el;
|
|
65
54
|
setRootEl(el);
|
|
66
55
|
}, []);
|
|
67
56
|
|
|
68
|
-
// ── Overlay state (only meaningful when `controls` is true) ────────────
|
|
69
57
|
const [overviewOpen, setOverviewOpen] = useState(false);
|
|
70
58
|
const [helpOpen, setHelpOpen] = useState(false);
|
|
71
59
|
const [blackout, setBlackout] = useState<'black' | 'white' | null>(null);
|
|
@@ -97,7 +85,6 @@ export function Player({
|
|
|
97
85
|
onNext: goNext,
|
|
98
86
|
});
|
|
99
87
|
|
|
100
|
-
// ── Fullscreen lifecycle ───────────────────────────────────────────────
|
|
101
88
|
useEffect(() => {
|
|
102
89
|
const el = rootRef.current;
|
|
103
90
|
if (!el) return;
|
|
@@ -118,11 +105,9 @@ export function Player({
|
|
|
118
105
|
return () => document.removeEventListener('fullscreenchange', onFsChange);
|
|
119
106
|
}, [onExit, allowExit]);
|
|
120
107
|
|
|
121
|
-
//
|
|
122
|
-
// Player is the source of truth. It re-publishes state on every change
|
|
108
|
+
// Player is the source of truth: it re-publishes state on every change
|
|
123
109
|
// and answers `request-state` pings so newly opened presenter windows
|
|
124
|
-
// hydrate immediately.
|
|
125
|
-
// the same slide module, so they don't cross the channel.
|
|
110
|
+
// hydrate immediately.
|
|
126
111
|
const presenterState = useMemo<PresenterState>(
|
|
127
112
|
() => ({ index, pageCount: pages.length, blackout, startedAt }),
|
|
128
113
|
[index, pages.length, blackout, startedAt],
|
|
@@ -155,7 +140,6 @@ export function Player({
|
|
|
155
140
|
channel.send({ type: 'state', state: presenterState });
|
|
156
141
|
}, [controls, channel, presenterState]);
|
|
157
142
|
|
|
158
|
-
// ── Keyboard ───────────────────────────────────────────────────────────
|
|
159
143
|
useEffect(() => {
|
|
160
144
|
const onKey = (e: KeyboardEvent) => {
|
|
161
145
|
const tgt = e.target;
|
|
@@ -256,10 +240,9 @@ export function Player({
|
|
|
256
240
|
slideId,
|
|
257
241
|
]);
|
|
258
242
|
|
|
259
|
-
// ── Chrome visibility / cursor ─────────────────────────────────────────
|
|
260
243
|
// The control bar + progress strip only surface when the pointer is in
|
|
261
|
-
// the bottom hot zone. Keyboard nav (arrows / space / PgDn) never
|
|
262
|
-
//
|
|
244
|
+
// the bottom hot zone. Keyboard nav (arrows / space / PgDn) never reveals
|
|
245
|
+
// them — intentional so the deck stays clean during a talk.
|
|
263
246
|
const pointerNearBottom = usePointerNearBottom(BAR_HOTZONE_PX, controls && !overlayActive);
|
|
264
247
|
const chromeVisible = pointerNearBottom || overlayActive;
|
|
265
248
|
const idle = useIdle(IDLE_HIDE_MS, controls && !overlayActive);
|
|
@@ -279,7 +262,6 @@ export function Player({
|
|
|
279
262
|
{PageComp ? <PageComp /> : null}
|
|
280
263
|
</SlideCanvas>
|
|
281
264
|
|
|
282
|
-
{/* Invisible side click zones — the original mobile-friendly nav. */}
|
|
283
265
|
<button
|
|
284
266
|
type="button"
|
|
285
267
|
aria-label="Previous page"
|
|
@@ -18,11 +18,6 @@ type Props = {
|
|
|
18
18
|
onSelect: (index: number) => void;
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
* Full-screen grid of slide thumbnails. Reuses SlideCanvas at fixed scale
|
|
23
|
-
* so each preview is rendered with the slide's design tokens but with
|
|
24
|
-
* motion frozen. Arrow keys move focus; Enter/click jumps and closes.
|
|
25
|
-
*/
|
|
26
21
|
export function PresentOverviewGrid({ pages, design, open, current, onClose, onSelect }: Props) {
|
|
27
22
|
const [focused, setFocused] = useState(current);
|
|
28
23
|
const gridRef = useRef<HTMLDivElement>(null);
|
|
@@ -20,16 +20,9 @@ type Handler = (msg: PresenterCommand) => void;
|
|
|
20
20
|
|
|
21
21
|
const SUPPORTED = typeof window !== 'undefined' && typeof BroadcastChannel !== 'undefined';
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
* open in different tabs do not cross-talk. Falls back to no-op when the
|
|
27
|
-
* API is missing (older browsers, SSR).
|
|
28
|
-
*
|
|
29
|
-
* The channel is owned by the effect (not useMemo) so React 18 StrictMode's
|
|
30
|
-
* double-invoke creates a fresh channel on the second mount instead of
|
|
31
|
-
* leaving a closed one behind that throws on the next `send()`.
|
|
32
|
-
*/
|
|
23
|
+
// Channel ownership lives in the effect (not useMemo) so StrictMode's
|
|
24
|
+
// double-invoke produces a fresh channel on remount rather than leaving a
|
|
25
|
+
// closed one behind that throws on the next send().
|
|
33
26
|
export function usePresenterChannel(slideId: string, onMessage?: Handler) {
|
|
34
27
|
const onMessageRef = useRef(onMessage);
|
|
35
28
|
onMessageRef.current = onMessage;
|
|
@@ -130,8 +130,6 @@ export function FolderItem({
|
|
|
130
130
|
<div
|
|
131
131
|
className={cn(
|
|
132
132
|
'group relative flex items-center gap-2.5 rounded-[5px] px-2 py-[5px] text-[12.5px] transition-colors',
|
|
133
|
-
// Editorial selected state: subtle warm tint + a thin vermillion
|
|
134
|
-
// ink-mark on the leading edge. Avoids the heavy "filled pill" look.
|
|
135
133
|
selected
|
|
136
134
|
? 'bg-muted text-foreground before:absolute before:inset-y-1.5 before:-left-0.5 before:w-[2px] before:rounded-full before:bg-brand'
|
|
137
135
|
: 'text-foreground/70 hover:bg-muted/60 hover:text-foreground',
|
|
@@ -3,9 +3,6 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
|
3
3
|
import type { FolderIcon } from '@/lib/sdk';
|
|
4
4
|
import { useLocale } from '@/lib/use-locale';
|
|
5
5
|
|
|
6
|
-
// Editorial palette — restrained warm/earth tones, no shadcn defaults
|
|
7
|
-
// (no #8b5cf6 violet, no #3b82f6 blue, etc.). Picked to coexist with the
|
|
8
|
-
// vermillion brand accent without shouting over it.
|
|
9
6
|
export const PRESET_COLORS = [
|
|
10
7
|
'#c0392b', // vermillion
|
|
11
8
|
'#b8743e', // ochre
|
|
@@ -5,21 +5,12 @@ import { CANVAS_HEIGHT, CANVAS_WIDTH } from '../lib/sdk';
|
|
|
5
5
|
|
|
6
6
|
type Props = {
|
|
7
7
|
children: ReactNode;
|
|
8
|
-
/** If set, use this scale directly
|
|
8
|
+
/** If set, use this scale directly. Otherwise fit to container. */
|
|
9
9
|
scale?: number;
|
|
10
|
-
/** Center the canvas within the container (default true). */
|
|
11
10
|
center?: boolean;
|
|
12
|
-
/** Flat mode: no rounded corners or drop shadow. */
|
|
13
11
|
flat?: boolean;
|
|
14
|
-
/** Freeze descendant animations and transitions, useful for thumbnail previews. */
|
|
15
12
|
freezeMotion?: boolean;
|
|
16
13
|
className?: string;
|
|
17
|
-
/**
|
|
18
|
-
* Per-slide design tokens. When set, the matching CSS custom properties
|
|
19
|
-
* are emitted on the canvas root so descendants can use `var(--osd-X)`
|
|
20
|
-
* regardless of which surface (editor, player, thumbnail, export) is
|
|
21
|
-
* rendering them.
|
|
22
|
-
*/
|
|
23
14
|
design?: DesignSystem;
|
|
24
15
|
};
|
|
25
16
|
|
|
@@ -48,7 +48,6 @@ export function DesignProvider({ slideId, children }: { slideId: string; childre
|
|
|
48
48
|
const draftRef = useRef<DesignSystem | null>(null);
|
|
49
49
|
draftRef.current = draft;
|
|
50
50
|
|
|
51
|
-
// Re-seed draft whenever the saved design changes (slide switch, post-save HMR).
|
|
52
51
|
useEffect(() => {
|
|
53
52
|
if (design) setDraft(clone(design));
|
|
54
53
|
}, [design]);
|
|
@@ -99,11 +98,8 @@ export function DesignProvider({ slideId, children }: { slideId: string; childre
|
|
|
99
98
|
});
|
|
100
99
|
}, [history]);
|
|
101
100
|
|
|
102
|
-
//
|
|
103
|
-
//
|
|
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.
|
|
101
|
+
// SlideCanvas emits its design vars inline on the canvas root, so a draft
|
|
102
|
+
// overlay must use `!important` to outrank those inline styles.
|
|
107
103
|
const previewCss = useMemo(() => {
|
|
108
104
|
if (!dirty || !draft) return '';
|
|
109
105
|
const lines = Object.entries(designToCssVars(draft))
|
|
@@ -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
|
|