@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.
@@ -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,
@@ -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
- addSlide: (afterId?: string) => void;
76
- duplicateSlide: (id: string) => void;
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) => {