@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
|
@@ -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 ??
|
|
31
|
-
: (labels?.toggleToDark ??
|
|
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
|
-
|
|
61
|
+
{labels.smart}
|
|
60
62
|
</span>
|
|
61
63
|
<input
|
|
62
|
-
aria-label=
|
|
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
|
|
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={
|
|
48
|
-
aria-label={
|
|
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,
|
|
@@ -60,8 +62,12 @@ export {
|
|
|
60
62
|
type SlidewiseRootProps,
|
|
61
63
|
type SlidewiseRootHandle,
|
|
62
64
|
type HistoryState,
|
|
65
|
+
type SelectionSnapshot,
|
|
63
66
|
type SlidewiseHostCallbacks,
|
|
64
67
|
type SlidewiseIcons,
|
|
68
|
+
type SlidewiseLabels,
|
|
69
|
+
type SlidewiseSurfaces,
|
|
70
|
+
type ResolvedLabels,
|
|
65
71
|
type RegionProps,
|
|
66
72
|
type TopBarProps,
|
|
67
73
|
type TopBarSlotId,
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { createEditorStore } from "../store";
|
|
3
|
+
import { CURRENT_DECK_VERSION } from "../schema/migrate";
|
|
4
|
+
import type { Deck } from "../types";
|
|
5
|
+
|
|
6
|
+
function makeDeck(slideCount = 3): Deck {
|
|
7
|
+
return {
|
|
8
|
+
version: CURRENT_DECK_VERSION,
|
|
9
|
+
title: "fixture",
|
|
10
|
+
slides: Array.from({ length: slideCount }, (_, i) => ({
|
|
11
|
+
id: `s${i + 1}`,
|
|
12
|
+
background: "#FFFFFF",
|
|
13
|
+
elements: [],
|
|
14
|
+
})),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe("store: zoom actions", () => {
|
|
19
|
+
it("zoomIn multiplies the current zoom by 1.25 (clamped to 4)", () => {
|
|
20
|
+
const store = createEditorStore(makeDeck());
|
|
21
|
+
store.getState().setZoom(1);
|
|
22
|
+
store.getState().zoomIn();
|
|
23
|
+
expect(store.getState().zoom).toBeCloseTo(1.25);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("zoomOut multiplies by 0.8 (clamped to 0.1)", () => {
|
|
27
|
+
const store = createEditorStore(makeDeck());
|
|
28
|
+
store.getState().setZoom(1);
|
|
29
|
+
store.getState().zoomOut();
|
|
30
|
+
expect(store.getState().zoom).toBeCloseTo(0.8);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("setZoom clamps the zoom to the valid range", () => {
|
|
34
|
+
const store = createEditorStore(makeDeck());
|
|
35
|
+
store.getState().setZoom(999);
|
|
36
|
+
expect(store.getState().zoom).toBe(4);
|
|
37
|
+
store.getState().setZoom(0);
|
|
38
|
+
expect(store.getState().zoom).toBe(0.1);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("store: slide CRUD return ids", () => {
|
|
43
|
+
it("addSlide returns the new slide's id and inserts after the target", () => {
|
|
44
|
+
const store = createEditorStore(makeDeck(2));
|
|
45
|
+
const newId = store.getState().addSlide("s1");
|
|
46
|
+
expect(typeof newId).toBe("string");
|
|
47
|
+
const slides = store.getState().deck.slides;
|
|
48
|
+
expect(slides).toHaveLength(3);
|
|
49
|
+
expect(slides[1].id).toBe(newId);
|
|
50
|
+
expect(store.getState().currentSlideId).toBe(newId);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("addSlide() with no afterId appends at the end", () => {
|
|
54
|
+
const store = createEditorStore(makeDeck(2));
|
|
55
|
+
const newId = store.getState().addSlide();
|
|
56
|
+
expect(store.getState().deck.slides[2].id).toBe(newId);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("duplicateSlide returns the new slide id and inserts after the original", () => {
|
|
60
|
+
const store = createEditorStore(makeDeck(2));
|
|
61
|
+
const copyId = store.getState().duplicateSlide("s1");
|
|
62
|
+
expect(typeof copyId).toBe("string");
|
|
63
|
+
expect(copyId).not.toBe("s1");
|
|
64
|
+
const slides = store.getState().deck.slides;
|
|
65
|
+
expect(slides).toHaveLength(3);
|
|
66
|
+
expect(slides[1].id).toBe(copyId);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("duplicateSlide returns null when slide id is not found", () => {
|
|
70
|
+
const store = createEditorStore(makeDeck(2));
|
|
71
|
+
expect(store.getState().duplicateSlide("does-not-exist")).toBeNull();
|
|
72
|
+
expect(store.getState().deck.slides).toHaveLength(2);
|
|
73
|
+
});
|
|
74
|
+
});
|
package/src/lib/store.ts
CHANGED
|
@@ -68,12 +68,18 @@ export interface EditorState {
|
|
|
68
68
|
setTool: (t: Tool) => void;
|
|
69
69
|
setTitle: (t: string) => void;
|
|
70
70
|
setZoom: (z: number) => void;
|
|
71
|
+
/** Multiplicative zoom-in step (×1.25), clamped to the same range as setZoom. */
|
|
72
|
+
zoomIn: () => void;
|
|
73
|
+
/** Multiplicative zoom-out step (×0.8), clamped to the same range as setZoom. */
|
|
74
|
+
zoomOut: () => void;
|
|
71
75
|
setFitMode: (f: "fit" | "fill" | "manual") => void;
|
|
72
76
|
selectSlide: (id: string) => void;
|
|
73
77
|
selectElement: (id: string | null, additive?: boolean) => void;
|
|
74
78
|
clearSelection: () => void;
|
|
75
|
-
|
|
76
|
-
|
|
79
|
+
/** Returns the id of the newly inserted slide. */
|
|
80
|
+
addSlide: (afterId?: string) => string;
|
|
81
|
+
/** Returns the id of the newly inserted duplicate, or null if `id` wasn't found. */
|
|
82
|
+
duplicateSlide: (id: string) => string | null;
|
|
77
83
|
deleteSlide: (id: string) => void;
|
|
78
84
|
reorderSlide: (id: string, toIndex: number) => void;
|
|
79
85
|
addElement: (partial: ElementDraft) => string;
|
|
@@ -194,6 +200,8 @@ export function createEditorStore(initialDeck: Deck): EditorStore {
|
|
|
194
200
|
},
|
|
195
201
|
setZoom: (z) =>
|
|
196
202
|
set({ zoom: Math.max(0.1, Math.min(4, z)), fitMode: "manual" }),
|
|
203
|
+
zoomIn: () => get().setZoom(get().zoom * 1.25),
|
|
204
|
+
zoomOut: () => get().setZoom(get().zoom * 0.8),
|
|
197
205
|
setFitMode: (f) => set({ fitMode: f }),
|
|
198
206
|
|
|
199
207
|
selectSlide: (id) =>
|
|
@@ -215,8 +223,8 @@ export function createEditorStore(initialDeck: Deck): EditorStore {
|
|
|
215
223
|
|
|
216
224
|
addSlide: (afterId) => {
|
|
217
225
|
get().pushHistory();
|
|
226
|
+
const slide = blankSlide();
|
|
218
227
|
set((s) => {
|
|
219
|
-
const slide = blankSlide();
|
|
220
228
|
const slides = [...s.deck.slides];
|
|
221
229
|
const idx = afterId
|
|
222
230
|
? slides.findIndex((sl) => sl.id === afterId)
|
|
@@ -228,26 +236,28 @@ export function createEditorStore(initialDeck: Deck): EditorStore {
|
|
|
228
236
|
selectedIds: [],
|
|
229
237
|
};
|
|
230
238
|
});
|
|
239
|
+
return slide.id;
|
|
231
240
|
},
|
|
232
241
|
|
|
233
242
|
duplicateSlide: (id) => {
|
|
243
|
+
const orig = get().deck.slides.find((sl) => sl.id === id);
|
|
244
|
+
if (!orig) return null;
|
|
234
245
|
get().pushHistory();
|
|
246
|
+
const copy: Slide = {
|
|
247
|
+
...structuredClone(orig),
|
|
248
|
+
id: nanoid(8),
|
|
249
|
+
elements: orig.elements.map((e) => ({ ...e, id: nanoid(8) })),
|
|
250
|
+
};
|
|
235
251
|
set((s) => {
|
|
236
252
|
const slides = [...s.deck.slides];
|
|
237
253
|
const idx = slides.findIndex((sl) => sl.id === id);
|
|
238
|
-
if (idx < 0) return s;
|
|
239
|
-
const orig = slides[idx];
|
|
240
|
-
const copy: Slide = {
|
|
241
|
-
...structuredClone(orig),
|
|
242
|
-
id: nanoid(8),
|
|
243
|
-
elements: orig.elements.map((e) => ({ ...e, id: nanoid(8) })),
|
|
244
|
-
};
|
|
245
254
|
slides.splice(idx + 1, 0, copy);
|
|
246
255
|
return {
|
|
247
256
|
deck: { ...s.deck, slides },
|
|
248
257
|
currentSlideId: copy.id,
|
|
249
258
|
};
|
|
250
259
|
});
|
|
260
|
+
return copy.id;
|
|
251
261
|
},
|
|
252
262
|
|
|
253
263
|
deleteSlide: (id) => {
|