@textcortex/slidewise 1.0.1 → 1.2.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.
Files changed (53) hide show
  1. package/dist/index.mjs +8085 -7881
  2. package/dist/index.mjs.map +1 -1
  3. package/dist/slidewise.css +1 -1
  4. package/package.json +4 -19
  5. package/src/SlidewiseEditor.css +121 -4
  6. package/src/SlidewiseEditor.tsx +93 -165
  7. package/src/SlidewiseFileEditor.tsx +109 -11
  8. package/src/components/editor/TopBar.tsx +37 -24
  9. package/src/compound/HostContext.tsx +29 -0
  10. package/src/compound/IconContext.tsx +42 -0
  11. package/src/compound/ReadOnlyContext.tsx +23 -0
  12. package/src/compound/SlidewiseRoot.tsx +325 -0
  13. package/src/compound/index.ts +51 -0
  14. package/src/compound/parts.tsx +160 -0
  15. package/src/css.d.ts +4 -0
  16. package/src/index.ts +43 -0
  17. package/src/lib/__tests__/history.test.ts +164 -0
  18. package/src/lib/store.ts +81 -4
  19. package/README.md +0 -112
  20. package/dist/file.svg +0 -1
  21. package/dist/globe.svg +0 -1
  22. package/dist/types/SlidewiseEditor.d.ts +0 -47
  23. package/dist/types/SlidewiseFileEditor.d.ts +0 -54
  24. package/dist/types/components/editor/BottomToolbar.d.ts +0 -1
  25. package/dist/types/components/editor/Canvas.d.ts +0 -1
  26. package/dist/types/components/editor/Editor.d.ts +0 -8
  27. package/dist/types/components/editor/ElementView.d.ts +0 -6
  28. package/dist/types/components/editor/FloatingToolbar.d.ts +0 -6
  29. package/dist/types/components/editor/GridView.d.ts +0 -1
  30. package/dist/types/components/editor/PlayMode.d.ts +0 -1
  31. package/dist/types/components/editor/SelectionFrame.d.ts +0 -8
  32. package/dist/types/components/editor/SlideRail.d.ts +0 -1
  33. package/dist/types/components/editor/SlideView.d.ts +0 -5
  34. package/dist/types/components/editor/TopBar.d.ts +0 -7
  35. package/dist/types/index.d.ts +0 -7
  36. package/dist/types/lib/StoreProvider.d.ts +0 -8
  37. package/dist/types/lib/fonts.d.ts +0 -9
  38. package/dist/types/lib/pptx/deckToPptx.d.ts +0 -9
  39. package/dist/types/lib/pptx/index.d.ts +0 -3
  40. package/dist/types/lib/pptx/pptxToDeck.d.ts +0 -18
  41. package/dist/types/lib/pptx/types.d.ts +0 -15
  42. package/dist/types/lib/pptx/units.d.ts +0 -25
  43. package/dist/types/lib/schema/migrate.d.ts +0 -25
  44. package/dist/types/lib/seed.d.ts +0 -2
  45. package/dist/types/lib/store.d.ts +0 -55
  46. package/dist/types/lib/types.d.ts +0 -141
  47. package/dist/window.svg +0 -1
  48. package/src/App.tsx +0 -261
  49. package/src/components/editor/Editor.tsx +0 -53
  50. package/src/index.css +0 -13
  51. package/src/lib/seed.ts +0 -777
  52. package/src/main.tsx +0 -10
  53. package/src/vite-env.d.ts +0 -3
