@open-slide/core 1.2.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.
Files changed (47) hide show
  1. package/dist/{build-6BeQ3cxb.js → build-_276DMmJ.js} +2 -2
  2. package/dist/cli/bin.js +5 -5
  3. package/dist/{config-AxZ5OE1u.js → config-BAwKWNtW.js} +215 -18
  4. package/dist/{config-CtT8K4VF.d.ts → config-D9cZ1A0X.d.ts} +2 -1
  5. package/dist/{dev-C9eLmUEq.js → dev-BoqeVXVq.js} +2 -2
  6. package/dist/en-CDKzoZvf.js +351 -0
  7. package/dist/index.d.ts +4 -3
  8. package/dist/index.js +229 -39
  9. package/dist/locale/index.d.ts +1 -1
  10. package/dist/locale/index.js +97 -333
  11. package/dist/{preview-Cunm-f4i.js → preview-BLPxspc9.js} +2 -2
  12. package/dist/sync-j9_QPovT.js +3 -0
  13. package/dist/{types-CRHIeoNq.d.ts → types-JYG1cmwC.d.ts} +31 -1
  14. package/dist/vite/index.d.ts +2 -2
  15. package/dist/vite/index.js +2 -2
  16. package/package.json +9 -1
  17. package/skills/create-slide/SKILL.md +1 -1
  18. package/skills/create-theme/SKILL.md +60 -12
  19. package/skills/slide-authoring/SKILL.md +11 -0
  20. package/src/app/app.tsx +11 -1
  21. package/src/app/components/asset-view.tsx +1 -13
  22. package/src/app/components/image-placeholder.tsx +123 -1
  23. package/src/app/components/inspector/inspector-panel.tsx +123 -10
  24. package/src/app/components/sidebar/folder-item.tsx +16 -5
  25. package/src/app/components/sidebar/mobile-pill.tsx +34 -0
  26. package/src/app/components/sidebar/sidebar.tsx +10 -0
  27. package/src/app/components/themes/theme-detail.tsx +300 -0
  28. package/src/app/components/themes/themes-gallery.tsx +146 -0
  29. package/src/app/components/thumbnail-rail.tsx +17 -5
  30. package/src/app/lib/assets.ts +55 -2
  31. package/src/app/lib/sdk.ts +1 -0
  32. package/src/app/lib/slides.ts +10 -1
  33. package/src/app/lib/themes.ts +22 -0
  34. package/src/app/lib/use-agent-socket.ts +18 -0
  35. package/src/app/routes/home-shell.tsx +173 -0
  36. package/src/app/routes/home.tsx +89 -207
  37. package/src/app/routes/slide.tsx +144 -14
  38. package/src/app/routes/themes.tsx +34 -0
  39. package/src/app/virtual.d.ts +20 -0
  40. package/src/locale/en.ts +35 -3
  41. package/src/locale/ja.ts +36 -3
  42. package/src/locale/types.ts +33 -1
  43. package/src/locale/zh-cn.ts +35 -3
  44. package/src/locale/zh-tw.ts +35 -3
  45. package/dist/sync-B4eLo2H6.js +0 -3
  46. /package/dist/{design-C13iz9_4.js → design-cpzS8aud.js} +0 -0
  47. /package/dist/{sync-3oqN1WyK.js → sync-BCJDRIqo.js} +0 -0
