@textcortex/slidewise 1.4.0 → 1.6.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.4.0",
3
+ "version": "1.6.0",
4
4
  "description": "Embeddable React PPTX editor.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -34,6 +34,54 @@
34
34
  --slidewise-bar-bg: var(--app-bg);
35
35
  --slidewise-accent: var(--accent);
36
36
 
37
+ /* Per-surface background tokens. Map onto internal vars by default so
38
+ existing theme styling is preserved. Hosts override any subset (or
39
+ pass the `surfaces` prop on <Slidewise.Root>) to retint individual
40
+ regions independently. */
41
+ --slidewise-bg-app: var(--app-bg);
42
+ --slidewise-bg-topbar: var(--app-bg);
43
+ --slidewise-bg-rail: var(--rail-bg);
44
+ --slidewise-bg-rail-item: transparent;
45
+ --slidewise-bg-rail-item-active: var(--accent-soft);
46
+ --slidewise-bg-canvas-frame: transparent;
47
+ --slidewise-bg-canvas-from: var(--canvas-bg-from);
48
+ --slidewise-bg-canvas-to: var(--canvas-bg-to);
49
+ --slidewise-bg-bottom-toolbar: var(--toolbar-bg);
50
+ --slidewise-bg-right-panel: var(--rail-bg);
51
+ --slidewise-bg-menu: var(--menu-bg);
52
+ --slidewise-bg-tooltip: var(--menu-bg);
53
+ --slidewise-bg-popover: var(--menu-bg);
54
+ --slidewise-bg-dialog: var(--menu-bg);
55
+ --slidewise-bg-slide: #ffffff;
56
+ --slidewise-bg-selection: var(--accent-soft);
57
+ --slidewise-bg-hover: var(--hover);
58
+ --slidewise-bg-active: var(--active);
59
+ --slidewise-bg-input: var(--input-bg);
60
+ --slidewise-bg-button: transparent;
61
+ --slidewise-bg-button-hover: var(--hover);
62
+ --slidewise-bg-pill: var(--smart-grad);
63
+ --slidewise-bg-unsaved-badge: rgba(232, 80, 76, 0.12);
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
+
37
85
  /* Layout backgrounds */
38
86
  --app-bg: #ffffff;
39
87
  --rail-bg: #fafafb;
@@ -126,6 +174,12 @@
126
174
  --canvas-bg-from: #181c38;
127
175
  --canvas-bg-to: #0c1027;
128
176
 
177
+ /* Dark-mode overrides for the public --slidewise-bg-* surface tokens.
178
+ Hosts can still override any one of these from a higher specificity
179
+ selector or via the `surfaces` prop. */
180
+ --slidewise-bg-slide: #ffffff;
181
+ --slidewise-bg-unsaved-badge: rgba(232, 80, 76, 0.18);
182
+
129
183
  /* Charcoal-purple surface kit from textcortex/platform#7428 — neutral
130
184
  dark base, faint inset highlight, layered shadow that lifts to a
131
185
  subtle plum tint on hover. */
@@ -261,3 +315,40 @@
261
315
  transition: none;
262
316
  }
263
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
+ }
@@ -15,6 +15,9 @@ import {
15
15
  CanvasFrame,
16
16
  } from "./compound/parts";
17
17
  import type { SlidewiseIcons } from "./compound/IconContext";
18
+ import type { SlidewiseLabels } from "./compound/LabelsContext";
19
+ import type { SlidewiseSurfaces } from "./compound/SurfacesContext";
20
+ import type { Transition } from "framer-motion";
18
21
  import type { Deck } from "@/lib/types";
19
22
  import "./SlidewiseEditor.css";
20
23
 
