@textcortex/slidewise 1.4.0 → 1.5.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.5.0",
4
4
  "description": "Embeddable React PPTX editor.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -34,6 +34,34 @@
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
+
37
65
  /* Layout backgrounds */
38
66
  --app-bg: #ffffff;
39
67
  --rail-bg: #fafafb;
@@ -126,6 +154,12 @@
126
154
  --canvas-bg-from: #181c38;
127
155
  --canvas-bg-to: #0c1027;
128
156
 
157
+ /* Dark-mode overrides for the public --slidewise-bg-* surface tokens.
158
+ Hosts can still override any one of these from a higher specificity
159
+ selector or via the `surfaces` prop. */
160
+ --slidewise-bg-slide: #ffffff;
161
+ --slidewise-bg-unsaved-badge: rgba(232, 80, 76, 0.18);
162
+
129
163
  /* Charcoal-purple surface kit from textcortex/platform#7428 — neutral
130
164
  dark base, faint inset highlight, layered shadow that lifts to a
131
165
  subtle plum tint on hover. */
@@ -15,6 +15,8 @@ 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";
18
20
  import type { Deck } from "@/lib/types";
19
21
  import "./SlidewiseEditor.css";
20
22
 
@@ -72,6 +74,17 @@ export interface SlidewiseEditorProps {
72
74
  * bundled lucide-react icons.
73
75
  */
74
76
  icons?: SlidewiseIcons;
77
+ /**
78
+ * User-visible string overrides for i18n. Pass any subset; missing
79
+ * entries fall back to English defaults.
80
+ */
81
+ labels?: SlidewiseLabels;
82
+ /**
83
+ * Per-surface background overrides, equivalent to setting the
84
+ * `--slidewise-bg-*` CSS variables. See `SlidewiseSurfaces` for the full
85
+ * list of keys.
86
+ */
87
+ surfaces?: SlidewiseSurfaces;
75
88
  /** Extra class names appended to the editor root. */
76
89
  className?: string;
77
90
  /** Inline style applied to the editor root. */
@@ -123,6 +136,8 @@ export const SlidewiseEditor = forwardRef<
123
136
  showBottomToolbar = true,
124
137
  fontFamily,
125
138
  icons,
139
+ labels,
140
+ surfaces,
126
141
  className,
127
142
  style,
128
143
  },
@@ -146,6 +161,8 @@ export const SlidewiseEditor = forwardRef<
146
161
  initialSlideId,
147
162
  fontFamily,
148
163
  icons,
164
+ labels,
165
+ surfaces,
149
166
  className,
150
167
  style,
151
168
  };
@@ -10,6 +10,9 @@ 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";
13
16
  import type { HistoryState, SelectionSnapshot } from "./compound/SlidewiseRoot";
14
17
 
