@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
@@ -1,5 +1,5 @@
1
1
  import "./design-cpzS8aud.js";
2
- import { createViteConfig } from "./config-XZJnC_fu.js";
2
+ import { createViteConfig } from "./config-PwUHqZ_X.js";
3
3
  import { createServer, mergeConfig } from "vite";
4
4
 
5
5
  //#region src/cli/dev.ts
@@ -34,6 +34,7 @@ const en = {
34
34
  home: {
35
35
  appTitle: "open-slide",
36
36
  draft: "Draft",
37
+ duplicate: "Duplicate",
37
38
  themes: "Themes",
38
39
  assets: "Assets",
39
40
  folders: "Folders",
@@ -45,6 +46,11 @@ const en = {
45
46
  folderActions: "Folder actions",
46
47
  searchPlaceholder: "Search slides",
47
48
  clearSearch: "Clear search",
49
+ sortLabel: "Sort",
50
+ sortByCreatedDesc: "Newest",
51
+ sortByCreatedAsc: "Oldest",
52
+ sortByTitleAsc: "A–Z",
53
+ sortByTitleDesc: "Z–A",
48
54
  noMatches: "No matches",
49
55
  nothingMatchesPrefix: "Nothing matches ",
50
56
  nothingMatchesSuffix: " in this folder.",
@@ -70,6 +76,8 @@ const en = {
70
76
  deleteDialogDescriptionSuffix: "This action cannot be undone.",
71
77
  toastFolderCreated: "Created folder “{name}”",
72
78
  toastFolderCreateFailed: "Failed to create folder",
79
+ toastSlideDuplicated: "Duplicated “{slide}” as {newSlide}",
80
+ toastSlideDuplicateFailed: "Could not duplicate slide",
73
81
  toastSlideMoved: "Moved “{slide}” to {folder}",
74
82
  toastSlideMoveFailed: "Failed to move slide",
75
83
  toastFolderDeleted: "Deleted folder “{name}”",
@@ -84,6 +92,9 @@ const en = {
84
92
  agentDisconnected: "Agent disconnected",
85
93
  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.",
86
94
  download: "Download",
95
+ copyLink: "Copy link",
96
+ toastCopyLinkSuccess: "Link copied to clipboard",
97
+ toastCopyLinkFailed: "Failed to copy link",
87
98
  exportAsHtml: "Export as HTML",
88
99
  exportAsPdf: "Export as PDF",
89
100
  pdfExportFailed: "PDF export failed",
@@ -202,7 +213,7 @@ const en = {
202
213
  cropResetAria: "Reset crop",
203
214
  leaveComment: "Leave a comment",
204
215
  commentPlaceholder: "Describe a change for the agent…",
205
- commentShortcutHint: "⌘↵ to add",
216
+ commentShortcutHint: "⌘/ to focus · ⌘↵ to add",
206
217
  addComment: "Add comment",
207
218
  unsavedChanges: {
208
219
  one: "{count} unsaved change",
@@ -251,6 +262,7 @@ const en = {
251
262
  one: "{count} file",
252
263
  other: "{count} files"
253
264
  },
265
+ usageUnused: "Unused",
254
266
  searchLogos: "Search logos",
255
267
  upload: "Upload",
256
268
  dropToUpload: "Drop to upload",
@@ -270,6 +282,11 @@ const en = {
270
282
  conflictRenameCopy: "Rename copy",
271
283
  deleteAssetTitle: "Delete asset",
272
284
  deleteAssetDescription: "Delete {name}? Imports referencing this file in the slide will break.",
285
+ deleteAssetInUseDescription: "{name} is used in {count} place(s) across {slides} slide(s).",
286
+ deleteAssetInUseHint: "Deleting will revert these usages back to image placeholders.",
287
+ deleteAndRevert: "Delete & revert",
288
+ toastRevertFailed: "Couldn't revert usage in {slideId}",
289
+ toastDeletedWithRevert: "Deleted {name} and reverted {count} usage(s)",
273
290
  noPreview: "No preview available",
274
291
  importHintComment: "import asset from ",
275
292
  importHintSemi: ";",
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { Locale, Plural } from "./types-QCpkHkiS.js";
2
- import { OpenSlideConfig } from "./config-s0YUbmUe.js";
1
+ import { Locale, Plural } from "./types-B-KrjgX8.js";
2
+ import { OpenSlideConfig } from "./config-CfMThYN9.js";
3
3
  import { CSSProperties, ComponentType, HTMLAttributes } from "react";
4
4
  import * as react_jsx_runtime0 from "react/jsx-runtime";
5
5
 
@@ -45,12 +45,21 @@ declare function designToCssVars(d: DesignSystem): Record<string, string>;
45
45
  declare function cssVarsToString(vars: Record<string, string>): string;
46
46
  declare const defaultDesign: DesignSystem;
47
47
 
48
+ //#endregion
49
+ //#region src/app/lib/page-context.d.ts
50
+ declare function useSlidePageNumber(): {
51
+ current: number;
52
+ total: number;
53
+ };
54
+
48
55
  //#endregion
49
56
  //#region src/app/lib/sdk.d.ts
50
57
  type Page = ComponentType;
51
58
  type SlideMeta = {
52
59
  title?: string;
53
60
  theme?: string;
61
+ /** ISO 8601 timestamp. Set once at scaffold time; used to sort the slide list. */
62
+ createdAt?: string;
54
63
  };
55
64
  type SlideModule = {
56
65
  default: Page[];
@@ -62,4 +71,4 @@ declare const CANVAS_WIDTH = 1920;
62
71
  declare const CANVAS_HEIGHT = 1080;
63
72
 
64
73
  //#endregion
65
- export { CANVAS_HEIGHT, CANVAS_WIDTH, DesignFonts, DesignPalette, DesignSystem, DesignTypeScale, ImagePlaceholder, ImagePlaceholderProps, Locale, OpenSlideConfig, Page, Plural, SlideMeta, SlideModule, cssVarsToString, defaultDesign, designToCssVars };
74
+ export { CANVAS_HEIGHT, CANVAS_WIDTH, DesignFonts, DesignPalette, DesignSystem, DesignTypeScale, ImagePlaceholder, ImagePlaceholderProps, Locale, OpenSlideConfig, Page, Plural, SlideMeta, SlideModule, cssVarsToString, defaultDesign, designToCssVars, useSlidePageNumber };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { en } from "./en-7GU-DHbJ.js";
1
+ import { en } from "./en-BDnM5zKJ.js";
2
2
  import { cssVarsToString, defaultDesign, designToCssVars } from "./design-cpzS8aud.js";
3
- import { useRef, useState } from "react";
3
+ import { createContext, useContext, useRef, useState } from "react";
4
4
  import { toast } from "sonner";
5
5
  import config from "virtual:open-slide/config";
6
6
  import { jsx, jsxs } from "react/jsx-runtime";
@@ -43,7 +43,8 @@ async function uploadWithAutoRename(slideId, file) {
43
43
  size: body?.size ?? uploaded.size,
44
44
  mtime: body?.mtime ?? Date.now(),
45
45
  mime: body?.mime ?? uploaded.type ?? "application/octet-stream",
46
- url: body?.url ?? `/__assets/${slideId}/${encodeURIComponent(uploaded.name)}`
46
+ url: body?.url ?? `/__assets/${slideId}/${encodeURIComponent(uploaded.name)}`,
47
+ unused: body?.unused ?? false
47
48
  };
48
49
  return {
49
50
  ok: true,
@@ -296,10 +297,25 @@ function PlaceholderIcon() {
296
297
  });
297
298
  }
298
299
 
300
+ //#endregion
301
+ //#region src/app/lib/page-context.tsx
302
+ const GLOBAL_KEY = "__open_slide_page_context__";
303
+ const g = globalThis;
304
+ if (!g[GLOBAL_KEY]) g[GLOBAL_KEY] = createContext(null);
305
+ const SlidePageContext = g[GLOBAL_KEY];
306
+ function useSlidePageNumber() {
307
+ const ctx = useContext(SlidePageContext);
308
+ if (!ctx) throw new Error("useSlidePageNumber must be called from a slide page rendered by @open-slide/core");
309
+ return {
310
+ current: ctx.index + 1,
311
+ total: ctx.total
312
+ };
313
+ }
314
+
299
315
  //#endregion
300
316
  //#region src/app/lib/sdk.ts
301
317
  const CANVAS_WIDTH = 1920;
302
318
  const CANVAS_HEIGHT = 1080;
303
319
 
304
320
  //#endregion
305
- export { CANVAS_HEIGHT, CANVAS_WIDTH, ImagePlaceholder, cssVarsToString, defaultDesign, designToCssVars };
321
+ export { CANVAS_HEIGHT, CANVAS_WIDTH, ImagePlaceholder, cssVarsToString, defaultDesign, designToCssVars, useSlidePageNumber };
@@ -1,4 +1,4 @@
1
- import { Locale, Plural } from "../types-QCpkHkiS.js";
1
+ import { Locale, Plural } from "../types-B-KrjgX8.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-7GU-DHbJ.js";
1
+ import { en } from "../en-BDnM5zKJ.js";
2
2
 
3
3
  //#region src/locale/format.ts
4
4
  function format(template, vars) {
@@ -48,6 +48,7 @@ const ja = {
48
48
  home: {
49
49
  appTitle: "open-slide",
50
50
  draft: "下書き",
51
+ duplicate: "複製",
51
52
  themes: "テーマ",
52
53
  assets: "アセット",
53
54
  folders: "フォルダ",
@@ -59,6 +60,11 @@ const ja = {
59
60
  folderActions: "フォルダ操作",
60
61
  searchPlaceholder: "スライドを検索",
61
62
  clearSearch: "検索をクリア",
63
+ sortLabel: "並べ替え",
64
+ sortByCreatedDesc: "新しい順",
65
+ sortByCreatedAsc: "古い順",
66
+ sortByTitleAsc: "A–Z",
67
+ sortByTitleDesc: "Z–A",
62
68
  noMatches: "一致なし",
63
69
  nothingMatchesPrefix: "このフォルダ内に ",
64
70
  nothingMatchesSuffix: " に一致する項目はありません。",
@@ -84,6 +90,8 @@ const ja = {
84
90
  deleteDialogDescriptionSuffix: "この操作は元に戻せません。",
85
91
  toastFolderCreated: "フォルダ「{name}」を作成しました",
86
92
  toastFolderCreateFailed: "フォルダの作成に失敗しました",
93
+ toastSlideDuplicated: "「{slide}」を {newSlide} として複製しました",
94
+ toastSlideDuplicateFailed: "スライドを複製できませんでした",
87
95
  toastSlideMoved: "「{slide}」を {folder} に移動しました",
88
96
  toastSlideMoveFailed: "スライドの移動に失敗しました",
89
97
  toastFolderDeleted: "フォルダ「{name}」を削除しました",
@@ -98,6 +106,9 @@ const ja = {
98
106
  home: "ホーム",
99
107
  backToHome: "ホームへ戻る",
100
108
  download: "ダウンロード",
109
+ copyLink: "リンクをコピー",
110
+ toastCopyLinkSuccess: "リンクをクリップボードにコピーしました",
111
+ toastCopyLinkFailed: "リンクのコピーに失敗しました",
101
112
  exportAsHtml: "HTML として書き出し",
102
113
  exportAsPdf: "PDF として書き出し",
103
114
  pdfExportFailed: "PDF の書き出しに失敗しました",
@@ -216,7 +227,7 @@ const ja = {
216
227
  agentNotWatchingTooltip: "dev server との接続が切れたため、選択中の要素がエージェントに見えなくなっています。dev server を再起動して接続を復旧してください。",
217
228
  leaveComment: "コメントを残す",
218
229
  commentPlaceholder: "エージェントに依頼する変更を記述…",
219
- commentShortcutHint: "⌘↵ で追加",
230
+ commentShortcutHint: "⌘/ でフォーカス · ⌘↵ で追加",
220
231
  addComment: "コメントを追加",
221
232
  unsavedChanges: {
222
233
  one: "未保存の変更 {count} 件",
@@ -265,6 +276,7 @@ const ja = {
265
276
  one: "ファイル {count} 件",
266
277
  other: "ファイル {count} 件"
267
278
  },
279
+ usageUnused: "未使用",
268
280
  searchLogos: "ロゴを検索",
269
281
  upload: "アップロード",
270
282
  dropToUpload: "ドロップでアップロード",
@@ -284,6 +296,11 @@ const ja = {
284
296
  conflictRenameCopy: "コピーをリネーム",
285
297
  deleteAssetTitle: "アセットを削除",
286
298
  deleteAssetDescription: "{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: ";",
@@ -408,6 +425,7 @@ const zhCN = {
408
425
  home: {
409
426
  appTitle: "open-slide",
410
427
  draft: "草稿",
428
+ duplicate: "复制",
411
429
  themes: "主题",
412
430
  assets: "素材",
413
431
  folders: "文件夹",
@@ -419,6 +437,11 @@ const zhCN = {
419
437
  folderActions: "文件夹操作",
420
438
  searchPlaceholder: "搜索幻灯片",
421
439
  clearSearch: "清除搜索",
440
+ sortLabel: "排序",
441
+ sortByCreatedDesc: "最新",
442
+ sortByCreatedAsc: "最旧",
443
+ sortByTitleAsc: "A–Z",
444
+ sortByTitleDesc: "Z–A",
422
445
  noMatches: "没有匹配项",
423
446
  nothingMatchesPrefix: "该文件夹中没有匹配 ",
424
447
  nothingMatchesSuffix: " 的内容。",
@@ -444,6 +467,8 @@ const zhCN = {
444
467
  deleteDialogDescriptionSuffix: "此操作无法撤销。",
445
468
  toastFolderCreated: "已创建文件夹\"{name}\"",
446
469
  toastFolderCreateFailed: "创建文件夹失败",
470
+ toastSlideDuplicated: "已将\"{slide}\"复制为 {newSlide}",
471
+ toastSlideDuplicateFailed: "无法复制幻灯片",
447
472
  toastSlideMoved: "已将\"{slide}\"移至 {folder}",
448
473
  toastSlideMoveFailed: "移动幻灯片失败",
449
474
  toastFolderDeleted: "已删除文件夹\"{name}\"",
@@ -458,6 +483,9 @@ const zhCN = {
458
483
  home: "首页",
459
484
  backToHome: "返回首页",
460
485
  download: "下载",
486
+ copyLink: "复制链接",
487
+ toastCopyLinkSuccess: "已复制链接到剪贴板",
488
+ toastCopyLinkFailed: "复制链接失败",
461
489
  exportAsHtml: "导出为 HTML",
462
490
  exportAsPdf: "导出为 PDF",
463
491
  pdfExportFailed: "PDF 导出失败",
@@ -576,7 +604,7 @@ const zhCN = {
576
604
  agentNotWatchingTooltip: "已和 dev server 断开连接,agent 看不到你选的元素了。请重新启动 dev server 来恢复连接。",
577
605
  leaveComment: "留个 comment",
578
606
  commentPlaceholder: "描述你希望代理执行的更改…",
579
- commentShortcutHint: "⌘↵ 添加",
607
+ commentShortcutHint: "⌘/ 聚焦 · ⌘↵ 添加",
580
608
  addComment: "添加 comment",
581
609
  unsavedChanges: {
582
610
  one: "{count} 项未保存的更改",
@@ -625,6 +653,7 @@ const zhCN = {
625
653
  one: "{count} 个文件",
626
654
  other: "{count} 个文件"
627
655
  },
656
+ usageUnused: "未使用",
628
657
  searchLogos: "搜索 Logo",
629
658
  upload: "上传",
630
659
  dropToUpload: "拖入即可上传",
@@ -644,6 +673,11 @@ const zhCN = {
644
673
  conflictRenameCopy: "重命名副本",
645
674
  deleteAssetTitle: "删除素材",
646
675
  deleteAssetDescription: "要删除 {name} 吗?幻灯片中引用此文件的导入将失效。",
676
+ deleteAssetInUseDescription: "{name} 在 {slides} 个幻灯片中被使用了 {count} 次。",
677
+ deleteAssetInUseHint: "删除后这些使用处会自动还原为图片占位符。",
678
+ deleteAndRevert: "删除并还原",
679
+ toastRevertFailed: "无法还原 {slideId} 中的使用",
680
+ toastDeletedWithRevert: "已删除 {name} 并还原 {count} 个使用处",
647
681
  noPreview: "无预览",
648
682
  importHintComment: "import asset from ",
649
683
  importHintSemi: ";",
@@ -768,6 +802,7 @@ const zhTW = {
768
802
  home: {
769
803
  appTitle: "open-slide",
770
804
  draft: "草稿",
805
+ duplicate: "複製",
771
806
  themes: "主題",
772
807
  assets: "素材",
773
808
  folders: "資料夾",
@@ -779,6 +814,11 @@ const zhTW = {
779
814
  folderActions: "資料夾操作",
780
815
  searchPlaceholder: "搜尋投影片",
781
816
  clearSearch: "清除搜尋",
817
+ sortLabel: "排序",
818
+ sortByCreatedDesc: "最新",
819
+ sortByCreatedAsc: "最舊",
820
+ sortByTitleAsc: "A–Z",
821
+ sortByTitleDesc: "Z–A",
782
822
  noMatches: "沒有相符結果",
783
823
  nothingMatchesPrefix: "此資料夾中沒有相符 ",
784
824
  nothingMatchesSuffix: " 的項目。",
@@ -804,6 +844,8 @@ const zhTW = {
804
844
  deleteDialogDescriptionSuffix: "此操作無法復原。",
805
845
  toastFolderCreated: "已建立資料夾「{name}」",
806
846
  toastFolderCreateFailed: "建立資料夾失敗",
847
+ toastSlideDuplicated: "已將「{slide}」複製為 {newSlide}",
848
+ toastSlideDuplicateFailed: "無法複製投影片",
807
849
  toastSlideMoved: "已將「{slide}」移至 {folder}",
808
850
  toastSlideMoveFailed: "移動投影片失敗",
809
851
  toastFolderDeleted: "已刪除資料夾「{name}」",
@@ -818,6 +860,9 @@ const zhTW = {
818
860
  home: "首頁",
819
861
  backToHome: "返回首頁",
820
862
  download: "下載",
863
+ copyLink: "複製連結",
864
+ toastCopyLinkSuccess: "已複製連結到剪貼簿",
865
+ toastCopyLinkFailed: "複製連結失敗",
821
866
  exportAsHtml: "匯出為 HTML",
822
867
  exportAsPdf: "匯出為 PDF",
823
868
  pdfExportFailed: "PDF 匯出失敗",
@@ -936,7 +981,7 @@ const zhTW = {
936
981
  agentNotWatchingTooltip: "已和 dev server 斷線,agent 看不到你選的元素了。請重新啟動 dev server 來恢復連線。",
937
982
  leaveComment: "留個 comment",
938
983
  commentPlaceholder: "描述你希望代理進行的修改…",
939
- commentShortcutHint: "⌘↵ 新增",
984
+ commentShortcutHint: "⌘/ 聚焦 · ⌘↵ 新增",
940
985
  addComment: "新增 comment",
941
986
  unsavedChanges: {
942
987
  one: "{count} 項未儲存的變更",
@@ -985,6 +1030,7 @@ const zhTW = {
985
1030
  one: "{count} 個檔案",
986
1031
  other: "{count} 個檔案"
987
1032
  },
1033
+ usageUnused: "未使用",
988
1034
  searchLogos: "搜尋 Logo",
989
1035
  upload: "上傳",
990
1036
  dropToUpload: "拖入即可上傳",
@@ -1004,6 +1050,11 @@ const zhTW = {
1004
1050
  conflictRenameCopy: "重新命名副本",
1005
1051
  deleteAssetTitle: "刪除素材",
1006
1052
  deleteAssetDescription: "要刪除 {name} 嗎?投影片中引用此檔案的匯入將失效。",
1053
+ deleteAssetInUseDescription: "{name} 在 {slides} 個投影片中被使用了 {count} 次。",
1054
+ deleteAssetInUseHint: "刪除後這些使用處會自動還原為圖片占位符。",
1055
+ deleteAndRevert: "刪除並還原",
1056
+ toastRevertFailed: "無法還原 {slideId} 中的使用",
1057
+ toastDeletedWithRevert: "已刪除 {name} 並還原 {count} 個使用處",
1007
1058
  noPreview: "無預覽",
1008
1059
  importHintComment: "import asset from ",
1009
1060
  importHintSemi: ";",
@@ -1,5 +1,5 @@
1
1
  import "./design-cpzS8aud.js";
2
- import { createViteConfig } from "./config-XZJnC_fu.js";
2
+ import { createViteConfig } from "./config-PwUHqZ_X.js";
3
3
  import { mergeConfig, preview as preview$1 } from "vite";
4
4
 
5
5
  //#region src/cli/preview.ts
@@ -38,6 +38,7 @@ 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 @@ 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 @@ 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;
@@ -91,6 +100,9 @@ type Locale = {
91
100
  agentDisconnected: string;
92
101
  agentDisconnectedTooltip: string;
93
102
  download: string;
103
+ copyLink: string;
104
+ toastCopyLinkSuccess: string;
105
+ toastCopyLinkFailed: string;
94
106
  exportAsHtml: string;
95
107
  exportAsPdf: string;
96
108
  pdfExportFailed: string;
@@ -257,6 +269,7 @@ type Locale = {
257
269
  scopeGlobal: string;
258
270
  /** templates: "{count} file" / "{count} files" */
259
271
  fileCount: Plural;
272
+ usageUnused: string;
260
273
  searchLogos: string;
261
274
  upload: string;
262
275
  dropToUpload: string;
@@ -280,6 +293,14 @@ type Locale = {
280
293
  deleteAssetTitle: string;
281
294
  /** template: "Delete {name}? Imports referencing this file in the slide will break." */
282
295
  deleteAssetDescription: string;
296
+ /** template: "{name} is used in {count} place across {slides} slide." (singular/plural via {count}/{slides}) */
297
+ deleteAssetInUseDescription: string;
298
+ deleteAssetInUseHint: string;
299
+ deleteAndRevert: string;
300
+ /** template: "Couldn't revert usage in {slideId}." */
301
+ toastRevertFailed: string;
302
+ /** template: "Deleted {name} and reverted {count} usage." */
303
+ toastDeletedWithRevert: string;
283
304
  noPreview: string;
284
305
  importHintComment: string;
285
306
  importHintSemi: string;
@@ -1,5 +1,5 @@
1
- import "../types-QCpkHkiS.js";
2
- import { OpenSlideConfig } from "../config-s0YUbmUe.js";
1
+ import "../types-B-KrjgX8.js";
2
+ import { OpenSlideConfig } from "../config-CfMThYN9.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-XZJnC_fu.js";
2
+ import { createViteConfig } from "../config-PwUHqZ_X.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.4.0",
3
+ "version": "1.6.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": {
@@ -105,24 +105,31 @@ const Title = ({ children }: { children: React.ReactNode }) => (
105
105
 
106
106
  ### Footer
107
107
 
108
+ Pull the page number from `useSlidePageNumber()` — never hardcode `pageNum` / `total` props.
109
+
108
110
  ```tsx
109
- const Footer = ({ pageNum, total }: { pageNum: number; total: number }) => (
110
- <div
111
- style={{
112
- position: 'absolute',
113
- left: 120,
114
- right: 120,
115
- bottom: 60,
116
- display: 'flex',
117
- justifyContent: 'space-between',
118
- fontSize: 24,
119
- color: '#94a3b8',
120
- }}
121
- >
122
- <span>EDITORIAL NOIR · 2026</span>
123
- <span>{pageNum} / {total}</span>
124
- </div>
125
- );
111
+ import { useSlidePageNumber } from '@open-slide/core';
112
+
113
+ const Footer = () => {
114
+ const { current, total } = useSlidePageNumber();
115
+ return (
116
+ <div
117
+ style={{
118
+ position: 'absolute',
119
+ left: 120,
120
+ right: 120,
121
+ bottom: 60,
122
+ display: 'flex',
123
+ justifyContent: 'space-between',
124
+ fontSize: 24,
125
+ color: '#94a3b8',
126
+ }}
127
+ >
128
+ <span>EDITORIAL NOIR · 2026</span>
129
+ <span>{current} / {total}</span>
130
+ </div>
131
+ );
132
+ };
126
133
  ```
127
134
 
128
135
  ### Eyebrow / accents (optional)
@@ -161,7 +168,7 @@ const Cover: Page = () => (
161
168
  <p style={{ fontSize: 36, color: '#94a3b8', maxWidth: 1200, marginTop: 32 }}>
162
169
  A short subtitle that explains what this slide is about.
163
170
  </p>
164
- <Footer pageNum={1} total={5} />
171
+ <Footer />
165
172
  </div>
166
173
  );
167
174
  ```
@@ -173,7 +180,7 @@ The demo is a normal slide module — same shape as `slides/<id>/index.tsx`, jus
173
180
 
174
181
  Contract:
175
182
 
176
- - `import type { Page } from '@open-slide/core';`
183
+ - `import { type Page, useSlidePageNumber } from '@open-slide/core';`
177
184
  - Inline the **same** `Title`, `Footer`, `Eyebrow` components defined in the theme markdown — verbatim, no abstractions, no imports from elsewhere. The demo and the markdown must stay in lockstep so what the user sees in the panel matches what `create-slide` will paste into a real slide.
178
185
  - Export 2–3 `Page` components and a default array. Aim for: a Cover (Eyebrow + Title + subtitle), one Content page exercising body type + accent, and a Closer or "End" card. The "Example usage" block at the bottom of the markdown is a good starting point — extend it.
179
186
  - If the theme has runtime-tweakable tokens worth surfacing in the Design panel later, also `export const design: DesignSystem = {...}`.
@@ -182,14 +189,15 @@ Contract:
182
189
  Skeleton:
183
190
 
184
191
  ```tsx
185
- import type { Page } from '@open-slide/core';
192
+ import { type Page, useSlidePageNumber } from '@open-slide/core';
186
193
 
187
194
  const Title = ({ children }: { children: React.ReactNode }) => (
188
195
  // …same JSX as in themes/<id>.md
189
196
  );
190
- const Footer = ({ pageNum, total }: { pageNum: number; total: number }) => (
197
+ const Footer = () => {
198
+ const { current, total } = useSlidePageNumber();
191
199
  // …
192
- );
200
+ };
193
201
  const Eyebrow = ({ children }: { children: React.ReactNode }) => (
194
202
  // …
195
203
  );
@@ -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,7 +240,10 @@ 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
 
@@ -284,6 +291,23 @@ The user uploads the real file via the Assets panel, then clicks the placeholder
284
291
 
285
292
  Size the placeholder to the slot it occupies. Pass `width`/`height` when the layout has a fixed image box; omit them when the placeholder fills a flex/grid cell. The `hint` should describe the *content* the user needs ("Q3 revenue chart") not the *role* ("hero image").
286
293
 
294
+ ## Page numbers
295
+
296
+ If a footer shows the current page (`03 / 12`, `Page 3`, etc.), read it from `useSlidePageNumber()` — **never hardcode** `n` / `TOTAL`. Inserting, reordering, or deleting a page would otherwise force you to retouch every footer.
297
+
298
+ ```tsx
299
+ import { useSlidePageNumber } from '@open-slide/core';
300
+
301
+ const Footer = () => {
302
+ const { current, total } = useSlidePageNumber();
303
+ return (
304
+ <span>{String(current).padStart(2, '0')} / {String(total).padStart(2, '0')}</span>
305
+ );
306
+ };
307
+ ```
308
+
309
+ `current` is 1-indexed (matches what readers see) and `total` is the slide's page count. The hook works in every render context (main viewer, thumbnails, overview grid, present mode, presenter window, HTML/PDF export) — the same `<Footer />` JSX is correct everywhere. Call the hook inside a component that's used **per page**; don't try to call it at module top level.
310
+
287
311
  ## Repeated elements: component, not `map`
288
312
 
289
313
  When a page has visually repeated items — cards, logo rows, gallery tiles, list rows, step indicators — **define a small component and instantiate it once per item**. Do **not** render the group with `array.map` over a data array.