@@ -9,6 +9,8 @@ import {
9
9
  import { SlidewiseEditor, type SlidewiseEditorHandle } from "./SlidewiseEditor";
10
10
  import { parsePptx, serializeDeck } from "@/lib/pptx";
11
11
  import type { Deck } from "@/lib/types";
12
+ import type { SlidewiseIcons } from "./compound/IconContext";
13
+ import type { HistoryState } from "./compound/SlidewiseRoot";
12
14
 
13
15
  export interface SlidewiseFileEditorProps {
14
16
  /**
@@ -23,7 +25,11 @@ export interface SlidewiseFileEditorProps {
23
25
  * Called when `save()` is invoked on the imperative API.
24
26
  */
25
27
  saveBlob: (blob: Blob) => Promise<void>;
26
- /** Disables editing affordances (TODO: not yet enforced). */
28
+ /**
29
+ * When `false`, the top bar's save / undo / redo buttons are hidden and
30
+ * the title input is read-only. Mirrors the host's "viewer doesn't have
31
+ * write access" state. Defaults to `true`.
32
+ */
27
33
  editable?: boolean;
28
34
  /**
29
35
  * The sha256 of the file's contents at load time, if the host wants to do
@@ -36,7 +42,43 @@ export interface SlidewiseFileEditorProps {
36
42
  * editor is mounted. Called with `null` on unmount.
37
43
  */
38
44
  onEditorApiChange?: (api: SlidewiseFileEditorApi | null) => void;
45
+ /** Fires after every committed mutation. Mirrors `SlidewiseEditor.onChange`. */
46
+ onChange?: (deck: Deck) => void;
47
+ /**
48
+ * Fires reactively when the dirty flag flips. Use this instead of polling
49
+ * `api.isDirty()` for "unsaved changes" UI.
50
+ */
51
+ onDirtyChange?: (dirty: boolean) => void;
52
+ /**
53
+ * Fires whenever the undo/redo stack depths change. Use this to enable/
54
+ * disable host-rendered Undo/Redo buttons reactively without polling
55
+ * `api.canUndo()` / `api.canRedo()`.
56
+ */
57
+ onHistoryChange?: (state: HistoryState) => void;
58
+ /**
59
+ * Fires when `loadBlob` or `parse` throws on mount. The default render
60
+ * still shows an in-editor "Could not open file" message, but hosts that
61
+ * want to surface their own error UI can replace it via this callback.
62
+ */
63
+ onLoadError?: (err: Error) => void;
39
64
  theme?: "light" | "dark";
65
+ /** Slide id to land on; falls back to the first. */
66
+ initialSlideId?: string;
67
+ /** Render the built-in top bar. Default `true`. */
68
+ showTopBar?: boolean;
69
+ /** Render the floating bottom toolbar. Default `true`. */
70
+ showBottomToolbar?: boolean;
71
+ /**
72
+ * Override the bundled Geist font; sets `--font-geist-sans` on the editor
73
+ * root.
74
+ */
75
+ fontFamily?: string;
76
+ /**
77
+ * Per-action icon overrides. Pass a ReactNode for any of `undo`, `redo`,
78
+ * `save`, `play`, `themeLight`, `themeDark`, `export`, `smart` to skin the
79
+ * editor's chrome with your own icon set.
80
+ */
81
+ icons?: SlidewiseIcons;
40
82
  className?: string;
41
83
  style?: CSSProperties;
42
84
  /**
@@ -59,6 +101,21 @@ export interface SlidewiseFileEditorApi {
59
101
  stop(): void;
60
102
  undo(): void;
61
103
  redo(): void;
104
+ /** True iff there's at least one snapshot to undo back to. */
105
+ canUndo(): boolean;
106
+ /** True iff there's at least one snapshot to redo forward to. */
107
+ canRedo(): boolean;
108
+ /** Current undo/redo stack depths. */
109
+ getHistorySize(): { undo: number; redo: number };
110
+ /**
111
+ * End the in-flight coalesce burst. Call on natural commit boundaries
112
+ * (mouseup after drag, blur on a text input) so the next mutation starts
113
+ * a fresh history step. Most hosts won't need this — a 500ms idle window
114
+ * handles typical typing/drag bursts automatically.
115
+ */
116
+ endCoalesce(): void;
117
+ /** Live deck snapshot. Hosts use this for header badges (slide count, etc.). */
118
+ getDeck(): Deck | null;
62
119
  getInitialSha256(): string | null;
63
120
  }
64
121
 
@@ -74,9 +131,19 @@ export const SlidewiseFileEditor = forwardRef<
74
131
  {
75
132
  loadBlob,
76
133
  saveBlob,
134
+ editable = true,
77
135
  initialSha256 = null,
78
136
  onEditorApiChange,
137
+ onChange,
138
+ onDirtyChange,
139
+ onLoadError,
140
+ onHistoryChange,
79
141
  theme,
142
+ initialSlideId,
143
+ showTopBar,
144
+ showBottomToolbar,
145
+ fontFamily,
146
+ icons,
80
147
  className,
81
148
  style,
82
149
  parse = parsePptx,
@@ -88,10 +155,16 @@ export const SlidewiseFileEditor = forwardRef<
88
155
  const editorRef = useRef<SlidewiseEditorHandle>(null);
89
156
  const [dirty, setDirty] = useState(false);
90
157
  const apiCallbackRef = useRef(onEditorApiChange);
158
+ const onChangeRef = useRef(onChange);
159
+ const onDirtyChangeRef = useRef(onDirtyChange);
160
+ const onLoadErrorRef = useRef(onLoadError);
91
161
 
92
162
  useEffect(() => {
93
163
  apiCallbackRef.current = onEditorApiChange;
94
- }, [onEditorApiChange]);
164
+ onChangeRef.current = onChange;
165
+ onDirtyChangeRef.current = onDirtyChange;
166
+ onLoadErrorRef.current = onLoadError;
167
+ }, [onEditorApiChange, onChange, onDirtyChange, onLoadError]);
95
168
 
