@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.
@@ -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={{
@@ -35,7 +35,8 @@ export function Root({
35
35
  alignItems: "center",
36
36
  padding: "0 14px",
37
37
  gap: 10,
38
- background: "var(--slidewise-bar-bg, var(--app-bg))",
38
+ background:
39
+ "var(--slidewise-bg-topbar, var(--slidewise-bar-bg, var(--app-bg)))",
39
40
  borderBottom: "1px solid var(--border)",
40
41
  boxShadow: "var(--topbar-shadow)",
41
42
  fontFamily: "Inter, system-ui, sans-serif",
@@ -4,6 +4,7 @@ import { useEditorStore } from "@/lib/StoreProvider";
4
4
  import { useHostCallbacks } from "../HostContext";
5
5
  import { useIcons } from "../IconContext";
6
6
  import { useReadOnly } from "../ReadOnlyContext";
7
+ import { useLabels } from "../LabelsContext";
7
8
  import { chromeBtnStyle, hoverHandlers } from "./styles";
8
9
 
9
10
  /**
@@ -27,7 +28,7 @@ export interface TopBarSaveProps {
27
28
  export function Save({
28
29
  className,
29
30
  style,
30
- ariaLabel = "Save",
31
+ ariaLabel,
31
32
  labels,
32
33
  children,
33
34
  }: TopBarSaveProps = {}) {
@@ -35,6 +36,7 @@ export function Save({
35
36
  const { onSave: onSaveHost } = useHostCallbacks();
36
37
  const icons = useIcons();
37
38
  const readOnly = useReadOnly();
39
+ const ctxLabels = useLabels();
38
40
  const [phase, setPhase] = useState<"idle" | "saving" | "saved">("idle");
39
41
 
40
42
  if (readOnly) return null;
@@ -59,16 +61,16 @@ export function Save({
59
61
 
60
62
  const text =
61
63
  phase === "saving"
62
- ? (labels?.saving ?? "Saving…")
64
+ ? (labels?.saving ?? ctxLabels.save.saving)
63
65
  : phase === "saved"
64
- ? (labels?.saved ?? "Saved")
65
- : (labels?.idle ?? "Save");
66
+ ? (labels?.saved ?? ctxLabels.save.saved)
67
+ : (labels?.idle ?? ctxLabels.save.idle);
66
68
 
67
69
  return (
68
70
  <button
69
71
  type="button"
70
72
  className={className}
71
- aria-label={ariaLabel}
73
+ aria-label={ariaLabel ?? ctxLabels.save.idle}
72
74
  onClick={onClick}
73
75
  style={{ ...chromeBtnStyle(), ...style }}
74
76
  {...hoverHandlers()}
@@ -2,6 +2,7 @@ import type { CSSProperties, ReactNode } from "react";
2
2
  import { Sun, Moon } from "lucide-react";
3
3
  import { useEditor } from "@/lib/StoreProvider";
4
4
  import { useIcons } from "../IconContext";
5
+ import { useLabels } from "../LabelsContext";
5
6
  import { iconBtnStyle, hoverHandlers } from "./styles";
6
7
 
7
8
  /**
@@ -24,11 +25,12 @@ export function ThemeToggle({
24
25
  const theme = useEditor((s) => s.theme);
25
26
  const toggleTheme = useEditor((s) => s.toggleTheme);
26
27
  const icons = useIcons();
28
+ const ctxLabels = useLabels();
27
29
 
28
30
  const label =
29
31
  theme === "dark"
30
- ? (labels?.toggleToLight ?? "Light mode")
31
- : (labels?.toggleToDark ?? "Dark mode");
32
+ ? (labels?.toggleToLight ?? ctxLabels.themeToggle.toLight)
33
+ : (labels?.toggleToDark ?? ctxLabels.themeToggle.toDark);
32
34
 
33
35
  return (
34
36
  <button
@@ -3,6 +3,7 @@ import { Sparkles } 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
 
7
8
  /**
8
9
  * Deck title input wrapped in the "Smart" pill. Reads + writes
@@ -23,6 +24,7 @@ export function Title({ className, style }: TopBarTitleProps = {}) {
23
24
  const setTitle = useEditor((s) => s.setTitle);
24
25
  const icons = useIcons();
25
26
  const readOnly = useReadOnly();
27
+ const labels = useLabels();
26
28
 
27
29
  return (
28
30
  <div
@@ -56,10 +58,10 @@ export function Title({ className, style }: TopBarTitleProps = {}) {
56
58
  }}
57
59
  >
58
60
  {icons.smart ?? <Sparkles size={11} />}
59
- Smart
61
+ {labels.smart}
60
62
  </span>
61
63
  <input
62
- aria-label="Deck title"
64
+ aria-label={labels.titleAriaLabel}
63
65
  value={title}
64
66
  readOnly={readOnly}
65
67
  onChange={(e) => setTitle(e.target.value)}
@@ -3,6 +3,7 @@ import { Undo2 } 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
  /**
@@ -31,21 +32,23 @@ export interface TopBarUndoProps {
31
32
  export function Undo({
32
33
  className,
33
34
  style,
34
- ariaLabel = "Undo",
35
+ ariaLabel,
35
36
  children,
36
37
  }: TopBarUndoProps = {}) {
37
38
  const undo = useEditor((s) => s.undo);
38
39
  const canUndo = useEditor((s) => s.history.length > 0);
39
40
  const icons = useIcons();
40
41
  const readOnly = useReadOnly();
42
+ const labels = useLabels();
43
+ const resolvedAria = ariaLabel ?? labels.undo;
41
44
  if (readOnly) return null;
42
45
 
43
46
  return (
44
47
  <button
45
48
  type="button"
46
49
  className={className}
47
- title={ariaLabel}
48
- aria-label={ariaLabel}
50
+ title={resolvedAria}
51
+ aria-label={resolvedAria}
49
52
  disabled={!canUndo}
50
53
  onClick={undo}
51
54
  style={{
package/src/index.ts CHANGED
@@ -46,6 +46,8 @@ export {
46
46
  useIcons,
47
47
  useReadOnly,
48
48
  useDirty,
49
+ useLabels,
50
+ useSurfaces,
49
51
  useEditor,
50
52
  useEditorStore,
51
53
  useSlides,
@@ -63,6 +65,9 @@ export {
63
65
  type SelectionSnapshot,
64
66
  type SlidewiseHostCallbacks,
65
67
  type SlidewiseIcons,
68
+ type SlidewiseLabels,
69
+ type SlidewiseSurfaces,
70
+ type ResolvedLabels,
66
71
  type RegionProps,
67
72
  type TopBarProps,
68
73
  type TopBarSlotId,