@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.
Files changed (142) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -0
  3. package/bin.js +2 -0
  4. package/dist/build-DxTqmvsO.js +17 -0
  5. package/dist/cli/bin.d.ts +1 -0
  6. package/dist/cli/bin.js +86 -0
  7. package/dist/config-CjzqjrEA.js +4280 -0
  8. package/dist/config-DIC-yVPp.d.ts +23 -0
  9. package/dist/design-cpzS8aud.js +35 -0
  10. package/dist/dev-BYuTeJbA.js +20 -0
  11. package/dist/format-BCeKbTOM.js +1605 -0
  12. package/dist/index.d.ts +134 -0
  13. package/dist/index.js +467 -0
  14. package/dist/locale/index.d.ts +24 -0
  15. package/dist/locale/index.js +3 -0
  16. package/dist/preview-DlQvnJPq.js +18 -0
  17. package/dist/sync-BPZ0m27m.js +139 -0
  18. package/dist/sync-EsYusbbL.js +3 -0
  19. package/dist/types-CHmFPIG_.d.ts +430 -0
  20. package/dist/vite/index.d.ts +14 -0
  21. package/dist/vite/index.js +4 -0
  22. package/env.d.ts +59 -0
  23. package/package.json +103 -0
  24. package/skills/apply-comments/SKILL.md +83 -0
  25. package/skills/create-slide/SKILL.md +91 -0
  26. package/skills/create-theme/SKILL.md +250 -0
  27. package/skills/current-slide/SKILL.md +110 -0
  28. package/skills/slide-authoring/SKILL.md +625 -0
  29. package/src/app/app.tsx +47 -0
  30. package/src/app/components/asset-view.tsx +966 -0
  31. package/src/app/components/history-provider.tsx +120 -0
  32. package/src/app/components/image-placeholder.tsx +243 -0
  33. package/src/app/components/inspector/asset-picker-dialog.tsx +196 -0
  34. package/src/app/components/inspector/comment-widget.tsx +93 -0
  35. package/src/app/components/inspector/image-crop-dialog.tsx +212 -0
  36. package/src/app/components/inspector/inspect-overlay.tsx +387 -0
  37. package/src/app/components/inspector/inspector-panel.tsx +1115 -0
  38. package/src/app/components/inspector/inspector-provider.tsx +1218 -0
  39. package/src/app/components/inspector/save-bar.tsx +48 -0
  40. package/src/app/components/language-toggle.tsx +39 -0
  41. package/src/app/components/notes-drawer.tsx +120 -0
  42. package/src/app/components/overview-grid.tsx +363 -0
  43. package/src/app/components/panel/panel-fields.tsx +60 -0
  44. package/src/app/components/panel/panel-shell.tsx +80 -0
  45. package/src/app/components/panel/save-card.tsx +142 -0
  46. package/src/app/components/pdf-progress-toast.tsx +32 -0
  47. package/src/app/components/player.tsx +466 -0
  48. package/src/app/components/pptx-progress-toast.tsx +32 -0
  49. package/src/app/components/present/blackout-overlay.tsx +18 -0
  50. package/src/app/components/present/control-bar.tsx +315 -0
  51. package/src/app/components/present/help-overlay.tsx +57 -0
  52. package/src/app/components/present/jump-input.tsx +74 -0
  53. package/src/app/components/present/laser-pointer.tsx +39 -0
  54. package/src/app/components/present/progress-bar.tsx +26 -0
  55. package/src/app/components/present/use-idle.ts +46 -0
  56. package/src/app/components/present/use-pointer-near-bottom.ts +34 -0
  57. package/src/app/components/present/use-presenter-channel.ts +66 -0
  58. package/src/app/components/present/use-touch-swipe.ts +66 -0
  59. package/src/app/components/shared-element.tsx +48 -0
  60. package/src/app/components/sidebar/folder-item.tsx +258 -0
  61. package/src/app/components/sidebar/icon-picker.tsx +61 -0
  62. package/src/app/components/sidebar/mobile-pill.tsx +34 -0
  63. package/src/app/components/sidebar/sidebar-footer.tsx +105 -0
  64. package/src/app/components/sidebar/sidebar.tsx +284 -0
  65. package/src/app/components/slide-canvas.tsx +102 -0
  66. package/src/app/components/slide-transition-layer.tsx +844 -0
  67. package/src/app/components/style-panel/design-provider.tsx +148 -0
  68. package/src/app/components/style-panel/style-panel.tsx +349 -0
  69. package/src/app/components/style-panel/use-design.ts +112 -0
  70. package/src/app/components/theme-toggle.tsx +59 -0
  71. package/src/app/components/themes/theme-detail.tsx +305 -0
  72. package/src/app/components/themes/themes-gallery.tsx +149 -0
  73. package/src/app/components/thumbnail-rail.tsx +805 -0
  74. package/src/app/components/ui/badge.tsx +45 -0
  75. package/src/app/components/ui/button.tsx +99 -0
  76. package/src/app/components/ui/card.tsx +92 -0
  77. package/src/app/components/ui/context-menu.tsx +237 -0
  78. package/src/app/components/ui/dialog.tsx +157 -0
  79. package/src/app/components/ui/dropdown-menu.tsx +245 -0
  80. package/src/app/components/ui/input.tsx +25 -0
  81. package/src/app/components/ui/label.tsx +24 -0
  82. package/src/app/components/ui/popover.tsx +75 -0
  83. package/src/app/components/ui/progress.tsx +31 -0
  84. package/src/app/components/ui/scroll-area.tsx +53 -0
  85. package/src/app/components/ui/select.tsx +196 -0
  86. package/src/app/components/ui/separator.tsx +28 -0
  87. package/src/app/components/ui/slider.tsx +61 -0
  88. package/src/app/components/ui/sonner.tsx +48 -0
  89. package/src/app/components/ui/tabs.tsx +79 -0
  90. package/src/app/components/ui/textarea.tsx +22 -0
  91. package/src/app/components/ui/toggle-group.tsx +83 -0
  92. package/src/app/components/ui/toggle.tsx +45 -0
  93. package/src/app/components/ui/tooltip.tsx +58 -0
  94. package/src/app/favicon.ico +0 -0
  95. package/src/app/index.html +13 -0
  96. package/src/app/lib/assets.ts +242 -0
  97. package/src/app/lib/design-presets.ts +94 -0
  98. package/src/app/lib/design.ts +58 -0
  99. package/src/app/lib/export-html.ts +326 -0
  100. package/src/app/lib/export-pdf.ts +298 -0
  101. package/src/app/lib/export-pptx.ts +284 -0
  102. package/src/app/lib/folders.ts +239 -0
  103. package/src/app/lib/inspector/fiber.test.ts +154 -0
  104. package/src/app/lib/inspector/fiber.ts +85 -0
  105. package/src/app/lib/inspector/use-comments.ts +74 -0
  106. package/src/app/lib/inspector/use-editor.ts +73 -0
  107. package/src/app/lib/inspector/use-notes.ts +134 -0
  108. package/src/app/lib/locale-store.ts +67 -0
  109. package/src/app/lib/page-context.tsx +38 -0
  110. package/src/app/lib/print-ready.test.ts +32 -0
  111. package/src/app/lib/print-ready.ts +51 -0
  112. package/src/app/lib/sdk.test.ts +13 -0
  113. package/src/app/lib/sdk.ts +37 -0
  114. package/src/app/lib/slides.ts +26 -0
  115. package/src/app/lib/step-context.tsx +261 -0
  116. package/src/app/lib/themes.ts +22 -0
  117. package/src/app/lib/transition.ts +30 -0
  118. package/src/app/lib/use-agent-socket.ts +18 -0
  119. package/src/app/lib/use-click-page-navigation.ts +60 -0
  120. package/src/app/lib/use-is-mobile.ts +21 -0
  121. package/src/app/lib/use-locale.ts +8 -0
  122. package/src/app/lib/use-prefers-reduced-motion.ts +19 -0
  123. package/src/app/lib/use-slide-module.ts +48 -0
  124. package/src/app/lib/use-wheel-page-navigation.ts +99 -0
  125. package/src/app/lib/utils.test.ts +25 -0
  126. package/src/app/lib/utils.ts +6 -0
  127. package/src/app/main.tsx +14 -0
  128. package/src/app/routes/assets.tsx +9 -0
  129. package/src/app/routes/home-shell.tsx +213 -0
  130. package/src/app/routes/home.tsx +807 -0
  131. package/src/app/routes/presenter.tsx +418 -0
  132. package/src/app/routes/slide.tsx +1108 -0
  133. package/src/app/routes/themes.tsx +34 -0
  134. package/src/app/styles.css +429 -0
  135. package/src/app/virtual.d.ts +51 -0
  136. package/src/locale/en.ts +416 -0
  137. package/src/locale/format.ts +12 -0
  138. package/src/locale/index.ts +6 -0
  139. package/src/locale/ja.ts +422 -0
  140. package/src/locale/types.ts +443 -0
  141. package/src/locale/zh-cn.ts +414 -0
  142. 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
+ }