@open-slide/core 1.1.0 → 1.3.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-DSqSio-T.js → build-_276DMmJ.js} +2 -2
- package/dist/cli/bin.js +5 -5
- package/dist/{config-KdiYeWtK.js → config-BAwKWNtW.js} +888 -229
- package/dist/{config-C7vMYzFD.d.ts → config-D9cZ1A0X.d.ts} +2 -1
- package/dist/{dev-B_GVbr11.js → dev-BoqeVXVq.js} +2 -2
- package/dist/en-CDKzoZvf.js +351 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +229 -39
- package/dist/locale/index.d.ts +1 -1
- package/dist/locale/index.js +166 -326
- package/dist/{preview-D_mxhj7w.js → preview-BLPxspc9.js} +2 -2
- package/dist/sync-j9_QPovT.js +3 -0
- package/dist/{types-DYgVpIGo.d.ts → types-JYG1cmwC.d.ts} +59 -5
- package/dist/vite/index.d.ts +2 -2
- package/dist/vite/index.js +2 -2
- package/package.json +9 -1
- package/skills/create-slide/SKILL.md +1 -1
- package/skills/create-theme/SKILL.md +60 -12
- package/skills/current-slide/SKILL.md +110 -0
- package/skills/slide-authoring/SKILL.md +59 -1
- package/src/app/app.tsx +11 -1
- package/src/app/components/asset-view.tsx +1 -13
- package/src/app/components/image-placeholder.tsx +123 -1
- package/src/app/components/inspector/image-crop-dialog.tsx +64 -20
- package/src/app/components/inspector/inspector-panel.tsx +163 -19
- package/src/app/components/inspector/inspector-provider.tsx +60 -7
- package/src/app/components/notes-drawer.tsx +117 -0
- package/src/app/components/player.tsx +11 -7
- package/src/app/components/present/overview-grid.tsx +2 -2
- package/src/app/components/sidebar/folder-item.tsx +16 -5
- package/src/app/components/sidebar/mobile-pill.tsx +34 -0
- package/src/app/components/sidebar/sidebar.tsx +10 -0
- package/src/app/components/themes/theme-detail.tsx +300 -0
- package/src/app/components/themes/themes-gallery.tsx +146 -0
- package/src/app/components/thumbnail-rail.tsx +136 -29
- package/src/app/components/ui/context-menu.tsx +237 -0
- package/src/app/lib/assets.ts +55 -2
- package/src/app/lib/inspector/use-notes.ts +134 -0
- package/src/app/lib/sdk.ts +1 -0
- package/src/app/lib/slides.ts +10 -1
- package/src/app/lib/themes.ts +22 -0
- package/src/app/lib/use-agent-socket.ts +18 -0
- package/src/app/routes/home-shell.tsx +173 -0
- package/src/app/routes/home.tsx +108 -204
- package/src/app/routes/slide.tsx +333 -68
- package/src/app/routes/themes.tsx +34 -0
- package/src/app/virtual.d.ts +20 -0
- package/src/locale/en.ts +61 -7
- package/src/locale/ja.ts +62 -7
- package/src/locale/types.ts +62 -5
- package/src/locale/zh-cn.ts +61 -7
- package/src/locale/zh-tw.ts +61 -7
- package/dist/sync-B4eLo2H6.js +0 -3
- /package/dist/{design-C13iz9_4.js → design-cpzS8aud.js} +0 -0
- /package/dist/{sync-3oqN1WyK.js → sync-BCJDRIqo.js} +0 -0
package/src/app/routes/home.tsx
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import {
|
|
2
|
+
FolderInput,
|
|
3
|
+
FolderPlus,
|
|
4
|
+
MoreHorizontal,
|
|
5
|
+
Palette,
|
|
6
|
+
Pencil,
|
|
7
|
+
Search,
|
|
8
|
+
Trash2,
|
|
9
|
+
X,
|
|
10
|
+
} from 'lucide-react';
|
|
11
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
12
|
+
import { Link, useOutletContext } from 'react-router-dom';
|
|
5
13
|
import { Button } from '@/components/ui/button';
|
|
6
14
|
import {
|
|
7
15
|
Dialog,
|
|
@@ -17,52 +25,30 @@ import {
|
|
|
17
25
|
DropdownMenuItem,
|
|
18
26
|
DropdownMenuTrigger,
|
|
19
27
|
} from '@/components/ui/dropdown-menu';
|
|
20
|
-
import {
|
|
21
|
-
import { format, useLocale } from '@/lib/use-locale';
|
|
28
|
+
import { useLocale } from '@/lib/use-locale';
|
|
22
29
|
import { cn } from '@/lib/utils';
|
|
23
30
|
import { FolderIconChip, SLIDE_DND_MIME } from '../components/sidebar/folder-item';
|
|
24
|
-
import { DRAFT_ID
|
|
31
|
+
import { DRAFT_ID } from '../components/sidebar/sidebar';
|
|
25
32
|
import { SlideCanvas } from '../components/slide-canvas';
|
|
26
33
|
import type { Folder, FolderIcon, SlideModule } from '../lib/sdk';
|
|
27
|
-
import { loadSlide
|
|
34
|
+
import { loadSlide } from '../lib/slides';
|
|
35
|
+
import type { HomeOutletContext } from './home-shell';
|
|
28
36
|
|
|
29
37
|
export function Home() {
|
|
30
|
-
const {
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
const {
|
|
39
|
+
manifest,
|
|
40
|
+
loading,
|
|
41
|
+
draftSlides,
|
|
42
|
+
slidesByFolder,
|
|
43
|
+
selectedId,
|
|
44
|
+
reportTitle,
|
|
45
|
+
titleMap,
|
|
46
|
+
assign,
|
|
47
|
+
renameSlide,
|
|
48
|
+
deleteSlide,
|
|
49
|
+
} = useOutletContext<HomeOutletContext>();
|
|
33
50
|
const t = useLocale();
|
|
34
51
|
|
|
35
|
-
const selectFolder = (id: string) => {
|
|
36
|
-
setSearchParams(
|
|
37
|
-
(prev) => {
|
|
38
|
-
const next = new URLSearchParams(prev);
|
|
39
|
-
if (id === DRAFT_ID) next.delete('f');
|
|
40
|
-
else next.set('f', id);
|
|
41
|
-
return next;
|
|
42
|
-
},
|
|
43
|
-
{ replace: true },
|
|
44
|
-
);
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
const { draftSlides, slidesByFolder } = useMemo(() => {
|
|
48
|
-
const byFolder: Record<string, string[]> = {};
|
|
49
|
-
const draft: string[] = [];
|
|
50
|
-
const known = new Set(manifest.folders.map((f) => f.id));
|
|
51
|
-
for (const id of slideIds) {
|
|
52
|
-
const folderId = manifest.assignments[id];
|
|
53
|
-
if (folderId && known.has(folderId)) {
|
|
54
|
-
byFolder[folderId] ??= [];
|
|
55
|
-
byFolder[folderId].push(id);
|
|
56
|
-
} else {
|
|
57
|
-
draft.push(id);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return { draftSlides: draft, slidesByFolder: byFolder };
|
|
61
|
-
}, [manifest]);
|
|
62
|
-
|
|
63
|
-
const countFor = (folderId: string | null) =>
|
|
64
|
-
folderId === null ? draftSlides.length : (slidesByFolder[folderId]?.length ?? 0);
|
|
65
|
-
|
|
66
52
|
const selectedFolder =
|
|
67
53
|
selectedId === DRAFT_ID ? null : (manifest.folders.find((f) => f.id === selectedId) ?? null);
|
|
68
54
|
const visibleSlides = selectedId === DRAFT_ID ? draftSlides : (slidesByFolder[selectedId] ?? []);
|
|
@@ -72,173 +58,68 @@ export function Home() {
|
|
|
72
58
|
const isDraft = selectedId === DRAFT_ID;
|
|
73
59
|
|
|
74
60
|
const [query, setQuery] = useState('');
|
|
75
|
-
const [titleMap, setTitleMap] = useState<Record<string, string>>({});
|
|
76
|
-
const reportTitle = useCallback((slideId: string, slideTitle: string) => {
|
|
77
|
-
setTitleMap((prev) =>
|
|
78
|
-
prev[slideId] === slideTitle ? prev : { ...prev, [slideId]: slideTitle },
|
|
79
|
-
);
|
|
80
|
-
}, []);
|
|
81
|
-
|
|
82
|
-
const moveSlideWithToast = useCallback(
|
|
83
|
-
async (slideId: string, folderId: string | null) => {
|
|
84
|
-
if (manifest.assignments[slideId] === (folderId ?? undefined)) return;
|
|
85
|
-
const slideName = titleMap[slideId] ?? slideId;
|
|
86
|
-
const folderName =
|
|
87
|
-
folderId === null
|
|
88
|
-
? t.home.draft
|
|
89
|
-
: (manifest.folders.find((f) => f.id === folderId)?.name ?? folderId);
|
|
90
|
-
try {
|
|
91
|
-
await assign(slideId, folderId);
|
|
92
|
-
toast.success(format(t.home.toastSlideMoved, { slide: slideName, folder: folderName }));
|
|
93
|
-
} catch {
|
|
94
|
-
toast.error(t.home.toastSlideMoveFailed);
|
|
95
|
-
}
|
|
96
|
-
},
|
|
97
|
-
[assign, manifest, titleMap, t],
|
|
98
|
-
);
|
|
99
61
|
|
|
100
62
|
const trimmedQuery = query.trim().toLowerCase();
|
|
101
63
|
const filteredSlides = useMemo(() => {
|
|
102
64
|
if (!trimmedQuery) return visibleSlides;
|
|
103
65
|
return visibleSlides.filter((id) => {
|
|
104
66
|
if (id.toLowerCase().includes(trimmedQuery)) return true;
|
|
105
|
-
const
|
|
106
|
-
return
|
|
67
|
+
const tl = titleMap[id]?.toLowerCase();
|
|
68
|
+
return tl ? tl.includes(trimmedQuery) : false;
|
|
107
69
|
});
|
|
108
70
|
}, [visibleSlides, titleMap, trimmedQuery]);
|
|
109
71
|
const isSearching = trimmedQuery.length > 0;
|
|
110
72
|
|
|
111
73
|
return (
|
|
112
|
-
|
|
113
|
-
<
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
}}
|
|
132
|
-
onDropToFolder={(folderId, slideId) => moveSlideWithToast(slideId, folderId)}
|
|
133
|
-
onDropToDraft={(slideId) => moveSlideWithToast(slideId, null)}
|
|
134
|
-
/>
|
|
135
|
-
</div>
|
|
136
|
-
|
|
137
|
-
<div className="paper relative flex min-w-0 flex-1 flex-col overflow-y-auto bg-canvas">
|
|
138
|
-
{/* Mobile chrome */}
|
|
139
|
-
<div className="flex items-center justify-between border-b border-hairline bg-sidebar px-4 py-3 md:hidden">
|
|
140
|
-
<h1 className="font-heading text-lg font-bold tracking-tight">{t.home.appTitle}</h1>
|
|
141
|
-
</div>
|
|
142
|
-
<div className="border-b border-hairline bg-sidebar px-4 py-2 md:hidden">
|
|
143
|
-
<div className="flex gap-2 overflow-x-auto pb-1">
|
|
144
|
-
<MobileFolderPill
|
|
145
|
-
icon={{ type: 'emoji', value: '📝' }}
|
|
146
|
-
label={t.home.draft}
|
|
147
|
-
count={countFor(null)}
|
|
148
|
-
active={selectedId === DRAFT_ID}
|
|
149
|
-
onClick={() => selectFolder(DRAFT_ID)}
|
|
150
|
-
/>
|
|
151
|
-
{manifest.folders.map((f) => (
|
|
152
|
-
<MobileFolderPill
|
|
153
|
-
key={f.id}
|
|
154
|
-
icon={f.icon}
|
|
155
|
-
label={f.name}
|
|
156
|
-
count={countFor(f.id)}
|
|
157
|
-
active={selectedId === f.id}
|
|
158
|
-
onClick={() => selectFolder(f.id)}
|
|
159
|
-
/>
|
|
160
|
-
))}
|
|
161
|
-
</div>
|
|
162
|
-
</div>
|
|
163
|
-
|
|
164
|
-
<div className="mx-auto w-full max-w-[1180px] px-5 py-8 md:px-10 md:py-12">
|
|
165
|
-
<header className="mb-8 md:mb-12">
|
|
166
|
-
<div className="flex flex-wrap items-center gap-3">
|
|
167
|
-
<FolderIconChip icon={headerIcon} className="size-7 text-2xl" />
|
|
168
|
-
<h1 className="font-heading text-[32px] font-semibold leading-[1.05] tracking-[-0.025em] md:text-[44px]">
|
|
169
|
-
{title}
|
|
170
|
-
</h1>
|
|
171
|
-
<span className="folio ml-1 self-end pb-2">
|
|
172
|
-
{(isSearching ? filteredSlides.length : visibleSlides.length)
|
|
173
|
-
.toString()
|
|
174
|
-
.padStart(2, '0')}
|
|
175
|
-
{isSearching && (
|
|
176
|
-
<span className="opacity-40">
|
|
177
|
-
/{visibleSlides.length.toString().padStart(2, '0')}
|
|
178
|
-
</span>
|
|
179
|
-
)}
|
|
180
|
-
</span>
|
|
181
|
-
<div className="ml-auto w-full md:w-auto">
|
|
182
|
-
<SearchInput value={query} onChange={setQuery} />
|
|
183
|
-
</div>
|
|
184
|
-
</div>
|
|
185
|
-
</header>
|
|
186
|
-
|
|
187
|
-
{visibleSlides.length === 0 ? (
|
|
188
|
-
<EmptyState isDraft={isDraft} folderName={selectedFolder?.name} />
|
|
189
|
-
) : filteredSlides.length === 0 ? (
|
|
190
|
-
<NoResultsState query={query} onClear={() => setQuery('')} />
|
|
191
|
-
) : (
|
|
192
|
-
<ul className="grid grid-cols-[repeat(auto-fill,minmax(240px,1fr))] gap-x-6 gap-y-9 md:grid-cols-[repeat(auto-fill,minmax(300px,1fr))]">
|
|
193
|
-
{filteredSlides.map((id) => (
|
|
194
|
-
<li key={id}>
|
|
195
|
-
<SlideCard
|
|
196
|
-
id={id}
|
|
197
|
-
folders={manifest.folders}
|
|
198
|
-
currentFolderId={manifest.assignments[id] ?? null}
|
|
199
|
-
onRename={(name) => renameSlide(id, name)}
|
|
200
|
-
onMove={(folderId) => assign(id, folderId)}
|
|
201
|
-
onDelete={() => deleteSlide(id)}
|
|
202
|
-
onTitleResolved={reportTitle}
|
|
203
|
-
/>
|
|
204
|
-
</li>
|
|
205
|
-
))}
|
|
206
|
-
</ul>
|
|
74
|
+
<>
|
|
75
|
+
<header className="mb-8 md:mb-12">
|
|
76
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
77
|
+
<FolderIconChip icon={headerIcon} className="size-7 text-2xl" />
|
|
78
|
+
<h1 className="font-heading text-[32px] font-semibold leading-[1.05] tracking-[-0.025em] md:text-[44px]">
|
|
79
|
+
{title}
|
|
80
|
+
</h1>
|
|
81
|
+
{!loading && (
|
|
82
|
+
<span className="folio ml-1 self-end pb-2">
|
|
83
|
+
{(isSearching ? filteredSlides.length : visibleSlides.length)
|
|
84
|
+
.toString()
|
|
85
|
+
.padStart(2, '0')}
|
|
86
|
+
{isSearching && (
|
|
87
|
+
<span className="opacity-40">
|
|
88
|
+
/{visibleSlides.length.toString().padStart(2, '0')}
|
|
89
|
+
</span>
|
|
90
|
+
)}
|
|
91
|
+
</span>
|
|
207
92
|
)}
|
|
93
|
+
<div className="ml-auto w-full md:w-auto">
|
|
94
|
+
<SearchInput value={query} onChange={setQuery} />
|
|
95
|
+
</div>
|
|
208
96
|
</div>
|
|
209
|
-
</
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
active
|
|
234
|
-
? 'border-foreground/40 bg-foreground text-background'
|
|
235
|
-
: 'border-border bg-card text-muted-foreground hover:text-foreground',
|
|
97
|
+
</header>
|
|
98
|
+
|
|
99
|
+
{loading ? (
|
|
100
|
+
<HomeLoading />
|
|
101
|
+
) : visibleSlides.length === 0 ? (
|
|
102
|
+
<EmptyState isDraft={isDraft} folderName={selectedFolder?.name} />
|
|
103
|
+
) : filteredSlides.length === 0 ? (
|
|
104
|
+
<NoResultsState query={query} onClear={() => setQuery('')} />
|
|
105
|
+
) : (
|
|
106
|
+
<ul className="grid grid-cols-[repeat(auto-fill,minmax(240px,1fr))] gap-x-6 gap-y-9 md:grid-cols-[repeat(auto-fill,minmax(300px,1fr))]">
|
|
107
|
+
{filteredSlides.map((id) => (
|
|
108
|
+
<li key={id}>
|
|
109
|
+
<SlideCard
|
|
110
|
+
id={id}
|
|
111
|
+
folders={manifest.folders}
|
|
112
|
+
currentFolderId={manifest.assignments[id] ?? null}
|
|
113
|
+
onRename={(name) => renameSlide(id, name)}
|
|
114
|
+
onMove={(folderId) => assign(id, folderId)}
|
|
115
|
+
onDelete={() => deleteSlide(id)}
|
|
116
|
+
onTitleResolved={reportTitle}
|
|
117
|
+
/>
|
|
118
|
+
</li>
|
|
119
|
+
))}
|
|
120
|
+
</ul>
|
|
236
121
|
)}
|
|
237
|
-
|
|
238
|
-
<FolderIconChip icon={icon} className="size-3.5 text-sm" />
|
|
239
|
-
<span className="truncate max-w-[8rem]">{label}</span>
|
|
240
|
-
<span className="folio nums">{count.toString().padStart(2, '0')}</span>
|
|
241
|
-
</button>
|
|
122
|
+
</>
|
|
242
123
|
);
|
|
243
124
|
}
|
|
244
125
|
|
|
@@ -271,6 +152,23 @@ function SearchInput({ value, onChange }: { value: string; onChange: (value: str
|
|
|
271
152
|
);
|
|
272
153
|
}
|
|
273
154
|
|
|
155
|
+
function HomeLoading() {
|
|
156
|
+
const t = useLocale();
|
|
157
|
+
return (
|
|
158
|
+
<div className="grid place-items-center px-8 py-24 text-muted-foreground">
|
|
159
|
+
<div className="flex flex-col items-center gap-4">
|
|
160
|
+
<div className="relative h-px w-56 overflow-hidden bg-hairline">
|
|
161
|
+
<span
|
|
162
|
+
aria-hidden
|
|
163
|
+
className="line-loader-bar absolute inset-y-[-0.5px] left-0 w-1/4 bg-foreground"
|
|
164
|
+
/>
|
|
165
|
+
</div>
|
|
166
|
+
<span className="eyebrow text-[11.5px]">{t.slide.loadingEyebrow}</span>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
274
172
|
function NoResultsState({ query, onClear }: { query: string; onClear: () => void }) {
|
|
275
173
|
const t = useLocale();
|
|
276
174
|
return (
|
|
@@ -315,11 +213,7 @@ function EmptyState({ isDraft, folderName }: { isDraft: boolean; folderName?: st
|
|
|
315
213
|
<p className="mt-1.5 text-[13px] leading-relaxed text-muted-foreground">
|
|
316
214
|
{t.home.createSlideHintPrefix}
|
|
317
215
|
<code className="rounded-[4px] bg-muted px-1.5 py-0.5 font-mono text-[11.5px] text-foreground">
|
|
318
|
-
|
|
319
|
-
</code>
|
|
320
|
-
{t.home.createSlideHintMid}
|
|
321
|
-
<code className="rounded-[4px] bg-muted px-1.5 py-0.5 font-mono text-[11.5px] text-foreground">
|
|
322
|
-
export default [Page1, Page2]
|
|
216
|
+
/create-slide
|
|
323
217
|
</code>
|
|
324
218
|
{t.home.createSlideHintSuffix}
|
|
325
219
|
</p>
|
|
@@ -458,13 +352,23 @@ function SlideCard({
|
|
|
458
352
|
</div>
|
|
459
353
|
)}
|
|
460
354
|
</div>
|
|
461
|
-
|
|
462
|
-
|
|
355
|
+
</Link>
|
|
356
|
+
<div className="mt-3 flex items-center gap-2">
|
|
357
|
+
<Link to={`/s/${id}`} className="min-w-0 flex-1 focus-visible:outline-none">
|
|
463
358
|
<h3 className="min-w-0 truncate font-heading text-[14px] font-medium tracking-tight">
|
|
464
359
|
{displayTitle}
|
|
465
360
|
</h3>
|
|
466
|
-
</
|
|
467
|
-
|
|
361
|
+
</Link>
|
|
362
|
+
{slide?.meta?.theme && (
|
|
363
|
+
<Link
|
|
364
|
+
to={`/themes/${encodeURIComponent(slide.meta.theme)}`}
|
|
365
|
+
className="inline-flex shrink-0 items-center gap-1 text-[11px] text-muted-foreground hover:text-foreground"
|
|
366
|
+
>
|
|
367
|
+
<Palette className="size-3" aria-hidden />
|
|
368
|
+
<span className="max-w-[120px] truncate">{slide.meta.theme}</span>
|
|
369
|
+
</Link>
|
|
370
|
+
)}
|
|
371
|
+
</div>
|
|
468
372
|
|
|
469
373
|
{import.meta.env.DEV && (
|
|
470
374
|
<div className="absolute right-2 top-2">
|