@@ -65,6 +68,16 @@ export interface SlidewiseEditorProps {
65
68
  showBottomToolbar?: boolean;
66
69
  /** Override the bundled Geist font; sets `--font-geist-sans` on the root. */
67
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;
68
81
  /**
69
82
  * Per-action icon overrides. Pass a ReactNode for any of `undo`, `redo`,
70
83
  * `save`, `play`, `themeLight`, `themeDark`, `export`, `smart` to skin the
@@ -72,6 +85,17 @@ export interface SlidewiseEditorProps {
72
85
  * bundled lucide-react icons.
73
86
  */
74
87
  icons?: SlidewiseIcons;
88
+ /**
89
+ * User-visible string overrides for i18n. Pass any subset; missing
90
+ * entries fall back to English defaults.
91
+ */
92
+ labels?: SlidewiseLabels;
93
+ /**
94
+ * Per-surface background overrides, equivalent to setting the
95
+ * `--slidewise-bg-*` CSS variables. See `SlidewiseSurfaces` for the full
96
+ * list of keys.
97
+ */
98
+ surfaces?: SlidewiseSurfaces;
75
99
  /** Extra class names appended to the editor root. */
76
100
  className?: string;
77
101
  /** Inline style applied to the editor root. */
@@ -122,7 +146,11 @@ export const SlidewiseEditor = forwardRef<
122
146
  showTopBar = true,
123
147
  showBottomToolbar = true,
124
148
  fontFamily,
149
+ reduceMotion,
150
+ transition,
125
151
  icons,
152
+ labels,
153
+ surfaces,
126
154
  className,
127
155
  style,
128
156
  },
@@ -145,7 +173,11 @@ export const SlidewiseEditor = forwardRef<
145
173
  theme,
146
174
  initialSlideId,
147
175
  fontFamily,
176
+ reduceMotion,
177
+ transition,
148
178
  icons,
179
+ labels,
180
+ surfaces,
149
181
  className,
150
182
  style,
151
183
  };
@@ -10,6 +10,10 @@ import { SlidewiseEditor, type SlidewiseEditorHandle } from "./SlidewiseEditor";
10
10
  import { parsePptx, serializeDeck } from "@/lib/pptx";
11
11
  import type { Deck } from "@/lib/types";
12
12
  import type { SlidewiseIcons } from "./compound/IconContext";
13
+ import type { SlidewiseLabels } from "./compound/LabelsContext";
14
+ import type { SlidewiseSurfaces } from "./compound/SurfacesContext";
15
+ import { DEFAULT_LABELS } from "./compound/LabelsContext";
16
+ import type { Transition } from "framer-motion";
13
17
  import type { HistoryState, SelectionSnapshot } from "./compound/SlidewiseRoot";
14
18
 
