@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.
- package/dist/{build-6BeQ3cxb.js → build-1Rqivz0d.js} +2 -2
- package/dist/cli/bin.js +5 -5
- package/dist/{config-AxZ5OE1u.js → config-XZJnC_fu.js} +735 -64
- package/dist/{config-CtT8K4VF.d.ts → config-s0YUbmUe.d.ts} +3 -1
- package/dist/{dev-C9eLmUEq.js → dev-0W8gYiSa.js} +2 -2
- package/dist/en-7GU-DHbJ.js +361 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +229 -39
- package/dist/locale/index.d.ts +1 -1
- package/dist/locale/index.js +136 -342
- package/dist/{preview-Cunm-f4i.js → preview-DT9hJvzM.js} +2 -2
- package/dist/sync-j9_QPovT.js +3 -0
- package/dist/{types-CRHIeoNq.d.ts → types-QCpkHkiS.d.ts} +42 -2
- package/dist/vite/index.d.ts +2 -2
- package/dist/vite/index.js +2 -2
- package/package.json +9 -1
- package/skills/create-slide/SKILL.md +1 -1
- package/skills/create-theme/SKILL.md +60 -12
- package/skills/slide-authoring/SKILL.md +21 -2
- package/src/app/app.tsx +13 -1
- package/src/app/components/asset-view.tsx +37 -22
- package/src/app/components/image-placeholder.tsx +123 -1
- package/src/app/components/inspector/inspect-overlay.tsx +49 -3
- package/src/app/components/inspector/inspector-panel.tsx +370 -30
- package/src/app/components/inspector/inspector-provider.tsx +390 -49
- package/src/app/components/player.tsx +25 -5
- package/src/app/components/present/control-bar.tsx +12 -0
- package/src/app/components/sidebar/folder-item.tsx +27 -5
- package/src/app/components/sidebar/mobile-pill.tsx +34 -0
- package/src/app/components/sidebar/sidebar.tsx +20 -0
- package/src/app/components/themes/theme-detail.tsx +300 -0
- package/src/app/components/themes/themes-gallery.tsx +146 -0
- package/src/app/components/thumbnail-rail.tsx +17 -5
- package/src/app/lib/assets.ts +55 -2
- package/src/app/lib/export-pdf.ts +6 -0
- package/src/app/lib/inspector/use-editor.ts +9 -1
- package/src/app/lib/sdk.ts +1 -0
- package/src/app/lib/slides.ts +17 -1
- package/src/app/lib/themes.ts +22 -0
- package/src/app/lib/use-agent-socket.ts +18 -0
- package/src/app/lib/use-slide-module.ts +48 -0
- package/src/app/routes/assets.tsx +9 -0
- package/src/app/routes/home-shell.tsx +194 -0
- package/src/app/routes/home.tsx +89 -207
- package/src/app/routes/presenter.tsx +2 -20
- package/src/app/routes/slide.tsx +217 -54
- package/src/app/routes/themes.tsx +34 -0
- package/src/app/virtual.d.ts +20 -0
- package/src/locale/en.ts +49 -7
- package/src/locale/ja.ts +50 -7
- package/src/locale/types.ts +44 -2
- package/src/locale/zh-cn.ts +49 -8
- package/src/locale/zh-tw.ts +49 -8
- package/dist/sync-B4eLo2H6.js +0 -3
- /package/dist/{design-C13iz9_4.js → design-cpzS8aud.js} +0 -0
- /package/dist/{sync-3oqN1WyK.js → sync-BCJDRIqo.js} +0 -0
package/src/app/routes/slide.tsx
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import config from 'virtual:open-slide/config';
|
|
2
|
-
import {
|
|
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 {
|
|
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
|
|
50
|
-
const [
|
|
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 (
|
|
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
|
-
|
|
228
|
+
setPlayMode('fullscreen');
|
|
234
229
|
}
|
|
235
230
|
};
|
|
236
231
|
window.addEventListener('keydown', onKey);
|
|
237
232
|
return () => window.removeEventListener('keydown', onKey);
|
|
238
|
-
}, [index, goTo,
|
|
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 (
|
|
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={() =>
|
|
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
|
-
<
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
-
<
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
|
|
568
|
-
|
|
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
|
+
}
|
package/src/app/virtual.d.ts
CHANGED
|
@@ -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: '
|
|
55
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
};
|