@kywk/open-slide-core 2.3.1 → 2.3.3

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 (55) hide show
  1. package/dist/{build-CZ6Qtrng.js → build-46dO14h1.js} +1 -1
  2. package/dist/cli/bin.js +4 -4
  3. package/dist/{config-CpmGqpQi.d.ts → config-DJ8ZOe7C.d.ts} +6 -2
  4. package/dist/{config-B5LArqpp.js → config-KyYvWqq-.js} +2061 -355
  5. package/dist/{dev-CCgdakbZ.js → dev-C0n-o0-c.js} +2 -2
  6. package/dist/index.d.ts +3 -3
  7. package/dist/index.js +73 -32
  8. package/dist/locale/index.d.ts +1 -1
  9. package/dist/locale/index.js +1 -1
  10. package/dist/{preview-AwAqvG17.js → preview-CDWAGSfB.js} +1 -1
  11. package/dist/{types-CvAqFqsY.d.ts → types-tdaXSyah.d.ts} +111 -0
  12. package/dist/{update-C_RlnKu4.js → update-CqZqQRQk.js} +128 -7
  13. package/dist/vite/index.d.ts +1 -1
  14. package/dist/vite/index.js +1 -1
  15. package/dist/{zh-tw-B7_Oqv2q.js → zh-tw-DXcqQTrA.js} +540 -0
  16. package/package.json +10 -3
  17. package/src/agent/prompt.ts +141 -0
  18. package/src/agent/provider.ts +123 -0
  19. package/src/agent/types.ts +151 -0
  20. package/src/app/components/agent/agent-dock.tsx +35 -0
  21. package/src/app/components/agent/agent-markdown.tsx +45 -0
  22. package/src/app/components/agent/agent-panel.tsx +692 -0
  23. package/src/app/components/agent/agent-presence.tsx +74 -0
  24. package/src/app/components/agent/composer.tsx +120 -0
  25. package/src/app/components/agent/connect-view.tsx +184 -0
  26. package/src/app/components/agent/history-view.tsx +135 -0
  27. package/src/app/components/agent/provider-logo.tsx +34 -0
  28. package/src/app/components/agent/transcript.tsx +609 -0
  29. package/src/app/components/command/slide-command-menu.tsx +10 -0
  30. package/src/app/components/inline-svg.tsx +52 -21
  31. package/src/app/components/inspector/comment-layer.tsx +20 -1
  32. package/src/app/components/inspector/image-crop-dialog.tsx +1 -5
  33. package/src/app/components/inspector/inspector-panel.tsx +1 -1
  34. package/src/app/components/mermaid.tsx +42 -11
  35. package/src/app/components/panel/editor-sidebar.tsx +85 -16
  36. package/src/app/components/panel/panel-shell.tsx +2 -2
  37. package/src/app/components/panel/resize-handle.tsx +129 -0
  38. package/src/app/components/player.tsx +105 -23
  39. package/src/app/components/present/curtain.tsx +52 -0
  40. package/src/app/components/present/last-slide-hint.tsx +36 -0
  41. package/src/app/components/slide-canvas.tsx +1 -1
  42. package/src/app/components/thumbnail-rail.tsx +1 -1
  43. package/src/app/lib/agent/use-agent.ts +543 -0
  44. package/src/app/lib/use-auto-grow.ts +15 -0
  45. package/src/app/lib/utils.ts +4 -0
  46. package/src/app/routes/home.tsx +51 -7
  47. package/src/app/routes/slide.tsx +172 -116
  48. package/src/app/styles.css +170 -0
  49. package/src/editing/placeholder-id.ts +1 -0
  50. package/src/locale/en.ts +107 -0
  51. package/src/locale/es.ts +109 -0
  52. package/src/locale/ja.ts +100 -0
  53. package/src/locale/types.ts +110 -0
  54. package/src/locale/zh-cn.ts +97 -0
  55. package/src/locale/zh-tw.ts +97 -0
@@ -12,9 +12,16 @@ import { usePrefersReducedMotion } from '../lib/use-prefers-reduced-motion';
12
12
  import { OverviewGrid } from './overview-grid';
13
13
  import { PresentBlackoutOverlay } from './present/blackout-overlay';
14
14
  import { PresentControlBar } from './present/control-bar';
15
+ import {
16
+ afterPaint,
17
+ CURTAIN_FADE_MS,
18
+ PresentCurtain,
19
+ whenFullscreenSettled,
20
+ } from './present/curtain';
15
21
  import { PresentHelpOverlay } from './present/help-overlay';
16
22
  import { PresentJumpInput } from './present/jump-input';
17
23
  import { PresentLaserPointer } from './present/laser-pointer';
24
+ import { PresentLastSlideHint } from './present/last-slide-hint';
18
25
  import { PresentProgressBar } from './present/progress-bar';
