@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.
Files changed (55) hide show
  1. package/dist/{build-DSqSio-T.js → build-_276DMmJ.js} +2 -2
  2. package/dist/cli/bin.js +5 -5
  3. package/dist/{config-KdiYeWtK.js → config-BAwKWNtW.js} +888 -229
  4. package/dist/{config-C7vMYzFD.d.ts → config-D9cZ1A0X.d.ts} +2 -1
  5. package/dist/{dev-B_GVbr11.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 +166 -326
  11. package/dist/{preview-D_mxhj7w.js → preview-BLPxspc9.js} +2 -2
  12. package/dist/sync-j9_QPovT.js +3 -0
  13. package/dist/{types-DYgVpIGo.d.ts → types-JYG1cmwC.d.ts} +59 -5
  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/current-slide/SKILL.md +110 -0
  20. package/skills/slide-authoring/SKILL.md +59 -1
  21. package/src/app/app.tsx +11 -1
  22. package/src/app/components/asset-view.tsx +1 -13
  23. package/src/app/components/image-placeholder.tsx +123 -1
  24. package/src/app/components/inspector/image-crop-dialog.tsx +64 -20
  25. package/src/app/components/inspector/inspector-panel.tsx +163 -19
  26. package/src/app/components/inspector/inspector-provider.tsx +60 -7
  27. package/src/app/components/notes-drawer.tsx +117 -0
  28. package/src/app/components/player.tsx +11 -7
  29. package/src/app/components/present/overview-grid.tsx +2 -2
  30. package/src/app/components/sidebar/folder-item.tsx +16 -5
  31. package/src/app/components/sidebar/mobile-pill.tsx +34 -0
  32. package/src/app/components/sidebar/sidebar.tsx +10 -0
  33. package/src/app/components/themes/theme-detail.tsx +300 -0
  34. package/src/app/components/themes/themes-gallery.tsx +146 -0
  35. package/src/app/components/thumbnail-rail.tsx +136 -29
  36. package/src/app/components/ui/context-menu.tsx +237 -0
  37. package/src/app/lib/assets.ts +55 -2
  38. package/src/app/lib/inspector/use-notes.ts +134 -0
  39. package/src/app/lib/sdk.ts +1 -0
  40. package/src/app/lib/slides.ts +10 -1
  41. package/src/app/lib/themes.ts +22 -0
  42. package/src/app/lib/use-agent-socket.ts +18 -0
  43. package/src/app/routes/home-shell.tsx +173 -0
  44. package/src/app/routes/home.tsx +108 -204
  45. package/src/app/routes/slide.tsx +333 -68
  46. package/src/app/routes/themes.tsx +34 -0
  47. package/src/app/virtual.d.ts +20 -0
  48. package/src/locale/en.ts +61 -7
  49. package/src/locale/ja.ts +62 -7
  50. package/src/locale/types.ts +62 -5
  51. package/src/locale/zh-cn.ts +61 -7
  52. package/src/locale/zh-tw.ts +61 -7
  53. package/dist/sync-B4eLo2H6.js +0 -3
  54. /package/dist/{design-C13iz9_4.js → design-cpzS8aud.js} +0 -0
  55. /package/dist/{sync-3oqN1WyK.js → sync-BCJDRIqo.js} +0 -0
@@ -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,52 +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, create, update, remove, assign, renameSlide, deleteSlide } = useFolders();
31
- const [searchParams, setSearchParams] = useSearchParams();
32
- 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>();
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 t = titleMap[id]?.toLowerCase();
106
- return t ? t.includes(trimmedQuery) : false;
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
- <div className="flex h-dvh overflow-hidden bg-background text-foreground">
113
- <div className="hidden md:block">
114
- <Sidebar
115
- folders={manifest.folders}
116
- countFor={countFor}
117
- selectedId={selectedId}
118
- onSelect={selectFolder}
119
- onCreate={(name, icon) => create(name, icon)}
120
- onRename={(id, name) => update(id, { name })}
121
- onChangeIcon={(id, icon) => update(id, { icon })}
122
- onDelete={async (id) => {
123
- const name = manifest.folders.find((f) => f.id === id)?.name ?? id;
124
- if (selectedId === id) selectFolder(DRAFT_ID);
125
- try {
126
- await remove(id);
127
- toast.success(format(t.home.toastFolderDeleted, { name }));
128
- } catch {
129
- toast.error(t.home.toastFolderDeleteFailed);
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
- </div>
210
- </div>
211
- );
212
- }
213
-
214
- function MobileFolderPill({
215
- icon,
216
- label,
217
- count,
218
- active,
219
- onClick,
220
- }: {
221
- icon: FolderIcon;
222
- label: string;
223
- count: number;
224
- active: boolean;
225
- onClick: () => void;
226
- }) {
227
- return (
228
- <button
229
- type="button"
230
- onClick={onClick}
231
- className={cn(
232
- 'flex shrink-0 items-center gap-1.5 rounded-[5px] border px-2.5 py-1 text-[11.5px] font-medium transition-colors',
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
- slides/my-slide/index.tsx
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
- <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">
463
358
  <h3 className="min-w-0 truncate font-heading text-[14px] font-medium tracking-tight">
464
359
  {displayTitle}
465
360
  </h3>
466
- </div>
467
- </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>
468
372
 
469
373
  {import.meta.env.DEV && (
470
374
  <div className="absolute right-2 top-2">