@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
@@ -1,5 +1,5 @@
1
1
  import "./design-cpzS8aud.js";
2
- import { createViteConfig } from "./config-BAwKWNtW.js";
2
+ import { createViteConfig } from "./config-iKjqaX08.js";
3
3
  import { createServer, mergeConfig } from "vite";
4
4
 
5
5
  //#region src/cli/dev.ts
@@ -35,6 +35,7 @@ const en = {
35
35
  appTitle: "open-slide",
36
36
  draft: "Draft",
37
37
  themes: "Themes",
38
+ assets: "Assets",
38
39
  folders: "Folders",
39
40
  newFolder: "New folder",
40
41
  folderName: "Folder name",
@@ -44,6 +45,11 @@ const en = {
44
45
  folderActions: "Folder actions",
45
46
  searchPlaceholder: "Search slides",
46
47
  clearSearch: "Clear search",
48
+ sortLabel: "Sort",
49
+ sortByCreatedDesc: "Newest",
50
+ sortByCreatedAsc: "Oldest",
51
+ sortByTitleAsc: "A–Z",
52
+ sortByTitleDesc: "Z–A",
47
53
  noMatches: "No matches",
48
54
  nothingMatchesPrefix: "Nothing matches ",
49
55
  nothingMatchesSuffix: " in this folder.",
@@ -79,14 +85,22 @@ const en = {
79
85
  home: "Home",
80
86
  backToHome: "Back to home",
81
87
  agentConnected: "Agent connected",
82
- agentConnectedTooltip: "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.",
88
+ agentConnectedTooltip: "The current slide and inspector selection are synced to your agent in real time.",
83
89
  agentDisconnected: "Agent disconnected",
84
90
  agentDisconnectedTooltip: "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.",
85
91
  download: "Download",
92
+ copyLink: "Copy link",
93
+ toastCopyLinkSuccess: "Link copied to clipboard",
94
+ toastCopyLinkFailed: "Failed to copy link",
86
95
  exportAsHtml: "Export as HTML",
87
96
  exportAsPdf: "Export as PDF",
88
97
  pdfExportFailed: "PDF export failed",
98
+ pdfExportSafariUnsupported: "Export as PDF is not supported on Safari. Please try a Chromium-based browser instead.",
89
99
  present: "Present",
100
+ presentMenuAria: "Present options",
101
+ presentInWindow: "Play",
102
+ presentFullscreen: "Fullscreen",
103
+ presentPresenter: "Presenter mode",
90
104
  slidesTab: "Slides",
91
105
  assetsTab: "Assets",
92
106
  renameSlide: "Rename slide",
@@ -128,6 +142,8 @@ const en = {
128
142
  whiteoutAria: "White screen (W)",
129
143
  laserAria: "Laser pointer (L)",
130
144
  presenterAria: "Presenter view (P)",
145
+ enterFullscreenAria: "Enter fullscreen",
146
+ exitFullscreenAria: "Exit fullscreen",
131
147
  helpAria: "Keyboard shortcuts (?)",
132
148
  exitAria: "Exit (Esc)",
133
149
  elapsedTime: "Elapsed time",
@@ -153,7 +169,7 @@ const en = {
153
169
  inspect: "Inspect",
154
170
  deselect: "Deselect",
155
171
  agentWatching: "Agent is watching",
156
- agentWatchingTooltip: "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.",
172
+ agentWatchingTooltip: "The selected element is synced to your agent in real time.",
157
173
  agentNotWatching: "Agent not watching",
158
174
  agentNotWatchingTooltip: "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
175
  contentSection: "Content",
@@ -194,7 +210,7 @@ const en = {
194
210
  cropResetAria: "Reset crop",
195
211
  leaveComment: "Leave a comment",
196
212
  commentPlaceholder: "Describe a change for the agent…",
197
- commentShortcutHint: "⌘↵ to add",
213
+ commentShortcutHint: "⌘/ to focus · ⌘↵ to add",
198
214
  addComment: "Add comment",
199
215
  unsavedChanges: {
200
216
  one: "{count} unsaved change",
@@ -237,6 +253,8 @@ const en = {
237
253
  devOnlyMessage: "Asset management is only available in dev mode.",
238
254
  sectionAria: "Slide assets",
239
255
  eyebrow: "Assets",
256
+ scopeSlide: "This slide",
257
+ scopeGlobal: "Global",
240
258
  fileCount: {
241
259
  one: "{count} file",
242
260
  other: "{count} files"
@@ -255,11 +273,16 @@ const en = {
255
273
  renameMenuItem: "Rename",
256
274
  deleteMenuItem: "Delete",
257
275
  conflictTitle: "File already exists",
258
- conflictDescription: "{name} is already in this slide's assets folder.",
276
+ conflictDescription: "{name} is already in the assets folder.",
259
277
  conflictReplace: "Replace",
260
278
  conflictRenameCopy: "Rename copy",
261
279
  deleteAssetTitle: "Delete asset",
262
280
  deleteAssetDescription: "Delete {name}? Imports referencing this file in the slide will break.",
281
+ deleteAssetInUseDescription: "{name} is used in {count} place(s) across {slides} slide(s).",
282
+ deleteAssetInUseHint: "Deleting will revert these usages back to image placeholders.",
283
+ deleteAndRevert: "Delete & revert",
284
+ toastRevertFailed: "Couldn't revert usage in {slideId}",
285
+ toastDeletedWithRevert: "Deleted {name} and reverted {count} usage(s)",
263
286
  noPreview: "No preview available",
264
287
  importHintComment: "import asset from ",
265
288
  importHintSemi: ";",
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { Locale, Plural } from "./types-JYG1cmwC.js";
2
- import { OpenSlideConfig } from "./config-D9cZ1A0X.js";
1
+ import { Locale, Plural } from "./types-Dpr8nbih.js";
2
+ import { OpenSlideConfig } from "./config-BQdTMho4.js";
3
3
  import { CSSProperties, ComponentType, HTMLAttributes } from "react";
4
4
  import * as react_jsx_runtime0 from "react/jsx-runtime";
5
5
 
@@ -51,6 +51,8 @@ type Page = ComponentType;
51
51
  type SlideMeta = {
52
52
  title?: string;
53
53
  theme?: string;
54
+ /** ISO 8601 timestamp. Set once at scaffold time; used to sort the slide list. */
55
+ createdAt?: string;
54
56
  };
55
57
  type SlideModule = {
56
58
  default: Page[];
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { en } from "./en-CDKzoZvf.js";
1
+ import { en } from "./en-DDGqyNaW.js";
2
2
  import { cssVarsToString, defaultDesign, designToCssVars } from "./design-cpzS8aud.js";
3
3
  import { useRef, useState } from "react";
4
4
  import { toast } from "sonner";
@@ -1,4 +1,4 @@
1
- import { Locale, Plural } from "../types-JYG1cmwC.js";
1
+ import { Locale, Plural } from "../types-Dpr8nbih.js";
2
2
 
3
3
  //#region src/locale/en.d.ts
4
4
  declare const en: Locale;
@@ -1,4 +1,4 @@
1
- import { en } from "../en-CDKzoZvf.js";
1
+ import { en } from "../en-DDGqyNaW.js";
2
2
 
3
3
  //#region src/locale/format.ts
4
4
  function format(template, vars) {
@@ -49,6 +49,7 @@ const ja = {
49
49
  appTitle: "open-slide",
50
50
  draft: "下書き",
51
51
  themes: "テーマ",
52
+ assets: "アセット",
52
53
  folders: "フォルダ",
53
54
  newFolder: "新規フォルダ",
54
55
  folderName: "フォルダ名",
@@ -58,6 +59,11 @@ const ja = {
58
59
  folderActions: "フォルダ操作",
59
60
  searchPlaceholder: "スライドを検索",
60
61
  clearSearch: "検索をクリア",
62
+ sortLabel: "並べ替え",
63
+ sortByCreatedDesc: "新しい順",
64
+ sortByCreatedAsc: "古い順",
65
+ sortByTitleAsc: "A–Z",
66
+ sortByTitleDesc: "Z–A",
61
67
  noMatches: "一致なし",
62
68
  nothingMatchesPrefix: "このフォルダ内に ",
63
69
  nothingMatchesSuffix: " に一致する項目はありません。",
@@ -91,16 +97,24 @@ const ja = {
91
97
  },
92
98
  slide: {
93
99
  agentConnected: "エージェント接続中",
94
- agentConnectedTooltip: "現在のスライドと Inspector の選択状態を dev server がエージェントに公開しています。チャットで「このスライド」「この要素」と言えば認識されます。本番ビルドでは表示されません。",
100
+ agentConnectedTooltip: "現在のスライドと Inspector の選択はエージェントにリアルタイムで同期されています。",
95
101
  agentDisconnected: "エージェント切断",
96
102
  agentDisconnectedTooltip: "dev server との接続が切れたため、現在のスライドや Inspector の選択がエージェントに届かなくなっています。dev server を再起動して接続を復旧してください。",
97
103
  home: "ホーム",
98
104
  backToHome: "ホームへ戻る",
99
105
  download: "ダウンロード",
106
+ copyLink: "リンクをコピー",
107
+ toastCopyLinkSuccess: "リンクをクリップボードにコピーしました",
108
+ toastCopyLinkFailed: "リンクのコピーに失敗しました",
100
109
  exportAsHtml: "HTML として書き出し",
101
110
  exportAsPdf: "PDF として書き出し",
102
111
  pdfExportFailed: "PDF の書き出しに失敗しました",
112
+ pdfExportSafariUnsupported: "PDF の書き出しは現在 Safari では対応していません。Chromium ベースのブラウザでお試しください。",
103
113
  present: "発表",
114
+ presentMenuAria: "発表オプション",
115
+ presentInWindow: "再生",
116
+ presentFullscreen: "フルスクリーン再生",
117
+ presentPresenter: "発表者モード",
104
118
  slidesTab: "スライド",
105
119
  assetsTab: "アセット",
106
120
  renameSlide: "スライドの名前を変更",
@@ -142,6 +156,8 @@ const ja = {
142
156
  whiteoutAria: "白い画面 (W)",
143
157
  laserAria: "レーザーポインタ (L)",
144
158
  presenterAria: "発表者ビュー (P)",
159
+ enterFullscreenAria: "フルスクリーンへ",
160
+ exitFullscreenAria: "フルスクリーン解除",
145
161
  helpAria: "キーボードショートカット (?)",
146
162
  exitAria: "終了 (Esc)",
147
163
  elapsedTime: "経過時間",
@@ -203,12 +219,12 @@ const ja = {
203
219
  cropApply: "適用",
204
220
  cropResetAria: "トリミングをリセット",
205
221
  agentWatching: "エージェント監視中",
206
- agentWatchingTooltip: "エージェントは選択中の要素を dev server 経由で把握しています。直接チャットで頼めます。ここにコメントを残すのは、複数の依頼をまとめて出したいときだけで OK。",
222
+ agentWatchingTooltip: "選択中の要素はエージェントにリアルタイムで同期されています。",
207
223
  agentNotWatching: "エージェント未接続",
208
224
  agentNotWatchingTooltip: "dev server との接続が切れたため、選択中の要素がエージェントに見えなくなっています。dev server を再起動して接続を復旧してください。",
209
225
  leaveComment: "コメントを残す",
210
226
  commentPlaceholder: "エージェントに依頼する変更を記述…",
211
- commentShortcutHint: "⌘↵ で追加",
227
+ commentShortcutHint: "⌘/ でフォーカス · ⌘↵ で追加",
212
228
  addComment: "コメントを追加",
213
229
  unsavedChanges: {
214
230
  one: "未保存の変更 {count} 件",
@@ -251,6 +267,8 @@ const ja = {
251
267
  devOnlyMessage: "アセット管理は開発モードでのみ利用できます。",
252
268
  sectionAria: "スライドのアセット",
253
269
  eyebrow: "アセット",
270
+ scopeSlide: "このスライド",
271
+ scopeGlobal: "グローバル",
254
272
  fileCount: {
255
273
  one: "ファイル {count} 件",
256
274
  other: "ファイル {count} 件"
@@ -269,11 +287,16 @@ const ja = {
269
287
  renameMenuItem: "名前を変更",
270
288
  deleteMenuItem: "削除",
271
289
  conflictTitle: "ファイルがすでに存在します",
272
- conflictDescription: "{name} はすでにこのスライドのアセットフォルダにあります。",
290
+ conflictDescription: "{name} はすでにアセットフォルダにあります。",
273
291
  conflictReplace: "置き換え",
274
292
  conflictRenameCopy: "コピーをリネーム",
275
293
  deleteAssetTitle: "アセットを削除",
276
294
  deleteAssetDescription: "{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: ";",
@@ -399,6 +422,7 @@ const zhCN = {
399
422
  appTitle: "open-slide",
400
423
  draft: "草稿",
401
424
  themes: "主题",
425
+ assets: "素材",
402
426
  folders: "文件夹",
403
427
  newFolder: "新建文件夹",
404
428
  folderName: "文件夹名称",
@@ -408,6 +432,11 @@ const zhCN = {
408
432
  folderActions: "文件夹操作",
409
433
  searchPlaceholder: "搜索幻灯片",
410
434
  clearSearch: "清除搜索",
435
+ sortLabel: "排序",
436
+ sortByCreatedDesc: "最新",
437
+ sortByCreatedAsc: "最旧",
438
+ sortByTitleAsc: "A–Z",
439
+ sortByTitleDesc: "Z–A",
411
440
  noMatches: "没有匹配项",
412
441
  nothingMatchesPrefix: "该文件夹中没有匹配 ",
413
442
  nothingMatchesSuffix: " 的内容。",
@@ -441,16 +470,24 @@ const zhCN = {
441
470
  },
442
471
  slide: {
443
472
  agentConnected: "Agent 已连接",
444
- agentConnectedTooltip: "Dev server 正在把你目前在哪张 slide、Inspector 选了哪个元素发布给 agent。直接到聊天说\"这张 slide\"或\"这个元素\"就行。Production build 不会出现。",
473
+ agentConnectedTooltip: "目前的 slide Inspector 选择会即时同步给 agent",
445
474
  agentDisconnected: "Agent 已断开",
446
475
  agentDisconnectedTooltip: "已和 dev server 断开连接,agent 没办法再看到你目前的 slide 或 Inspector 选择。请重新启动 dev server 来恢复连接。",
447
476
  home: "首页",
448
477
  backToHome: "返回首页",
449
478
  download: "下载",
479
+ copyLink: "复制链接",
480
+ toastCopyLinkSuccess: "已复制链接到剪贴板",
481
+ toastCopyLinkFailed: "复制链接失败",
450
482
  exportAsHtml: "导出为 HTML",
451
483
  exportAsPdf: "导出为 PDF",
452
484
  pdfExportFailed: "PDF 导出失败",
485
+ pdfExportSafariUnsupported: "导出 PDF 目前不支持 Safari 设备,请尝试使用基于 Chromium 的浏览器替代。",
453
486
  present: "演示",
487
+ presentMenuAria: "演示选项",
488
+ presentInWindow: "播放",
489
+ presentFullscreen: "全屏播放",
490
+ presentPresenter: "演讲者模式",
454
491
  slidesTab: "幻灯片",
455
492
  assetsTab: "素材",
456
493
  renameSlide: "重命名幻灯片",
@@ -492,6 +529,8 @@ const zhCN = {
492
529
  whiteoutAria: "白屏 (W)",
493
530
  laserAria: "激光笔 (L)",
494
531
  presenterAria: "演讲者视图 (P)",
532
+ enterFullscreenAria: "进入全屏",
533
+ exitFullscreenAria: "退出全屏",
495
534
  helpAria: "键盘快捷键 (?)",
496
535
  exitAria: "退出 (Esc)",
497
536
  elapsedTime: "已用时",
@@ -553,12 +592,12 @@ const zhCN = {
553
592
  cropApply: "应用",
554
593
  cropResetAria: "重置裁剪",
555
594
  agentWatching: "Agent 正在关注",
556
- agentWatchingTooltip: "Agent 已经通过 dev server 看到你选的元素了,直接到聊天请它修改就行。想累积几个再一次问才需要在这里留 comments。",
595
+ agentWatchingTooltip: "选取的元素会即时同步给 agent。",
557
596
  agentNotWatching: "Agent 没在关注",
558
597
  agentNotWatchingTooltip: "已和 dev server 断开连接,agent 看不到你选的元素了。请重新启动 dev server 来恢复连接。",
559
598
  leaveComment: "留个 comment",
560
599
  commentPlaceholder: "描述你希望代理执行的更改…",
561
- commentShortcutHint: "⌘↵ 添加",
600
+ commentShortcutHint: "⌘/ 聚焦 · ⌘↵ 添加",
562
601
  addComment: "添加 comment",
563
602
  unsavedChanges: {
564
603
  one: "{count} 项未保存的更改",
@@ -601,6 +640,8 @@ const zhCN = {
601
640
  devOnlyMessage: "素材管理仅在开发模式下可用。",
602
641
  sectionAria: "幻灯片素材",
603
642
  eyebrow: "素材",
643
+ scopeSlide: "当前幻灯片",
644
+ scopeGlobal: "全局",
604
645
  fileCount: {
605
646
  one: "{count} 个文件",
606
647
  other: "{count} 个文件"
@@ -619,11 +660,16 @@ const zhCN = {
619
660
  renameMenuItem: "重命名",
620
661
  deleteMenuItem: "删除",
621
662
  conflictTitle: "文件已存在",
622
- conflictDescription: "{name} 已在该幻灯片的素材文件夹中。",
663
+ conflictDescription: "{name} 已在素材文件夹中。",
623
664
  conflictReplace: "替换",
624
665
  conflictRenameCopy: "重命名副本",
625
666
  deleteAssetTitle: "删除素材",
626
667
  deleteAssetDescription: "要删除 {name} 吗?幻灯片中引用此文件的导入将失效。",
668
+ deleteAssetInUseDescription: "{name} 在 {slides} 个幻灯片中被使用了 {count} 次。",
669
+ deleteAssetInUseHint: "删除后这些使用处会自动还原为图片占位符。",
670
+ deleteAndRevert: "删除并还原",
671
+ toastRevertFailed: "无法还原 {slideId} 中的使用",
672
+ toastDeletedWithRevert: "已删除 {name} 并还原 {count} 个使用处",
627
673
  noPreview: "无预览",
628
674
  importHintComment: "import asset from ",
629
675
  importHintSemi: ";",
@@ -749,6 +795,7 @@ const zhTW = {
749
795
  appTitle: "open-slide",
750
796
  draft: "草稿",
751
797
  themes: "主題",
798
+ assets: "素材",
752
799
  folders: "資料夾",
753
800
  newFolder: "新增資料夾",
754
801
  folderName: "資料夾名稱",
@@ -758,6 +805,11 @@ const zhTW = {
758
805
  folderActions: "資料夾操作",
759
806
  searchPlaceholder: "搜尋投影片",
760
807
  clearSearch: "清除搜尋",
808
+ sortLabel: "排序",
809
+ sortByCreatedDesc: "最新",
810
+ sortByCreatedAsc: "最舊",
811
+ sortByTitleAsc: "A–Z",
812
+ sortByTitleDesc: "Z–A",
761
813
  noMatches: "沒有相符結果",
762
814
  nothingMatchesPrefix: "此資料夾中沒有相符 ",
763
815
  nothingMatchesSuffix: " 的項目。",
@@ -791,16 +843,24 @@ const zhTW = {
791
843
  },
792
844
  slide: {
793
845
  agentConnected: "Agent 已連線",
794
- agentConnectedTooltip: "Dev server 正在把你目前在哪張 slide、Inspector 選了哪個元素發布給 agent。直接到聊天說「這張 slide」或「這個元素」就行。Production build 不會出現。",
846
+ agentConnectedTooltip: "目前的 slide Inspector 選擇會即時同步給 agent",
795
847
  agentDisconnected: "Agent 已斷線",
796
848
  agentDisconnectedTooltip: "已和 dev server 斷線,agent 沒辦法再看到你目前的 slide 或 Inspector 選擇。請重新啟動 dev server 來恢復連線。",
797
849
  home: "首頁",
798
850
  backToHome: "返回首頁",
799
851
  download: "下載",
852
+ copyLink: "複製連結",
853
+ toastCopyLinkSuccess: "已複製連結到剪貼簿",
854
+ toastCopyLinkFailed: "複製連結失敗",
800
855
  exportAsHtml: "匯出為 HTML",
801
856
  exportAsPdf: "匯出為 PDF",
802
857
  pdfExportFailed: "PDF 匯出失敗",
858
+ pdfExportSafariUnsupported: "匯出 PDF 目前不支援 Safari 裝置,請嘗試用 Chromium 基底瀏覽器替代。",
803
859
  present: "簡報",
860
+ presentMenuAria: "簡報選項",
861
+ presentInWindow: "播放",
862
+ presentFullscreen: "全螢幕播放",
863
+ presentPresenter: "簡報者模式",
804
864
  slidesTab: "投影片",
805
865
  assetsTab: "素材",
806
866
  renameSlide: "重新命名投影片",
@@ -842,6 +902,8 @@ const zhTW = {
842
902
  whiteoutAria: "白屏 (W)",
843
903
  laserAria: "雷射筆 (L)",
844
904
  presenterAria: "主講人檢視 (P)",
905
+ enterFullscreenAria: "進入全螢幕",
906
+ exitFullscreenAria: "退出全螢幕",
845
907
  helpAria: "鍵盤快速鍵 (?)",
846
908
  exitAria: "離開 (Esc)",
847
909
  elapsedTime: "已耗時",
@@ -903,12 +965,12 @@ const zhTW = {
903
965
  cropApply: "套用",
904
966
  cropResetAria: "重設裁切",
905
967
  agentWatching: "Agent 正在關注",
906
- agentWatchingTooltip: "Agent 已經透過 dev server 看到你選的元素了,直接到聊天請它修改就行。想累積幾個再一次問才需要在這裡留 comments。",
968
+ agentWatchingTooltip: "選取的元素會即時同步給 agent。",
907
969
  agentNotWatching: "Agent 沒在關注",
908
970
  agentNotWatchingTooltip: "已和 dev server 斷線,agent 看不到你選的元素了。請重新啟動 dev server 來恢復連線。",
909
971
  leaveComment: "留個 comment",
910
972
  commentPlaceholder: "描述你希望代理進行的修改…",
911
- commentShortcutHint: "⌘↵ 新增",
973
+ commentShortcutHint: "⌘/ 聚焦 · ⌘↵ 新增",
912
974
  addComment: "新增 comment",
913
975
  unsavedChanges: {
914
976
  one: "{count} 項未儲存的變更",
@@ -951,6 +1013,8 @@ const zhTW = {
951
1013
  devOnlyMessage: "素材管理僅在開發模式下可用。",
952
1014
  sectionAria: "投影片素材",
953
1015
  eyebrow: "素材",
1016
+ scopeSlide: "此投影片",
1017
+ scopeGlobal: "全域",
954
1018
  fileCount: {
955
1019
  one: "{count} 個檔案",
956
1020
  other: "{count} 個檔案"
@@ -969,11 +1033,16 @@ const zhTW = {
969
1033
  renameMenuItem: "重新命名",
970
1034
  deleteMenuItem: "刪除",
971
1035
  conflictTitle: "檔案已存在",
972
- conflictDescription: "{name} 已在此投影片的素材資料夾中。",
1036
+ conflictDescription: "{name} 已在素材資料夾中。",
973
1037
  conflictReplace: "取代",
974
1038
  conflictRenameCopy: "重新命名副本",
975
1039
  deleteAssetTitle: "刪除素材",
976
1040
  deleteAssetDescription: "要刪除 {name} 嗎?投影片中引用此檔案的匯入將失效。",
1041
+ deleteAssetInUseDescription: "{name} 在 {slides} 個投影片中被使用了 {count} 次。",
1042
+ deleteAssetInUseHint: "刪除後這些使用處會自動還原為圖片占位符。",
1043
+ deleteAndRevert: "刪除並還原",
1044
+ toastRevertFailed: "無法還原 {slideId} 中的使用",
1045
+ toastDeletedWithRevert: "已刪除 {name} 並還原 {count} 個使用處",
977
1046
  noPreview: "無預覽",
978
1047
  importHintComment: "import asset from ",
979
1048
  importHintSemi: ";",
@@ -1,5 +1,5 @@
1
1
  import "./design-cpzS8aud.js";
2
- import { createViteConfig } from "./config-BAwKWNtW.js";
2
+ import { createViteConfig } from "./config-iKjqaX08.js";
3
3
  import { mergeConfig, preview as preview$1 } from "vite";
4
4
 
5
5
  //#region src/cli/preview.ts
@@ -39,6 +39,7 @@ 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 @@ 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;
@@ -90,10 +96,18 @@ type Locale = {
90
96
  agentDisconnected: string;
91
97
  agentDisconnectedTooltip: string;
92
98
  download: string;
99
+ copyLink: string;
100
+ toastCopyLinkSuccess: string;
101
+ toastCopyLinkFailed: string;
93
102
  exportAsHtml: string;
94
103
  exportAsPdf: string;
95
104
  pdfExportFailed: string;
105
+ pdfExportSafariUnsupported: string;
96
106
  present: string;
107
+ presentMenuAria: string;
108
+ presentInWindow: string;
109
+ presentFullscreen: string;
110
+ presentPresenter: string;
97
111
  slidesTab: string;
98
112
  assetsTab: string;
99
113
  renameSlide: string;
@@ -136,6 +150,8 @@ type Locale = {
136
150
  whiteoutAria: string;
137
151
  laserAria: string;
138
152
  presenterAria: string;
153
+ enterFullscreenAria: string;
154
+ exitFullscreenAria: string;
139
155
  helpAria: string;
140
156
  exitAria: string;
141
157
  elapsedTime: string;
@@ -245,6 +261,8 @@ type Locale = {
245
261
  devOnlyMessage: string;
246
262
  sectionAria: string;
247
263
  eyebrow: string;
264
+ scopeSlide: string;
265
+ scopeGlobal: string;
248
266
  /** templates: "{count} file" / "{count} files" */
249
267
  fileCount: Plural;
250
268
  searchLogos: string;
@@ -263,13 +281,21 @@ type Locale = {
263
281
  renameMenuItem: string;
264
282
  deleteMenuItem: string;
265
283
  conflictTitle: string;
266
- /** template: "{name} is already in this slide's assets folder." */
284
+ /** template: "{name} is already in the assets folder." */
267
285
  conflictDescription: string;
268
286
  conflictReplace: string;
269
287
  conflictRenameCopy: string;
270
288
  deleteAssetTitle: string;
271
289
  /** template: "Delete {name}? Imports referencing this file in the slide will break." */
272
290
  deleteAssetDescription: string;
291
+ /** template: "{name} is used in {count} place across {slides} slide." (singular/plural via {count}/{slides}) */
292
+ deleteAssetInUseDescription: string;
293
+ deleteAssetInUseHint: string;
294
+ deleteAndRevert: string;
295
+ /** template: "Couldn't revert usage in {slideId}." */
296
+ toastRevertFailed: string;
297
+ /** template: "Deleted {name} and reverted {count} usage." */
298
+ toastDeletedWithRevert: string;
273
299
  noPreview: string;
274
300
  importHintComment: string;
275
301
  importHintSemi: string;
@@ -1,5 +1,5 @@
1
- import "../types-JYG1cmwC.js";
2
- import { OpenSlideConfig } from "../config-D9cZ1A0X.js";
1
+ import "../types-Dpr8nbih.js";
2
+ import { OpenSlideConfig } from "../config-BQdTMho4.js";
3
3
  import { InlineConfig } from "vite";
4
4
 
5
5
  //#region src/vite/config.d.ts
@@ -1,4 +1,4 @@
1
1
  import "../design-cpzS8aud.js";
2
- import { createViteConfig } from "../config-BAwKWNtW.js";
2
+ import { createViteConfig } from "../config-iKjqaX08.js";
3
3
 
4
4
  export { createViteConfig };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-slide/core",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Runtime and CLI for open-slide — write slides in slides/, we handle the rest.",
5
5
  "type": "module",
6
6
  "exports": {
@@ -31,7 +31,10 @@ import type { Page, SlideMeta } from '@open-slide/core';
31
31
  const Cover: Page = () => <div>…</div>;
32
32
  const Body: Page = () => <div>…</div>;
33
33
 
34
- export const meta: SlideMeta = { title: 'My slide' };
34
+ export const meta: SlideMeta = {
35
+ title: 'My slide',
36
+ createdAt: '2026-05-16T12:00:00Z',
37
+ };
35
38
  export default [Cover, Body] satisfies Page[];
36
39
  ```
37
40
 
@@ -39,6 +42,7 @@ export default [Cover, Body] satisfies Page[];
39
42
  - `meta.title` (optional) shows in the slide header. Default is the folder name.
40
43
  - The slide id is the kebab-case folder name. Pick something short and descriptive (`q2-roadmap`, `team-offsite-2026`).
41
44
  - `meta.theme` (optional) marks the slide as built from a theme under `themes/`. The id must match a `<id>.md` basename. Surfaces a back-link chip on the slide card and lists the slide on `/themes/<id>`. Omit if the slide isn't derived from a registered theme.
45
+ - `meta.createdAt` is an **ISO 8601 string literal** (e.g. `'2026-05-16T12:00:00Z'`) set once when the slide is scaffolded. The home page uses it for the default "newest first" sort. Always include it on new slides — **immediately before writing the file, run `node -e "console.log(new Date().toISOString())"` via Bash and paste the exact output** as the value. Don't type a timestamp from memory — you will get the date or time wrong. Must be a plain string literal (no `new Date(...)` or imports in the slide itself) — the framework reads it via a regex at build time, not by evaluating the module.
42
46
 
43
47
  ## Editing an existing slide
44
48
 
@@ -236,13 +240,16 @@ const Content: Page = () => (
236
240
  </div>
237
241
  );
238
242
 
239
- export const meta: SlideMeta = { title: 'The Big Idea' };
243
+ export const meta: SlideMeta = {
244
+ title: 'The Big Idea',
245
+ createdAt: '2026-05-16T12:00:00Z',
246
+ };
240
247
  export default [Cover, Content] satisfies Page[];
241
248
  ```
242
249
 
243
250
  ## Assets
244
251
 
245
- Place files under `slides/<id>/assets/`. Import them as ES modules:
252
+ **Slide-local assets** live under `slides/<id>/assets/` — anything one-off to a single slide. Import them as ES modules:
246
253
 
247
254
  ```tsx
248
255
  import hero from './assets/hero.jpg';
@@ -256,6 +263,14 @@ For URL-only access:
256
263
  const videoUrl = new URL('./assets/intro.mp4', import.meta.url).href;
257
264
  ```
258
265
 
266
+ **Global assets** — anything reused across decks or themes (company logos, presenter avatars, recurring icons) — live in the project root `assets/` folder. Import them via the `@assets` alias:
267
+
268
+ ```tsx
269
+ import logo from '@assets/logos/acme.svg';
270
+ ```
271
+
272
+ A `themes/*.md` file may name an asset path in its prose (e.g. "use `@assets/logos/acme.svg` in the title slot"); the slide imports it explicitly.
273
+
259
274
  Skip the `assets/` folder entirely for pure-text slides.
260
275
 
261
276
  ## Image placeholders
@@ -339,7 +354,7 @@ This applies whenever the *visual element* repeats, not whenever the *data* does
339
354
  - [ ] Slide declares a top-level `export const design: DesignSystem = { … }` and references the values via `var(--osd-X)` (use `design.X` only when you need a JS number for arithmetic). Only omit the `design` const for a one-off slide whose palette is intentionally locked.
340
355
  - [ ] One idea per page.
341
356
  - [ ] Visually repeated elements (cards, tiles, logo rows) are rendered as explicit `<Component />` instances, not via `array.map` over a data list.
342
- - [ ] All imported assets exist on disk under `slides/<id>/assets/`.
357
+ - [ ] All imported assets exist on disk — slide-local under `slides/<id>/assets/`, or global under `assets/` (imported via `@assets/...`).
343
358
  - [ ] Every `<ImagePlaceholder>` corresponds to a real image the user must supply — not decorative filler. If it could be replaced by typography or layout, it should be.
344
359
  - [ ] Nothing outside `slides/<id>/` was edited.
345
360
 
package/src/app/app.tsx CHANGED
@@ -2,6 +2,7 @@ import config from 'virtual:open-slide/config';
2
2
  import { BrowserRouter, Route, Routes } from 'react-router-dom';
3
3
  import { Toaster } from './components/ui/sonner';
4
4
  import { useLocale } from './lib/use-locale';
5
+ import { AssetsPage } from './routes/assets';
5
6
  import { Home } from './routes/home';
6
7
  import { HomeShell } from './routes/home-shell';
7
8
  import { Presenter } from './routes/presenter';
@@ -17,6 +18,7 @@ export function App() {
17
18
  <Route path="/" element={<Home />} />
18
19
  <Route path="/themes" element={<ThemesGalleryPage />} />
19
20
  <Route path="/themes/:themeId" element={<ThemeDetailPage />} />
21
+ <Route path="/assets" element={<AssetsPage />} />
20
22
  </Route>
21
23
  ) : (
22
24
  <Route path="/" element={<NotFound />} />