@open-slide/core 1.2.0 → 1.4.0

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 (56) hide show
  1. package/dist/{build-6BeQ3cxb.js → build-1Rqivz0d.js} +2 -2
  2. package/dist/cli/bin.js +5 -5
  3. package/dist/{config-AxZ5OE1u.js → config-XZJnC_fu.js} +735 -64
  4. package/dist/{config-CtT8K4VF.d.ts → config-s0YUbmUe.d.ts} +3 -1
  5. package/dist/{dev-C9eLmUEq.js → dev-0W8gYiSa.js} +2 -2
  6. package/dist/en-7GU-DHbJ.js +361 -0
  7. package/dist/index.d.ts +4 -3
  8. package/dist/index.js +229 -39
  9. package/dist/locale/index.d.ts +1 -1
  10. package/dist/locale/index.js +136 -342
  11. package/dist/{preview-Cunm-f4i.js → preview-DT9hJvzM.js} +2 -2
  12. package/dist/sync-j9_QPovT.js +3 -0
  13. package/dist/{types-CRHIeoNq.d.ts → types-QCpkHkiS.d.ts} +42 -2
  14. package/dist/vite/index.d.ts +2 -2
  15. package/dist/vite/index.js +2 -2
  16. package/package.json +9 -1
  17. package/skills/create-slide/SKILL.md +1 -1
  18. package/skills/create-theme/SKILL.md +60 -12
  19. package/skills/slide-authoring/SKILL.md +21 -2
  20. package/src/app/app.tsx +13 -1
  21. package/src/app/components/asset-view.tsx +37 -22
  22. package/src/app/components/image-placeholder.tsx +123 -1
  23. package/src/app/components/inspector/inspect-overlay.tsx +49 -3
  24. package/src/app/components/inspector/inspector-panel.tsx +370 -30
  25. package/src/app/components/inspector/inspector-provider.tsx +390 -49
  26. package/src/app/components/player.tsx +25 -5
  27. package/src/app/components/present/control-bar.tsx +12 -0
  28. package/src/app/components/sidebar/folder-item.tsx +27 -5
  29. package/src/app/components/sidebar/mobile-pill.tsx +34 -0
  30. package/src/app/components/sidebar/sidebar.tsx +20 -0
  31. package/src/app/components/themes/theme-detail.tsx +300 -0
  32. package/src/app/components/themes/themes-gallery.tsx +146 -0
  33. package/src/app/components/thumbnail-rail.tsx +17 -5
  34. package/src/app/lib/assets.ts +55 -2
  35. package/src/app/lib/export-pdf.ts +6 -0
  36. package/src/app/lib/inspector/use-editor.ts +9 -1
  37. package/src/app/lib/sdk.ts +1 -0
  38. package/src/app/lib/slides.ts +17 -1
  39. package/src/app/lib/themes.ts +22 -0
  40. package/src/app/lib/use-agent-socket.ts +18 -0
  41. package/src/app/lib/use-slide-module.ts +48 -0
  42. package/src/app/routes/assets.tsx +9 -0
  43. package/src/app/routes/home-shell.tsx +194 -0
  44. package/src/app/routes/home.tsx +89 -207
  45. package/src/app/routes/presenter.tsx +2 -20
  46. package/src/app/routes/slide.tsx +217 -54
  47. package/src/app/routes/themes.tsx +34 -0
  48. package/src/app/virtual.d.ts +20 -0
  49. package/src/locale/en.ts +49 -7
  50. package/src/locale/ja.ts +50 -7
  51. package/src/locale/types.ts +44 -2
  52. package/src/locale/zh-cn.ts +49 -8
  53. package/src/locale/zh-tw.ts +49 -8
  54. package/dist/sync-B4eLo2H6.js +0 -3
  55. /package/dist/{design-C13iz9_4.js → design-cpzS8aud.js} +0 -0
  56. /package/dist/{sync-3oqN1WyK.js → sync-BCJDRIqo.js} +0 -0
