@textcortex/slidewise 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@textcortex/slidewise",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "description": "Embeddable React PPTX editor.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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 { SlidewiseCanvasConfig } from "./compound/CanvasContext";
20
21
  import type { Transition } from "framer-motion";
21
22
  import type { Deck } from "@/lib/types";
22
23
  import "./SlidewiseEditor.css";
@@ -96,6 +97,13 @@ export interface SlidewiseEditorProps {
96
97
  * list of keys.
97
98
  */
98
99
  surfaces?: SlidewiseSurfaces;
100
+ /**
101
+ * Canvas/viewport configuration: padding, initial zoom, slide shadow +
102
+ * radius, and host-driven slide-background overrides. Use this when the
103
+ * default "slide fills the workspace" presentation isn't right for your
104
+ * host chrome.
105
+ */
106
+ canvas?: SlidewiseCanvasConfig;
99
107
  /** Extra class names appended to the editor root. */
100
108
  className?: string;
101
109
  /** Inline style applied to the editor root. */
@@ -151,6 +159,7 @@ export const SlidewiseEditor = forwardRef<
151
159
  icons,
152
160
  labels,
153
161
  surfaces,
162
+ canvas,
154
163
  className,
155
164
  style,
156
165
  },
@@ -178,6 +187,7 @@ export const SlidewiseEditor = forwardRef<
178
187
  icons,
179
188
  labels,
180
189
  surfaces,
190
+ canvas,
181
191
  className,
182
192
  style,
183
193
  };
@@ -12,6 +12,7 @@ import type { Deck } from "@/lib/types";
12
12
  import type { SlidewiseIcons } from "./compound/IconContext";
13
13
  import type { SlidewiseLabels } from "./compound/LabelsContext";
14
14
  import type { SlidewiseSurfaces } from "./compound/SurfacesContext";
15
+ import type { SlidewiseCanvasConfig } from "./compound/CanvasContext";
15
16
  import { DEFAULT_LABELS } from "./compound/LabelsContext";
16
17
  import type { Transition } from "framer-motion";
17
18
  import type { HistoryState, SelectionSnapshot } from "./compound/SlidewiseRoot";
