@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
@@ -0,0 +1,146 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { format, useLocale } from '@/lib/use-locale';
3
+ import { loadThemeDemo, type Theme, type ThemeDemoModule, themes } from '../../lib/themes';
4
+ import { SlideCanvas } from '../slide-canvas';
5
+
6
+ export function ThemesGallery({ onOpen }: { onOpen: (id: string) => void }) {
7
+ const t = useLocale();
8
+
9
+ if (themes.length === 0) {
10
+ return <ThemesEmptyState />;
11
+ }
12
+
13
+ return (
14
+ <ul className="grid grid-cols-[repeat(auto-fill,minmax(min(240px,100%),1fr))] gap-x-6 gap-y-9 md:grid-cols-[repeat(auto-fill,minmax(340px,1fr))]">
15
+ {themes.map((theme) => (
16
+ <li key={theme.id}>
17
+ <ThemeCard
18
+ theme={theme}
19
+ onOpen={() => onOpen(theme.id)}
20
+ ariaLabel={format(t.themes.openThemeAria, { name: theme.name })}
21
+ />
22
+ </li>
23
+ ))}
24
+ </ul>
25
+ );
26
+ }
27
+
28
+ function ThemeCard({
29
+ theme,
30
+ onOpen,
31
+ ariaLabel,
32
+ }: {
33
+ theme: Theme;
34
+ onOpen: () => void;
35
+ ariaLabel: string;
36
+ }) {
37
+ return (
38
+ <button
39
+ type="button"
40
+ onClick={onOpen}
41
+ aria-label={ariaLabel}
42
+ className="group block w-full text-left focus-visible:outline-none"
43
+ >
44
+ <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">
45
+ <ThemePreview theme={theme} />
46
+ </div>
47
+ <div className="mt-3">
48
+ <h3 className="min-w-0 truncate font-heading text-[14px] font-medium tracking-tight">
49
+ {theme.name}
50
+ </h3>
51
+ </div>
52
+ {theme.description ? (
53
+ <p className="mt-1 line-clamp-2 text-[12px] leading-snug text-muted-foreground">
54
+ {theme.description}
55
+ </p>
56
+ ) : null}
57
+ </button>
58
+ );
59
+ }
60
+
61
+ function ThemePreview({ theme }: { theme: Theme }) {
62
+ const t = useLocale();
63
+ const demo = useThemeDemo(theme);
64
+
65
+ if (!theme.hasDemo) {
66
+ return <NoDemoState />;
67
+ }
68
+ if (!demo) {
69
+ return (
70
+ <div className="grid h-full w-full place-items-center text-[10px] tracking-[0.16em] uppercase text-muted-foreground/60">
71
+ {t.common.loading}
72
+ </div>
73
+ );
74
+ }
75
+ const FirstPage = demo.default[0];
76
+ if (!FirstPage) return <NoDemoState />;
77
+
78
+ return (
79
+ <div className="h-full w-full motion-safe:transition-transform motion-safe:duration-300 motion-safe:group-hover:scale-[1.03]">
80
+ <SlideCanvas flat freezeMotion design={demo.design}>
81
+ <FirstPage />
82
+ </SlideCanvas>
83
+ </div>
84
+ );
85
+ }
86
+
87
+ function NoDemoState() {
88
+ const t = useLocale();
89
+ return (
90
+ <div className="grid h-full w-full place-items-center bg-muted/40 px-6 text-center">
91
+ <div>
92
+ <p className="font-heading text-[12px] font-semibold tracking-tight text-foreground/80">
93
+ {t.themes.noDemoYet}
94
+ </p>
95
+ <p className="mt-1 text-[10.5px] leading-snug text-muted-foreground">
96
+ {t.themes.noDemoHintPrefix}
97
+ <code className="rounded-[3px] bg-card px-1 py-0.5 font-mono text-[10px] text-foreground">
98
+ /create-theme
99
+ </code>
100
+ {t.themes.noDemoHintSuffix}
101
+ </p>
102
+ </div>
103
+ </div>
104
+ );
105
+ }
106
+
107
+ function ThemesEmptyState() {
108
+ const t = useLocale();
109
+ return (
110
+ <div className="rounded-[10px] border border-dashed border-border bg-card/60 px-8 py-20">
111
+ <div className="mx-auto flex max-w-md flex-col items-center text-center">
112
+ <div className="text-2xl">🎨</div>
113
+ <p className="mt-3 font-heading text-[15px] font-semibold tracking-tight">
114
+ {t.themes.noThemesTitle}
115
+ </p>
116
+ <p className="mt-1.5 text-[13px] leading-relaxed text-muted-foreground">
117
+ {t.themes.noThemesHintPrefix}
118
+ <code className="rounded-[4px] bg-muted px-1.5 py-0.5 font-mono text-[11.5px] text-foreground">
119
+ /create-theme
120
+ </code>
121
+ {t.themes.noThemesHintSuffix}
122
+ </p>
123
+ </div>
124
+ </div>
125
+ );
126
+ }
127
+
128
+ function useThemeDemo(theme: Theme): ThemeDemoModule | null {
129
+ const [demo, setDemo] = useState<ThemeDemoModule | null>(null);
130
+ useEffect(() => {
131
+ if (!theme.hasDemo) {
132
+ setDemo(null);
133
+ return;
134
+ }
135
+ let cancelled = false;
136
+ loadThemeDemo(theme.id)
137
+ .then((mod) => {
138
+ if (!cancelled) setDemo(mod);
139
+ })
140
+ .catch(() => {});
141
+ return () => {
142
+ cancelled = true;
143
+ };
144
+ }, [theme.id, theme.hasDemo]);
145
+ return demo;
146
+ }
@@ -2,6 +2,7 @@ import {
2
2
  closestCenter,
3
3
  DndContext,
4
4
  type DragEndEvent,
5
+ type DragStartEvent,
5
6
  KeyboardSensor,
6
7
  PointerSensor,
7
8
  useSensor,
@@ -14,7 +15,15 @@ import {
14
15
  verticalListSortingStrategy,
15
16
  } from '@dnd-kit/sortable';
16
17
  import { CSS } from '@dnd-kit/utilities';
17
- import { useEffect, useRef } from 'react';
18
+ import { Copy, Trash2 } from 'lucide-react';
19
+ import { Fragment, useEffect, useRef } from 'react';
20
+ import {
21
+ ContextMenu,
22
+ ContextMenuContent,
23
+ ContextMenuItem,
24
+ ContextMenuSeparator,
25
+ ContextMenuTrigger,
26
+ } from '@/components/ui/context-menu';
18
27
  import { ScrollArea } from '@/components/ui/scroll-area';
19
28
  import { format, useLocale } from '@/lib/use-locale';
20
29
  import { cn } from '@/lib/utils';
@@ -25,16 +34,26 @@ import { SlideCanvas } from './slide-canvas';
25
34
 
26
35
  type Orientation = 'vertical' | 'horizontal';
27
36
 
37
+ export type ThumbnailActions = {
38
+ onDuplicate: (index: number) => void;
39
+ onDelete: (index: number) => void;
40
+ };
41
+
28
42
  type Props = {
29
43
  pages: Page[];
30
44
  design?: DesignSystem;
31
45
  current: number;
32
46
  onSelect: (index: number) => void;
33
47
  onReorder?: (from: number, to: number) => void;
48
+ actions?: ThumbnailActions;
34
49
  orientation?: Orientation;
50
+ /** Vertical-only: total rail width in px. Thumbnails scale to fit. */
51
+ width?: number;
35
52
  };
36
53
 
37
- const VERTICAL_THUMB_WIDTH = 184;
54
+ const DEFAULT_VERTICAL_THUMB_WIDTH = 184;
55
+ const VERTICAL_RAIL_CHROME = 80;
56
+ const MIN_VERTICAL_THUMB_WIDTH = 120;
38
57
  const HORIZONTAL_THUMB_HEIGHT = 64;
39
58
 
40
59
  export function ThumbnailRail({
@@ -43,7 +62,9 @@ export function ThumbnailRail({
43
62
  current,
44
63
  onSelect,
45
64
  onReorder,
65
+ actions,
46
66
  orientation = 'vertical',
67
+ width,
47
68
  }: Props) {
48
69
  const activeRef = useRef<HTMLButtonElement | null>(null);
49
70
  const t = useLocale();
@@ -68,7 +89,7 @@ export function ThumbnailRail({
68
89
  <div className="flex items-center gap-2 px-3 py-2.5">
69
90
  {pages.map((PageComp, i) => {
70
91
  const active = i === current;
71
- return (
92
+ const button = (
72
93
  <button
73
94
  // biome-ignore lint/suspicious/noArrayIndexKey: pages list is render-stable
74
95
  key={i}
@@ -89,7 +110,7 @@ export function ThumbnailRail({
89
110
  </span>
90
111
  <div
91
112
  className={cn(
92
- 'relative shrink-0 overflow-hidden rounded-[4px] border bg-card motion-safe:transition-all',
113
+ 'relative shrink-0 overflow-hidden rounded-[4px] border bg-card motion-safe:transition-[border-color,box-shadow]',
93
114
  active
94
115
  ? 'border-brand shadow-[0_0_0_1px_var(--brand)]'
95
116
  : 'border-hairline group-hover/thumb:border-foreground/25',
@@ -102,6 +123,19 @@ export function ThumbnailRail({
102
123
  </div>
103
124
  </button>
104
125
  );
126
+ if (!actions) return button;
127
+ return (
128
+ <ThumbContextMenu
129
+ // biome-ignore lint/suspicious/noArrayIndexKey: pages list is render-stable
130
+ key={i}
131
+ index={i}
132
+ actions={actions}
133
+ pageCount={pages.length}
134
+ ariaLabel={format(t.thumbnailRail.pageActionsAria, { n: i + 1 })}
135
+ >
136
+ {button}
137
+ </ThumbContextMenu>
138
+ );
105
139
  })}
106
140
  </div>
107
141
  </div>
@@ -109,7 +143,11 @@ export function ThumbnailRail({
109
143
  );
110
144
  }
111
145
 
112
- const scale = VERTICAL_THUMB_WIDTH / CANVAS_WIDTH;
146
+ const thumbWidth =
147
+ width != null
148
+ ? Math.max(MIN_VERTICAL_THUMB_WIDTH, width - VERTICAL_RAIL_CHROME)
149
+ : DEFAULT_VERTICAL_THUMB_WIDTH;
150
+ const scale = thumbWidth / CANVAS_WIDTH;
113
151
  const height = CANVAS_HEIGHT * scale;
114
152
 
115
153
  const renderThumb = (PageComp: Page, i: number) => {
@@ -121,28 +159,23 @@ export function ThumbnailRail({
121
159
  page={PageComp}
122
160
  design={design}
123
161
  scale={scale}
162
+ thumbWidth={thumbWidth}
124
163
  height={height}
125
164
  />
126
165
  );
127
166
 
128
- if (onReorder) {
129
- return (
130
- <SortableThumb
131
- key={i}
132
- index={i}
133
- active={active}
134
- activeRef={active ? activeRef : undefined}
135
- onSelect={() => onSelect(i)}
136
- ariaLabel={format(t.thumbnailRail.goToPageAria, { n: i + 1 })}
137
- >
138
- {inner}
139
- </SortableThumb>
140
- );
141
- }
142
-
143
- return (
167
+ const node = onReorder ? (
168
+ <SortableThumb
169
+ index={i}
170
+ active={active}
171
+ activeRef={active ? activeRef : undefined}
172
+ onSelect={() => onSelect(i)}
173
+ ariaLabel={format(t.thumbnailRail.goToPageAria, { n: i + 1 })}
174
+ >
175
+ {inner}
176
+ </SortableThumb>
177
+ ) : (
144
178
  <button
145
- key={i}
146
179
  type="button"
147
180
  ref={active ? activeRef : undefined}
148
181
  onClick={() => onSelect(i)}
@@ -153,6 +186,21 @@ export function ThumbnailRail({
153
186
  {inner}
154
187
  </button>
155
188
  );
189
+
190
+ if (!actions) {
191
+ return <Fragment key={i}>{node}</Fragment>;
192
+ }
193
+ return (
194
+ <ThumbContextMenu
195
+ key={i}
196
+ index={i}
197
+ actions={actions}
198
+ pageCount={pages.length}
199
+ ariaLabel={format(t.thumbnailRail.pageActionsAria, { n: i + 1 })}
200
+ >
201
+ {node}
202
+ </ThumbContextMenu>
203
+ );
156
204
  };
157
205
 
158
206
  const list = (
@@ -171,7 +219,7 @@ export function ThumbnailRail({
171
219
 
172
220
  return (
173
221
  <ScrollArea className="h-full border-r border-hairline bg-sidebar">
174
- <SortableRail pages={pages} onReorder={onReorder}>
222
+ <SortableRail pages={pages} onReorder={onReorder} onSelect={onSelect}>
175
223
  {list}
176
224
  </SortableRail>
177
225
  </ScrollArea>
@@ -192,6 +240,7 @@ function ThumbContents({
192
240
  page: PageComp,
193
241
  design,
194
242
  scale,
243
+ thumbWidth,
195
244
  height,
196
245
  }: {
197
246
  index: number;
@@ -199,6 +248,7 @@ function ThumbContents({
199
248
  page: Page;
200
249
  design?: DesignSystem;
201
250
  scale: number;
251
+ thumbWidth: number;
202
252
  height: number;
203
253
  }) {
204
254
  return (
@@ -213,12 +263,12 @@ function ThumbContents({
213
263
  </span>
214
264
  <div
215
265
  className={cn(
216
- 'relative shrink-0 overflow-hidden rounded-[4px] border bg-card motion-safe:transition-all',
266
+ 'relative shrink-0 overflow-hidden rounded-[4px] border bg-card motion-safe:transition-[border-color,box-shadow]',
217
267
  active
218
268
  ? 'border-brand shadow-[0_0_0_1px_var(--brand)]'
219
269
  : 'border-hairline group-hover/thumb:border-foreground/25',
220
270
  )}
221
- style={{ width: VERTICAL_THUMB_WIDTH, height }}
271
+ style={{ width: thumbWidth, height }}
222
272
  >
223
273
  <SlideCanvas scale={scale} center={false} flat freezeMotion design={design}>
224
274
  <PageComp />
@@ -234,13 +284,56 @@ function ThumbContents({
234
284
  );
235
285
  }
236
286
 
287
+ function ThumbContextMenu({
288
+ index,
289
+ actions,
290
+ pageCount,
291
+ ariaLabel,
292
+ children,
293
+ }: {
294
+ index: number;
295
+ actions: ThumbnailActions;
296
+ pageCount: number;
297
+ ariaLabel: string;
298
+ children: React.ReactNode;
299
+ }) {
300
+ const t = useLocale();
301
+ const canDelete = pageCount > 1;
302
+ return (
303
+ <ContextMenu>
304
+ <ContextMenuTrigger asChild aria-label={ariaLabel}>
305
+ {children}
306
+ </ContextMenuTrigger>
307
+ <ContextMenuContent className="min-w-[180px]">
308
+ <ContextMenuItem onSelect={() => actions.onDuplicate(index)}>
309
+ <Copy />
310
+ {t.thumbnailRail.duplicatePage}
311
+ </ContextMenuItem>
312
+ <ContextMenuSeparator />
313
+ <ContextMenuItem
314
+ variant="destructive"
315
+ disabled={!canDelete}
316
+ onSelect={() => {
317
+ if (canDelete) actions.onDelete(index);
318
+ }}
319
+ >
320
+ <Trash2 />
321
+ {t.thumbnailRail.deletePage}
322
+ </ContextMenuItem>
323
+ </ContextMenuContent>
324
+ </ContextMenu>
325
+ );
326
+ }
327
+
237
328
  function SortableRail({
238
329
  pages,
239
330
  onReorder,
331
+ onSelect,
240
332
  children,
241
333
  }: {
242
334
  pages: Page[];
243
335
  onReorder: (from: number, to: number) => void;
336
+ onSelect: (index: number) => void;
244
337
  children: React.ReactNode;
245
338
  }) {
246
339
  const sensors = useSensors(
@@ -250,6 +343,11 @@ function SortableRail({
250
343
 
251
344
  const items = pages.map((_, i) => i + 1);
252
345
 
346
+ const handleDragStart = (event: DragStartEvent) => {
347
+ const i = (event.active.id as number) - 1;
348
+ if (i >= 0) onSelect(i);
349
+ };
350
+
253
351
  const handleDragEnd = (event: DragEndEvent) => {
254
352
  const { active, over } = event;
255
353
  if (!over || active.id === over.id) return;
@@ -260,7 +358,12 @@ function SortableRail({
260
358
  };
261
359
 
262
360
  return (
263
- <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
361
+ <DndContext
362
+ sensors={sensors}
363
+ collisionDetection={closestCenter}
364
+ onDragStart={handleDragStart}
365
+ onDragEnd={handleDragEnd}
366
+ >
264
367
  <SortableContext items={items} strategy={verticalListSortingStrategy}>
265
368
  {children}
266
369
  </SortableContext>
@@ -275,6 +378,7 @@ function SortableThumb({
275
378
  onSelect,
276
379
  ariaLabel,
277
380
  children,
381
+ ...rest
278
382
  }: {
279
383
  index: number;
280
384
  active: boolean;
@@ -282,7 +386,10 @@ function SortableThumb({
282
386
  onSelect: () => void;
283
387
  ariaLabel: string;
284
388
  children: React.ReactNode;
285
- }) {
389
+ } & Omit<
390
+ React.ButtonHTMLAttributes<HTMLButtonElement>,
391
+ 'onClick' | 'aria-label' | 'aria-current' | 'type' | 'style' | 'className' | 'ref' | 'children'
392
+ >) {
286
393
  const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
287
394
  id: index + 1,
288
395
  });
@@ -296,6 +403,7 @@ function SortableThumb({
296
403
 
297
404
  return (
298
405
  <button
406
+ {...rest}
299
407
  ref={setRef}
300
408
  type="button"
301
409
  onClick={onSelect}
@@ -308,8 +416,7 @@ function SortableThumb({
308
416
  }}
309
417
  className={cn(
310
418
  thumbButtonClass(active),
311
- 'cursor-grab active:cursor-grabbing',
312
- isDragging && 'z-10 opacity-60 shadow-edge ring-1 ring-brand',
419
+ isDragging && 'z-10 cursor-grabbing opacity-60 shadow-edge ring-1 ring-brand',
313
420
  )}
314
421
  {...attributes}
315
422
  {...listeners}