@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.
Files changed (88) hide show
  1. package/dist/{build-DHiRlpjn.js → build-aiY_8kwE.js} +2 -1
  2. package/dist/cli/bin.js +43 -4
  3. package/dist/{config-LZM903FE.js → config-CVqRAagl.js} +592 -63
  4. package/dist/design-CROQh0AA.js +35 -0
  5. package/dist/{dev-B3JzCYn7.js → dev-R2we2iaF.js} +2 -1
  6. package/dist/index.d.ts +55 -4
  7. package/dist/index.js +110 -1
  8. package/dist/{preview-UikovHEt.js → preview-CU4zSyGp.js} +2 -1
  9. package/dist/sync-3oqN1WyK.js +139 -0
  10. package/dist/sync-B4eLo2H6.js +3 -0
  11. package/dist/vite/index.d.ts +1 -1
  12. package/dist/vite/index.js +2 -1
  13. package/package.json +2 -1
  14. package/skills/apply-comments/SKILL.md +83 -0
  15. package/skills/create-slide/SKILL.md +81 -0
  16. package/skills/create-theme/SKILL.md +194 -0
  17. package/skills/slide-authoring/SKILL.md +288 -0
  18. package/src/app/{App.tsx → app.tsx} +8 -6
  19. package/src/app/components/{AssetView.tsx → asset-view.tsx} +41 -33
  20. package/src/app/components/{ClickNavZones.tsx → click-nav-zones.tsx} +1 -1
  21. package/src/app/components/history-provider.tsx +120 -0
  22. package/src/app/components/image-placeholder.tsx +121 -0
  23. package/src/app/components/inspector/{CommentWidget.tsx → comment-widget.tsx} +1 -1
  24. package/src/app/components/inspector/{InspectOverlay.tsx → inspect-overlay.tsx} +1 -1
  25. package/src/app/components/inspector/{InspectorPanel.tsx → inspector-panel.tsx} +164 -212
  26. package/src/app/components/inspector/{InspectorProvider.tsx → inspector-provider.tsx} +186 -18
  27. package/src/app/components/inspector/save-bar.tsx +47 -0
  28. package/src/app/components/panel/panel-fields.tsx +60 -0
  29. package/src/app/components/panel/panel-shell.tsx +78 -0
  30. package/src/app/components/panel/save-card.tsx +139 -0
  31. package/src/app/components/pdf-progress-toast.tsx +25 -0
  32. package/src/app/components/player.tsx +341 -0
  33. package/src/app/components/present/blackout-overlay.tsx +18 -0
  34. package/src/app/components/present/control-bar.tsx +204 -0
  35. package/src/app/components/present/help-overlay.tsx +56 -0
  36. package/src/app/components/present/jump-input.tsx +74 -0
  37. package/src/app/components/present/laser-pointer.tsx +40 -0
  38. package/src/app/components/present/overview-grid.tsx +184 -0
  39. package/src/app/components/present/progress-bar.tsx +26 -0
  40. package/src/app/components/present/use-idle.ts +44 -0
  41. package/src/app/components/present/use-pointer-near-bottom.ts +34 -0
  42. package/src/app/components/present/use-presenter-channel.ts +71 -0
  43. package/src/app/components/present/use-touch-swipe.ts +63 -0
  44. package/src/app/components/sidebar/{FolderItem.tsx → folder-item.tsx} +62 -27
  45. package/src/app/components/sidebar/{IconPicker.tsx → icon-picker.tsx} +13 -10
  46. package/src/app/components/sidebar/{Sidebar.tsx → sidebar.tsx} +40 -34
  47. package/src/app/components/{SlideCanvas.tsx → slide-canvas.tsx} +35 -10
  48. package/src/app/components/style-panel/design-provider.tsx +139 -0
  49. package/src/app/components/style-panel/style-panel.tsx +326 -0
  50. package/src/app/components/style-panel/use-design.ts +112 -0
  51. package/src/app/components/theme-toggle.tsx +57 -0
  52. package/src/app/components/thumbnail-rail.tsx +151 -0
  53. package/src/app/components/ui/button.tsx +51 -19
  54. package/src/app/components/ui/card.tsx +1 -1
  55. package/src/app/components/ui/dialog.tsx +25 -9
  56. package/src/app/components/ui/dropdown-menu.tsx +29 -12
  57. package/src/app/components/ui/input.tsx +13 -9
  58. package/src/app/components/ui/popover.tsx +5 -2
  59. package/src/app/components/ui/progress.tsx +2 -2
  60. package/src/app/components/ui/select.tsx +11 -5
  61. package/src/app/components/ui/separator.tsx +1 -1
  62. package/src/app/components/ui/slider.tsx +4 -4
  63. package/src/app/components/ui/sonner.tsx +11 -1
  64. package/src/app/components/ui/tabs.tsx +6 -6
  65. package/src/app/components/ui/textarea.tsx +11 -7
  66. package/src/app/components/ui/toggle-group.tsx +2 -2
  67. package/src/app/components/ui/toggle.tsx +6 -6
  68. package/src/app/components/ui/tooltip.tsx +5 -2
  69. package/src/app/lib/export-html.ts +10 -1
  70. package/src/app/lib/export-pdf.ts +7 -0
  71. package/src/app/lib/folders.ts +1 -1
  72. package/src/app/lib/inspector/{useEditor.ts → use-editor.ts} +2 -1
  73. package/src/app/lib/sdk.ts +5 -0
  74. package/src/app/lib/slides.ts +1 -1
  75. package/src/app/lib/utils.ts +1 -1
  76. package/src/app/main.tsx +5 -2
  77. package/src/app/routes/{Home.tsx → home.tsx} +266 -97
  78. package/src/app/routes/presenter.tsx +400 -0
  79. package/src/app/routes/slide.tsx +519 -0
  80. package/src/app/styles.css +338 -67
  81. package/src/app/components/PdfProgressToast.tsx +0 -23
  82. package/src/app/components/Player.tsx +0 -100
  83. package/src/app/components/ThumbnailRail.tsx +0 -68
  84. package/src/app/components/inspector/SaveBar.tsx +0 -77
  85. package/src/app/routes/Slide.tsx +0 -478
  86. /package/dist/{config-SXL5qIl6.d.ts → config-DweCbRkQ.d.ts} +0 -0
  87. /package/src/app/lib/inspector/{useComments.ts → use-comments.ts} +0 -0
  88. /package/src/app/lib/{useWheelPageNavigation.ts → use-wheel-page-navigation.ts} +0 -0
