@open-slide/core 1.3.0 → 1.5.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-_276DMmJ.js → build-DZhbjQpQ.js} +1 -1
  2. package/dist/cli/bin.js +3 -3
  3. package/dist/{config-D9cZ1A0X.d.ts → config-BQdTMho4.d.ts} +2 -1
  4. package/dist/{config-BAwKWNtW.js → config-iKjqaX08.js} +2528 -1640
  5. package/dist/{dev-BoqeVXVq.js → dev-BjLGk5nN.js} +1 -1
  6. package/dist/{en-CDKzoZvf.js → en-DDGqyNaW.js} +27 -4
  7. package/dist/index.d.ts +4 -2
  8. package/dist/index.js +1 -1
  9. package/dist/locale/index.d.ts +1 -1
  10. package/dist/locale/index.js +82 -13
  11. package/dist/{preview-BLPxspc9.js → preview-jwLWHWkQ.js} +1 -1
  12. package/dist/{types-JYG1cmwC.d.ts → types-Dpr8nbih.d.ts} +27 -1
  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/slide-authoring/SKILL.md +19 -4
  17. package/src/app/app.tsx +2 -0
  18. package/src/app/components/asset-view.tsx +111 -18
  19. package/src/app/components/inspector/inspect-overlay.tsx +49 -3
  20. package/src/app/components/inspector/inspector-panel.tsx +267 -25
  21. package/src/app/components/inspector/inspector-provider.tsx +390 -49
  22. package/src/app/components/panel/panel-shell.tsx +5 -3
  23. package/src/app/components/player.tsx +25 -5
  24. package/src/app/components/present/control-bar.tsx +12 -0
  25. package/src/app/components/present/laser-pointer.tsx +3 -4
  26. package/src/app/components/present/progress-bar.tsx +4 -4
  27. package/src/app/components/sidebar/folder-item.tsx +14 -3
  28. package/src/app/components/sidebar/sidebar.tsx +10 -0
  29. package/src/app/lib/assets.ts +21 -0
  30. package/src/app/lib/export-pdf.ts +6 -0
  31. package/src/app/lib/inspector/use-editor.ts +9 -1
  32. package/src/app/lib/sdk.ts +2 -0
  33. package/src/app/lib/slides.ts +9 -0
  34. package/src/app/lib/use-slide-module.ts +48 -0
  35. package/src/app/routes/assets.tsx +9 -0
  36. package/src/app/routes/home-shell.tsx +23 -2
  37. package/src/app/routes/home.tsx +101 -3
  38. package/src/app/routes/presenter.tsx +2 -20
  39. package/src/app/routes/slide.tsx +117 -39
  40. package/src/app/virtual.d.ts +1 -0
  41. package/src/locale/en.ts +28 -5
  42. package/src/locale/ja.ts +28 -5
  43. package/src/locale/types.ts +27 -1
  44. package/src/locale/zh-cn.ts +28 -6
  45. package/src/locale/zh-tw.ts +28 -6
@@ -9,14 +9,12 @@ import {
9
9
  usePresenterChannel,
10
10
  } from '../components/present/use-presenter-channel';
11
11
  import { SlideCanvas } from '../components/slide-canvas';
12
- import type { SlideModule } from '../lib/sdk';
13
12
  import { CANVAS_HEIGHT, CANVAS_WIDTH } from '../lib/sdk';
14
- import { loadSlide } from '../lib/slides';
13
+ import { useSlideModule } from '../lib/use-slide-module';
15
14
 
