@open-slide/core 1.3.0 → 1.4.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-_276DMmJ.js → build-1Rqivz0d.js} +1 -1
- package/dist/cli/bin.js +3 -3
- package/dist/{config-BAwKWNtW.js → config-XZJnC_fu.js} +533 -59
- package/dist/{config-D9cZ1A0X.d.ts → config-s0YUbmUe.d.ts} +2 -1
- package/dist/{dev-BoqeVXVq.js → dev-0W8gYiSa.js} +1 -1
- package/dist/{en-CDKzoZvf.js → en-7GU-DHbJ.js} +13 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/locale/index.d.ts +1 -1
- package/dist/locale/index.js +40 -10
- package/dist/{preview-BLPxspc9.js → preview-DT9hJvzM.js} +1 -1
- package/dist/{types-JYG1cmwC.d.ts → types-QCpkHkiS.d.ts} +11 -1
- package/dist/vite/index.d.ts +2 -2
- package/dist/vite/index.js +1 -1
- package/package.json +1 -1
- package/skills/slide-authoring/SKILL.md +10 -2
- package/src/app/app.tsx +2 -0
- package/src/app/components/asset-view.tsx +36 -9
- package/src/app/components/inspector/inspect-overlay.tsx +49 -3
- package/src/app/components/inspector/inspector-panel.tsx +251 -24
- package/src/app/components/inspector/inspector-provider.tsx +390 -49
- package/src/app/components/player.tsx +25 -5
- package/src/app/components/present/control-bar.tsx +12 -0
- package/src/app/components/sidebar/folder-item.tsx +14 -3
- package/src/app/components/sidebar/sidebar.tsx +10 -0
- package/src/app/lib/export-pdf.ts +6 -0
- package/src/app/lib/inspector/use-editor.ts +9 -1
- package/src/app/lib/slides.ts +7 -0
- package/src/app/lib/use-slide-module.ts +48 -0
- package/src/app/routes/assets.tsx +9 -0
- package/src/app/routes/home-shell.tsx +23 -2
- package/src/app/routes/presenter.tsx +2 -20
- package/src/app/routes/slide.tsx +73 -40
- package/src/locale/en.ts +14 -4
- package/src/locale/ja.ts +14 -4
- package/src/locale/types.ts +11 -1
- package/src/locale/zh-cn.ts +14 -5
- package/src/locale/zh-tw.ts +14 -5
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Locale } from "./types-
|
|
1
|
+
import { Locale } from "./types-QCpkHkiS.js";
|
|
2
2
|
|
|
3
3
|
//#region src/config.d.ts
|
|
4
4
|
type OpenSlideBuildConfig = {
|
|
@@ -9,6 +9,7 @@ type OpenSlideBuildConfig = {
|
|
|
9
9
|
type OpenSlideConfig = {
|
|
10
10
|
slidesDir?: string;
|
|
11
11
|
themesDir?: string;
|
|
12
|
+
assetsDir?: string;
|
|
12
13
|
port?: number;
|
|
13
14
|
locale?: Locale;
|
|
14
15
|
build?: OpenSlideBuildConfig;
|
|
@@ -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",
|
|
@@ -79,14 +80,19 @@ const en = {
|
|
|
79
80
|
home: "Home",
|
|
80
81
|
backToHome: "Back to home",
|
|
81
82
|
agentConnected: "Agent connected",
|
|
82
|
-
agentConnectedTooltip: "The
|
|
83
|
+
agentConnectedTooltip: "The current slide and inspector selection are synced to your agent in real time.",
|
|
83
84
|
agentDisconnected: "Agent disconnected",
|
|
84
85
|
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
86
|
download: "Download",
|
|
86
87
|
exportAsHtml: "Export as HTML",
|
|
87
88
|
exportAsPdf: "Export as PDF",
|
|
88
89
|
pdfExportFailed: "PDF export failed",
|
|
90
|
+
pdfExportSafariUnsupported: "Export as PDF is not supported on Safari. Please try a Chromium-based browser instead.",
|
|
89
91
|
present: "Present",
|
|
92
|
+
presentMenuAria: "Present options",
|
|
93
|
+
presentInWindow: "Play",
|
|
94
|
+
presentFullscreen: "Fullscreen",
|
|
95
|
+
presentPresenter: "Presenter mode",
|
|
90
96
|
slidesTab: "Slides",
|
|
91
97
|
assetsTab: "Assets",
|
|
92
98
|
renameSlide: "Rename slide",
|
|
@@ -128,6 +134,8 @@ const en = {
|
|
|
128
134
|
whiteoutAria: "White screen (W)",
|
|
129
135
|
laserAria: "Laser pointer (L)",
|
|
130
136
|
presenterAria: "Presenter view (P)",
|
|
137
|
+
enterFullscreenAria: "Enter fullscreen",
|
|
138
|
+
exitFullscreenAria: "Exit fullscreen",
|
|
131
139
|
helpAria: "Keyboard shortcuts (?)",
|
|
132
140
|
exitAria: "Exit (Esc)",
|
|
133
141
|
elapsedTime: "Elapsed time",
|
|
@@ -153,7 +161,7 @@ const en = {
|
|
|
153
161
|
inspect: "Inspect",
|
|
154
162
|
deselect: "Deselect",
|
|
155
163
|
agentWatching: "Agent is watching",
|
|
156
|
-
agentWatchingTooltip: "
|
|
164
|
+
agentWatchingTooltip: "The selected element is synced to your agent in real time.",
|
|
157
165
|
agentNotWatching: "Agent not watching",
|
|
158
166
|
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
167
|
contentSection: "Content",
|
|
@@ -237,6 +245,8 @@ const en = {
|
|
|
237
245
|
devOnlyMessage: "Asset management is only available in dev mode.",
|
|
238
246
|
sectionAria: "Slide assets",
|
|
239
247
|
eyebrow: "Assets",
|
|
248
|
+
scopeSlide: "This slide",
|
|
249
|
+
scopeGlobal: "Global",
|
|
240
250
|
fileCount: {
|
|
241
251
|
one: "{count} file",
|
|
242
252
|
other: "{count} files"
|
|
@@ -255,7 +265,7 @@ const en = {
|
|
|
255
265
|
renameMenuItem: "Rename",
|
|
256
266
|
deleteMenuItem: "Delete",
|
|
257
267
|
conflictTitle: "File already exists",
|
|
258
|
-
conflictDescription: "{name} is already in
|
|
268
|
+
conflictDescription: "{name} is already in the assets folder.",
|
|
259
269
|
conflictReplace: "Replace",
|
|
260
270
|
conflictRenameCopy: "Rename copy",
|
|
261
271
|
deleteAssetTitle: "Delete asset",
|
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-QCpkHkiS.js";
|
|
2
|
+
import { OpenSlideConfig } from "./config-s0YUbmUe.js";
|
|
3
3
|
import { CSSProperties, ComponentType, HTMLAttributes } from "react";
|
|
4
4
|
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
5
5
|
|
package/dist/index.js
CHANGED
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-7GU-DHbJ.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: "フォルダ名",
|
|
@@ -91,7 +92,7 @@ const ja = {
|
|
|
91
92
|
},
|
|
92
93
|
slide: {
|
|
93
94
|
agentConnected: "エージェント接続中",
|
|
94
|
-
agentConnectedTooltip: "現在のスライドと Inspector
|
|
95
|
+
agentConnectedTooltip: "現在のスライドと Inspector の選択はエージェントにリアルタイムで同期されています。",
|
|
95
96
|
agentDisconnected: "エージェント切断",
|
|
96
97
|
agentDisconnectedTooltip: "dev server との接続が切れたため、現在のスライドや Inspector の選択がエージェントに届かなくなっています。dev server を再起動して接続を復旧してください。",
|
|
97
98
|
home: "ホーム",
|
|
@@ -100,7 +101,12 @@ const ja = {
|
|
|
100
101
|
exportAsHtml: "HTML として書き出し",
|
|
101
102
|
exportAsPdf: "PDF として書き出し",
|
|
102
103
|
pdfExportFailed: "PDF の書き出しに失敗しました",
|
|
104
|
+
pdfExportSafariUnsupported: "PDF の書き出しは現在 Safari では対応していません。Chromium ベースのブラウザでお試しください。",
|
|
103
105
|
present: "発表",
|
|
106
|
+
presentMenuAria: "発表オプション",
|
|
107
|
+
presentInWindow: "再生",
|
|
108
|
+
presentFullscreen: "フルスクリーン再生",
|
|
109
|
+
presentPresenter: "発表者モード",
|
|
104
110
|
slidesTab: "スライド",
|
|
105
111
|
assetsTab: "アセット",
|
|
106
112
|
renameSlide: "スライドの名前を変更",
|
|
@@ -142,6 +148,8 @@ const ja = {
|
|
|
142
148
|
whiteoutAria: "白い画面 (W)",
|
|
143
149
|
laserAria: "レーザーポインタ (L)",
|
|
144
150
|
presenterAria: "発表者ビュー (P)",
|
|
151
|
+
enterFullscreenAria: "フルスクリーンへ",
|
|
152
|
+
exitFullscreenAria: "フルスクリーン解除",
|
|
145
153
|
helpAria: "キーボードショートカット (?)",
|
|
146
154
|
exitAria: "終了 (Esc)",
|
|
147
155
|
elapsedTime: "経過時間",
|
|
@@ -203,7 +211,7 @@ const ja = {
|
|
|
203
211
|
cropApply: "適用",
|
|
204
212
|
cropResetAria: "トリミングをリセット",
|
|
205
213
|
agentWatching: "エージェント監視中",
|
|
206
|
-
agentWatchingTooltip: "
|
|
214
|
+
agentWatchingTooltip: "選択中の要素はエージェントにリアルタイムで同期されています。",
|
|
207
215
|
agentNotWatching: "エージェント未接続",
|
|
208
216
|
agentNotWatchingTooltip: "dev server との接続が切れたため、選択中の要素がエージェントに見えなくなっています。dev server を再起動して接続を復旧してください。",
|
|
209
217
|
leaveComment: "コメントを残す",
|
|
@@ -251,6 +259,8 @@ const ja = {
|
|
|
251
259
|
devOnlyMessage: "アセット管理は開発モードでのみ利用できます。",
|
|
252
260
|
sectionAria: "スライドのアセット",
|
|
253
261
|
eyebrow: "アセット",
|
|
262
|
+
scopeSlide: "このスライド",
|
|
263
|
+
scopeGlobal: "グローバル",
|
|
254
264
|
fileCount: {
|
|
255
265
|
one: "ファイル {count} 件",
|
|
256
266
|
other: "ファイル {count} 件"
|
|
@@ -269,7 +279,7 @@ const ja = {
|
|
|
269
279
|
renameMenuItem: "名前を変更",
|
|
270
280
|
deleteMenuItem: "削除",
|
|
271
281
|
conflictTitle: "ファイルがすでに存在します",
|
|
272
|
-
conflictDescription: "{name}
|
|
282
|
+
conflictDescription: "{name} はすでにアセットフォルダにあります。",
|
|
273
283
|
conflictReplace: "置き換え",
|
|
274
284
|
conflictRenameCopy: "コピーをリネーム",
|
|
275
285
|
deleteAssetTitle: "アセットを削除",
|
|
@@ -399,6 +409,7 @@ const zhCN = {
|
|
|
399
409
|
appTitle: "open-slide",
|
|
400
410
|
draft: "草稿",
|
|
401
411
|
themes: "主题",
|
|
412
|
+
assets: "素材",
|
|
402
413
|
folders: "文件夹",
|
|
403
414
|
newFolder: "新建文件夹",
|
|
404
415
|
folderName: "文件夹名称",
|
|
@@ -441,7 +452,7 @@ const zhCN = {
|
|
|
441
452
|
},
|
|
442
453
|
slide: {
|
|
443
454
|
agentConnected: "Agent 已连接",
|
|
444
|
-
agentConnectedTooltip: "
|
|
455
|
+
agentConnectedTooltip: "目前的 slide 与 Inspector 选择会即时同步给 agent。",
|
|
445
456
|
agentDisconnected: "Agent 已断开",
|
|
446
457
|
agentDisconnectedTooltip: "已和 dev server 断开连接,agent 没办法再看到你目前的 slide 或 Inspector 选择。请重新启动 dev server 来恢复连接。",
|
|
447
458
|
home: "首页",
|
|
@@ -450,7 +461,12 @@ const zhCN = {
|
|
|
450
461
|
exportAsHtml: "导出为 HTML",
|
|
451
462
|
exportAsPdf: "导出为 PDF",
|
|
452
463
|
pdfExportFailed: "PDF 导出失败",
|
|
464
|
+
pdfExportSafariUnsupported: "导出 PDF 目前不支持 Safari 设备,请尝试使用基于 Chromium 的浏览器替代。",
|
|
453
465
|
present: "演示",
|
|
466
|
+
presentMenuAria: "演示选项",
|
|
467
|
+
presentInWindow: "播放",
|
|
468
|
+
presentFullscreen: "全屏播放",
|
|
469
|
+
presentPresenter: "演讲者模式",
|
|
454
470
|
slidesTab: "幻灯片",
|
|
455
471
|
assetsTab: "素材",
|
|
456
472
|
renameSlide: "重命名幻灯片",
|
|
@@ -492,6 +508,8 @@ const zhCN = {
|
|
|
492
508
|
whiteoutAria: "白屏 (W)",
|
|
493
509
|
laserAria: "激光笔 (L)",
|
|
494
510
|
presenterAria: "演讲者视图 (P)",
|
|
511
|
+
enterFullscreenAria: "进入全屏",
|
|
512
|
+
exitFullscreenAria: "退出全屏",
|
|
495
513
|
helpAria: "键盘快捷键 (?)",
|
|
496
514
|
exitAria: "退出 (Esc)",
|
|
497
515
|
elapsedTime: "已用时",
|
|
@@ -553,7 +571,7 @@ const zhCN = {
|
|
|
553
571
|
cropApply: "应用",
|
|
554
572
|
cropResetAria: "重置裁剪",
|
|
555
573
|
agentWatching: "Agent 正在关注",
|
|
556
|
-
agentWatchingTooltip: "
|
|
574
|
+
agentWatchingTooltip: "选取的元素会即时同步给 agent。",
|
|
557
575
|
agentNotWatching: "Agent 没在关注",
|
|
558
576
|
agentNotWatchingTooltip: "已和 dev server 断开连接,agent 看不到你选的元素了。请重新启动 dev server 来恢复连接。",
|
|
559
577
|
leaveComment: "留个 comment",
|
|
@@ -601,6 +619,8 @@ const zhCN = {
|
|
|
601
619
|
devOnlyMessage: "素材管理仅在开发模式下可用。",
|
|
602
620
|
sectionAria: "幻灯片素材",
|
|
603
621
|
eyebrow: "素材",
|
|
622
|
+
scopeSlide: "当前幻灯片",
|
|
623
|
+
scopeGlobal: "全局",
|
|
604
624
|
fileCount: {
|
|
605
625
|
one: "{count} 个文件",
|
|
606
626
|
other: "{count} 个文件"
|
|
@@ -619,7 +639,7 @@ const zhCN = {
|
|
|
619
639
|
renameMenuItem: "重命名",
|
|
620
640
|
deleteMenuItem: "删除",
|
|
621
641
|
conflictTitle: "文件已存在",
|
|
622
|
-
conflictDescription: "{name}
|
|
642
|
+
conflictDescription: "{name} 已在素材文件夹中。",
|
|
623
643
|
conflictReplace: "替换",
|
|
624
644
|
conflictRenameCopy: "重命名副本",
|
|
625
645
|
deleteAssetTitle: "删除素材",
|
|
@@ -749,6 +769,7 @@ const zhTW = {
|
|
|
749
769
|
appTitle: "open-slide",
|
|
750
770
|
draft: "草稿",
|
|
751
771
|
themes: "主題",
|
|
772
|
+
assets: "素材",
|
|
752
773
|
folders: "資料夾",
|
|
753
774
|
newFolder: "新增資料夾",
|
|
754
775
|
folderName: "資料夾名稱",
|
|
@@ -791,7 +812,7 @@ const zhTW = {
|
|
|
791
812
|
},
|
|
792
813
|
slide: {
|
|
793
814
|
agentConnected: "Agent 已連線",
|
|
794
|
-
agentConnectedTooltip: "
|
|
815
|
+
agentConnectedTooltip: "目前的 slide 與 Inspector 選擇會即時同步給 agent。",
|
|
795
816
|
agentDisconnected: "Agent 已斷線",
|
|
796
817
|
agentDisconnectedTooltip: "已和 dev server 斷線,agent 沒辦法再看到你目前的 slide 或 Inspector 選擇。請重新啟動 dev server 來恢復連線。",
|
|
797
818
|
home: "首頁",
|
|
@@ -800,7 +821,12 @@ const zhTW = {
|
|
|
800
821
|
exportAsHtml: "匯出為 HTML",
|
|
801
822
|
exportAsPdf: "匯出為 PDF",
|
|
802
823
|
pdfExportFailed: "PDF 匯出失敗",
|
|
824
|
+
pdfExportSafariUnsupported: "匯出 PDF 目前不支援 Safari 裝置,請嘗試用 Chromium 基底瀏覽器替代。",
|
|
803
825
|
present: "簡報",
|
|
826
|
+
presentMenuAria: "簡報選項",
|
|
827
|
+
presentInWindow: "播放",
|
|
828
|
+
presentFullscreen: "全螢幕播放",
|
|
829
|
+
presentPresenter: "簡報者模式",
|
|
804
830
|
slidesTab: "投影片",
|
|
805
831
|
assetsTab: "素材",
|
|
806
832
|
renameSlide: "重新命名投影片",
|
|
@@ -842,6 +868,8 @@ const zhTW = {
|
|
|
842
868
|
whiteoutAria: "白屏 (W)",
|
|
843
869
|
laserAria: "雷射筆 (L)",
|
|
844
870
|
presenterAria: "主講人檢視 (P)",
|
|
871
|
+
enterFullscreenAria: "進入全螢幕",
|
|
872
|
+
exitFullscreenAria: "退出全螢幕",
|
|
845
873
|
helpAria: "鍵盤快速鍵 (?)",
|
|
846
874
|
exitAria: "離開 (Esc)",
|
|
847
875
|
elapsedTime: "已耗時",
|
|
@@ -903,7 +931,7 @@ const zhTW = {
|
|
|
903
931
|
cropApply: "套用",
|
|
904
932
|
cropResetAria: "重設裁切",
|
|
905
933
|
agentWatching: "Agent 正在關注",
|
|
906
|
-
agentWatchingTooltip: "
|
|
934
|
+
agentWatchingTooltip: "選取的元素會即時同步給 agent。",
|
|
907
935
|
agentNotWatching: "Agent 沒在關注",
|
|
908
936
|
agentNotWatchingTooltip: "已和 dev server 斷線,agent 看不到你選的元素了。請重新啟動 dev server 來恢復連線。",
|
|
909
937
|
leaveComment: "留個 comment",
|
|
@@ -951,6 +979,8 @@ const zhTW = {
|
|
|
951
979
|
devOnlyMessage: "素材管理僅在開發模式下可用。",
|
|
952
980
|
sectionAria: "投影片素材",
|
|
953
981
|
eyebrow: "素材",
|
|
982
|
+
scopeSlide: "此投影片",
|
|
983
|
+
scopeGlobal: "全域",
|
|
954
984
|
fileCount: {
|
|
955
985
|
one: "{count} 個檔案",
|
|
956
986
|
other: "{count} 個檔案"
|
|
@@ -969,7 +999,7 @@ const zhTW = {
|
|
|
969
999
|
renameMenuItem: "重新命名",
|
|
970
1000
|
deleteMenuItem: "刪除",
|
|
971
1001
|
conflictTitle: "檔案已存在",
|
|
972
|
-
conflictDescription: "{name}
|
|
1002
|
+
conflictDescription: "{name} 已在素材資料夾中。",
|
|
973
1003
|
conflictReplace: "取代",
|
|
974
1004
|
conflictRenameCopy: "重新命名副本",
|
|
975
1005
|
deleteAssetTitle: "刪除素材",
|
|
@@ -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;
|
|
@@ -93,7 +94,12 @@ type Locale = {
|
|
|
93
94
|
exportAsHtml: string;
|
|
94
95
|
exportAsPdf: string;
|
|
95
96
|
pdfExportFailed: string;
|
|
97
|
+
pdfExportSafariUnsupported: string;
|
|
96
98
|
present: string;
|
|
99
|
+
presentMenuAria: string;
|
|
100
|
+
presentInWindow: string;
|
|
101
|
+
presentFullscreen: string;
|
|
102
|
+
presentPresenter: string;
|
|
97
103
|
slidesTab: string;
|
|
98
104
|
assetsTab: string;
|
|
99
105
|
renameSlide: string;
|
|
@@ -136,6 +142,8 @@ type Locale = {
|
|
|
136
142
|
whiteoutAria: string;
|
|
137
143
|
laserAria: string;
|
|
138
144
|
presenterAria: string;
|
|
145
|
+
enterFullscreenAria: string;
|
|
146
|
+
exitFullscreenAria: string;
|
|
139
147
|
helpAria: string;
|
|
140
148
|
exitAria: string;
|
|
141
149
|
elapsedTime: string;
|
|
@@ -245,6 +253,8 @@ type Locale = {
|
|
|
245
253
|
devOnlyMessage: string;
|
|
246
254
|
sectionAria: string;
|
|
247
255
|
eyebrow: string;
|
|
256
|
+
scopeSlide: string;
|
|
257
|
+
scopeGlobal: string;
|
|
248
258
|
/** templates: "{count} file" / "{count} files" */
|
|
249
259
|
fileCount: Plural;
|
|
250
260
|
searchLogos: string;
|
|
@@ -263,7 +273,7 @@ type Locale = {
|
|
|
263
273
|
renameMenuItem: string;
|
|
264
274
|
deleteMenuItem: string;
|
|
265
275
|
conflictTitle: string;
|
|
266
|
-
/** template: "{name} is already in
|
|
276
|
+
/** template: "{name} is already in the assets folder." */
|
|
267
277
|
conflictDescription: string;
|
|
268
278
|
conflictReplace: string;
|
|
269
279
|
conflictRenameCopy: string;
|
package/dist/vite/index.d.ts
CHANGED
package/dist/vite/index.js
CHANGED
package/package.json
CHANGED
|
@@ -242,7 +242,7 @@ export default [Cover, Content] satisfies Page[];
|
|
|
242
242
|
|
|
243
243
|
## Assets
|
|
244
244
|
|
|
245
|
-
|
|
245
|
+
**Slide-local assets** live under `slides/<id>/assets/` — anything one-off to a single slide. Import them as ES modules:
|
|
246
246
|
|
|
247
247
|
```tsx
|
|
248
248
|
import hero from './assets/hero.jpg';
|
|
@@ -256,6 +256,14 @@ For URL-only access:
|
|
|
256
256
|
const videoUrl = new URL('./assets/intro.mp4', import.meta.url).href;
|
|
257
257
|
```
|
|
258
258
|
|
|
259
|
+
**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:
|
|
260
|
+
|
|
261
|
+
```tsx
|
|
262
|
+
import logo from '@assets/logos/acme.svg';
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
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.
|
|
266
|
+
|
|
259
267
|
Skip the `assets/` folder entirely for pure-text slides.
|
|
260
268
|
|
|
261
269
|
## Image placeholders
|
|
@@ -339,7 +347,7 @@ This applies whenever the *visual element* repeats, not whenever the *data* does
|
|
|
339
347
|
- [ ] 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
348
|
- [ ] One idea per page.
|
|
341
349
|
- [ ] 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
|
|
350
|
+
- [ ] All imported assets exist on disk — slide-local under `slides/<id>/assets/`, or global under `assets/` (imported via `@assets/...`).
|
|
343
351
|
- [ ] 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
352
|
- [ ] Nothing outside `slides/<id>/` was edited.
|
|
345
353
|
|
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 />} />
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
DropdownMenuItem,
|
|
31
31
|
DropdownMenuTrigger,
|
|
32
32
|
} from '@/components/ui/dropdown-menu';
|
|
33
|
+
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
33
34
|
import {
|
|
34
35
|
type AssetEntry,
|
|
35
36
|
fetchSvgAsFile,
|
|
@@ -41,7 +42,11 @@ import {
|
|
|
41
42
|
import { format, useLocale } from '@/lib/use-locale';
|
|
42
43
|
import { cn } from '@/lib/utils';
|
|
43
44
|
|
|
44
|
-
type Props = { slideId: string };
|
|
45
|
+
type Props = { slideId: string | null };
|
|
46
|
+
|
|
47
|
+
type Scope = 'slide' | 'global';
|
|
48
|
+
|
|
49
|
+
const GLOBAL_SLIDE_ID = '@global';
|
|
45
50
|
|
|
46
51
|
type ConflictState = {
|
|
47
52
|
file: File;
|
|
@@ -49,7 +54,10 @@ type ConflictState = {
|
|
|
49
54
|
};
|
|
50
55
|
|
|
51
56
|
export function AssetView({ slideId }: Props) {
|
|
52
|
-
const
|
|
57
|
+
const lockedToGlobal = slideId === null;
|
|
58
|
+
const [scope, setScope] = useState<Scope>(lockedToGlobal ? 'global' : 'slide');
|
|
59
|
+
const effectiveSlideId = scope === 'global' || slideId === null ? GLOBAL_SLIDE_ID : slideId;
|
|
60
|
+
const { assets, loading, available, upload, rename, remove } = useAssets(effectiveSlideId);
|
|
53
61
|
const [dragActive, setDragActive] = useState(false);
|
|
54
62
|
const [conflict, setConflict] = useState<ConflictState | null>(null);
|
|
55
63
|
const [preview, setPreview] = useState<AssetEntry | null>(null);
|
|
@@ -133,10 +141,21 @@ export function AssetView({ slideId }: Props) {
|
|
|
133
141
|
}}
|
|
134
142
|
>
|
|
135
143
|
<div className="flex shrink-0 items-center justify-between gap-3 border-b border-hairline bg-sidebar px-6 py-3">
|
|
136
|
-
<div className="min-w-0">
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
144
|
+
<div className="flex min-w-0 items-center gap-3">
|
|
145
|
+
{lockedToGlobal ? (
|
|
146
|
+
<span className="eyebrow">{t.asset.eyebrow}</span>
|
|
147
|
+
) : (
|
|
148
|
+
<Tabs value={scope} onValueChange={(next) => setScope(next as Scope)}>
|
|
149
|
+
<TabsList>
|
|
150
|
+
<TabsTrigger value="slide">{t.asset.scopeSlide}</TabsTrigger>
|
|
151
|
+
<TabsTrigger value="global">{t.asset.scopeGlobal}</TabsTrigger>
|
|
152
|
+
</TabsList>
|
|
153
|
+
</Tabs>
|
|
154
|
+
)}
|
|
155
|
+
<p className="min-w-0 truncate text-[12px] text-muted-foreground">
|
|
156
|
+
<span className="font-mono text-[11.5px]">
|
|
157
|
+
{scope === 'global' ? 'assets/' : `slides/${slideId}/assets/`}
|
|
158
|
+
</span>
|
|
140
159
|
{!loading && (
|
|
141
160
|
<>
|
|
142
161
|
<span className="mx-2 opacity-50">·</span>
|
|
@@ -274,7 +293,7 @@ export function AssetView({ slideId }: Props) {
|
|
|
274
293
|
/>
|
|
275
294
|
)}
|
|
276
295
|
|
|
277
|
-
{preview && <PreviewDialog asset={preview} onClose={() => setPreview(null)} />}
|
|
296
|
+
{preview && <PreviewDialog asset={preview} scope={scope} onClose={() => setPreview(null)} />}
|
|
278
297
|
|
|
279
298
|
{logoSearchOpen && (
|
|
280
299
|
<LogoSearchDialog
|
|
@@ -542,9 +561,17 @@ function NoResultsMessage({ query, t }: { query: string; t: ReturnType<typeof us
|
|
|
542
561
|
);
|
|
543
562
|
}
|
|
544
563
|
|
|
545
|
-
function PreviewDialog({
|
|
564
|
+
function PreviewDialog({
|
|
565
|
+
asset,
|
|
566
|
+
scope,
|
|
567
|
+
onClose,
|
|
568
|
+
}: {
|
|
569
|
+
asset: AssetEntry;
|
|
570
|
+
scope: Scope;
|
|
571
|
+
onClose: () => void;
|
|
572
|
+
}) {
|
|
546
573
|
const isImage = asset.mime.startsWith('image/');
|
|
547
|
-
const importPath = `./assets/${asset.name}`;
|
|
574
|
+
const importPath = scope === 'global' ? `@assets/${asset.name}` : `./assets/${asset.name}`;
|
|
548
575
|
const t = useLocale();
|
|
549
576
|
return (
|
|
550
577
|
<Dialog open onOpenChange={(open) => !open && onClose()}>
|
|
@@ -31,7 +31,7 @@ export function InspectOverlay() {
|
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
const onMove = (e: PointerEvent) => {
|
|
34
|
-
const el = pickElement(e.clientX, e.clientY);
|
|
34
|
+
const el = pickInspectorTarget(pickElement(e.clientX, e.clientY));
|
|
35
35
|
if (!el) return setHover(null);
|
|
36
36
|
const hit = findSlideSource(el, slideId, { hostOnly: true });
|
|
37
37
|
if (!hit) return setHover(null);
|
|
@@ -40,7 +40,7 @@ export function InspectOverlay() {
|
|
|
40
40
|
|
|
41
41
|
const onClick = (e: MouseEvent) => {
|
|
42
42
|
if (e.target instanceof Element && e.target.closest('[data-inspector-ui]')) return;
|
|
43
|
-
const el = pickElement(e.clientX, e.clientY);
|
|
43
|
+
const el = pickInspectorTarget(pickElement(e.clientX, e.clientY));
|
|
44
44
|
if (!el) return;
|
|
45
45
|
const hit = findSlideSource(el, slideId, { hostOnly: true });
|
|
46
46
|
if (!hit) return;
|
|
@@ -52,7 +52,7 @@ export function InspectOverlay() {
|
|
|
52
52
|
|
|
53
53
|
const onDblClick = (e: MouseEvent) => {
|
|
54
54
|
if (e.target instanceof Element && e.target.closest('[data-inspector-ui]')) return;
|
|
55
|
-
const el = pickElement(e.clientX, e.clientY);
|
|
55
|
+
const el = pickInspectorTarget(pickElement(e.clientX, e.clientY));
|
|
56
56
|
if (!el) return;
|
|
57
57
|
const hit = findSlideSource(el, slideId, { hostOnly: true });
|
|
58
58
|
if (!hit) return;
|
|
@@ -221,3 +221,49 @@ function pickElement(x: number, y: number): HTMLElement | null {
|
|
|
221
221
|
}
|
|
222
222
|
return null;
|
|
223
223
|
}
|
|
224
|
+
|
|
225
|
+
const INLINE_TEXT_TAGS = new Set([
|
|
226
|
+
'B',
|
|
227
|
+
'CODE',
|
|
228
|
+
'DEL',
|
|
229
|
+
'EM',
|
|
230
|
+
'I',
|
|
231
|
+
'INS',
|
|
232
|
+
'MARK',
|
|
233
|
+
'S',
|
|
234
|
+
'SMALL',
|
|
235
|
+
'SPAN',
|
|
236
|
+
'STRONG',
|
|
237
|
+
'SUB',
|
|
238
|
+
'SUP',
|
|
239
|
+
'U',
|
|
240
|
+
]);
|
|
241
|
+
|
|
242
|
+
function pickInspectorTarget(el: HTMLElement | null): HTMLElement | null {
|
|
243
|
+
if (!el) return null;
|
|
244
|
+
const root = el.closest('[data-inspector-root]');
|
|
245
|
+
const startedOnInlineText = INLINE_TEXT_TAGS.has(el.tagName);
|
|
246
|
+
for (let cur: HTMLElement | null = el; cur && root?.contains(cur); cur = cur.parentElement) {
|
|
247
|
+
if (startedOnInlineText && INLINE_TEXT_TAGS.has(cur.tagName)) continue;
|
|
248
|
+
if (isEditableTextContainer(cur)) return cur;
|
|
249
|
+
}
|
|
250
|
+
return el;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function isEditableTextContainer(el: HTMLElement): boolean {
|
|
254
|
+
if (!el.textContent?.trim()) return false;
|
|
255
|
+
return hasOnlyInlineTextChildren(el);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function hasOnlyInlineTextChildren(el: HTMLElement): boolean {
|
|
259
|
+
for (const child of Array.from(el.childNodes)) {
|
|
260
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
261
|
+
continue;
|
|
262
|
+
} else if (child instanceof HTMLElement) {
|
|
263
|
+
if (child.tagName === 'BR') continue;
|
|
264
|
+
if (INLINE_TEXT_TAGS.has(child.tagName) && hasOnlyInlineTextChildren(child)) continue;
|
|
265
|
+
}
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
return true;
|
|
269
|
+
}
|