@open-aippt/core 1.13.2
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/LICENSE +21 -0
- package/README.md +98 -0
- package/bin.js +2 -0
- package/dist/build-DxTqmvsO.js +17 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +86 -0
- package/dist/config-CjzqjrEA.js +4280 -0
- package/dist/config-DIC-yVPp.d.ts +23 -0
- package/dist/design-cpzS8aud.js +35 -0
- package/dist/dev-BYuTeJbA.js +20 -0
- package/dist/format-BCeKbTOM.js +1605 -0
- package/dist/index.d.ts +134 -0
- package/dist/index.js +467 -0
- package/dist/locale/index.d.ts +24 -0
- package/dist/locale/index.js +3 -0
- package/dist/preview-DlQvnJPq.js +18 -0
- package/dist/sync-BPZ0m27m.js +139 -0
- package/dist/sync-EsYusbbL.js +3 -0
- package/dist/types-CHmFPIG_.d.ts +430 -0
- package/dist/vite/index.d.ts +14 -0
- package/dist/vite/index.js +4 -0
- package/env.d.ts +59 -0
- package/package.json +103 -0
- package/skills/apply-comments/SKILL.md +83 -0
- package/skills/create-slide/SKILL.md +91 -0
- package/skills/create-theme/SKILL.md +250 -0
- package/skills/current-slide/SKILL.md +110 -0
- package/skills/slide-authoring/SKILL.md +625 -0
- package/src/app/app.tsx +47 -0
- package/src/app/components/asset-view.tsx +966 -0
- package/src/app/components/history-provider.tsx +120 -0
- package/src/app/components/image-placeholder.tsx +243 -0
- package/src/app/components/inspector/asset-picker-dialog.tsx +196 -0
- package/src/app/components/inspector/comment-widget.tsx +93 -0
- package/src/app/components/inspector/image-crop-dialog.tsx +212 -0
- package/src/app/components/inspector/inspect-overlay.tsx +387 -0
- package/src/app/components/inspector/inspector-panel.tsx +1115 -0
- package/src/app/components/inspector/inspector-provider.tsx +1218 -0
- package/src/app/components/inspector/save-bar.tsx +48 -0
- package/src/app/components/language-toggle.tsx +39 -0
- package/src/app/components/notes-drawer.tsx +120 -0
- package/src/app/components/overview-grid.tsx +363 -0
- package/src/app/components/panel/panel-fields.tsx +60 -0
- package/src/app/components/panel/panel-shell.tsx +80 -0
- package/src/app/components/panel/save-card.tsx +142 -0
- package/src/app/components/pdf-progress-toast.tsx +32 -0
- package/src/app/components/player.tsx +466 -0
- package/src/app/components/pptx-progress-toast.tsx +32 -0
- package/src/app/components/present/blackout-overlay.tsx +18 -0
- package/src/app/components/present/control-bar.tsx +315 -0
- package/src/app/components/present/help-overlay.tsx +57 -0
- package/src/app/components/present/jump-input.tsx +74 -0
- package/src/app/components/present/laser-pointer.tsx +39 -0
- package/src/app/components/present/progress-bar.tsx +26 -0
- package/src/app/components/present/use-idle.ts +46 -0
- package/src/app/components/present/use-pointer-near-bottom.ts +34 -0
- package/src/app/components/present/use-presenter-channel.ts +66 -0
- package/src/app/components/present/use-touch-swipe.ts +66 -0
- package/src/app/components/shared-element.tsx +48 -0
- package/src/app/components/sidebar/folder-item.tsx +258 -0
- package/src/app/components/sidebar/icon-picker.tsx +61 -0
- package/src/app/components/sidebar/mobile-pill.tsx +34 -0
- package/src/app/components/sidebar/sidebar-footer.tsx +105 -0
- package/src/app/components/sidebar/sidebar.tsx +284 -0
- package/src/app/components/slide-canvas.tsx +102 -0
- package/src/app/components/slide-transition-layer.tsx +844 -0
- package/src/app/components/style-panel/design-provider.tsx +148 -0
- package/src/app/components/style-panel/style-panel.tsx +349 -0
- package/src/app/components/style-panel/use-design.ts +112 -0
- package/src/app/components/theme-toggle.tsx +59 -0
- package/src/app/components/themes/theme-detail.tsx +305 -0
- package/src/app/components/themes/themes-gallery.tsx +149 -0
- package/src/app/components/thumbnail-rail.tsx +805 -0
- package/src/app/components/ui/badge.tsx +45 -0
- package/src/app/components/ui/button.tsx +99 -0
- package/src/app/components/ui/card.tsx +92 -0
- package/src/app/components/ui/context-menu.tsx +237 -0
- package/src/app/components/ui/dialog.tsx +157 -0
- package/src/app/components/ui/dropdown-menu.tsx +245 -0
- package/src/app/components/ui/input.tsx +25 -0
- package/src/app/components/ui/label.tsx +24 -0
- package/src/app/components/ui/popover.tsx +75 -0
- package/src/app/components/ui/progress.tsx +31 -0
- package/src/app/components/ui/scroll-area.tsx +53 -0
- package/src/app/components/ui/select.tsx +196 -0
- package/src/app/components/ui/separator.tsx +28 -0
- package/src/app/components/ui/slider.tsx +61 -0
- package/src/app/components/ui/sonner.tsx +48 -0
- package/src/app/components/ui/tabs.tsx +79 -0
- package/src/app/components/ui/textarea.tsx +22 -0
- package/src/app/components/ui/toggle-group.tsx +83 -0
- package/src/app/components/ui/toggle.tsx +45 -0
- package/src/app/components/ui/tooltip.tsx +58 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/index.html +13 -0
- package/src/app/lib/assets.ts +242 -0
- package/src/app/lib/design-presets.ts +94 -0
- package/src/app/lib/design.ts +58 -0
- package/src/app/lib/export-html.ts +326 -0
- package/src/app/lib/export-pdf.ts +298 -0
- package/src/app/lib/export-pptx.ts +284 -0
- package/src/app/lib/folders.ts +239 -0
- package/src/app/lib/inspector/fiber.test.ts +154 -0
- package/src/app/lib/inspector/fiber.ts +85 -0
- package/src/app/lib/inspector/use-comments.ts +74 -0
- package/src/app/lib/inspector/use-editor.ts +73 -0
- package/src/app/lib/inspector/use-notes.ts +134 -0
- package/src/app/lib/locale-store.ts +67 -0
- package/src/app/lib/page-context.tsx +38 -0
- package/src/app/lib/print-ready.test.ts +32 -0
- package/src/app/lib/print-ready.ts +51 -0
- package/src/app/lib/sdk.test.ts +13 -0
- package/src/app/lib/sdk.ts +37 -0
- package/src/app/lib/slides.ts +26 -0
- package/src/app/lib/step-context.tsx +261 -0
- package/src/app/lib/themes.ts +22 -0
- package/src/app/lib/transition.ts +30 -0
- package/src/app/lib/use-agent-socket.ts +18 -0
- package/src/app/lib/use-click-page-navigation.ts +60 -0
- package/src/app/lib/use-is-mobile.ts +21 -0
- package/src/app/lib/use-locale.ts +8 -0
- package/src/app/lib/use-prefers-reduced-motion.ts +19 -0
- package/src/app/lib/use-slide-module.ts +48 -0
- package/src/app/lib/use-wheel-page-navigation.ts +99 -0
- package/src/app/lib/utils.test.ts +25 -0
- package/src/app/lib/utils.ts +6 -0
- package/src/app/main.tsx +14 -0
- package/src/app/routes/assets.tsx +9 -0
- package/src/app/routes/home-shell.tsx +213 -0
- package/src/app/routes/home.tsx +807 -0
- package/src/app/routes/presenter.tsx +418 -0
- package/src/app/routes/slide.tsx +1108 -0
- package/src/app/routes/themes.tsx +34 -0
- package/src/app/styles.css +429 -0
- package/src/app/virtual.d.ts +51 -0
- package/src/locale/en.ts +416 -0
- package/src/locale/format.ts +12 -0
- package/src/locale/index.ts +6 -0
- package/src/locale/ja.ts +422 -0
- package/src/locale/types.ts +443 -0
- package/src/locale/zh-cn.ts +414 -0
- package/src/locale/zh-tw.ts +414 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import { useClickPageNavigation } from '@/lib/use-click-page-navigation';
|
|
3
|
+
import { useWheelPageNavigation } from '@/lib/use-wheel-page-navigation';
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
5
|
+
import type { DesignSystem } from '../lib/design';
|
|
6
|
+
import type { Page } from '../lib/sdk';
|
|
7
|
+
import type { EntryDirection, StepAggregate, StepController } from '../lib/step-context';
|
|
8
|
+
import type { SlideTransition } from '../lib/transition';
|
|
9
|
+
import { useIsMobile } from '../lib/use-is-mobile';
|
|
10
|
+
import { usePrefersReducedMotion } from '../lib/use-prefers-reduced-motion';
|
|
11
|
+
import { OverviewGrid } from './overview-grid';
|
|
12
|
+
import { PresentBlackoutOverlay } from './present/blackout-overlay';
|
|
13
|
+
import { PresentControlBar } from './present/control-bar';
|
|
14
|
+
import { PresentHelpOverlay } from './present/help-overlay';
|
|
15
|
+
import { PresentJumpInput } from './present/jump-input';
|
|
16
|
+
import { PresentLaserPointer } from './present/laser-pointer';
|
|
17
|
+
import { PresentProgressBar } from './present/progress-bar';
|
|
18
|
+
import { useIdle } from './present/use-idle';
|
|
19
|
+
import { usePointerNearBottom } from './present/use-pointer-near-bottom';
|
|
20
|
+
import {
|
|
21
|
+
type PresenterCommand,
|
|
22
|
+
type PresenterState,
|
|
23
|
+
usePresenterChannel,
|
|
24
|
+
} from './present/use-presenter-channel';
|
|
25
|
+
import { useTouchSwipe } from './present/use-touch-swipe';
|
|
26
|
+
import { SlideCanvas } from './slide-canvas';
|
|
27
|
+
import { SlideTransitionLayer } from './slide-transition-layer';
|
|
28
|
+
|
|
29
|
+
const IDLE_HIDE_MS = 2000;
|
|
30
|
+
const BAR_HOTZONE_PX = 160;
|
|
31
|
+
const MOBILE_CHROME_HIDE_MS = 2200;
|
|
32
|
+
|
|
33
|
+
type Props = {
|
|
34
|
+
pages: Page[];
|
|
35
|
+
design?: DesignSystem;
|
|
36
|
+
transition?: SlideTransition;
|
|
37
|
+
index: number;
|
|
38
|
+
onIndexChange: (index: number) => void;
|
|
39
|
+
onExit: () => void;
|
|
40
|
+
allowExit?: boolean;
|
|
41
|
+
controls?: boolean;
|
|
42
|
+
slideId?: string;
|
|
43
|
+
/**
|
|
44
|
+
* When true, the Player enters the browser Fullscreen API on mount.
|
|
45
|
+
* When false, it renders as a window-sized overlay (viewport-filling)
|
|
46
|
+
* without entering fullscreen. Defaults to true for back-compat.
|
|
47
|
+
*/
|
|
48
|
+
fullscreen?: boolean;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function Player({
|
|
52
|
+
pages,
|
|
53
|
+
design,
|
|
54
|
+
transition,
|
|
55
|
+
index,
|
|
56
|
+
onIndexChange,
|
|
57
|
+
onExit,
|
|
58
|
+
allowExit = true,
|
|
59
|
+
controls = false,
|
|
60
|
+
slideId,
|
|
61
|
+
fullscreen = true,
|
|
62
|
+
}: Props) {
|
|
63
|
+
const isMobile = useIsMobile();
|
|
64
|
+
const prefersReducedMotion = usePrefersReducedMotion();
|
|
65
|
+
const rootRef = useRef<HTMLDivElement | null>(null);
|
|
66
|
+
// Mirrored as state so descendants portaling *into* the player subtree
|
|
67
|
+
// (tooltips, popovers — the body is outside the fullscreen tree) re-render
|
|
68
|
+
// once the node mounts.
|
|
69
|
+
const [rootEl, setRootEl] = useState<HTMLDivElement | null>(null);
|
|
70
|
+
const setRoot = useCallback((el: HTMLDivElement | null) => {
|
|
71
|
+
rootRef.current = el;
|
|
72
|
+
setRootEl(el);
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
75
|
+
const [overviewOpen, setOverviewOpen] = useState(false);
|
|
76
|
+
const [helpOpen, setHelpOpen] = useState(false);
|
|
77
|
+
const [blackout, setBlackout] = useState<'black' | 'white' | null>(null);
|
|
78
|
+
const [laser, setLaser] = useState(false);
|
|
79
|
+
const [keyboardDriven, setKeyboardDriven] = useState(false);
|
|
80
|
+
const [mobileChromeVisible, setMobileChromeVisible] = useState(false);
|
|
81
|
+
const [mobileChromeDeadline, setMobileChromeDeadline] = useState(0);
|
|
82
|
+
const [startedAt] = useState(() => Date.now());
|
|
83
|
+
const [windowed, setWindowed] = useState(!fullscreen);
|
|
84
|
+
// Mirror windowed into a ref so the fullscreenchange listener can read the
|
|
85
|
+
// latest value without re-binding — exits from window mode must not call
|
|
86
|
+
// onExit, but exits initiated by the browser (Esc in fullscreen) must.
|
|
87
|
+
const windowedRef = useRef(windowed);
|
|
88
|
+
windowedRef.current = windowed;
|
|
89
|
+
|
|
90
|
+
const canPrev = index > 0;
|
|
91
|
+
const canNext = index < pages.length - 1;
|
|
92
|
+
|
|
93
|
+
const stepControllerRef = useRef<StepController | null>(null);
|
|
94
|
+
const [entryDirection, setEntryDirection] = useState<EntryDirection>('jump');
|
|
95
|
+
const [stepAggregate, setStepAggregate] = useState<StepAggregate>({
|
|
96
|
+
revealed: 0,
|
|
97
|
+
stepCount: 0,
|
|
98
|
+
});
|
|
99
|
+
const handleAggregateChange = useCallback((a: StepAggregate) => {
|
|
100
|
+
setStepAggregate((cur) =>
|
|
101
|
+
cur.revealed === a.revealed && cur.stepCount === a.stepCount ? cur : a,
|
|
102
|
+
);
|
|
103
|
+
}, []);
|
|
104
|
+
|
|
105
|
+
// Every navigation funnels through here so entryDirection is settled
|
|
106
|
+
// synchronously, before the incoming page's <Steps> reads it on mount.
|
|
107
|
+
const handleIndexChange = useCallback(
|
|
108
|
+
(next: number) => {
|
|
109
|
+
const delta = next - index;
|
|
110
|
+
setEntryDirection(delta === 1 ? 'forward' : delta === -1 ? 'backward' : 'jump');
|
|
111
|
+
onIndexChange(next);
|
|
112
|
+
},
|
|
113
|
+
[index, onIndexChange],
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const goPrev = useCallback(() => {
|
|
117
|
+
if (stepControllerRef.current?.retreat()) return;
|
|
118
|
+
if (index > 0) handleIndexChange(index - 1);
|
|
119
|
+
}, [index, handleIndexChange]);
|
|
120
|
+
const goNext = useCallback(() => {
|
|
121
|
+
if (stepControllerRef.current?.advance()) return;
|
|
122
|
+
if (index < pages.length - 1) handleIndexChange(index + 1);
|
|
123
|
+
}, [index, pages.length, handleIndexChange]);
|
|
124
|
+
|
|
125
|
+
const overlayActive = controls && (overviewOpen || helpOpen);
|
|
126
|
+
const overlayActiveRef = useRef(overlayActive);
|
|
127
|
+
const showMobileChrome = useCallback(() => {
|
|
128
|
+
if (!controls || !isMobile) return;
|
|
129
|
+
setMobileChromeVisible(true);
|
|
130
|
+
setMobileChromeDeadline(Date.now() + MOBILE_CHROME_HIDE_MS);
|
|
131
|
+
}, [controls, isMobile]);
|
|
132
|
+
const handleMobileViewportClick = useCallback(
|
|
133
|
+
({ y }: { x: number; y: number }) => {
|
|
134
|
+
if (!controls || !isMobile || y < 0.5) return;
|
|
135
|
+
showMobileChrome();
|
|
136
|
+
},
|
|
137
|
+
[controls, isMobile, showMobileChrome],
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (!controls || !isMobile) {
|
|
142
|
+
setMobileChromeVisible(false);
|
|
143
|
+
setMobileChromeDeadline(0);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
setMobileChromeVisible(true);
|
|
147
|
+
setMobileChromeDeadline(Date.now() + MOBILE_CHROME_HIDE_MS);
|
|
148
|
+
}, [controls, isMobile]);
|
|
149
|
+
|
|
150
|
+
useEffect(() => {
|
|
151
|
+
const wasOverlayActive = overlayActiveRef.current;
|
|
152
|
+
overlayActiveRef.current = overlayActive;
|
|
153
|
+
if (wasOverlayActive && !overlayActive) showMobileChrome();
|
|
154
|
+
}, [overlayActive, showMobileChrome]);
|
|
155
|
+
|
|
156
|
+
useEffect(() => {
|
|
157
|
+
if (
|
|
158
|
+
!controls ||
|
|
159
|
+
!isMobile ||
|
|
160
|
+
overlayActive ||
|
|
161
|
+
!mobileChromeVisible ||
|
|
162
|
+
mobileChromeDeadline === 0
|
|
163
|
+
) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const id = window.setTimeout(
|
|
167
|
+
() => {
|
|
168
|
+
setMobileChromeVisible(false);
|
|
169
|
+
},
|
|
170
|
+
Math.max(0, mobileChromeDeadline - Date.now()),
|
|
171
|
+
);
|
|
172
|
+
return () => window.clearTimeout(id);
|
|
173
|
+
}, [controls, isMobile, mobileChromeDeadline, mobileChromeVisible, overlayActive]);
|
|
174
|
+
|
|
175
|
+
useClickPageNavigation({
|
|
176
|
+
ref: rootRef,
|
|
177
|
+
enabled: !overlayActive,
|
|
178
|
+
canPrev,
|
|
179
|
+
canNext,
|
|
180
|
+
onPrev: goPrev,
|
|
181
|
+
onNext: goNext,
|
|
182
|
+
onViewportClick: controls && isMobile ? handleMobileViewportClick : undefined,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
useWheelPageNavigation({
|
|
186
|
+
ref: rootRef,
|
|
187
|
+
enabled: !overlayActive,
|
|
188
|
+
canPrev,
|
|
189
|
+
canNext,
|
|
190
|
+
onPrev: goPrev,
|
|
191
|
+
onNext: goNext,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
useTouchSwipe({
|
|
195
|
+
ref: rootRef,
|
|
196
|
+
enabled: controls && !overlayActive,
|
|
197
|
+
onPrev: goPrev,
|
|
198
|
+
onNext: goNext,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
useEffect(() => {
|
|
202
|
+
if (windowed) return;
|
|
203
|
+
const el = rootRef.current;
|
|
204
|
+
if (!el) return;
|
|
205
|
+
if (document.fullscreenElement !== el) {
|
|
206
|
+
el.requestFullscreen?.().catch(() => {});
|
|
207
|
+
}
|
|
208
|
+
return () => {
|
|
209
|
+
if (document.fullscreenElement) document.exitFullscreen?.().catch(() => {});
|
|
210
|
+
};
|
|
211
|
+
}, [windowed]);
|
|
212
|
+
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
if (!allowExit) return;
|
|
215
|
+
const onFsChange = () => {
|
|
216
|
+
if (!document.fullscreenElement && !windowedRef.current) onExit();
|
|
217
|
+
};
|
|
218
|
+
document.addEventListener('fullscreenchange', onFsChange);
|
|
219
|
+
return () => document.removeEventListener('fullscreenchange', onFsChange);
|
|
220
|
+
}, [onExit, allowExit]);
|
|
221
|
+
|
|
222
|
+
const toggleFullscreen = useCallback(() => {
|
|
223
|
+
setWindowed((w) => !w);
|
|
224
|
+
}, []);
|
|
225
|
+
|
|
226
|
+
// Player is the source of truth: it re-publishes state on every change
|
|
227
|
+
// and answers `request-state` pings so newly opened presenter windows
|
|
228
|
+
// hydrate immediately.
|
|
229
|
+
const presenterState = useMemo<PresenterState>(
|
|
230
|
+
() => ({
|
|
231
|
+
index,
|
|
232
|
+
pageCount: pages.length,
|
|
233
|
+
blackout,
|
|
234
|
+
startedAt,
|
|
235
|
+
stepIndex: stepAggregate.revealed,
|
|
236
|
+
stepCount: stepAggregate.stepCount,
|
|
237
|
+
}),
|
|
238
|
+
[index, pages.length, blackout, startedAt, stepAggregate],
|
|
239
|
+
);
|
|
240
|
+
const presenterStateRef = useRef(presenterState);
|
|
241
|
+
presenterStateRef.current = presenterState;
|
|
242
|
+
|
|
243
|
+
const handlePresenterCommand = useCallback(
|
|
244
|
+
(msg: PresenterCommand, send: (m: PresenterCommand) => void) => {
|
|
245
|
+
if (msg.type === 'next') goNext();
|
|
246
|
+
else if (msg.type === 'prev') goPrev();
|
|
247
|
+
else if (msg.type === 'goto') {
|
|
248
|
+
handleIndexChange(Math.max(0, Math.min(pages.length - 1, msg.index)));
|
|
249
|
+
} else if (msg.type === 'toggle-blackout') {
|
|
250
|
+
setBlackout((cur) => (cur === msg.mode ? null : msg.mode));
|
|
251
|
+
} else if (msg.type === 'request-state') {
|
|
252
|
+
send({ type: 'state', state: presenterStateRef.current });
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
[goNext, goPrev, handleIndexChange, pages.length],
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const channel = usePresenterChannel(slideId ?? '__none__', (msg) => {
|
|
259
|
+
if (!controls) return;
|
|
260
|
+
handlePresenterCommand(msg, (m) => channel.send(m));
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
useEffect(() => {
|
|
264
|
+
if (!controls || !channel.available) return;
|
|
265
|
+
channel.send({ type: 'state', state: presenterState });
|
|
266
|
+
}, [controls, channel, presenterState]);
|
|
267
|
+
|
|
268
|
+
useEffect(() => {
|
|
269
|
+
const onKey = (e: KeyboardEvent) => {
|
|
270
|
+
const tgt = e.target;
|
|
271
|
+
if (tgt instanceof HTMLElement && tgt.matches('input, textarea')) return;
|
|
272
|
+
|
|
273
|
+
// While an overlay is open, only Esc and the toggle that owns it
|
|
274
|
+
// should reach the Player. Overview installs its own capture-phase
|
|
275
|
+
// listener and stops propagation, so it won't double-fire here.
|
|
276
|
+
if (overlayActive) {
|
|
277
|
+
if (e.key === 'Escape') {
|
|
278
|
+
e.preventDefault();
|
|
279
|
+
if (overviewOpen) setOverviewOpen(false);
|
|
280
|
+
if (helpOpen) setHelpOpen(false);
|
|
281
|
+
} else if (helpOpen && (e.key === '?' || e.key === 'h' || e.key === 'H')) {
|
|
282
|
+
e.preventDefault();
|
|
283
|
+
setHelpOpen(false);
|
|
284
|
+
}
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Esc → close blackout if any, otherwise exit fullscreen.
|
|
289
|
+
if (e.key === 'Escape') {
|
|
290
|
+
if (controls && blackout) {
|
|
291
|
+
e.preventDefault();
|
|
292
|
+
setBlackout(null);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (allowExit) onExit();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const isNext =
|
|
300
|
+
e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === ' ' || e.key === 'PageDown';
|
|
301
|
+
const isPrev = e.key === 'ArrowLeft' || e.key === 'ArrowUp' || e.key === 'PageUp';
|
|
302
|
+
|
|
303
|
+
if (isNext || isPrev) {
|
|
304
|
+
if (controls && blackout) setBlackout(null);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (isNext) {
|
|
308
|
+
e.preventDefault();
|
|
309
|
+
setKeyboardDriven(true);
|
|
310
|
+
goNext();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (isPrev) {
|
|
314
|
+
e.preventDefault();
|
|
315
|
+
setKeyboardDriven(true);
|
|
316
|
+
goPrev();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (e.key === 'Home') {
|
|
320
|
+
setKeyboardDriven(true);
|
|
321
|
+
handleIndexChange(0);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (e.key === 'End') {
|
|
325
|
+
setKeyboardDriven(true);
|
|
326
|
+
handleIndexChange(pages.length - 1);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (!controls) return;
|
|
331
|
+
// Single-letter shortcuts only fire when no modifier is held — keeps
|
|
332
|
+
// browser shortcuts (Cmd/Ctrl-something) from being hijacked.
|
|
333
|
+
if (e.altKey || e.ctrlKey || e.metaKey) return;
|
|
334
|
+
|
|
335
|
+
if (e.key === 'b' || e.key === 'B') {
|
|
336
|
+
e.preventDefault();
|
|
337
|
+
setBlackout((c) => (c === 'black' ? null : 'black'));
|
|
338
|
+
} else if (e.key === 'w' || e.key === 'W') {
|
|
339
|
+
e.preventDefault();
|
|
340
|
+
setBlackout((c) => (c === 'white' ? null : 'white'));
|
|
341
|
+
} else if (e.key === 'o' || e.key === 'O') {
|
|
342
|
+
e.preventDefault();
|
|
343
|
+
setOverviewOpen((v) => !v);
|
|
344
|
+
} else if (e.key === 'l' || e.key === 'L') {
|
|
345
|
+
e.preventDefault();
|
|
346
|
+
setLaser((v) => !v);
|
|
347
|
+
} else if (e.key === 'h' || e.key === 'H' || e.key === '?') {
|
|
348
|
+
e.preventDefault();
|
|
349
|
+
setHelpOpen((v) => !v);
|
|
350
|
+
} else if ((e.key === 'p' || e.key === 'P') && slideId) {
|
|
351
|
+
e.preventDefault();
|
|
352
|
+
openPresenterWindow(slideId);
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
window.addEventListener('keydown', onKey);
|
|
356
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
357
|
+
}, [
|
|
358
|
+
controls,
|
|
359
|
+
overlayActive,
|
|
360
|
+
overviewOpen,
|
|
361
|
+
helpOpen,
|
|
362
|
+
blackout,
|
|
363
|
+
allowExit,
|
|
364
|
+
onExit,
|
|
365
|
+
goNext,
|
|
366
|
+
goPrev,
|
|
367
|
+
handleIndexChange,
|
|
368
|
+
pages.length,
|
|
369
|
+
slideId,
|
|
370
|
+
]);
|
|
371
|
+
|
|
372
|
+
// The control bar + progress strip only surface when the pointer is in
|
|
373
|
+
// the bottom hot zone. Keyboard nav (arrows / space / PgDn) never reveals
|
|
374
|
+
// them — intentional so the deck stays clean during a talk.
|
|
375
|
+
const pointerNearBottom = usePointerNearBottom(
|
|
376
|
+
BAR_HOTZONE_PX,
|
|
377
|
+
controls && !overlayActive && !isMobile,
|
|
378
|
+
);
|
|
379
|
+
const chromeVisible = overlayActive || (isMobile ? mobileChromeVisible : pointerNearBottom);
|
|
380
|
+
const idle = useIdle(IDLE_HIDE_MS, controls && !overlayActive);
|
|
381
|
+
|
|
382
|
+
useEffect(() => {
|
|
383
|
+
if (!keyboardDriven) return;
|
|
384
|
+
const clear = () => setKeyboardDriven(false);
|
|
385
|
+
window.addEventListener('mousemove', clear, { passive: true });
|
|
386
|
+
return () => window.removeEventListener('mousemove', clear);
|
|
387
|
+
}, [keyboardDriven]);
|
|
388
|
+
|
|
389
|
+
const hideCursor =
|
|
390
|
+
controls &&
|
|
391
|
+
!isMobile &&
|
|
392
|
+
(laser || keyboardDriven || (idle && !overlayActive && !pointerNearBottom));
|
|
393
|
+
|
|
394
|
+
return (
|
|
395
|
+
<div
|
|
396
|
+
ref={setRoot}
|
|
397
|
+
className={cn(
|
|
398
|
+
'fixed inset-0 flex items-center justify-center overflow-hidden bg-black',
|
|
399
|
+
controls && 'select-none',
|
|
400
|
+
controls && (hideCursor ? 'cursor-none' : 'cursor-default'),
|
|
401
|
+
)}
|
|
402
|
+
style={design ? { background: design.palette.bg } : undefined}
|
|
403
|
+
>
|
|
404
|
+
<SlideCanvas flat design={design}>
|
|
405
|
+
<SlideTransitionLayer
|
|
406
|
+
pages={pages}
|
|
407
|
+
index={index}
|
|
408
|
+
total={pages.length}
|
|
409
|
+
moduleTransition={transition}
|
|
410
|
+
disabled={prefersReducedMotion}
|
|
411
|
+
stepControllerRef={stepControllerRef}
|
|
412
|
+
entryDirection={entryDirection}
|
|
413
|
+
onStepAggregateChange={handleAggregateChange}
|
|
414
|
+
/>
|
|
415
|
+
</SlideCanvas>
|
|
416
|
+
|
|
417
|
+
{controls && (
|
|
418
|
+
<div data-osd-chrome style={{ display: 'contents' }}>
|
|
419
|
+
<PresentProgressBar index={index} total={pages.length} visible={chromeVisible} />
|
|
420
|
+
<PresentBlackoutOverlay mode={blackout} />
|
|
421
|
+
<PresentJumpInput pageCount={pages.length} onJump={handleIndexChange} />
|
|
422
|
+
<PresentLaserPointer enabled={laser} />
|
|
423
|
+
<PresentControlBar
|
|
424
|
+
tooltipContainer={rootEl}
|
|
425
|
+
index={index}
|
|
426
|
+
total={pages.length}
|
|
427
|
+
visible={chromeVisible}
|
|
428
|
+
startedAt={startedAt}
|
|
429
|
+
blackout={blackout}
|
|
430
|
+
laser={laser}
|
|
431
|
+
allowExit={allowExit}
|
|
432
|
+
windowed={windowed}
|
|
433
|
+
onPrev={goPrev}
|
|
434
|
+
onNext={goNext}
|
|
435
|
+
onMobileInteraction={showMobileChrome}
|
|
436
|
+
onOverview={() => setOverviewOpen(true)}
|
|
437
|
+
onBlackout={(mode) => setBlackout((c) => (c === mode ? null : mode))}
|
|
438
|
+
onLaser={() => setLaser((v) => !v)}
|
|
439
|
+
onPresenter={() => slideId && openPresenterWindow(slideId)}
|
|
440
|
+
onToggleFullscreen={toggleFullscreen}
|
|
441
|
+
onHelp={() => setHelpOpen(true)}
|
|
442
|
+
onExit={onExit}
|
|
443
|
+
/>
|
|
444
|
+
<OverviewGrid
|
|
445
|
+
pages={pages}
|
|
446
|
+
design={design}
|
|
447
|
+
open={overviewOpen}
|
|
448
|
+
current={index}
|
|
449
|
+
onClose={() => setOverviewOpen(false)}
|
|
450
|
+
onSelect={handleIndexChange}
|
|
451
|
+
variant="present"
|
|
452
|
+
moduleTransition={transition}
|
|
453
|
+
tooltipContainer={rootEl}
|
|
454
|
+
/>
|
|
455
|
+
<PresentHelpOverlay open={helpOpen} onOpenChange={setHelpOpen} container={rootEl} />
|
|
456
|
+
</div>
|
|
457
|
+
)}
|
|
458
|
+
</div>
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export function openPresenterWindow(slideId: string) {
|
|
463
|
+
if (typeof window === 'undefined') return;
|
|
464
|
+
const url = `${import.meta.env.BASE_URL}s/${encodeURIComponent(slideId)}/presenter`;
|
|
465
|
+
window.open(url, `open-aippt-presenter-${slideId}`, 'popup,width=1280,height=800');
|
|
466
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Loader2 } from 'lucide-react';
|
|
2
|
+
import { format, useLocale } from '@/lib/use-locale';
|
|
3
|
+
import type { PptxExportProgress } from '../lib/export-pptx';
|
|
4
|
+
import { Progress } from './ui/progress';
|
|
5
|
+
|
|
6
|
+
export function PptxProgressToast({ progress }: { progress: PptxExportProgress }) {
|
|
7
|
+
const t = useLocale();
|
|
8
|
+
const text =
|
|
9
|
+
progress.phase === 'processing'
|
|
10
|
+
? format(t.pptxToast.processing, {
|
|
11
|
+
current: progress.current.toString().padStart(2, '0'),
|
|
12
|
+
total: progress.total.toString().padStart(2, '0'),
|
|
13
|
+
})
|
|
14
|
+
: progress.phase === 'generating'
|
|
15
|
+
? t.pptxToast.generating
|
|
16
|
+
: t.pptxToast.done;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="flex w-80 items-start gap-3 rounded-[8px] border border-border bg-popover px-3.5 py-3 text-popover-foreground shadow-floating">
|
|
20
|
+
<Loader2 className="mt-0.5 size-3.5 shrink-0 animate-spin text-brand" />
|
|
21
|
+
<div className="min-w-0 flex-1">
|
|
22
|
+
<p className="font-heading text-[12.5px] font-semibold tracking-tight">
|
|
23
|
+
{t.pptxToast.title}
|
|
24
|
+
</p>
|
|
25
|
+
<p className="truncate font-mono text-[10.5px] tracking-[0.04em] text-muted-foreground">
|
|
26
|
+
{text}
|
|
27
|
+
</p>
|
|
28
|
+
<Progress value={Math.round(progress.percent)} className="mt-2 h-[3px]" />
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils';
|
|
2
|
+
|
|
3
|
+
type Props = {
|
|
4
|
+
mode: 'black' | 'white' | null;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function PresentBlackoutOverlay({ mode }: Props) {
|
|
8
|
+
if (!mode) return null;
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
aria-hidden
|
|
12
|
+
className={cn(
|
|
13
|
+
'pointer-events-none absolute inset-0 z-20 motion-safe:transition-opacity motion-safe:duration-150',
|
|
14
|
+
mode === 'black' ? 'bg-black' : 'bg-white',
|
|
15
|
+
)}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|