16
15
  export function Presenter() {
17
16
  const { slideId = '' } = useParams();
18
- const [slide, setSlide] = useState<SlideModule | null>(null);
19
- const [error, setError] = useState<string | null>(null);
17
+ const { slide, error } = useSlideModule(slideId);
20
18
 
21
19
  // Presenter view is a passive mirror of the projection window. It only
22
20
  // tracks the index it last heard about; navigation buttons send commands
@@ -29,22 +27,6 @@ export function Presenter() {
29
27
  const requestedRef = useRef(false);
30
28
  const t = useLocale();
31
29
 
32
- useEffect(() => {
33
- let cancelled = false;
34
- setSlide(null);
35
- setError(null);
36
- loadSlide(slideId)
37
- .then((mod) => {
38
- if (!cancelled) setSlide(mod);
39
- })
40
- .catch((e) => {
41
- if (!cancelled) setError(String(e?.message ?? e));
42
- });
43
- return () => {
44
- cancelled = true;
45
- };
46
- }, [slideId]);
47
-
48
30
  const channel = usePresenterChannel(slideId, (msg) => {
49
31
  if (msg.type === 'state') {
50
32
  setState(msg.state);
@@ -1,5 +1,18 @@
1
1
  import config from 'virtual:open-slide/config';
2
- import { ChevronLeft, Download, FileCode2, FileText, Loader2, Pencil, Play } from 'lucide-react';
2
+ import {
3
+ Check,
4
+ ChevronDown,
5
+ ChevronLeft,
6
+ Download,
7
+ FileCode2,
8
+ FileText,
9
+ Link2,
10
+ Loader2,
11
+ Maximize,
12
+ MonitorSpeaker,
13
+ Pencil,
14
+ Play,
15
+ } from 'lucide-react';
3
16
  import { type RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
17
  import { Link, useParams, useSearchParams } from 'react-router-dom';
5
18
  import { toast } from 'sonner';
@@ -33,44 +46,35 @@ import { cn } from '@/lib/utils';
33
46
  import { ClickNavZones } from '../components/click-nav-zones';
34
47
  import { NotesDrawer } from '../components/notes-drawer';
35
48
  import { PdfProgressToast } from '../components/pdf-progress-toast';
36
- import { Player } from '../components/player';
49
+ import { openPresenterWindow, Player } from '../components/player';
37
50
  import { SlideCanvas } from '../components/slide-canvas';
38
51
  import { type ThumbnailActions, ThumbnailRail } from '../components/thumbnail-rail';
39
52
  import { exportSlideAsHtml } from '../lib/export-html';
40
- import { exportSlideAsPdf } from '../lib/export-pdf';
53
+ import { exportSlideAsPdf, isSafari } from '../lib/export-pdf';
41
54
  import { remapNotesSessionCacheAfterReorder } from '../lib/inspector/use-notes';
42
55
  import type { SlideModule } from '../lib/sdk';
43
- import { loadSlide } from '../lib/slides';
56
+ import { useSlideModule } from '../lib/use-slide-module';
44
57
 
45
58
  const { showSlideUi, showSlideBrowser, allowHtmlDownload } = config.build;
46
59
 
47
60
  export function Slide() {
48
61
  const { slideId = '' } = useParams();
49
62
  const [searchParams, setSearchParams] = useSearchParams();
50
- const [slide, setSlide] = useState<SlideModule | null>(null);
51
- const [error, setError] = useState<string | null>(null);
52
- const [playing, setPlaying] = useState(false);
63
+ const { slide, error } = useSlideModule(slideId);
64
+ const [playMode, setPlayMode] = useState<'window' | 'fullscreen' | null>(null);
53
65
  const [exporting, setExporting] = useState(false);
66
+ const [linkCopied, setLinkCopied] = useState(false);
67
+ const linkCopiedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
54
68
  const [designOpen, setDesignOpen] = useState(false);
55
- const { renameSlide } = useFolders();
56
- const slideViewportRef = useRef<HTMLElement>(null);
57
- const t = useLocale();
58
69
 
59
70
  useEffect(() => {
60
- let cancelled = false;
61
- setSlide(null);
62
- setError(null);
63
- loadSlide(slideId)
64
- .then((mod) => {
65
- if (!cancelled) setSlide(mod);
66
- })
67
- .catch((e) => {
68
- if (!cancelled) setError(String(e?.message ?? e));
69
- });
70
71
  return () => {
71
- cancelled = true;
72
+ if (linkCopiedTimerRef.current) clearTimeout(linkCopiedTimerRef.current);
72
73
  };
73
- }, [slideId]);
74
+ }, []);
75
+ const { renameSlide } = useFolders();
76
+ const slideViewportRef = useRef<HTMLElement>(null);
77
+ const t = useLocale();
74
78
 
75
79
  const modulePages = useMemo(() => slide?.default ?? [], [slide]);
76
80
  const [pages, setPages] = useState<typeof modulePages>(modulePages);
@@ -221,7 +225,7 @@ export function Slide() {
221
225
  );
222
226
 
223
227
  useEffect(() => {
224
- if (playing) return;
228
+ if (playMode) return;
225
229
  const onKey = (e: KeyboardEvent) => {
226
230
  if (e.target instanceof HTMLElement && e.target.matches('input, textarea')) return;
227
231
  if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === 'PageDown') {
@@ -231,12 +235,12 @@ export function Slide() {
231
235
  e.preventDefault();
232
236
  goTo(index - 1);
233
237
  } else if (e.key === 'f' || e.key === 'F') {
234
- setPlaying(true);
238
+ setPlayMode('fullscreen');
235
239
  }
236
240
  };
237
241
  window.addEventListener('keydown', onKey);
238
242
  return () => window.removeEventListener('keydown', onKey);
239
- }, [index, goTo, playing]);
243
+ }, [index, goTo, playMode]);
240
244
 
241
245
  if (error) {
242
246
  return (
@@ -315,16 +319,17 @@ export function Slide() {
315
319
  );
316
320
  }
317
321
 
318
- if (playing) {
322
+ if (playMode) {
319
323
  return (
320
324
  <Player
321
325
  pages={pages}
322
326
  design={slide.design}
323
327
  index={index}
324
328
  onIndexChange={goTo}
325
- onExit={() => setPlaying(false)}
329
+ onExit={() => setPlayMode(null)}
326
330
  controls
327
331
  slideId={slideId}
332
+ fullscreen={playMode === 'fullscreen'}
328
333
  />
329
334
  );
330
335
  }
@@ -380,6 +385,41 @@ export function Slide() {
380
385
  </div>
381
386
 
382
387
  <div className="flex items-center gap-1">
388
+ {view === 'slides' && (
389
+ <button
390
+ type="button"
391
+ aria-label={t.slide.copyLink}
392
+ title={t.slide.copyLink}
393
+ className={cn(buttonVariants({ variant: 'ghost', size: 'icon-sm' }))}
394
+ onClick={async () => {
395
+ try {
396
+ await navigator.clipboard.writeText(window.location.href);
397
+ toast.success(t.slide.toastCopyLinkSuccess);
398
+ setLinkCopied(true);
399
+ if (linkCopiedTimerRef.current) clearTimeout(linkCopiedTimerRef.current);
400
+ linkCopiedTimerRef.current = setTimeout(() => setLinkCopied(false), 1200);
401
+ } catch (err) {
402
+ console.error('[open-slide] copy link failed', err);
403
+ toast.error(t.slide.toastCopyLinkFailed);
404
+ }
405
+ }}
406
+ >
407
+ <span className="relative grid size-4 place-items-center">
408
+ <Link2
409
+ className={cn(
410
+ 'col-start-1 row-start-1 size-4 transition-opacity duration-200',
411
+ linkCopied ? 'opacity-0' : 'opacity-100',
412
+ )}
413
+ />
414
+ <Check
415
+ className={cn(
416
+ 'col-start-1 row-start-1 size-4 transition-opacity duration-200',
417
+ linkCopied ? 'opacity-100' : 'opacity-0',
418
+ )}
419
+ />
420
+ </span>
421
+ </button>
422
+ )}
383
423
  {view === 'slides' && allowHtmlDownload && (
384
424
  <DropdownMenu>
385
425
  <DropdownMenuTrigger
@@ -417,6 +457,10 @@ export function Slide() {
417
457
  disabled={exporting}
418
458
  onSelect={async () => {
419
459
  if (!slide || exporting) return;
460
+ if (isSafari()) {
461
+ toast.error(t.slide.pdfExportSafariUnsupported, { duration: 5000 });
462
+ return;
463
+ }
420
464
  setExporting(true);
421
465
  const toastId = `pdf-export-${slideId}`;
422
466
  toast.custom(
@@ -460,18 +504,52 @@ export function Slide() {
460
504
  {view === 'slides' && <InspectToggleButton />}
461
505
  <span aria-hidden className="mx-0.5 hidden h-5 w-px bg-hairline md:block" />
462
506
  {view === 'slides' && (
463
- <Button
464
- size="sm"
465
- variant="brand"
466
- onClick={() => setPlaying(true)}
467
- className="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>
507
+ <div className="inline-flex items-stretch">
508
+ <Button
509
+ size="sm"
510
+ variant="brand"
511
+ onClick={() => setPlayMode('fullscreen')}
512
+ className="rounded-r-none px-2.5 md:px-3"
513
+ >
514
+ <Play className="size-3.5 fill-current" />
515
+ <span className="hidden md:inline">{t.slide.present}</span>
516
+ <kbd className="ml-1 hidden rounded-[3px] bg-brand-foreground/15 px-1 font-mono text-[9.5px] tracking-[0.04em] md:inline">
517
+ F
518
+ </kbd>
519
+ </Button>
520
+ <DropdownMenu>
521
+ <DropdownMenuTrigger
522
+ type="button"
523
+ aria-label={t.slide.presentMenuAria}
524
+ title={t.slide.presentMenuAria}
525
+ className={cn(
526
+ buttonVariants({ variant: 'brand', size: 'sm' }),
527
+ '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)]',
528
+ )}
529
+ >
530
+ <ChevronDown className="size-3.5" />
531
+ </DropdownMenuTrigger>
532
+ <DropdownMenuContent align="end" className="min-w-[200px]">
533
+ <DropdownMenuItem onSelect={() => setPlayMode('window')}>
534
+ <Play />
535
+ {t.slide.presentInWindow}
536
+ </DropdownMenuItem>
537
+ <DropdownMenuItem onSelect={() => setPlayMode('fullscreen')}>
538
+ <Maximize />
539
+ {t.slide.presentFullscreen}
540
+ </DropdownMenuItem>
541
+ <DropdownMenuItem
542
+ onSelect={() => {
543
+ if (slideId) openPresenterWindow(slideId);
544
+ setPlayMode('window');
545
+ }}
546
+ >
547
+ <MonitorSpeaker />
548
+ {t.slide.presentPresenter}
549
+ </DropdownMenuItem>
550
+ </DropdownMenuContent>
551
+ </DropdownMenu>
552
+ </div>
475
553
  )}
476
554
  </div>
477
555
  </header>
@@ -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
@@ -39,6 +39,7 @@ export const en: Locale = {
39
39
  appTitle: 'open-slide',
40
40
  draft: 'Draft',
41
41
  themes: 'Themes',
42
+ assets: 'Assets',
42
43
  folders: 'Folders',
43
44
  newFolder: 'New folder',
44
45
  folderName: 'Folder name',
@@ -48,6 +49,11 @@ export const en: Locale = {
48
49
  folderActions: 'Folder actions',
49
50
  searchPlaceholder: 'Search slides',
50
51
  clearSearch: 'Clear search',
52
+ sortLabel: 'Sort',
53
+ sortByCreatedDesc: 'Newest',
54
+ sortByCreatedAsc: 'Oldest',
55
+ sortByTitleAsc: 'A–Z',
56
+ sortByTitleDesc: 'Z–A',
51
57
  noMatches: 'No matches',
52
58
  nothingMatchesPrefix: 'Nothing matches ',
53
59
  nothingMatchesSuffix: ' in this folder.',
@@ -85,15 +91,24 @@ export const en: Locale = {
85
91
  backToHome: 'Back to home',
86
92
  agentConnected: 'Agent connected',
87
93
  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.',
94
+ 'The current slide and inspector selection are synced to your agent in real time.',
89
95
  agentDisconnected: 'Agent disconnected',
90
96
  agentDisconnectedTooltip:
91
97
  '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.',
92
98
  download: 'Download',
99
+ copyLink: 'Copy link',
100
+ toastCopyLinkSuccess: 'Link copied to clipboard',
101
+ toastCopyLinkFailed: 'Failed to copy link',
93
102
  exportAsHtml: 'Export as HTML',
94
103
  exportAsPdf: 'Export as PDF',
95
104
  pdfExportFailed: 'PDF export failed',
105
+ pdfExportSafariUnsupported:
106
+ 'Export as PDF is not supported on Safari. Please try a Chromium-based browser instead.',
96
107
  present: 'Present',
108
+ presentMenuAria: 'Present options',
109
+ presentInWindow: 'Play',
110
+ presentFullscreen: 'Fullscreen',
111
+ presentPresenter: 'Presenter mode',
97
112
  slidesTab: 'Slides',
98
113
  assetsTab: 'Assets',
99
114
  renameSlide: 'Rename slide',
@@ -137,6 +152,8 @@ export const en: Locale = {
137
152
  whiteoutAria: 'White screen (W)',
138
153
  laserAria: 'Laser pointer (L)',
139
154
  presenterAria: 'Presenter view (P)',
155
+ enterFullscreenAria: 'Enter fullscreen',
156
+ exitFullscreenAria: 'Exit fullscreen',
140
157
  helpAria: 'Keyboard shortcuts (?)',
141
158
  exitAria: 'Exit (Esc)',
142
159
  elapsedTime: 'Elapsed time',
@@ -163,8 +180,7 @@ export const en: Locale = {
163
180
  inspect: 'Inspect',
164
181
  deselect: 'Deselect',
165
182
  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.',
183
+ agentWatchingTooltip: 'The selected element is synced to your agent in real time.',
168
184
  agentNotWatching: 'Agent not watching',
169
185
  agentNotWatchingTooltip:
170
186
  'Lost connection to the dev server, so your agent can no longer see the selected element. Restart the dev server to restore the connection.',
@@ -206,7 +222,7 @@ export const en: Locale = {
206
222
  cropResetAria: 'Reset crop',
207
223
  leaveComment: 'Leave a comment',
208
224
  commentPlaceholder: 'Describe a change for the agent…',
209
- commentShortcutHint: '⌘↵ to add',
225
+ commentShortcutHint: '⌘/ to focus · ⌘↵ to add',
210
226
  addComment: 'Add comment',
211
227
  unsavedChanges: {
212
228
  one: '{count} unsaved change',
@@ -251,6 +267,8 @@ export const en: Locale = {
251
267
  devOnlyMessage: 'Asset management is only available in dev mode.',
252
268
  sectionAria: 'Slide assets',
253
269
  eyebrow: 'Assets',
270
+ scopeSlide: 'This slide',
271
+ scopeGlobal: 'Global',
254
272
  fileCount: { one: '{count} file', other: '{count} files' },
255
273
  searchLogos: 'Search logos',
256
274
  upload: 'Upload',
@@ -266,11 +284,16 @@ export const en: Locale = {
266
284
  renameMenuItem: 'Rename',
267
285
  deleteMenuItem: 'Delete',
268
286
  conflictTitle: 'File already exists',
269
- conflictDescription: "{name} is already in this slide's assets folder.",
287
+ conflictDescription: '{name} is already in the assets folder.',
270
288
  conflictReplace: 'Replace',
271
289
  conflictRenameCopy: 'Rename copy',
272
290
  deleteAssetTitle: 'Delete asset',
273
291
  deleteAssetDescription: 'Delete {name}? Imports referencing this file in the slide will break.',
292
+ deleteAssetInUseDescription: '{name} is used in {count} place(s) across {slides} slide(s).',
293
+ deleteAssetInUseHint: 'Deleting will revert these usages back to image placeholders.',
294
+ deleteAndRevert: 'Delete & revert',
295
+ toastRevertFailed: "Couldn't revert usage in {slideId}",
296
+ toastDeletedWithRevert: 'Deleted {name} and reverted {count} usage(s)',
274
297
  noPreview: 'No preview available',
275
298
  importHintComment: 'import asset from ',
276
299
  importHintSemi: ';',
package/src/locale/ja.ts CHANGED
@@ -39,6 +39,7 @@ export const ja: Locale = {
39
39
  appTitle: 'open-slide',
40
40
  draft: '下書き',
41
41
  themes: 'テーマ',
42
+ assets: 'アセット',
42
43
  folders: 'フォルダ',
43
44
  newFolder: '新規フォルダ',
44
45
  folderName: 'フォルダ名',
@@ -48,6 +49,11 @@ export const ja: Locale = {
48
49
  folderActions: 'フォルダ操作',
49
50
  searchPlaceholder: 'スライドを検索',
50
51
  clearSearch: '検索をクリア',
52
+ sortLabel: '並べ替え',
53
+ sortByCreatedDesc: '新しい順',
54
+ sortByCreatedAsc: '古い順',
55
+ sortByTitleAsc: 'A–Z',
56
+ sortByTitleDesc: 'Z–A',
51
57
  noMatches: '一致なし',
52
58
  nothingMatchesPrefix: 'このフォルダ内に ',
53
59
  nothingMatchesSuffix: ' に一致する項目はありません。',
@@ -83,17 +89,26 @@ export const ja: Locale = {
83
89
  slide: {
84
90
  agentConnected: 'エージェント接続中',
85
91
  agentConnectedTooltip:
86
- '現在のスライドと Inspector の選択状態を dev server がエージェントに公開しています。チャットで「このスライド」「この要素」と言えば認識されます。本番ビルドでは表示されません。',
92
+ '現在のスライドと Inspector の選択はエージェントにリアルタイムで同期されています。',
87
93
  agentDisconnected: 'エージェント切断',
88
94
  agentDisconnectedTooltip:
89
95
  'dev server との接続が切れたため、現在のスライドや Inspector の選択がエージェントに届かなくなっています。dev server を再起動して接続を復旧してください。',
90
96
  home: 'ホーム',
91
97
  backToHome: 'ホームへ戻る',
92
98
  download: 'ダウンロード',
99
+ copyLink: 'リンクをコピー',
100
+ toastCopyLinkSuccess: 'リンクをクリップボードにコピーしました',
101
+ toastCopyLinkFailed: 'リンクのコピーに失敗しました',
93
102
  exportAsHtml: 'HTML として書き出し',
94
103
  exportAsPdf: 'PDF として書き出し',
95
104
  pdfExportFailed: 'PDF の書き出しに失敗しました',
105
+ pdfExportSafariUnsupported:
106
+ 'PDF の書き出しは現在 Safari では対応していません。Chromium ベースのブラウザでお試しください。',
96
107
  present: '発表',
108
+ presentMenuAria: '発表オプション',
109
+ presentInWindow: '再生',
110
+ presentFullscreen: 'フルスクリーン再生',
111
+ presentPresenter: '発表者モード',
97
112
  slidesTab: 'スライド',
98
113
  assetsTab: 'アセット',
99
114
  renameSlide: 'スライドの名前を変更',
@@ -137,6 +152,8 @@ export const ja: Locale = {
137
152
  whiteoutAria: '白い画面 (W)',
138
153
  laserAria: 'レーザーポインタ (L)',
139
154
  presenterAria: '発表者ビュー (P)',
155
+ enterFullscreenAria: 'フルスクリーンへ',
156
+ exitFullscreenAria: 'フルスクリーン解除',
140
157
  helpAria: 'キーボードショートカット (?)',
141
158
  exitAria: '終了 (Esc)',
142
159
  elapsedTime: '経過時間',
@@ -200,14 +217,13 @@ export const ja: Locale = {
200
217
  cropApply: '適用',
201
218
  cropResetAria: 'トリミングをリセット',
202
219
  agentWatching: 'エージェント監視中',
203
- agentWatchingTooltip:
204
- 'エージェントは選択中の要素を dev server 経由で把握しています。直接チャットで頼めます。ここにコメントを残すのは、複数の依頼をまとめて出したいときだけで OK。',
220
+ agentWatchingTooltip: '選択中の要素はエージェントにリアルタイムで同期されています。',
205
221
  agentNotWatching: 'エージェント未接続',
206
222
  agentNotWatchingTooltip:
207
223
  'dev server との接続が切れたため、選択中の要素がエージェントに見えなくなっています。dev server を再起動して接続を復旧してください。',
208
224
  leaveComment: 'コメントを残す',
209
225
  commentPlaceholder: 'エージェントに依頼する変更を記述…',
210
- commentShortcutHint: '⌘↵ で追加',
226
+ commentShortcutHint: '⌘/ でフォーカス · ⌘↵ で追加',
211
227
  addComment: 'コメントを追加',
212
228
  unsavedChanges: {
213
229
  one: '未保存の変更 {count} 件',
@@ -253,6 +269,8 @@ export const ja: Locale = {
253
269
  devOnlyMessage: 'アセット管理は開発モードでのみ利用できます。',
254
270
  sectionAria: 'スライドのアセット',
255
271
  eyebrow: 'アセット',
272
+ scopeSlide: 'このスライド',
273
+ scopeGlobal: 'グローバル',
256
274
  fileCount: { one: 'ファイル {count} 件', other: 'ファイル {count} 件' },
257
275
  searchLogos: 'ロゴを検索',
258
276
  upload: 'アップロード',
@@ -268,12 +286,17 @@ export const ja: Locale = {
268
286
  renameMenuItem: '名前を変更',
269
287
  deleteMenuItem: '削除',
270
288
  conflictTitle: 'ファイルがすでに存在します',
271
- conflictDescription: '{name} はすでにこのスライドのアセットフォルダにあります。',
289
+ conflictDescription: '{name} はすでにアセットフォルダにあります。',
272
290
  conflictReplace: '置き換え',
273
291
  conflictRenameCopy: 'コピーをリネーム',
274
292
  deleteAssetTitle: 'アセットを削除',
275
293
  deleteAssetDescription:
276
294
  '{name} を削除しますか?スライド内でこのファイルを参照しているインポートは壊れます。',
295
+ deleteAssetInUseDescription: '{name} は {slides} 枚のスライドで {count} 箇所使用されています。',
296
+ deleteAssetInUseHint: '削除すると、これらの使用箇所は画像プレースホルダーに戻ります。',
297
+ deleteAndRevert: '削除して戻す',
298
+ toastRevertFailed: '{slideId} の使用箇所を戻せませんでした',
299
+ toastDeletedWithRevert: '{name} を削除し、{count} 箇所をプレースホルダーに戻しました',
277
300
  noPreview: 'プレビューはありません',
278
301
  importHintComment: 'import asset from ',
279
302
  importHintSemi: ';',
@@ -39,6 +39,7 @@ export type Locale = {
39
39
  appTitle: string;
40
40
  draft: string;
41
41
  themes: string;
42
+ assets: string;
42
43
  folders: string;
43
44
  newFolder: string;
44
45
  folderName: string;
@@ -48,6 +49,11 @@ export type Locale = {
48
49
  folderActions: string;
49
50
  searchPlaceholder: string;
50
51
  clearSearch: string;
52
+ sortLabel: string;
53
+ sortByCreatedDesc: string;
54
+ sortByCreatedAsc: string;
55
+ sortByTitleAsc: string;
56
+ sortByTitleDesc: string;
51
57
  noMatches: string;
52
58
  nothingMatchesPrefix: string;
53
59
  nothingMatchesSuffix: string;
@@ -91,10 +97,18 @@ export type Locale = {
91
97
  agentDisconnected: string;
92
98
  agentDisconnectedTooltip: string;
93
99
  download: string;
100
+ copyLink: string;
101
+ toastCopyLinkSuccess: string;
102
+ toastCopyLinkFailed: string;
94
103
  exportAsHtml: string;
95
104
  exportAsPdf: string;
96
105
  pdfExportFailed: string;
106
+ pdfExportSafariUnsupported: string;
97
107
  present: string;
108
+ presentMenuAria: string;
109
+ presentInWindow: string;
110
+ presentFullscreen: string;
111
+ presentPresenter: string;
98
112
  slidesTab: string;
99
113
  assetsTab: string;
100
114
  renameSlide: string;
@@ -139,6 +153,8 @@ export type Locale = {
139
153
  whiteoutAria: string;
140
154
  laserAria: string;
141
155
  presenterAria: string;
156
+ enterFullscreenAria: string;
157
+ exitFullscreenAria: string;
142
158
  helpAria: string;
143
159
  exitAria: string;
144
160
  elapsedTime: string;
@@ -251,6 +267,8 @@ export type Locale = {
251
267
  devOnlyMessage: string;
252
268
  sectionAria: string;
253
269
  eyebrow: string;
270
+ scopeSlide: string;
271
+ scopeGlobal: string;
254
272
  /** templates: "{count} file" / "{count} files" */
255
273
  fileCount: Plural;
256
274
  searchLogos: string;
@@ -269,13 +287,21 @@ export type Locale = {
269
287
  renameMenuItem: string;
270
288
  deleteMenuItem: string;
271
289
  conflictTitle: string;
272
- /** template: "{name} is already in this slide's assets folder." */
290
+ /** template: "{name} is already in the assets folder." */
273
291
  conflictDescription: string;
274
292
  conflictReplace: string;
275
293
  conflictRenameCopy: string;
276
294
  deleteAssetTitle: string;
277
295
  /** template: "Delete {name}? Imports referencing this file in the slide will break." */
278
296
  deleteAssetDescription: string;
297
+ /** template: "{name} is used in {count} place across {slides} slide." (singular/plural via {count}/{slides}) */
298
+ deleteAssetInUseDescription: string;
299
+ deleteAssetInUseHint: string;
300
+ deleteAndRevert: string;
301
+ /** template: "Couldn't revert usage in {slideId}." */
302
+ toastRevertFailed: string;
303
+ /** template: "Deleted {name} and reverted {count} usage." */
304
+ toastDeletedWithRevert: string;
279
305
  noPreview: string;
280
306
  importHintComment: string;
281
307
  importHintSemi: string;
@@ -39,6 +39,7 @@ export const zhCN: Locale = {
39
39
  appTitle: 'open-slide',
40
40
  draft: '草稿',
41
41
  themes: '主题',
42
+ assets: '素材',
42
43
  folders: '文件夹',
43
44
  newFolder: '新建文件夹',
44
45
  folderName: '文件夹名称',
@@ -48,6 +49,11 @@ export const zhCN: Locale = {
48
49
  folderActions: '文件夹操作',
49
50
  searchPlaceholder: '搜索幻灯片',
50
51
  clearSearch: '清除搜索',
52
+ sortLabel: '排序',
53
+ sortByCreatedDesc: '最新',
54
+ sortByCreatedAsc: '最旧',
55
+ sortByTitleAsc: 'A–Z',
56
+ sortByTitleDesc: 'Z–A',
51
57
  noMatches: '没有匹配项',
52
58
  nothingMatchesPrefix: '该文件夹中没有匹配 ',
53
59
  nothingMatchesSuffix: ' 的内容。',
@@ -82,18 +88,26 @@ export const zhCN: Locale = {
82
88
 
83
89
  slide: {
84
90
  agentConnected: 'Agent 已连接',
85
- agentConnectedTooltip:
86
- 'Dev server 正在把你目前在哪张 slide、Inspector 选了哪个元素发布给 agent。直接到聊天说"这张 slide"或"这个元素"就行。Production build 不会出现。',
91
+ agentConnectedTooltip: '目前的 slide 与 Inspector 选择会即时同步给 agent。',
87
92
  agentDisconnected: 'Agent 已断开',
88
93
  agentDisconnectedTooltip:
89
94
  '已和 dev server 断开连接,agent 没办法再看到你目前的 slide 或 Inspector 选择。请重新启动 dev server 来恢复连接。',
90
95
  home: '首页',
91
96
  backToHome: '返回首页',
92
97
  download: '下载',
98
+ copyLink: '复制链接',
99
+ toastCopyLinkSuccess: '已复制链接到剪贴板',
100
+ toastCopyLinkFailed: '复制链接失败',
93
101
  exportAsHtml: '导出为 HTML',
94
102
  exportAsPdf: '导出为 PDF',
95
103
  pdfExportFailed: 'PDF 导出失败',
104
+ pdfExportSafariUnsupported:
105
+ '导出 PDF 目前不支持 Safari 设备,请尝试使用基于 Chromium 的浏览器替代。',
96
106
  present: '演示',
107
+ presentMenuAria: '演示选项',
108
+ presentInWindow: '播放',
109
+ presentFullscreen: '全屏播放',
110
+ presentPresenter: '演讲者模式',
97
111
  slidesTab: '幻灯片',
98
112
  assetsTab: '素材',
99
113
  renameSlide: '重命名幻灯片',
@@ -137,6 +151,8 @@ export const zhCN: Locale = {
137
151
  whiteoutAria: '白屏 (W)',
138
152
  laserAria: '激光笔 (L)',
139
153
  presenterAria: '演讲者视图 (P)',
154
+ enterFullscreenAria: '进入全屏',
155
+ exitFullscreenAria: '退出全屏',
140
156
  helpAria: '键盘快捷键 (?)',
141
157
  exitAria: '退出 (Esc)',
142
158
  elapsedTime: '已用时',
@@ -199,14 +215,13 @@ export const zhCN: Locale = {
199
215
  cropApply: '应用',
200
216
  cropResetAria: '重置裁剪',
201
217
  agentWatching: 'Agent 正在关注',
202
- agentWatchingTooltip:
203
- 'Agent 已经通过 dev server 看到你选的元素了,直接到聊天请它修改就行。想累积几个再一次问才需要在这里留 comments。',
218
+ agentWatchingTooltip: '选取的元素会即时同步给 agent。',
204
219
  agentNotWatching: 'Agent 没在关注',
205
220
  agentNotWatchingTooltip:
206
221
  '已和 dev server 断开连接,agent 看不到你选的元素了。请重新启动 dev server 来恢复连接。',
207
222
  leaveComment: '留个 comment',
208
223
  commentPlaceholder: '描述你希望代理执行的更改…',
209
- commentShortcutHint: '⌘↵ 添加',
224
+ commentShortcutHint: '⌘/ 聚焦 · ⌘↵ 添加',
210
225
  addComment: '添加 comment',
211
226
  unsavedChanges: {
212
227
  one: '{count} 项未保存的更改',
@@ -251,6 +266,8 @@ export const zhCN: Locale = {
251
266
  devOnlyMessage: '素材管理仅在开发模式下可用。',
252
267
  sectionAria: '幻灯片素材',
253
268
  eyebrow: '素材',
269
+ scopeSlide: '当前幻灯片',
270
+ scopeGlobal: '全局',
254
271
  fileCount: { one: '{count} 个文件', other: '{count} 个文件' },
255
272
  searchLogos: '搜索 Logo',
256
273
  upload: '上传',
@@ -266,11 +283,16 @@ export const zhCN: Locale = {
266
283
  renameMenuItem: '重命名',
267
284
  deleteMenuItem: '删除',
268
285
  conflictTitle: '文件已存在',
269
- conflictDescription: '{name} 已在该幻灯片的素材文件夹中。',
286
+ conflictDescription: '{name} 已在素材文件夹中。',
270
287
  conflictReplace: '替换',
271
288
  conflictRenameCopy: '重命名副本',
272
289
  deleteAssetTitle: '删除素材',
273
290
  deleteAssetDescription: '要删除 {name} 吗?幻灯片中引用此文件的导入将失效。',
291
+ deleteAssetInUseDescription: '{name} 在 {slides} 个幻灯片中被使用了 {count} 次。',
292
+ deleteAssetInUseHint: '删除后这些使用处会自动还原为图片占位符。',
293
+ deleteAndRevert: '删除并还原',
294
+ toastRevertFailed: '无法还原 {slideId} 中的使用',
295
+ toastDeletedWithRevert: '已删除 {name} 并还原 {count} 个使用处',
274
296
  noPreview: '无预览',
275
297
  importHintComment: 'import asset from ',
276
298
  importHintSemi: ';',