@textcortex/slidewise 1.7.0 → 1.9.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.7.0",
3
+ "version": "1.9.0",
4
4
  "description": "Embeddable React PPTX editor.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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
@@ -65,12 +65,17 @@ function TextView({
65
65
  : el.vAlign === "middle"
66
66
  ? "center"
67
67
  : "flex-end",
68
+ background: el.background,
69
+ padding: el.padding
70
+ ? `${el.padding.t}px ${el.padding.r}px ${el.padding.b}px ${el.padding.l}px`
71
+ : undefined,
72
+ boxSizing: el.padding ? "border-box" : undefined,
68
73
  cursor: editing ? "text" : "inherit",
69
74
  };
70
75
  const inner: React.CSSProperties = {
71
76
  width: "100%",
72
77
  color: el.color,
73
- fontFamily: el.fontFamily,
78
+ fontFamily: withGenericFallback(el.fontFamily),
74
79
  fontSize: el.fontSize,
75
80
  fontWeight: el.fontWeight,
76
81
  fontStyle: el.italic ? "italic" : "normal",
@@ -85,11 +90,40 @@ function TextView({
85
90
  outline: "none",
86
91
  };
87
92
 
93
+ const backingPath = el.backingPath;
94
+ const positionedOuter: React.CSSProperties = backingPath
95
+ ? { ...outer, position: "relative" }
96
+ : outer;
97
+ const innerStacked: React.CSSProperties = backingPath
98
+ ? { ...inner, position: "relative", zIndex: 1 }
99
+ : inner;
100
+ const backingSvg = backingPath ? (
101
+ <svg
102
+ viewBox={`0 0 ${backingPath.viewW} ${backingPath.viewH}`}
103
+ preserveAspectRatio="none"
104
+ style={{
105
+ position: "absolute",
106
+ inset: 0,
107
+ width: "100%",
108
+ height: "100%",
109
+ pointerEvents: "none",
110
+ zIndex: 0,
111
+ }}
112
+ >
113
+ <path
114
+ d={backingPath.d}
115
+ fill={backingPath.fill}
116
+ fillRule={backingPath.fillRule ?? "nonzero"}
117
+ />
118
+ </svg>
119
+ ) : null;
120
+
88
121
  if (editing) {
89
122
  return (
90
- <div style={outer}>
123
+ <div style={positionedOuter}>
124
+ {backingSvg}
91
125
  <EditableText
92
- style={inner}
126
+ style={innerStacked}
93
127
  initialText={el.text}
94
128
  initialRuns={el.runs}
95
129
  onCommit={(t, r) => onCommit?.(t, r)}
@@ -100,8 +134,9 @@ function TextView({
100
134
 
101
135
  if (el.runs && el.runs.length) {
102
136
  return (
103
- <div style={outer}>
104
- <div style={inner}>
137
+ <div style={positionedOuter}>
138
+ {backingSvg}
139
+ <div style={innerStacked}>
105
140
  {el.runs.map((r, i) => (
106
141
  <span key={i} style={runCssStyle(r)}>
107
142
  {r.text}
@@ -113,15 +148,40 @@ function TextView({
113
148
  }
114
149
 
115
150
  return (
116
- <div style={outer}>
117
- <div style={inner}>{el.text}</div>
151
+ <div style={positionedOuter}>
152
+ {backingSvg}
153
+ <div style={innerStacked}>{el.text}</div>
118
154
  </div>
119
155
  );
120
156
  }
121
157
 
158
+ /**
159
+ * Append a `sans-serif` generic so brand families imported from PPTX
160
+ * (e.g. "EON Office Head") degrade gracefully when the typeface isn't
161
+ * installed locally — without the generic the browser silently picks
162
+ * its default serif. Already-qualified stacks (containing a comma) and
163
+ * plain generics ("serif"/"monospace") pass through untouched.
164
+ */
165
+ function withGenericFallback(family: string | undefined): string | undefined {
166
+ if (!family) return family;
167
+ if (family.includes(",")) return family;
168
+ const lower = family.trim().toLowerCase();
169
+ if (
170
+ lower === "serif" ||
171
+ lower === "sans-serif" ||
172
+ lower === "monospace" ||
173
+ lower === "cursive" ||
174
+ lower === "fantasy" ||
175
+ lower === "system-ui"
176
+ ) {
177
+ return family;
178
+ }
179
+ return `${family}, sans-serif`;
180
+ }
181
+
122
182
  function runCssStyle(r: TextRun): React.CSSProperties {
123
183
  const s: React.CSSProperties = {};
124
- if (r.fontFamily) s.fontFamily = r.fontFamily;
184
+ if (r.fontFamily) s.fontFamily = withGenericFallback(r.fontFamily);
125
185
  if (r.fontSize) s.fontSize = r.fontSize;
126
186
  if (r.fontWeight) s.fontWeight = r.fontWeight;
127
187
  if (r.color) s.color = r.color;
@@ -315,6 +375,27 @@ function sameStyle(a: TextRun, b: TextRun): boolean {
315
375
  function ShapeView({ el }: { el: ShapeElement }) {
316
376
  const stroke = el.stroke ?? "transparent";
317
377
  const sw = el.strokeWidth ?? 0;
378
+ // Custom vector path (PPTX <a:custGeom>) takes precedence over the preset
379
+ // kind — the path coordinates already encode the actual silhouette.
380
+ if (el.path) {
381
+ return (
382
+ <svg
383
+ viewBox={`0 0 ${el.path.viewW} ${el.path.viewH}`}
384
+ preserveAspectRatio="none"
385
+ width="100%"
386
+ height="100%"
387
+ >
388
+ <path
389
+ d={el.path.d}
390
+ fill={el.fill}
391
+ fillRule={el.path.fillRule ?? "nonzero"}
392
+ stroke={stroke}
393
+ strokeWidth={sw || undefined}
394
+ vectorEffect="non-scaling-stroke"
395
+ />
396
+ </svg>
397
+ );
398
+ }
318
399
  if (el.shape === "rect" || el.shape === "rounded") {
319
400
  return (
320
401
  <div
@@ -474,7 +555,9 @@ function LineView({ el }: { el: LineElement }) {
474
555
  function TableView({ el }: { el: TableElement }) {
475
556
  const cols = el.rows[0]?.length ?? 1;
476
557
  // PPTX-faithful: contiguous cells, no inter-cell gap, no rounded corners.
477
- // Earlier "card grid" styling drifted too far from the source look.
558
+ // Cells share their dividers via inset box-shadows so we draw a single
559
+ // grid line between adjacent cells instead of doubling-up borders.
560
+ const stroke = el.borderColor ?? "rgba(0, 0, 0, 0.12)";
478
561
  return (
479
562
  <div
480
563
  style={{
@@ -485,6 +568,7 @@ function TableView({ el }: { el: TableElement }) {
485
568
  height: "100%",
486
569
  gap: 0,
487
570
  background: "transparent",
571
+ boxShadow: `inset 0 0 0 1px ${stroke}`,
488
572
  }}
489
573
  >
490
574
  {el.rows.flatMap((row, ri) =>
@@ -504,6 +588,10 @@ function TableView({ el }: { el: TableElement }) {
504
588
  minHeight: 0,
505
589
  overflow: "hidden",
506
590
  wordBreak: "break-word",
591
+ borderRight:
592
+ ci < cols - 1 ? `1px solid ${stroke}` : undefined,
593
+ borderBottom:
594
+ ri < el.rows.length - 1 ? `1px solid ${stroke}` : undefined,
507
595
  }}
508
596
  >
509
597
  {cell}
@@ -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>
@@ -99,6 +99,14 @@ export {
99
99
  surfacesToCssVars,
100
100
  type SlidewiseSurfaces,
101
101
  } from "./SurfacesContext";
102
+ export {
103
+ CanvasConfigProvider,
104
+ useCanvasConfig,
105
+ resolveSlideBackground,
106
+ DEFAULT_CANVAS_CONFIG,
107
+ type SlidewiseCanvasConfig,
108
+ type ResolvedCanvasConfig,
109
+ } from "./CanvasContext";
102
110
 
103
111
  /**
104
112
  * Store hooks. Use these from host components anywhere under
package/src/index.ts CHANGED
@@ -48,6 +48,9 @@ export {
48
48
  useDirty,
49
49
  useLabels,
50
50
  useSurfaces,
51
+ useCanvasConfig,
52
+ resolveSlideBackground,
53
+ DEFAULT_CANVAS_CONFIG,
51
54
  useEditor,
52
55
  useEditorStore,
53
56
  useSlides,
@@ -67,7 +70,9 @@ export {
67
70
  type SlidewiseIcons,
68
71
  type SlidewiseLabels,
69
72
  type SlidewiseSurfaces,
73
+ type SlidewiseCanvasConfig,
70
74
  type ResolvedLabels,
75
+ type ResolvedCanvasConfig,
71
76
  useSlideRailItem,
72
77
  type RegionProps,
73
78
  type TopBarProps,