@@ -1,5 +1,16 @@
1
1
  import config from 'virtual:open-slide/config';
2
- import { ChevronLeft, Download, FileCode2, FileText, Loader2, Pencil, Play } from 'lucide-react';
2
+ import {
3
+ ChevronDown,
4
+ ChevronLeft,
5
+ Download,
6
+ FileCode2,
7
+ FileText,
8
+ Loader2,
9
+ Maximize,
10
+ MonitorSpeaker,
11
+ Pencil,
12
+ Play,
13
+ } from 'lucide-react';
3
14
  import { type RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
15
  import { Link, useParams, useSearchParams } from 'react-router-dom';
5
16
  import { toast } from 'sonner';
@@ -26,51 +37,35 @@ import {
26
37
  import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
27
38
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
28
39
  import { useFolders } from '@/lib/folders';
40
+ import { useAgentSocketConnected } from '@/lib/use-agent-socket';
29
41
  import { format, useLocale } from '@/lib/use-locale';
30
42
  import { useWheelPageNavigation } from '@/lib/use-wheel-page-navigation';
31
43
  import { cn } from '@/lib/utils';
32
44
  import { ClickNavZones } from '../components/click-nav-zones';
33
45
  import { NotesDrawer } from '../components/notes-drawer';
34
46
  import { PdfProgressToast } from '../components/pdf-progress-toast';
35
- import { Player } from '../components/player';
47
+ import { openPresenterWindow, Player } from '../components/player';
36
48
  import { SlideCanvas } from '../components/slide-canvas';
37
49
  import { type ThumbnailActions, ThumbnailRail } from '../components/thumbnail-rail';
38
50
  import { exportSlideAsHtml } from '../lib/export-html';
39
- import { exportSlideAsPdf } from '../lib/export-pdf';
51
+ import { exportSlideAsPdf, isSafari } from '../lib/export-pdf';
40
52
  import { remapNotesSessionCacheAfterReorder } from '../lib/inspector/use-notes';
41
53
  import type { SlideModule } from '../lib/sdk';
42
- import { loadSlide } from '../lib/slides';
54
+ import { useSlideModule } from '../lib/use-slide-module';
43
55
 
44
56
  const { showSlideUi, showSlideBrowser, allowHtmlDownload } = config.build;
45
57
 
46
58
  export function Slide() {
47
59
  const { slideId = '' } = useParams();
48
60
  const [searchParams, setSearchParams] = useSearchParams();
49
- const [slide, setSlide] = useState<SlideModule | null>(null);
50
- const [error, setError] = useState<string | null>(null);
51
- const [playing, setPlaying] = useState(false);
61
+ const { slide, error } = useSlideModule(slideId);
62
+ const [playMode, setPlayMode] = useState<'window' | 'fullscreen' | null>(null);
52
63
  const [exporting, setExporting] = useState(false);
53
64
  const [designOpen, setDesignOpen] = useState(false);
54
65
  const { renameSlide } = useFolders();
55
66
  const slideViewportRef = useRef<HTMLElement>(null);
56
67
  const t = useLocale();
57
68
 
58
- useEffect(() => {
59
- let cancelled = false;
60
- setSlide(null);
61
- setError(null);
62
- loadSlide(slideId)
63
- .then((mod) => {
64
- if (!cancelled) setSlide(mod);
65
- })
66
- .catch((e) => {
67
- if (!cancelled) setError(String(e?.message ?? e));
68
- });
69
- return () => {
70
- cancelled = true;
71
- };
72
- }, [slideId]);
73
-
74
69
  const modulePages = useMemo(() => slide?.default ?? [], [slide]);
75
70
  const [pages, setPages] = useState<typeof modulePages>(modulePages);
76
71
  useEffect(() => {
@@ -220,7 +215,7 @@ export function Slide() {
220
215
  );
221
216
 
222
217
  useEffect(() => {
223
- if (playing) return;
218
+ if (playMode) return;
224
219
  const onKey = (e: KeyboardEvent) => {
225
220
  if (e.target instanceof HTMLElement && e.target.matches('input, textarea')) return;
226
221
  if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === 'PageDown') {
@@ -230,12 +225,12 @@ export function Slide() {
230
225
  e.preventDefault();
231
226
  goTo(index - 1);
232
227
  } else if (e.key === 'f' || e.key === 'F') {
233
- setPlaying(true);
228
+ setPlayMode('fullscreen');
234
229
  }
235
230
  };
236
231
  window.addEventListener('keydown', onKey);
237
232
  return () => window.removeEventListener('keydown', onKey);
238
- }, [index, goTo, playing]);
233
+ }, [index, goTo, playMode]);
239
234
 
240
235
  if (error) {
241
236
  return (
@@ -314,16 +309,17 @@ export function Slide() {
314
309
  );
315
310
  }
316
311
 
317
- if (playing) {
312
+ if (playMode) {
318
313
  return (
319
314
  <Player
320
315
  pages={pages}
321
316
  design={slide.design}
322
317
  index={index}
323
318
  onIndexChange={goTo}
324
- onExit={() => setPlaying(false)}
319
+ onExit={() => setPlayMode(null)}
325
320
  controls
326
321
  slideId={slideId}
322
+ fullscreen={playMode === 'fullscreen'}
327
323
  />
328
324
  );
329
325
  }
@@ -416,6 +412,10 @@ export function Slide() {
416
412
  disabled={exporting}
417
413
  onSelect={async () => {
418
414
  if (!slide || exporting) return;
415
+ if (isSafari()) {
416
+ toast.error(t.slide.pdfExportSafariUnsupported, { duration: 5000 });
417
+ return;
418
+ }
419
419
  setExporting(true);
420
420
  const toastId = `pdf-export-${slideId}`;
421
421
  toast.custom(
@@ -459,18 +459,52 @@ export function Slide() {
459
459
  {view === 'slides' && <InspectToggleButton />}
460
460
  <span aria-hidden className="mx-0.5 hidden h-5 w-px bg-hairline md:block" />
461
461
  {view === 'slides' && (
462
- <Button
463
- size="sm"
464
- variant="brand"
465
- onClick={() => setPlaying(true)}
466
- className="px-2.5 md:px-3"
467
- >
468
- <Play className="size-3.5 fill-current" />
469
- <span className="hidden md:inline">{t.slide.present}</span>
470
- <kbd className="ml-1 hidden rounded-[3px] bg-brand-foreground/15 px-1 font-mono text-[9.5px] tracking-[0.04em] md:inline">
471
- F
472
- </kbd>
473
- </Button>
462
+ <div className="inline-flex items-stretch">
463
+ <Button
464
+ size="sm"
465
+ variant="brand"
466
+ onClick={() => setPlayMode('fullscreen')}
467
+ className="rounded-r-none px-2.5 md:px-3"
468
+ >
469
+ <Play className="size-3.5 fill-current" />
470
+ <span className="hidden md:inline">{t.slide.present}</span>
471
+ <kbd className="ml-1 hidden rounded-[3px] bg-brand-foreground/15 px-1 font-mono text-[9.5px] tracking-[0.04em] md:inline">
472
+ F
473
+ </kbd>
474
+ </Button>
475
+ <DropdownMenu>
476
+ <DropdownMenuTrigger
477
+ type="button"
478
+ aria-label={t.slide.presentMenuAria}
479
+ title={t.slide.presentMenuAria}
480
+ className={cn(
481
+ buttonVariants({ variant: 'brand', size: 'sm' }),
482
+ 'rounded-l-none px-1.5 shadow-[inset_1px_0_0_oklch(0_0_0/0.12),inset_0_1px_0_oklch(1_0_0/0.18),0_1px_0_oklch(0_0_0/0.16)]',
483
+ )}
484
+ >
485
+ <ChevronDown className="size-3.5" />
486
+ </DropdownMenuTrigger>
487
+ <DropdownMenuContent align="end" className="min-w-[200px]">
488
+ <DropdownMenuItem onSelect={() => setPlayMode('window')}>
489
+ <Play />
490
+ {t.slide.presentInWindow}
491
+ </DropdownMenuItem>
492
+ <DropdownMenuItem onSelect={() => setPlayMode('fullscreen')}>
493
+ <Maximize />
494
+ {t.slide.presentFullscreen}
495
+ </DropdownMenuItem>
496
+ <DropdownMenuItem
497
+ onSelect={() => {
498
+ if (slideId) openPresenterWindow(slideId);
499
+ setPlayMode('window');
500
+ }}
501
+ >
502
+ <MonitorSpeaker />
503
+ {t.slide.presentPresenter}
504
+ </DropdownMenuItem>
505
+ </DropdownMenuContent>
506
+ </DropdownMenu>
507
+ </div>
474
508
  )}
475
509
  </div>
476
510
  </header>
@@ -483,19 +517,18 @@ export function Slide() {
483
517
  <DesignProvider slideId={slideId}>
484
518
  <div className="flex min-h-0 flex-1 flex-col">
485
519
  <div className="flex min-h-0 flex-1 flex-col md:flex-row">
486
- <div className="hidden w-[16.5rem] shrink-0 md:block">
487
- <ThumbnailRail
488
- pages={pages}
489
- design={slide.design}
490
- current={index}
491
- onSelect={goTo}
492
- onReorder={import.meta.env.DEV ? reorderPage : undefined}
493
- actions={thumbnailActions}
494
- />
495
- </div>
520
+ <ResizableRail
521
+ pages={pages}
522
+ design={slide.design}
523
+ current={index}
524
+ onSelect={goTo}
525
+ onReorder={import.meta.env.DEV ? reorderPage : undefined}
526
+ actions={thumbnailActions}
527
+ />
496
528
  <main
497
529
  ref={slideViewportRef}
498
530
  data-inspector-root
531
+ data-slide-id={slideId}
499
532
  className="paper relative min-h-0 min-w-0 flex-1 bg-canvas p-2 md:p-10"
500
533
  >
501
534
  <SlideWheelNavigation
@@ -553,8 +586,132 @@ export function Slide() {
553
586
  );
554
587
  }
555
588
 
589
+ const RAIL_WIDTH_STORAGE_KEY = 'open-slide:thumbnail-rail-width';
590
+ const DEFAULT_RAIL_WIDTH = 264;
591
+ const MIN_RAIL_WIDTH = 200;
592
+ const MAX_RAIL_WIDTH = 480;
593
+
594
+ function readStoredRailWidth(): number {
595
+ if (typeof window === 'undefined') return DEFAULT_RAIL_WIDTH;
596
+ const raw = window.localStorage.getItem(RAIL_WIDTH_STORAGE_KEY);
597
+ const parsed = raw == null ? Number.NaN : Number(raw);
598
+ if (!Number.isFinite(parsed)) return DEFAULT_RAIL_WIDTH;
599
+ return Math.min(MAX_RAIL_WIDTH, Math.max(MIN_RAIL_WIDTH, parsed));
600
+ }
601
+
602
+ function ResizableRail(props: {
603
+ pages: SlideModule['default'];
604
+ design?: SlideModule['design'];
605
+ current: number;
606
+ onSelect: (i: number) => void;
607
+ onReorder?: (from: number, to: number) => void;
608
+ actions?: ThumbnailActions;
609
+ }) {
610
+ const t = useLocale();
611
+ const [width, setWidth] = useState<number>(readStoredRailWidth);
612
+ const [resizing, setResizing] = useState(false);
613
+ const dragRef = useRef<{ startX: number; startWidth: number } | null>(null);
614
+
615
+ useEffect(() => {
616
+ if (typeof window === 'undefined') return;
617
+ window.localStorage.setItem(RAIL_WIDTH_STORAGE_KEY, String(width));
618
+ }, [width]);
619
+
620
+ useEffect(() => {
621
+ if (!resizing) return;
622
+ const prev = {
623
+ cursor: document.body.style.cursor,
624
+ userSelect: document.body.style.userSelect,
625
+ };
626
+ document.body.style.cursor = 'col-resize';
627
+ document.body.style.userSelect = 'none';
628
+ return () => {
629
+ document.body.style.cursor = prev.cursor;
630
+ document.body.style.userSelect = prev.userSelect;
631
+ };
632
+ }, [resizing]);
633
+
634
+ const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
635
+ e.preventDefault();
636
+ e.currentTarget.setPointerCapture(e.pointerId);
637
+ dragRef.current = { startX: e.clientX, startWidth: width };
638
+ setResizing(true);
639
+ };
640
+
641
+ const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
642
+ if (!dragRef.current) return;
643
+ const delta = e.clientX - dragRef.current.startX;
644
+ const next = Math.min(
645
+ MAX_RAIL_WIDTH,
646
+ Math.max(MIN_RAIL_WIDTH, dragRef.current.startWidth + delta),
647
+ );
648
+ setWidth(next);
649
+ };
650
+
651
+ const onPointerUp = (e: React.PointerEvent<HTMLDivElement>) => {
652
+ if (e.currentTarget.hasPointerCapture(e.pointerId)) {
653
+ e.currentTarget.releasePointerCapture(e.pointerId);
654
+ }
655
+ dragRef.current = null;
656
+ setResizing(false);
657
+ };
658
+
659
+ const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
660
+ const step = e.shiftKey ? 32 : 8;
661
+ if (e.key === 'ArrowLeft') {
662
+ e.preventDefault();
663
+ e.stopPropagation();
664
+ setWidth((w) => Math.max(MIN_RAIL_WIDTH, w - step));
665
+ } else if (e.key === 'ArrowRight') {
666
+ e.preventDefault();
667
+ e.stopPropagation();
668
+ setWidth((w) => Math.min(MAX_RAIL_WIDTH, w + step));
669
+ } else if (e.key === 'Home') {
670
+ e.preventDefault();
671
+ e.stopPropagation();
672
+ setWidth(DEFAULT_RAIL_WIDTH);
673
+ }
674
+ };
675
+
676
+ return (
677
+ <div className="relative hidden shrink-0 md:block" style={{ width }}>
678
+ <ThumbnailRail width={width} {...props} />
679
+ {/* biome-ignore lint/a11y/useSemanticElements: focusable resize handle (splitter pattern), not a static <hr> */}
680
+ <div
681
+ role="separator"
682
+ aria-orientation="vertical"
683
+ aria-label={t.thumbnailRail.resizeRail}
684
+ aria-valuenow={width}
685
+ aria-valuemin={MIN_RAIL_WIDTH}
686
+ aria-valuemax={MAX_RAIL_WIDTH}
687
+ tabIndex={0}
688
+ onPointerDown={onPointerDown}
689
+ onPointerMove={onPointerMove}
690
+ onPointerUp={onPointerUp}
691
+ onPointerCancel={onPointerUp}
692
+ onKeyDown={onKeyDown}
693
+ onDoubleClick={() => setWidth(DEFAULT_RAIL_WIDTH)}
694
+ className={cn(
695
+ 'group/resize absolute inset-y-0 right-0 z-20 w-1.5 translate-x-1/2 cursor-col-resize touch-none outline-none',
696
+ 'focus-visible:bg-brand/20',
697
+ )}
698
+ >
699
+ <span
700
+ aria-hidden
701
+ className={cn(
702
+ 'pointer-events-none absolute inset-y-0 left-1/2 w-px -translate-x-1/2 bg-brand opacity-0 transition-opacity',
703
+ 'group-hover/resize:opacity-100 group-focus-visible/resize:opacity-100',
704
+ resizing && 'opacity-100',
705
+ )}
706
+ />
707
+ </div>
708
+ </div>
709
+ );
710
+ }
711
+
556
712
  function AgentConnectedBadge() {
557
713
  const t = useLocale();
714
+ const connected = useAgentSocketConnected();
558
715
  return (
559
716
  <TooltipProvider delayDuration={200}>
560
717
  <Tooltip>
@@ -564,14 +721,20 @@ function AgentConnectedBadge() {
564
721
  className="ml-1 flex shrink-0 cursor-help items-center gap-1.5 rounded-[3px] border border-hairline bg-card px-1.5 py-0.5 text-[10.5px] text-foreground/85 outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
565
722
  >
566
723
  <span aria-hidden className="relative flex size-1.5 items-center justify-center">
567
- <span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-500 opacity-60" />
568
- <span className="relative inline-flex size-1.5 rounded-full bg-emerald-500" />
724
+ {connected ? (
725
+ <>
726
+ <span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-500 opacity-60" />
727
+ <span className="relative inline-flex size-1.5 rounded-full bg-emerald-500" />
728
+ </>
729
+ ) : (
730
+ <span className="relative inline-flex size-1.5 rounded-full bg-rose-500" />
731
+ )}
569
732
  </span>
570
- {t.slide.agentConnected}
733
+ {connected ? t.slide.agentConnected : t.slide.agentDisconnected}
571
734
  </button>
572
735
  </TooltipTrigger>
573
736
  <TooltipContent side="bottom" align="start" className="max-w-[280px] leading-relaxed">
574
- {t.slide.agentConnectedTooltip}
737
+ {connected ? t.slide.agentConnectedTooltip : t.slide.agentDisconnectedTooltip}
575
738
  </TooltipContent>
576
739
  </Tooltip>
577
740
  </TooltipProvider>
@@ -0,0 +1,34 @@
1
+ import { useNavigate, useParams } from 'react-router-dom';
2
+ import { useLocale } from '@/lib/use-locale';
3
+ import { FolderIconChip } from '../components/sidebar/folder-item';
4
+ import { ThemeDetail } from '../components/themes/theme-detail';
5
+ import { ThemesGallery } from '../components/themes/themes-gallery';
6
+ import { themes as themeRegistry } from '../lib/themes';
7
+
8
+ export function ThemesGalleryPage() {
9
+ const navigate = useNavigate();
10
+ const t = useLocale();
11
+ return (
12
+ <>
13
+ <header className="mb-8 md:mb-12">
14
+ <div className="flex flex-wrap items-center gap-3">
15
+ <FolderIconChip icon={{ type: 'emoji', value: '🎨' }} className="size-7 text-2xl" />
16
+ <h1 className="font-heading text-[32px] font-semibold leading-[1.05] tracking-[-0.025em] md:text-[44px]">
17
+ {t.themes.title}
18
+ </h1>
19
+ <span className="folio ml-1 self-end pb-2">
20
+ {themeRegistry.length.toString().padStart(2, '0')}
21
+ </span>
22
+ </div>
23
+ </header>
24
+ <ThemesGallery onOpen={(id) => navigate(`/themes/${encodeURIComponent(id)}`)} />
25
+ </>
26
+ );
27
+ }
28
+
29
+ export function ThemeDetailPage() {
30
+ const { themeId } = useParams<{ themeId: string }>();
31
+ const navigate = useNavigate();
32
+ if (!themeId) return null;
33
+ return <ThemeDetail themeId={themeId} onBack={() => navigate('/themes')} />;
34
+ }
@@ -1,6 +1,7 @@
1
1
  declare module 'virtual:open-slide/slides' {
2
2
  import type { SlideModule } from './lib/sdk';
3
3
  export const slideIds: string[];
4
+ export const slideThemes: Record<string, string>;
4
5
  export function loadSlide(id: string): Promise<SlideModule>;
5
6
  }
6
7
 
@@ -26,3 +27,22 @@ declare module 'virtual:open-slide/folders' {
26
27
  const manifest: FoldersManifest;
27
28
  export default manifest;
28
29
  }
30
+
31
+ declare module 'virtual:open-slide/themes' {
32
+ import type { DesignSystem } from './lib/design';
33
+ import type { Page } from './lib/sdk';
34
+
35
+ export type ThemeMeta = {
36
+ id: string;
37
+ name: string;
38
+ description: string;
39
+ body: string;
40
+ hasDemo: boolean;
41
+ };
42
+
43
+ export const themes: ThemeMeta[];
44
+ export function loadThemeDemo(id: string): Promise<{
45
+ default: Page[];
46
+ design?: DesignSystem;
47
+ }>;
48
+ }
package/src/locale/en.ts CHANGED
@@ -38,6 +38,8 @@ export const en: Locale = {
38
38
  home: {
39
39
  appTitle: 'open-slide',
40
40
  draft: 'Draft',
41
+ themes: 'Themes',
42
+ assets: 'Assets',
41
43
  folders: 'Folders',
42
44
  newFolder: 'New folder',
43
45
  folderName: 'Folder name',
@@ -51,9 +53,8 @@ export const en: Locale = {
51
53
  nothingMatchesPrefix: 'Nothing matches ',
52
54
  nothingMatchesSuffix: ' in this folder.',
53
55
  noSlidesYet: 'No slides yet',
54
- createSlideHintPrefix: 'Create ',
55
- createSlideHintMid: ' that ',
56
- createSlideHintSuffix: '.',
56
+ createSlideHintPrefix: 'Run ',
57
+ createSlideHintSuffix: ' in your agent to scaffold one.',
57
58
  folderEmptyTitle: '{name} is empty',
58
59
  folderEmptyHint: 'Drag a slide from Draft into this folder in the sidebar.',
59
60
  slideActions: 'Slide actions',
@@ -85,12 +86,21 @@ export const en: Locale = {
85
86
  backToHome: 'Back to home',
86
87
  agentConnected: 'Agent connected',
87
88
  agentConnectedTooltip:
88
- 'The dev server is publishing your current slide and inspector selection to your agent. Ask "this slide" or "this element" in chat and it will resolve. Disappears in production builds.',
89
+ 'The current slide and inspector selection are synced to your agent in real time.',
90
+ agentDisconnected: 'Agent disconnected',
91
+ agentDisconnectedTooltip:
92
+ 'Lost connection to the dev server, so your agent can no longer see the current slide or inspector selection. Restart the dev server to restore the connection.',
89
93
  download: 'Download',
90
94
  exportAsHtml: 'Export as HTML',
91
95
  exportAsPdf: 'Export as PDF',
92
96
  pdfExportFailed: 'PDF export failed',
97
+ pdfExportSafariUnsupported:
98
+ 'Export as PDF is not supported on Safari. Please try a Chromium-based browser instead.',
93
99
  present: 'Present',
100
+ presentMenuAria: 'Present options',
101
+ presentInWindow: 'Play',
102
+ presentFullscreen: 'Fullscreen',
103
+ presentPresenter: 'Presenter mode',
94
104
  slidesTab: 'Slides',
95
105
  assetsTab: 'Assets',
96
106
  renameSlide: 'Rename slide',
@@ -134,6 +144,8 @@ export const en: Locale = {
134
144
  whiteoutAria: 'White screen (W)',
135
145
  laserAria: 'Laser pointer (L)',
136
146
  presenterAria: 'Presenter view (P)',
147
+ enterFullscreenAria: 'Enter fullscreen',
148
+ exitFullscreenAria: 'Exit fullscreen',
137
149
  helpAria: 'Keyboard shortcuts (?)',
138
150
  exitAria: 'Exit (Esc)',
139
151
  elapsedTime: 'Elapsed time',
@@ -160,8 +172,10 @@ export const en: Locale = {
160
172
  inspect: 'Inspect',
161
173
  deselect: 'Deselect',
162
174
  agentWatching: 'Agent is watching',
163
- agentWatchingTooltip:
164
- 'Your agent already sees the selected element via the dev server — just ask it in chat. Leave comments here only when you want to queue a few before asking.',
175
+ agentWatchingTooltip: 'The selected element is synced to your agent in real time.',
176
+ agentNotWatching: 'Agent not watching',
177
+ agentNotWatchingTooltip:
178
+ 'Lost connection to the dev server, so your agent can no longer see the selected element. Restart the dev server to restore the connection.',
165
179
  contentSection: 'Content',
166
180
  typographySection: 'Typography',
167
181
  colorSection: 'Color',
@@ -245,6 +259,8 @@ export const en: Locale = {
245
259
  devOnlyMessage: 'Asset management is only available in dev mode.',
246
260
  sectionAria: 'Slide assets',
247
261
  eyebrow: 'Assets',
262
+ scopeSlide: 'This slide',
263
+ scopeGlobal: 'Global',
248
264
  fileCount: { one: '{count} file', other: '{count} files' },
249
265
  searchLogos: 'Search logos',
250
266
  upload: 'Upload',
@@ -260,7 +276,7 @@ export const en: Locale = {
260
276
  renameMenuItem: 'Rename',
261
277
  deleteMenuItem: 'Delete',
262
278
  conflictTitle: 'File already exists',
263
- conflictDescription: "{name} is already in this slide's assets folder.",
279
+ conflictDescription: '{name} is already in the assets folder.',
264
280
  conflictReplace: 'Replace',
265
281
  conflictRenameCopy: 'Rename copy',
266
282
  deleteAssetTitle: 'Delete asset',
@@ -301,6 +317,7 @@ export const en: Locale = {
301
317
  toastDeleted: 'Deleted page {n}',
302
318
  toastDuplicateFailed: 'Could not duplicate page',
303
319
  toastDeleteFailed: 'Could not delete page',
320
+ resizeRail: 'Resize thumbnail rail',
304
321
  },
305
322
 
306
323
  pdfToast: {
@@ -323,6 +340,12 @@ export const en: Locale = {
323
340
  nextAria: 'Next page',
324
341
  },
325
342
 
343
+ imagePlaceholder: {
344
+ dropOverlay: 'Drop image to use here',
345
+ uploading: 'Uploading…',
346
+ uploadFailed: "Couldn't upload image",
347
+ },
348
+
326
349
  notesDrawer: {
327
350
  toggle: 'Notes',
328
351
  pageLabel: 'page {n}/{total}',
@@ -331,4 +354,23 @@ export const en: Locale = {
331
354
  statusSaved: 'Saved',
332
355
  statusError: 'Save failed: {msg}',
333
356
  },
357
+
358
+ themes: {
359
+ title: 'Themes',
360
+ noThemesTitle: 'No themes yet',
361
+ noThemesHintPrefix: 'Run ',
362
+ noThemesHintSuffix: ' to author one — a markdown file under themes/ plus a sibling demo slide.',
363
+ noDemoYet: 'No demo yet',
364
+ noDemoHintPrefix: 'Re-run ',
365
+ noDemoHintSuffix: ' for this theme to generate a preview slide.',
366
+ backToGallery: 'Back to themes',
367
+ pageOf: 'page {n}/{total}',
368
+ nextPageAria: 'Next page',
369
+ prevPageAria: 'Previous page',
370
+ openThemeAria: 'Open theme {name}',
371
+ usedBy: 'Slides using this theme',
372
+ usedByEmpty: 'No slides use this theme yet.',
373
+ expandPromptAria: 'Expand prompt',
374
+ collapsePromptAria: 'Collapse prompt',
375
+ },
334
376
  };