96
169
  // Load file once on mount.
97
170
  useEffect(() => {
@@ -103,12 +176,10 @@ export const SlidewiseFileEditor = forwardRef<
103
176
  const deck = await parse(blob);
104
177
  if (!cancelled) setState({ status: "ready", deck });
105
178
  } catch (err) {
106
- if (!cancelled) {
107
- setState({
108
- status: "error",
109
- error: err instanceof Error ? err : new Error(String(err)),
110
- });
111
- }
179
+ if (cancelled) return;
180
+ const error = err instanceof Error ? err : new Error(String(err));
181
+ setState({ status: "error", error });
182
+ onLoadErrorRef.current?.(error);
112
183
  }
113
184
  })();
114
185
  return () => {
@@ -125,8 +196,7 @@ export const SlidewiseFileEditor = forwardRef<
125
196
 
126
197
  const api: SlidewiseFileEditorApi = {
127
198
  save: async () => {
128
- const current =
129
- editorRef.current?.getDeck() ?? state.deck;
199
+ const current = editorRef.current?.getDeck() ?? state.deck;
130
200
  const blob = await serialize(current);
131
201
  await saveBlob(blob);
132
202
  editorRef.current?.resetDirty();
@@ -136,6 +206,12 @@ export const SlidewiseFileEditor = forwardRef<
136
206
  stop: () => editorRef.current?.stop(),
137
207
  undo: () => editorRef.current?.undo(),
138
208
  redo: () => editorRef.current?.redo(),
209
+ canUndo: () => editorRef.current?.canUndo() ?? false,
210
+ canRedo: () => editorRef.current?.canRedo() ?? false,
211
+ getHistorySize: () =>
212
+ editorRef.current?.getHistorySize() ?? { undo: 0, redo: 0 },
213
+ endCoalesce: () => editorRef.current?.endCoalesce(),
214
+ getDeck: () => editorRef.current?.getDeck() ?? state.deck,
139
215
  getInitialSha256: () => initialSha256,
140
216
  };
141
217
 
@@ -160,6 +236,15 @@ export const SlidewiseFileEditor = forwardRef<
160
236
  stop: () => editorRef.current?.stop(),
161
237
  undo: () => editorRef.current?.undo(),
162
238
  redo: () => editorRef.current?.redo(),
239
+ canUndo: () => editorRef.current?.canUndo() ?? false,
240
+ canRedo: () => editorRef.current?.canRedo() ?? false,
241
+ getHistorySize: () =>
242
+ editorRef.current?.getHistorySize() ?? { undo: 0, redo: 0 },
243
+ endCoalesce: () => editorRef.current?.endCoalesce(),
244
+ getDeck: () =>
245
+ state.status === "ready"
246
+ ? editorRef.current?.getDeck() ?? state.deck
247
+ : null,
163
248
  getInitialSha256: () => initialSha256,
164
249
  }),
