@textcortex/slidewise 1.5.0 → 1.7.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@textcortex/slidewise",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "Embeddable React PPTX editor.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -62,6 +62,26 @@
62
62
  --slidewise-bg-pill: var(--smart-grad);
63
63
  --slidewise-bg-unsaved-badge: rgba(232, 80, 76, 0.12);
64
64
 
65
+ /* Motion tokens. Hosts retune the editor's whole animation feel by
66
+ overriding these. Defaults match Material 3 "standard" easings. */
67
+ --slidewise-duration-instant: 0ms;
68
+ --slidewise-duration-fast: 120ms;
69
+ --slidewise-duration-base: 200ms;
70
+ --slidewise-duration-slow: 320ms;
71
+ --slidewise-easing-standard: cubic-bezier(0.2, 0, 0, 1);
72
+ --slidewise-easing-emphasized: cubic-bezier(0.05, 0.7, 0.1, 1);
73
+ --slidewise-easing-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
74
+
75
+ /* Per-region animation flags. Set to 0 on a wrapping scope to disable a
76
+ specific region's animations without killing motion globally. They are
77
+ read internally by region-scoped transitions; CSS rules use them via
78
+ `transition-duration: calc(var(--slidewise-duration-base) * var(--slidewise-anim-topbar))`. */
79
+ --slidewise-anim-topbar: 1;
80
+ --slidewise-anim-rail: 1;
81
+ --slidewise-anim-canvas: 1;
82
+ --slidewise-anim-floating-toolbar: 1;
83
+ --slidewise-anim-play-mode: 1;
84
+
65
85
  /* Layout backgrounds */
66
86
  --app-bg: #ffffff;
67
87
  --rail-bg: #fafafb;
@@ -295,3 +315,40 @@
295
315
  transition: none;
296
316
  }
297
317
  }
318
+
319
+ /*
320
+ * Reduced motion plumbing.
321
+ *
322
+ * <Slidewise.Root reduceMotion="system"> → no class; respects the
323
+ * OS preference via the media
324
+ * query below.
325
+ * <Slidewise.Root reduceMotion={true}> → adds `.reduce-motion` class
326
+ * for hard-off.
327
+ * <Slidewise.Root reduceMotion={false}> → adds `.reduce-motion-off`
328
+ * class to override the OS
329
+ * preference (testing /
330
+ * previewing animations).
331
+ */
332
+ .slidewise-editor.reduce-motion,
333
+ .slidewise-editor.reduce-motion *,
334
+ .slidewise-editor.reduce-motion *::before,
335
+ .slidewise-editor.reduce-motion *::after {
336
+ animation-duration: 0ms !important;
337
+ animation-delay: 0ms !important;
338
+ transition-duration: 0ms !important;
339
+ transition-delay: 0ms !important;
340
+ scroll-behavior: auto !important;
341
+ }
342
+
343
+ @media (prefers-reduced-motion: reduce) {
344
+ .slidewise-editor:not(.reduce-motion-off),
345
+ .slidewise-editor:not(.reduce-motion-off) *,
346
+ .slidewise-editor:not(.reduce-motion-off) *::before,
347
+ .slidewise-editor:not(.reduce-motion-off) *::after {
348
+ animation-duration: 0ms !important;
349
+ animation-delay: 0ms !important;
350
+ transition-duration: 0ms !important;
351
+ transition-delay: 0ms !important;
352
+ scroll-behavior: auto !important;
353
+ }
354
+ }
@@ -7,8 +7,8 @@ import {
7
7
  type SelectionSnapshot,
8
8
  } from "./compound/SlidewiseRoot";
9
9
  import { TopBar } from "./compound/topbar";