19
26
  import { useIdle } from './present/use-idle';
20
27
  import { usePointerNearBottom } from './present/use-pointer-near-bottom';
@@ -48,6 +55,8 @@ type Props = {
48
55
  * without entering fullscreen. Defaults to true for back-compat.
49
56
  */
50
57
  fullscreen?: boolean;
58
+ /** Fade in from black on mount and fade to black before calling `onExit`. */
59
+ curtain?: boolean;
51
60
  };
52
61
 
53
62
  export function Player({
@@ -62,9 +71,13 @@ export function Player({
62
71
  slideId,
63
72
  onSwitchSlide,
64
73
  fullscreen = true,
74
+ curtain = false,
65
75
  }: Props) {
66
76
  const isMobile = useIsMobile();
67
77
  const prefersReducedMotion = usePrefersReducedMotion();
78
+ const fade = curtain && !prefersReducedMotion;
79
+ const [curtainOpaque, setCurtainOpaque] = useState(fade);
80
+ const exitingRef = useRef(false);
68
81
  const rootRef = useRef<HTMLDivElement | null>(null);
69
82
  // Mirrored as state so descendants portaling *into* the player subtree
70
83
  // (tooltips, popovers — the body is outside the fullscreen tree) re-render
@@ -93,6 +106,28 @@ export function Player({
93
106
  const canPrev = index > 0;
94
107
  const canNext = index < pages.length - 1;
95
108
 
109
+ // Stepping past either end nudges the slide like a rubber band so the
110
+ // press visibly registered; past the last slide a hint says how to leave.
111
+ const nudgeRef = useRef<HTMLDivElement | null>(null);
112
+ const [lastSlideHintAt, setLastSlideHintAt] = useState(0);
113
+ const bumpEdge = useCallback(
114
+ (direction: 'start' | 'end') => {
115
+ if (!controls) return;
116
+ if (direction === 'end') setLastSlideHintAt(Date.now());
117
+ if (prefersReducedMotion) return;
118
+ const offset = direction === 'end' ? -14 : 14;
119
+ nudgeRef.current?.animate(
120
+ [
121
+ { transform: 'translateX(0)' },
122
+ { transform: `translateX(${offset}px)`, offset: 0.3 },
123
+ { transform: 'translateX(0)' },
124
+ ],
125
+ { duration: 360, easing: 'cubic-bezier(0.2, 0.8, 0.3, 1)' },
126
+ );
127
+ },
128
+ [controls, prefersReducedMotion],
129
+ );
130
+
96
131
  const stepControllerRef = useRef<StepController | null>(null);
97
132
  const [entryDirection, setEntryDirection] = useState<EntryDirection>('jump');
98
133
  const [stepAggregate, setStepAggregate] = useState<StepAggregate>({
@@ -116,14 +151,34 @@ export function Player({
116
151
  [index, onIndexChange],
117
152
  );
118
153
 
154
+ const exitTimerRef = useRef<number | null>(null);
155
+ useEffect(
156
+ () => () => {
157
+ if (exitTimerRef.current !== null) window.clearTimeout(exitTimerRef.current);
158
+ },
159
+ [],
160
+ );
161
+ const requestExit = useCallback(() => {
162
+ if (!fade) {
163
+ onExit();
164
+ return;
165
+ }
166
+ if (exitingRef.current) return;
167
+ exitingRef.current = true;
168
+ setCurtainOpaque(true);
169
+ exitTimerRef.current = window.setTimeout(onExit, CURTAIN_FADE_MS);
170
+ }, [fade, onExit]);
171
+
119
172
  const goPrev = useCallback(() => {
120
173
  if (stepControllerRef.current?.retreat()) return;
121
174
  if (index > 0) handleIndexChange(index - 1);
122
- }, [index, handleIndexChange]);
175
+ else bumpEdge('start');
176
+ }, [index, handleIndexChange, bumpEdge]);
123
177
  const goNext = useCallback(() => {
124
178
  if (stepControllerRef.current?.advance()) return;
125
179
  if (index < pages.length - 1) handleIndexChange(index + 1);
126
- }, [index, pages.length, handleIndexChange]);
180
+ else bumpEdge('end');
181
+ }, [index, pages.length, handleIndexChange, bumpEdge]);
127
182
 
128
183
  const overlayActive = controls && (overviewOpen || helpOpen);
129
184
  const overlayActiveRef = useRef(overlayActive);
@@ -178,8 +233,8 @@ export function Player({
178
233
  useClickPageNavigation({
179
234
  ref: rootRef,
180
235
  enabled: !overlayActive,
181
- canPrev,
182
- canNext,
236
+ canPrev: canPrev || controls,
237
+ canNext: canNext || controls,
183
238
  onPrev: goPrev,
184
239
  onNext: goNext,
185
240
  onViewportClick: controls && isMobile ? handleMobileViewportClick : undefined,
@@ -216,11 +271,33 @@ export function Player({
216
271
  useEffect(() => {
217
272
  if (!allowExit) return;
218
273
  const onFsChange = () => {
219
- if (!document.fullscreenElement && !windowedRef.current) onExit();
274
+ if (!document.fullscreenElement && !windowedRef.current) requestExit();
220
275
  };
221
276
  document.addEventListener('fullscreenchange', onFsChange);
222
277
  return () => document.removeEventListener('fullscreenchange', onFsChange);
223
- }, [onExit, allowExit]);
278
+ }, [requestExit, allowExit]);
279
+
280
+ // Wait out the browser's own fullscreen animation before revealing the
281
+ // first slide, otherwise the fade plays while the screen is still resizing.
282
+ // biome-ignore lint/correctness/useExhaustiveDependencies: runs once on mount
283
+ useEffect(() => {
284
+ if (!fade) return;
285
+ let cancelPaint = () => {};
286
+ const reveal = () => {
287
+ cancelPaint = afterPaint(() => {
288
+ if (!exitingRef.current) setCurtainOpaque(false);
289
+ });
290
+ };
291
+ if (!fullscreen || document.fullscreenElement) {
292
+ reveal();
293
+ return () => cancelPaint();
294
+ }
295
+ const cancelWait = whenFullscreenSettled(reveal);
296
+ return () => {
297
+ cancelWait();
298
+ cancelPaint();
299
+ };
300
+ }, []);
224
301
 
225
302
  const toggleFullscreen = useCallback(() => {
226
303
  setWindowed((w) => !w);
@@ -296,7 +373,7 @@ export function Player({
296
373
  setBlackout(null);
297
374
  return;
298
375
  }
299
- if (allowExit) onExit();
376
+ if (allowExit) requestExit();
300
377
  return;
301
378
  }
302
379
 
@@ -364,7 +441,7 @@ export function Player({
364
441
  helpOpen,
365
442
  blackout,
366
443
  allowExit,
367
- onExit,
444
+ requestExit,
368
445
  goNext,
369
446
  goPrev,
370
447
  handleIndexChange,
@@ -403,26 +480,29 @@ export function Player({
403
480
  controls && (hideCursor ? 'cursor-none' : 'cursor-default'),
404
481
  )}
405
482
  >
406
- <SlideCanvas flat design={design}>
407
- {/* Keyed per deck so a presenter-driven deck switch cuts instead of
483
+ <div ref={nudgeRef} className="h-full w-full">
484
+ <SlideCanvas flat design={design}>
485
+ {/* Keyed per deck so a presenter-driven deck switch cuts instead of
408
486
  animating a transition between two unrelated decks. */}
409
- <SlideTransitionLayer
410
- key={slideId}
411
- pages={pages}
412
- index={index}
413
- total={pages.length}
414
- moduleTransition={transition}
415
- disabled={prefersReducedMotion}
416
- stepControllerRef={stepControllerRef}
417
- entryDirection={entryDirection}
418
- onStepAggregateChange={handleAggregateChange}
419
- />
420
- </SlideCanvas>
487
+ <SlideTransitionLayer
488
+ key={slideId}
489
+ pages={pages}
490
+ index={index}
491
+ total={pages.length}
492
+ moduleTransition={transition}
493
+ disabled={prefersReducedMotion}
494
+ stepControllerRef={stepControllerRef}
495
+ entryDirection={entryDirection}
496
+ onStepAggregateChange={handleAggregateChange}
497
+ />
498
+ </SlideCanvas>
499
+ </div>
421
500
 
422
501
  {controls && (
423
502
  <div data-osd-chrome style={{ display: 'contents' }}>
424
503
  <PresentProgressBar index={index} total={pages.length} visible={chromeVisible} />
425
504
  <PresentBlackoutOverlay mode={blackout} />
505
+ <PresentLastSlideHint shownAt={lastSlideHintAt} />
426
506
  <PresentJumpInput pageCount={pages.length} onJump={handleIndexChange} />
427
507
  <PresentLaserPointer enabled={laser} />
428
508
  <PresentControlBar
@@ -444,7 +524,7 @@ export function Player({
444
524
  onPresenter={() => slideId && openPresenterWindow(slideId)}
445
525
  onToggleFullscreen={toggleFullscreen}
446
526
  onHelp={() => setHelpOpen(true)}
447
- onExit={onExit}
527
+ onExit={requestExit}
448
528
  />
449
529
  <OverviewGrid
450
530
  pages={pages}
@@ -460,6 +540,8 @@ export function Player({
460
540
  <PresentHelpOverlay open={helpOpen} onOpenChange={setHelpOpen} container={rootEl} />
461
541
  </div>
462
542
  )}
543
+
544
+ {fade && <PresentCurtain opaque={curtainOpaque} className="absolute z-[100]" />}
463
545
  </div>
464
546
  );
465
547
  }
@@ -0,0 +1,52 @@
1
+ import { cn } from '@/lib/utils';
2
+
3
+ export const CURTAIN_FADE_MS = 400;
4
+
5
+ type Props = {
6
+ opaque: boolean;
7
+ className?: string;
8
+ };
9
+
10
+ export function PresentCurtain({ opaque, className }: Props) {
11
+ return (
12
+ <div
13
+ aria-hidden
14
+ className={cn(
15
+ 'pointer-events-none inset-0 bg-black transition-opacity ease-in-out',
16
+ opaque ? 'opacity-100' : 'opacity-0',
17
+ className,
18
+ )}
19
+ style={{ transitionDuration: `${CURTAIN_FADE_MS}ms` }}
20
+ />
21
+ );
22
+ }
23
+
24
+ // Two frames so the opaque state is painted before the fade starts;
25
+ // a single rAF can land in the same style recalc and skip the transition.
26
+ export function afterPaint(cb: () => void): () => void {
27
+ let inner = 0;
28
+ const outer = requestAnimationFrame(() => {
29
+ inner = requestAnimationFrame(cb);
30
+ });
31
+ return () => {
32
+ cancelAnimationFrame(outer);
33
+ cancelAnimationFrame(inner);
34
+ };
35
+ }
36
+
37
+ export function whenFullscreenSettled(cb: () => void, maxWaitMs = 800): () => void {
38
+ let fired = false;
39
+ const timer = window.setTimeout(done, maxWaitMs);
40
+ document.addEventListener('fullscreenchange', done);
41
+ function done() {
42
+ if (fired) return;
43
+ fired = true;
44
+ cleanup();
45
+ cb();
46
+ }
47
+ function cleanup() {
48
+ window.clearTimeout(timer);
49
+ document.removeEventListener('fullscreenchange', done);
50
+ }
51
+ return cleanup;
52
+ }
@@ -0,0 +1,36 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { usePanelMount } from '@/components/panel/panel-shell';
3
+ import { useLocale } from '@/lib/use-locale';
4
+ import { cn } from '@/lib/utils';
5
+
6
+ const VISIBLE_MS = 1800;
7
+
8
+ type Props = {
9
+ shownAt: number;
10
+ };
11
+
12
+ export function PresentLastSlideHint({ shownAt }: Props) {
13
+ const t = useLocale();
14
+ const [open, setOpen] = useState(false);
15
+ useEffect(() => {
16
+ if (!shownAt) return;
17
+ setOpen(true);
18
+ const id = window.setTimeout(() => setOpen(false), VISIBLE_MS);
19
+ return () => window.clearTimeout(id);
20
+ }, [shownAt]);
21
+ const { mounted, animVisible } = usePanelMount(open);
22
+ if (!mounted) return null;
23
+ return (
24
+ <div
25
+ role="status"
26
+ className={cn(
27
+ 'pointer-events-none absolute bottom-10 left-1/2 z-30 -translate-x-1/2 rounded-full bg-black/70 px-3.5 py-1.5 text-[12px] text-white/85 shadow-lg backdrop-blur-md motion-safe:transition-[opacity,translate]',
28
+ animVisible
29
+ ? 'opacity-100 motion-safe:duration-200'
30
+ : 'translate-y-1 opacity-0 motion-safe:duration-200',
31
+ )}
32
+ >
33
+ {t.present.lastSlideHint}
34
+ </div>
35
+ );
36
+ }
@@ -51,7 +51,7 @@ export function SlideCanvas({
51
51
  <div ref={containerRef} className={cn('relative h-full w-full', flat && 'overflow-hidden')}>
52
52
  <div
53
53
  className={cn(
54
- 'overflow-hidden bg-white text-black',
54
+ 'overflow-hidden bg-white text-start text-black',
55
55
  !flat && 'rounded-[6px] shadow-floating',
56
56
  )}
57
57
  style={
@@ -568,7 +568,7 @@ function HorizontalVirtualThumbList({
568
568
  aria-label={format(t.thumbnailRail.goToPageAria, { n: i + 1 })}
569
569
  aria-current={active ? 'page' : undefined}
570
570
  className={cn(
571
- 'group/thumb relative flex shrink-0 flex-col items-center gap-1.5 rounded-[6px] outline-none motion-safe:transition-[scale] motion-safe:duration-100 active:scale-[0.985]',
571
+ 'group/thumb relative flex shrink-0 flex-col items-center gap-1.5 rounded-[6px] text-left outline-none motion-safe:transition-[scale] motion-safe:duration-100 active:scale-[0.985]',
572
572
  'focus-visible:ring-1 focus-visible:ring-brand focus-visible:ring-offset-1 focus-visible:ring-offset-sidebar',
573
573
  )}
574
574
  >