@open-slide/core 1.1.0 → 1.3.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-DSqSio-T.js → build-_276DMmJ.js} +2 -2
- package/dist/cli/bin.js +5 -5
- package/dist/{config-KdiYeWtK.js → config-BAwKWNtW.js} +888 -229
- package/dist/{config-C7vMYzFD.d.ts → config-D9cZ1A0X.d.ts} +2 -1
- package/dist/{dev-B_GVbr11.js → dev-BoqeVXVq.js} +2 -2
- package/dist/en-CDKzoZvf.js +351 -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 +166 -326
- package/dist/{preview-D_mxhj7w.js → preview-BLPxspc9.js} +2 -2
- package/dist/sync-j9_QPovT.js +3 -0
- package/dist/{types-DYgVpIGo.d.ts → types-JYG1cmwC.d.ts} +59 -5
- 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/current-slide/SKILL.md +110 -0
- package/skills/slide-authoring/SKILL.md +59 -1
- package/src/app/app.tsx +11 -1
- package/src/app/components/asset-view.tsx +1 -13
- package/src/app/components/image-placeholder.tsx +123 -1
- package/src/app/components/inspector/image-crop-dialog.tsx +64 -20
- package/src/app/components/inspector/inspector-panel.tsx +163 -19
- package/src/app/components/inspector/inspector-provider.tsx +60 -7
- package/src/app/components/notes-drawer.tsx +117 -0
- package/src/app/components/player.tsx +11 -7
- package/src/app/components/present/overview-grid.tsx +2 -2
- package/src/app/components/sidebar/folder-item.tsx +16 -5
- package/src/app/components/sidebar/mobile-pill.tsx +34 -0
- package/src/app/components/sidebar/sidebar.tsx +10 -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 +136 -29
- package/src/app/components/ui/context-menu.tsx +237 -0
- package/src/app/lib/assets.ts +55 -2
- package/src/app/lib/inspector/use-notes.ts +134 -0
- package/src/app/lib/sdk.ts +1 -0
- package/src/app/lib/slides.ts +10 -1
- package/src/app/lib/themes.ts +22 -0
- package/src/app/lib/use-agent-socket.ts +18 -0
- package/src/app/routes/home-shell.tsx +173 -0
- package/src/app/routes/home.tsx +108 -204
- package/src/app/routes/slide.tsx +333 -68
- package/src/app/routes/themes.tsx +34 -0
- package/src/app/virtual.d.ts +20 -0
- package/src/locale/en.ts +61 -7
- package/src/locale/ja.ts +62 -7
- package/src/locale/types.ts +62 -5
- package/src/locale/zh-cn.ts +61 -7
- package/src/locale/zh-tw.ts +61 -7
- 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
|
@@ -24,17 +24,21 @@ import {
|
|
|
24
24
|
DropdownMenuTrigger,
|
|
25
25
|
} from '@/components/ui/dropdown-menu';
|
|
26
26
|
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
27
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
27
28
|
import { useFolders } from '@/lib/folders';
|
|
28
|
-
import {
|
|
29
|
+
import { useAgentSocketConnected } from '@/lib/use-agent-socket';
|
|
30
|
+
import { format, useLocale } from '@/lib/use-locale';
|
|
29
31
|
import { useWheelPageNavigation } from '@/lib/use-wheel-page-navigation';
|
|
30
32
|
import { cn } from '@/lib/utils';
|
|
31
33
|
import { ClickNavZones } from '../components/click-nav-zones';
|
|
34
|
+
import { NotesDrawer } from '../components/notes-drawer';
|
|
32
35
|
import { PdfProgressToast } from '../components/pdf-progress-toast';
|
|
33
36
|
import { Player } from '../components/player';
|
|
34
37
|
import { SlideCanvas } from '../components/slide-canvas';
|
|
35
|
-
import { ThumbnailRail } from '../components/thumbnail-rail';
|
|
38
|
+
import { type ThumbnailActions, ThumbnailRail } from '../components/thumbnail-rail';
|
|
36
39
|
import { exportSlideAsHtml } from '../lib/export-html';
|
|
37
40
|
import { exportSlideAsPdf } from '../lib/export-pdf';
|
|
41
|
+
import { remapNotesSessionCacheAfterReorder } from '../lib/inspector/use-notes';
|
|
38
42
|
import type { SlideModule } from '../lib/sdk';
|
|
39
43
|
import { loadSlide } from '../lib/slides';
|
|
40
44
|
|
|
@@ -78,6 +82,33 @@ export function Slide() {
|
|
|
78
82
|
const index = Number.isFinite(rawIndex) ? Math.max(0, Math.min(pageCount - 1, rawIndex)) : 0;
|
|
79
83
|
const view = searchParams.get('view') === 'assets' ? 'assets' : 'slides';
|
|
80
84
|
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (!import.meta.hot) return;
|
|
87
|
+
if (!slideId || !slide || pageCount === 0) return;
|
|
88
|
+
import.meta.hot.send('open-slide:current', {
|
|
89
|
+
slideId,
|
|
90
|
+
pageIndex: index,
|
|
91
|
+
totalPages: pageCount,
|
|
92
|
+
slideTitle: slide.meta?.title ?? slideId,
|
|
93
|
+
view,
|
|
94
|
+
});
|
|
95
|
+
}, [slideId, index, pageCount, slide, view]);
|
|
96
|
+
|
|
97
|
+
const goTo = useCallback(
|
|
98
|
+
(i: number) => {
|
|
99
|
+
const clamped = Math.max(0, Math.min(pageCount - 1, i));
|
|
100
|
+
setSearchParams(
|
|
101
|
+
(prev) => {
|
|
102
|
+
const next = new URLSearchParams(prev);
|
|
103
|
+
next.set('p', String(clamped + 1));
|
|
104
|
+
return next;
|
|
105
|
+
},
|
|
106
|
+
{ replace: true },
|
|
107
|
+
);
|
|
108
|
+
},
|
|
109
|
+
[pageCount, setSearchParams],
|
|
110
|
+
);
|
|
111
|
+
|
|
81
112
|
const reorderPage = useCallback(
|
|
82
113
|
async (from: number, to: number) => {
|
|
83
114
|
if (from === to) return;
|
|
@@ -91,21 +122,14 @@ export function Slide() {
|
|
|
91
122
|
const [movedIdx] = order.splice(from, 1);
|
|
92
123
|
order.splice(to, 0, movedIdx);
|
|
93
124
|
|
|
125
|
+
remapNotesSessionCacheAfterReorder(slideId, order);
|
|
126
|
+
|
|
94
127
|
// Keep the user looking at the same page they were on before the drag.
|
|
95
128
|
let nextIndex = index;
|
|
96
129
|
if (index === from) nextIndex = to;
|
|
97
130
|
else if (from < index && to >= index) nextIndex = index - 1;
|
|
98
131
|
else if (from > index && to <= index) nextIndex = index + 1;
|
|
99
|
-
if (nextIndex !== index)
|
|
100
|
-
setSearchParams(
|
|
101
|
-
(prev) => {
|
|
102
|
-
const params = new URLSearchParams(prev);
|
|
103
|
-
params.set('p', String(nextIndex + 1));
|
|
104
|
-
return params;
|
|
105
|
-
},
|
|
106
|
-
{ replace: true },
|
|
107
|
-
);
|
|
108
|
-
}
|
|
132
|
+
if (nextIndex !== index) goTo(nextIndex);
|
|
109
133
|
|
|
110
134
|
try {
|
|
111
135
|
const res = await fetch(`/__slides/${encodeURIComponent(slideId)}/reorder`, {
|
|
@@ -119,25 +143,81 @@ export function Slide() {
|
|
|
119
143
|
}
|
|
120
144
|
} catch (err) {
|
|
121
145
|
setPages(before);
|
|
146
|
+
const inverse = order.map((_, i) => order.indexOf(i));
|
|
147
|
+
remapNotesSessionCacheAfterReorder(slideId, inverse);
|
|
122
148
|
toast.error(`Reorder failed: ${String((err as Error).message ?? err)}`);
|
|
123
149
|
}
|
|
124
150
|
},
|
|
125
|
-
[pages, index, slideId,
|
|
151
|
+
[pages, index, slideId, goTo],
|
|
126
152
|
);
|
|
127
153
|
|
|
128
|
-
const
|
|
129
|
-
(i: number) => {
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
154
|
+
const duplicatePage = useCallback(
|
|
155
|
+
async (i: number) => {
|
|
156
|
+
const before = pages;
|
|
157
|
+
if (i < 0 || i >= before.length) return;
|
|
158
|
+
const nextPages = [...before];
|
|
159
|
+
nextPages.splice(i + 1, 0, before[i]);
|
|
160
|
+
setPages(nextPages);
|
|
161
|
+
if (index > i) goTo(index + 1);
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const res = await fetch(`/__slides/${encodeURIComponent(slideId)}/pages/${i}/duplicate`, {
|
|
165
|
+
method: 'POST',
|
|
166
|
+
});
|
|
167
|
+
if (!res.ok) {
|
|
168
|
+
const detail = await res.json().catch(() => ({ error: res.statusText }));
|
|
169
|
+
throw new Error(detail.error ?? `HTTP ${res.status}`);
|
|
170
|
+
}
|
|
171
|
+
toast.success(format(t.thumbnailRail.toastDuplicated, { n: i + 1 }));
|
|
172
|
+
} catch (err) {
|
|
173
|
+
setPages(before);
|
|
174
|
+
toast.error(
|
|
175
|
+
`${t.thumbnailRail.toastDuplicateFailed}: ${String((err as Error).message ?? err)}`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
139
178
|
},
|
|
140
|
-
[
|
|
179
|
+
[pages, index, slideId, goTo, t.thumbnailRail],
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const deletePage = useCallback(
|
|
183
|
+
async (i: number) => {
|
|
184
|
+
const before = pages;
|
|
185
|
+
if (i < 0 || i >= before.length || before.length <= 1) return;
|
|
186
|
+
const nextPages = before.slice(0, i).concat(before.slice(i + 1));
|
|
187
|
+
setPages(nextPages);
|
|
188
|
+
if (index >= i && index > 0) {
|
|
189
|
+
const target = index === i ? Math.min(index, nextPages.length - 1) : index - 1;
|
|
190
|
+
goTo(target);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const res = await fetch(`/__slides/${encodeURIComponent(slideId)}/pages/${i}`, {
|
|
195
|
+
method: 'DELETE',
|
|
196
|
+
});
|
|
197
|
+
if (!res.ok) {
|
|
198
|
+
const detail = await res.json().catch(() => ({ error: res.statusText }));
|
|
199
|
+
throw new Error(detail.error ?? `HTTP ${res.status}`);
|
|
200
|
+
}
|
|
201
|
+
toast.success(format(t.thumbnailRail.toastDeleted, { n: i + 1 }));
|
|
202
|
+
} catch (err) {
|
|
203
|
+
setPages(before);
|
|
204
|
+
toast.error(
|
|
205
|
+
`${t.thumbnailRail.toastDeleteFailed}: ${String((err as Error).message ?? err)}`,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
[pages, index, slideId, goTo, t.thumbnailRail],
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const thumbnailActions = useMemo<ThumbnailActions | undefined>(
|
|
213
|
+
() =>
|
|
214
|
+
import.meta.env.DEV
|
|
215
|
+
? {
|
|
216
|
+
onDuplicate: duplicatePage,
|
|
217
|
+
onDelete: deletePage,
|
|
218
|
+
}
|
|
219
|
+
: undefined,
|
|
220
|
+
[duplicatePage, deletePage],
|
|
141
221
|
);
|
|
142
222
|
|
|
143
223
|
useEffect(() => {
|
|
@@ -255,6 +335,7 @@ export function Slide() {
|
|
|
255
335
|
return (
|
|
256
336
|
<HistoryProvider>
|
|
257
337
|
<InspectorProvider slideId={slideId}>
|
|
338
|
+
<SelectionReporter />
|
|
258
339
|
<div className="flex h-dvh flex-col overflow-hidden bg-background text-foreground">
|
|
259
340
|
{/* Editorial toolbar — three zones, hairline separators, mono-folio center */}
|
|
260
341
|
<header className="relative flex h-12 shrink-0 items-center justify-between border-b border-hairline bg-sidebar/85 px-2 backdrop-blur-md md:px-3">
|
|
@@ -288,6 +369,7 @@ export function Slide() {
|
|
|
288
369
|
</TabsList>
|
|
289
370
|
</Tabs>
|
|
290
371
|
)}
|
|
372
|
+
{import.meta.env.DEV && <AgentConnectedBadge />}
|
|
291
373
|
</div>
|
|
292
374
|
|
|
293
375
|
{/* Centered title — the rail and mobile pill carry the page count. */}
|
|
@@ -400,57 +482,68 @@ export function Slide() {
|
|
|
400
482
|
</div>
|
|
401
483
|
) : (
|
|
402
484
|
<DesignProvider slideId={slideId}>
|
|
403
|
-
<div className="flex min-h-0 flex-1 flex-col
|
|
404
|
-
<div className="
|
|
405
|
-
<
|
|
485
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
486
|
+
<div className="flex min-h-0 flex-1 flex-col md:flex-row">
|
|
487
|
+
<ResizableRail
|
|
406
488
|
pages={pages}
|
|
407
489
|
design={slide.design}
|
|
408
490
|
current={index}
|
|
409
491
|
onSelect={goTo}
|
|
410
492
|
onReorder={import.meta.env.DEV ? reorderPage : undefined}
|
|
493
|
+
actions={thumbnailActions}
|
|
411
494
|
/>
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
495
|
+
<main
|
|
496
|
+
ref={slideViewportRef}
|
|
497
|
+
data-inspector-root
|
|
498
|
+
data-slide-id={slideId}
|
|
499
|
+
className="paper relative min-h-0 min-w-0 flex-1 bg-canvas p-2 md:p-10"
|
|
500
|
+
>
|
|
501
|
+
<SlideWheelNavigation
|
|
502
|
+
targetRef={slideViewportRef}
|
|
503
|
+
onPrev={() => goTo(index - 1)}
|
|
504
|
+
onNext={() => goTo(index + 1)}
|
|
505
|
+
canPrev={index > 0}
|
|
506
|
+
canNext={index < pageCount - 1}
|
|
507
|
+
/>
|
|
508
|
+
<SlideCanvas design={slide.design}>
|
|
509
|
+
<CurrentPage />
|
|
510
|
+
</SlideCanvas>
|
|
511
|
+
<ClickNavZones
|
|
512
|
+
onPrev={() => goTo(index - 1)}
|
|
513
|
+
onNext={() => goTo(index + 1)}
|
|
514
|
+
canPrev={index > 0}
|
|
515
|
+
canNext={index < pageCount - 1}
|
|
516
|
+
/>
|
|
517
|
+
<InspectOverlay />
|
|
518
|
+
<SaveBar />
|
|
519
|
+
{import.meta.env.DEV && <CommentWidget />}
|
|
520
|
+
</main>
|
|
521
|
+
{/* Mobile-only horizontal rail. Sits below the canvas and
|
|
439
522
|
pads its bottom for the iOS home indicator / Safari URL bar. */}
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
523
|
+
<div
|
|
524
|
+
className="shrink-0 border-t border-hairline md:hidden"
|
|
525
|
+
style={{ paddingBottom: 'env(safe-area-inset-bottom)' }}
|
|
526
|
+
>
|
|
527
|
+
<ThumbnailRail
|
|
528
|
+
pages={pages}
|
|
529
|
+
design={slide.design}
|
|
530
|
+
current={index}
|
|
531
|
+
onSelect={goTo}
|
|
532
|
+
orientation="horizontal"
|
|
533
|
+
actions={thumbnailActions}
|
|
534
|
+
/>
|
|
535
|
+
</div>
|
|
536
|
+
<InspectorPanel />
|
|
537
|
+
<DesignPanel open={designOpen} onClose={() => setDesignOpen(false)} />
|
|
451
538
|
</div>
|
|
452
|
-
|
|
453
|
-
|
|
539
|
+
{import.meta.env.DEV && (
|
|
540
|
+
<NotesDrawer
|
|
541
|
+
slideId={slideId}
|
|
542
|
+
index={index}
|
|
543
|
+
total={pageCount}
|
|
544
|
+
initial={slide.notes?.[index]}
|
|
545
|
+
/>
|
|
546
|
+
)}
|
|
454
547
|
</div>
|
|
455
548
|
</DesignProvider>
|
|
456
549
|
)}
|
|
@@ -460,6 +553,178 @@ export function Slide() {
|
|
|
460
553
|
);
|
|
461
554
|
}
|
|
462
555
|
|
|
556
|
+
const RAIL_WIDTH_STORAGE_KEY = 'open-slide:thumbnail-rail-width';
|
|
557
|
+
const DEFAULT_RAIL_WIDTH = 264;
|
|
558
|
+
const MIN_RAIL_WIDTH = 200;
|
|
559
|
+
const MAX_RAIL_WIDTH = 480;
|
|
560
|
+
|
|
561
|
+
function readStoredRailWidth(): number {
|
|
562
|
+
if (typeof window === 'undefined') return DEFAULT_RAIL_WIDTH;
|
|
563
|
+
const raw = window.localStorage.getItem(RAIL_WIDTH_STORAGE_KEY);
|
|
564
|
+
const parsed = raw == null ? Number.NaN : Number(raw);
|
|
565
|
+
if (!Number.isFinite(parsed)) return DEFAULT_RAIL_WIDTH;
|
|
566
|
+
return Math.min(MAX_RAIL_WIDTH, Math.max(MIN_RAIL_WIDTH, parsed));
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function ResizableRail(props: {
|
|
570
|
+
pages: SlideModule['default'];
|
|
571
|
+
design?: SlideModule['design'];
|
|
572
|
+
current: number;
|
|
573
|
+
onSelect: (i: number) => void;
|
|
574
|
+
onReorder?: (from: number, to: number) => void;
|
|
575
|
+
actions?: ThumbnailActions;
|
|
576
|
+
}) {
|
|
577
|
+
const t = useLocale();
|
|
578
|
+
const [width, setWidth] = useState<number>(readStoredRailWidth);
|
|
579
|
+
const [resizing, setResizing] = useState(false);
|
|
580
|
+
const dragRef = useRef<{ startX: number; startWidth: number } | null>(null);
|
|
581
|
+
|
|
582
|
+
useEffect(() => {
|
|
583
|
+
if (typeof window === 'undefined') return;
|
|
584
|
+
window.localStorage.setItem(RAIL_WIDTH_STORAGE_KEY, String(width));
|
|
585
|
+
}, [width]);
|
|
586
|
+
|
|
587
|
+
useEffect(() => {
|
|
588
|
+
if (!resizing) return;
|
|
589
|
+
const prev = {
|
|
590
|
+
cursor: document.body.style.cursor,
|
|
591
|
+
userSelect: document.body.style.userSelect,
|
|
592
|
+
};
|
|
593
|
+
document.body.style.cursor = 'col-resize';
|
|
594
|
+
document.body.style.userSelect = 'none';
|
|
595
|
+
return () => {
|
|
596
|
+
document.body.style.cursor = prev.cursor;
|
|
597
|
+
document.body.style.userSelect = prev.userSelect;
|
|
598
|
+
};
|
|
599
|
+
}, [resizing]);
|
|
600
|
+
|
|
601
|
+
const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
602
|
+
e.preventDefault();
|
|
603
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
604
|
+
dragRef.current = { startX: e.clientX, startWidth: width };
|
|
605
|
+
setResizing(true);
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
609
|
+
if (!dragRef.current) return;
|
|
610
|
+
const delta = e.clientX - dragRef.current.startX;
|
|
611
|
+
const next = Math.min(
|
|
612
|
+
MAX_RAIL_WIDTH,
|
|
613
|
+
Math.max(MIN_RAIL_WIDTH, dragRef.current.startWidth + delta),
|
|
614
|
+
);
|
|
615
|
+
setWidth(next);
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
const onPointerUp = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
619
|
+
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
|
620
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
621
|
+
}
|
|
622
|
+
dragRef.current = null;
|
|
623
|
+
setResizing(false);
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
627
|
+
const step = e.shiftKey ? 32 : 8;
|
|
628
|
+
if (e.key === 'ArrowLeft') {
|
|
629
|
+
e.preventDefault();
|
|
630
|
+
e.stopPropagation();
|
|
631
|
+
setWidth((w) => Math.max(MIN_RAIL_WIDTH, w - step));
|
|
632
|
+
} else if (e.key === 'ArrowRight') {
|
|
633
|
+
e.preventDefault();
|
|
634
|
+
e.stopPropagation();
|
|
635
|
+
setWidth((w) => Math.min(MAX_RAIL_WIDTH, w + step));
|
|
636
|
+
} else if (e.key === 'Home') {
|
|
637
|
+
e.preventDefault();
|
|
638
|
+
e.stopPropagation();
|
|
639
|
+
setWidth(DEFAULT_RAIL_WIDTH);
|
|
640
|
+
}
|
|
641
|
+
};
|
|
642
|
+
|
|
643
|
+
return (
|
|
644
|
+
<div className="relative hidden shrink-0 md:block" style={{ width }}>
|
|
645
|
+
<ThumbnailRail width={width} {...props} />
|
|
646
|
+
{/* biome-ignore lint/a11y/useSemanticElements: focusable resize handle (splitter pattern), not a static <hr> */}
|
|
647
|
+
<div
|
|
648
|
+
role="separator"
|
|
649
|
+
aria-orientation="vertical"
|
|
650
|
+
aria-label={t.thumbnailRail.resizeRail}
|
|
651
|
+
aria-valuenow={width}
|
|
652
|
+
aria-valuemin={MIN_RAIL_WIDTH}
|
|
653
|
+
aria-valuemax={MAX_RAIL_WIDTH}
|
|
654
|
+
tabIndex={0}
|
|
655
|
+
onPointerDown={onPointerDown}
|
|
656
|
+
onPointerMove={onPointerMove}
|
|
657
|
+
onPointerUp={onPointerUp}
|
|
658
|
+
onPointerCancel={onPointerUp}
|
|
659
|
+
onKeyDown={onKeyDown}
|
|
660
|
+
onDoubleClick={() => setWidth(DEFAULT_RAIL_WIDTH)}
|
|
661
|
+
className={cn(
|
|
662
|
+
'group/resize absolute inset-y-0 right-0 z-20 w-1.5 translate-x-1/2 cursor-col-resize touch-none outline-none',
|
|
663
|
+
'focus-visible:bg-brand/20',
|
|
664
|
+
)}
|
|
665
|
+
>
|
|
666
|
+
<span
|
|
667
|
+
aria-hidden
|
|
668
|
+
className={cn(
|
|
669
|
+
'pointer-events-none absolute inset-y-0 left-1/2 w-px -translate-x-1/2 bg-brand opacity-0 transition-opacity',
|
|
670
|
+
'group-hover/resize:opacity-100 group-focus-visible/resize:opacity-100',
|
|
671
|
+
resizing && 'opacity-100',
|
|
672
|
+
)}
|
|
673
|
+
/>
|
|
674
|
+
</div>
|
|
675
|
+
</div>
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function AgentConnectedBadge() {
|
|
680
|
+
const t = useLocale();
|
|
681
|
+
const connected = useAgentSocketConnected();
|
|
682
|
+
return (
|
|
683
|
+
<TooltipProvider delayDuration={200}>
|
|
684
|
+
<Tooltip>
|
|
685
|
+
<TooltipTrigger asChild>
|
|
686
|
+
<button
|
|
687
|
+
type="button"
|
|
688
|
+
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"
|
|
689
|
+
>
|
|
690
|
+
<span aria-hidden className="relative flex size-1.5 items-center justify-center">
|
|
691
|
+
{connected ? (
|
|
692
|
+
<>
|
|
693
|
+
<span className="absolute inline-flex size-full animate-ping rounded-full bg-emerald-500 opacity-60" />
|
|
694
|
+
<span className="relative inline-flex size-1.5 rounded-full bg-emerald-500" />
|
|
695
|
+
</>
|
|
696
|
+
) : (
|
|
697
|
+
<span className="relative inline-flex size-1.5 rounded-full bg-rose-500" />
|
|
698
|
+
)}
|
|
699
|
+
</span>
|
|
700
|
+
{connected ? t.slide.agentConnected : t.slide.agentDisconnected}
|
|
701
|
+
</button>
|
|
702
|
+
</TooltipTrigger>
|
|
703
|
+
<TooltipContent side="bottom" align="start" className="max-w-[280px] leading-relaxed">
|
|
704
|
+
{connected ? t.slide.agentConnectedTooltip : t.slide.agentDisconnectedTooltip}
|
|
705
|
+
</TooltipContent>
|
|
706
|
+
</Tooltip>
|
|
707
|
+
</TooltipProvider>
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function SelectionReporter() {
|
|
712
|
+
const { selected } = useInspector();
|
|
713
|
+
useEffect(() => {
|
|
714
|
+
if (!import.meta.hot) return;
|
|
715
|
+
const selection = selected
|
|
716
|
+
? {
|
|
717
|
+
line: selected.line,
|
|
718
|
+
column: selected.column,
|
|
719
|
+
tagName: selected.anchor.tagName.toLowerCase(),
|
|
720
|
+
text: (selected.anchor.textContent ?? '').replace(/\s+/g, ' ').trim().slice(0, 120),
|
|
721
|
+
}
|
|
722
|
+
: null;
|
|
723
|
+
import.meta.hot.send('open-slide:current', { selection });
|
|
724
|
+
}, [selected]);
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
|
|
463
728
|
function SlideWheelNavigation({
|
|
464
729
|
targetRef,
|
|
465
730
|
onPrev,
|
|
@@ -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,7 @@ export const en: Locale = {
|
|
|
38
38
|
home: {
|
|
39
39
|
appTitle: 'open-slide',
|
|
40
40
|
draft: 'Draft',
|
|
41
|
+
themes: 'Themes',
|
|
41
42
|
folders: 'Folders',
|
|
42
43
|
newFolder: 'New folder',
|
|
43
44
|
folderName: 'Folder name',
|
|
@@ -51,9 +52,8 @@ export const en: Locale = {
|
|
|
51
52
|
nothingMatchesPrefix: 'Nothing matches ',
|
|
52
53
|
nothingMatchesSuffix: ' in this folder.',
|
|
53
54
|
noSlidesYet: 'No slides yet',
|
|
54
|
-
createSlideHintPrefix: '
|
|
55
|
-
|
|
56
|
-
createSlideHintSuffix: '.',
|
|
55
|
+
createSlideHintPrefix: 'Run ',
|
|
56
|
+
createSlideHintSuffix: ' in your agent to scaffold one.',
|
|
57
57
|
folderEmptyTitle: '{name} is empty',
|
|
58
58
|
folderEmptyHint: 'Drag a slide from Draft into this folder in the sidebar.',
|
|
59
59
|
slideActions: 'Slide actions',
|
|
@@ -83,6 +83,12 @@ export const en: Locale = {
|
|
|
83
83
|
slide: {
|
|
84
84
|
home: 'Home',
|
|
85
85
|
backToHome: 'Back to home',
|
|
86
|
+
agentConnected: 'Agent connected',
|
|
87
|
+
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
|
+
agentDisconnected: 'Agent disconnected',
|
|
90
|
+
agentDisconnectedTooltip:
|
|
91
|
+
'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.',
|
|
86
92
|
download: 'Download',
|
|
87
93
|
exportAsHtml: 'Export as HTML',
|
|
88
94
|
exportAsPdf: 'Export as PDF',
|
|
@@ -156,6 +162,12 @@ export const en: Locale = {
|
|
|
156
162
|
inspector: {
|
|
157
163
|
inspect: 'Inspect',
|
|
158
164
|
deselect: 'Deselect',
|
|
165
|
+
agentWatching: 'Agent is watching',
|
|
166
|
+
agentWatchingTooltip:
|
|
167
|
+
'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.',
|
|
168
|
+
agentNotWatching: 'Agent not watching',
|
|
169
|
+
agentNotWatchingTooltip:
|
|
170
|
+
'Lost connection to the dev server, so your agent can no longer see the selected element. Restart the dev server to restore the connection.',
|
|
159
171
|
contentSection: 'Content',
|
|
160
172
|
typographySection: 'Typography',
|
|
161
173
|
colorSection: 'Color',
|
|
@@ -192,10 +204,10 @@ export const en: Locale = {
|
|
|
192
204
|
cropFitContain: 'Fit',
|
|
193
205
|
cropApply: 'Apply',
|
|
194
206
|
cropResetAria: 'Reset crop',
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
207
|
+
leaveComment: 'Leave a comment',
|
|
208
|
+
commentPlaceholder: 'Describe a change for the agent…',
|
|
209
|
+
commentShortcutHint: '⌘↵ to add',
|
|
210
|
+
addComment: 'Add comment',
|
|
199
211
|
unsavedChanges: {
|
|
200
212
|
one: '{count} unsaved change',
|
|
201
213
|
other: '{count} unsaved changes',
|
|
@@ -288,6 +300,14 @@ export const en: Locale = {
|
|
|
288
300
|
thumbnailRail: {
|
|
289
301
|
pages: 'Pages',
|
|
290
302
|
goToPageAria: 'Go to page {n}',
|
|
303
|
+
duplicatePage: 'Duplicate',
|
|
304
|
+
deletePage: 'Delete',
|
|
305
|
+
pageActionsAria: 'Page {n} actions',
|
|
306
|
+
toastDuplicated: 'Duplicated page {n}',
|
|
307
|
+
toastDeleted: 'Deleted page {n}',
|
|
308
|
+
toastDuplicateFailed: 'Could not duplicate page',
|
|
309
|
+
toastDeleteFailed: 'Could not delete page',
|
|
310
|
+
resizeRail: 'Resize thumbnail rail',
|
|
291
311
|
},
|
|
292
312
|
|
|
293
313
|
pdfToast: {
|
|
@@ -309,4 +329,38 @@ export const en: Locale = {
|
|
|
309
329
|
prevAria: 'Previous page',
|
|
310
330
|
nextAria: 'Next page',
|
|
311
331
|
},
|
|
332
|
+
|
|
333
|
+
imagePlaceholder: {
|
|
334
|
+
dropOverlay: 'Drop image to use here',
|
|
335
|
+
uploading: 'Uploading…',
|
|
336
|
+
uploadFailed: "Couldn't upload image",
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
notesDrawer: {
|
|
340
|
+
toggle: 'Notes',
|
|
341
|
+
pageLabel: 'page {n}/{total}',
|
|
342
|
+
placeholder: 'Write speaker notes for this slide…',
|
|
343
|
+
statusSaving: 'Saving…',
|
|
344
|
+
statusSaved: 'Saved',
|
|
345
|
+
statusError: 'Save failed: {msg}',
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
themes: {
|
|
349
|
+
title: 'Themes',
|
|
350
|
+
noThemesTitle: 'No themes yet',
|
|
351
|
+
noThemesHintPrefix: 'Run ',
|
|
352
|
+
noThemesHintSuffix: ' to author one — a markdown file under themes/ plus a sibling demo slide.',
|
|
353
|
+
noDemoYet: 'No demo yet',
|
|
354
|
+
noDemoHintPrefix: 'Re-run ',
|
|
355
|
+
noDemoHintSuffix: ' for this theme to generate a preview slide.',
|
|
356
|
+
backToGallery: 'Back to themes',
|
|
357
|
+
pageOf: 'page {n}/{total}',
|
|
358
|
+
nextPageAria: 'Next page',
|
|
359
|
+
prevPageAria: 'Previous page',
|
|
360
|
+
openThemeAria: 'Open theme {name}',
|
|
361
|
+
usedBy: 'Slides using this theme',
|
|
362
|
+
usedByEmpty: 'No slides use this theme yet.',
|
|
363
|
+
expandPromptAria: 'Expand prompt',
|
|
364
|
+
collapsePromptAria: 'Collapse prompt',
|
|
365
|
+
},
|
|
312
366
|
};
|