10
+ import { SlideRail } from "./compound/sliderail";
10
11
  import {
11
- SlideRail,
12
12
  Canvas,
13
13
  BottomToolbar,
14
14
  Body,
@@ -17,6 +17,7 @@ import {
17
17
  import type { SlidewiseIcons } from "./compound/IconContext";
18
18
  import type { SlidewiseLabels } from "./compound/LabelsContext";
19
19
  import type { SlidewiseSurfaces } from "./compound/SurfacesContext";
20
+ import type { Transition } from "framer-motion";
20
21
  import type { Deck } from "@/lib/types";
21
22
  import "./SlidewiseEditor.css";
22
23
 
@@ -67,6 +68,16 @@ export interface SlidewiseEditorProps {
67
68
  showBottomToolbar?: boolean;
68
69
  /** Override the bundled Geist font; sets `--font-geist-sans` on the root. */
69
70
  fontFamily?: string;
71
+ /**
72
+ * Reduced-motion behavior. `"system"` (default) respects the OS
73
+ * preference; `true` forces motion off; `false` forces it on.
74
+ */
75
+ reduceMotion?: boolean | "system";
76
+ /**
77
+ * Default framer-motion transition applied via `<MotionConfig>`. Use
78
+ * this to retune the editor's animation feel globally.
79
+ */
80
+ transition?: Transition;
70
81
  /**
71
82
  * Per-action icon overrides. Pass a ReactNode for any of `undo`, `redo`,
72
83
  * `save`, `play`, `themeLight`, `themeDark`, `export`, `smart` to skin the
@@ -135,6 +146,8 @@ export const SlidewiseEditor = forwardRef<
135
146
  showTopBar = true,
136
147
  showBottomToolbar = true,
137
148
  fontFamily,
149
+ reduceMotion,
150
+ transition,
138
151
  icons,
139
152
  labels,
140
153
  surfaces,
@@ -160,6 +173,8 @@ export const SlidewiseEditor = forwardRef<
160
173
  theme,
161
174
  initialSlideId,
162
175
  fontFamily,
176
+ reduceMotion,
177
+ transition,
163
178
  icons,
164
179
  labels,
165
180
  surfaces,
@@ -13,6 +13,7 @@ import type { SlidewiseIcons } from "./compound/IconContext";
13
13
  import type { SlidewiseLabels } from "./compound/LabelsContext";
14
14
  import type { SlidewiseSurfaces } from "./compound/SurfacesContext";
15
15
  import { DEFAULT_LABELS } from "./compound/LabelsContext";
16
+ import type { Transition } from "framer-motion";
16
17
  import type { HistoryState, SelectionSnapshot } from "./compound/SlidewiseRoot";
17
18
 
18
19
  export interface SlidewiseFileEditorProps {
@@ -105,6 +106,13 @@ export interface SlidewiseFileEditorProps {
105
106
  * `--slidewise-bg-*` CSS variables.
106
107
  */
107
108
  surfaces?: SlidewiseSurfaces;
109
+ /**
110
+ * Reduced-motion behavior. `"system"` (default) respects the OS
111
+ * preference; `true` forces motion off; `false` forces it on.
112
+ */
113
+ reduceMotion?: boolean | "system";
114
+ /** Default framer-motion transition applied via `<MotionConfig>`. */
115
+ transition?: Transition;
108
116
  className?: string;
109
117
  style?: CSSProperties;
110
118
  /**
@@ -209,6 +217,8 @@ export const SlidewiseFileEditor = forwardRef<
209
217
  icons,
210
218
  labels,
211
219
  surfaces,
220
+ reduceMotion,
221
+ transition,
212
222
  className,
213
223
  style,
214
224
  parse = parsePptx,
@@ -384,6 +394,8 @@ export const SlidewiseFileEditor = forwardRef<
384
394
  icons={icons}
385
395
  labels={labels}
386
396
  surfaces={surfaces}
397
+ reduceMotion={reduceMotion}
398
+ transition={transition}
387
399
  onChange={(next) => {
388
400
  onChangeRef.current?.(next);
389
401
  }}
@@ -18,6 +18,7 @@ import { collectFontFamilies, ensureGoogleFontsLoaded } from "@/lib/fonts";
18
18
  import type { Deck } from "@/lib/types";
19
19
  import { GridView } from "@/components/editor/GridView";
20
20
  import { PlayMode } from "@/components/editor/PlayMode";
21
+ import { MotionConfig, type Transition } from "framer-motion";
21
22
  import { HostProvider } from "./HostContext";
22
23
  import { IconProvider, type SlidewiseIcons } from "./IconContext";
23
24
  import { ReadOnlyProvider } from "./ReadOnlyContext";
@@ -72,6 +73,29 @@ export interface SlidewiseRootProps {
72
73
  initialSlideId?: string;
73
74
  /** Override the default Geist font; sets `--slidewise-font-sans`. */
74
75
  fontFamily?: string;
76
+ /**
77
+ * Controls reduced-motion behavior.
78
+ *
79
+ * - `"system"` (default) — respect the user's OS preference via the
80
+ * `prefers-reduced-motion` media query.
81
+ * - `true` — force all CSS animations + transitions off and tell
82
+ * framer-motion to skip motion. Use for hosts whose own product
83
+ * already has a global motion-off toggle.
84
+ * - `false` — force motion on even when the OS reports reduced-motion.
85
+ * Useful for previewing animations during development; not generally
86
+ * recommended in production since it overrides an accessibility hint.
87
+ */
88
+ reduceMotion?: boolean | "system";
89
+ /**
90
+ * Default framer-motion transition. Passed through to a wrapping
91
+ * `<MotionConfig>` so every motion component inside the editor inherits
92
+ * it. Useful for retuning the editor's overall feel (faster, springier,
93
+ * etc.) without touching individual components.
94
+ *
95
+ * For CSS transitions, override the duration/easing tokens instead —
96
+ * `--slidewise-duration-base`, `--slidewise-easing-standard`, etc.
97
+ */
98
+ transition?: Transition;
75
99
  /**
76
100
  * Per-action icon overrides for the chrome. Hosts pass any subset to
77
101
  * skin Slidewise with their own icon set; missing slots fall back to
@@ -212,6 +236,8 @@ function RootInner({
212
236
  theme,
213
237
  initialSlideId,
214
238
  fontFamily,
239
+ reduceMotion = "system",
240
+ transition,
215
241
  icons,
216
242
  labels,
217
243
  surfaces,
@@ -461,26 +487,50 @@ function RootInner({
461
487
  ? { ...style, ...(surfaceVars as CSSProperties) }
462
488
  : style;
463
489
 
490
+ // Map our reduceMotion enum to:
491
+ // - a CSS class on the root (drives the @media + class rules in
492
+ // SlidewiseEditor.css)
493
+ // - framer-motion's reducedMotion prop on MotionConfig
494
+ const motionClass =
495
+ reduceMotion === true
496
+ ? "reduce-motion"
497
+ : reduceMotion === false
498
+ ? "reduce-motion-off"
499
+ : "";
500
+ const fmReducedMotion =
501
+ reduceMotion === true
502
+ ? "always"
503
+ : reduceMotion === false
504
+ ? "never"
505
+ : "user";
506
+ const combinedClassName = motionClass
507
+ ? className
508
+ ? `${className} ${motionClass}`
509
+ : motionClass
510
+ : className;
511
+
464
512
  return (
465
- <ReadOnlyProvider readOnly={readOnly}>
466
- <IconProvider icons={icons ?? {}}>
467
- <LabelsProvider labels={labels}>
468
- <SurfacesProvider surfaces={surfaces}>
469
- <DirtyProvider dirty={dirty}>
470
- <HostProvider callbacks={{ onSave: wrappedSave, onExport }}>
471
- <RootShell
472
- fontFamily={fontFamily}
473
- className={className}
474
- style={mergedStyle}
475
- >
476
- {children}
477
- </RootShell>
478
- </HostProvider>
479
- </DirtyProvider>
480
- </SurfacesProvider>
481
- </LabelsProvider>
482
- </IconProvider>
483
- </ReadOnlyProvider>
513
+ <MotionConfig reducedMotion={fmReducedMotion} transition={transition}>
514
+ <ReadOnlyProvider readOnly={readOnly}>
515
+ <IconProvider icons={icons ?? {}}>
516
+ <LabelsProvider labels={labels}>
517
+ <SurfacesProvider surfaces={surfaces}>
518
+ <DirtyProvider dirty={dirty}>
519
+ <HostProvider callbacks={{ onSave: wrappedSave, onExport }}>
520
+ <RootShell
521
+ fontFamily={fontFamily}
522
+ className={combinedClassName}
523
+ style={mergedStyle}
524
+ >
525
+ {children}
526
+ </RootShell>
527
+ </HostProvider>
528
+ </DirtyProvider>
529
+ </SurfacesProvider>
530
+ </LabelsProvider>
531
+ </IconProvider>
532
+ </ReadOnlyProvider>
533
+ </MotionConfig>
484
534
  );
485
535
  }
486
536
 
@@ -40,7 +40,6 @@ export {
40
40
  type SelectionSnapshot,
41
41
  } from "./SlidewiseRoot";
42
42
  export {
43
- SlideRail,
44
43
  Canvas,
45
44
  BottomToolbar,
46
45
  RightPanel,
@@ -50,6 +49,19 @@ export {
50
49
  } from "./parts";
51
50
 
52
51
  export { TopBar, type TopBarProps, type TopBarSlotId } from "./topbar";
52
+ export {
53
+ SlideRail,
54
+ useSlideRailItem,
55
+ type SlideRailProps,
56
+ type SlideRailRootProps,
57
+ type SlideRailHeaderProps,
58
+ type SlideRailListProps,
59
+ type SlideRailItemProps,
60
+ type SlideRailThumbnailProps,
61
+ type SlideRailNumberProps,
62
+ type SlideRailAddButtonProps,
63
+ type SlideRailItemContextValue,
64
+ } from "./sliderail";
53
65
  export type {
54
66
  TopBarRootProps,
55
67
  TopBarTitleProps,
@@ -1,5 +1,4 @@
1
1
  import type { CSSProperties, ReactNode } from "react";
2
- import { SlideRail as SlideRailInternal } from "@/components/editor/SlideRail";
3
2
  import { Canvas as CanvasInternal } from "@/components/editor/Canvas";
4
3
  import { BottomToolbar as BottomToolbarInternal } from "@/components/editor/BottomToolbar";
5
4
 
@@ -8,9 +7,10 @@ import { BottomToolbar as BottomToolbarInternal } from "@/components/editor/Bott
8
7
  * so any part can be omitted, wrapped, or replaced. None of these accept
9
8
  * deck/onChange/onSave props — those live on `<Slidewise.Root>`.
10
9
  *
11
- * Note: `<Slidewise.TopBar>` is defined separately in `./topbar/` because
12
- * it itself decomposes into subparts (`TopBar.Root`, `TopBar.Title`,
13
- * `TopBar.Undo`, etc.) and ships a `hide` prop for per-button removal.
10
+ * Note: `<Slidewise.TopBar>` and `<Slidewise.SlideRail>` are defined
11
+ * separately under `./topbar/` and `./sliderail/` because they decompose
12
+ * into their own subparts. Both ship a callable component for the default
13
+ * arrangement plus a namespace of named subparts.
14
14
  */
15
15
 
16
16
  export interface RegionProps {
@@ -18,13 +18,6 @@ export interface RegionProps {
18
18
  style?: CSSProperties;
19
19
  }
20
20
 
21
- /**
22
- * Left-side slide thumbnail rail with add/duplicate/delete.
23
- */
24
- export function SlideRail(_props: RegionProps = {}) {
25
- return <SlideRailInternal />;
26
- }
27
-
28
21
  /**
29
22
  * The main editing canvas. This is the only part that's effectively required
30
23
  * — without it the editor renders nothing visible. Layout-wise it expects
@@ -0,0 +1,76 @@
1
+ import type { CSSProperties, ReactNode } from "react";
2
+ import { Plus } from "lucide-react";
3
+ import { useEditor } from "@/lib/StoreProvider";
4
+ import { useReadOnly } from "../ReadOnlyContext";
5
+ import { useLabels } from "../LabelsContext";
6
+
7
+ /**
8
+ * Dashed "New Slide" button at the bottom of the rail. Calls
9
+ * `store.addSlide()`. Hidden in read-only mode.
10
+ *
11
+ * Replace with your own button when you need a different shape; the
12
+ * store action is exported (`useEditor((s) => s.addSlide)`).
13
+ */
14
+ export interface SlideRailAddButtonProps {
15
+ className?: string;
16
+ style?: CSSProperties;
17
+ ariaLabel?: string;
18
+ /** Override the visible label. Defaults to `labels.addSlide`. */
19
+ label?: string;
20
+ children?: ReactNode;
21
+ }
22
+
23
+ export function AddButton({
24
+ className,
25
+ style,
26
+ ariaLabel,
27
+ label,
28
+ children,
29
+ }: SlideRailAddButtonProps = {}) {
30
+ const addSlide = useEditor((s) => s.addSlide);
31
+ const readOnly = useReadOnly();
32
+ const labels = useLabels();
33
+ if (readOnly) return null;
34
+
35
+ const resolved = label ?? labels.addSlide;
36
+
37
+ return (
38
+ <button
39
+ type="button"
40
+ className={className}
41
+ aria-label={ariaLabel ?? resolved}
42
+ onClick={() => addSlide()}
43
+ style={{
44
+ height: 44,
45
+ margin: 12,
46
+ display: "flex",
47
+ alignItems: "center",
48
+ justifyContent: "center",
49
+ gap: 6,
50
+ background: "var(--slidewise-bg-app, var(--app-bg))",
51
+ border: "1px dashed var(--border-dashed)",
52
+ borderRadius: "var(--slidewise-radius, 10px)",
53
+ color: "var(--ink)",
54
+ fontSize: 13,
55
+ fontWeight: 500,
56
+ cursor: "pointer",
57
+ transition: "background 120ms, border-color 120ms, color 120ms",
58
+ fontFamily: "inherit",
59
+ ...style,
60
+ }}
61
+ onMouseEnter={(e) => {
62
+ e.currentTarget.style.borderColor =
63
+ "var(--slidewise-accent, var(--accent))";
64
+ e.currentTarget.style.color =
65
+ "var(--slidewise-accent, var(--accent))";
66
+ }}
67
+ onMouseLeave={(e) => {
68
+ e.currentTarget.style.borderColor = "var(--border-dashed)";
69
+ e.currentTarget.style.color = "var(--ink)";
70
+ }}
71
+ >
72
+ {children ?? <Plus size={14} />}
73
+ {resolved}
74
+ </button>
75
+ );
76
+ }
@@ -0,0 +1,84 @@
1
+ import type { CSSProperties, PropsWithChildren } from "react";
2
+ import { LayoutGrid } from "lucide-react";
3
+ import { useEditor } from "@/lib/StoreProvider";
4
+
5
+ /**
6
+ * Default rail header. Renders the grid-view button and a "current / total"
7
+ * slide counter. Hosts can pass `children` to replace the whole content
8
+ * while keeping the height + border, or render their own `<header>` instead.
9
+ *
10
+ * ```tsx
11
+ * // Default counter
12
+ * <Slidewise.SlideRail.Header />
13
+ *
14
+ * // Custom content
15
+ * <Slidewise.SlideRail.Header>
16
+ * <strong>{deckTitle}</strong>
17
+ * </Slidewise.SlideRail.Header>
18
+ * ```
19
+ */
20
+ export interface SlideRailHeaderProps {
21
+ className?: string;
22
+ style?: CSSProperties;
23
+ }
24
+
25
+ export function Header({
26
+ className,
27
+ style,
28
+ children,
29
+ }: PropsWithChildren<SlideRailHeaderProps>) {
30
+ const slides = useEditor((s) => s.deck.slides);
31
+ const currentId = useEditor((s) => s.currentSlideId);
32
+ const setView = useEditor((s) => s.setView);
33
+ const idx = slides.findIndex((s) => s.id === currentId);
34
+
35
+ return (
36
+ <div
37
+ className={className}
38
+ style={{
39
+ height: 36,
40
+ display: "flex",
41
+ alignItems: "center",
42
+ justifyContent: "space-between",
43
+ padding: "0 12px",
44
+ fontSize: 12,
45
+ color: "var(--ink-muted)",
46
+ borderBottom: "1px solid var(--border)",
47
+ ...style,
48
+ }}
49
+ >
50
+ {children ?? (
51
+ <>
52
+ <button
53
+ title="Slide overview"
54
+ aria-label="Open slide overview"
55
+ onClick={() => setView("grid")}
56
+ style={{
57
+ width: 28,
58
+ height: 28,
59
+ border: "none",
60
+ borderRadius: 6,
61
+ background: "transparent",
62
+ display: "flex",
63
+ alignItems: "center",
64
+ justifyContent: "center",
65
+ color: "var(--ink)",
66
+ cursor: "pointer",
67
+ }}
68
+ onMouseEnter={(e) =>
69
+ (e.currentTarget.style.background = "var(--hover-strong)")
70
+ }
71
+ onMouseLeave={(e) =>
72
+ (e.currentTarget.style.background = "transparent")
73
+ }
74
+ >
75
+ <LayoutGrid size={14} />
76
+ </button>
77
+ <span>
78
+ {idx + 1} / {slides.length}
79
+ </span>
80
+ </>
81
+ )}
82
+ </div>
83
+ );
84
+ }
@@ -0,0 +1,82 @@
1
+ import type { CSSProperties, PropsWithChildren } from "react";
2
+ import { useEditor } from "@/lib/StoreProvider";
3
+ import type { Slide } from "@/lib/types";
4
+ import { SlideRailItemProvider } from "./ItemContext";
5
+
6
+ /**
7
+ * Wraps a single slide in the rail. Provides the slide to descendants via
8
+ * context (read with `useSlideRailItem`) and wires click → `selectSlide`.
9
+ *
10
+ * The default subparts (`Thumbnail`, `Number`) read the slide off the
11
+ * context, so hosts can rearrange them freely. Host content (e.g. a
12
+ * three-dots menu, a duplicate button) drops in as additional children.
13
+ *
14
+ * ```tsx
15
+ * <Slidewise.SlideRail.Item slide={slide}>
16
+ * <Slidewise.SlideRail.Thumbnail />
17
+ * <Slidewise.SlideRail.Number />
18
+ * <MyContextMenu slide={slide} />
19
+ * </Slidewise.SlideRail.Item>
20
+ * ```
21
+ */
22
+ export interface SlideRailItemProps {
23
+ slide: Slide;
24
+ className?: string;
25
+ style?: CSSProperties;
26
+ /**
27
+ * Override the click handler. The default selects the slide via the
28
+ * editor store; pass a function here to add host behavior (e.g. open
29
+ * a context drawer instead of selecting).
30
+ */
31
+ onClick?: (slide: Slide) => void;
32
+ }
33
+
34
+ export function Item({
35
+ slide,
36
+ className,
37
+ style,
38
+ onClick,
39
+ children,
40
+ }: PropsWithChildren<SlideRailItemProps>) {
41
+ const slides = useEditor((s) => s.deck.slides);
42
+ const currentId = useEditor((s) => s.currentSlideId);
43
+ const selectSlide = useEditor((s) => s.selectSlide);
44
+ const index = slides.findIndex((s) => s.id === slide.id);
45
+ const isCurrent = slide.id === currentId;
46
+
47
+ return (
48
+ <SlideRailItemProvider value={{ slide, index, isCurrent }}>
49
+ <div
50
+ className={className}
51
+ onClick={() => (onClick ? onClick(slide) : selectSlide(slide.id))}
52
+ role="button"
53
+ tabIndex={0}
54
+ aria-current={isCurrent ? "true" : undefined}
55
+ aria-label={`Open slide ${index + 1}`}
56
+ onKeyDown={(e) => {
57
+ if (e.key === "Enter" || e.key === " ") {
58
+ e.preventDefault();
59
+ onClick ? onClick(slide) : selectSlide(slide.id);
60
+ }
61
+ }}
62
+ style={{
63
+ position: "relative",
64
+ padding: "0 12px",
65
+ marginBottom: 14,
66
+ cursor: "pointer",
67
+ background:
68
+ "var(--slidewise-bg-rail-item, transparent)",
69
+ ...(isCurrent
70
+ ? {
71
+ background:
72
+ "var(--slidewise-bg-rail-item-active, var(--accent-soft))",
73
+ }
74
+ : null),
75
+ ...style,
76
+ }}
77
+ >
78
+ {children}
79
+ </div>
80
+ </SlideRailItemProvider>
81
+ );
82
+ }
@@ -0,0 +1,42 @@
1
+ import { createContext, useContext } from "react";
2
+ import type { Slide } from "@/lib/types";
3
+
4
+ /**
5
+ * Context handed to descendants of `<SlideRail.Item>` so leaf subparts
6
+ * (`Thumbnail`, `Number`) and host content can read which slide they're
7
+ * rendering against without prop drilling.
8
+ */
9
+ export interface SlideRailItemContextValue {
10
+ slide: Slide;
11
+ /** Zero-based position in `deck.slides`. */
12
+ index: number;
13
+ /** True when this is the editor's currently-focused slide. */
14
+ isCurrent: boolean;
15
+ }
16
+
17
+ const ItemContext = createContext<SlideRailItemContextValue | null>(null);
18
+
19
+ export function SlideRailItemProvider({
20
+ value,
21
+ children,
22
+ }: {
23
+ value: SlideRailItemContextValue;
24
+ children: React.ReactNode;
25
+ }) {
26
+ return <ItemContext.Provider value={value}>{children}</ItemContext.Provider>;
27
+ }
28
+
29
+ /**
30
+ * Read the current item context. Throws when used outside a
31
+ * `<SlideRail.Item>` — the leaf subparts are meaningless without a
32
+ * concrete slide to render.
33
+ */
34
+ export function useSlideRailItem(): SlideRailItemContextValue {
35
+ const ctx = useContext(ItemContext);
36
+ if (!ctx) {
37
+ throw new Error(
38
+ "useSlideRailItem must be used inside <Slidewise.SlideRail.Item>"
39
+ );
40
+ }
41
+ return ctx;
42
+ }