@@ -106,6 +107,11 @@ export interface SlidewiseFileEditorProps {
106
107
  * `--slidewise-bg-*` CSS variables.
107
108
  */
108
109
  surfaces?: SlidewiseSurfaces;
110
+ /**
111
+ * Canvas/viewport configuration: padding, initial zoom, slide shadow +
112
+ * radius, and host-driven slide-background overrides.
113
+ */
114
+ canvas?: SlidewiseCanvasConfig;
109
115
  /**
110
116
  * Reduced-motion behavior. `"system"` (default) respects the OS
111
117
  * preference; `true` forces motion off; `false` forces it on.
@@ -217,6 +223,7 @@ export const SlidewiseFileEditor = forwardRef<
217
223
  icons,
218
224
  labels,
219
225
  surfaces,
226
+ canvas,
220
227
  reduceMotion,
221
228
  transition,
222
229
  className,
@@ -394,6 +401,7 @@ export const SlidewiseFileEditor = forwardRef<
394
401
  icons={icons}
395
402
  labels={labels}
396
403
  surfaces={surfaces}
404
+ canvas={canvas}
397
405
  reduceMotion={reduceMotion}
398
406
  transition={transition}
399
407
  onChange={(next) => {
@@ -5,6 +5,10 @@ import { SLIDE_W, SLIDE_H, type SlideElement, type ElementDraft } from "@/lib/ty
5
5
  import { ElementView } from "./ElementView";
6
6
  import { SelectionFrame } from "./SelectionFrame";
7
7
  import { FloatingToolbar } from "./FloatingToolbar";
8
+ import {
9
+ useCanvasConfig,
10
+ resolveSlideBackground,
11
+ } from "@/compound/CanvasContext";
8
12
 
9
13
  export function Canvas() {
10
14
  const store = useEditorStore();
@@ -22,6 +26,7 @@ export function Canvas() {
22
26
  const deleteElement = useEditor((s) => s.deleteElement);
23
27
  const pushHistory = useEditor((s) => s.pushHistory);
24
28
 
29
+ const canvasConfig = useCanvasConfig();
25
30
  const [editingId, setEditingId] = useState<string | null>(null);
26
31
  const wrapRef = useRef<HTMLDivElement>(null);
27
32
  const [autoScale, setAutoScale] = useState(0.6);
@@ -30,10 +35,12 @@ export function Canvas() {
30
35
  if (fitMode !== "fit" || !wrapRef.current) return;
31
36
  const recompute = () => {
32
37
  const r = wrapRef.current!.getBoundingClientRect();
33
- // Generous fill: small breathing room horizontally, plus enough vertical
34
- // headroom for the floating toolbar (~56) and the bottom toolbar (~76).
35
- const padX = 32;
36
- const padY = 56 + 76 + 16;
38
+ // Host-controllable padding via `canvas.padding` on <Slidewise.Root>.
39
+ // Default mirrors what the editor needed before the prop existed
40
+ // ~32px horizontal breathing room and enough vertical headroom for
41
+ // the floating toolbar (~56) and the bottom toolbar (~76).
42
+ const padX = canvasConfig.padding.x * 2;
43
+ const padY = canvasConfig.padding.y * 2;
37
44
  const fit = Math.min(
38
45
  (r.width - padX) / SLIDE_W,
39
46
  (r.height - padY) / SLIDE_H
@@ -44,7 +51,7 @@ export function Canvas() {
44
51
  const ro = new ResizeObserver(recompute);
45
52
  ro.observe(wrapRef.current);
46
53
  return () => ro.disconnect();
47
- }, [fitMode]);
54
+ }, [fitMode, canvasConfig.padding.x, canvasConfig.padding.y]);
48
55
 
49
56
  const scale = fitMode === "fit" ? autoScale : zoom;
50
57
 
@@ -245,9 +252,9 @@ export function Canvas() {
245
252
  width: SLIDE_W * scale,
246
253
  height: SLIDE_H * scale,
247
254
  transform: "translate(-50%, -50%)",
248
- background: slide.background,
249
- borderRadius: 8,
250
- boxShadow: "var(--slide-shadow)",
255
+ background: resolveSlideBackground(canvasConfig, slide),
256
+ borderRadius: canvasConfig.slideRadius,
257
+ boxShadow: canvasConfig.slideShadow,
251
258
  }}
252
259
  >
253
260
  <div
@@ -1,6 +1,10 @@
1
1
  import type { Slide } from "@/lib/types";
2
2
  import { SLIDE_W, SLIDE_H } from "@/lib/types";
3
3
  import { ElementView } from "./ElementView";
4
+ import {
5
+ useCanvasConfig,
6
+ resolveSlideBackground,
7
+ } from "@/compound/CanvasContext";
4
8
 
5
9
  export function SlideView({
6
10
  slide,
@@ -9,12 +13,13 @@ export function SlideView({
9
13
  slide: Slide;
10
14
  scale?: number;
11
15
  }) {
16
+ const canvasConfig = useCanvasConfig();
12
17
  return (
13
18
  <div
14
19
  style={{
15
20
  width: SLIDE_W * scale,
16
21
  height: SLIDE_H * scale,
17
- background: slide.background,
22
+ background: resolveSlideBackground(canvasConfig, slide),
18
23
  position: "relative",
19
24
  overflow: "hidden",
20
25
  borderRadius: 12 * scale,
@@ -0,0 +1,176 @@
1
+ import { createContext, useContext, useMemo, type ReactNode } from "react";
2
+ import type { Slide } from "@/lib/types";
3
+
4
+ /**
5
+ * Canvas/viewport configuration. Lets hosts tame the slide presentation so a
6
+ * slide with a bold background fill doesn't paint the entire workspace —
7
+ * which is what `<Slidewise.Root>` did by default before this prop existed.
8
+ *
9
+ * Pass any subset:
10
+ *
11
+ * ```tsx
12
+ * <Slidewise.Root
13
+ * canvas={{
14
+ * padding: { x: 48, y: 32 },
15
+ * defaultZoom: 0.7,
16
+ * slideRadius: 12,
17
+ * slideShadow:
18
+ * "0 1px 2px rgba(0,0,0,0.25), 0 24px 60px rgba(0,0,0,0.45)",
19
+ * resolveSlideBackground: (slide) =>
20
+ * hostThemeOverridesSlideBg ? "#ffffff" : undefined,
21
+ * }}
22
+ * >
23
+ * ```
24
+ *
25
+ * Outside the slide rectangle hosts still own the canvas-frame backdrop via
26
+ * the `--slidewise-bg-canvas-from` / `--slidewise-bg-canvas-to` CSS tokens
27
+ * or the `surfaces.canvasFrom` / `surfaces.canvasTo` prop entries.
28
+ */
29
+ export interface SlidewiseCanvasConfig {
30
+ /**
31
+ * Padding between the slide and the canvas frame, in pixels. Used both
32
+ * for the auto-fit calculation and as actual whitespace around the
33
+ * slide. Pass a number for uniform padding, or an object for per-axis
34
+ * control. Default: roughly the room the floating toolbars need —
35
+ * 32px horizontal, ~148px vertical (top bar + bottom toolbar).
36
+ */
37
+ padding?: number | { x?: number; y?: number };
38
+ /**
39
+ * How the slide scales inside the canvas:
40
+ * - `"fit"` (default) auto-shrinks the slide to fit while preserving aspect
41
+ * - `"fill"` reserved for a future fill behavior; currently equivalent to fit
42
+ * - `"manual"` uses `defaultZoom` (or whatever the user has set via
43
+ * `api.setZoom` / pinch-zoom) verbatim
44
+ *
45
+ * Applied via `store.setFitMode()` once on mount. Subsequent user
46
+ * interactions (zoom in, fit toggle) override this.
47
+ */
48
+ fitMode?: "fit" | "fill" | "manual";
49
+ /**
50
+ * Initial zoom level when `fitMode === "manual"` (or as a starting point
51
+ * when the user switches off auto-fit). 1 = 100%. Clamped to [0.1, 4].
52
+ */
53
+ defaultZoom?: number;
54
+ /**
55
+ * Box-shadow applied to the slide paper. Any valid CSS `box-shadow`
56
+ * value. Defaults to `var(--slide-shadow)`.
57
+ */
58
+ slideShadow?: string;
59
+ /**
60
+ * Border-radius applied to the slide paper. Number = pixels; string =
61
+ * any valid CSS length. Defaults to `8`.
62
+ */
63
+ slideRadius?: number | string;
64
+ /**
65
+ * Hard-override the slide's background paint regardless of what the
66
+ * deck's `slide.background` says. Useful when the host wants every
67
+ * slide to render as a neutral surface (`#ffffff`, host-tinted, etc.)
68
+ * for a viewer experience where the deck's baked fills would clash
69
+ * with the chrome.
70
+ *
71
+ * If both `forceSlideBackground` and `resolveSlideBackground` are
72
+ * passed, the force value wins.
73
+ */
74
+ forceSlideBackground?: string;
75
+ /**
76
+ * Per-slide background resolver. Receives the current slide; return a
77
+ * CSS value to override, or `undefined` to fall through to the slide's
78
+ * own `background` property. Useful for "respect host theme but only
79
+ * for slides that don't explicitly set a fill" patterns.
80
+ */
81
+ resolveSlideBackground?: (slide: Slide) => string | undefined;
82
+ }
83
+
84
+ /**
85
+ * Fully-resolved canvas config, with internal defaults filled in. Consumers
86
+ * (the Canvas component, host-rendered surfaces) read this shape via
87
+ * `useCanvasConfig()` and never have to check for undefined.
88
+ */
89
+ export interface ResolvedCanvasConfig {
90
+ padding: { x: number; y: number };
91
+ fitMode: "fit" | "fill" | "manual" | null;
92
+ defaultZoom: number | null;
93
+ slideShadow: string;
94
+ slideRadius: number | string;
95
+ forceSlideBackground: string | null;
96
+ resolveSlideBackground:
97
+ | ((slide: Slide) => string | undefined)
98
+ | null;
99
+ }
100
+
101
+ export const DEFAULT_CANVAS_CONFIG: ResolvedCanvasConfig = {
102
+ // Matches the previous hardcoded padX=32 / padY=56+76+16 in Canvas.tsx —
103
+ // enough vertical room for the top bar + the floating bottom toolbar.
104
+ padding: { x: 32, y: 148 },
105
+ fitMode: null,
106
+ defaultZoom: null,
107
+ slideShadow: "var(--slide-shadow)",
108
+ slideRadius: 8,
109
+ forceSlideBackground: null,
110
+ resolveSlideBackground: null,
111
+ };
112
+
113
+ function mergeCanvasConfig(
114
+ config: SlidewiseCanvasConfig | undefined
115
+ ): ResolvedCanvasConfig {
116
+ if (!config) return DEFAULT_CANVAS_CONFIG;
117
+ let padding = DEFAULT_CANVAS_CONFIG.padding;
118
+ if (typeof config.padding === "number") {
119
+ padding = { x: config.padding, y: config.padding };
120
+ } else if (config.padding && typeof config.padding === "object") {
121
+ padding = {
122
+ x: config.padding.x ?? DEFAULT_CANVAS_CONFIG.padding.x,
123
+ y: config.padding.y ?? DEFAULT_CANVAS_CONFIG.padding.y,
124
+ };
125
+ }
126
+ return {
127
+ padding,
128
+ fitMode: config.fitMode ?? null,
129
+ defaultZoom: config.defaultZoom ?? null,
130
+ slideShadow: config.slideShadow ?? DEFAULT_CANVAS_CONFIG.slideShadow,
131
+ slideRadius: config.slideRadius ?? DEFAULT_CANVAS_CONFIG.slideRadius,
132
+ forceSlideBackground: config.forceSlideBackground ?? null,
133
+ resolveSlideBackground: config.resolveSlideBackground ?? null,
134
+ };
135
+ }
136
+
137
+ const CanvasContext = createContext<ResolvedCanvasConfig>(DEFAULT_CANVAS_CONFIG);
138
+
139
+ export function CanvasConfigProvider({
140
+ config,
141
+ children,
142
+ }: {
143
+ config: SlidewiseCanvasConfig | undefined;
144
+ children: ReactNode;
145
+ }) {
146
+ const value = useMemo(() => mergeCanvasConfig(config), [config]);
147
+ return (
148
+ <CanvasContext.Provider value={value}>{children}</CanvasContext.Provider>
149
+ );
150
+ }
151
+
152
+ /**
153
+ * Read the resolved canvas configuration. Always returns a complete object
154
+ * with defaults filled in.
155
+ */
156
+ export function useCanvasConfig(): ResolvedCanvasConfig {
157
+ return useContext(CanvasContext);
158
+ }
159
+
160
+ /**
161
+ * Convenience helper used by the internal Canvas component (and exported
162
+ * so host-rendered slide previews can reuse the same resolution rules).
163
+ * Returns whatever the canvas config says, falling back to the slide's
164
+ * own `background` property.
165
+ */
166
+ export function resolveSlideBackground(
167
+ config: ResolvedCanvasConfig,
168
+ slide: Slide
169
+ ): string {
170
+ if (config.forceSlideBackground) return config.forceSlideBackground;
171
+ if (config.resolveSlideBackground) {
172
+ const resolved = config.resolveSlideBackground(slide);
173
+ if (resolved !== undefined) return resolved;
174
+ }
175
+ return slide.background;
176
+ }
@@ -29,6 +29,10 @@ import {
29
29
  surfacesToCssVars,
30
30
  type SlidewiseSurfaces,
31
31
  } from "./SurfacesContext";
32
+ import {
33
+ CanvasConfigProvider,
34
+ type SlidewiseCanvasConfig,
35
+ } from "./CanvasContext";
32
36
 
33
37
  export interface SlidewiseRootProps {
34
38
  /**
@@ -118,6 +122,14 @@ export interface SlidewiseRootProps {
118
122
  * ```
119
123
  */
120
124
  surfaces?: SlidewiseSurfaces;
125
+ /**
126
+ * Canvas/viewport configuration. Set `padding`, `slideRadius`,
127
+ * `slideShadow`, and an initial `defaultZoom` to keep the slide
128
+ * presented as a centered card rather than letting a bold deck fill
129
+ * paint the entire workspace. `forceSlideBackground` /
130
+ * `resolveSlideBackground` let hosts override per-slide fills.
131
+ */
132
+ canvas?: SlidewiseCanvasConfig;
121
133
  /** Extra class names appended to the root. */
122
134
  className?: string;
123
135
  /** Inline style applied to the root. */
@@ -241,6 +253,7 @@ function RootInner({
241
253
  icons,
242
254
  labels,
243
255
  surfaces,
256
+ canvas,
244
257
  className,
245
258
  style,
246
259
  children,
@@ -305,6 +318,14 @@ function RootInner({
305
318
  store.getState().selectSlide(initialSlideId);
306
319
  }
307
320
  }
321
+ // Apply canvas defaults once on mount. setZoom auto-flips fitMode to
322
+ // "manual" so we set fitMode last to honor an explicit canvas.fitMode.
323
+ if (canvas?.defaultZoom !== undefined) {
324
+ store.getState().setZoom(canvas.defaultZoom);
325
+ }
326
+ if (canvas?.fitMode) {
327
+ store.getState().setFitMode(canvas.fitMode);
328
+ }
308
329
  // run once on mount
309
330
  // eslint-disable-next-line react-hooks/exhaustive-deps
310
331
  }, []);
@@ -515,17 +536,19 @@ function RootInner({
515
536
  <IconProvider icons={icons ?? {}}>
516
537
  <LabelsProvider labels={labels}>
517
538
  <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>
539
+ <CanvasConfigProvider config={canvas}>
540
+ <DirtyProvider dirty={dirty}>
541
+ <HostProvider callbacks={{ onSave: wrappedSave, onExport }}>
542
+ <RootShell
543
+ fontFamily={fontFamily}
544
+ className={combinedClassName}
545
+ style={mergedStyle}
546
+ >
547
+ {children}
548
+ </RootShell>
549
+ </HostProvider>
550
+ </DirtyProvider>
551
+ </CanvasConfigProvider>
529
552
  </SurfacesProvider>
530
553
  </LabelsProvider>
531
554
  </IconProvider>
@@ -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,
@@ -87,6 +99,14 @@ export {
87
99
  surfacesToCssVars,
88
100
  type SlidewiseSurfaces,
89
101
  } from "./SurfacesContext";
102
+ export {
103
+ CanvasConfigProvider,
104
+ useCanvasConfig,
105
+ resolveSlideBackground,
106
+ DEFAULT_CANVAS_CONFIG,
107
+ type SlidewiseCanvasConfig,
108
+ type ResolvedCanvasConfig,
109
+ } from "./CanvasContext";
90
110
 
91
111
  /**
92
112
  * Store hooks. Use these from host components anywhere under
@@ -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
+ }