@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.
- package/dist/{build-_276DMmJ.js → build-DZhbjQpQ.js} +1 -1
- package/dist/cli/bin.js +3 -3
- package/dist/{config-D9cZ1A0X.d.ts → config-BQdTMho4.d.ts} +2 -1
- package/dist/{config-BAwKWNtW.js → config-iKjqaX08.js} +2528 -1640
- package/dist/{dev-BoqeVXVq.js → dev-BjLGk5nN.js} +1 -1
- package/dist/{en-CDKzoZvf.js → en-DDGqyNaW.js} +27 -4
- package/dist/index.d.ts +4 -2
- package/dist/index.js +1 -1
- package/dist/locale/index.d.ts +1 -1
- package/dist/locale/index.js +82 -13
- package/dist/{preview-BLPxspc9.js → preview-jwLWHWkQ.js} +1 -1
- package/dist/{types-JYG1cmwC.d.ts → types-Dpr8nbih.d.ts} +27 -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 +19 -4
- package/src/app/app.tsx +2 -0
- package/src/app/components/asset-view.tsx +111 -18
- package/src/app/components/inspector/inspect-overlay.tsx +49 -3
- package/src/app/components/inspector/inspector-panel.tsx +267 -25
- package/src/app/components/inspector/inspector-provider.tsx +390 -49
- package/src/app/components/panel/panel-shell.tsx +5 -3
- package/src/app/components/player.tsx +25 -5
- package/src/app/components/present/control-bar.tsx +12 -0
- package/src/app/components/present/laser-pointer.tsx +3 -4
- package/src/app/components/present/progress-bar.tsx +4 -4
- package/src/app/components/sidebar/folder-item.tsx +14 -3
- package/src/app/components/sidebar/sidebar.tsx +10 -0
- package/src/app/lib/assets.ts +21 -0
- package/src/app/lib/export-pdf.ts +6 -0
- package/src/app/lib/inspector/use-editor.ts +9 -1
- package/src/app/lib/sdk.ts +2 -0
- package/src/app/lib/slides.ts +9 -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/home.tsx +101 -3
- package/src/app/routes/presenter.tsx +2 -20
- package/src/app/routes/slide.tsx +117 -39
- package/src/app/virtual.d.ts +1 -0
- package/src/locale/en.ts +28 -5
- package/src/locale/ja.ts +28 -5
- package/src/locale/types.ts +27 -1
- package/src/locale/zh-cn.ts +28 -6
- package/src/locale/zh-tw.ts +28 -6
|
@@ -30,10 +30,14 @@ 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,
|
|
36
|
+
type AssetUsage,
|
|
35
37
|
fetchSvgAsFile,
|
|
38
|
+
listAssetUsages,
|
|
36
39
|
renamedCopy,
|
|
40
|
+
revertAssetUsage,
|
|
37
41
|
type SvglItem,
|
|
38
42
|
searchSvgl,
|
|
39
43
|
useAssets,
|
|
@@ -41,7 +45,11 @@ import {
|
|
|
41
45
|
import { format, useLocale } from '@/lib/use-locale';
|
|
42
46
|
import { cn } from '@/lib/utils';
|
|
43
47
|
|
|
44
|
-
type Props = { slideId: string };
|
|
48
|
+
type Props = { slideId: string | null };
|
|
49
|
+
|
|
50
|
+
type Scope = 'slide' | 'global';
|
|
51
|
+
|
|
52
|
+
const GLOBAL_SLIDE_ID = '@global';
|
|
45
53
|
|
|
46
54
|
type ConflictState = {
|
|
47
55
|
file: File;
|
|
@@ -49,11 +57,15 @@ type ConflictState = {
|
|
|
49
57
|
};
|
|
50
58
|
|
|
51
59
|
export function AssetView({ slideId }: Props) {
|
|
52
|
-
const
|
|
60
|
+
const lockedToGlobal = slideId === null;
|
|
61
|
+
const [scope, setScope] = useState<Scope>(lockedToGlobal ? 'global' : 'slide');
|
|
62
|
+
const effectiveSlideId = scope === 'global' || slideId === null ? GLOBAL_SLIDE_ID : slideId;
|
|
63
|
+
const { assets, loading, available, upload, rename, remove } = useAssets(effectiveSlideId);
|
|
53
64
|
const [dragActive, setDragActive] = useState(false);
|
|
54
65
|
const [conflict, setConflict] = useState<ConflictState | null>(null);
|
|
55
66
|
const [preview, setPreview] = useState<AssetEntry | null>(null);
|
|
56
67
|
const [confirmDelete, setConfirmDelete] = useState<AssetEntry | null>(null);
|
|
68
|
+
const [confirmDeleteUsages, setConfirmDeleteUsages] = useState<AssetUsage[] | null>(null);
|
|
57
69
|
const [renaming, setRenaming] = useState<string | null>(null);
|
|
58
70
|
const [logoSearchOpen, setLogoSearchOpen] = useState(false);
|
|
59
71
|
const dragDepth = useRef(0);
|
|
@@ -133,10 +145,21 @@ export function AssetView({ slideId }: Props) {
|
|
|
133
145
|
}}
|
|
134
146
|
>
|
|
135
147
|
<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
|
-
|
|
148
|
+
<div className="flex min-w-0 items-center gap-3">
|
|
149
|
+
{lockedToGlobal ? (
|
|
150
|
+
<span className="eyebrow">{t.asset.eyebrow}</span>
|
|
151
|
+
) : (
|
|
152
|
+
<Tabs value={scope} onValueChange={(next) => setScope(next as Scope)}>
|
|
153
|
+
<TabsList>
|
|
154
|
+
<TabsTrigger value="slide">{t.asset.scopeSlide}</TabsTrigger>
|
|
155
|
+
<TabsTrigger value="global">{t.asset.scopeGlobal}</TabsTrigger>
|
|
156
|
+
</TabsList>
|
|
157
|
+
</Tabs>
|
|
158
|
+
)}
|
|
159
|
+
<p className="min-w-0 truncate text-[12px] text-muted-foreground">
|
|
160
|
+
<span className="font-mono text-[11.5px]">
|
|
161
|
+
{scope === 'global' ? 'assets/' : `slides/${slideId}/assets/`}
|
|
162
|
+
</span>
|
|
140
163
|
{!loading && (
|
|
141
164
|
<>
|
|
142
165
|
<span className="mx-2 opacity-50">·</span>
|
|
@@ -226,7 +249,13 @@ export function AssetView({ slideId }: Props) {
|
|
|
226
249
|
asset={asset}
|
|
227
250
|
onPreview={() => setPreview(asset)}
|
|
228
251
|
onRename={() => setRenaming(asset.name)}
|
|
229
|
-
onDelete={() =>
|
|
252
|
+
onDelete={() => {
|
|
253
|
+
setConfirmDelete(asset);
|
|
254
|
+
setConfirmDeleteUsages(null);
|
|
255
|
+
listAssetUsages(effectiveSlideId, asset.name)
|
|
256
|
+
.then((u) => setConfirmDeleteUsages(u))
|
|
257
|
+
.catch(() => setConfirmDeleteUsages([]));
|
|
258
|
+
}}
|
|
230
259
|
/>
|
|
231
260
|
),
|
|
232
261
|
)}
|
|
@@ -263,18 +292,46 @@ export function AssetView({ slideId }: Props) {
|
|
|
263
292
|
{confirmDelete && (
|
|
264
293
|
<DeleteDialog
|
|
265
294
|
asset={confirmDelete}
|
|
266
|
-
|
|
295
|
+
usages={confirmDeleteUsages}
|
|
296
|
+
onCancel={() => {
|
|
297
|
+
setConfirmDelete(null);
|
|
298
|
+
setConfirmDeleteUsages(null);
|
|
299
|
+
}}
|
|
267
300
|
onConfirm={async () => {
|
|
268
301
|
const target = confirmDelete;
|
|
302
|
+
const usages = confirmDeleteUsages ?? [];
|
|
269
303
|
setConfirmDelete(null);
|
|
304
|
+
setConfirmDeleteUsages(null);
|
|
305
|
+
const assetPath =
|
|
306
|
+
scope === 'global' ? `@assets/${target.name}` : `./assets/${target.name}`;
|
|
307
|
+
for (const u of usages) {
|
|
308
|
+
const rev = await revertAssetUsage(u.slideId, assetPath);
|
|
309
|
+
if (!rev.ok) {
|
|
310
|
+
toast.error(format(t.asset.toastRevertFailed, { slideId: u.slideId }));
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
270
314
|
const res = await remove(target.name);
|
|
271
|
-
if (!res.ok)
|
|
272
|
-
|
|
315
|
+
if (!res.ok) {
|
|
316
|
+
toast.error(format(t.asset.toastDeleteFailed, { status: res.status }));
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const totalUsages = usages.reduce((acc, u) => acc + u.count, 0);
|
|
320
|
+
if (totalUsages > 0) {
|
|
321
|
+
toast.success(
|
|
322
|
+
format(t.asset.toastDeletedWithRevert, {
|
|
323
|
+
name: target.name,
|
|
324
|
+
count: totalUsages,
|
|
325
|
+
}),
|
|
326
|
+
);
|
|
327
|
+
} else {
|
|
328
|
+
toast.success(format(t.asset.toastDeleted, { name: target.name }));
|
|
329
|
+
}
|
|
273
330
|
}}
|
|
274
331
|
/>
|
|
275
332
|
)}
|
|
276
333
|
|
|
277
|
-
{preview && <PreviewDialog asset={preview} onClose={() => setPreview(null)} />}
|
|
334
|
+
{preview && <PreviewDialog asset={preview} scope={scope} onClose={() => setPreview(null)} />}
|
|
278
335
|
|
|
279
336
|
{logoSearchOpen && (
|
|
280
337
|
<LogoSearchDialog
|
|
@@ -498,14 +555,19 @@ function ConflictDialog({
|
|
|
498
555
|
|
|
499
556
|
function DeleteDialog({
|
|
500
557
|
asset,
|
|
558
|
+
usages,
|
|
501
559
|
onCancel,
|
|
502
560
|
onConfirm,
|
|
503
561
|
}: {
|
|
504
562
|
asset: AssetEntry;
|
|
563
|
+
usages: AssetUsage[] | null;
|
|
505
564
|
onCancel: () => void;
|
|
506
565
|
onConfirm: () => void;
|
|
507
566
|
}) {
|
|
508
567
|
const t = useLocale();
|
|
568
|
+
const inUse = (usages?.length ?? 0) > 0;
|
|
569
|
+
const totalUses = usages?.reduce((acc, u) => acc + u.count, 0) ?? 0;
|
|
570
|
+
const slideCount = usages?.length ?? 0;
|
|
509
571
|
const [descPrefix, descSuffix] = t.asset.deleteAssetDescription.split('{name}');
|
|
510
572
|
return (
|
|
511
573
|
<Dialog open onOpenChange={(open) => !open && onCancel()}>
|
|
@@ -513,17 +575,40 @@ function DeleteDialog({
|
|
|
513
575
|
<DialogHeader>
|
|
514
576
|
<DialogTitle>{t.asset.deleteAssetTitle}</DialogTitle>
|
|
515
577
|
<DialogDescription>
|
|
516
|
-
{
|
|
517
|
-
|
|
518
|
-
|
|
578
|
+
{inUse ? (
|
|
579
|
+
<>
|
|
580
|
+
{format(t.asset.deleteAssetInUseDescription, {
|
|
581
|
+
name: asset.name,
|
|
582
|
+
count: totalUses,
|
|
583
|
+
slides: slideCount,
|
|
584
|
+
})}{' '}
|
|
585
|
+
{t.asset.deleteAssetInUseHint}
|
|
586
|
+
</>
|
|
587
|
+
) : (
|
|
588
|
+
<>
|
|
589
|
+
{descPrefix}
|
|
590
|
+
<span className="font-mono">{asset.name}</span>
|
|
591
|
+
{descSuffix}
|
|
592
|
+
</>
|
|
593
|
+
)}
|
|
519
594
|
</DialogDescription>
|
|
520
595
|
</DialogHeader>
|
|
596
|
+
{inUse && usages && (
|
|
597
|
+
<ul className="max-h-40 overflow-y-auto rounded-[5px] border border-hairline bg-muted/40 px-3 py-2 font-mono text-[11.5px] leading-relaxed">
|
|
598
|
+
{usages.map((u) => (
|
|
599
|
+
<li key={u.slideId} className="flex items-center justify-between gap-3">
|
|
600
|
+
<span className="truncate">{u.slideId}</span>
|
|
601
|
+
<span className="text-muted-foreground">×{u.count}</span>
|
|
602
|
+
</li>
|
|
603
|
+
))}
|
|
604
|
+
</ul>
|
|
605
|
+
)}
|
|
521
606
|
<DialogFooter>
|
|
522
607
|
<Button variant="outline" onClick={onCancel}>
|
|
523
608
|
{t.common.cancel}
|
|
524
609
|
</Button>
|
|
525
|
-
<Button variant="destructive" onClick={onConfirm}>
|
|
526
|
-
{t.common.delete}
|
|
610
|
+
<Button variant="destructive" onClick={onConfirm} disabled={usages === null}>
|
|
611
|
+
{inUse ? t.asset.deleteAndRevert : t.common.delete}
|
|
527
612
|
</Button>
|
|
528
613
|
</DialogFooter>
|
|
529
614
|
</DialogContent>
|
|
@@ -542,9 +627,17 @@ function NoResultsMessage({ query, t }: { query: string; t: ReturnType<typeof us
|
|
|
542
627
|
);
|
|
543
628
|
}
|
|
544
629
|
|
|
545
|
-
function PreviewDialog({
|
|
630
|
+
function PreviewDialog({
|
|
631
|
+
asset,
|
|
632
|
+
scope,
|
|
633
|
+
onClose,
|
|
634
|
+
}: {
|
|
635
|
+
asset: AssetEntry;
|
|
636
|
+
scope: Scope;
|
|
637
|
+
onClose: () => void;
|
|
638
|
+
}) {
|
|
546
639
|
const isImage = asset.mime.startsWith('image/');
|
|
547
|
-
const importPath = `./assets/${asset.name}`;
|
|
640
|
+
const importPath = scope === 'global' ? `@assets/${asset.name}` : `./assets/${asset.name}`;
|
|
548
641
|
const t = useLocale();
|
|
549
642
|
return (
|
|
550
643
|
<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
|
+
}
|