@open-slide/core 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{build-1Rqivz0d.js → build-tLrkKUHr.js} +1 -1
- package/dist/cli/bin.js +3 -3
- package/dist/{config-s0YUbmUe.d.ts → config-CfMThYN9.d.ts} +1 -1
- package/dist/{config-XZJnC_fu.js → config-PwUHqZ_X.js} +2312 -1654
- package/dist/{dev-0W8gYiSa.js → dev-DpCIRbhT.js} +1 -1
- package/dist/{en-7GU-DHbJ.js → en-BDnM5zKJ.js} +18 -1
- package/dist/index.d.ts +12 -3
- package/dist/index.js +20 -4
- package/dist/locale/index.d.ts +1 -1
- package/dist/locale/index.js +55 -4
- package/dist/{preview-DT9hJvzM.js → preview-BSGlM6Se.js} +1 -1
- package/dist/{types-QCpkHkiS.d.ts → types-B-KrjgX8.d.ts} +21 -0
- package/dist/vite/index.d.ts +2 -2
- package/dist/vite/index.js +1 -1
- package/package.json +1 -1
- package/skills/create-theme/SKILL.md +30 -22
- package/skills/slide-authoring/SKILL.md +26 -2
- package/src/app/components/asset-view.tsx +83 -10
- package/src/app/components/inspector/inspector-panel.tsx +16 -1
- package/src/app/components/panel/panel-shell.tsx +5 -3
- package/src/app/components/player.tsx +6 -1
- package/src/app/components/present/laser-pointer.tsx +3 -4
- package/src/app/components/present/overview-grid.tsx +4 -1
- package/src/app/components/present/progress-bar.tsx +4 -4
- package/src/app/components/themes/theme-detail.tsx +7 -2
- package/src/app/components/themes/themes-gallery.tsx +4 -1
- package/src/app/components/thumbnail-rail.tsx +10 -2
- package/src/app/lib/assets.ts +23 -0
- package/src/app/lib/export-html.ts +7 -2
- package/src/app/lib/export-pdf.ts +34 -2
- package/src/app/lib/folders.ts +35 -1
- package/src/app/lib/page-context.tsx +38 -0
- package/src/app/lib/sdk.ts +2 -0
- package/src/app/lib/slides.ts +2 -0
- package/src/app/lib/use-wheel-page-navigation.ts +7 -0
- package/src/app/routes/home-shell.tsx +13 -2
- package/src/app/routes/home.tsx +129 -5
- package/src/app/routes/presenter.tsx +7 -2
- package/src/app/routes/slide.tsx +49 -1
- package/src/app/virtual.d.ts +1 -0
- package/src/locale/en.ts +18 -1
- package/src/locale/ja.ts +18 -1
- package/src/locale/types.ts +21 -0
- package/src/locale/zh-cn.ts +18 -1
- package/src/locale/zh-tw.ts +18 -1
|
@@ -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-
|
|
2
|
-
import { OpenSlideConfig } from "./config-
|
|
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-
|
|
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 };
|
package/dist/locale/index.d.ts
CHANGED
package/dist/locale/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { en } from "../en-
|
|
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: ";",
|
|
@@ -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;
|
package/dist/vite/index.d.ts
CHANGED
package/dist/vite/index.js
CHANGED
package/package.json
CHANGED
|
@@ -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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
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
|
|
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
|
|
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 = (
|
|
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 = {
|
|
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 = {
|
|
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.
|