@open-slide/core 1.6.0 → 1.8.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 (45) hide show
  1. package/dist/{build-tLrkKUHr.js → build-CCZDC8eF.js} +1 -1
  2. package/dist/cli/bin.js +3 -3
  3. package/dist/{config-PwUHqZ_X.js → config-C7sZtiY2.js} +45 -18
  4. package/dist/{config-CfMThYN9.d.ts → config-D1bANimZ.d.ts} +1 -1
  5. package/dist/{dev-DpCIRbhT.js → dev-kLS_4CAI.js} +1 -1
  6. package/dist/{en-BDnM5zKJ.js → en-hyGpmL1O.js} +1 -4
  7. package/dist/index.d.ts +22 -4
  8. package/dist/index.js +1 -1
  9. package/dist/locale/index.d.ts +1 -1
  10. package/dist/locale/index.js +4 -13
  11. package/dist/{preview-BSGlM6Se.js → preview-DUkOjOx8.js} +1 -1
  12. package/dist/{types-B-KrjgX8.d.ts → types-Bvk1pM70.d.ts} +1 -4
  13. package/dist/vite/index.d.ts +2 -2
  14. package/dist/vite/index.js +1 -1
  15. package/package.json +1 -1
  16. package/skills/create-theme/SKILL.md +1 -1
  17. package/skills/slide-authoring/SKILL.md +169 -0
  18. package/src/app/components/inspector/asset-picker-dialog.tsx +196 -0
  19. package/src/app/components/inspector/comment-widget.tsx +16 -2
  20. package/src/app/components/inspector/inspect-overlay.tsx +132 -35
  21. package/src/app/components/inspector/inspector-panel.tsx +19 -256
  22. package/src/app/components/inspector/inspector-provider.tsx +102 -1
  23. package/src/app/components/panel/save-card.tsx +4 -4
  24. package/src/app/components/player.tsx +25 -25
  25. package/src/app/components/sidebar/folder-item.tsx +7 -2
  26. package/src/app/components/sidebar/sidebar.tsx +87 -16
  27. package/src/app/components/slide-transition-layer.tsx +154 -0
  28. package/src/app/components/style-panel/style-panel.tsx +3 -0
  29. package/src/app/lib/folders.ts +28 -0
  30. package/src/app/lib/inspector/fiber.test.ts +154 -0
  31. package/src/app/lib/inspector/fiber.ts +12 -1
  32. package/src/app/lib/sdk.ts +3 -1
  33. package/src/app/lib/transition.ts +23 -0
  34. package/src/app/lib/use-click-page-navigation.ts +52 -0
  35. package/src/app/lib/use-is-mobile.ts +21 -0
  36. package/src/app/lib/use-prefers-reduced-motion.ts +19 -0
  37. package/src/app/routes/home-shell.tsx +8 -0
  38. package/src/app/routes/home.tsx +1 -1
  39. package/src/app/routes/slide.tsx +92 -60
  40. package/src/locale/en.ts +1 -5
  41. package/src/locale/ja.ts +1 -5
  42. package/src/locale/types.ts +1 -5
  43. package/src/locale/zh-cn.ts +1 -5
  44. package/src/locale/zh-tw.ts +1 -5
  45. package/src/app/components/click-nav-zones.tsx +0 -36
@@ -39,6 +39,7 @@ export function HomeShell() {
39
39
  create,
40
40
  update,
41
41
  remove,
42
+ reorder,
42
43
  assign,
43
44
  renameSlide,
44
45
  duplicateSlide,
@@ -147,6 +148,13 @@ export function HomeShell() {
147
148
  }}
148
149
  onDropToFolder={(folderId, slideId) => moveSlideWithToast(slideId, folderId)}
149
150
  onDropToDraft={(slideId) => moveSlideWithToast(slideId, null)}
