@open-slide/core 0.0.7 → 0.0.8
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-cUKUY4bh.js → build-CXY2DSzy.js} +1 -1
- package/dist/cli/bin.js +3 -3
- package/dist/{config-DOcMmFJ7.js → config-BYTf0qVz.js} +10 -1
- package/dist/{dev-Brzmgu64.js → dev-BxCKugi3.js} +1 -1
- package/dist/{preview-Bf8iFXA-.js → preview-C1F-rHfx.js} +1 -1
- package/dist/vite/index.js +1 -1
- package/package.json +1 -1
- package/src/app/components/Player.tsx +18 -3
- package/src/app/lib/useWheelPageNavigation.ts +92 -0
- package/src/app/routes/Slide.tsx +44 -3
package/dist/cli/bin.js
CHANGED
|
@@ -22,15 +22,15 @@ async function run(argv) {
|
|
|
22
22
|
const program = new Command();
|
|
23
23
|
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)"));
|
|
24
24
|
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").action(async (flags) => {
|
|
25
|
-
const { dev } = await import("../dev-
|
|
25
|
+
const { dev } = await import("../dev-BxCKugi3.js");
|
|
26
26
|
await dev(flags);
|
|
27
27
|
});
|
|
28
28
|
program.command("build").description("Build a static site").option("--out-dir <dir>", "output directory (defaults to `dist`)").action(async (flags) => {
|
|
29
|
-
const { build } = await import("../build-
|
|
29
|
+
const { build } = await import("../build-CXY2DSzy.js");
|
|
30
30
|
await build(flags);
|
|
31
31
|
});
|
|
32
32
|
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) => {
|
|
33
|
-
const { preview } = await import("../preview-
|
|
33
|
+
const { preview } = await import("../preview-C1F-rHfx.js");
|
|
34
34
|
await preview(flags);
|
|
35
35
|
});
|
|
36
36
|
await program.parseAsync(argv, { from: "user" });
|
|
@@ -758,7 +758,16 @@ async function createViteConfig(opts) {
|
|
|
758
758
|
"tailwind-merge",
|
|
759
759
|
"class-variance-authority",
|
|
760
760
|
"emoji-picker-react"
|
|
761
|
-
]
|
|
761
|
+
],
|
|
762
|
+
esbuildOptions: { plugins: [{
|
|
763
|
+
name: "open-slide:virtual-externals",
|
|
764
|
+
setup(build$1) {
|
|
765
|
+
build$1.onResolve({ filter: /^virtual:open-slide\// }, (args) => ({
|
|
766
|
+
path: args.path,
|
|
767
|
+
external: true
|
|
768
|
+
}));
|
|
769
|
+
}
|
|
770
|
+
}] }
|
|
762
771
|
},
|
|
763
772
|
server: {
|
|
764
773
|
port: config.port ?? 5173,
|
package/dist/vite/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { useEffect, useRef } from 'react';
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
2
|
+
import { useWheelPageNavigation } from '@/lib/useWheelPageNavigation';
|
|
2
3
|
import type { Page } from '../lib/sdk';
|
|
3
4
|
import { SlideCanvas } from './SlideCanvas';
|
|
4
5
|
|
|
@@ -12,6 +13,20 @@ type Props = {
|
|
|
12
13
|
|
|
13
14
|
export function Player({ pages, index, onIndexChange, onExit, allowExit = true }: Props) {
|
|
14
15
|
const rootRef = useRef<HTMLDivElement>(null);
|
|
16
|
+
const goPrev = useCallback(() => {
|
|
17
|
+
if (index > 0) onIndexChange(index - 1);
|
|
18
|
+
}, [index, onIndexChange]);
|
|
19
|
+
const goNext = useCallback(() => {
|
|
20
|
+
if (index < pages.length - 1) onIndexChange(index + 1);
|
|
21
|
+
}, [index, pages.length, onIndexChange]);
|
|
22
|
+
|
|
23
|
+
useWheelPageNavigation({
|
|
24
|
+
ref: rootRef,
|
|
25
|
+
canPrev: index > 0,
|
|
26
|
+
canNext: index < pages.length - 1,
|
|
27
|
+
onPrev: goPrev,
|
|
28
|
+
onNext: goNext,
|
|
29
|
+
});
|
|
15
30
|
|
|
16
31
|
useEffect(() => {
|
|
17
32
|
const el = rootRef.current;
|
|
@@ -69,14 +84,14 @@ export function Player({ pages, index, onIndexChange, onExit, allowExit = true }
|
|
|
69
84
|
<button
|
|
70
85
|
type="button"
|
|
71
86
|
aria-label="Previous page"
|
|
72
|
-
onClick={
|
|
87
|
+
onClick={goPrev}
|
|
73
88
|
disabled={index === 0}
|
|
74
89
|
className="absolute inset-y-0 left-0 z-10 w-[30%]"
|
|
75
90
|
/>
|
|
76
91
|
<button
|
|
77
92
|
type="button"
|
|
78
93
|
aria-label="Next page"
|
|
79
|
-
onClick={
|
|
94
|
+
onClick={goNext}
|
|
80
95
|
disabled={index === pages.length - 1}
|
|
81
96
|
className="absolute inset-y-0 right-0 z-10 w-[30%]"
|
|
82
97
|
/>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { type RefObject, useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
const WHEEL_PAGE_THRESHOLD_PX = 12;
|
|
4
|
+
const WHEEL_NAV_COOLDOWN_MS = 100;
|
|
5
|
+
const WHEEL_GESTURE_IDLE_MS = 80;
|
|
6
|
+
|
|
7
|
+
type UseWheelPageNavigationOptions<T extends HTMLElement> = {
|
|
8
|
+
ref: RefObject<T>;
|
|
9
|
+
enabled?: boolean;
|
|
10
|
+
canPrev: boolean;
|
|
11
|
+
canNext: boolean;
|
|
12
|
+
onPrev: () => void;
|
|
13
|
+
onNext: () => void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function useWheelPageNavigation<T extends HTMLElement>({
|
|
17
|
+
ref,
|
|
18
|
+
enabled = true,
|
|
19
|
+
canPrev,
|
|
20
|
+
canNext,
|
|
21
|
+
onPrev,
|
|
22
|
+
onNext,
|
|
23
|
+
}: UseWheelPageNavigationOptions<T>) {
|
|
24
|
+
const accumulatedDeltaRef = useRef(0);
|
|
25
|
+
const lastWheelAtRef = useRef(0);
|
|
26
|
+
const lastNavigateAtRef = useRef(0);
|
|
27
|
+
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const el = ref.current;
|
|
30
|
+
if (!el || !enabled) return;
|
|
31
|
+
|
|
32
|
+
const onWheel = (event: WheelEvent) => {
|
|
33
|
+
if (event.defaultPrevented || event.ctrlKey || shouldIgnoreWheelTarget(event.target)) return;
|
|
34
|
+
|
|
35
|
+
const deltaY = normalizeDeltaY(event);
|
|
36
|
+
if (Math.abs(deltaY) <= Math.abs(normalizeDeltaX(event))) return;
|
|
37
|
+
|
|
38
|
+
const now = performance.now();
|
|
39
|
+
if (now - lastWheelAtRef.current > WHEEL_GESTURE_IDLE_MS) {
|
|
40
|
+
accumulatedDeltaRef.current = 0;
|
|
41
|
+
}
|
|
42
|
+
lastWheelAtRef.current = now;
|
|
43
|
+
|
|
44
|
+
if (now - lastNavigateAtRef.current < WHEEL_NAV_COOLDOWN_MS) {
|
|
45
|
+
event.preventDefault();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
accumulatedDeltaRef.current += deltaY;
|
|
50
|
+
if (Math.abs(accumulatedDeltaRef.current) < WHEEL_PAGE_THRESHOLD_PX) {
|
|
51
|
+
event.preventDefault();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const direction = Math.sign(accumulatedDeltaRef.current);
|
|
56
|
+
accumulatedDeltaRef.current = 0;
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
|
|
59
|
+
if (direction > 0 && canNext) {
|
|
60
|
+
lastNavigateAtRef.current = now;
|
|
61
|
+
onNext();
|
|
62
|
+
} else if (direction < 0 && canPrev) {
|
|
63
|
+
lastNavigateAtRef.current = now;
|
|
64
|
+
onPrev();
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
el.addEventListener('wheel', onWheel, { passive: false });
|
|
69
|
+
return () => el.removeEventListener('wheel', onWheel);
|
|
70
|
+
}, [ref, enabled, canPrev, canNext, onPrev, onNext]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeDeltaY(event: WheelEvent) {
|
|
74
|
+
return normalizeWheelDelta(event.deltaY, event.deltaMode);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeDeltaX(event: WheelEvent) {
|
|
78
|
+
return normalizeWheelDelta(event.deltaX, event.deltaMode);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeWheelDelta(delta: number, deltaMode: number) {
|
|
82
|
+
if (deltaMode === WheelEvent.DOM_DELTA_LINE) return delta * 16;
|
|
83
|
+
if (deltaMode === WheelEvent.DOM_DELTA_PAGE) return delta * 800;
|
|
84
|
+
return delta;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function shouldIgnoreWheelTarget(target: EventTarget | null) {
|
|
88
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
89
|
+
return Boolean(
|
|
90
|
+
target.closest('input, textarea, select, [contenteditable="true"], [data-wheel-nav-ignore]'),
|
|
91
|
+
);
|
|
92
|
+
}
|
package/src/app/routes/Slide.tsx
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import config from 'virtual:open-slide/config';
|
|
1
2
|
import { ChevronLeft, Download, FileCode2, Loader2, Pencil, Play } from 'lucide-react';
|
|
2
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { type RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
4
|
import { Link, useParams, useSearchParams } from 'react-router-dom';
|
|
4
|
-
import config from 'virtual:open-slide/config';
|
|
5
5
|
import { CommentWidget } from '@/components/inspector/CommentWidget';
|
|
6
6
|
import { InspectOverlay } from '@/components/inspector/InspectOverlay';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
InspectorProvider,
|
|
9
|
+
InspectToggleButton,
|
|
10
|
+
useInspector,
|
|
11
|
+
} from '@/components/inspector/InspectorProvider';
|
|
8
12
|
import { Button, buttonVariants } from '@/components/ui/button';
|
|
9
13
|
import {
|
|
10
14
|
DropdownMenu,
|
|
@@ -14,6 +18,7 @@ import {
|
|
|
14
18
|
} from '@/components/ui/dropdown-menu';
|
|
15
19
|
import { Separator } from '@/components/ui/separator';
|
|
16
20
|
import { useFolders } from '@/lib/folders';
|
|
21
|
+
import { useWheelPageNavigation } from '@/lib/useWheelPageNavigation';
|
|
17
22
|
import { cn } from '@/lib/utils';
|
|
18
23
|
import { ClickNavZones } from '../components/ClickNavZones';
|
|
19
24
|
import { Player } from '../components/Player';
|
|
@@ -33,6 +38,7 @@ export function Slide() {
|
|
|
33
38
|
const [playing, setPlaying] = useState(false);
|
|
34
39
|
const [exporting, setExporting] = useState(false);
|
|
35
40
|
const { renameSlide } = useFolders();
|
|
41
|
+
const slideViewportRef = useRef<HTMLElement>(null);
|
|
36
42
|
|
|
37
43
|
useEffect(() => {
|
|
38
44
|
let cancelled = false;
|
|
@@ -231,9 +237,17 @@ export function Slide() {
|
|
|
231
237
|
<ThumbnailRail pages={pages} current={index} onSelect={goTo} />
|
|
232
238
|
</div>
|
|
233
239
|
<main
|
|
240
|
+
ref={slideViewportRef}
|
|
234
241
|
data-inspector-root
|
|
235
242
|
className="relative min-h-0 min-w-0 flex-1 bg-background p-2 md:p-8"
|
|
236
243
|
>
|
|
244
|
+
<SlideWheelNavigation
|
|
245
|
+
targetRef={slideViewportRef}
|
|
246
|
+
onPrev={() => goTo(index - 1)}
|
|
247
|
+
onNext={() => goTo(index + 1)}
|
|
248
|
+
canPrev={index > 0}
|
|
249
|
+
canNext={index < pageCount - 1}
|
|
250
|
+
/>
|
|
237
251
|
<SlideCanvas>
|
|
238
252
|
<CurrentPage />
|
|
239
253
|
</SlideCanvas>
|
|
@@ -256,6 +270,33 @@ export function Slide() {
|
|
|
256
270
|
);
|
|
257
271
|
}
|
|
258
272
|
|
|
273
|
+
function SlideWheelNavigation({
|
|
274
|
+
targetRef,
|
|
275
|
+
onPrev,
|
|
276
|
+
onNext,
|
|
277
|
+
canPrev,
|
|
278
|
+
canNext,
|
|
279
|
+
}: {
|
|
280
|
+
targetRef: RefObject<HTMLElement>;
|
|
281
|
+
onPrev: () => void;
|
|
282
|
+
onNext: () => void;
|
|
283
|
+
canPrev: boolean;
|
|
284
|
+
canNext: boolean;
|
|
285
|
+
}) {
|
|
286
|
+
const { active } = useInspector();
|
|
287
|
+
|
|
288
|
+
useWheelPageNavigation({
|
|
289
|
+
ref: targetRef,
|
|
290
|
+
enabled: !active,
|
|
291
|
+
canPrev,
|
|
292
|
+
canNext,
|
|
293
|
+
onPrev,
|
|
294
|
+
onNext,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
|
|
259
300
|
function InlineTitleEditor({
|
|
260
301
|
title,
|
|
261
302
|
onSubmit,
|