@@ -1,8 +1,15 @@
1
- import { FolderInput, FolderPlus, MoreHorizontal, Pencil, Trash2 } from 'lucide-react';
2
- import { useEffect, useMemo, useRef, useState } from 'react';
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 { SlideCanvas } from '../components/SlideCanvas';
23
- import { FolderIconChip, SLIDE_DND_MIME } from '../components/sidebar/FolderItem';
24
- import { DRAFT_ID, Sidebar } from '../components/sidebar/Sidebar';
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-screen overflow-hidden bg-background">
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
- <div className="border-b bg-card/40 px-4 py-3 md:hidden">
93
- <div className="mb-2 font-heading text-lg font-bold tracking-tight">open-slide</div>
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-6xl px-4 py-6 md:px-8 md:py-12">
116
- <header className="mb-6 flex items-center gap-3 md:mb-8">
117
- <FolderIconChip icon={headerIcon} className="size-6 text-xl" />
118
- <h2 className="font-heading text-xl font-bold tracking-tight md:text-2xl">{title}</h2>
119
- <span className="text-sm text-muted-foreground">
120
- {visibleSlides.length} slide{visibleSlides.length === 1 ? '' : 's'}
121
- </span>
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={selectedId === DRAFT_ID} folderName={selectedFolder?.name} />
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(220px,1fr))] gap-4 md:grid-cols-[repeat(auto-fill,minmax(280px,1fr))] md:gap-5">
128
- {visibleSlides.map((id) => (
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-full border px-3 py-1.5 text-xs font-medium transition-colors ' +
167
- (active
168
- ? 'border-primary bg-primary/10 text-primary'
169
- : 'border-border bg-background text-muted-foreground hover:text-foreground')
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-4 text-sm" />
215
+ <FolderIconChip icon={icon} className="size-3.5 text-sm" />
173
216
  <span className="truncate max-w-[8rem]">{label}</span>
174
- <span className="tabular-nums opacity-70">{count}</span>
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">&ldquo;{query}&rdquo;</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
- <Card className="border-dashed">
182
- <CardContent className="flex flex-col items-center gap-3 py-16 text-center text-muted-foreground">
183
- <FolderPlus className="size-8 opacity-50" />
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>No slides yet.</p>
187
- <p className="text-sm">
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-xs">
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
- with{' '}
193
- <code className="rounded bg-muted px-1.5 py-0.5 font-mono text-xs">
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>No slides in {folderName ?? 'this folder'}.</p>
202
- <p className="text-sm">Drag a slide from Draft into the sidebar folder.</p>
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
- </CardContent>
206
- </Card>
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
- const pageCount = slide?.default.length ?? 0;
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('group relative', dragging && 'opacity-50')}
416
+ className={cn(
417
+ 'group relative motion-safe:transition-opacity',
418
+ dragging && 'opacity-40',
419
+ )}
259
420
  >
260
- <Link
261
- to={`/s/${id}`}
262
- className="block overflow-hidden rounded-xl bg-card text-card-foreground ring-1 ring-foreground/10 transition-all duration-200 hover:-translate-y-0.5 hover:ring-foreground/20 hover:shadow-lg"
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
- <SlideCanvas flat>
267
- <FirstPage />
268
- </SlideCanvas>
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-xs tracking-widest uppercase text-muted-foreground/60">
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
- <div className="flex items-baseline justify-between gap-3 px-4 py-3">
276
- <span className="truncate text-sm font-medium">{displayTitle}</span>
277
- {pageCount > 0 && (
278
- <span className="shrink-0 text-xs text-muted-foreground tabular-nums">
279
- {pageCount} page{pageCount === 1 ? '' : 's'}
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
- <div className="absolute right-2 top-2">
286
- <DropdownMenu>
287
- <DropdownMenuTrigger asChild>
288
- <button
289
- type="button"
290
- onClick={(e) => {
291
- e.stopPropagation();
292
- e.preventDefault();
293
- }}
294
- className="flex size-7 items-center justify-center rounded-md bg-background/80 text-foreground shadow-sm ring-1 ring-foreground/10 opacity-0 backdrop-blur transition-opacity hover:bg-background group-hover:opacity-100 aria-expanded:opacity-100"
295
- aria-label="Slide actions"
296
- >
297
- <MoreHorizontal className="size-4" />
298
- </button>
299
- </DropdownMenuTrigger>
300
- <DropdownMenuContent align="end" className="min-w-[160px]">
301
- <DropdownMenuItem onSelect={() => setDialog('rename')}>
302
- <Pencil />
303
- Rename
304
- </DropdownMenuItem>
305
- <DropdownMenuItem onSelect={() => setDialog('move')}>
306
- <FolderInput />
307
- Move to folder
308
- </DropdownMenuItem>
309
- <DropdownMenuItem variant="destructive" onSelect={() => setDialog('delete')}>
310
- <Trash2 />
311
- Delete
312
- </DropdownMenuItem>
313
- </DropdownMenuContent>
314
- </DropdownMenu>
315
- </div>
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-md border bg-background px-3 text-sm outline-none ring-ring/40 focus:ring-2"
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="outline" size="sm" onClick={() => onOpenChange(false)}>
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-md border">
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="outline" size="sm" onClick={() => onOpenChange(false)}>
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-sm last:border-b-0 transition-colors',
519
- active ? 'bg-primary/10 text-primary' : 'hover:bg-muted/60',
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 && <span className="ml-auto text-xs tracking-wide opacity-70">Selected</span>}
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="outline" size="sm" onClick={() => onOpenChange(false)}>
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}>