@open-slide/core 1.4.0 → 1.6.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-1Rqivz0d.js → build-tLrkKUHr.js} +1 -1
- package/dist/cli/bin.js +3 -3
- package/dist/{config-s0YUbmUe.d.ts → config-CfMThYN9.d.ts} +1 -1
- package/dist/{config-XZJnC_fu.js → config-PwUHqZ_X.js} +2312 -1654
- package/dist/{dev-0W8gYiSa.js → dev-DpCIRbhT.js} +1 -1
- package/dist/{en-7GU-DHbJ.js → en-BDnM5zKJ.js} +18 -1
- package/dist/index.d.ts +12 -3
- package/dist/index.js +20 -4
- package/dist/locale/index.d.ts +1 -1
- package/dist/locale/index.js +55 -4
- package/dist/{preview-DT9hJvzM.js → preview-BSGlM6Se.js} +1 -1
- package/dist/{types-QCpkHkiS.d.ts → types-B-KrjgX8.d.ts} +21 -0
- package/dist/vite/index.d.ts +2 -2
- package/dist/vite/index.js +1 -1
- package/package.json +1 -1
- package/skills/create-theme/SKILL.md +30 -22
- package/skills/slide-authoring/SKILL.md +26 -2
- package/src/app/components/asset-view.tsx +83 -10
- package/src/app/components/inspector/inspector-panel.tsx +16 -1
- package/src/app/components/panel/panel-shell.tsx +5 -3
- package/src/app/components/player.tsx +6 -1
- package/src/app/components/present/laser-pointer.tsx +3 -4
- package/src/app/components/present/overview-grid.tsx +4 -1
- package/src/app/components/present/progress-bar.tsx +4 -4
- package/src/app/components/themes/theme-detail.tsx +7 -2
- package/src/app/components/themes/themes-gallery.tsx +4 -1
- package/src/app/components/thumbnail-rail.tsx +10 -2
- package/src/app/lib/assets.ts +23 -0
- package/src/app/lib/export-html.ts +7 -2
- package/src/app/lib/export-pdf.ts +34 -2
- package/src/app/lib/folders.ts +35 -1
- package/src/app/lib/page-context.tsx +38 -0
- package/src/app/lib/sdk.ts +2 -0
- package/src/app/lib/slides.ts +2 -0
- package/src/app/lib/use-wheel-page-navigation.ts +7 -0
- package/src/app/routes/home-shell.tsx +13 -2
- package/src/app/routes/home.tsx +129 -5
- package/src/app/routes/presenter.tsx +7 -2
- package/src/app/routes/slide.tsx +49 -1
- package/src/app/virtual.d.ts +1 -0
- package/src/locale/en.ts +18 -1
- package/src/locale/ja.ts +18 -1
- package/src/locale/types.ts +21 -0
- package/src/locale/zh-cn.ts +18 -1
- package/src/locale/zh-tw.ts +18 -1
|
@@ -31,6 +31,7 @@ export function useWheelPageNavigation<T extends HTMLElement>({
|
|
|
31
31
|
|
|
32
32
|
const onWheel = (event: WheelEvent) => {
|
|
33
33
|
if (event.defaultPrevented || event.ctrlKey || shouldIgnoreWheelTarget(event.target)) return;
|
|
34
|
+
if (isVisualViewportZoomed()) return;
|
|
34
35
|
|
|
35
36
|
const deltaY = normalizeDeltaY(event);
|
|
36
37
|
if (Math.abs(deltaY) <= Math.abs(normalizeDeltaX(event))) return;
|
|
@@ -84,6 +85,12 @@ function normalizeWheelDelta(delta: number, deltaMode: number) {
|
|
|
84
85
|
return delta;
|
|
85
86
|
}
|
|
86
87
|
|
|
88
|
+
function isVisualViewportZoomed() {
|
|
89
|
+
if (typeof window === 'undefined') return false;
|
|
90
|
+
const vv = window.visualViewport;
|
|
91
|
+
return vv != null && vv.scale > 1.01;
|
|
92
|
+
}
|
|
93
|
+
|
|
87
94
|
function shouldIgnoreWheelTarget(target: EventTarget | null) {
|
|
88
95
|
if (!(target instanceof HTMLElement)) return false;
|
|
89
96
|
return Boolean(
|
|
@@ -22,6 +22,7 @@ export type HomeOutletContext = {
|
|
|
22
22
|
titleMap: Record<string, string>;
|
|
23
23
|
assign: (slideId: string, folderId: string | null) => Promise<void>;
|
|
24
24
|
renameSlide: (slideId: string, name: string) => Promise<void>;
|
|
25
|
+
duplicateSlide: (slideId: string, newId?: string) => Promise<string>;
|
|
25
26
|
deleteSlide: (slideId: string) => Promise<void>;
|
|
26
27
|
};
|
|
27
28
|
|
|
@@ -32,8 +33,17 @@ function pathToSelectedId(pathname: string, search: URLSearchParams): string {
|
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export function HomeShell() {
|
|
35
|
-
const {
|
|
36
|
-
|
|
36
|
+
const {
|
|
37
|
+
manifest,
|
|
38
|
+
loading,
|
|
39
|
+
create,
|
|
40
|
+
update,
|
|
41
|
+
remove,
|
|
42
|
+
assign,
|
|
43
|
+
renameSlide,
|
|
44
|
+
duplicateSlide,
|
|
45
|
+
deleteSlide,
|
|
46
|
+
} = useFolders();
|
|
37
47
|
const navigate = useNavigate();
|
|
38
48
|
const location = useLocation();
|
|
39
49
|
const [searchParams] = useSearchParams();
|
|
@@ -108,6 +118,7 @@ export function HomeShell() {
|
|
|
108
118
|
titleMap,
|
|
109
119
|
assign,
|
|
110
120
|
renameSlide,
|
|
121
|
+
duplicateSlide,
|
|
111
122
|
deleteSlide,
|
|
112
123
|
};
|
|
113
124
|
|
package/src/app/routes/home.tsx
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ArrowDownAZ,
|
|
3
|
+
ChevronDown,
|
|
4
|
+
Clock,
|
|
5
|
+
Copy,
|
|
2
6
|
FolderInput,
|
|
3
7
|
FolderPlus,
|
|
4
8
|
MoreHorizontal,
|
|
@@ -10,6 +14,7 @@ import {
|
|
|
10
14
|
} from 'lucide-react';
|
|
11
15
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
12
16
|
import { Link, useOutletContext } from 'react-router-dom';
|
|
17
|
+
import { toast } from 'sonner';
|
|
13
18
|
import { Button } from '@/components/ui/button';
|
|
14
19
|
import {
|
|
15
20
|
Dialog,
|
|
@@ -25,15 +30,45 @@ import {
|
|
|
25
30
|
DropdownMenuItem,
|
|
26
31
|
DropdownMenuTrigger,
|
|
27
32
|
} from '@/components/ui/dropdown-menu';
|
|
28
|
-
import { useLocale } from '@/lib/use-locale';
|
|
33
|
+
import { format, useLocale } from '@/lib/use-locale';
|
|
29
34
|
import { cn } from '@/lib/utils';
|
|
30
35
|
import { FolderIconChip, SLIDE_DND_MIME } from '../components/sidebar/folder-item';
|
|
31
36
|
import { DRAFT_ID } from '../components/sidebar/sidebar';
|
|
32
37
|
import { SlideCanvas } from '../components/slide-canvas';
|
|
38
|
+
import { SlidePageProvider } from '../lib/page-context';
|
|
33
39
|
import type { Folder, FolderIcon, SlideModule } from '../lib/sdk';
|
|
34
|
-
import { loadSlide } from '../lib/slides';
|
|
40
|
+
import { loadSlide, slideCreatedAt } from '../lib/slides';
|
|
35
41
|
import type { HomeOutletContext } from './home-shell';
|
|
36
42
|
|
|
43
|
+
type SortKey = 'created-desc' | 'created-asc' | 'title-asc' | 'title-desc';
|
|
44
|
+
|
|
45
|
+
const SORT_KEYS: readonly SortKey[] = ['created-desc', 'created-asc', 'title-asc', 'title-desc'];
|
|
46
|
+
|
|
47
|
+
const DEFAULT_SORT: SortKey = 'created-desc';
|
|
48
|
+
const SORT_STORAGE_KEY = 'open-slide:home-sort';
|
|
49
|
+
|
|
50
|
+
function readSortPref(): SortKey {
|
|
51
|
+
if (typeof window === 'undefined') return DEFAULT_SORT;
|
|
52
|
+
try {
|
|
53
|
+
const raw = window.localStorage.getItem(SORT_STORAGE_KEY);
|
|
54
|
+
if (raw && (SORT_KEYS as readonly string[]).includes(raw)) return raw as SortKey;
|
|
55
|
+
} catch {}
|
|
56
|
+
return DEFAULT_SORT;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function useSortPref(): [SortKey, (next: SortKey) => void] {
|
|
60
|
+
const [sortKey, setSortKey] = useState<SortKey>(readSortPref);
|
|
61
|
+
const update = (next: SortKey) => {
|
|
62
|
+
setSortKey(next);
|
|
63
|
+
try {
|
|
64
|
+
window.localStorage.setItem(SORT_STORAGE_KEY, next);
|
|
65
|
+
} catch {}
|
|
66
|
+
};
|
|
67
|
+
return [sortKey, update];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const TITLE_COLLATOR = new Intl.Collator(undefined, { sensitivity: 'base', numeric: true });
|
|
71
|
+
|
|
37
72
|
export function Home() {
|
|
38
73
|
const {
|
|
39
74
|
manifest,
|
|
@@ -45,6 +80,7 @@ export function Home() {
|
|
|
45
80
|
titleMap,
|
|
46
81
|
assign,
|
|
47
82
|
renameSlide,
|
|
83
|
+
duplicateSlide,
|
|
48
84
|
deleteSlide,
|
|
49
85
|
} = useOutletContext<HomeOutletContext>();
|
|
50
86
|
const t = useLocale();
|
|
@@ -58,6 +94,7 @@ export function Home() {
|
|
|
58
94
|
const isDraft = selectedId === DRAFT_ID;
|
|
59
95
|
|
|
60
96
|
const [query, setQuery] = useState('');
|
|
97
|
+
const [sortKey, setSortKey] = useSortPref();
|
|
61
98
|
|
|
62
99
|
const trimmedQuery = query.trim().toLowerCase();
|
|
63
100
|
const filteredSlides = useMemo(() => {
|
|
@@ -68,6 +105,24 @@ export function Home() {
|
|
|
68
105
|
return tl ? tl.includes(trimmedQuery) : false;
|
|
69
106
|
});
|
|
70
107
|
}, [visibleSlides, titleMap, trimmedQuery]);
|
|
108
|
+
const sortedSlides = useMemo(() => {
|
|
109
|
+
const list = filteredSlides.slice();
|
|
110
|
+
const titleOf = (id: string) => titleMap[id] ?? id;
|
|
111
|
+
switch (sortKey) {
|
|
112
|
+
case 'title-asc':
|
|
113
|
+
list.sort((a, b) => TITLE_COLLATOR.compare(titleOf(a), titleOf(b)));
|
|
114
|
+
break;
|
|
115
|
+
case 'title-desc':
|
|
116
|
+
list.sort((a, b) => TITLE_COLLATOR.compare(titleOf(b), titleOf(a)));
|
|
117
|
+
break;
|
|
118
|
+
case 'created-asc':
|
|
119
|
+
list.sort((a, b) => (slideCreatedAt[a] ?? 0) - (slideCreatedAt[b] ?? 0));
|
|
120
|
+
break;
|
|
121
|
+
default:
|
|
122
|
+
list.sort((a, b) => (slideCreatedAt[b] ?? 0) - (slideCreatedAt[a] ?? 0));
|
|
123
|
+
}
|
|
124
|
+
return list;
|
|
125
|
+
}, [filteredSlides, sortKey, titleMap]);
|
|
71
126
|
const isSearching = trimmedQuery.length > 0;
|
|
72
127
|
|
|
73
128
|
return (
|
|
@@ -90,7 +145,8 @@ export function Home() {
|
|
|
90
145
|
)}
|
|
91
146
|
</span>
|
|
92
147
|
)}
|
|
93
|
-
<div className="ml-auto w-full md:w-auto">
|
|
148
|
+
<div className="ml-auto flex w-full items-center gap-2 md:w-auto">
|
|
149
|
+
<SortControl value={sortKey} onChange={setSortKey} />
|
|
94
150
|
<SearchInput value={query} onChange={setQuery} />
|
|
95
151
|
</div>
|
|
96
152
|
</div>
|
|
@@ -104,13 +160,27 @@ export function Home() {
|
|
|
104
160
|
<NoResultsState query={query} onClear={() => setQuery('')} />
|
|
105
161
|
) : (
|
|
106
162
|
<ul className="grid grid-cols-[repeat(auto-fill,minmax(240px,1fr))] gap-x-6 gap-y-9 md:grid-cols-[repeat(auto-fill,minmax(300px,1fr))]">
|
|
107
|
-
{
|
|
163
|
+
{sortedSlides.map((id) => (
|
|
108
164
|
<li key={id}>
|
|
109
165
|
<SlideCard
|
|
110
166
|
id={id}
|
|
111
167
|
folders={manifest.folders}
|
|
112
168
|
currentFolderId={manifest.assignments[id] ?? null}
|
|
113
169
|
onRename={(name) => renameSlide(id, name)}
|
|
170
|
+
onDuplicate={async () => {
|
|
171
|
+
const slideName = titleMap[id] ?? id;
|
|
172
|
+
try {
|
|
173
|
+
const newSlideId = await duplicateSlide(id);
|
|
174
|
+
toast.success(
|
|
175
|
+
format(t.home.toastSlideDuplicated, {
|
|
176
|
+
slide: slideName,
|
|
177
|
+
newSlide: newSlideId,
|
|
178
|
+
}),
|
|
179
|
+
);
|
|
180
|
+
} catch {
|
|
181
|
+
toast.error(t.home.toastSlideDuplicateFailed);
|
|
182
|
+
}
|
|
183
|
+
}}
|
|
114
184
|
onMove={(folderId) => assign(id, folderId)}
|
|
115
185
|
onDelete={() => deleteSlide(id)}
|
|
116
186
|
onTitleResolved={reportTitle}
|
|
@@ -152,6 +222,52 @@ function SearchInput({ value, onChange }: { value: string; onChange: (value: str
|
|
|
152
222
|
);
|
|
153
223
|
}
|
|
154
224
|
|
|
225
|
+
function SortControl({ value, onChange }: { value: SortKey; onChange: (next: SortKey) => void }) {
|
|
226
|
+
const t = useLocale();
|
|
227
|
+
const labels: Record<SortKey, string> = {
|
|
228
|
+
'created-desc': t.home.sortByCreatedDesc,
|
|
229
|
+
'created-asc': t.home.sortByCreatedAsc,
|
|
230
|
+
'title-asc': t.home.sortByTitleAsc,
|
|
231
|
+
'title-desc': t.home.sortByTitleDesc,
|
|
232
|
+
};
|
|
233
|
+
const FieldIcon = ({ k, className }: { k: SortKey; className?: string }) =>
|
|
234
|
+
k === 'title-asc' || k === 'title-desc' ? (
|
|
235
|
+
<ArrowDownAZ className={className} aria-hidden />
|
|
236
|
+
) : (
|
|
237
|
+
<Clock className={className} aria-hidden />
|
|
238
|
+
);
|
|
239
|
+
return (
|
|
240
|
+
<DropdownMenu>
|
|
241
|
+
<DropdownMenuTrigger asChild>
|
|
242
|
+
<button
|
|
243
|
+
type="button"
|
|
244
|
+
aria-label={`${t.home.sortLabel}: ${labels[value]}`}
|
|
245
|
+
className="flex h-8 items-center gap-1.5 rounded-[6px] border border-border bg-background pl-2 pr-1.5 text-[12.5px] font-medium text-foreground outline-none hover:bg-muted focus-visible:border-foreground/40 focus-visible:ring-2 focus-visible:ring-ring/30"
|
|
246
|
+
>
|
|
247
|
+
<FieldIcon k={value} className="size-3.5 text-muted-foreground" />
|
|
248
|
+
<span>{labels[value]}</span>
|
|
249
|
+
<ChevronDown className="size-3 text-muted-foreground" aria-hidden />
|
|
250
|
+
</button>
|
|
251
|
+
</DropdownMenuTrigger>
|
|
252
|
+
<DropdownMenuContent align="end" className="min-w-[180px]">
|
|
253
|
+
{SORT_KEYS.map((key) => {
|
|
254
|
+
const active = value === key;
|
|
255
|
+
return (
|
|
256
|
+
<DropdownMenuItem
|
|
257
|
+
key={key}
|
|
258
|
+
onSelect={() => onChange(key)}
|
|
259
|
+
className={cn(active && 'bg-muted text-foreground')}
|
|
260
|
+
>
|
|
261
|
+
<FieldIcon k={key} className="size-3.5 text-muted-foreground" />
|
|
262
|
+
<span>{labels[key]}</span>
|
|
263
|
+
</DropdownMenuItem>
|
|
264
|
+
);
|
|
265
|
+
})}
|
|
266
|
+
</DropdownMenuContent>
|
|
267
|
+
</DropdownMenu>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
155
271
|
function HomeLoading() {
|
|
156
272
|
const t = useLocale();
|
|
157
273
|
return (
|
|
@@ -283,6 +399,7 @@ function SlideCard({
|
|
|
283
399
|
folders,
|
|
284
400
|
currentFolderId,
|
|
285
401
|
onRename,
|
|
402
|
+
onDuplicate,
|
|
286
403
|
onMove,
|
|
287
404
|
onDelete,
|
|
288
405
|
onTitleResolved,
|
|
@@ -291,6 +408,7 @@ function SlideCard({
|
|
|
291
408
|
folders: Folder[];
|
|
292
409
|
currentFolderId: string | null;
|
|
293
410
|
onRename: (name: string) => Promise<void> | void;
|
|
411
|
+
onDuplicate: () => Promise<void> | void;
|
|
294
412
|
onMove: (folderId: string | null) => Promise<void> | void;
|
|
295
413
|
onDelete: () => Promise<void> | void;
|
|
296
414
|
onTitleResolved?: (id: string, title: string) => void;
|
|
@@ -343,7 +461,9 @@ function SlideCard({
|
|
|
343
461
|
{FirstPage ? (
|
|
344
462
|
<div className="h-full w-full motion-safe:transition-transform motion-safe:duration-300 motion-safe:group-hover:scale-[1.03]">
|
|
345
463
|
<SlideCanvas flat freezeMotion design={slide?.design}>
|
|
346
|
-
<
|
|
464
|
+
<SlidePageProvider index={0} total={slide?.default.length ?? 1}>
|
|
465
|
+
<FirstPage />
|
|
466
|
+
</SlidePageProvider>
|
|
347
467
|
</SlideCanvas>
|
|
348
468
|
</div>
|
|
349
469
|
) : (
|
|
@@ -391,6 +511,10 @@ function SlideCard({
|
|
|
391
511
|
<Pencil />
|
|
392
512
|
{tCard.common.rename}
|
|
393
513
|
</DropdownMenuItem>
|
|
514
|
+
<DropdownMenuItem onSelect={() => onDuplicate()}>
|
|
515
|
+
<Copy />
|
|
516
|
+
{tCard.home.duplicate}
|
|
517
|
+
</DropdownMenuItem>
|
|
394
518
|
<DropdownMenuItem onSelect={() => setDialog('move')}>
|
|
395
519
|
<FolderInput />
|
|
396
520
|
{tCard.home.moveToFolder}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
usePresenterChannel,
|
|
10
10
|
} from '../components/present/use-presenter-channel';
|
|
11
11
|
import { SlideCanvas } from '../components/slide-canvas';
|
|
12
|
+
import { SlidePageProvider } from '../lib/page-context';
|
|
12
13
|
import { CANVAS_HEIGHT, CANVAS_WIDTH } from '../lib/sdk';
|
|
13
14
|
import { useSlideModule } from '../lib/use-slide-module';
|
|
14
15
|
|
|
@@ -138,7 +139,9 @@ export function Presenter() {
|
|
|
138
139
|
<SectionLabel>{t.presenter.nowShowing}</SectionLabel>
|
|
139
140
|
<div className="relative min-h-0 flex-1 overflow-hidden rounded-[8px] bg-black ring-1 ring-border">
|
|
140
141
|
<SlideCanvas flat design={slide.design}>
|
|
141
|
-
<
|
|
142
|
+
<SlidePageProvider index={index} total={total}>
|
|
143
|
+
<CurrentPage />
|
|
144
|
+
</SlidePageProvider>
|
|
142
145
|
</SlideCanvas>
|
|
143
146
|
{blackout && (
|
|
144
147
|
<div
|
|
@@ -164,7 +167,9 @@ export function Presenter() {
|
|
|
164
167
|
>
|
|
165
168
|
{NextPage ? (
|
|
166
169
|
<SlideCanvas flat freezeMotion design={slide.design}>
|
|
167
|
-
<
|
|
170
|
+
<SlidePageProvider index={nextIndex} total={total}>
|
|
171
|
+
<NextPage />
|
|
172
|
+
</SlidePageProvider>
|
|
168
173
|
</SlideCanvas>
|
|
169
174
|
) : (
|
|
170
175
|
<div className="grid h-full place-items-center text-[11.5px] text-muted-foreground">
|
package/src/app/routes/slide.tsx
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import config from 'virtual:open-slide/config';
|
|
2
2
|
import {
|
|
3
|
+
Check,
|
|
3
4
|
ChevronDown,
|
|
4
5
|
ChevronLeft,
|
|
5
6
|
Download,
|
|
6
7
|
FileCode2,
|
|
7
8
|
FileText,
|
|
9
|
+
Link2,
|
|
8
10
|
Loader2,
|
|
9
11
|
Maximize,
|
|
10
12
|
MonitorSpeaker,
|
|
@@ -50,6 +52,7 @@ import { type ThumbnailActions, ThumbnailRail } from '../components/thumbnail-ra
|
|
|
50
52
|
import { exportSlideAsHtml } from '../lib/export-html';
|
|
51
53
|
import { exportSlideAsPdf, isSafari } from '../lib/export-pdf';
|
|
52
54
|
import { remapNotesSessionCacheAfterReorder } from '../lib/inspector/use-notes';
|
|
55
|
+
import { SlidePageProvider } from '../lib/page-context';
|
|
53
56
|
import type { SlideModule } from '../lib/sdk';
|
|
54
57
|
import { useSlideModule } from '../lib/use-slide-module';
|
|
55
58
|
|
|
@@ -61,7 +64,15 @@ export function Slide() {
|
|
|
61
64
|
const { slide, error } = useSlideModule(slideId);
|
|
62
65
|
const [playMode, setPlayMode] = useState<'window' | 'fullscreen' | null>(null);
|
|
63
66
|
const [exporting, setExporting] = useState(false);
|
|
67
|
+
const [linkCopied, setLinkCopied] = useState(false);
|
|
68
|
+
const linkCopiedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
64
69
|
const [designOpen, setDesignOpen] = useState(false);
|
|
70
|
+
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
return () => {
|
|
73
|
+
if (linkCopiedTimerRef.current) clearTimeout(linkCopiedTimerRef.current);
|
|
74
|
+
};
|
|
75
|
+
}, []);
|
|
65
76
|
const { renameSlide } = useFolders();
|
|
66
77
|
const slideViewportRef = useRef<HTMLElement>(null);
|
|
67
78
|
const t = useLocale();
|
|
@@ -375,6 +386,41 @@ export function Slide() {
|
|
|
375
386
|
</div>
|
|
376
387
|
|
|
377
388
|
<div className="flex items-center gap-1">
|
|
389
|
+
{view === 'slides' && (
|
|
390
|
+
<button
|
|
391
|
+
type="button"
|
|
392
|
+
aria-label={t.slide.copyLink}
|
|
393
|
+
title={t.slide.copyLink}
|
|
394
|
+
className={cn(buttonVariants({ variant: 'ghost', size: 'icon-sm' }))}
|
|
395
|
+
onClick={async () => {
|
|
396
|
+
try {
|
|
397
|
+
await navigator.clipboard.writeText(window.location.href);
|
|
398
|
+
toast.success(t.slide.toastCopyLinkSuccess);
|
|
399
|
+
setLinkCopied(true);
|
|
400
|
+
if (linkCopiedTimerRef.current) clearTimeout(linkCopiedTimerRef.current);
|
|
401
|
+
linkCopiedTimerRef.current = setTimeout(() => setLinkCopied(false), 1200);
|
|
402
|
+
} catch (err) {
|
|
403
|
+
console.error('[open-slide] copy link failed', err);
|
|
404
|
+
toast.error(t.slide.toastCopyLinkFailed);
|
|
405
|
+
}
|
|
406
|
+
}}
|
|
407
|
+
>
|
|
408
|
+
<span className="relative grid size-4 place-items-center">
|
|
409
|
+
<Link2
|
|
410
|
+
className={cn(
|
|
411
|
+
'col-start-1 row-start-1 size-4 transition-opacity duration-200',
|
|
412
|
+
linkCopied ? 'opacity-0' : 'opacity-100',
|
|
413
|
+
)}
|
|
414
|
+
/>
|
|
415
|
+
<Check
|
|
416
|
+
className={cn(
|
|
417
|
+
'col-start-1 row-start-1 size-4 transition-opacity duration-200',
|
|
418
|
+
linkCopied ? 'opacity-100' : 'opacity-0',
|
|
419
|
+
)}
|
|
420
|
+
/>
|
|
421
|
+
</span>
|
|
422
|
+
</button>
|
|
423
|
+
)}
|
|
378
424
|
{view === 'slides' && allowHtmlDownload && (
|
|
379
425
|
<DropdownMenu>
|
|
380
426
|
<DropdownMenuTrigger
|
|
@@ -539,7 +585,9 @@ export function Slide() {
|
|
|
539
585
|
canNext={index < pageCount - 1}
|
|
540
586
|
/>
|
|
541
587
|
<SlideCanvas design={slide.design}>
|
|
542
|
-
<
|
|
588
|
+
<SlidePageProvider index={index} total={pageCount}>
|
|
589
|
+
<CurrentPage />
|
|
590
|
+
</SlidePageProvider>
|
|
543
591
|
</SlideCanvas>
|
|
544
592
|
<ClickNavZones
|
|
545
593
|
onPrev={() => goTo(index - 1)}
|
package/src/app/virtual.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ declare module 'virtual:open-slide/slides' {
|
|
|
2
2
|
import type { SlideModule } from './lib/sdk';
|
|
3
3
|
export const slideIds: string[];
|
|
4
4
|
export const slideThemes: Record<string, string>;
|
|
5
|
+
export const slideCreatedAt: Record<string, number>;
|
|
5
6
|
export function loadSlide(id: string): Promise<SlideModule>;
|
|
6
7
|
}
|
|
7
8
|
|
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
|
+
duplicate: 'Duplicate',
|
|
41
42
|
themes: 'Themes',
|
|
42
43
|
assets: 'Assets',
|
|
43
44
|
folders: 'Folders',
|
|
@@ -49,6 +50,11 @@ export const en: Locale = {
|
|
|
49
50
|
folderActions: 'Folder actions',
|
|
50
51
|
searchPlaceholder: 'Search slides',
|
|
51
52
|
clearSearch: 'Clear search',
|
|
53
|
+
sortLabel: 'Sort',
|
|
54
|
+
sortByCreatedDesc: 'Newest',
|
|
55
|
+
sortByCreatedAsc: 'Oldest',
|
|
56
|
+
sortByTitleAsc: 'A–Z',
|
|
57
|
+
sortByTitleDesc: 'Z–A',
|
|
52
58
|
noMatches: 'No matches',
|
|
53
59
|
nothingMatchesPrefix: 'Nothing matches ',
|
|
54
60
|
nothingMatchesSuffix: ' in this folder.',
|
|
@@ -74,6 +80,8 @@ export const en: Locale = {
|
|
|
74
80
|
deleteDialogDescriptionSuffix: 'This action cannot be undone.',
|
|
75
81
|
toastFolderCreated: 'Created folder “{name}”',
|
|
76
82
|
toastFolderCreateFailed: 'Failed to create folder',
|
|
83
|
+
toastSlideDuplicated: 'Duplicated “{slide}” as {newSlide}',
|
|
84
|
+
toastSlideDuplicateFailed: 'Could not duplicate slide',
|
|
77
85
|
toastSlideMoved: 'Moved “{slide}” to {folder}',
|
|
78
86
|
toastSlideMoveFailed: 'Failed to move slide',
|
|
79
87
|
toastFolderDeleted: 'Deleted folder “{name}”',
|
|
@@ -91,6 +99,9 @@ export const en: Locale = {
|
|
|
91
99
|
agentDisconnectedTooltip:
|
|
92
100
|
'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.',
|
|
93
101
|
download: 'Download',
|
|
102
|
+
copyLink: 'Copy link',
|
|
103
|
+
toastCopyLinkSuccess: 'Link copied to clipboard',
|
|
104
|
+
toastCopyLinkFailed: 'Failed to copy link',
|
|
94
105
|
exportAsHtml: 'Export as HTML',
|
|
95
106
|
exportAsPdf: 'Export as PDF',
|
|
96
107
|
pdfExportFailed: 'PDF export failed',
|
|
@@ -214,7 +225,7 @@ export const en: Locale = {
|
|
|
214
225
|
cropResetAria: 'Reset crop',
|
|
215
226
|
leaveComment: 'Leave a comment',
|
|
216
227
|
commentPlaceholder: 'Describe a change for the agent…',
|
|
217
|
-
commentShortcutHint: '⌘↵ to add',
|
|
228
|
+
commentShortcutHint: '⌘/ to focus · ⌘↵ to add',
|
|
218
229
|
addComment: 'Add comment',
|
|
219
230
|
unsavedChanges: {
|
|
220
231
|
one: '{count} unsaved change',
|
|
@@ -262,6 +273,7 @@ export const en: Locale = {
|
|
|
262
273
|
scopeSlide: 'This slide',
|
|
263
274
|
scopeGlobal: 'Global',
|
|
264
275
|
fileCount: { one: '{count} file', other: '{count} files' },
|
|
276
|
+
usageUnused: 'Unused',
|
|
265
277
|
searchLogos: 'Search logos',
|
|
266
278
|
upload: 'Upload',
|
|
267
279
|
dropToUpload: 'Drop to upload',
|
|
@@ -281,6 +293,11 @@ export const en: Locale = {
|
|
|
281
293
|
conflictRenameCopy: 'Rename copy',
|
|
282
294
|
deleteAssetTitle: 'Delete asset',
|
|
283
295
|
deleteAssetDescription: 'Delete {name}? Imports referencing this file in the slide will break.',
|
|
296
|
+
deleteAssetInUseDescription: '{name} is used in {count} place(s) across {slides} slide(s).',
|
|
297
|
+
deleteAssetInUseHint: 'Deleting will revert these usages back to image placeholders.',
|
|
298
|
+
deleteAndRevert: 'Delete & revert',
|
|
299
|
+
toastRevertFailed: "Couldn't revert usage in {slideId}",
|
|
300
|
+
toastDeletedWithRevert: 'Deleted {name} and reverted {count} usage(s)',
|
|
284
301
|
noPreview: 'No preview available',
|
|
285
302
|
importHintComment: 'import asset from ',
|
|
286
303
|
importHintSemi: ';',
|
package/src/locale/ja.ts
CHANGED
|
@@ -38,6 +38,7 @@ export const ja: Locale = {
|
|
|
38
38
|
home: {
|
|
39
39
|
appTitle: 'open-slide',
|
|
40
40
|
draft: '下書き',
|
|
41
|
+
duplicate: '複製',
|
|
41
42
|
themes: 'テーマ',
|
|
42
43
|
assets: 'アセット',
|
|
43
44
|
folders: 'フォルダ',
|
|
@@ -49,6 +50,11 @@ export const ja: Locale = {
|
|
|
49
50
|
folderActions: 'フォルダ操作',
|
|
50
51
|
searchPlaceholder: 'スライドを検索',
|
|
51
52
|
clearSearch: '検索をクリア',
|
|
53
|
+
sortLabel: '並べ替え',
|
|
54
|
+
sortByCreatedDesc: '新しい順',
|
|
55
|
+
sortByCreatedAsc: '古い順',
|
|
56
|
+
sortByTitleAsc: 'A–Z',
|
|
57
|
+
sortByTitleDesc: 'Z–A',
|
|
52
58
|
noMatches: '一致なし',
|
|
53
59
|
nothingMatchesPrefix: 'このフォルダ内に ',
|
|
54
60
|
nothingMatchesSuffix: ' に一致する項目はありません。',
|
|
@@ -74,6 +80,8 @@ export const ja: Locale = {
|
|
|
74
80
|
deleteDialogDescriptionSuffix: 'この操作は元に戻せません。',
|
|
75
81
|
toastFolderCreated: 'フォルダ「{name}」を作成しました',
|
|
76
82
|
toastFolderCreateFailed: 'フォルダの作成に失敗しました',
|
|
83
|
+
toastSlideDuplicated: '「{slide}」を {newSlide} として複製しました',
|
|
84
|
+
toastSlideDuplicateFailed: 'スライドを複製できませんでした',
|
|
77
85
|
toastSlideMoved: '「{slide}」を {folder} に移動しました',
|
|
78
86
|
toastSlideMoveFailed: 'スライドの移動に失敗しました',
|
|
79
87
|
toastFolderDeleted: 'フォルダ「{name}」を削除しました',
|
|
@@ -91,6 +99,9 @@ export const ja: Locale = {
|
|
|
91
99
|
home: 'ホーム',
|
|
92
100
|
backToHome: 'ホームへ戻る',
|
|
93
101
|
download: 'ダウンロード',
|
|
102
|
+
copyLink: 'リンクをコピー',
|
|
103
|
+
toastCopyLinkSuccess: 'リンクをクリップボードにコピーしました',
|
|
104
|
+
toastCopyLinkFailed: 'リンクのコピーに失敗しました',
|
|
94
105
|
exportAsHtml: 'HTML として書き出し',
|
|
95
106
|
exportAsPdf: 'PDF として書き出し',
|
|
96
107
|
pdfExportFailed: 'PDF の書き出しに失敗しました',
|
|
@@ -215,7 +226,7 @@ export const ja: Locale = {
|
|
|
215
226
|
'dev server との接続が切れたため、選択中の要素がエージェントに見えなくなっています。dev server を再起動して接続を復旧してください。',
|
|
216
227
|
leaveComment: 'コメントを残す',
|
|
217
228
|
commentPlaceholder: 'エージェントに依頼する変更を記述…',
|
|
218
|
-
commentShortcutHint: '⌘↵ で追加',
|
|
229
|
+
commentShortcutHint: '⌘/ でフォーカス · ⌘↵ で追加',
|
|
219
230
|
addComment: 'コメントを追加',
|
|
220
231
|
unsavedChanges: {
|
|
221
232
|
one: '未保存の変更 {count} 件',
|
|
@@ -264,6 +275,7 @@ export const ja: Locale = {
|
|
|
264
275
|
scopeSlide: 'このスライド',
|
|
265
276
|
scopeGlobal: 'グローバル',
|
|
266
277
|
fileCount: { one: 'ファイル {count} 件', other: 'ファイル {count} 件' },
|
|
278
|
+
usageUnused: '未使用',
|
|
267
279
|
searchLogos: 'ロゴを検索',
|
|
268
280
|
upload: 'アップロード',
|
|
269
281
|
dropToUpload: 'ドロップでアップロード',
|
|
@@ -284,6 +296,11 @@ export const ja: Locale = {
|
|
|
284
296
|
deleteAssetTitle: 'アセットを削除',
|
|
285
297
|
deleteAssetDescription:
|
|
286
298
|
'{name} を削除しますか?スライド内でこのファイルを参照しているインポートは壊れます。',
|
|
299
|
+
deleteAssetInUseDescription: '{name} は {slides} 枚のスライドで {count} 箇所使用されています。',
|
|
300
|
+
deleteAssetInUseHint: '削除すると、これらの使用箇所は画像プレースホルダーに戻ります。',
|
|
301
|
+
deleteAndRevert: '削除して戻す',
|
|
302
|
+
toastRevertFailed: '{slideId} の使用箇所を戻せませんでした',
|
|
303
|
+
toastDeletedWithRevert: '{name} を削除し、{count} 箇所をプレースホルダーに戻しました',
|
|
287
304
|
noPreview: 'プレビューはありません',
|
|
288
305
|
importHintComment: 'import asset from ',
|
|
289
306
|
importHintSemi: ';',
|
package/src/locale/types.ts
CHANGED
|
@@ -38,6 +38,7 @@ export type Locale = {
|
|
|
38
38
|
home: {
|
|
39
39
|
appTitle: string;
|
|
40
40
|
draft: string;
|
|
41
|
+
duplicate: string;
|
|
41
42
|
themes: string;
|
|
42
43
|
assets: string;
|
|
43
44
|
folders: string;
|
|
@@ -49,6 +50,11 @@ export type Locale = {
|
|
|
49
50
|
folderActions: string;
|
|
50
51
|
searchPlaceholder: string;
|
|
51
52
|
clearSearch: string;
|
|
53
|
+
sortLabel: string;
|
|
54
|
+
sortByCreatedDesc: string;
|
|
55
|
+
sortByCreatedAsc: string;
|
|
56
|
+
sortByTitleAsc: string;
|
|
57
|
+
sortByTitleDesc: string;
|
|
52
58
|
noMatches: string;
|
|
53
59
|
nothingMatchesPrefix: string;
|
|
54
60
|
nothingMatchesSuffix: string;
|
|
@@ -75,6 +81,9 @@ export type Locale = {
|
|
|
75
81
|
/** template: "Created folder “{name}”" */
|
|
76
82
|
toastFolderCreated: string;
|
|
77
83
|
toastFolderCreateFailed: string;
|
|
84
|
+
/** template: "Duplicated “{slide}” as {newSlide}" */
|
|
85
|
+
toastSlideDuplicated: string;
|
|
86
|
+
toastSlideDuplicateFailed: string;
|
|
78
87
|
/** template: "Moved “{slide}” to {folder}" */
|
|
79
88
|
toastSlideMoved: string;
|
|
80
89
|
toastSlideMoveFailed: string;
|
|
@@ -92,6 +101,9 @@ export type Locale = {
|
|
|
92
101
|
agentDisconnected: string;
|
|
93
102
|
agentDisconnectedTooltip: string;
|
|
94
103
|
download: string;
|
|
104
|
+
copyLink: string;
|
|
105
|
+
toastCopyLinkSuccess: string;
|
|
106
|
+
toastCopyLinkFailed: string;
|
|
95
107
|
exportAsHtml: string;
|
|
96
108
|
exportAsPdf: string;
|
|
97
109
|
pdfExportFailed: string;
|
|
@@ -263,6 +275,7 @@ export type Locale = {
|
|
|
263
275
|
scopeGlobal: string;
|
|
264
276
|
/** templates: "{count} file" / "{count} files" */
|
|
265
277
|
fileCount: Plural;
|
|
278
|
+
usageUnused: string;
|
|
266
279
|
searchLogos: string;
|
|
267
280
|
upload: string;
|
|
268
281
|
dropToUpload: string;
|
|
@@ -286,6 +299,14 @@ export type Locale = {
|
|
|
286
299
|
deleteAssetTitle: string;
|
|
287
300
|
/** template: "Delete {name}? Imports referencing this file in the slide will break." */
|
|
288
301
|
deleteAssetDescription: string;
|
|
302
|
+
/** template: "{name} is used in {count} place across {slides} slide." (singular/plural via {count}/{slides}) */
|
|
303
|
+
deleteAssetInUseDescription: string;
|
|
304
|
+
deleteAssetInUseHint: string;
|
|
305
|
+
deleteAndRevert: string;
|
|
306
|
+
/** template: "Couldn't revert usage in {slideId}." */
|
|
307
|
+
toastRevertFailed: string;
|
|
308
|
+
/** template: "Deleted {name} and reverted {count} usage." */
|
|
309
|
+
toastDeletedWithRevert: string;
|
|
289
310
|
noPreview: string;
|
|
290
311
|
importHintComment: string;
|
|
291
312
|
importHintSemi: string;
|
package/src/locale/zh-cn.ts
CHANGED
|
@@ -38,6 +38,7 @@ export const zhCN: Locale = {
|
|
|
38
38
|
home: {
|
|
39
39
|
appTitle: 'open-slide',
|
|
40
40
|
draft: '草稿',
|
|
41
|
+
duplicate: '复制',
|
|
41
42
|
themes: '主题',
|
|
42
43
|
assets: '素材',
|
|
43
44
|
folders: '文件夹',
|
|
@@ -49,6 +50,11 @@ export const zhCN: Locale = {
|
|
|
49
50
|
folderActions: '文件夹操作',
|
|
50
51
|
searchPlaceholder: '搜索幻灯片',
|
|
51
52
|
clearSearch: '清除搜索',
|
|
53
|
+
sortLabel: '排序',
|
|
54
|
+
sortByCreatedDesc: '最新',
|
|
55
|
+
sortByCreatedAsc: '最旧',
|
|
56
|
+
sortByTitleAsc: 'A–Z',
|
|
57
|
+
sortByTitleDesc: 'Z–A',
|
|
52
58
|
noMatches: '没有匹配项',
|
|
53
59
|
nothingMatchesPrefix: '该文件夹中没有匹配 ',
|
|
54
60
|
nothingMatchesSuffix: ' 的内容。',
|
|
@@ -74,6 +80,8 @@ export const zhCN: Locale = {
|
|
|
74
80
|
deleteDialogDescriptionSuffix: '此操作无法撤销。',
|
|
75
81
|
toastFolderCreated: '已创建文件夹"{name}"',
|
|
76
82
|
toastFolderCreateFailed: '创建文件夹失败',
|
|
83
|
+
toastSlideDuplicated: '已将"{slide}"复制为 {newSlide}',
|
|
84
|
+
toastSlideDuplicateFailed: '无法复制幻灯片',
|
|
77
85
|
toastSlideMoved: '已将"{slide}"移至 {folder}',
|
|
78
86
|
toastSlideMoveFailed: '移动幻灯片失败',
|
|
79
87
|
toastFolderDeleted: '已删除文件夹"{name}"',
|
|
@@ -90,6 +98,9 @@ export const zhCN: Locale = {
|
|
|
90
98
|
home: '首页',
|
|
91
99
|
backToHome: '返回首页',
|
|
92
100
|
download: '下载',
|
|
101
|
+
copyLink: '复制链接',
|
|
102
|
+
toastCopyLinkSuccess: '已复制链接到剪贴板',
|
|
103
|
+
toastCopyLinkFailed: '复制链接失败',
|
|
93
104
|
exportAsHtml: '导出为 HTML',
|
|
94
105
|
exportAsPdf: '导出为 PDF',
|
|
95
106
|
pdfExportFailed: 'PDF 导出失败',
|
|
@@ -213,7 +224,7 @@ export const zhCN: Locale = {
|
|
|
213
224
|
'已和 dev server 断开连接,agent 看不到你选的元素了。请重新启动 dev server 来恢复连接。',
|
|
214
225
|
leaveComment: '留个 comment',
|
|
215
226
|
commentPlaceholder: '描述你希望代理执行的更改…',
|
|
216
|
-
commentShortcutHint: '⌘↵ 添加',
|
|
227
|
+
commentShortcutHint: '⌘/ 聚焦 · ⌘↵ 添加',
|
|
217
228
|
addComment: '添加 comment',
|
|
218
229
|
unsavedChanges: {
|
|
219
230
|
one: '{count} 项未保存的更改',
|
|
@@ -261,6 +272,7 @@ export const zhCN: Locale = {
|
|
|
261
272
|
scopeSlide: '当前幻灯片',
|
|
262
273
|
scopeGlobal: '全局',
|
|
263
274
|
fileCount: { one: '{count} 个文件', other: '{count} 个文件' },
|
|
275
|
+
usageUnused: '未使用',
|
|
264
276
|
searchLogos: '搜索 Logo',
|
|
265
277
|
upload: '上传',
|
|
266
278
|
dropToUpload: '拖入即可上传',
|
|
@@ -280,6 +292,11 @@ export const zhCN: Locale = {
|
|
|
280
292
|
conflictRenameCopy: '重命名副本',
|
|
281
293
|
deleteAssetTitle: '删除素材',
|
|
282
294
|
deleteAssetDescription: '要删除 {name} 吗?幻灯片中引用此文件的导入将失效。',
|
|
295
|
+
deleteAssetInUseDescription: '{name} 在 {slides} 个幻灯片中被使用了 {count} 次。',
|
|
296
|
+
deleteAssetInUseHint: '删除后这些使用处会自动还原为图片占位符。',
|
|
297
|
+
deleteAndRevert: '删除并还原',
|
|
298
|
+
toastRevertFailed: '无法还原 {slideId} 中的使用',
|
|
299
|
+
toastDeletedWithRevert: '已删除 {name} 并还原 {count} 个使用处',
|
|
283
300
|
noPreview: '无预览',
|
|
284
301
|
importHintComment: 'import asset from ',
|
|
285
302
|
importHintSemi: ';',
|