@@ -0,0 +1,18 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ export function useAgentSocketConnected() {
4
+ const [connected, setConnected] = useState(true);
5
+ useEffect(() => {
6
+ const hot = import.meta.hot;
7
+ if (!hot) return;
8
+ const onConnect = () => setConnected(true);
9
+ const onDisconnect = () => setConnected(false);
10
+ hot.on('vite:ws:connect', onConnect);
11
+ hot.on('vite:ws:disconnect', onDisconnect);
12
+ return () => {
13
+ hot.off('vite:ws:connect', onConnect);
14
+ hot.off('vite:ws:disconnect', onDisconnect);
15
+ };
16
+ }, []);
17
+ return connected;
18
+ }
@@ -0,0 +1,173 @@
1
+ import { useCallback, useMemo, useState } from 'react';
2
+ import { Outlet, useLocation, useNavigate, useSearchParams } from 'react-router-dom';
3
+ import { toast } from 'sonner';
4
+ import { useFolders } from '@/lib/folders';
5
+ import { format, useLocale } from '@/lib/use-locale';
6
+ import { MobileFolderPill } from '../components/sidebar/mobile-pill';
7
+ import { DRAFT_ID, Sidebar, THEMES_ID } from '../components/sidebar/sidebar';
8
+ import type { FoldersManifest } from '../lib/sdk';
9
+ import { slideIds } from '../lib/slides';
10
+ import { themes as themeRegistry } from '../lib/themes';
11
+
12
+ export type HomeOutletContext = {
13
+ manifest: FoldersManifest;
14
+ loading: boolean;
15
+ draftSlides: string[];
16
+ slidesByFolder: Record<string, string[]>;
17
+ /** Selected folder id when on `/`; equals DRAFT_ID, a folder id, or THEMES_ID. */
18
+ selectedId: string;
19
+ reportTitle: (slideId: string, title: string) => void;
20
+ titleMap: Record<string, string>;
21
+ assign: (slideId: string, folderId: string | null) => Promise<void>;
22
+ renameSlide: (slideId: string, name: string) => Promise<void>;
23
+ deleteSlide: (slideId: string) => Promise<void>;
24
+ };
25
+
26
+ function pathToSelectedId(pathname: string, search: URLSearchParams): string {
27
+ if (pathname === '/themes' || pathname.startsWith('/themes/')) return THEMES_ID;
28
+ return search.get('f') ?? DRAFT_ID;
29
+ }
30
+
31
+ export function HomeShell() {
32
+ const { manifest, loading, create, update, remove, assign, renameSlide, deleteSlide } =
33
+ useFolders();
34
+ const navigate = useNavigate();
35
+ const location = useLocation();
36
+ const [searchParams] = useSearchParams();
37
+ const t = useLocale();
38
+
39
+ const selectedId = pathToSelectedId(location.pathname, searchParams);
40
+
41
+ const [titleMap, setTitleMap] = useState<Record<string, string>>({});
42
+ const reportTitle = useCallback((slideId: string, slideTitle: string) => {
43
+ setTitleMap((prev) =>
44
+ prev[slideId] === slideTitle ? prev : { ...prev, [slideId]: slideTitle },
45
+ );
46
+ }, []);
47
+
48
+ const selectFolder = useCallback(
49
+ (id: string) => {
50
+ if (id === THEMES_ID) navigate('/themes', { replace: true });
51
+ else if (id === DRAFT_ID) navigate('/', { replace: true });
52
+ else navigate(`/?f=${encodeURIComponent(id)}`, { replace: true });
53
+ },
54
+ [navigate],
55
+ );
56
+
57
+ const { draftSlides, slidesByFolder } = useMemo(() => {
58
+ const byFolder: Record<string, string[]> = {};
59
+ const draft: string[] = [];
60
+ const known = new Set(manifest.folders.map((f) => f.id));
61
+ for (const id of slideIds) {
62
+ const folderId = manifest.assignments[id];
63
+ if (folderId && known.has(folderId)) {
64
+ byFolder[folderId] ??= [];
65
+ byFolder[folderId].push(id);
66
+ } else {
67
+ draft.push(id);
68
+ }
69
+ }
70
+ return { draftSlides: draft, slidesByFolder: byFolder };
71
+ }, [manifest]);
72
+
73
+ const countFor = (folderId: string | null) =>
74
+ folderId === null ? draftSlides.length : (slidesByFolder[folderId]?.length ?? 0);
75
+
76
+ const moveSlideWithToast = useCallback(
77
+ async (slideId: string, folderId: string | null) => {
78
+ if (manifest.assignments[slideId] === (folderId ?? undefined)) return;
79
+ const slideName = titleMap[slideId] ?? slideId;
80
+ const folderName =
81
+ folderId === null
82
+ ? t.home.draft
83
+ : (manifest.folders.find((f) => f.id === folderId)?.name ?? folderId);
84
+ try {
85
+ await assign(slideId, folderId);
86
+ toast.success(format(t.home.toastSlideMoved, { slide: slideName, folder: folderName }));
87
+ } catch {
88
+ toast.error(t.home.toastSlideMoveFailed);
89
+ }
90
+ },
91
+ [assign, manifest, titleMap, t],
92
+ );
93
+
94
+ const ctx: HomeOutletContext = {
95
+ manifest,
96
+ loading,
97
+ draftSlides,
98
+ slidesByFolder,
99
+ selectedId,
100
+ reportTitle,
101
+ titleMap,
102
+ assign,
103
+ renameSlide,
104
+ deleteSlide,
105
+ };
106
+
107
+ return (
108
+ <div className="flex h-dvh overflow-hidden bg-background text-foreground">
109
+ <div className="hidden md:block">
110
+ <Sidebar
111
+ folders={manifest.folders}
112
+ countFor={countFor}
113
+ themesCount={themeRegistry.length}
114
+ selectedId={selectedId}
115
+ onSelect={selectFolder}
116
+ onCreate={(name, icon) => create(name, icon)}
117
+ onRename={(id, name) => update(id, { name })}
118
+ onChangeIcon={(id, icon) => update(id, { icon })}
119
+ onDelete={async (id) => {
120
+ const name = manifest.folders.find((f) => f.id === id)?.name ?? id;
121
+ if (selectedId === id) selectFolder(DRAFT_ID);
122
+ try {
123
+ await remove(id);
124
+ toast.success(format(t.home.toastFolderDeleted, { name }));
125
+ } catch {
126
+ toast.error(t.home.toastFolderDeleteFailed);
127
+ }
128
+ }}
129
+ onDropToFolder={(folderId, slideId) => moveSlideWithToast(slideId, folderId)}
130
+ onDropToDraft={(slideId) => moveSlideWithToast(slideId, null)}
131
+ />
132
+ </div>
133
+
134
+ <div className="paper relative flex min-w-0 flex-1 flex-col overflow-y-auto bg-canvas">
135
+ <div className="flex items-center justify-between border-b border-hairline bg-sidebar px-4 py-3 md:hidden">
136
+ <h1 className="font-heading text-lg font-bold tracking-tight">{t.home.appTitle}</h1>
137
+ </div>
138
+ <div className="border-b border-hairline bg-sidebar px-4 py-2 md:hidden">
139
+ <div className="flex gap-2 overflow-x-auto pb-1">
140
+ <MobileFolderPill
141
+ icon={{ type: 'emoji', value: '📝' }}
142
+ label={t.home.draft}
143
+ count={countFor(null)}
144
+ active={selectedId === DRAFT_ID}
145
+ onClick={() => selectFolder(DRAFT_ID)}
146
+ />
147
+ <MobileFolderPill
148
+ icon={{ type: 'emoji', value: '🎨' }}
149
+ label={t.home.themes}
150
+ count={themeRegistry.length}
151
+ active={selectedId === THEMES_ID}
152
+ onClick={() => selectFolder(THEMES_ID)}
153
+ />
154
+ {manifest.folders.map((f) => (
155
+ <MobileFolderPill
156
+ key={f.id}
157
+ icon={f.icon}
158
+ label={f.name}
159
+ count={countFor(f.id)}
160
+ active={selectedId === f.id}
161
+ onClick={() => selectFolder(f.id)}
162
+ />
163
+ ))}
164
+ </div>
165
+ </div>
166
+
167
+ <div className="mx-auto w-full max-w-[1180px] px-5 py-8 md:px-10 md:py-12">
168
+ <Outlet context={ctx} />
169
+ </div>
170
+ </div>
171
+ </div>
172
+ );
173
+ }
@@ -1,7 +1,15 @@
1
- import { FolderInput, FolderPlus, MoreHorizontal, Pencil, Search, Trash2, X } from 'lucide-react';
2
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
- import { Link, useSearchParams } from 'react-router-dom';
4
- import { toast } from 'sonner';
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,53 +25,30 @@ import {
17
25
  DropdownMenuItem,
18
26
  DropdownMenuTrigger,
19
27
  } from '@/components/ui/dropdown-menu';
