@textcortex/slidewise 1.3.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/dist/index.mjs +4603 -4369
- package/dist/index.mjs.map +1 -1
- package/dist/slidewise.css +1 -1
- package/package.json +1 -1
- package/src/SlidewiseEditor.css +34 -0
- package/src/SlidewiseEditor.tsx +42 -0
- package/src/SlidewiseFileEditor.tsx +123 -7
- package/src/compound/LabelsContext.tsx +121 -0
- package/src/compound/SlidewiseRoot.tsx +228 -19
- package/src/compound/SurfacesContext.tsx +128 -0
- package/src/compound/index.ts +14 -0
- package/src/compound/parts.tsx +1 -1
- package/src/compound/topbar/Export.tsx +7 -4
- package/src/compound/topbar/Play.tsx +7 -4
- package/src/compound/topbar/Redo.tsx +6 -3
- package/src/compound/topbar/Root.tsx +2 -1
- package/src/compound/topbar/Save.tsx +7 -5
- package/src/compound/topbar/ThemeToggle.tsx +4 -2
- package/src/compound/topbar/Title.tsx +4 -2
- package/src/compound/topbar/Undo.tsx +6 -3
- package/src/index.ts +6 -0
- package/src/lib/__tests__/api-extensions.test.ts +74 -0
- package/src/lib/store.ts +20 -10
package/package.json
CHANGED
package/src/SlidewiseEditor.css
CHANGED
|
@@ -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. */
|
package/src/SlidewiseEditor.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type SlidewiseRootHandle,
|
|
5
5
|
type SlidewiseRootProps,
|
|
6
6
|
type HistoryState,
|
|
7
|
+
type SelectionSnapshot,
|
|
7
8
|
} from "./compound/SlidewiseRoot";
|
|
8
9
|
import { TopBar } from "./compound/topbar";
|
|
9
10
|
import {
|
|
@@ -14,6 +15,8 @@ import {
|
|
|
14
15
|
CanvasFrame,
|
|
15
16
|
} from "./compound/parts";
|
|
16
17
|
import type { SlidewiseIcons } from "./compound/IconContext";
|
|
18
|
+
import type { SlidewiseLabels } from "./compound/LabelsContext";
|
|
19
|
+
import type { SlidewiseSurfaces } from "./compound/SurfacesContext";
|
|
17
20
|
import type { Deck } from "@/lib/types";
|
|
18
21
|
import "./SlidewiseEditor.css";
|
|
19
22
|
|
|
@@ -40,6 +43,18 @@ export interface SlidewiseEditorProps {
|
|
|
40
43
|
* Undo and Redo buttons reactively without polling `api.canUndo()`.
|
|
41
44
|
*/
|
|
42
45
|
onHistoryChange?: (state: HistoryState) => void;
|
|
46
|
+
/** Fires when the active slide changes (user click, programmatic goToSlide). */
|
|
47
|
+
onActiveSlideChange?: (slideId: string) => void;
|
|
48
|
+
/** Fires when the selected element ids change. */
|
|
49
|
+
onSelectionChange?: (selection: SelectionSnapshot) => void;
|
|
50
|
+
/** Fires when the canvas zoom level changes. */
|
|
51
|
+
onZoomChange?: (scale: number) => void;
|
|
52
|
+
/** Fires immediately before the host's `onSave` is invoked. */
|
|
53
|
+
onSaveStart?: () => void;
|
|
54
|
+
/** Fires after the host's `onSave` resolves successfully. */
|
|
55
|
+
onSaveSuccess?: () => void;
|
|
56
|
+
/** Fires when the host's `onSave` throws. The error still propagates. */
|
|
57
|
+
onSaveError?: (err: Error) => void;
|
|
43
58
|
/** Reserved for future use; not enforced yet. */
|
|
44
59
|
readOnly?: boolean;
|
|
45
60
|
/** "light" or "dark"; defaults to "light". Ignored after first render. */
|
|
@@ -59,6 +74,17 @@ export interface SlidewiseEditorProps {
|
|
|
59
74
|
* bundled lucide-react icons.
|
|
60
75
|
*/
|
|
61
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;
|
|
62
88
|
/** Extra class names appended to the editor root. */
|
|
63
89
|
className?: string;
|
|
64
90
|
/** Inline style applied to the editor root. */
|
|
@@ -97,6 +123,12 @@ export const SlidewiseEditor = forwardRef<
|
|
|
97
123
|
onExport,
|
|
98
124
|
onDirtyChange,
|
|
99
125
|
onHistoryChange,
|
|
126
|
+
onActiveSlideChange,
|
|
127
|
+
onSelectionChange,
|
|
128
|
+
onZoomChange,
|
|
129
|
+
onSaveStart,
|
|
130
|
+
onSaveSuccess,
|
|
131
|
+
onSaveError,
|
|
100
132
|
readOnly,
|
|
101
133
|
theme,
|
|
102
134
|
initialSlideId,
|
|
@@ -104,6 +136,8 @@ export const SlidewiseEditor = forwardRef<
|
|
|
104
136
|
showBottomToolbar = true,
|
|
105
137
|
fontFamily,
|
|
106
138
|
icons,
|
|
139
|
+
labels,
|
|
140
|
+
surfaces,
|
|
107
141
|
className,
|
|
108
142
|
style,
|
|
109
143
|
},
|
|
@@ -116,11 +150,19 @@ export const SlidewiseEditor = forwardRef<
|
|
|
116
150
|
onExport,
|
|
117
151
|
onDirtyChange,
|
|
118
152
|
onHistoryChange,
|
|
153
|
+
onActiveSlideChange,
|
|
154
|
+
onSelectionChange,
|
|
155
|
+
onZoomChange,
|
|
156
|
+
onSaveStart,
|
|
157
|
+
onSaveSuccess,
|
|
158
|
+
onSaveError,
|
|
119
159
|
readOnly,
|
|
120
160
|
theme,
|
|
121
161
|
initialSlideId,
|
|
122
162
|
fontFamily,
|
|
123
163
|
icons,
|
|
164
|
+
labels,
|
|
165
|
+
surfaces,
|
|
124
166
|
className,
|
|
125
167
|
style,
|
|
126
168
|
};
|
|
@@ -10,7 +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 {
|
|
13
|
+
import type { SlidewiseLabels } from "./compound/LabelsContext";
|
|
14
|
+
import type { SlidewiseSurfaces } from "./compound/SurfacesContext";
|
|
15
|
+
import { DEFAULT_LABELS } from "./compound/LabelsContext";
|
|
16
|
+
import type { HistoryState, SelectionSnapshot } from "./compound/SlidewiseRoot";
|
|
14
17
|
|
|
15
18
|
export interface SlidewiseFileEditorProps {
|
|
16
19
|
/**
|
|
@@ -55,6 +58,18 @@ export interface SlidewiseFileEditorProps {
|
|
|
55
58
|
* `api.canUndo()` / `api.canRedo()`.
|
|
56
59
|
*/
|
|
57
60
|
onHistoryChange?: (state: HistoryState) => void;
|
|
61
|
+
/** Fires when the active slide changes (user click, programmatic goToSlide). */
|
|
62
|
+
onActiveSlideChange?: (slideId: string) => void;
|
|
63
|
+
/** Fires when the selected element ids change. */
|
|
64
|
+
onSelectionChange?: (selection: SelectionSnapshot) => void;
|
|
65
|
+
/** Fires when the canvas zoom level changes. */
|
|
66
|
+
onZoomChange?: (scale: number) => void;
|
|
67
|
+
/** Fires immediately before the host's `saveBlob` is invoked. */
|
|
68
|
+
onSaveStart?: () => void;
|
|
69
|
+
/** Fires after `saveBlob` resolves successfully. */
|
|
70
|
+
onSaveSuccess?: () => void;
|
|
71
|
+
/** Fires when `saveBlob` throws. The error still propagates. */
|
|
72
|
+
onSaveError?: (err: Error) => void;
|
|
58
73
|
/**
|
|
59
74
|
* Fires when `loadBlob` or `parse` throws on mount. The default render
|
|
60
75
|
* still shows an in-editor "Could not open file" message, but hosts that
|
|
@@ -79,6 +94,17 @@ export interface SlidewiseFileEditorProps {
|
|
|
79
94
|
* editor's chrome with your own icon set.
|
|
80
95
|
*/
|
|
81
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;
|
|
82
108
|
className?: string;
|
|
83
109
|
style?: CSSProperties;
|
|
84
110
|
/**
|
|
@@ -114,6 +140,37 @@ export interface SlidewiseFileEditorApi {
|
|
|
114
140
|
* handles typical typing/drag bursts automatically.
|
|
115
141
|
*/
|
|
116
142
|
endCoalesce(): void;
|
|
143
|
+
|
|
144
|
+
/** Switch the active slide. No-op when `slideId` is not in the deck. */
|
|
145
|
+
goToSlide(slideId: string): void;
|
|
146
|
+
/** Advance to the next slide. No-op past the last slide. */
|
|
147
|
+
nextSlide(): void;
|
|
148
|
+
/** Step back to the previous slide. No-op past the first. */
|
|
149
|
+
prevSlide(): void;
|
|
150
|
+
|
|
151
|
+
/** Zoom out by one step. */
|
|
152
|
+
zoomOut(): void;
|
|
153
|
+
/** Zoom in by one step. */
|
|
154
|
+
zoomIn(): void;
|
|
155
|
+
/** Set absolute zoom (1 = 100%); clamped to [0.1, 4]. */
|
|
156
|
+
setZoom(scale: number): void;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Insert a blank slide after `afterId`, or at the end. Returns the new
|
|
160
|
+
* slide's id. The new slide becomes active.
|
|
161
|
+
*/
|
|
162
|
+
addSlide(afterId?: string): string | null;
|
|
163
|
+
/**
|
|
164
|
+
* Duplicate `slideId`. Returns the new slide's id, or `null` if not
|
|
165
|
+
* found. The duplicate becomes active.
|
|
166
|
+
*/
|
|
167
|
+
duplicateSlide(slideId: string): string | null;
|
|
168
|
+
/** Delete `slideId`. No-op when the deck would be left empty. */
|
|
169
|
+
deleteSlide(slideId: string): void;
|
|
170
|
+
|
|
171
|
+
/** Current selection snapshot (slide id + selected element ids). */
|
|
172
|
+
getSelection(): SelectionSnapshot;
|
|
173
|
+
|
|
117
174
|
/** Live deck snapshot. Hosts use this for header badges (slide count, etc.). */
|
|
118
175
|
getDeck(): Deck | null;
|
|
119
176
|
getInitialSha256(): string | null;
|
|
@@ -138,12 +195,20 @@ export const SlidewiseFileEditor = forwardRef<
|
|
|
138
195
|
onDirtyChange,
|
|
139
196
|
onLoadError,
|
|
140
197
|
onHistoryChange,
|
|
198
|
+
onActiveSlideChange,
|
|
199
|
+
onSelectionChange,
|
|
200
|
+
onZoomChange,
|
|
201
|
+
onSaveStart,
|
|
202
|
+
onSaveSuccess,
|
|
203
|
+
onSaveError,
|
|
141
204
|
theme,
|
|
142
205
|
initialSlideId,
|
|
143
206
|
showTopBar,
|
|
144
207
|
showBottomToolbar,
|
|
145
208
|
fontFamily,
|
|
146
209
|
icons,
|
|
210
|
+
labels,
|
|
211
|
+
surfaces,
|
|
147
212
|
className,
|
|
148
213
|
style,
|
|
149
214
|
parse = parsePptx,
|
|
@@ -211,6 +276,23 @@ export const SlidewiseFileEditor = forwardRef<
|
|
|
211
276
|
getHistorySize: () =>
|
|
212
277
|
editorRef.current?.getHistorySize() ?? { undo: 0, redo: 0 },
|
|
213
278
|
endCoalesce: () => editorRef.current?.endCoalesce(),
|
|
279
|
+
goToSlide: (slideId: string) => editorRef.current?.goToSlide(slideId),
|
|
280
|
+
nextSlide: () => editorRef.current?.nextSlide(),
|
|
281
|
+
prevSlide: () => editorRef.current?.prevSlide(),
|
|
282
|
+
zoomIn: () => editorRef.current?.zoomIn(),
|
|
283
|
+
zoomOut: () => editorRef.current?.zoomOut(),
|
|
284
|
+
setZoom: (scale: number) => editorRef.current?.setZoom(scale),
|
|
285
|
+
addSlide: (afterId?: string) =>
|
|
286
|
+
editorRef.current?.addSlide(afterId) ?? null,
|
|
287
|
+
duplicateSlide: (slideId: string) =>
|
|
288
|
+
editorRef.current?.duplicateSlide(slideId) ?? null,
|
|
289
|
+
deleteSlide: (slideId: string) =>
|
|
290
|
+
editorRef.current?.deleteSlide(slideId),
|
|
291
|
+
getSelection: () =>
|
|
292
|
+
editorRef.current?.getSelection() ?? {
|
|
293
|
+
slideId: state.deck.slides[0]?.id ?? "",
|
|
294
|
+
elementIds: [],
|
|
295
|
+
},
|
|
214
296
|
getDeck: () => editorRef.current?.getDeck() ?? state.deck,
|
|
215
297
|
getInitialSha256: () => initialSha256,
|
|
216
298
|
};
|
|
@@ -241,6 +323,24 @@ export const SlidewiseFileEditor = forwardRef<
|
|
|
241
323
|
getHistorySize: () =>
|
|
242
324
|
editorRef.current?.getHistorySize() ?? { undo: 0, redo: 0 },
|
|
243
325
|
endCoalesce: () => editorRef.current?.endCoalesce(),
|
|
326
|
+
goToSlide: (slideId: string) => editorRef.current?.goToSlide(slideId),
|
|
327
|
+
nextSlide: () => editorRef.current?.nextSlide(),
|
|
328
|
+
prevSlide: () => editorRef.current?.prevSlide(),
|
|
329
|
+
zoomIn: () => editorRef.current?.zoomIn(),
|
|
330
|
+
zoomOut: () => editorRef.current?.zoomOut(),
|
|
331
|
+
setZoom: (scale: number) => editorRef.current?.setZoom(scale),
|
|
332
|
+
addSlide: (afterId?: string) =>
|
|
333
|
+
editorRef.current?.addSlide(afterId) ?? null,
|
|
334
|
+
duplicateSlide: (slideId: string) =>
|
|
335
|
+
editorRef.current?.duplicateSlide(slideId) ?? null,
|
|
336
|
+
deleteSlide: (slideId: string) =>
|
|
337
|
+
editorRef.current?.deleteSlide(slideId),
|
|
338
|
+
getSelection: () =>
|
|
339
|
+
editorRef.current?.getSelection() ?? {
|
|
340
|
+
slideId:
|
|
341
|
+
state.status === "ready" ? state.deck.slides[0]?.id ?? "" : "",
|
|
342
|
+
elementIds: [],
|
|
343
|
+
},
|
|
244
344
|
getDeck: () =>
|
|
245
345
|
state.status === "ready"
|
|
246
346
|
? editorRef.current?.getDeck() ?? state.deck
|
|
@@ -253,15 +353,18 @@ export const SlidewiseFileEditor = forwardRef<
|
|
|
253
353
|
if (state.status === "loading") {
|
|
254
354
|
return (
|
|
255
355
|
<div style={{ ...frameStyle, ...style }} className={className}>
|
|
256
|
-
<div style={messageStyle}>
|
|
356
|
+
<div style={messageStyle}>
|
|
357
|
+
{labels?.fileLoading ?? DEFAULT_LABELS.fileLoading}
|
|
358
|
+
</div>
|
|
257
359
|
</div>
|
|
258
360
|
);
|
|
259
361
|
}
|
|
260
362
|
if (state.status === "error") {
|
|
363
|
+
const fmt = labels?.fileLoadError ?? DEFAULT_LABELS.fileLoadError;
|
|
261
364
|
return (
|
|
262
365
|
<div style={{ ...frameStyle, ...style }} className={className}>
|
|
263
366
|
<div style={{ ...messageStyle, color: "#E8504C" }}>
|
|
264
|
-
|
|
367
|
+
{fmt(state.error.message)}
|
|
265
368
|
</div>
|
|
266
369
|
</div>
|
|
267
370
|
);
|
|
@@ -279,6 +382,8 @@ export const SlidewiseFileEditor = forwardRef<
|
|
|
279
382
|
showBottomToolbar={showBottomToolbar}
|
|
280
383
|
fontFamily={fontFamily}
|
|
281
384
|
icons={icons}
|
|
385
|
+
labels={labels}
|
|
386
|
+
surfaces={surfaces}
|
|
282
387
|
onChange={(next) => {
|
|
283
388
|
onChangeRef.current?.(next);
|
|
284
389
|
}}
|
|
@@ -287,12 +392,22 @@ export const SlidewiseFileEditor = forwardRef<
|
|
|
287
392
|
onDirtyChangeRef.current?.(d);
|
|
288
393
|
}}
|
|
289
394
|
onHistoryChange={onHistoryChange}
|
|
395
|
+
onActiveSlideChange={onActiveSlideChange}
|
|
396
|
+
onSelectionChange={onSelectionChange}
|
|
397
|
+
onZoomChange={onZoomChange}
|
|
398
|
+
onSaveStart={onSaveStart}
|
|
399
|
+
onSaveSuccess={onSaveSuccess}
|
|
400
|
+
onSaveError={onSaveError}
|
|
290
401
|
onSave={async (next) => {
|
|
291
402
|
const blob = await serialize(next);
|
|
292
403
|
await saveBlob(blob);
|
|
293
404
|
}}
|
|
294
405
|
/>
|
|
295
|
-
{dirty &&
|
|
406
|
+
{dirty && (
|
|
407
|
+
<UnsavedBadge
|
|
408
|
+
label={labels?.unsavedBadge ?? DEFAULT_LABELS.unsavedBadge}
|
|
409
|
+
/>
|
|
410
|
+
)}
|
|
296
411
|
</div>
|
|
297
412
|
);
|
|
298
413
|
});
|
|
@@ -313,7 +428,7 @@ const messageStyle: CSSProperties = {
|
|
|
313
428
|
color: "#5b6178",
|
|
314
429
|
};
|
|
315
430
|
|
|
316
|
-
function UnsavedBadge() {
|
|
431
|
+
function UnsavedBadge({ label }: { label: string }) {
|
|
317
432
|
return (
|
|
318
433
|
<div
|
|
319
434
|
style={{
|
|
@@ -321,7 +436,8 @@ function UnsavedBadge() {
|
|
|
321
436
|
top: 12,
|
|
322
437
|
right: 12,
|
|
323
438
|
padding: "4px 10px",
|
|
324
|
-
background:
|
|
439
|
+
background:
|
|
440
|
+
"var(--slidewise-bg-unsaved-badge, rgba(232, 80, 76, 0.12))",
|
|
325
441
|
color: "#E8504C",
|
|
326
442
|
borderRadius: 999,
|
|
327
443
|
fontFamily: "Inter, system-ui, sans-serif",
|
|
@@ -332,7 +448,7 @@ function UnsavedBadge() {
|
|
|
332
448
|
pointerEvents: "none",
|
|
333
449
|
}}
|
|
334
450
|
>
|
|
335
|
-
|
|
451
|
+
{label}
|
|
336
452
|
</div>
|
|
337
453
|
);
|
|
338
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
|
+
}
|