15
19
  export interface SlidewiseFileEditorProps {
@@ -91,6 +95,24 @@ export interface SlidewiseFileEditorProps {
91
95
  * editor's chrome with your own icon set.
92
96
  */
93
97
  icons?: SlidewiseIcons;
98
+ /**
99
+ * User-visible string overrides for i18n. Pass any subset; missing
100
+ * entries fall back to English defaults. The "Unsaved changes" badge
101
+ * and the loading / load-error messages also key off this table.
102
+ */
103
+ labels?: SlidewiseLabels;
104
+ /**
105
+ * Per-surface background overrides; equivalent to setting the
106
+ * `--slidewise-bg-*` CSS variables.
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;
94
116
  className?: string;
95
117
  style?: CSSProperties;
96
118
  /**
@@ -193,6 +215,10 @@ export const SlidewiseFileEditor = forwardRef<
193
215
  showBottomToolbar,
194
216
  fontFamily,
195
217
  icons,
218
+ labels,
219
+ surfaces,
220
+ reduceMotion,
221
+ transition,
196
222
  className,
197
223
  style,
198
224
  parse = parsePptx,
@@ -337,15 +363,18 @@ export const SlidewiseFileEditor = forwardRef<
337
363
  if (state.status === "loading") {
338
364
  return (
339
365
  <div style={{ ...frameStyle, ...style }} className={className}>
340
- <div style={messageStyle}>Loading…</div>
366
+ <div style={messageStyle}>
367
+ {labels?.fileLoading ?? DEFAULT_LABELS.fileLoading}
368
+ </div>
341
369
  </div>
342
370
  );
343
371
  }
344
372
  if (state.status === "error") {
373
+ const fmt = labels?.fileLoadError ?? DEFAULT_LABELS.fileLoadError;
345
374
  return (
346
375
  <div style={{ ...frameStyle, ...style }} className={className}>
347
376
  <div style={{ ...messageStyle, color: "#E8504C" }}>
348
- Could not open file: {state.error.message}
377
+ {fmt(state.error.message)}
349
378
  </div>
350
379
  </div>
351
380
  );
@@ -363,6 +392,10 @@ export const SlidewiseFileEditor = forwardRef<
363
392
  showBottomToolbar={showBottomToolbar}
364
393
  fontFamily={fontFamily}
365
394
  icons={icons}
395
+ labels={labels}
396
+ surfaces={surfaces}
397
+ reduceMotion={reduceMotion}
398
+ transition={transition}
366
399
  onChange={(next) => {
367
400
  onChangeRef.current?.(next);
368
401
  }}
@@ -382,7 +415,11 @@ export const SlidewiseFileEditor = forwardRef<
382
415
  await saveBlob(blob);
383
416
  }}
384
417
  />
385
- {dirty && <UnsavedBadge />}
418
+ {dirty && (
419
+ <UnsavedBadge
420
+ label={labels?.unsavedBadge ?? DEFAULT_LABELS.unsavedBadge}
421
+ />
422
+ )}
386
423
  </div>
387
424
  );
388
425
  });
@@ -403,7 +440,7 @@ const messageStyle: CSSProperties = {
403
440
  color: "#5b6178",
404
441
  };
405
442
 
406
- function UnsavedBadge() {
443
+ function UnsavedBadge({ label }: { label: string }) {
407
444
  return (
408
445
  <div
409
446
  style={{
@@ -411,7 +448,8 @@ function UnsavedBadge() {
411
448
  top: 12,
412
449
  right: 12,
413
450
  padding: "4px 10px",
414
- background: "rgba(232, 80, 76, 0.12)",
451
+ background:
452
+ "var(--slidewise-bg-unsaved-badge, rgba(232, 80, 76, 0.12))",
415
453
  color: "#E8504C",
416
454
  borderRadius: 999,
417
455
  fontFamily: "Inter, system-ui, sans-serif",
@@ -422,7 +460,7 @@ function UnsavedBadge() {
422
460
  pointerEvents: "none",
423
461
  }}
424
462
  >
425
- Unsaved changes
463
+ {label}
426
464
  </div>
427
465
  );
428
466
  }
@@ -0,0 +1,121 @@
1
+ import { createContext, useContext, useMemo, type ReactNode } from "react";
2
+
3
+ /**
4
+ * Every user-visible string in the editor's chrome. Pass any subset on
5
+ * `<Slidewise.Root labels={...}>` to localise; missing entries fall back
6
+ * to the English defaults. Use this prop together with `icons` to fully
7
+ * skin Slidewise for non-English deployments.
8
+ *
9
+ * Single-string entries are used both as the button's visible label AND
10
+ * as its `aria-label`. Where the two need to diverge (the Save button
11
+ * cycles through three states; the theme toggle shows different labels
12
+ * per theme), nested keys are provided.
13
+ */
14
+ export interface SlidewiseLabels {
15
+ // TopBar buttons (visible text)
16
+ save?: { idle?: string; saving?: string; saved?: string };
17
+ play?: string;
18
+ stop?: string;
19
+ export?: string;
20
+ undo?: string;
21
+ redo?: string;
22
+ themeToggle?: { toDark?: string; toLight?: string };
23
+
24
+ // Pills + status indicators
25
+ smart?: string;
26
+ unsavedBadge?: string;
27
+
28
+ // Inputs
29
+ titleAriaLabel?: string;
30
+
31
+ // SlideRail (reserved for the future SlideRail compound subparts)
32
+ addSlide?: string;
33
+ duplicateSlide?: string;
34
+ deleteSlide?: string;
35
+
36
+ // Errors
37
+ fileLoadError?: (msg: string) => string;
38
+ fileLoading?: string;
39
+ }
40
+
41
+ /**
42
+ * The fully-resolved label table — every key present, no optionals. Internal
43
+ * components read this directly so they never have to check for undefined.
44
+ */
45
+ export interface ResolvedLabels {
46
+ save: { idle: string; saving: string; saved: string };
47
+ play: string;
48
+ stop: string;
49
+ export: string;
50
+ undo: string;
51
+ redo: string;
52
+ themeToggle: { toDark: string; toLight: string };
53
+ smart: string;
54
+ unsavedBadge: string;
55
+ titleAriaLabel: string;
56
+ addSlide: string;
57
+ duplicateSlide: string;
58
+ deleteSlide: string;
59
+ fileLoadError: (msg: string) => string;
60
+ fileLoading: string;
61
+ }
62
+
63
+ export const DEFAULT_LABELS: ResolvedLabels = {
64
+ save: { idle: "Save", saving: "Saving…", saved: "Saved" },
65
+ play: "Play",
66
+ stop: "Stop",
67
+ export: "Export",
68
+ undo: "Undo",
69
+ redo: "Redo",
70
+ themeToggle: { toDark: "Dark mode", toLight: "Light mode" },
71
+ smart: "Smart",
72
+ unsavedBadge: "Unsaved changes",
73
+ titleAriaLabel: "Deck title",
74
+ addSlide: "Add slide",
75
+ duplicateSlide: "Duplicate slide",
76
+ deleteSlide: "Delete slide",
77
+ fileLoadError: (msg) => `Could not open file: ${msg}`,
78
+ fileLoading: "Loading…",
79
+ };
80
+
81
+ function mergeLabels(
82
+ overrides: SlidewiseLabels | undefined
83
+ ): ResolvedLabels {
84
+ if (!overrides) return DEFAULT_LABELS;
85
+ return {
86
+ save: { ...DEFAULT_LABELS.save, ...overrides.save },
87
+ play: overrides.play ?? DEFAULT_LABELS.play,
88
+ stop: overrides.stop ?? DEFAULT_LABELS.stop,
89
+ export: overrides.export ?? DEFAULT_LABELS.export,
90
+ undo: overrides.undo ?? DEFAULT_LABELS.undo,
91
+ redo: overrides.redo ?? DEFAULT_LABELS.redo,
92
+ themeToggle: { ...DEFAULT_LABELS.themeToggle, ...overrides.themeToggle },
93
+ smart: overrides.smart ?? DEFAULT_LABELS.smart,
94
+ unsavedBadge: overrides.unsavedBadge ?? DEFAULT_LABELS.unsavedBadge,
95
+ titleAriaLabel: overrides.titleAriaLabel ?? DEFAULT_LABELS.titleAriaLabel,
96
+ addSlide: overrides.addSlide ?? DEFAULT_LABELS.addSlide,
97
+ duplicateSlide: overrides.duplicateSlide ?? DEFAULT_LABELS.duplicateSlide,
98
+ deleteSlide: overrides.deleteSlide ?? DEFAULT_LABELS.deleteSlide,
99
+ fileLoadError: overrides.fileLoadError ?? DEFAULT_LABELS.fileLoadError,
100
+ fileLoading: overrides.fileLoading ?? DEFAULT_LABELS.fileLoading,
101
+ };
102
+ }
103
+
104
+ const LabelsContext = createContext<ResolvedLabels>(DEFAULT_LABELS);
105
+
106
+ export function LabelsProvider({
107
+ labels,
108
+ children,
109
+ }: {
110
+ labels: SlidewiseLabels | undefined;
111
+ children: ReactNode;
112
+ }) {
113
+ const resolved = useMemo(() => mergeLabels(labels), [labels]);
114
+ return (
115
+ <LabelsContext.Provider value={resolved}>{children}</LabelsContext.Provider>
116
+ );
117
+ }
118
+
119
+ export function useLabels(): ResolvedLabels {
120
+ return useContext(LabelsContext);
121
+ }
@@ -18,10 +18,17 @@ 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";
24
25
  import { DirtyProvider } from "./DirtyContext";
26
+ import { LabelsProvider, type SlidewiseLabels } from "./LabelsContext";
27
+ import {
28
+ SurfacesProvider,
29
+ surfacesToCssVars,
30
+ type SlidewiseSurfaces,
31
+ } from "./SurfacesContext";
25
32
 
26
33
  export interface SlidewiseRootProps {
27
34
  /**
@@ -66,12 +73,51 @@ export interface SlidewiseRootProps {
66
73
  initialSlideId?: string;
67
74
  /** Override the default Geist font; sets `--slidewise-font-sans`. */
68
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;
69
99
  /**
70
100
  * Per-action icon overrides for the chrome. Hosts pass any subset to
71
101
  * skin Slidewise with their own icon set; missing slots fall back to
72
102
  * the bundled lucide icons.
73
103
  */
74
104
  icons?: SlidewiseIcons;
105
+ /**
106
+ * User-visible string overrides for i18n. Pass any subset; missing
107
+ * entries fall back to English defaults. Pairs with `icons` for full
108
+ * locale customization.
109
+ */
110
+ labels?: SlidewiseLabels;
111
+ /**
112
+ * Per-surface background overrides. Equivalent to setting the
113
+ * `--slidewise-bg-*` CSS variables on the root, but as a typed prop so
114
+ * hosts can drive theming from JS without writing CSS:
115
+ *
116
+ * ```tsx
117
+ * <Slidewise.Root surfaces={{ app: "#0b0d10", rail: "#1c1c22" }}>
118
+ * ```
119
+ */
120
+ surfaces?: SlidewiseSurfaces;
75
121
  /** Extra class names appended to the root. */
76
122
  className?: string;
77
123
  /** Inline style applied to the root. */
@@ -190,7 +236,11 @@ function RootInner({
190
236
  theme,
191
237
  initialSlideId,
192
238
  fontFamily,
239
+ reduceMotion = "system",
240
+ transition,
193
241
  icons,
242
+ labels,
243
+ surfaces,
194
244
  className,
195
245
  style,
196
246
  children,
@@ -429,22 +479,58 @@ function RootInner({
429
479
  }
430
480
  : undefined;
431
481
 
482
+ // Merge host's `style` prop with the surface overrides so a host can
483
+ // pass both without one clobbering the other. Surfaces win on conflict
484
+ // because they're the more specific theming intent.
485
+ const surfaceVars = surfacesToCssVars(surfaces);
486
+ const mergedStyle: CSSProperties | undefined = surfaceVars
487
+ ? { ...style, ...(surfaceVars as CSSProperties) }
488
+ : style;
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
+
432
512
  return (
433
- <ReadOnlyProvider readOnly={readOnly}>
434
- <IconProvider icons={icons ?? {}}>
435
- <DirtyProvider dirty={dirty}>
436
- <HostProvider callbacks={{ onSave: wrappedSave, onExport }}>
437
- <RootShell
438
- fontFamily={fontFamily}
439
- className={className}
440
- style={style}
441
- >
442
- {children}
443
- </RootShell>
444
- </HostProvider>
445
- </DirtyProvider>
446
- </IconProvider>
447
- </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>
448
534
  );
449
535
  }
450
536