@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.
- package/dist/{build-6BeQ3cxb.js → build-_276DMmJ.js} +2 -2
- package/dist/cli/bin.js +5 -5
- package/dist/{config-AxZ5OE1u.js → config-BAwKWNtW.js} +215 -18
- package/dist/{config-CtT8K4VF.d.ts → config-D9cZ1A0X.d.ts} +2 -1
- package/dist/{dev-C9eLmUEq.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 +97 -333
- package/dist/{preview-Cunm-f4i.js → preview-BLPxspc9.js} +2 -2
- package/dist/sync-j9_QPovT.js +3 -0
- package/dist/{types-CRHIeoNq.d.ts → types-JYG1cmwC.d.ts} +31 -1
- 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/slide-authoring/SKILL.md +11 -0
- 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/inspector-panel.tsx +123 -10
- 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 +17 -5
- package/src/app/lib/assets.ts +55 -2
- 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 +89 -207
- package/src/app/routes/slide.tsx +144 -14
- package/src/app/routes/themes.tsx +34 -0
- package/src/app/virtual.d.ts +20 -0
- package/src/locale/en.ts +35 -3
- package/src/locale/ja.ts +36 -3
- package/src/locale/types.ts +33 -1
- package/src/locale/zh-cn.ts +35 -3
- package/src/locale/zh-tw.ts +35 -3
- 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
|
@@ -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
|
+
}
|
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,53 +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
|
-
|
|
33
|
-
|
|
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
|
|
107
|
-
return
|
|
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
|
-
|
|
114
|
-
<
|
|
115
|
-
<
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
</
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
</
|
|
489
|
-
|
|
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">
|