20
- import { useFolders } from '@/lib/folders';
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, Sidebar } from '../components/sidebar/sidebar';
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, slideIds } from '../lib/slides';
34
+ import { loadSlide } from '../lib/slides';
35
+ import type { HomeOutletContext } from './home-shell';
28
36
 
29
37
  export function Home() {
30
- const { manifest, loading, create, update, remove, assign, renameSlide, deleteSlide } =
31
- useFolders();
32
- const [searchParams, setSearchParams] = useSearchParams();
33
- const selectedId = searchParams.get('f') ?? DRAFT_ID;
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>();
34
50
  const t = useLocale();
35
51
 
36
- const selectFolder = (id: string) => {
37
- setSearchParams(
38
- (prev) => {
39
- const next = new URLSearchParams(prev);
40
- if (id === DRAFT_ID) next.delete('f');
41
- else next.set('f', id);
42
- return next;
43
- },
44
- { replace: true },
45
- );
46
- };
47
-
48
- const { draftSlides, slidesByFolder } = useMemo(() => {
49
- const byFolder: Record<string, string[]> = {};
50
- const draft: string[] = [];
51
- const known = new Set(manifest.folders.map((f) => f.id));
52
- for (const id of slideIds) {
53
- const folderId = manifest.assignments[id];
54
- if (folderId && known.has(folderId)) {
55
- byFolder[folderId] ??= [];
56
- byFolder[folderId].push(id);
57
- } else {
58
- draft.push(id);
59
- }
60
- }
61
- return { draftSlides: draft, slidesByFolder: byFolder };
62
- }, [manifest]);
63
-
64
- const countFor = (folderId: string | null) =>
65
- folderId === null ? draftSlides.length : (slidesByFolder[folderId]?.length ?? 0);
66
-
67
52
  const selectedFolder =
68
53
  selectedId === DRAFT_ID ? null : (manifest.folders.find((f) => f.id === selectedId) ?? null);
69
54
  const visibleSlides = selectedId === DRAFT_ID ? draftSlides : (slidesByFolder[selectedId] ?? []);
@@ -73,177 +58,68 @@ export function Home() {
73
58
  const isDraft = selectedId === DRAFT_ID;
74
59
 
75
60
  const [query, setQuery] = useState('');
76
- const [titleMap, setTitleMap] = useState<Record<string, string>>({});
77
- const reportTitle = useCallback((slideId: string, slideTitle: string) => {
78
- setTitleMap((prev) =>
79
- prev[slideId] === slideTitle ? prev : { ...prev, [slideId]: slideTitle },
80
- );
81
- }, []);
82
-
83
- const moveSlideWithToast = useCallback(
84
- async (slideId: string, folderId: string | null) => {
85
- if (manifest.assignments[slideId] === (folderId ?? undefined)) return;
86
- const slideName = titleMap[slideId] ?? slideId;
87
- const folderName =
88
- folderId === null
89
- ? t.home.draft
90
- : (manifest.folders.find((f) => f.id === folderId)?.name ?? folderId);
91
- try {
92
- await assign(slideId, folderId);
93
- toast.success(format(t.home.toastSlideMoved, { slide: slideName, folder: folderName }));
94
- } catch {
95
- toast.error(t.home.toastSlideMoveFailed);
96
- }
97
- },
98
- [assign, manifest, titleMap, t],
99
- );
100
61
 
101
62
  const trimmedQuery = query.trim().toLowerCase();
102
63
  const filteredSlides = useMemo(() => {
103
64
  if (!trimmedQuery) return visibleSlides;
104
65
  return visibleSlides.filter((id) => {
105
66
  if (id.toLowerCase().includes(trimmedQuery)) return true;
106
- const t = titleMap[id]?.toLowerCase();
107
- return t ? t.includes(trimmedQuery) : false;
67
+ const tl = titleMap[id]?.toLowerCase();
68
+ return tl ? tl.includes(trimmedQuery) : false;
108
69
  });
109
70
  }, [visibleSlides, titleMap, trimmedQuery]);
110
71
  const isSearching = trimmedQuery.length > 0;
111
72
 
112
73
  return (
113
- <div className="flex h-dvh overflow-hidden bg-background text-foreground">
114
- <div className="hidden md:block">
115
- <Sidebar
116
- folders={manifest.folders}
117
- countFor={countFor}
118
- selectedId={selectedId}
119
- onSelect={selectFolder}
120
- onCreate={(name, icon) => create(name, icon)}
121
- onRename={(id, name) => update(id, { name })}
122
- onChangeIcon={(id, icon) => update(id, { icon })}
123
- onDelete={async (id) => {
124
- const name = manifest.folders.find((f) => f.id === id)?.name ?? id;
125
- if (selectedId === id) selectFolder(DRAFT_ID);
126
- try {
127
- await remove(id);
128
- toast.success(format(t.home.toastFolderDeleted, { name }));
129
- } catch {
130
- toast.error(t.home.toastFolderDeleteFailed);
131
- }
132
- }}
133
- onDropToFolder={(folderId, slideId) => moveSlideWithToast(slideId, folderId)}
134
- onDropToDraft={(slideId) => moveSlideWithToast(slideId, null)}
135
- />
136
- </div>
137
-
138
- <div className="paper relative flex min-w-0 flex-1 flex-col overflow-y-auto bg-canvas">
139
- {/* Mobile chrome */}
140
- <div className="flex items-center justify-between border-b border-hairline bg-sidebar px-4 py-3 md:hidden">
141
- <h1 className="font-heading text-lg font-bold tracking-tight">{t.home.appTitle}</h1>
142
- </div>
143
- <div className="border-b border-hairline bg-sidebar px-4 py-2 md:hidden">
144
- <div className="flex gap-2 overflow-x-auto pb-1">
145
- <MobileFolderPill
146
- icon={{ type: 'emoji', value: '📝' }}
147
- label={t.home.draft}
148
- count={countFor(null)}
149
- active={selectedId === DRAFT_ID}
150
- onClick={() => selectFolder(DRAFT_ID)}
151
- />
152
- {manifest.folders.map((f) => (
153
- <MobileFolderPill
154
- key={f.id}
155
- icon={f.icon}
156
- label={f.name}
157
- count={countFor(f.id)}
158
- active={selectedId === f.id}
159
- onClick={() => selectFolder(f.id)}
160
- />
161
- ))}
162
- </div>
163
- </div>
164
-
165
- <div className="mx-auto w-full max-w-[1180px] px-5 py-8 md:px-10 md:py-12">
166
- <header className="mb-8 md:mb-12">
167
- <div className="flex flex-wrap items-center gap-3">
168
- <FolderIconChip icon={headerIcon} className="size-7 text-2xl" />
169
- <h1 className="font-heading text-[32px] font-semibold leading-[1.05] tracking-[-0.025em] md:text-[44px]">
170
- {title}
171
- </h1>
172
- {!loading && (
173
- <span className="folio ml-1 self-end pb-2">
174
- {(isSearching ? filteredSlides.length : visibleSlides.length)
175
- .toString()
176
- .padStart(2, '0')}
177
- {isSearching && (
178
- <span className="opacity-40">
179
- /{visibleSlides.length.toString().padStart(2, '0')}
180
- </span>
181
- )}
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')}
182
89
  </span>
183
90
  )}
184
- <div className="ml-auto w-full md:w-auto">
185
- <SearchInput value={query} onChange={setQuery} />
186
- </div>
187
- </div>
188
- </header>
189
-
190
- {loading ? (
191
- <HomeLoading />
192
- ) : visibleSlides.length === 0 ? (
193
- <EmptyState isDraft={isDraft} folderName={selectedFolder?.name} />
194
- ) : filteredSlides.length === 0 ? (
195
- <NoResultsState query={query} onClear={() => setQuery('')} />
196
- ) : (
197
- <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))]">
198
- {filteredSlides.map((id) => (
199
- <li key={id}>
200
- <SlideCard
201
- id={id}
202
- folders={manifest.folders}
203
- currentFolderId={manifest.assignments[id] ?? null}
204
- onRename={(name) => renameSlide(id, name)}
205
- onMove={(folderId) => assign(id, folderId)}
206
- onDelete={() => deleteSlide(id)}
207
- onTitleResolved={reportTitle}
208
- />
209
- </li>
210
- ))}
211
- </ul>
91
+ </span>
212
92
  )}
