@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
@@ -14,6 +14,7 @@ import {
14
14
  MoreHorizontal,
15
15
  Play,
16
16
  Presentation,
17
+ Sparkles,
17
18
  Terminal,
18
19
  } from 'lucide-react';
19
20
  import {
@@ -21,12 +22,16 @@ import {
21
22
  type RefObject,
22
23
  useCallback,
23
24
  useEffect,
25
+ useLayoutEffect,
24
26
  useMemo,
25
27
  useRef,
26
28
  useState,
27
29
  } from 'react';
28
30
  import { Link, useNavigate, useParams, useSearchParams } from 'react-router-dom';
29
31
  import { toast } from 'sonner';
32
+ import { AgentToggleButton } from '@/components/agent/agent-dock';
33
+ import { AgentPanel } from '@/components/agent/agent-panel';
34
+ import { AgentPresence } from '@/components/agent/agent-presence';
30
35
  import { AssetView } from '@/components/asset-view';
31
36
  import { HistoryProvider } from '@/components/history-provider';
32
37
  import { CommentLayer } from '@/components/inspector/comment-layer';
@@ -40,6 +45,7 @@ import {
40
45
  } from '@/components/inspector/inspector-provider';
41
46
  import { SaveBar } from '@/components/inspector/save-bar';
42
47
  import { EditorSidebar } from '@/components/panel/editor-sidebar';
48
+ import { ResizeHandle, useStoredWidth } from '@/components/panel/resize-handle';
43
49
  import { DesignProvider } from '@/components/style-panel/design-provider';
44
50
  import { DesignToggleButton } from '@/components/style-panel/style-panel';
45
51
  import { Button, buttonVariants } from '@/components/ui/button';
@@ -57,6 +63,7 @@ import {
57
63
  } from '@/components/ui/dropdown-menu';
58
64
  import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
59
65
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
66
+ import { setDockOpen, useAgentActivity, useAgentDock } from '@/lib/agent/use-agent';
60
67
  import { useFolders } from '@/lib/folders';
61
68
  import {
62
69
  hasModifier,
@@ -74,11 +81,18 @@ import { format, useLocale } from '@/lib/use-locale';
74
81
  import { useMediaQuery } from '@/lib/use-media-query';
75
82
  import { useWheelPageNavigation } from '@/lib/use-wheel-page-navigation';
76
83
  import { cn } from '@/lib/utils';
84
+ import type { AgentTurnContext } from '../../agent/types';
77
85
  import { SlideCommandMenu } from '../components/command/slide-command-menu';
78
86
  import { PdfProgressToast, PptxProgressToast } from '../components/export-progress-toast';
79
87
  import { NotesDrawer } from '../components/notes-drawer';
80
88
  import { OverviewGrid } from '../components/overview-grid';
81
89
  import { openPresenterWindow, Player } from '../components/player';
90
+ import {
91
+ afterPaint,
92
+ CURTAIN_FADE_MS,
93
+ PresentCurtain,
94
+ whenFullscreenSettled,
95
+ } from '../components/present/curtain';
82
96
  import { SlideCanvas } from '../components/slide-canvas';
83
97
  import { isDeckWarmed, markDeckWarmed, SlidePreloadLayer } from '../components/slide-preload-layer';
84
98
  import { SlideTransitionLayer } from '../components/slide-transition-layer';
@@ -132,6 +146,13 @@ export function Slide() {
132
146
  const [designOpen, setDesignOpen] = useState(false);
133
147
  const [overviewOpen, setOverviewOpen] = useState(false);
134
148
  const [commandOpen, setCommandOpen] = useState(false);
149
+ const { open: agentOpen, toggle: toggleAgent } = useAgentDock();
150
+ useLayoutEffect(() => {
151
+ if (agentOpen) setDesignOpen(false);
152
+ }, [agentOpen]);
153
+ useLayoutEffect(() => {
154
+ if (designOpen) setDockOpen(false);
155
+ }, [designOpen]);
135
156
  const [, setWarmedTick] = useState(0);
136
157
  const handleAssetsWarmed = useCallback(() => {
137
158
  markDeckWarmed(slideId);
@@ -151,6 +172,55 @@ export function Slide() {
151
172
  const narrowToolbar = useMediaQuery('(max-width: 639.98px)');
152
173
  const prefersReducedMotion = usePrefersReducedMotion();
153
174
 
175
+ // 'entering' darkens the editor before the Player mounts; 'leaving' keeps
176
+ // the editor dark after the Player unmounts, then fades it back in.
177
+ const [curtain, setCurtain] = useState<'entering' | 'leaving' | null>(null);
178
+ const curtainTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
179
+ useEffect(
180
+ () => () => {
181
+ if (curtainTimerRef.current) clearTimeout(curtainTimerRef.current);
182
+ },
183
+ [],
184
+ );
185
+ const present = useCallback(
186
+ (mode: 'window' | 'fullscreen') => {
187
+ if (prefersReducedMotion) {
188
+ setPlayMode(mode);
189
+ return;
190
+ }
191
+ if (curtainTimerRef.current) return;
192
+ setCurtain('entering');
193
+ // Fullscreen is requested when the Player mounts; the delay stays well
194
+ // inside the browser's transient user-activation window.
195
+ curtainTimerRef.current = setTimeout(() => {
196
+ curtainTimerRef.current = null;
197
+ setCurtain(null);
198
+ setPlayMode(mode);
199
+ }, CURTAIN_FADE_MS);
200
+ },
201
+ [prefersReducedMotion],
202
+ );
203
+ const exitPresent = useCallback(() => {
204
+ setPlayMode(null);
205
+ if (!prefersReducedMotion) setCurtain('leaving');
206
+ }, [prefersReducedMotion]);
207
+ useEffect(() => {
208
+ if (curtain !== 'leaving' || playMode) return;
209
+ let cancelPaint = () => {};
210
+ const reveal = () => {
211
+ cancelPaint = afterPaint(() => setCurtain(null));
212
+ };
213
+ if (!document.fullscreenElement) {
214
+ reveal();
215
+ return () => cancelPaint();
216
+ }
217
+ const cancelWait = whenFullscreenSettled(reveal);
218
+ return () => {
219
+ cancelWait();
220
+ cancelPaint();
221
+ };
222
+ }, [curtain, playMode]);
223
+
154
224
  const modulePages = useMemo(() => slide?.default ?? [], [slide]);
155
225
  const [pages, setPages] = useState<typeof modulePages>(modulePages);
156
226
  useEffect(() => {
@@ -195,6 +265,16 @@ export function Slide() {
195
265
  [pageCount, setSearchParams],
196
266
  );
197
267
 
268
+ // While an agent writes this deck page by page, jump to each page as it
269
+ // lands so the user can follow along.
270
+ const agentActivity = useAgentActivity(slideId);
271
+ const agentWriting = agentActivity !== null;
272
+ const seenPageCount = useRef(pageCount);
273
+ useEffect(() => {
274
+ if (agentWriting && pageCount > seenPageCount.current) goTo(pageCount - 1);
275
+ seenPageCount.current = pageCount;
276
+ }, [agentWriting, pageCount, goTo]);
277
+
198
278
  const reorderPage = useCallback(
199
279
  async (from: number, to: number) => {
200
280
  if (from === to) return;
@@ -310,7 +390,7 @@ export function Slide() {
310
390
  // When showSlideUi is false the read-only <Player> is rendered and owns
311
391
  // keyboard navigation (including step-aware advance/retreat). Attaching this
312
392
  // page-nav handler too would race it and skip <Steps> reveals, so bail out.
313
- if (playMode || !showSlideUi) return;
393
+ if (playMode || curtain || !showSlideUi) return;
314
394
  const onKey = (e: KeyboardEvent) => {
315
395
  if (
316
396
  isTypingTarget(e.target) ||
@@ -343,19 +423,21 @@ export function Slide() {
343
423
  return;
344
424
  }
345
425
  if (e.key === 'f' || e.key === 'F') {
346
- setPlayMode('fullscreen');
426
+ present('fullscreen');
347
427
  } else if (e.key === 'Enter') {
348
- setPlayMode('window');
428
+ present('window');
349
429
  } else if (e.key === 'p' || e.key === 'P') {
350
430
  if (slideId) openPresenterWindow(slideId);
351
- setPlayMode('window');
431
+ present('window');
352
432
  } else if (import.meta.env.DEV && (e.key === 'd' || e.key === 'D')) {
353
433
  setDesignOpen((v) => !v);
434
+ } else if (import.meta.env.DEV && (e.key === 'a' || e.key === 'A')) {
435
+ toggleAgent();
354
436
  }
355
437
  };
356
438
  window.addEventListener('keydown', onKey);
357
439
  return () => window.removeEventListener('keydown', onKey);
358
- }, [index, goTo, playMode, slideId, overviewOpen]);
440
+ }, [index, goTo, playMode, curtain, slideId, overviewOpen, toggleAgent, present]);
359
441
 
360
442
  if (error) {
361
443
  return (
@@ -396,8 +478,9 @@ export function Slide() {
396
478
  transition={presented.slide.transition}
397
479
  index={presented.index}
398
480
  onIndexChange={presentReady ? goTo : noop}
399
- onExit={() => setPlayMode(null)}
481
+ onExit={exitPresent}
400
482
  controls
483
+ curtain={!prefersReducedMotion}
401
484
  slideId={presented.slideId}
402
485
  onSwitchSlide={switchPresentedSlide}
403
486
  fullscreen={playMode === 'fullscreen'}
@@ -619,8 +702,11 @@ export function Slide() {
619
702
  <InspectorProvider
620
703
  slideId={slideId}
621
704
  pageIndex={index}
622
- panelHidden={designOpen}
623
- onPanelOpen={() => setDesignOpen(false)}
705
+ panelHidden={designOpen || agentOpen}
706
+ onPanelOpen={() => {
707
+ setDesignOpen(false);
708
+ setDockOpen(false);
709
+ }}
624
710
  >
625
711
  <SelectionReporter />
626
712
  <div className="flex h-dvh flex-col overflow-hidden bg-sidebar text-foreground">
@@ -739,6 +825,12 @@ export function Slide() {
739
825
  />
740
826
  )}
741
827
  <DropdownMenuGroup>
828
+ {narrowToolbar && import.meta.env.DEV && (
829
+ <DropdownMenuItem onClick={toggleAgent}>
830
+ <Sparkles />
831
+ {t.agent.openTitle}
832
+ </DropdownMenuItem>
833
+ )}
742
834
  <DropdownMenuItem onClick={() => setCommandOpen(true)}>
743
835
  <Terminal />
744
836
  {t.commandMenu.trigger}
@@ -753,6 +845,7 @@ export function Slide() {
753
845
  </DropdownMenuContent>
754
846
  </DropdownMenu>
755
847
  )}
848
+ {import.meta.env.DEV && view === 'slides' && !narrowToolbar && <AgentToggleButton />}
756
849
  {view === 'slides' && !narrowToolbar && (
757
850
  <DesignToggleButton active={designOpen} onToggle={() => setDesignOpen((v) => !v)} />
758
851
  )}
@@ -764,7 +857,7 @@ export function Slide() {
764
857
  size="sm"
765
858
  variant="brand"
766
859
  aria-label={t.slide.present}
767
- onClick={() => setPlayMode(isMobile ? 'window' : 'fullscreen')}
860
+ onClick={() => present(isMobile ? 'window' : 'fullscreen')}
768
861
  className="rounded-r-none px-2.5 lg:px-3"
769
862
  >
770
863
  <Play className="size-3.5 fill-current" />
@@ -784,12 +877,12 @@ export function Slide() {
784
877
  </DropdownMenuTrigger>
785
878
  <DropdownMenuContent align="end" className="min-w-[200px]">
786
879
  <DropdownMenuGroup>
787
- <DropdownMenuItem onClick={() => setPlayMode('window')}>
880
+ <DropdownMenuItem onClick={() => present('window')}>
788
881
  <Play />
789
882
  {t.slide.presentInWindow}
790
883
  <DropdownMenuShortcut>↵</DropdownMenuShortcut>
791
884
  </DropdownMenuItem>
792
- <DropdownMenuItem onClick={() => setPlayMode('fullscreen')}>
885
+ <DropdownMenuItem onClick={() => present('fullscreen')}>
793
886
  <Maximize />
794
887
  {t.slide.presentFullscreen}
795
888
  <DropdownMenuShortcut>F</DropdownMenuShortcut>
@@ -797,7 +890,7 @@ export function Slide() {
797
890
  <DropdownMenuItem
798
891
  onClick={() => {
799
892
  if (slideId) openPresenterWindow(slideId);
800
- setPlayMode('window');
893
+ present('window');
801
894
  }}
802
895
  >
803
896
  <MonitorSpeaker />
@@ -834,6 +927,7 @@ export function Slide() {
834
927
  ref={slideViewportRef}
835
928
  data-inspector-root
836
929
  data-slide-id={slideId}
930
+ data-agent-provider={agentActivity?.provider}
837
931
  className="relative min-h-0 min-w-0 flex-1 bg-background p-2 md:mx-2 md:mb-2 md:rounded-[10px] md:p-10 md:shadow-edge md:ring-1 md:ring-foreground/[0.06]"
838
932
  >
839
933
  <SlideViewportNavigation
@@ -856,6 +950,7 @@ export function Slide() {
856
950
  <InlineEditLayer />
857
951
  <SaveBar />
858
952
  {import.meta.env.DEV && <CommentLayer />}
953
+ {agentActivity && <AgentPresence activity={agentActivity} revision={pages} />}
859
954
  </main>
860
955
  {/* Mobile-only horizontal rail. Sits below the canvas and
861
956
  pads its bottom for the iOS home indicator / Safari URL bar. */}
@@ -875,6 +970,17 @@ export function Slide() {
875
970
  <EditorSidebar
876
971
  designOpen={designOpen}
877
972
  onCloseDesign={() => setDesignOpen(false)}
973
+ agentOpen={agentOpen}
974
+ agent={
975
+ import.meta.env.DEV && (
976
+ <SlideAgentDock
977
+ slideId={slideId}
978
+ slideTitle={title}
979
+ pageNumber={index + 1}
980
+ totalPages={pageCount}
981
+ />
982
+ )
983
+ }
878
984
  />
879
985
  </div>
880
986
  {import.meta.env.DEV && (
@@ -907,15 +1013,16 @@ export function Slide() {
907
1013
  currentIndex={index}
908
1014
  exporting={exporting}
909
1015
  handlers={{
910
- onPresentWindow: () => setPlayMode('window'),
911
- onPresentFullscreen: () => setPlayMode('fullscreen'),
1016
+ onPresentWindow: () => present('window'),
1017
+ onPresentFullscreen: () => present('fullscreen'),
912
1018
  onPresenterView: () => {
913
1019
  if (slideId) openPresenterWindow(slideId);
914
- setPlayMode('window');
1020
+ present('window');
915
1021
  },
916
1022
  onCopyLink: copyLink,
917
1023
  onOverview: () => setOverviewOpen(true),
918
1024
  onToggleDesignPanel: () => setDesignOpen((v) => !v),
1025
+ onToggleAgentPanel: toggleAgent,
919
1026
  onExportHtml: exportHtml,
920
1027
  onExportPdf: exportPdf,
921
1028
  onExportPptx: exportPptx,
@@ -925,6 +1032,7 @@ export function Slide() {
925
1032
  />
926
1033
  )}
927
1034
  </div>
1035
+ <PresentCurtain opaque={curtain !== null} className="fixed z-[1000]" />
928
1036
  </InspectorProvider>
929
1037
  </HistoryProvider>
930
1038
  );
@@ -935,14 +1043,6 @@ const DEFAULT_RAIL_WIDTH = 264;
935
1043
  const MIN_RAIL_WIDTH = 200;
936
1044
  const MAX_RAIL_WIDTH = 480;
937
1045
 
938
- function readStoredRailWidth(): number {
939
- if (typeof window === 'undefined') return DEFAULT_RAIL_WIDTH;
940
- const raw = window.localStorage.getItem(RAIL_WIDTH_STORAGE_KEY);
941
- const parsed = raw == null ? Number.NaN : Number(raw);
942
- if (!Number.isFinite(parsed)) return DEFAULT_RAIL_WIDTH;
943
- return Math.min(MAX_RAIL_WIDTH, Math.max(MIN_RAIL_WIDTH, parsed));
944
- }
945
-
946
1046
  function ResizableRail(props: {
947
1047
  pages: SlideModule['default'];
948
1048
  design?: SlideModule['design'];
@@ -954,103 +1054,26 @@ function ResizableRail(props: {
954
1054
  onOverview?: () => void;
955
1055
  }) {
956
1056
  const t = useLocale();
957
- const [width, setWidth] = useState<number>(readStoredRailWidth);
958
- const [resizing, setResizing] = useState(false);
959
- const dragRef = useRef<{ startX: number; startWidth: number } | null>(null);
960
-
961
- useEffect(() => {
962
- if (typeof window === 'undefined') return;
963
- window.localStorage.setItem(RAIL_WIDTH_STORAGE_KEY, String(width));
964
- }, [width]);
965
-
966
- useEffect(() => {
967
- if (!resizing) return;
968
- const prev = {
969
- cursor: document.body.style.cursor,
970
- userSelect: document.body.style.userSelect,
971
- };
972
- document.body.style.cursor = 'col-resize';
973
- document.body.style.userSelect = 'none';
974
- return () => {
975
- document.body.style.cursor = prev.cursor;
976
- document.body.style.userSelect = prev.userSelect;
977
- };
978
- }, [resizing]);
979
-
980
- const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
981
- e.preventDefault();
982
- e.currentTarget.setPointerCapture(e.pointerId);
983
- dragRef.current = { startX: e.clientX, startWidth: width };
984
- setResizing(true);
985
- };
986
-
987
- const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
988
- if (!dragRef.current) return;
989
- const delta = e.clientX - dragRef.current.startX;
990
- const next = Math.min(
991
- MAX_RAIL_WIDTH,
992
- Math.max(MIN_RAIL_WIDTH, dragRef.current.startWidth + delta),
993
- );
994
- setWidth(next);
995
- };
996
-
997
- const onPointerUp = (e: React.PointerEvent<HTMLDivElement>) => {
998
- if (e.currentTarget.hasPointerCapture(e.pointerId)) {
999
- e.currentTarget.releasePointerCapture(e.pointerId);
1000
- }
1001
- dragRef.current = null;
1002
- setResizing(false);
1003
- };
1004
-
1005
- const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
1006
- const step = e.shiftKey ? 32 : 8;
1007
- if (e.key === 'ArrowLeft') {
1008
- e.preventDefault();
1009
- e.stopPropagation();
1010
- setWidth((w) => Math.max(MIN_RAIL_WIDTH, w - step));
1011
- } else if (e.key === 'ArrowRight') {
1012
- e.preventDefault();
1013
- e.stopPropagation();
1014
- setWidth((w) => Math.min(MAX_RAIL_WIDTH, w + step));
1015
- } else if (e.key === 'Home') {
1016
- e.preventDefault();
1017
- e.stopPropagation();
1018
- setWidth(DEFAULT_RAIL_WIDTH);
1019
- }
1020
- };
1057
+ const [width, setWidth] = useStoredWidth(
1058
+ RAIL_WIDTH_STORAGE_KEY,
1059
+ DEFAULT_RAIL_WIDTH,
1060
+ MIN_RAIL_WIDTH,
1061
+ MAX_RAIL_WIDTH,
1062
+ );
1021
1063
 
1022
1064
  return (
1023
1065
  <div className="relative hidden shrink-0 md:block" style={{ width }}>
1024
1066
  <ThumbnailRail width={width} {...props} />
1025
- {/* biome-ignore lint/a11y/useSemanticElements: focusable resize handle (splitter pattern), not a static <hr> */}
1026
- <div
1027
- role="separator"
1028
- aria-orientation="vertical"
1029
- aria-label={t.thumbnailRail.resizeRail}
1030
- aria-valuenow={width}
1031
- aria-valuemin={MIN_RAIL_WIDTH}
1032
- aria-valuemax={MAX_RAIL_WIDTH}
1033
- tabIndex={0}
1034
- onPointerDown={onPointerDown}
1035
- onPointerMove={onPointerMove}
1036
- onPointerUp={onPointerUp}
1037
- onPointerCancel={onPointerUp}
1038
- onKeyDown={onKeyDown}
1039
- onDoubleClick={() => setWidth(DEFAULT_RAIL_WIDTH)}
1040
- className={cn(
1041
- 'group/resize absolute inset-y-0 right-0 z-20 w-1.5 translate-x-1/2 cursor-col-resize touch-none outline-none',
1042
- 'focus-visible:bg-brand/20',
1043
- )}
1044
- >
1045
- <span
1046
- aria-hidden
1047
- className={cn(
1048
- 'pointer-events-none absolute inset-y-0 left-1/2 w-px -translate-x-1/2 bg-brand opacity-0 transition-opacity duration-150',
1049
- 'group-hover/resize:opacity-100 group-focus-visible/resize:opacity-100',
1050
- resizing && 'opacity-100',
1051
- )}
1052
- />
1053
- </div>
1067
+ <ResizeHandle
1068
+ width={width}
1069
+ min={MIN_RAIL_WIDTH}
1070
+ max={MAX_RAIL_WIDTH}
1071
+ defaultWidth={DEFAULT_RAIL_WIDTH}
1072
+ edge="right"
1073
+ label={t.thumbnailRail.resizeRail}
1074
+ onResize={setWidth}
1075
+ className="right-0 translate-x-1/2"
1076
+ />
1054
1077
  </div>
1055
1078
  );
1056
1079
  }
@@ -1096,6 +1119,39 @@ function EditorMenuItems({
1096
1119
  );
1097
1120
  }
1098
1121
 
1122
+ function SlideAgentDock({
1123
+ slideId,
1124
+ slideTitle,
1125
+ pageNumber,
1126
+ totalPages,
1127
+ }: {
1128
+ slideId: string;
1129
+ slideTitle: string;
1130
+ pageNumber: number;
1131
+ totalPages: number;
1132
+ }) {
1133
+ const { selected, comments } = useInspector();
1134
+ const context = useMemo<AgentTurnContext>(
1135
+ () => ({
1136
+ slideId,
1137
+ slideTitle,
1138
+ pageNumber,
1139
+ totalPages,
1140
+ pendingComments: comments.length,
1141
+ selection: selected
1142
+ ? {
1143
+ line: selected.line,
1144
+ column: selected.column,
1145
+ tagName: selected.anchor.tagName.toLowerCase(),
1146
+ text: (selected.anchor.textContent ?? '').replace(/\s+/g, ' ').trim().slice(0, 120),
1147
+ }
1148
+ : null,
1149
+ }),
1150
+ [slideId, slideTitle, pageNumber, totalPages, comments.length, selected],
1151
+ );
1152
+ return <AgentPanel context={context} onClose={() => setDockOpen(false)} />;
1153
+ }
1154
+
1099
1155
  function AgentConnectedBadge() {
1100
1156
  const t = useLocale();
1101
1157
  const connected = useAgentSocketConnected();
@@ -383,3 +383,173 @@
383
383
  background-clip: content-box;
384
384
  }
385
385
  }
386
+
387
+ /* Agent presence: ring, write sweep and shimmer shown while an agent edits a deck. */
388
+ @property --agent-angle {
389
+ syntax: "<angle>";
390
+ inherits: false;
391
+ initial-value: 0deg;
392
+ }
393
+
394
+ [data-agent-provider] {
395
+ --agent-a: oklch(0.68 0.14 42);
396
+ --agent-b: oklch(0.82 0.12 75);
397
+ }
398
+
399
+ [data-agent-provider="codex"] {
400
+ --agent-a: oklch(0.64 0.13 170);
401
+ --agent-b: oklch(0.7 0.14 240);
402
+ }
403
+
404
+ @keyframes agent-spin {
405
+ to {
406
+ --agent-angle: 360deg;
407
+ }
408
+ }
409
+
410
+ @keyframes agent-fade-in {
411
+ from {
412
+ opacity: 0;
413
+ }
414
+ }
415
+
416
+ .agent-ring {
417
+ pointer-events: none;
418
+ position: absolute;
419
+ inset: 0;
420
+ border-radius: inherit;
421
+ padding: 1.5px;
422
+ background: conic-gradient(
423
+ from var(--agent-angle),
424
+ transparent 0deg,
425
+ var(--agent-a) 70deg,
426
+ var(--agent-b) 140deg,
427
+ transparent 210deg,
428
+ transparent 360deg
429
+ );
430
+ mask:
431
+ linear-gradient(#000 0 0) content-box exclude,
432
+ linear-gradient(#000 0 0);
433
+ animation:
434
+ agent-spin 3.6s linear infinite,
435
+ agent-fade-in 400ms var(--ease-swift) both;
436
+ }
437
+
438
+ .agent-glow {
439
+ pointer-events: none;
440
+ position: absolute;
441
+ inset: 0;
442
+ border-radius: inherit;
443
+ box-shadow:
444
+ inset 0 0 0 1px color-mix(in oklch, var(--agent-a) 35%, transparent),
445
+ inset 0 0 48px -12px color-mix(in oklch, var(--agent-a) 45%, transparent);
446
+ animation: agent-fade-in 400ms var(--ease-swift) both;
447
+ }
448
+
449
+ @keyframes agent-sweep {
450
+ from {
451
+ background-position: 120% 0;
452
+ opacity: 1;
453
+ }
454
+ to {
455
+ background-position: -20% 0;
456
+ opacity: 0;
457
+ }
458
+ }
459
+
460
+ .agent-sweep {
461
+ pointer-events: none;
462
+ position: absolute;
463
+ inset: 0;
464
+ border-radius: inherit;
465
+ background: linear-gradient(
466
+ 105deg,
467
+ transparent 35%,
468
+ color-mix(in oklch, var(--agent-b) 22%, transparent) 50%,
469
+ transparent 65%
470
+ );
471
+ background-size: 250% 100%;
472
+ animation: agent-sweep 900ms var(--ease-swift) both;
473
+ }
474
+
475
+ @keyframes agent-shimmer {
476
+ from {
477
+ background-position: 100% 0;
478
+ }
479
+ to {
480
+ background-position: 0% 0;
481
+ }
482
+ }
483
+
484
+ .agent-shimmer {
485
+ background: linear-gradient(
486
+ 90deg,
487
+ var(--muted-foreground) 35%,
488
+ var(--foreground) 50%,
489
+ var(--muted-foreground) 65%
490
+ );
491
+ background-size: 300% 100%;
492
+ background-clip: text;
493
+ color: transparent;
494
+ animation: agent-shimmer 1.8s linear infinite;
495
+ }
496
+
497
+ @keyframes agent-progress {
498
+ from {
499
+ transform: translateX(-100%);
500
+ }
501
+ to {
502
+ transform: translateX(250%);
503
+ }
504
+ }
505
+
506
+ .agent-progress {
507
+ pointer-events: none;
508
+ position: absolute;
509
+ inset-inline: 0;
510
+ top: 0;
511
+ height: 1.5px;
512
+ overflow: hidden;
513
+ }
514
+
515
+ .agent-progress::after {
516
+ content: "";
517
+ position: absolute;
518
+ inset-block: 0;
519
+ width: 40%;
520
+ background: linear-gradient(90deg, transparent, var(--agent-a), var(--agent-b), transparent);
521
+ animation: agent-progress 1.6s cubic-bezier(0.4, 0, 0.6, 1) infinite;
522
+ }
523
+
524
+ @media (prefers-reduced-motion: reduce) {
525
+ .agent-ring {
526
+ animation: agent-fade-in 160ms ease-out both;
527
+ --agent-angle: 120deg;
528
+ }
529
+
530
+ .agent-sweep {
531
+ display: none;
532
+ }
533
+
534
+ .agent-shimmer {
535
+ animation: none;
536
+ color: var(--foreground);
537
+ background: none;
538
+ }
539
+
540
+ .agent-progress::after {
541
+ animation: none;
542
+ width: 100%;
543
+ opacity: 0.6;
544
+ }
545
+ }
546
+
547
+ .sidebar-swap {
548
+ animation: agent-fade-in 160ms var(--ease-swift, ease-out);
549
+ }
550
+
551
+ @media (prefers-reduced-motion: reduce) {
552
+ .sidebar-swap {
553
+ animation: none;
554
+ }
555
+ }
@@ -0,0 +1 @@
1
+ export const UNTITLED_SLIDE_ID_RE = /^untitled(-\d+)?$/;