@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
|
@@ -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
|
/**
|
|
@@ -43,6 +49,18 @@ export interface SlidewiseRootProps {
|
|
|
43
49
|
* "Undo"/"Redo" button enabled state without polling `canUndo()`/`canRedo()`.
|
|
44
50
|
*/
|
|
45
51
|
onHistoryChange?: (state: HistoryState) => void;
|
|
52
|
+
/** Fires when the active slide changes (user click, programmatic goToSlide). */
|
|
53
|
+
onActiveSlideChange?: (slideId: string) => void;
|
|
54
|
+
/** Fires when the selected element ids change. */
|
|
55
|
+
onSelectionChange?: (selection: SelectionSnapshot) => void;
|
|
56
|
+
/** Fires when the canvas zoom level changes. */
|
|
57
|
+
onZoomChange?: (scale: number) => void;
|
|
58
|
+
/** Fires immediately before the host's `onSave` is invoked. */
|
|
59
|
+
onSaveStart?: () => void;
|
|
60
|
+
/** Fires after the host's `onSave` resolves successfully. */
|
|
61
|
+
onSaveSuccess?: () => void;
|
|
62
|
+
/** Fires when the host's `onSave` throws. The error still propagates. */
|
|
63
|
+
onSaveError?: (err: Error) => void;
|
|
46
64
|
/**
|
|
47
65
|
* Hide editing affordances (save / undo / redo) and disable canvas
|
|
48
66
|
* mutations. Use this when the host viewer doesn't have write access.
|
|
@@ -60,6 +78,22 @@ export interface SlidewiseRootProps {
|
|
|
60
78
|
* the bundled lucide icons.
|
|
61
79
|
*/
|
|
62
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;
|
|
63
97
|
/** Extra class names appended to the root. */
|
|
64
98
|
className?: string;
|
|
65
99
|
/** Inline style applied to the root. */
|
|
@@ -74,6 +108,12 @@ export interface HistoryState {
|
|
|
74
108
|
redoSize: number;
|
|
75
109
|
}
|
|
76
110
|
|
|
111
|
+
/** Snapshot of the current selection, scoped to the active slide. */
|
|
112
|
+
export interface SelectionSnapshot {
|
|
113
|
+
slideId: string;
|
|
114
|
+
elementIds: string[];
|
|
115
|
+
}
|
|
116
|
+
|
|
77
117
|
export interface SlidewiseRootHandle {
|
|
78
118
|
play(): void;
|
|
79
119
|
stop(): void;
|
|
@@ -92,6 +132,44 @@ export interface SlidewiseRootHandle {
|
|
|
92
132
|
* window handles typical typing/drag bursts.
|
|
93
133
|
*/
|
|
94
134
|
endCoalesce(): void;
|
|
135
|
+
|
|
136
|
+
// ---- Navigation ----
|
|
137
|
+
/** Switch the active slide. No-op when `slideId` is not in the deck. */
|
|
138
|
+
goToSlide(slideId: string): void;
|
|
139
|
+
/** Advance to the slide after the current one. No-op past the last slide. */
|
|
140
|
+
nextSlide(): void;
|
|
141
|
+
/** Step back to the slide before the current one. No-op past the first. */
|
|
142
|
+
prevSlide(): void;
|
|
143
|
+
|
|
144
|
+
// ---- Zoom ----
|
|
145
|
+
/** Zoom out by one step (×0.8), clamped to the editor's min zoom. */
|
|
146
|
+
zoomOut(): void;
|
|
147
|
+
/** Zoom in by one step (×1.25), clamped to the editor's max zoom. */
|
|
148
|
+
zoomIn(): void;
|
|
149
|
+
/** Set the absolute zoom (1 = 100%); clamped to [0.1, 4]. */
|
|
150
|
+
setZoom(scale: number): void;
|
|
151
|
+
|
|
152
|
+
// ---- Slide CRUD ----
|
|
153
|
+
/**
|
|
154
|
+
* Insert a blank slide after `afterId`, or at the end if `afterId` is
|
|
155
|
+
* omitted. Returns the new slide's id. The new slide becomes active.
|
|
156
|
+
*/
|
|
157
|
+
addSlide(afterId?: string): string;
|
|
158
|
+
/**
|
|
159
|
+
* Insert a copy of `slideId` immediately after it. Returns the new
|
|
160
|
+
* slide's id, or `null` if `slideId` wasn't found. The copy becomes
|
|
161
|
+
* active.
|
|
162
|
+
*/
|
|
163
|
+
duplicateSlide(slideId: string): string | null;
|
|
164
|
+
/**
|
|
165
|
+
* Delete a slide. No-op when the deck would be left with zero slides.
|
|
166
|
+
*/
|
|
167
|
+
deleteSlide(slideId: string): void;
|
|
168
|
+
|
|
169
|
+
// ---- Selection ----
|
|
170
|
+
/** Current selection snapshot (slide id + selected element ids). */
|
|
171
|
+
getSelection(): SelectionSnapshot;
|
|
172
|
+
|
|
95
173
|
getDeck(): Deck;
|
|
96
174
|
isDirty(): boolean;
|
|
97
175
|
resetDirty(): void;
|
|
@@ -124,11 +202,19 @@ function RootInner({
|
|
|
124
202
|
onExport,
|
|
125
203
|
onDirtyChange,
|
|
126
204
|
onHistoryChange: props_onHistoryChange,
|
|
205
|
+
onActiveSlideChange,
|
|
206
|
+
onSelectionChange,
|
|
207
|
+
onZoomChange,
|
|
208
|
+
onSaveStart,
|
|
209
|
+
onSaveSuccess,
|
|
210
|
+
onSaveError,
|
|
127
211
|
readOnly = false,
|
|
128
212
|
theme,
|
|
129
213
|
initialSlideId,
|
|
130
214
|
fontFamily,
|
|
131
215
|
icons,
|
|
216
|
+
labels,
|
|
217
|
+
surfaces,
|
|
132
218
|
className,
|
|
133
219
|
style,
|
|
134
220
|
children,
|
|
@@ -145,6 +231,12 @@ function RootInner({
|
|
|
145
231
|
const onSaveRef = useRef(onSave);
|
|
146
232
|
const onExportRef = useRef(onExport);
|
|
147
233
|
const onHistoryChangeRef = useRef(props_onHistoryChange);
|
|
234
|
+
const onActiveSlideChangeRef = useRef(onActiveSlideChange);
|
|
235
|
+
const onSelectionChangeRef = useRef(onSelectionChange);
|
|
236
|
+
const onZoomChangeRef = useRef(onZoomChange);
|
|
237
|
+
const onSaveStartRef = useRef(onSaveStart);
|
|
238
|
+
const onSaveSuccessRef = useRef(onSaveSuccess);
|
|
239
|
+
const onSaveErrorRef = useRef(onSaveError);
|
|
148
240
|
|
|
149
241
|
useEffect(() => {
|
|
150
242
|
onChangeRef.current = onChange;
|
|
@@ -152,7 +244,25 @@ function RootInner({
|
|
|
152
244
|
onSaveRef.current = onSave;
|
|
153
245
|
onExportRef.current = onExport;
|
|
154
246
|
onHistoryChangeRef.current = props_onHistoryChange;
|
|
155
|
-
|
|
247
|
+
onActiveSlideChangeRef.current = onActiveSlideChange;
|
|
248
|
+
onSelectionChangeRef.current = onSelectionChange;
|
|
249
|
+
onZoomChangeRef.current = onZoomChange;
|
|
250
|
+
onSaveStartRef.current = onSaveStart;
|
|
251
|
+
onSaveSuccessRef.current = onSaveSuccess;
|
|
252
|
+
onSaveErrorRef.current = onSaveError;
|
|
253
|
+
}, [
|
|
254
|
+
onChange,
|
|
255
|
+
onDirtyChange,
|
|
256
|
+
onSave,
|
|
257
|
+
onExport,
|
|
258
|
+
props_onHistoryChange,
|
|
259
|
+
onActiveSlideChange,
|
|
260
|
+
onSelectionChange,
|
|
261
|
+
onZoomChange,
|
|
262
|
+
onSaveStart,
|
|
263
|
+
onSaveSuccess,
|
|
264
|
+
onSaveError,
|
|
265
|
+
]);
|
|
156
266
|
|
|
157
267
|
useEffect(() => {
|
|
158
268
|
if (theme) {
|
|
@@ -207,6 +317,29 @@ function RootInner({
|
|
|
207
317
|
redoSize: nextFut,
|
|
208
318
|
});
|
|
209
319
|
}
|
|
320
|
+
|
|
321
|
+
// Active slide changes (click in rail, programmatic goToSlide).
|
|
322
|
+
if (state.currentSlideId !== prev.currentSlideId) {
|
|
323
|
+
onActiveSlideChangeRef.current?.(state.currentSlideId);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Selection — shallow compare ids since the array is rebuilt on
|
|
327
|
+
// every selectElement call. Same slideId + same ids = no emit.
|
|
328
|
+
if (
|
|
329
|
+
state.selectedIds !== prev.selectedIds &&
|
|
330
|
+
!shallowEqualIds(state.selectedIds, prev.selectedIds)
|
|
331
|
+
) {
|
|
332
|
+
onSelectionChangeRef.current?.({
|
|
333
|
+
slideId: state.currentSlideId,
|
|
334
|
+
elementIds: state.selectedIds,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Zoom.
|
|
339
|
+
if (state.zoom !== prev.zoom) {
|
|
340
|
+
onZoomChangeRef.current?.(state.zoom);
|
|
341
|
+
}
|
|
342
|
+
|
|
210
343
|
if (state.deck === prev.deck) return;
|
|
211
344
|
onChangeRef.current?.(state.deck);
|
|
212
345
|
const nextDirty = state.deck !== savedDeckRef.current;
|
|
@@ -239,12 +372,54 @@ function RootInner({
|
|
|
239
372
|
return { undo: s.history.length, redo: s.future.length };
|
|
240
373
|
},
|
|
241
374
|
endCoalesce: () => store.getState().endCoalesce(),
|
|
375
|
+
|
|
376
|
+
goToSlide: (slideId: string) => {
|
|
377
|
+
const s = store.getState();
|
|
378
|
+
if (s.deck.slides.some((sl) => sl.id === slideId)) {
|
|
379
|
+
s.selectSlide(slideId);
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
nextSlide: () => {
|
|
383
|
+
const s = store.getState();
|
|
384
|
+
const idx = s.deck.slides.findIndex(
|
|
385
|
+
(sl) => sl.id === s.currentSlideId
|
|
386
|
+
);
|
|
387
|
+
const next = s.deck.slides[idx + 1];
|
|
388
|
+
if (next) s.selectSlide(next.id);
|
|
389
|
+
},
|
|
390
|
+
prevSlide: () => {
|
|
391
|
+
const s = store.getState();
|
|
392
|
+
const idx = s.deck.slides.findIndex(
|
|
393
|
+
(sl) => sl.id === s.currentSlideId
|
|
394
|
+
);
|
|
395
|
+
const prev = s.deck.slides[idx - 1];
|
|
396
|
+
if (prev) s.selectSlide(prev.id);
|
|
397
|
+
},
|
|
398
|
+
|
|
399
|
+
zoomIn: () => store.getState().zoomIn(),
|
|
400
|
+
zoomOut: () => store.getState().zoomOut(),
|
|
401
|
+
setZoom: (scale: number) => store.getState().setZoom(scale),
|
|
402
|
+
|
|
403
|
+
addSlide: (afterId?: string) => store.getState().addSlide(afterId),
|
|
404
|
+
duplicateSlide: (slideId: string) =>
|
|
405
|
+
store.getState().duplicateSlide(slideId),
|
|
406
|
+
deleteSlide: (slideId: string) => store.getState().deleteSlide(slideId),
|
|
407
|
+
|
|
408
|
+
getSelection: (): SelectionSnapshot => {
|
|
409
|
+
const s = store.getState();
|
|
410
|
+
return {
|
|
411
|
+
slideId: s.currentSlideId,
|
|
412
|
+
elementIds: [...s.selectedIds],
|
|
413
|
+
};
|
|
414
|
+
},
|
|
415
|
+
|
|
242
416
|
getDeck: () => store.getState().deck,
|
|
243
417
|
isDirty: () => dirtyRef.current,
|
|
244
418
|
resetDirty: () => {
|
|
245
419
|
savedDeckRef.current = store.getState().deck;
|
|
246
420
|
if (dirtyRef.current) {
|
|
247
421
|
dirtyRef.current = false;
|
|
422
|
+
setDirty(false);
|
|
248
423
|
onDirtyChangeRef.current?.(false);
|
|
249
424
|
}
|
|
250
425
|
},
|
|
@@ -252,33 +427,58 @@ function RootInner({
|
|
|
252
427
|
[store]
|
|
253
428
|
);
|
|
254
429
|
|
|
255
|
-
// Wrap host save with
|
|
256
|
-
//
|
|
430
|
+
// Wrap host save with:
|
|
431
|
+
// - onSaveStart / onSaveSuccess / onSaveError lifecycle hooks
|
|
432
|
+
// - dirty-flag reset on success
|
|
433
|
+
// The error still propagates so TopBar.Save's local "Saving…" → "idle"
|
|
434
|
+
// transition kicks in correctly.
|
|
257
435
|
const wrappedSave = onSave
|
|
258
436
|
? async (d: Deck) => {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
437
|
+
onSaveStartRef.current?.();
|
|
438
|
+
try {
|
|
439
|
+
await onSaveRef.current!(d);
|
|
440
|
+
savedDeckRef.current = d;
|
|
441
|
+
if (dirtyRef.current) {
|
|
442
|
+
dirtyRef.current = false;
|
|
443
|
+
setDirty(false);
|
|
444
|
+
onDirtyChangeRef.current?.(false);
|
|
445
|
+
}
|
|
446
|
+
onSaveSuccessRef.current?.();
|
|
447
|
+
} catch (err) {
|
|
448
|
+
onSaveErrorRef.current?.(
|
|
449
|
+
err instanceof Error ? err : new Error(String(err))
|
|
450
|
+
);
|
|
451
|
+
throw err;
|
|
264
452
|
}
|
|
265
453
|
}
|
|
266
454
|
: undefined;
|
|
267
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
|
+
|
|
268
464
|
return (
|
|
269
465
|
<ReadOnlyProvider readOnly={readOnly}>
|
|
270
466
|
<IconProvider icons={icons ?? {}}>
|
|
271
|
-
<
|
|
272
|
-
<
|
|
273
|
-
<
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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>
|
|
282
482
|
</IconProvider>
|
|
283
483
|
</ReadOnlyProvider>
|
|
284
484
|
);
|
|
@@ -330,3 +530,12 @@ function RootShell({
|
|
|
330
530
|
</div>
|
|
331
531
|
);
|
|
332
532
|
}
|
|
533
|
+
|
|
534
|
+
function shallowEqualIds(a: readonly string[], b: readonly string[]): boolean {
|
|
535
|
+
if (a === b) return true;
|
|
536
|
+
if (a.length !== b.length) return false;
|
|
537
|
+
for (let i = 0; i < a.length; i++) {
|
|
538
|
+
if (a[i] !== b[i]) return false;
|
|
539
|
+
}
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
@@ -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
|
+
}
|
package/src/compound/index.ts
CHANGED
|
@@ -37,6 +37,7 @@ export {
|
|
|
37
37
|
type SlidewiseRootProps,
|
|
38
38
|
type SlidewiseRootHandle,
|
|
39
39
|
type HistoryState,
|
|
40
|
+
type SelectionSnapshot,
|
|
40
41
|
} from "./SlidewiseRoot";
|
|
41
42
|
export {
|
|
42
43
|
SlideRail,
|
|
@@ -73,6 +74,19 @@ export {
|
|
|
73
74
|
} from "./IconContext";
|
|
74
75
|
export { ReadOnlyProvider, useReadOnly } from "./ReadOnlyContext";
|
|
75
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";
|
|
76
90
|
|
|
77
91
|
/**
|
|
78
92
|
* Store hooks. Use these from host components anywhere under
|
package/src/compound/parts.tsx
CHANGED
|
@@ -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
|
|
28
|
-
label
|
|
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
|
-
{
|
|
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
|
|
23
|
-
label
|
|
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
|
-
{
|
|
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
|
|
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={
|
|
36
|
-
aria-label={
|
|
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:
|
|
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
|
|
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 ??
|
|
64
|
+
? (labels?.saving ?? ctxLabels.save.saving)
|
|
63
65
|
: phase === "saved"
|
|
64
|
-
? (labels?.saved ??
|
|
65
|
-
: (labels?.idle ??
|
|
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()}
|