93
+ <div className="ml-auto w-full md:w-auto">
94
+ <SearchInput value={query} onChange={setQuery} />
95
+ </div>
213
96
  </div>
214
- </div>
215
- </div>
216
- );
217
- }
218
-
219
- function MobileFolderPill({
220
- icon,
221
- label,
222
- count,
223
- active,
224
- onClick,
225
- }: {
226
- icon: FolderIcon;
227
- label: string;
228
- count: number;
229
- active: boolean;
230
- onClick: () => void;
231
- }) {
232
- return (
233
- <button
234
- type="button"
235
- onClick={onClick}
236
- className={cn(
237
- 'flex shrink-0 items-center gap-1.5 rounded-[5px] border px-2.5 py-1 text-[11.5px] font-medium transition-colors',
238
- active
239
- ? 'border-foreground/40 bg-foreground text-background'
240
- : '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>
241
121
  )}
242
- >
243
- <FolderIconChip icon={icon} className="size-3.5 text-sm" />
244
- <span className="truncate max-w-[8rem]">{label}</span>
245
- <span className="folio nums">{count.toString().padStart(2, '0')}</span>
246
- </button>
122
+ </>
247
123
  );
248
124
  }
249
125
 
@@ -337,11 +213,7 @@ function EmptyState({ isDraft, folderName }: { isDraft: boolean; folderName?: st
337
213
  <p className="mt-1.5 text-[13px] leading-relaxed text-muted-foreground">
338
214
  {t.home.createSlideHintPrefix}
339
215
  <code className="rounded-[4px] bg-muted px-1.5 py-0.5 font-mono text-[11.5px] text-foreground">
340
- slides/my-slide/index.tsx
341
- </code>
342
- {t.home.createSlideHintMid}
343
- <code className="rounded-[4px] bg-muted px-1.5 py-0.5 font-mono text-[11.5px] text-foreground">
344
- export default [Page1, Page2]
216
+ /create-slide
345
217
  </code>
346
218
  {t.home.createSlideHintSuffix}
347
219
  </p>
@@ -480,13 +352,23 @@ function SlideCard({
480
352
  </div>
481
353
  )}
482
354
  </div>
483
-
484
- <div className="mt-3">
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">
485
358
  <h3 className="min-w-0 truncate font-heading text-[14px] font-medium tracking-tight">
486
359
  {displayTitle}
487
360
  </h3>
488
- </div>
489
- </Link>
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>
490
372
 
491
373
  {import.meta.env.DEV && (
492
374
  <div className="absolute right-2 top-2">