15
18
  export interface SlidewiseFileEditorProps {
@@ -91,6 +94,17 @@ export interface SlidewiseFileEditorProps {
91
94
  * editor's chrome with your own icon set.
92
95
  */
93
96
  icons?: SlidewiseIcons;
97
+ /**
98
+ * User-visible string overrides for i18n. Pass any subset; missing
99
+ * entries fall back to English defaults. The "Unsaved changes" badge
100
+ * and the loading / load-error messages also key off this table.
101
+ */
102
+ labels?: SlidewiseLabels;
103
+ /**
104
+ * Per-surface background overrides; equivalent to setting the
105
+ * `--slidewise-bg-*` CSS variables.
106
+ */
107
+ surfaces?: SlidewiseSurfaces;
94
108
  className?: string;
95
109
  style?: CSSProperties;
96
110
  /**
@@ -193,6 +207,8 @@ export const SlidewiseFileEditor = forwardRef<
193
207
  showBottomToolbar,
194
208
  fontFamily,
195
209
  icons,
210
+ labels,
211
+ surfaces,
196
212
  className,
197
213
  style,
198
214
  parse = parsePptx,
@@ -337,15 +353,18 @@ export const SlidewiseFileEditor = forwardRef<
337
353
  if (state.status === "loading") {
338
354
  return (
339
355
  <div style={{ ...frameStyle, ...style }} className={className}>
340
- <div style={messageStyle}>Loading…</div>
356
+ <div style={messageStyle}>
357
+ {labels?.fileLoading ?? DEFAULT_LABELS.fileLoading}
358
+ </div>
341
359
  </div>
342
360
  );
343
361
  }
344
362
  if (state.status === "error") {
363
+ const fmt = labels?.fileLoadError ?? DEFAULT_LABELS.fileLoadError;
345
364
  return (
346
365
  <div style={{ ...frameStyle, ...style }} className={className}>
347
366
  <div style={{ ...messageStyle, color: "#E8504C" }}>
348
- Could not open file: {state.error.message}
367
+ {fmt(state.error.message)}
349
368
  </div>
350
369
  </div>
351
370
  );
@@ -363,6 +382,8 @@ export const SlidewiseFileEditor = forwardRef<
363
382
  showBottomToolbar={showBottomToolbar}
364
383
  fontFamily={fontFamily}
365
384
  icons={icons}
385
+ labels={labels}
386
+ surfaces={surfaces}
366
387
  onChange={(next) => {
367
388
  onChangeRef.current?.(next);
368
389
  }}
@@ -382,7 +403,11 @@ export const SlidewiseFileEditor = forwardRef<
382
403
  await saveBlob(blob);
383
404
  }}
384
405
  />
385
- {dirty && <UnsavedBadge />}
406
+ {dirty && (
407
+ <UnsavedBadge
408
+ label={labels?.unsavedBadge ?? DEFAULT_LABELS.unsavedBadge}
409
+ />
410
+ )}
386
411
  </div>
387
412
  );
388
413
  });
@@ -403,7 +428,7 @@ const messageStyle: CSSProperties = {
403
428
  color: "#5b6178",
404
429
  };
405
430
 
406
- function UnsavedBadge() {
431
+ function UnsavedBadge({ label }: { label: string }) {
407
432
  return (
408
433
  <div
409
434
  style={{
@@ -411,7 +436,8 @@ function UnsavedBadge() {
411
436
  top: 12,
412
437
  right: 12,
413
438
  padding: "4px 10px",
414
- background: "rgba(232, 80, 76, 0.12)",
439
+ background:
440
+ "var(--slidewise-bg-unsaved-badge, rgba(232, 80, 76, 0.12))",
415
441
  color: "#E8504C",
416
442
  borderRadius: 999,
417
443
  fontFamily: "Inter, system-ui, sans-serif",
@@ -422,7 +448,7 @@ function UnsavedBadge() {
422
448
  pointerEvents: "none",
423
449
  }}
424
450
  >
425
- Unsaved changes
451
+ {label}
426
452
  </div>
427
453
  );
428
454
  }
@@ -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
+ }
@@ -22,6 +22,12 @@ import { HostProvider } from "./HostContext";
22
22
  import { IconProvider, type SlidewiseIcons } from "./IconContext";
23
23
  import { ReadOnlyProvider } from "./ReadOnlyContext";
24
24
  import { DirtyProvider } from "./DirtyContext";
25
+ import { LabelsProvider, type SlidewiseLabels } from "./LabelsContext";
26
+ import {
27
+ SurfacesProvider,
28
+ surfacesToCssVars,
29
+ type SlidewiseSurfaces,
30
+ } from "./SurfacesContext";
25
31
 
26
32
  export interface SlidewiseRootProps {
27
33
  /**
@@ -72,6 +78,22 @@ export interface SlidewiseRootProps {
72
78
  * the bundled lucide icons.
73
79
  */
74
80
  icons?: SlidewiseIcons;
81
+ /**
82
+ * User-visible string overrides for i18n. Pass any subset; missing
83
+ * entries fall back to English defaults. Pairs with `icons` for full
84
+ * locale customization.
85
+ */
86
+ labels?: SlidewiseLabels;
87
+ /**
88
+ * Per-surface background overrides. Equivalent to setting the
89
+ * `--slidewise-bg-*` CSS variables on the root, but as a typed prop so
90
+ * hosts can drive theming from JS without writing CSS:
91
+ *
92
+ * ```tsx
93
+ * <Slidewise.Root surfaces={{ app: "#0b0d10", rail: "#1c1c22" }}>
94
+ * ```
95
+ */
96
+ surfaces?: SlidewiseSurfaces;
75
97
  /** Extra class names appended to the root. */
76
98
  className?: string;
77
99
  /** Inline style applied to the root. */
@@ -191,6 +213,8 @@ function RootInner({
191
213
  initialSlideId,
192
214
  fontFamily,
193
215
  icons,
216
+ labels,
217
+ surfaces,
194
218
  className,
195
219
  style,
196
220
  children,
@@ -429,20 +453,32 @@ function RootInner({
429
453
  }
430
454
  : undefined;
431
455
 
456
+ // Merge host's `style` prop with the surface overrides so a host can
457
+ // pass both without one clobbering the other. Surfaces win on conflict
458
+ // because they're the more specific theming intent.
459
+ const surfaceVars = surfacesToCssVars(surfaces);
460
+ const mergedStyle: CSSProperties | undefined = surfaceVars
461
+ ? { ...style, ...(surfaceVars as CSSProperties) }
462
+ : style;
463
+
432
464
  return (
433
465
  <ReadOnlyProvider readOnly={readOnly}>
434
466
  <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>
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>
446
482
  </IconProvider>
447
483
  </ReadOnlyProvider>
448
484
  );
@@ -0,0 +1,128 @@
1
+ import { createContext, useContext, useMemo, type ReactNode } from "react";
2
+
3
+ /**
4
+ * Per-surface background overrides. Each key maps to a CSS variable that the
5
+ * library's stylesheet reads. Values can be any valid CSS background
6
+ * (color, gradient, var() reference, etc.). Equivalent to setting the
7
+ * `--slidewise-bg-*` variables manually — provided as a typed prop so
8
+ * hosts can drive theming from JS.
9
+ *
10
+ * Missing keys fall back to the library's defaults; supply only what you
11
+ * want to override.
12
+ */
13
+ export interface SlidewiseSurfaces {
14
+ /** Outermost app shell. Maps to `--slidewise-bg-app`. */
15
+ app?: string;
16
+ /** Top bar surface. `--slidewise-bg-topbar`. */
17
+ topbar?: string;
18
+ /** Slide rail container. `--slidewise-bg-rail`. */
19
+ rail?: string;
20
+ /** Inactive slide rail item. `--slidewise-bg-rail-item`. */
21
+ railItem?: string;
22
+ /** Active/selected slide rail item. `--slidewise-bg-rail-item-active`. */
23
+ railItemActive?: string;
24
+ /** Canvas frame (around the slide). `--slidewise-bg-canvas-frame`. */
25
+ canvasFrame?: string;
26
+ /** Canvas backdrop gradient start. `--slidewise-bg-canvas-from`. */
27
+ canvasFrom?: string;
28
+ /** Canvas backdrop gradient end. `--slidewise-bg-canvas-to`. */
29
+ canvasTo?: string;
30
+ /** Floating bottom toolbar. `--slidewise-bg-bottom-toolbar`. */
31
+ bottomToolbar?: string;
32
+ /** Right panel surface. `--slidewise-bg-right-panel`. */
33
+ rightPanel?: string;
34
+ /** Popover/menu surface. `--slidewise-bg-menu`. */
35
+ menu?: string;
36
+ /** Tooltip surface. `--slidewise-bg-tooltip`. */
37
+ tooltip?: string;
38
+ /** Popover surface. `--slidewise-bg-popover`. */
39
+ popover?: string;
40
+ /** Dialog surface. `--slidewise-bg-dialog`. */
41
+ dialog?: string;
42
+ /** Slide paper. `--slidewise-bg-slide`. */
43
+ slide?: string;
44
+ /** Selection overlay tint. `--slidewise-bg-selection`. */
45
+ selection?: string;
46
+ /** Hover state. `--slidewise-bg-hover`. */
47
+ hover?: string;
48
+ /** Active/pressed state. `--slidewise-bg-active`. */
49
+ active?: string;
50
+ /** Form input. `--slidewise-bg-input`. */
51
+ input?: string;
52
+ /** Chrome button. `--slidewise-bg-button`. */
53
+ button?: string;
54
+ /** Chrome button on hover. `--slidewise-bg-button-hover`. */
55
+ buttonHover?: string;
56
+ /** Smart pill. `--slidewise-bg-pill`. */
57
+ pill?: string;
58
+ /** Unsaved-changes badge. `--slidewise-bg-unsaved-badge`. */
59
+ unsavedBadge?: string;
60
+ }
61
+
62
+ const KEY_TO_VAR: Record<keyof SlidewiseSurfaces, string> = {
63
+ app: "--slidewise-bg-app",
64
+ topbar: "--slidewise-bg-topbar",
65
+ rail: "--slidewise-bg-rail",
66
+ railItem: "--slidewise-bg-rail-item",
67
+ railItemActive: "--slidewise-bg-rail-item-active",
68
+ canvasFrame: "--slidewise-bg-canvas-frame",
69
+ canvasFrom: "--slidewise-bg-canvas-from",
70
+ canvasTo: "--slidewise-bg-canvas-to",
71
+ bottomToolbar: "--slidewise-bg-bottom-toolbar",
72
+ rightPanel: "--slidewise-bg-right-panel",
73
+ menu: "--slidewise-bg-menu",
74
+ tooltip: "--slidewise-bg-tooltip",
75
+ popover: "--slidewise-bg-popover",
76
+ dialog: "--slidewise-bg-dialog",
77
+ slide: "--slidewise-bg-slide",
78
+ selection: "--slidewise-bg-selection",
79
+ hover: "--slidewise-bg-hover",
80
+ active: "--slidewise-bg-active",
81
+ input: "--slidewise-bg-input",
82
+ button: "--slidewise-bg-button",
83
+ buttonHover: "--slidewise-bg-button-hover",
84
+ pill: "--slidewise-bg-pill",
85
+ unsavedBadge: "--slidewise-bg-unsaved-badge",
86
+ };
87
+
88
+ /**
89
+ * Convert a `SlidewiseSurfaces` map into an object suitable for spreading
90
+ * into a React `style` prop. Returns `null` when no overrides are present
91
+ * so the consumer doesn't allocate a fresh style object every render.
92
+ */
93
+ export function surfacesToCssVars(
94
+ surfaces: SlidewiseSurfaces | undefined
95
+ ): Record<string, string> | null {
96
+ if (!surfaces) return null;
97
+ const entries: [string, string][] = [];
98
+ for (const key of Object.keys(KEY_TO_VAR) as (keyof SlidewiseSurfaces)[]) {
99
+ const value = surfaces[key];
100
+ if (value !== undefined) entries.push([KEY_TO_VAR[key], value]);
101
+ }
102
+ if (entries.length === 0) return null;
103
+ return Object.fromEntries(entries);
104
+ }
105
+
106
+ const SurfacesContext = createContext<SlidewiseSurfaces | null>(null);
107
+
108
+ export function SurfacesProvider({
109
+ surfaces,
110
+ children,
111
+ }: {
112
+ surfaces: SlidewiseSurfaces | undefined;
113
+ children: ReactNode;
114
+ }) {
115
+ const value = useMemo(() => surfaces ?? null, [surfaces]);
116
+ return (
117
+ <SurfacesContext.Provider value={value}>{children}</SurfacesContext.Provider>
118
+ );
119
+ }
120
+
121
+ /**
122
+ * Read the surface override map. Mostly used by `<Slidewise.Root>` itself,
123
+ * but exposed so host-rendered surfaces (e.g. a custom panel) can apply
124
+ * the same tokens for visual consistency.
125
+ */
126
+ export function useSurfaces(): SlidewiseSurfaces | null {
127
+ return useContext(SurfacesContext);
128
+ }
@@ -74,6 +74,19 @@ export {
74
74
  } from "./IconContext";
75
75
  export { ReadOnlyProvider, useReadOnly } from "./ReadOnlyContext";
76
76
  export { DirtyProvider, useDirty } from "./DirtyContext";
77
+ export {
78
+ LabelsProvider,
79
+ useLabels,
80
+ DEFAULT_LABELS,
81
+ type SlidewiseLabels,
82
+ type ResolvedLabels,
83
+ } from "./LabelsContext";
84
+ export {
85
+ SurfacesProvider,
86
+ useSurfaces,
87
+ surfacesToCssVars,
88
+ type SlidewiseSurfaces,
89
+ } from "./SurfacesContext";
77
90
 
78
91
  /**
79
92
  * Store hooks. Use these from host components anywhere under
@@ -68,7 +68,7 @@ export function RightPanel({
68
68
  width,
69
69
  flexShrink: 0,
70
70
  height: "100%",
71
- background: "var(--rail-bg)",
71
+ background: "var(--slidewise-bg-right-panel, var(--rail-bg))",
72
72
  borderLeft: "1px solid var(--border)",
73
73
  boxShadow: "var(--rail-shadow)",
74
74
  overflow: "auto",
@@ -3,6 +3,7 @@ import { Download } from "lucide-react";
3
3
  import { useEditorStore } from "@/lib/StoreProvider";
4
4
  import { useHostCallbacks } from "../HostContext";
5
5
  import { useIcons } from "../IconContext";
6
+ import { useLabels } from "../LabelsContext";
6
7
  import { primaryBtnStyle, primaryHoverHandlers } from "./styles";
7
8
 
8
9
  /**
@@ -24,13 +25,15 @@ export interface TopBarExportProps {
24
25
  export function Export({
25
26
  className,
26
27
  style,
27
- ariaLabel = "Export",
28
- label = "Export",
28
+ ariaLabel,
29
+ label,
29
30
  children,
30
31
  }: TopBarExportProps = {}) {
31
32
  const store = useEditorStore();
32
33
  const { onExport: onExportHost } = useHostCallbacks();
33
34
  const icons = useIcons();
35
+ const labels = useLabels();
36
+ const resolved = label ?? labels.export;
34
37
 
35
38
  const onClick = () => {
36
39
  const deck = store.getState().deck;
@@ -53,13 +56,13 @@ export function Export({
53
56
  <button
54
57
  type="button"
55
58
  className={className}
56
- aria-label={ariaLabel}
59
+ aria-label={ariaLabel ?? resolved}
57
60
  onClick={onClick}
58
61
  style={{ ...primaryBtnStyle(), ...style }}
59
62
  {...primaryHoverHandlers()}
60
63
  >
61
64
  {children ?? icons.export ?? <Download size={14} />}
62
- {label}
65
+ {resolved}
63
66
  </button>
64
67
  );
65
68
  }
@@ -2,6 +2,7 @@ import type { CSSProperties, ReactNode } from "react";
2
2
  import { Play as PlayIcon } from "lucide-react";
3
3
  import { useEditor } from "@/lib/StoreProvider";
4
4
  import { useIcons } from "../IconContext";
5
+ import { useLabels } from "../LabelsContext";
5
6
  import { chromeBtnStyle, hoverHandlers } from "./styles";
6
7
 
7
8
  /**
@@ -19,24 +20,26 @@ export interface TopBarPlayProps {
19
20
  export function Play({
20
21
  className,
21
22
  style,
22
- ariaLabel = "Play",
23
- label = "Play",
23
+ ariaLabel,
24
+ label,
24
25
  children,
25
26
  }: TopBarPlayProps = {}) {
26
27
  const play = useEditor((s) => s.play);
27
28
  const icons = useIcons();
29
+ const labels = useLabels();
30
+ const resolved = label ?? labels.play;
28
31
 
29
32
  return (
30
33
  <button
31
34
  type="button"
32
35
  className={className}
33
- aria-label={ariaLabel}
36
+ aria-label={ariaLabel ?? resolved}
34
37
  onClick={play}
35
38
  style={{ ...chromeBtnStyle(), ...style }}
36
39
  {...hoverHandlers()}
37
40
  >
38
41
  {children ?? icons.play ?? <PlayIcon size={14} />}
39
- {label}
42
+ {resolved}
40
43
  </button>
41
44
  );
42
45
  }
@@ -3,6 +3,7 @@ import { Redo2 } from "lucide-react";
3
3
  import { useEditor } from "@/lib/StoreProvider";
4
4
  import { useIcons } from "../IconContext";
5
5
  import { useReadOnly } from "../ReadOnlyContext";
6
+ import { useLabels } from "../LabelsContext";
6
7
  import { iconBtnStyle, hoverHandlers } from "./styles";
7
8
 
8
9
  /**
@@ -19,21 +20,23 @@ export interface TopBarRedoProps {
19
20
  export function Redo({
20
21
  className,
21
22
  style,
22
- ariaLabel = "Redo",
23
+ ariaLabel,
23
24
  children,
24
25
  }: TopBarRedoProps = {}) {
25
26
  const redo = useEditor((s) => s.redo);
26
27
  const canRedo = useEditor((s) => s.future.length > 0);
27
28
  const icons = useIcons();
28
29
  const readOnly = useReadOnly();
30
+ const labels = useLabels();
31
+ const resolvedAria = ariaLabel ?? labels.redo;
29
32
  if (readOnly) return null;
30
33
 
31
34
  return (
32
35
  <button
33
36
  type="button"
34
37
  className={className}
35
- title={ariaLabel}
36
- aria-label={ariaLabel}
38
+ title={resolvedAria}
39
+ aria-label={resolvedAria}
37
40
  disabled={!canRedo}
38
41
  onClick={redo}
39
42
  style={{