@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.
Files changed (45) hide show
  1. package/dist/{build-1Rqivz0d.js → build-tLrkKUHr.js} +1 -1
  2. package/dist/cli/bin.js +3 -3
  3. package/dist/{config-s0YUbmUe.d.ts → config-CfMThYN9.d.ts} +1 -1
  4. package/dist/{config-XZJnC_fu.js → config-PwUHqZ_X.js} +2312 -1654
  5. package/dist/{dev-0W8gYiSa.js → dev-DpCIRbhT.js} +1 -1
  6. package/dist/{en-7GU-DHbJ.js → en-BDnM5zKJ.js} +18 -1
  7. package/dist/index.d.ts +12 -3
  8. package/dist/index.js +20 -4
  9. package/dist/locale/index.d.ts +1 -1
  10. package/dist/locale/index.js +55 -4
  11. package/dist/{preview-DT9hJvzM.js → preview-BSGlM6Se.js} +1 -1
  12. package/dist/{types-QCpkHkiS.d.ts → types-B-KrjgX8.d.ts} +21 -0
  13. package/dist/vite/index.d.ts +2 -2
  14. package/dist/vite/index.js +1 -1
  15. package/package.json +1 -1
  16. package/skills/create-theme/SKILL.md +30 -22
  17. package/skills/slide-authoring/SKILL.md +26 -2
  18. package/src/app/components/asset-view.tsx +83 -10
  19. package/src/app/components/inspector/inspector-panel.tsx +16 -1
  20. package/src/app/components/panel/panel-shell.tsx +5 -3
  21. package/src/app/components/player.tsx +6 -1
  22. package/src/app/components/present/laser-pointer.tsx +3 -4
  23. package/src/app/components/present/overview-grid.tsx +4 -1
  24. package/src/app/components/present/progress-bar.tsx +4 -4
  25. package/src/app/components/themes/theme-detail.tsx +7 -2
  26. package/src/app/components/themes/themes-gallery.tsx +4 -1
  27. package/src/app/components/thumbnail-rail.tsx +10 -2
  28. package/src/app/lib/assets.ts +23 -0
  29. package/src/app/lib/export-html.ts +7 -2
  30. package/src/app/lib/export-pdf.ts +34 -2
  31. package/src/app/lib/folders.ts +35 -1
  32. package/src/app/lib/page-context.tsx +38 -0
  33. package/src/app/lib/sdk.ts +2 -0
  34. package/src/app/lib/slides.ts +2 -0
  35. package/src/app/lib/use-wheel-page-navigation.ts +7 -0
  36. package/src/app/routes/home-shell.tsx +13 -2
  37. package/src/app/routes/home.tsx +129 -5
  38. package/src/app/routes/presenter.tsx +7 -2
  39. package/src/app/routes/slide.tsx +49 -1
  40. package/src/app/virtual.d.ts +1 -0
  41. package/src/locale/en.ts +18 -1
  42. package/src/locale/ja.ts +18 -1
  43. package/src/locale/types.ts +21 -0
  44. package/src/locale/zh-cn.ts +18 -1
  45. 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 { manifest, loading, create, update, remove, assign, renameSlide, deleteSlide } =
36
- useFolders();
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
 
@@ -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
- {filteredSlides.map((id) => (
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
- <FirstPage />
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
- <CurrentPage />
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
- <NextPage />
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">
@@ -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
- <CurrentPage />
588
+ <SlidePageProvider index={index} total={pageCount}>
589
+ <CurrentPage />
590
+ </SlidePageProvider>
543
591
  </SlideCanvas>
544
592
  <ClickNavZones
545
593
  onPrev={() => goTo(index - 1)}
@@ -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: ';',
@@ -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;
@@ -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: ';',