@open-slide/core 0.0.11 → 0.0.12
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-DHiRlpjn.js → build-aiY_8kwE.js} +2 -1
- package/dist/cli/bin.js +43 -4
- package/dist/{config-LZM903FE.js → config-CVqRAagl.js} +592 -63
- package/dist/design-CROQh0AA.js +35 -0
- package/dist/{dev-B3JzCYn7.js → dev-R2we2iaF.js} +2 -1
- package/dist/index.d.ts +55 -4
- package/dist/index.js +110 -1
- package/dist/{preview-UikovHEt.js → preview-CU4zSyGp.js} +2 -1
- package/dist/sync-3oqN1WyK.js +139 -0
- package/dist/sync-B4eLo2H6.js +3 -0
- package/dist/vite/index.d.ts +1 -1
- package/dist/vite/index.js +2 -1
- package/package.json +2 -1
- package/skills/apply-comments/SKILL.md +83 -0
- package/skills/create-slide/SKILL.md +81 -0
- package/skills/create-theme/SKILL.md +194 -0
- package/skills/slide-authoring/SKILL.md +288 -0
- package/src/app/{App.tsx → app.tsx} +8 -6
- package/src/app/components/{AssetView.tsx → asset-view.tsx} +41 -33
- package/src/app/components/{ClickNavZones.tsx → click-nav-zones.tsx} +1 -1
- package/src/app/components/history-provider.tsx +120 -0
- package/src/app/components/image-placeholder.tsx +121 -0
- package/src/app/components/inspector/{CommentWidget.tsx → comment-widget.tsx} +1 -1
- package/src/app/components/inspector/{InspectOverlay.tsx → inspect-overlay.tsx} +1 -1
- package/src/app/components/inspector/{InspectorPanel.tsx → inspector-panel.tsx} +164 -212
- package/src/app/components/inspector/{InspectorProvider.tsx → inspector-provider.tsx} +186 -18
- package/src/app/components/inspector/save-bar.tsx +47 -0
- package/src/app/components/panel/panel-fields.tsx +60 -0
- package/src/app/components/panel/panel-shell.tsx +78 -0
- package/src/app/components/panel/save-card.tsx +139 -0
- package/src/app/components/pdf-progress-toast.tsx +25 -0
- package/src/app/components/player.tsx +341 -0
- package/src/app/components/present/blackout-overlay.tsx +18 -0
- package/src/app/components/present/control-bar.tsx +204 -0
- package/src/app/components/present/help-overlay.tsx +56 -0
- package/src/app/components/present/jump-input.tsx +74 -0
- package/src/app/components/present/laser-pointer.tsx +40 -0
- package/src/app/components/present/overview-grid.tsx +184 -0
- package/src/app/components/present/progress-bar.tsx +26 -0
- package/src/app/components/present/use-idle.ts +44 -0
- package/src/app/components/present/use-pointer-near-bottom.ts +34 -0
- package/src/app/components/present/use-presenter-channel.ts +71 -0
- package/src/app/components/present/use-touch-swipe.ts +63 -0
- package/src/app/components/sidebar/{FolderItem.tsx → folder-item.tsx} +62 -27
- package/src/app/components/sidebar/{IconPicker.tsx → icon-picker.tsx} +13 -10
- package/src/app/components/sidebar/{Sidebar.tsx → sidebar.tsx} +40 -34
- package/src/app/components/{SlideCanvas.tsx → slide-canvas.tsx} +35 -10
- package/src/app/components/style-panel/design-provider.tsx +139 -0
- package/src/app/components/style-panel/style-panel.tsx +326 -0
- package/src/app/components/style-panel/use-design.ts +112 -0
- package/src/app/components/theme-toggle.tsx +57 -0
- package/src/app/components/thumbnail-rail.tsx +151 -0
- package/src/app/components/ui/button.tsx +51 -19
- package/src/app/components/ui/card.tsx +1 -1
- package/src/app/components/ui/dialog.tsx +25 -9
- package/src/app/components/ui/dropdown-menu.tsx +29 -12
- package/src/app/components/ui/input.tsx +13 -9
- package/src/app/components/ui/popover.tsx +5 -2
- package/src/app/components/ui/progress.tsx +2 -2
- package/src/app/components/ui/select.tsx +11 -5
- package/src/app/components/ui/separator.tsx +1 -1
- package/src/app/components/ui/slider.tsx +4 -4
- package/src/app/components/ui/sonner.tsx +11 -1
- package/src/app/components/ui/tabs.tsx +6 -6
- package/src/app/components/ui/textarea.tsx +11 -7
- package/src/app/components/ui/toggle-group.tsx +2 -2
- package/src/app/components/ui/toggle.tsx +6 -6
- package/src/app/components/ui/tooltip.tsx +5 -2
- package/src/app/lib/export-html.ts +10 -1
- package/src/app/lib/export-pdf.ts +7 -0
- package/src/app/lib/folders.ts +1 -1
- package/src/app/lib/inspector/{useEditor.ts → use-editor.ts} +2 -1
- package/src/app/lib/sdk.ts +5 -0
- package/src/app/lib/slides.ts +1 -1
- package/src/app/lib/utils.ts +1 -1
- package/src/app/main.tsx +5 -2
- package/src/app/routes/{Home.tsx → home.tsx} +266 -97
- package/src/app/routes/presenter.tsx +400 -0
- package/src/app/routes/slide.tsx +519 -0
- package/src/app/styles.css +338 -67
- package/src/app/components/PdfProgressToast.tsx +0 -23
- package/src/app/components/Player.tsx +0 -100
- package/src/app/components/ThumbnailRail.tsx +0 -68
- package/src/app/components/inspector/SaveBar.tsx +0 -77
- package/src/app/routes/Slide.tsx +0 -478
- /package/dist/{config-SXL5qIl6.d.ts → config-DweCbRkQ.d.ts} +0 -0
- /package/src/app/lib/inspector/{useComments.ts → use-comments.ts} +0 -0
- /package/src/app/lib/{useWheelPageNavigation.ts → use-wheel-page-navigation.ts} +0 -0
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
FolderInput,
|
|
3
|
+
FolderPlus,
|
|
4
|
+
MoreHorizontal,
|
|
5
|
+
Pencil,
|
|
6
|
+
Search,
|
|
7
|
+
Trash2,
|
|
8
|
+
X,
|
|
9
|
+
} from 'lucide-react';
|
|
10
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
11
|
import { Link, useSearchParams } from 'react-router-dom';
|
|
4
12
|
import { Button } from '@/components/ui/button';
|
|
5
|
-
import { Card, CardContent } from '@/components/ui/card';
|
|
6
13
|
import {
|
|
7
14
|
Dialog,
|
|
8
15
|
DialogContent,
|
|
@@ -19,9 +26,9 @@ import {
|
|
|
19
26
|
} from '@/components/ui/dropdown-menu';
|
|
20
27
|
import { useFolders } from '@/lib/folders';
|
|
21
28
|
import { cn } from '@/lib/utils';
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
29
|
+
import { FolderIconChip, SLIDE_DND_MIME } from '../components/sidebar/folder-item';
|
|
30
|
+
import { DRAFT_ID, Sidebar } from '../components/sidebar/sidebar';
|
|
31
|
+
import { SlideCanvas } from '../components/slide-canvas';
|
|
25
32
|
import type { Folder, FolderIcon, SlideModule } from '../lib/sdk';
|
|
26
33
|
import { loadSlide, slideIds } from '../lib/slides';
|
|
27
34
|
|
|
@@ -67,9 +74,27 @@ export function Home() {
|
|
|
67
74
|
|
|
68
75
|
const title = selectedFolder?.name ?? 'Draft';
|
|
69
76
|
const headerIcon = selectedFolder?.icon ?? { type: 'emoji' as const, value: '📝' };
|
|
77
|
+
const isDraft = selectedId === DRAFT_ID;
|
|
78
|
+
|
|
79
|
+
const [query, setQuery] = useState('');
|
|
80
|
+
const [titleMap, setTitleMap] = useState<Record<string, string>>({});
|
|
81
|
+
const reportTitle = useCallback((slideId: string, slideTitle: string) => {
|
|
82
|
+
setTitleMap((prev) => (prev[slideId] === slideTitle ? prev : { ...prev, [slideId]: slideTitle }));
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
85
|
+
const trimmedQuery = query.trim().toLowerCase();
|
|
86
|
+
const filteredSlides = useMemo(() => {
|
|
87
|
+
if (!trimmedQuery) return visibleSlides;
|
|
88
|
+
return visibleSlides.filter((id) => {
|
|
89
|
+
if (id.toLowerCase().includes(trimmedQuery)) return true;
|
|
90
|
+
const t = titleMap[id]?.toLowerCase();
|
|
91
|
+
return t ? t.includes(trimmedQuery) : false;
|
|
92
|
+
});
|
|
93
|
+
}, [visibleSlides, titleMap, trimmedQuery]);
|
|
94
|
+
const isSearching = trimmedQuery.length > 0;
|
|
70
95
|
|
|
71
96
|
return (
|
|
72
|
-
<div className="flex h-
|
|
97
|
+
<div className="flex h-dvh overflow-hidden bg-background text-foreground">
|
|
73
98
|
<div className="hidden md:block">
|
|
74
99
|
<Sidebar
|
|
75
100
|
folders={manifest.folders}
|
|
@@ -88,9 +113,12 @@ export function Home() {
|
|
|
88
113
|
/>
|
|
89
114
|
</div>
|
|
90
115
|
|
|
91
|
-
<div className="flex min-w-0 flex-1 flex-col overflow-y-auto">
|
|
92
|
-
|
|
93
|
-
|
|
116
|
+
<div className="paper relative flex min-w-0 flex-1 flex-col overflow-y-auto bg-canvas">
|
|
117
|
+
{/* Mobile chrome */}
|
|
118
|
+
<div className="flex items-center justify-between border-b border-hairline bg-sidebar px-4 py-3 md:hidden">
|
|
119
|
+
<h1 className="font-heading text-lg font-bold tracking-tight">open-slide</h1>
|
|
120
|
+
</div>
|
|
121
|
+
<div className="border-b border-hairline bg-sidebar px-4 py-2 md:hidden">
|
|
94
122
|
<div className="flex gap-2 overflow-x-auto pb-1">
|
|
95
123
|
<MobileFolderPill
|
|
96
124
|
icon={{ type: 'emoji', value: '📝' }}
|
|
@@ -112,20 +140,34 @@ export function Home() {
|
|
|
112
140
|
</div>
|
|
113
141
|
</div>
|
|
114
142
|
|
|
115
|
-
<div className="mx-auto w-full max-w-
|
|
116
|
-
<header className="mb-
|
|
117
|
-
<
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
143
|
+
<div className="mx-auto w-full max-w-[1180px] px-5 py-8 md:px-10 md:py-12">
|
|
144
|
+
<header className="mb-8 md:mb-12">
|
|
145
|
+
<div className="flex flex-wrap items-center gap-3">
|
|
146
|
+
<FolderIconChip icon={headerIcon} className="size-7 text-2xl" />
|
|
147
|
+
<h1 className="font-heading text-[32px] font-semibold leading-[1.05] tracking-[-0.025em] md:text-[44px]">
|
|
148
|
+
{title}
|
|
149
|
+
</h1>
|
|
150
|
+
<span className="folio ml-1 self-end pb-2">
|
|
151
|
+
{(isSearching ? filteredSlides.length : visibleSlides.length)
|
|
152
|
+
.toString()
|
|
153
|
+
.padStart(2, '0')}
|
|
154
|
+
{isSearching && (
|
|
155
|
+
<span className="opacity-40">/{visibleSlides.length.toString().padStart(2, '0')}</span>
|
|
156
|
+
)}
|
|
157
|
+
</span>
|
|
158
|
+
<div className="ml-auto w-full md:w-auto">
|
|
159
|
+
<SearchInput value={query} onChange={setQuery} />
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
122
162
|
</header>
|
|
123
163
|
|
|
124
164
|
{visibleSlides.length === 0 ? (
|
|
125
|
-
<EmptyState isDraft={
|
|
165
|
+
<EmptyState isDraft={isDraft} folderName={selectedFolder?.name} />
|
|
166
|
+
) : filteredSlides.length === 0 ? (
|
|
167
|
+
<NoResultsState query={query} onClear={() => setQuery('')} />
|
|
126
168
|
) : (
|
|
127
|
-
<ul className="grid grid-cols-[repeat(auto-fill,minmax(
|
|
128
|
-
{
|
|
169
|
+
<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))]">
|
|
170
|
+
{filteredSlides.map((id) => (
|
|
129
171
|
<li key={id}>
|
|
130
172
|
<SlideCard
|
|
131
173
|
id={id}
|
|
@@ -134,6 +176,7 @@ export function Home() {
|
|
|
134
176
|
onRename={(name) => renameSlide(id, name)}
|
|
135
177
|
onMove={(folderId) => assign(id, folderId)}
|
|
136
178
|
onDelete={() => deleteSlide(id)}
|
|
179
|
+
onTitleResolved={reportTitle}
|
|
137
180
|
/>
|
|
138
181
|
</li>
|
|
139
182
|
))}
|
|
@@ -162,35 +205,93 @@ function MobileFolderPill({
|
|
|
162
205
|
<button
|
|
163
206
|
type="button"
|
|
164
207
|
onClick={onClick}
|
|
165
|
-
className={
|
|
166
|
-
'flex shrink-0 items-center gap-1.5 rounded-
|
|
167
|
-
|
|
168
|
-
? 'border-
|
|
169
|
-
: 'border-border bg-
|
|
170
|
-
}
|
|
208
|
+
className={cn(
|
|
209
|
+
'flex shrink-0 items-center gap-1.5 rounded-[5px] border px-2.5 py-1 text-[11.5px] font-medium transition-colors',
|
|
210
|
+
active
|
|
211
|
+
? 'border-foreground/40 bg-foreground text-background'
|
|
212
|
+
: 'border-border bg-card text-muted-foreground hover:text-foreground',
|
|
213
|
+
)}
|
|
171
214
|
>
|
|
172
|
-
<FolderIconChip icon={icon} className="size-
|
|
215
|
+
<FolderIconChip icon={icon} className="size-3.5 text-sm" />
|
|
173
216
|
<span className="truncate max-w-[8rem]">{label}</span>
|
|
174
|
-
<span className="
|
|
217
|
+
<span className="folio nums">{count.toString().padStart(2, '0')}</span>
|
|
175
218
|
</button>
|
|
176
219
|
);
|
|
177
220
|
}
|
|
178
221
|
|
|
222
|
+
function SearchInput({
|
|
223
|
+
value,
|
|
224
|
+
onChange,
|
|
225
|
+
}: {
|
|
226
|
+
value: string;
|
|
227
|
+
onChange: (value: string) => void;
|
|
228
|
+
}) {
|
|
229
|
+
return (
|
|
230
|
+
<div className="relative w-full md:w-[240px]">
|
|
231
|
+
<Search
|
|
232
|
+
className="pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground"
|
|
233
|
+
aria-hidden
|
|
234
|
+
/>
|
|
235
|
+
<input
|
|
236
|
+
type="text"
|
|
237
|
+
value={value}
|
|
238
|
+
onChange={(e) => onChange(e.target.value)}
|
|
239
|
+
placeholder="Search slides"
|
|
240
|
+
className="h-8 w-full rounded-[6px] border border-border bg-background pl-8 pr-7 text-[12.5px] outline-none placeholder:text-muted-foreground/70 focus-visible:border-foreground/40 focus-visible:ring-2 focus-visible:ring-ring/30"
|
|
241
|
+
/>
|
|
242
|
+
{value && (
|
|
243
|
+
<button
|
|
244
|
+
type="button"
|
|
245
|
+
onClick={() => onChange('')}
|
|
246
|
+
aria-label="Clear search"
|
|
247
|
+
className="absolute right-1.5 top-1/2 flex size-5 -translate-y-1/2 items-center justify-center rounded-[4px] text-muted-foreground hover:bg-muted hover:text-foreground"
|
|
248
|
+
>
|
|
249
|
+
<X className="size-3" />
|
|
250
|
+
</button>
|
|
251
|
+
)}
|
|
252
|
+
</div>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function NoResultsState({ query, onClear }: { query: string; onClear: () => void }) {
|
|
257
|
+
return (
|
|
258
|
+
<div className="rounded-[10px] border border-dashed border-border bg-card/60 px-8 py-20">
|
|
259
|
+
<div className="mx-auto flex max-w-md flex-col items-center text-center">
|
|
260
|
+
<div className="flex size-12 items-center justify-center rounded-full border border-hairline bg-card text-muted-foreground">
|
|
261
|
+
<Search className="size-5" />
|
|
262
|
+
</div>
|
|
263
|
+
<p className="mt-4 font-heading text-[15px] font-semibold tracking-tight">No matches</p>
|
|
264
|
+
<p className="mt-1.5 text-[13px] leading-relaxed text-muted-foreground">
|
|
265
|
+
Nothing matches{' '}
|
|
266
|
+
<span className="font-medium text-foreground">“{query}”</span> in this folder.
|
|
267
|
+
</p>
|
|
268
|
+
<Button variant="ghost" size="sm" className="mt-4" onClick={onClear}>
|
|
269
|
+
Clear search
|
|
270
|
+
</Button>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
179
276
|
function EmptyState({ isDraft, folderName }: { isDraft: boolean; folderName?: string }) {
|
|
180
277
|
return (
|
|
181
|
-
<
|
|
182
|
-
<
|
|
183
|
-
<
|
|
278
|
+
<div className="rounded-[10px] border border-dashed border-border bg-card/60 px-8 py-20">
|
|
279
|
+
<div className="mx-auto flex max-w-md flex-col items-center text-center">
|
|
280
|
+
<div className="flex size-12 items-center justify-center rounded-full border border-hairline bg-card text-muted-foreground">
|
|
281
|
+
<FolderPlus className="size-5" />
|
|
282
|
+
</div>
|
|
184
283
|
{isDraft ? (
|
|
185
284
|
<>
|
|
186
|
-
<p
|
|
187
|
-
|
|
285
|
+
<p className="mt-4 font-heading text-[15px] font-semibold tracking-tight">
|
|
286
|
+
No slides yet
|
|
287
|
+
</p>
|
|
288
|
+
<p className="mt-1.5 text-[13px] leading-relaxed text-muted-foreground">
|
|
188
289
|
Create{' '}
|
|
189
|
-
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-
|
|
290
|
+
<code className="rounded-[4px] bg-muted px-1.5 py-0.5 font-mono text-[11.5px] text-foreground">
|
|
190
291
|
slides/my-slide/index.tsx
|
|
191
292
|
</code>{' '}
|
|
192
|
-
|
|
193
|
-
<code className="rounded bg-muted px-1.5 py-0.5 font-mono text-
|
|
293
|
+
that{' '}
|
|
294
|
+
<code className="rounded-[4px] bg-muted px-1.5 py-0.5 font-mono text-[11.5px] text-foreground">
|
|
194
295
|
export default [Page1, Page2]
|
|
195
296
|
</code>
|
|
196
297
|
.
|
|
@@ -198,15 +299,62 @@ function EmptyState({ isDraft, folderName }: { isDraft: boolean; folderName?: st
|
|
|
198
299
|
</>
|
|
199
300
|
) : (
|
|
200
301
|
<>
|
|
201
|
-
<p
|
|
202
|
-
|
|
302
|
+
<p className="mt-4 font-heading text-[15px] font-semibold tracking-tight">
|
|
303
|
+
{folderName ?? 'This folder'} is empty
|
|
304
|
+
</p>
|
|
305
|
+
<p className="mt-1.5 text-[13px] leading-relaxed text-muted-foreground">
|
|
306
|
+
Drag a slide from Draft into this folder in the sidebar.
|
|
307
|
+
</p>
|
|
203
308
|
</>
|
|
204
309
|
)}
|
|
205
|
-
</
|
|
206
|
-
</
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
207
312
|
);
|
|
208
313
|
}
|
|
209
314
|
|
|
315
|
+
function createDragChip(title: string): HTMLElement | null {
|
|
316
|
+
if (typeof document === 'undefined') return null;
|
|
317
|
+
const chip = document.createElement('div');
|
|
318
|
+
chip.style.cssText = [
|
|
319
|
+
'position: fixed',
|
|
320
|
+
'top: -9999px',
|
|
321
|
+
'left: -9999px',
|
|
322
|
+
'display: inline-flex',
|
|
323
|
+
'align-items: center',
|
|
324
|
+
'gap: 8px',
|
|
325
|
+
'padding: 6px 10px 6px 6px',
|
|
326
|
+
'border-radius: 6px',
|
|
327
|
+
'background: var(--card)',
|
|
328
|
+
'color: var(--foreground)',
|
|
329
|
+
'border: 1px solid var(--border)',
|
|
330
|
+
'box-shadow: 0 12px 32px -8px rgba(0,0,0,0.25), 0 2px 6px rgba(0,0,0,0.08)',
|
|
331
|
+
'font: 500 12.5px/1 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif',
|
|
332
|
+
'white-space: nowrap',
|
|
333
|
+
'pointer-events: none',
|
|
334
|
+
'z-index: 9999',
|
|
335
|
+
].join(';');
|
|
336
|
+
|
|
337
|
+
const thumb = document.createElement('span');
|
|
338
|
+
thumb.style.cssText = [
|
|
339
|
+
'display: inline-block',
|
|
340
|
+
'width: 30px',
|
|
341
|
+
'height: 18px',
|
|
342
|
+
'border-radius: 3px',
|
|
343
|
+
'background: var(--muted)',
|
|
344
|
+
'border: 1px solid var(--border)',
|
|
345
|
+
'flex: 0 0 auto',
|
|
346
|
+
].join(';');
|
|
347
|
+
|
|
348
|
+
const label = document.createElement('span');
|
|
349
|
+
label.textContent = title;
|
|
350
|
+
label.style.cssText = 'overflow: hidden; text-overflow: ellipsis; max-width: 220px;';
|
|
351
|
+
|
|
352
|
+
chip.appendChild(thumb);
|
|
353
|
+
chip.appendChild(label);
|
|
354
|
+
document.body.appendChild(chip);
|
|
355
|
+
return chip;
|
|
356
|
+
}
|
|
357
|
+
|
|
210
358
|
type DialogKind = null | 'rename' | 'move' | 'delete';
|
|
211
359
|
|
|
212
360
|
function SlideCard({
|
|
@@ -216,6 +364,7 @@ function SlideCard({
|
|
|
216
364
|
onRename,
|
|
217
365
|
onMove,
|
|
218
366
|
onDelete,
|
|
367
|
+
onTitleResolved,
|
|
219
368
|
}: {
|
|
220
369
|
id: string;
|
|
221
370
|
folders: Folder[];
|
|
@@ -223,6 +372,7 @@ function SlideCard({
|
|
|
223
372
|
onRename: (name: string) => Promise<void> | void;
|
|
224
373
|
onMove: (folderId: string | null) => Promise<void> | void;
|
|
225
374
|
onDelete: () => Promise<void> | void;
|
|
375
|
+
onTitleResolved?: (id: string, title: string) => void;
|
|
226
376
|
}) {
|
|
227
377
|
const [slide, setSlide] = useState<SlideModule | null>(null);
|
|
228
378
|
const [dragging, setDragging] = useState(false);
|
|
@@ -242,7 +392,10 @@ function SlideCard({
|
|
|
242
392
|
|
|
243
393
|
const FirstPage = slide?.default[0];
|
|
244
394
|
const displayTitle = slide?.meta?.title ?? id;
|
|
245
|
-
|
|
395
|
+
|
|
396
|
+
useEffect(() => {
|
|
397
|
+
if (slide && onTitleResolved) onTitleResolved(id, displayTitle);
|
|
398
|
+
}, [id, slide, displayTitle, onTitleResolved]);
|
|
246
399
|
|
|
247
400
|
return (
|
|
248
401
|
<>
|
|
@@ -252,67 +405,75 @@ function SlideCard({
|
|
|
252
405
|
onDragStart={(e) => {
|
|
253
406
|
e.dataTransfer.setData(SLIDE_DND_MIME, id);
|
|
254
407
|
e.dataTransfer.effectAllowed = 'move';
|
|
408
|
+
const chip = createDragChip(displayTitle);
|
|
409
|
+
if (chip) {
|
|
410
|
+
e.dataTransfer.setDragImage(chip, 14, 14);
|
|
411
|
+
setTimeout(() => chip.remove(), 0);
|
|
412
|
+
}
|
|
255
413
|
setDragging(true);
|
|
256
414
|
}}
|
|
257
415
|
onDragEnd={() => setDragging(false)}
|
|
258
|
-
className={cn(
|
|
416
|
+
className={cn(
|
|
417
|
+
'group relative motion-safe:transition-opacity',
|
|
418
|
+
dragging && 'opacity-40',
|
|
419
|
+
)}
|
|
259
420
|
>
|
|
260
|
-
<Link
|
|
261
|
-
|
|
262
|
-
className="
|
|
263
|
-
>
|
|
264
|
-
<div className="relative aspect-video overflow-hidden bg-gradient-to-br from-indigo-50 to-violet-50">
|
|
421
|
+
<Link to={`/s/${id}`} className="block focus-visible:outline-none">
|
|
422
|
+
{/* Slide thumb — tight border, grey baseboard, no shadcn rounded-xl */}
|
|
423
|
+
<div className="relative aspect-video overflow-hidden rounded-[6px] border border-hairline bg-card shadow-edge ring-1 ring-foreground/[0.04] group-hover:shadow-floating group-hover:ring-foreground/20 motion-safe:transition-[box-shadow,--tw-ring-color] motion-safe:duration-200">
|
|
265
424
|
{FirstPage ? (
|
|
266
|
-
<
|
|
267
|
-
<
|
|
268
|
-
|
|
425
|
+
<div className="h-full w-full motion-safe:transition-transform motion-safe:duration-300 motion-safe:group-hover:scale-[1.03]">
|
|
426
|
+
<SlideCanvas flat freezeMotion design={slide?.design}>
|
|
427
|
+
<FirstPage />
|
|
428
|
+
</SlideCanvas>
|
|
429
|
+
</div>
|
|
269
430
|
) : (
|
|
270
|
-
<div className="grid h-full w-full place-items-center text-
|
|
431
|
+
<div className="grid h-full w-full place-items-center text-[10px] tracking-[0.16em] uppercase text-muted-foreground/60">
|
|
271
432
|
Loading
|
|
272
433
|
</div>
|
|
273
434
|
)}
|
|
274
435
|
</div>
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
</span>
|
|
281
|
-
)}
|
|
436
|
+
|
|
437
|
+
<div className="mt-3">
|
|
438
|
+
<h3 className="min-w-0 truncate font-heading text-[14px] font-medium tracking-tight">
|
|
439
|
+
{displayTitle}
|
|
440
|
+
</h3>
|
|
282
441
|
</div>
|
|
283
442
|
</Link>
|
|
284
443
|
|
|
285
|
-
|
|
286
|
-
<
|
|
287
|
-
<
|
|
288
|
-
<
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
e
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
<
|
|
302
|
-
<
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
<
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
<
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
444
|
+
{import.meta.env.DEV && (
|
|
445
|
+
<div className="absolute right-2 top-2">
|
|
446
|
+
<DropdownMenu>
|
|
447
|
+
<DropdownMenuTrigger asChild>
|
|
448
|
+
<button
|
|
449
|
+
type="button"
|
|
450
|
+
onClick={(e) => {
|
|
451
|
+
e.stopPropagation();
|
|
452
|
+
e.preventDefault();
|
|
453
|
+
}}
|
|
454
|
+
className="flex size-7 items-center justify-center rounded-[5px] bg-card/90 text-foreground shadow-edge ring-1 ring-border opacity-0 backdrop-blur hover:bg-card group-hover:opacity-100 aria-expanded:opacity-100 motion-safe:transition-opacity"
|
|
455
|
+
aria-label="Slide actions"
|
|
456
|
+
>
|
|
457
|
+
<MoreHorizontal className="size-3.5" />
|
|
458
|
+
</button>
|
|
459
|
+
</DropdownMenuTrigger>
|
|
460
|
+
<DropdownMenuContent align="end" className="min-w-[160px]">
|
|
461
|
+
<DropdownMenuItem onSelect={() => setDialog('rename')}>
|
|
462
|
+
<Pencil />
|
|
463
|
+
Rename
|
|
464
|
+
</DropdownMenuItem>
|
|
465
|
+
<DropdownMenuItem onSelect={() => setDialog('move')}>
|
|
466
|
+
<FolderInput />
|
|
467
|
+
Move to folder…
|
|
468
|
+
</DropdownMenuItem>
|
|
469
|
+
<DropdownMenuItem variant="destructive" onSelect={() => setDialog('delete')}>
|
|
470
|
+
<Trash2 />
|
|
471
|
+
Delete
|
|
472
|
+
</DropdownMenuItem>
|
|
473
|
+
</DropdownMenuContent>
|
|
474
|
+
</DropdownMenu>
|
|
475
|
+
</div>
|
|
476
|
+
)}
|
|
316
477
|
</div>
|
|
317
478
|
|
|
318
479
|
<RenameDialog
|
|
@@ -392,6 +553,7 @@ function RenameDialog({
|
|
|
392
553
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
393
554
|
<DialogContent>
|
|
394
555
|
<DialogHeader>
|
|
556
|
+
<span className="eyebrow">Rename</span>
|
|
395
557
|
<DialogTitle>Rename slide</DialogTitle>
|
|
396
558
|
<DialogDescription>Give this slide a new display name.</DialogDescription>
|
|
397
559
|
</DialogHeader>
|
|
@@ -407,10 +569,10 @@ function RenameDialog({
|
|
|
407
569
|
}}
|
|
408
570
|
maxLength={80}
|
|
409
571
|
placeholder="Slide name"
|
|
410
|
-
className="h-9 w-full rounded-
|
|
572
|
+
className="h-9 w-full rounded-[6px] border border-border bg-background px-3 text-[13px] outline-none focus-visible:border-foreground/40 focus-visible:ring-2 focus-visible:ring-ring/30"
|
|
411
573
|
/>
|
|
412
574
|
<DialogFooter>
|
|
413
|
-
<Button variant="
|
|
575
|
+
<Button variant="ghost" size="sm" onClick={() => onOpenChange(false)}>
|
|
414
576
|
Cancel
|
|
415
577
|
</Button>
|
|
416
578
|
<Button size="sm" disabled={submitting} onClick={submit}>
|
|
@@ -464,12 +626,13 @@ function MoveDialog({
|
|
|
464
626
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
465
627
|
<DialogContent>
|
|
466
628
|
<DialogHeader>
|
|
629
|
+
<span className="eyebrow">Move</span>
|
|
467
630
|
<DialogTitle>Move slide</DialogTitle>
|
|
468
631
|
<DialogDescription>
|
|
469
632
|
Choose a folder for <span className="font-medium text-foreground">{slideName}</span>.
|
|
470
633
|
</DialogDescription>
|
|
471
634
|
</DialogHeader>
|
|
472
|
-
<div className="max-h-[320px] overflow-y-auto rounded-
|
|
635
|
+
<div className="max-h-[320px] overflow-y-auto rounded-[6px] border border-border bg-background">
|
|
473
636
|
<FolderOption
|
|
474
637
|
icon={{ type: 'emoji', value: '📝' }}
|
|
475
638
|
label="Draft"
|
|
@@ -487,7 +650,7 @@ function MoveDialog({
|
|
|
487
650
|
))}
|
|
488
651
|
</div>
|
|
489
652
|
<DialogFooter>
|
|
490
|
-
<Button variant="
|
|
653
|
+
<Button variant="ghost" size="sm" onClick={() => onOpenChange(false)}>
|
|
491
654
|
Cancel
|
|
492
655
|
</Button>
|
|
493
656
|
<Button size="sm" disabled={submitting || selected === currentFolderId} onClick={submit}>
|
|
@@ -515,13 +678,18 @@ function FolderOption({
|
|
|
515
678
|
type="button"
|
|
516
679
|
onClick={onClick}
|
|
517
680
|
className={cn(
|
|
518
|
-
'flex w-full items-center gap-2 border-b px-3 py-2 text-left text-
|
|
519
|
-
active ? 'bg-
|
|
681
|
+
'flex w-full items-center gap-2 border-b border-hairline px-3 py-2 text-left text-[13px] transition-colors last:border-b-0',
|
|
682
|
+
active ? 'bg-muted text-foreground' : 'hover:bg-muted/60',
|
|
520
683
|
)}
|
|
521
684
|
>
|
|
522
685
|
<FolderIconChip icon={icon} />
|
|
523
686
|
<span className="truncate">{label}</span>
|
|
524
|
-
{active &&
|
|
687
|
+
{active && (
|
|
688
|
+
<span className="ml-auto inline-flex items-center gap-1 text-[10.5px] text-brand">
|
|
689
|
+
<span className="inline-block size-1 rounded-full bg-brand" aria-hidden />
|
|
690
|
+
Selected
|
|
691
|
+
</span>
|
|
692
|
+
)}
|
|
525
693
|
</button>
|
|
526
694
|
);
|
|
527
695
|
}
|
|
@@ -556,6 +724,7 @@ function DeleteDialog({
|
|
|
556
724
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
557
725
|
<DialogContent>
|
|
558
726
|
<DialogHeader>
|
|
727
|
+
<span className="eyebrow text-destructive/80">Destructive</span>
|
|
559
728
|
<DialogTitle>Delete slide?</DialogTitle>
|
|
560
729
|
<DialogDescription>
|
|
561
730
|
This permanently removes{' '}
|
|
@@ -564,7 +733,7 @@ function DeleteDialog({
|
|
|
564
733
|
</DialogDescription>
|
|
565
734
|
</DialogHeader>
|
|
566
735
|
<DialogFooter>
|
|
567
|
-
<Button variant="
|
|
736
|
+
<Button variant="ghost" size="sm" onClick={() => onOpenChange(false)}>
|
|
568
737
|
Cancel
|
|
569
738
|
</Button>
|
|
570
739
|
<Button variant="destructive" size="sm" disabled={submitting} onClick={confirm}>
|