151
+ onReorder={async (ids) => {
152
+ try {
153
+ await reorder(ids);
154
+ } catch {
155
+ toast.error(t.home.toastFolderReorderFailed);
156
+ }
157
+ }}
150
158
  />
151
159
  </div>
152
160
 
@@ -242,7 +242,7 @@ function SortControl({ value, onChange }: { value: SortKey; onChange: (next: Sor
242
242
  <button
243
243
  type="button"
244
244
  aria-label={`${t.home.sortLabel}: ${labels[value]}`}
245
- className="flex h-8 items-center gap-1.5 rounded-[6px] border border-border bg-background pl-2 pr-1.5 text-[12.5px] font-medium text-foreground outline-none hover:bg-muted focus-visible:border-foreground/40 focus-visible:ring-2 focus-visible:ring-ring/30"
245
+ className="flex h-8 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-[6px] border border-border bg-background pl-2 pr-1.5 text-[12.5px] font-medium text-foreground outline-none hover:bg-muted focus-visible:border-foreground/40 focus-visible:ring-2 focus-visible:ring-ring/30"
246
246
  >
247
247
  <FieldIcon k={value} className="size-3.5 text-muted-foreground" />
248
248
  <span>{labels[value]}</span>
@@ -10,7 +10,6 @@ import {
10
10
  Loader2,
11
11
  Maximize,
12
12
  MonitorSpeaker,
13
- Pencil,
14
13
  Play,
15
14
  } from 'lucide-react';
16
15
  import { type RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
@@ -40,20 +39,22 @@ import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
40
39
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
41
40
  import { useFolders } from '@/lib/folders';
42
41
  import { useAgentSocketConnected } from '@/lib/use-agent-socket';
42
+ import { useClickPageNavigation } from '@/lib/use-click-page-navigation';
43
+ import { useIsMobile } from '@/lib/use-is-mobile';
43
44
  import { format, useLocale } from '@/lib/use-locale';
44
45
  import { useWheelPageNavigation } from '@/lib/use-wheel-page-navigation';
45
46
  import { cn } from '@/lib/utils';
46
- import { ClickNavZones } from '../components/click-nav-zones';
47
47
  import { NotesDrawer } from '../components/notes-drawer';
48
48
  import { PdfProgressToast } from '../components/pdf-progress-toast';
49
49
  import { openPresenterWindow, Player } from '../components/player';
50
50
  import { SlideCanvas } from '../components/slide-canvas';
51
+ import { SlideTransitionLayer } from '../components/slide-transition-layer';
51
52
  import { type ThumbnailActions, ThumbnailRail } from '../components/thumbnail-rail';
52
53
  import { exportSlideAsHtml } from '../lib/export-html';
53
54
  import { exportSlideAsPdf, isSafari } from '../lib/export-pdf';
54
55
  import { remapNotesSessionCacheAfterReorder } from '../lib/inspector/use-notes';
55
- import { SlidePageProvider } from '../lib/page-context';
56
56
  import type { SlideModule } from '../lib/sdk';
57
+ import { usePrefersReducedMotion } from '../lib/use-prefers-reduced-motion';
57
58
  import { useSlideModule } from '../lib/use-slide-module';
58
59
 
59
60
  const { showSlideUi, showSlideBrowser, allowHtmlDownload } = config.build;
@@ -76,6 +77,7 @@ export function Slide() {
76
77
  const { renameSlide } = useFolders();
77
78
  const slideViewportRef = useRef<HTMLElement>(null);
78
79
  const t = useLocale();
80
+ const prefersReducedMotion = usePrefersReducedMotion();
79
81
 
80
82
  const modulePages = useMemo(() => slide?.default ?? [], [slide]);
81
83
  const [pages, setPages] = useState<typeof modulePages>(modulePages);
@@ -237,6 +239,8 @@ export function Slide() {
237
239
  goTo(index - 1);
238
240
  } else if (e.key === 'f' || e.key === 'F') {
239
241
  setPlayMode('fullscreen');
242
+ } else if (import.meta.env.DEV && (e.key === 'd' || e.key === 'D')) {
243
+ setDesignOpen((v) => !v);
240
244
  }
241
245
  };
242
246
  window.addEventListener('keydown', onKey);
@@ -325,6 +329,7 @@ export function Slide() {
325
329
  <Player
326
330
  pages={pages}
327
331
  design={slide.design}
332
+ transition={slide.transition}
328
333
  index={index}
329
334
  onIndexChange={goTo}
330
335
  onExit={() => setPlayMode(null)}
@@ -335,17 +340,16 @@ export function Slide() {
335
340
  );
336
341
  }
337
342
 
338
- const CurrentPage = pages[index];
339
343
  const title = slide.meta?.title ?? slideId;
340
344
 
341
345
  return (
342
346
  <HistoryProvider>
343
- <InspectorProvider slideId={slideId}>
347
+ <InspectorProvider slideId={slideId} pageIndex={index}>
344
348
  <SelectionReporter />
345
349
  <div className="flex h-dvh flex-col overflow-hidden bg-background text-foreground">
346
350
  {/* Editorial toolbar — three zones, hairline separators, mono-folio center */}
347
- <header className="relative flex h-12 shrink-0 items-center justify-between border-b border-hairline bg-sidebar/85 px-2 backdrop-blur-md md:px-3">
348
- <div className="flex items-center gap-1.5 md:gap-2">
351
+ <header className="relative flex h-12 shrink-0 items-center gap-2 border-b border-hairline bg-sidebar/85 px-2 backdrop-blur-md md:px-3">
352
+ <div className="flex shrink-0 items-center gap-1.5 md:gap-2">
349
353
  {showSlideBrowser && (
350
354
  <Button asChild variant="ghost" size="icon-sm" title={t.slide.home}>
351
355
  <Link to="/" aria-label={t.slide.backToHome}>
@@ -378,14 +382,14 @@ export function Slide() {
378
382
  {import.meta.env.DEV && <AgentConnectedBadge />}
379
383
  </div>
380
384
 
381
- {/* Centered title the rail and mobile pill carry the page count. */}
382
- <div className="pointer-events-none absolute inset-x-0 top-1/2 flex -translate-y-1/2 justify-center px-2">
383
- <div className="pointer-events-auto min-w-0 max-w-[min(34rem,calc(100vw-22rem))]">
385
+ {/* Title centered to the viewport, not the leftover space between the side groups. */}
386
+ <div className="pointer-events-none absolute inset-x-0 flex justify-center px-2">
387
+ <div className="pointer-events-auto min-w-0 max-w-[34rem]">
384
388
  <InlineTitleEditor title={title} onSubmit={(next) => renameSlide(slideId, next)} />
385
389
  </div>
386
390
  </div>
387
391
 
388
- <div className="flex items-center gap-1">
392
+ <div className="ml-auto flex shrink-0 items-center gap-1">
389
393
  {view === 'slides' && (
390
394
  <button
391
395
  type="button"
@@ -577,7 +581,7 @@ export function Slide() {
577
581
  data-slide-id={slideId}
578
582
  className="paper relative min-h-0 min-w-0 flex-1 bg-canvas p-2 md:p-10"
579
583
  >
580
- <SlideWheelNavigation
584
+ <SlideViewportNavigation
581
585
  targetRef={slideViewportRef}
582
586
  onPrev={() => goTo(index - 1)}
583
587
  onNext={() => goTo(index + 1)}
@@ -585,16 +589,14 @@ export function Slide() {
585
589
  canNext={index < pageCount - 1}
586
590
  />
587
591
  <SlideCanvas design={slide.design}>
588
- <SlidePageProvider index={index} total={pageCount}>
589
- <CurrentPage />
590
- </SlidePageProvider>
592
+ <SlideTransitionLayer
593
+ pages={pages}
594
+ index={index}
595
+ total={pageCount}
596
+ moduleTransition={slide.transition}
597
+ disabled={prefersReducedMotion}
598
+ />
591
599
  </SlideCanvas>
592
- <ClickNavZones
593
- onPrev={() => goTo(index - 1)}
594
- onNext={() => goTo(index + 1)}
595
- canPrev={index > 0}
596
- canNext={index < pageCount - 1}
597
- />
598
600
  <InspectOverlay />
599
601
  <SaveBar />
600
602
  {import.meta.env.DEV && <CommentWidget />}
@@ -806,7 +808,7 @@ function SelectionReporter() {
806
808
  return null;
807
809
  }
808
810
 
809
- function SlideWheelNavigation({
811
+ function SlideViewportNavigation({
810
812
  targetRef,
811
813
  onPrev,
812
814
  onNext,
@@ -820,6 +822,7 @@ function SlideWheelNavigation({
820
822
  canNext: boolean;
821
823
  }) {
822
824
  const { active } = useInspector();
825
+ const isMobile = useIsMobile();
823
826
 
824
827
  useWheelPageNavigation({
825
828
  ref: targetRef,
@@ -830,6 +833,19 @@ function SlideWheelNavigation({
830
833
  onNext,
831
834
  });
832
835
 
836
+ // Tap-to-navigate is a touch affordance — desktop has visible prev/next
837
+ // chrome, so it stays edge-only on small screens (matches the old md:hidden
838
+ // zones). Interactive slide content keeps its tap via the hook's passthrough.
839
+ useClickPageNavigation({
840
+ ref: targetRef,
841
+ enabled: isMobile && !active,
842
+ edgeRatio: 0.18,
843
+ canPrev,
844
+ canNext,
845
+ onPrev,
846
+ onNext,
847
+ });
848
+
833
849
  return null;
834
850
  }
835
851
 
@@ -882,49 +898,65 @@ function InlineTitleEditor({
882
898
 
883
899
  if (editing) {
884
900
  return (
885
- <div className="flex flex-1 items-center justify-center">
886
- <input
887
- ref={inputRef}
888
- value={value}
889
- disabled={saving}
890
- onChange={(e) => setValue(e.target.value)}
891
- onBlur={() => {
892
- if (!saving) commit();
893
- }}
894
- onKeyDown={(e) => {
895
- if (e.key === 'Enter') {
896
- e.preventDefault();
897
- commit();
898
- } else if (e.key === 'Escape') {
899
- e.preventDefault();
900
- cancel();
901
- }
902
- }}
903
- maxLength={80}
904
- className="min-w-0 max-w-[min(34rem,90%)] rounded-[5px] border border-foreground/30 bg-card px-2 py-0.5 text-center font-heading text-[13px] font-medium tracking-tight outline-none focus-visible:ring-2 focus-visible:ring-ring/30"
905
- />
901
+ <div className="flex min-w-0 flex-1 items-center justify-center">
902
+ <div className="inline-grid max-w-full items-center">
903
+ <span
904
+ aria-hidden
905
+ className="invisible col-start-1 row-start-1 overflow-hidden whitespace-pre border border-transparent px-2 py-0.5 font-heading text-[13.5px] font-semibold tracking-[-0.01em]"
906
+ >
907
+ {value || ' '}
908
+ </span>
909
+ <input
910
+ ref={inputRef}
911
+ size={1}
912
+ value={value}
913
+ disabled={saving}
914
+ onChange={(e) => setValue(e.target.value)}
915
+ onBlur={() => {
916
+ if (!saving) commit();
917
+ }}
918
+ onKeyDown={(e) => {
919
+ if (e.key === 'Enter') {
920
+ e.preventDefault();
921
+ commit();
922
+ } else if (e.key === 'Escape') {
923
+ e.preventDefault();
924
+ cancel();
925
+ }
926
+ }}
927
+ maxLength={80}
928
+ className="col-start-1 row-start-1 w-full min-w-0 rounded-[5px] border border-foreground/30 bg-card px-2 py-0.5 text-center font-heading text-[13.5px] font-semibold tracking-[-0.01em] outline-none"
929
+ />
930
+ </div>
931
+ </div>
932
+ );
933
+ }
934
+
935
+ if (!import.meta.env.DEV) {
936
+ return (
937
+ <div className="flex min-w-0 items-baseline justify-center">
938
+ <h1 className="truncate font-heading text-[13.5px] font-semibold tracking-[-0.01em]">
939
+ {title}
940
+ </h1>
906
941
  </div>
907
942
  );
908
943
  }
909
944
 
910
945
  return (
911
- <div className="group/title flex min-w-0 items-baseline justify-center gap-1.5">
912
- <h1 className="truncate font-heading text-[13.5px] font-semibold tracking-[-0.01em]">
913
- {title}
914
- </h1>
915
- {import.meta.env.DEV && (
916
- <button
917
- type="button"
918
- onClick={() => setEditing(true)}
919
- aria-label={t.slide.renameSlide}
920
- className={cn(
921
- 'flex size-5 shrink-0 items-center justify-center rounded-[4px] text-muted-foreground transition-opacity hover:bg-muted hover:text-foreground',
922
- 'opacity-0 group-hover/title:opacity-100 focus-visible:opacity-100',
923
- )}
924
- >
925
- <Pencil className="size-3" />
926
- </button>
927
- )}
946
+ <div className="flex min-w-0 items-center justify-center">
947
+ <button
948
+ type="button"
949
+ onClick={() => setEditing(true)}
950
+ aria-label={t.slide.renameSlide}
951
+ className={cn(
952
+ 'min-w-0 max-w-full cursor-text rounded-[5px] border border-transparent px-2 py-0.5 transition-colors',
953
+ 'hover:border-foreground/30 hover:bg-card focus-visible:border-foreground/30 focus-visible:bg-card focus-visible:outline-none',
954
+ )}
955
+ >
956
+ <h1 className="truncate font-heading text-[13.5px] font-semibold tracking-[-0.01em]">
957
+ {title}
958
+ </h1>
959
+ </button>
928
960
  </div>
929
961
  );
930
962
  }
package/src/locale/en.ts CHANGED
@@ -86,6 +86,7 @@ export const en: Locale = {
86
86
  toastSlideMoveFailed: 'Failed to move slide',
87
87
  toastFolderDeleted: 'Deleted folder “{name}”',
88
88
  toastFolderDeleteFailed: 'Failed to delete folder',
89
+ toastFolderReorderFailed: 'Failed to reorder folders',
89
90
  pickIcon: 'Pick icon',
90
91
  },
91
92
 
@@ -352,11 +353,6 @@ export const en: Locale = {
352
353
  system: 'System',
353
354
  },
354
355
 
355
- clickNav: {
356
- prevAria: 'Previous page',
357
- nextAria: 'Next page',
358
- },
359
-
360
356
  imagePlaceholder: {
361
357
  dropOverlay: 'Drop image to use here',
362
358
  uploading: 'Uploading…',
package/src/locale/ja.ts CHANGED
@@ -86,6 +86,7 @@ export const ja: Locale = {
86
86
  toastSlideMoveFailed: 'スライドの移動に失敗しました',
87
87
  toastFolderDeleted: 'フォルダ「{name}」を削除しました',
88
88
  toastFolderDeleteFailed: 'フォルダの削除に失敗しました',
89
+ toastFolderReorderFailed: 'フォルダの並び替えに失敗しました',
89
90
  pickIcon: 'アイコンを選択',
90
91
  },
91
92
 
@@ -356,11 +357,6 @@ export const ja: Locale = {
356
357
  system: 'システム',
357
358
  },
358
359
 
359
- clickNav: {
360
- prevAria: '前のページ',
361
- nextAria: '次のページ',
362
- },
363
-
364
360
  imagePlaceholder: {
365
361
  dropOverlay: 'ここにドロップして使用',
366
362
  uploading: 'アップロード中…',
@@ -90,6 +90,7 @@ export type Locale = {
90
90
  /** template: "Deleted folder “{name}”" */
91
91
  toastFolderDeleted: string;
92
92
  toastFolderDeleteFailed: string;
93
+ toastFolderReorderFailed: string;
93
94
  pickIcon: string;
94
95
  };
95
96
 
@@ -375,11 +376,6 @@ export type Locale = {
375
376
  system: string;
376
377
  };
377
378
 
378
- clickNav: {
379
- prevAria: string;
380
- nextAria: string;
381
- };
382
-
383
379
  imagePlaceholder: {
384
380
  dropOverlay: string;
385
381
  uploading: string;
@@ -86,6 +86,7 @@ export const zhCN: Locale = {
86
86
  toastSlideMoveFailed: '移动幻灯片失败',
87
87
  toastFolderDeleted: '已删除文件夹"{name}"',
88
88
  toastFolderDeleteFailed: '删除文件夹失败',
89
+ toastFolderReorderFailed: '文件夹排序失败',
89
90
  pickIcon: '选择图标',
90
91
  },
91
92
 
@@ -351,11 +352,6 @@ export const zhCN: Locale = {
351
352
  system: '系统',
352
353
  },
353
354
 
354
- clickNav: {
355
- prevAria: '上一页',
356
- nextAria: '下一页',
357
- },
358
-
359
355
  imagePlaceholder: {
360
356
  dropOverlay: '拖入图片以使用',
361
357
  uploading: '上传中…',
@@ -86,6 +86,7 @@ export const zhTW: Locale = {
86
86
  toastSlideMoveFailed: '移動投影片失敗',
87
87
  toastFolderDeleted: '已刪除資料夾「{name}」',
88
88
  toastFolderDeleteFailed: '刪除資料夾失敗',
89
+ toastFolderReorderFailed: '資料夾排序失敗',
89
90
  pickIcon: '選擇圖示',
90
91
  },
91
92
 
@@ -351,11 +352,6 @@ export const zhTW: Locale = {
351
352
  system: '系統',
352
353
  },
353
354
 
354
- clickNav: {
355
- prevAria: '上一頁',
356
- nextAria: '下一頁',
357
- },
358
-
359
355
  imagePlaceholder: {
360
356
  dropOverlay: '拖入圖片以使用',
361
357
  uploading: '上傳中…',
@@ -1,36 +0,0 @@
1
- import { useLocale } from '@/lib/use-locale';
2
- import { useInspector } from './inspector/inspector-provider';
3
-
4
- type Props = {
5
- onPrev: () => void;
6
- onNext: () => void;
7
- canPrev: boolean;
8
- canNext: boolean;
9
- };
10
-
11
- export function ClickNavZones({ onPrev, onNext, canPrev, canNext }: Props) {
12
- const { active } = useInspector();
13
- const t = useLocale();
14
- if (active) return null;
15
-
16
- return (
17
- <>
18
- <button
19
- type="button"
20
- aria-label={t.clickNav.prevAria}
21
- onClick={onPrev}
22
- disabled={!canPrev}
23
- data-inspector-ui
24
- className="absolute inset-y-0 left-0 z-20 w-[18%] min-w-12 md:hidden"
25
- />
26
- <button
27
- type="button"
28
- aria-label={t.clickNav.nextAria}
29
- onClick={onNext}
30
- disabled={!canNext}
31
- data-inspector-ui
32
- className="absolute inset-y-0 right-0 z-20 w-[18%] min-w-12 md:hidden"
33
- />
34
- </>
35
- );
36
- }