165
250
  [state, serialize, saveBlob, initialSha256]
@@ -188,7 +273,20 @@ export const SlidewiseFileEditor = forwardRef<
188
273
  ref={editorRef}
189
274
  deck={state.deck}
190
275
  theme={theme}
191
- onDirtyChange={setDirty}
276
+ readOnly={!editable}
277
+ initialSlideId={initialSlideId}
278
+ showTopBar={showTopBar}
279
+ showBottomToolbar={showBottomToolbar}
280
+ fontFamily={fontFamily}
281
+ icons={icons}
282
+ onChange={(next) => {
283
+ onChangeRef.current?.(next);
284
+ }}
285
+ onDirtyChange={(d) => {
286
+ setDirty(d);
287
+ onDirtyChangeRef.current?.(d);
288
+ }}
289
+ onHistoryChange={onHistoryChange}
192
290
  onSave={async (next) => {
193
291
  const blob = await serialize(next);
194
292
  await saveBlob(blob);
@@ -9,8 +9,10 @@ import {
9
9
  Moon,
10
10
  } from "lucide-react";
11
11
  import { useEditor, useEditorStore } from "@/lib/StoreProvider";
12
- import { useState } from "react";
12
+ import { useState, type ReactNode } from "react";
13
13
  import type { Deck } from "@/lib/types";
14
+ import { useIcons } from "@/compound/IconContext";
15
+ import { useReadOnly } from "@/compound/ReadOnlyContext";
14
16
 
15
17
  interface TopBarProps {
16
18
  onSave?: (deck: Deck) => void | Promise<void>;
@@ -26,6 +28,8 @@ export function TopBar({ onSave: onSaveProp, onExport: onExportProp }: TopBarPro
26
28
  const play = useEditor((s) => s.play);
27
29
  const theme = useEditor((s) => s.theme);
28
30
  const toggleTheme = useEditor((s) => s.toggleTheme);
31
+ const icons = useIcons();
32
+ const readOnly = useReadOnly();
29
33
  const [saved, setSaved] = useState<"idle" | "saving" | "saved">("idle");
30
34
 
31
35
  const onSave = async () => {
@@ -71,7 +75,7 @@ export function TopBar({ onSave: onSaveProp, onExport: onExportProp }: TopBarPro
71
75
  alignItems: "center",
72
76
  padding: "0 14px",
73
77
  gap: 10,
74
- background: "var(--app-bg)",
78
+ background: "var(--slidewise-bar-bg, var(--app-bg))",
75
79
  borderBottom: "1px solid var(--border)",
76
80
  boxShadow: "var(--topbar-shadow)",
77
81
  fontFamily: "Inter, system-ui, sans-serif",
@@ -80,12 +84,16 @@ export function TopBar({ onSave: onSaveProp, onExport: onExportProp }: TopBarPro
80
84
  color: "var(--ink)",
81
85
  }}
82
86
  >
83
- <IconBtn onClick={undo} title="Undo">
84
- <Undo2 size={16} />
85
- </IconBtn>
86
- <IconBtn onClick={redo} title="Redo">
87
- <Redo2 size={16} />
88
- </IconBtn>
87
+ {!readOnly && (
88
+ <>
89
+ <IconBtn onClick={undo} title="Undo">
90
+ {icons.undo ?? <Undo2 size={16} />}
91
+ </IconBtn>
92
+ <IconBtn onClick={redo} title="Redo">
93
+ {icons.redo ?? <Redo2 size={16} />}
94
+ </IconBtn>
95
+ </>
96
+ )}
89
97
 
90
98
  <div
91
99
  style={{
@@ -111,12 +119,13 @@ export function TopBar({ onSave: onSaveProp, onExport: onExportProp }: TopBarPro
111
119
  letterSpacing: 0.2,
112
120
  }}
113
121
  >
114
- <Sparkles size={11} />
122
+ {icons.smart ?? <Sparkles size={11} />}
115
123
  Smart
116
124
  </span>
117
125
  <input
118
126
  aria-label="Deck title"
119
127
  value={title}
128
+ readOnly={readOnly}
120
129
  onChange={(e) => setTitle(e.target.value)}
121
130
  style={{
122
131
  background: "transparent",
@@ -135,18 +144,22 @@ export function TopBar({ onSave: onSaveProp, onExport: onExportProp }: TopBarPro
135
144
  onClick={toggleTheme}
136
145
  title={theme === "dark" ? "Light mode" : "Dark mode"}
137
146
  >
138
- {theme === "dark" ? <Sun size={16} /> : <Moon size={16} />}
147
+ {theme === "dark"
148
+ ? (icons.themeLight ?? <Sun size={16} />)
149
+ : (icons.themeDark ?? <Moon size={16} />)}
139
150
  </IconBtn>
140
151
 
141
- <button
142
- onClick={onSave}
143
- style={chromeBtnStyle()}
144
- onMouseEnter={(e) => (e.currentTarget.style.background = "var(--hover)")}
145
- onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")}
146
- >
147
- <Save size={14} />
148
- {saved === "saving" ? "Saving…" : saved === "saved" ? "Saved" : "Save"}
149
- </button>
152
+ {!readOnly && (
153
+ <button
154
+ onClick={onSave}
155
+ style={chromeBtnStyle()}
156
+ onMouseEnter={(e) => (e.currentTarget.style.background = "var(--hover)")}
157
+ onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")}
158
+ >
159
+ {icons.save ?? <Save size={14} />}
160
+ {saved === "saving" ? "Saving…" : saved === "saved" ? "Saved" : "Save"}
161
+ </button>
162
+ )}
150
163
 
151
164
  <button
152
165
  onClick={play}
@@ -154,7 +167,7 @@ export function TopBar({ onSave: onSaveProp, onExport: onExportProp }: TopBarPro
154
167
  onMouseEnter={(e) => (e.currentTarget.style.background = "var(--hover)")}
155
168
  onMouseLeave={(e) => (e.currentTarget.style.background = "transparent")}
156
169
  >
157
- <Play size={14} />
170
+ {icons.play ?? <Play size={14} />}
158
171
  Play
159
172
  </button>
160
173
 
@@ -168,7 +181,7 @@ export function TopBar({ onSave: onSaveProp, onExport: onExportProp }: TopBarPro
168
181
  gap: 6,
169
182
  background: "var(--primary-bg)",
170
183
  border: "1px solid var(--primary-bg)",
171
- borderRadius: 10,
184
+ borderRadius: "var(--slidewise-radius, 10px)",
172
185
  cursor: "pointer",
173
186
  color: "var(--primary-fg)",
174
187
  fontSize: 13,
@@ -181,7 +194,7 @@ export function TopBar({ onSave: onSaveProp, onExport: onExportProp }: TopBarPro
181
194
  (e.currentTarget.style.background = "var(--primary-bg)")
182
195
  }
183
196
  >
184
- <Download size={14} />
197
+ {icons.export ?? <Download size={14} />}
185
198
  Export
186
199
  </button>
187
200
  </div>
@@ -197,7 +210,7 @@ function chromeBtnStyle(): React.CSSProperties {
197
210
  gap: 6,
198
211
  background: "transparent",
199
212
  border: "1px solid var(--border-strong)",
200
- borderRadius: 10,
213
+ borderRadius: "var(--slidewise-radius, 10px)",
201
214
  cursor: "pointer",
202
215
  color: "var(--ink)",
203
216
  fontSize: 13,
@@ -210,7 +223,7 @@ function IconBtn({
210
223
  onClick,
211
224
  title,
212
225
  }: {
213
- children: React.ReactNode;
226
+ children: ReactNode;
214
227
  onClick: () => void;
215
228
  title: string;
216
229
  }) {
@@ -0,0 +1,29 @@
1
+ import { createContext, useContext, type ReactNode } from "react";
2
+ import type { Deck } from "@/lib/types";
3
+
4
+ /**
5
+ * Host-supplied callbacks consumed by leaf compound parts (e.g. TopBar's
6
+ * save/export buttons). Distinct from the editor store, which owns deck +
7
+ * UI state. This context exists so child parts can invoke host effects
8
+ * without prop-drilling through every region.
9
+ */
10
+ export interface SlidewiseHostCallbacks {
11
+ onSave?: (deck: Deck) => void | Promise<void>;
12
+ onExport?: (deck: Deck) => void;
13
+ }
14
+
15
+ const HostContext = createContext<SlidewiseHostCallbacks>({});
16
+
17
+ export function HostProvider({
18
+ callbacks,
19
+ children,
20
+ }: {
21
+ callbacks: SlidewiseHostCallbacks;
22
+ children: ReactNode;
23
+ }) {
24
+ return <HostContext.Provider value={callbacks}>{children}</HostContext.Provider>;
25
+ }
26
+
27
+ export function useHostCallbacks(): SlidewiseHostCallbacks {
28
+ return useContext(HostContext);
29
+ }
@@ -0,0 +1,42 @@
1
+ import { createContext, useContext, type ReactNode } from "react";
2
+
3
+ /**
4
+ * Per-action icon overrides for the editor's chrome. Pass a `ReactNode` for
5
+ * any button you want to skin with your own icon set (Nucleo, custom SVG,
6
+ * etc.). Slots you don't override fall back to the bundled lucide-react
7
+ * icons so partial overrides are fine.
8
+ *
9
+ * The icons are rendered inline at ~14–16px next to text labels; pick an
10
+ * SVG that has a transparent fill and uses `currentColor` for the stroke
11
+ * so it inherits the surrounding `--ink` / `--primary-fg` color.
12
+ */
13
+ export interface SlidewiseIcons {
14
+ undo?: ReactNode;
15
+ redo?: ReactNode;
16
+ save?: ReactNode;
17
+ play?: ReactNode;
18
+ stop?: ReactNode;
19
+ /** Sun icon shown in the theme toggle when the dark theme is active. */
20
+ themeLight?: ReactNode;
21
+ /** Moon icon shown in the theme toggle when the light theme is active. */
22
+ themeDark?: ReactNode;
23
+ export?: ReactNode;
24
+ /** "Smart" pill in the title bar. */
25
+ smart?: ReactNode;
26
+ }
27
+
28
+ const IconContext = createContext<SlidewiseIcons>({});
29
+
30
+ export function IconProvider({
31
+ icons,
32
+ children,
33
+ }: {
34
+ icons: SlidewiseIcons;
35
+ children: ReactNode;
36
+ }) {
37
+ return <IconContext.Provider value={icons}>{children}</IconContext.Provider>;
38
+ }
39
+
40
+ export function useIcons(): SlidewiseIcons {
41
+ return useContext(IconContext);
42
+ }
@@ -0,0 +1,23 @@
1
+ import { createContext, useContext, type ReactNode } from "react";
2
+
3
+ const ReadOnlyContext = createContext<boolean>(false);
4
+
5
+ export function ReadOnlyProvider({
6
+ readOnly,
7
+ children,
8
+ }: {
9
+ readOnly: boolean;
10
+ children: ReactNode;
11
+ }) {
12
+ return (
13
+ <ReadOnlyContext.Provider value={readOnly}>{children}</ReadOnlyContext.Provider>
14
+ );
15
+ }
16
+
17
+ /**
18
+ * Read-only mode flag. Region parts (TopBar, Canvas) hide editing affordances
19
+ * and skip mutation handlers when this is `true`.
20
+ */
21
+ export function useReadOnly(): boolean {
22
+ return useContext(ReadOnlyContext);
23
+ }