@tsdraw/react 0.5.0 → 0.6.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.cjs CHANGED
@@ -197,29 +197,52 @@ function ToolOverlay({
197
197
  ) });
198
198
  }
199
199
  function getDefaultToolbarIcon(toolId, isActive) {
200
- if (toolId === "select") return /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconPointer, { size: 18, stroke: 1.8, fill: isActive ? "currentColor" : "none" });
201
- if (toolId === "pen") return /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconPencil, { size: 18, stroke: 1.8, fill: isActive ? "currentColor" : "none" });
202
- if (toolId === "eraser") return /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconEraser, { size: 18, stroke: 1.8, fill: isActive ? "currentColor" : "none" });
203
- if (toolId === "hand") return /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconHandStop, { size: 18, stroke: isActive ? 1 : 1.8, fill: isActive ? "currentColor" : "none", style: isActive ? { stroke: "#000000" } : void 0 });
200
+ if (toolId === "select") return /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconPointer, { size: 16, stroke: 1.8, fill: isActive ? "currentColor" : "none" });
201
+ if (toolId === "pen") return /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconPencil, { size: 16, stroke: 1.8, fill: isActive ? "currentColor" : "none" });
202
+ if (toolId === "eraser") return /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconEraser, { size: 16, stroke: 1.8, fill: isActive ? "currentColor" : "none" });
203
+ if (toolId === "hand") return /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconHandStop, { size: 16, stroke: isActive ? 1 : 1.8, fill: isActive ? "currentColor" : "none", style: isActive ? { stroke: "#000000" } : void 0 });
204
204
  return null;
205
205
  }
206
- function Toolbar({ items, currentTool, onToolChange, style }) {
207
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "tsdraw-toolbar", style, children: items.map((item) => {
208
- const isActive = currentTool === item.id;
209
- return /* @__PURE__ */ jsxRuntime.jsx(
210
- "button",
211
- {
212
- type: "button",
213
- className: "tsdraw-toolbar-btn",
214
- "data-active": isActive ? "true" : void 0,
215
- onClick: () => onToolChange(item.id),
216
- title: item.label,
217
- "aria-label": item.label,
218
- children: typeof item.icon === "function" ? item.icon(isActive) : item.icon
219
- },
220
- item.id
221
- );
222
- }) });
206
+ function getActionIcon(actionId) {
207
+ if (actionId === "undo") return /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconArrowBackUp, { size: 16, stroke: 1.8 });
208
+ return /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconArrowForwardUp, { size: 16, stroke: 1.8 });
209
+ }
210
+ function Toolbar({ parts, currentTool, onToolChange, disabled, style }) {
211
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "tsdraw-toolbar", style, children: parts.map((part, partIndex) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "tsdraw-toolbar-part", children: [
212
+ part.items.map((item) => {
213
+ if (item.type === "action") {
214
+ return /* @__PURE__ */ jsxRuntime.jsx(
215
+ "button",
216
+ {
217
+ type: "button",
218
+ className: "tsdraw-toolbar-btn",
219
+ onClick: item.onSelect,
220
+ title: item.label,
221
+ "aria-label": item.label,
222
+ disabled: disabled || item.disabled,
223
+ children: getActionIcon(item.id)
224
+ },
225
+ item.id
226
+ );
227
+ }
228
+ const isActive = currentTool === item.id;
229
+ return /* @__PURE__ */ jsxRuntime.jsx(
230
+ "button",
231
+ {
232
+ type: "button",
233
+ className: "tsdraw-toolbar-btn",
234
+ "data-active": isActive ? "true" : void 0,
235
+ onClick: () => onToolChange(item.id),
236
+ title: item.label,
237
+ "aria-label": item.label,
238
+ disabled,
239
+ children: typeof item.icon === "function" ? item.icon(isActive) : item.icon
240
+ },
241
+ item.id
242
+ );
243
+ }),
244
+ partIndex < parts.length - 1 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: "tsdraw-toolbar-separator" }) : null
245
+ ] }, part.id)) });
223
246
  }
224
247
  function getCanvasCursor(currentTool, state) {
225
248
  if (currentTool === "hand") return "grab";
@@ -234,10 +257,11 @@ function getCanvasCursor(currentTool, state) {
234
257
 
235
258
  // src/persistence/localIndexedDb.ts
236
259
  var DATABASE_PREFIX = "tsdraw_v1_";
237
- var DATABASE_VERSION = 1;
260
+ var DATABASE_VERSION = 2;
238
261
  var STORE = {
239
262
  records: "records",
240
- state: "state"
263
+ state: "state",
264
+ history: "history"
241
265
  };
242
266
  function requestToPromise(request) {
243
267
  return new Promise((resolve, reject) => {
@@ -263,6 +287,9 @@ function openLocalDatabase(persistenceKey) {
263
287
  if (!database.objectStoreNames.contains(STORE.state)) {
264
288
  database.createObjectStore(STORE.state);
265
289
  }
290
+ if (!database.objectStoreNames.contains(STORE.history)) {
291
+ database.createObjectStore(STORE.history);
292
+ }
266
293
  };
267
294
  request.onsuccess = () => resolve(request.result);
268
295
  request.onerror = () => reject(request.error ?? new Error("Failed to open IndexedDB"));
@@ -279,11 +306,13 @@ var TsdrawLocalIndexedDb = class {
279
306
  }
280
307
  async load(sessionId) {
281
308
  const database = await this.databasePromise;
282
- const transaction = database.transaction([STORE.records, STORE.state], "readonly");
309
+ const transaction = database.transaction([STORE.records, STORE.state, STORE.history], "readonly");
283
310
  const recordStore = transaction.objectStore(STORE.records);
284
311
  const stateStore = transaction.objectStore(STORE.state);
312
+ const historyStore = transaction.objectStore(STORE.history);
285
313
  const records = await requestToPromise(recordStore.getAll());
286
314
  let state = (await requestToPromise(stateStore.get(sessionId)))?.snapshot ?? null;
315
+ let history = (await requestToPromise(historyStore.get(sessionId)))?.snapshot ?? null;
287
316
  if (!state) {
288
317
  const allStates = await requestToPromise(stateStore.getAll());
289
318
  if (allStates.length > 0) {
@@ -291,14 +320,22 @@ var TsdrawLocalIndexedDb = class {
291
320
  state = allStates[allStates.length - 1]?.snapshot ?? null;
292
321
  }
293
322
  }
323
+ if (!history) {
324
+ const allHistoryRows = await requestToPromise(historyStore.getAll());
325
+ if (allHistoryRows.length > 0) {
326
+ allHistoryRows.sort((left, right) => left.updatedAt - right.updatedAt);
327
+ history = allHistoryRows[allHistoryRows.length - 1]?.snapshot ?? null;
328
+ }
329
+ }
294
330
  await transactionDone(transaction);
295
- return { records, state };
331
+ return { records, state, history };
296
332
  }
297
333
  async storeSnapshot(args) {
298
334
  const database = await this.databasePromise;
299
- const transaction = database.transaction([STORE.records, STORE.state], "readwrite");
335
+ const transaction = database.transaction([STORE.records, STORE.state, STORE.history], "readwrite");
300
336
  const recordStore = transaction.objectStore(STORE.records);
301
337
  const stateStore = transaction.objectStore(STORE.state);
338
+ const historyStore = transaction.objectStore(STORE.history);
302
339
  recordStore.clear();
303
340
  for (const record of args.records) {
304
341
  recordStore.put(record, record.id);
@@ -309,6 +346,12 @@ var TsdrawLocalIndexedDb = class {
309
346
  updatedAt: Date.now()
310
347
  };
311
348
  stateStore.put(stateRow, args.sessionId);
349
+ const historyRow = {
350
+ id: args.sessionId,
351
+ snapshot: args.history,
352
+ updatedAt: Date.now()
353
+ };
354
+ historyStore.put(historyRow, args.sessionId);
312
355
  await transactionDone(transaction);
313
356
  }
314
357
  };
@@ -386,6 +429,9 @@ function useTsdrawCanvasController(options = {}) {
386
429
  const [isMovingSelection, setIsMovingSelection] = react.useState(false);
387
430
  const [isResizingSelection, setIsResizingSelection] = react.useState(false);
388
431
  const [isRotatingSelection, setIsRotatingSelection] = react.useState(false);
432
+ const [canUndo, setCanUndo] = react.useState(false);
433
+ const [canRedo, setCanRedo] = react.useState(false);
434
+ const [isPersistenceReady, setIsPersistenceReady] = react.useState(!options.persistenceKey);
389
435
  const [pointerScreenPoint, setPointerScreenPoint] = react.useState({ x: 0, y: 0 });
390
436
  const [isPointerInsideCanvas, setIsPointerInsideCanvas] = react.useState(false);
391
437
  react.useEffect(() => {
@@ -463,6 +509,7 @@ function useTsdrawCanvasController(options = {}) {
463
509
  startBounds: bounds,
464
510
  startShapes: core.buildTransformSnapshots(editor, selectedShapeIdsRef.current)
465
511
  };
512
+ editor.beginHistoryEntry();
466
513
  selectDragRef.current.mode = "resize";
467
514
  const p = getPagePointFromClient(editor, e.clientX, e.clientY);
468
515
  editor.input.pointerDown(p.x, p.y, 0.5, false);
@@ -492,6 +539,7 @@ function useTsdrawCanvasController(options = {}) {
492
539
  startSelectionRotationDeg: selectionRotationRef.current,
493
540
  startShapes: core.buildTransformSnapshots(editor, selectedShapeIdsRef.current)
494
541
  };
542
+ editor.beginHistoryEntry();
495
543
  selectDragRef.current.mode = "rotate";
496
544
  editor.input.pointerDown(p.x, p.y, 0.5, false);
497
545
  selectDragRef.current.startPage = p;
@@ -524,10 +572,15 @@ function useTsdrawCanvasController(options = {}) {
524
572
  let persistenceActive = false;
525
573
  const persistenceKey = options.persistenceKey;
526
574
  const sessionId = getOrCreateSessionId();
575
+ const syncHistoryState = () => {
576
+ setCanUndo(editor.canUndo());
577
+ setCanRedo(editor.canRedo());
578
+ };
527
579
  const activeTool = editor.getCurrentToolId();
528
580
  editorRef.current = editor;
529
581
  setCurrentToolState(activeTool);
530
582
  currentToolRef.current = activeTool;
583
+ syncHistoryState();
531
584
  const initialStyle = editor.getCurrentDrawStyle();
532
585
  setDrawColor(initialStyle.color);
533
586
  setDrawDash(initialStyle.dash);
@@ -554,6 +607,7 @@ function useTsdrawCanvasController(options = {}) {
554
607
  await persistenceDb.storeSnapshot({
555
608
  records: snapshot.document.records,
556
609
  state: snapshot.state,
610
+ history: editor.getHistorySnapshot(),
557
611
  sessionId
558
612
  });
559
613
  persistenceChannel?.postMessage({
@@ -594,6 +648,7 @@ function useTsdrawCanvasController(options = {}) {
594
648
  const applyRemoteDocumentSnapshot = (document) => {
595
649
  ignorePersistenceChanges = true;
596
650
  editor.loadDocumentSnapshot(document);
651
+ editor.clearRedoHistory();
597
652
  reconcileSelectionAfterDocumentLoad();
598
653
  render();
599
654
  ignorePersistenceChanges = false;
@@ -626,6 +681,7 @@ function useTsdrawCanvasController(options = {}) {
626
681
  const handlePointerDown = (e) => {
627
682
  if (!canvas.contains(e.target)) return;
628
683
  isPointerActiveRef.current = true;
684
+ editor.beginHistoryEntry();
629
685
  canvas.setPointerCapture(e.pointerId);
630
686
  lastPointerClientRef.current = { x: e.clientX, y: e.clientY };
631
687
  updatePointerPreview(e.clientX, e.clientY);
@@ -750,6 +806,7 @@ function useTsdrawCanvasController(options = {}) {
750
806
  };
751
807
  render();
752
808
  refreshSelectionBounds(editor);
809
+ editor.endHistoryEntry();
753
810
  return;
754
811
  }
755
812
  if (drag.mode === "resize") {
@@ -758,6 +815,7 @@ function useTsdrawCanvasController(options = {}) {
758
815
  resizeRef.current = { handle: null, startBounds: null, startShapes: /* @__PURE__ */ new Map() };
759
816
  render();
760
817
  refreshSelectionBounds(editor);
818
+ editor.endHistoryEntry();
761
819
  return;
762
820
  }
763
821
  if (drag.mode === "move") {
@@ -765,6 +823,7 @@ function useTsdrawCanvasController(options = {}) {
765
823
  selectDragRef.current.mode = "none";
766
824
  render();
767
825
  refreshSelectionBounds(editor);
826
+ editor.endHistoryEntry();
768
827
  return;
769
828
  }
770
829
  if (drag.mode === "marquee") {
@@ -796,6 +855,7 @@ function useTsdrawCanvasController(options = {}) {
796
855
  pendingRemoteDocumentRef.current = null;
797
856
  applyRemoteDocumentSnapshot(pendingRemoteDocument);
798
857
  }
858
+ editor.endHistoryEntry();
799
859
  return;
800
860
  }
801
861
  }
@@ -807,8 +867,25 @@ function useTsdrawCanvasController(options = {}) {
807
867
  pendingRemoteDocumentRef.current = null;
808
868
  applyRemoteDocumentSnapshot(pendingRemoteDocument);
809
869
  }
870
+ editor.endHistoryEntry();
810
871
  };
811
872
  const handleKeyDown = (e) => {
873
+ const isMetaPressed = e.metaKey || e.ctrlKey;
874
+ const loweredKey = e.key.toLowerCase();
875
+ const isUndoOrRedoKey = loweredKey === "z" || loweredKey === "y";
876
+ if (isMetaPressed && isUndoOrRedoKey) {
877
+ const shouldRedo = loweredKey === "y" || loweredKey === "z" && e.shiftKey;
878
+ const changed = shouldRedo ? editor.redo() : editor.undo();
879
+ if (changed) {
880
+ e.preventDefault();
881
+ e.stopPropagation();
882
+ reconcileSelectionAfterDocumentLoad();
883
+ setSelectionRotationDeg(0);
884
+ render();
885
+ syncHistoryState();
886
+ return;
887
+ }
888
+ }
812
889
  editor.input.setModifiers(e.shiftKey, e.ctrlKey, e.metaKey);
813
890
  editor.tools.keyDown({ key: e.key });
814
891
  render();
@@ -819,40 +896,56 @@ function useTsdrawCanvasController(options = {}) {
819
896
  render();
820
897
  };
821
898
  const initializePersistence = async () => {
822
- if (!persistenceKey) return;
823
- persistenceDb = new TsdrawLocalIndexedDb(persistenceKey);
824
- const loaded = await persistenceDb.load(sessionId);
825
- const snapshot = {};
826
- if (loaded.records.length > 0) {
827
- snapshot.document = { records: loaded.records };
828
- }
829
- if (loaded.state) {
830
- snapshot.state = loaded.state;
831
- }
832
- if (snapshot.document || snapshot.state) {
833
- applyLoadedSnapshot(snapshot);
899
+ if (!persistenceKey) {
900
+ setIsPersistenceReady(true);
901
+ return;
834
902
  }
835
- persistenceActive = true;
836
- persistenceChannel = new BroadcastChannel(`tsdraw:persistence:${persistenceKey}`);
837
- persistenceChannel.onmessage = async (event) => {
838
- const data = event.data;
839
- if (data?.type !== "tsdraw:persisted" || data.senderSessionId === sessionId) return;
840
- if (!persistenceDb || disposed) return;
841
- const nextLoaded = await persistenceDb.load(sessionId);
842
- if (nextLoaded.records.length > 0) {
843
- const nextDocument = { records: nextLoaded.records };
844
- if (isPointerActiveRef.current) {
845
- pendingRemoteDocumentRef.current = nextDocument;
846
- return;
903
+ try {
904
+ persistenceDb = new TsdrawLocalIndexedDb(persistenceKey);
905
+ const loaded = await persistenceDb.load(sessionId);
906
+ const snapshot = {};
907
+ if (loaded.records.length > 0) {
908
+ snapshot.document = { records: loaded.records };
909
+ }
910
+ if (loaded.state) {
911
+ snapshot.state = loaded.state;
912
+ }
913
+ if (snapshot.document || snapshot.state) {
914
+ applyLoadedSnapshot(snapshot);
915
+ }
916
+ editor.loadHistorySnapshot(loaded.history);
917
+ syncHistoryState();
918
+ persistenceActive = true;
919
+ persistenceChannel = new BroadcastChannel(`tsdraw:persistence:${persistenceKey}`);
920
+ persistenceChannel.onmessage = async (event) => {
921
+ const data = event.data;
922
+ if (data?.type !== "tsdraw:persisted" || data.senderSessionId === sessionId) return;
923
+ if (!persistenceDb || disposed) return;
924
+ const nextLoaded = await persistenceDb.load(sessionId);
925
+ if (nextLoaded.records.length > 0) {
926
+ const nextDocument = { records: nextLoaded.records };
927
+ if (isPointerActiveRef.current) {
928
+ pendingRemoteDocumentRef.current = nextDocument;
929
+ return;
930
+ }
931
+ applyRemoteDocumentSnapshot(nextDocument);
847
932
  }
848
- applyRemoteDocumentSnapshot(nextDocument);
933
+ };
934
+ } finally {
935
+ if (!disposed) {
936
+ setIsPersistenceReady(true);
849
937
  }
850
- };
938
+ }
851
939
  };
852
940
  const cleanupEditorListener = editor.listen(() => {
853
941
  if (ignorePersistenceChanges) return;
854
942
  schedulePersist();
855
943
  });
944
+ const cleanupHistoryListener = editor.listenHistory(() => {
945
+ syncHistoryState();
946
+ if (ignorePersistenceChanges) return;
947
+ schedulePersist();
948
+ });
856
949
  resize();
857
950
  const ro = new ResizeObserver(resize);
858
951
  ro.observe(container);
@@ -875,6 +968,26 @@ function useTsdrawCanvasController(options = {}) {
875
968
  currentToolRef.current = tool;
876
969
  },
877
970
  getCurrentTool: () => editor.getCurrentToolId(),
971
+ undo: () => {
972
+ const changed = editor.undo();
973
+ if (!changed) return false;
974
+ reconcileSelectionAfterDocumentLoad();
975
+ setSelectionRotationDeg(0);
976
+ render();
977
+ syncHistoryState();
978
+ return true;
979
+ },
980
+ redo: () => {
981
+ const changed = editor.redo();
982
+ if (!changed) return false;
983
+ reconcileSelectionAfterDocumentLoad();
984
+ setSelectionRotationDeg(0);
985
+ render();
986
+ syncHistoryState();
987
+ return true;
988
+ },
989
+ canUndo: () => editor.canUndo(),
990
+ canRedo: () => editor.canRedo(),
878
991
  applyDrawStyle: (partial) => {
879
992
  editor.setCurrentDrawStyle(partial);
880
993
  if (partial.color) setDrawColor(partial.color);
@@ -887,6 +1000,7 @@ function useTsdrawCanvasController(options = {}) {
887
1000
  disposed = true;
888
1001
  schedulePersistRef.current = null;
889
1002
  cleanupEditorListener();
1003
+ cleanupHistoryListener();
890
1004
  disposeMount?.();
891
1005
  ro.disconnect();
892
1006
  canvas.removeEventListener("pointerdown", handlePointerDown);
@@ -940,6 +1054,38 @@ function useTsdrawCanvasController(options = {}) {
940
1054
  },
941
1055
  [render]
942
1056
  );
1057
+ const undo = react.useCallback(() => {
1058
+ const editor = editorRef.current;
1059
+ if (!editor) return false;
1060
+ const changed = editor.undo();
1061
+ if (!changed) return false;
1062
+ const nextSelectedShapeIds = selectedShapeIdsRef.current.filter((shapeId) => editor.getShape(shapeId) != null);
1063
+ if (nextSelectedShapeIds.length !== selectedShapeIdsRef.current.length) {
1064
+ selectedShapeIdsRef.current = nextSelectedShapeIds;
1065
+ setSelectedShapeIds(nextSelectedShapeIds);
1066
+ }
1067
+ setSelectionRotationDeg(0);
1068
+ render();
1069
+ setCanUndo(editor.canUndo());
1070
+ setCanRedo(editor.canRedo());
1071
+ return true;
1072
+ }, [render]);
1073
+ const redo = react.useCallback(() => {
1074
+ const editor = editorRef.current;
1075
+ if (!editor) return false;
1076
+ const changed = editor.redo();
1077
+ if (!changed) return false;
1078
+ const nextSelectedShapeIds = selectedShapeIdsRef.current.filter((shapeId) => editor.getShape(shapeId) != null);
1079
+ if (nextSelectedShapeIds.length !== selectedShapeIdsRef.current.length) {
1080
+ selectedShapeIdsRef.current = nextSelectedShapeIds;
1081
+ setSelectedShapeIds(nextSelectedShapeIds);
1082
+ }
1083
+ setSelectionRotationDeg(0);
1084
+ render();
1085
+ setCanUndo(editor.canUndo());
1086
+ setCanRedo(editor.canRedo());
1087
+ return true;
1088
+ }, [render]);
943
1089
  const showToolOverlay = isPointerInsideCanvas && (currentTool === "pen" || currentTool === "eraser");
944
1090
  const canvasCursor = getCanvasCursor(currentTool, {
945
1091
  isMovingSelection,
@@ -978,14 +1124,19 @@ function useTsdrawCanvasController(options = {}) {
978
1124
  canvasCursor,
979
1125
  cursorContext,
980
1126
  toolOverlay,
1127
+ isPersistenceReady,
981
1128
  showStylePanel: stylePanelToolIdsRef.current.includes(currentTool),
1129
+ canUndo,
1130
+ canRedo,
1131
+ undo,
1132
+ redo,
982
1133
  setTool,
983
1134
  applyDrawStyle,
984
1135
  handleResizePointerDown,
985
1136
  handleRotatePointerDown
986
1137
  };
987
1138
  }
988
- var DEFAULT_TOOL_IDS = ["select", "pen", "eraser", "hand"];
1139
+ var DEFAULT_TOOLBAR_PARTS = [["undo", "redo"], ["select", "hand", "pen", "eraser"]];
989
1140
  var DEFAULT_TOOL_LABELS = {
990
1141
  select: "Select",
991
1142
  pen: "Pen",
@@ -1002,11 +1153,8 @@ function parseAnchor(anchor) {
1002
1153
  }
1003
1154
  return { vertical, horizontal };
1004
1155
  }
1005
- function isCustomTool(toolItem) {
1006
- return typeof toolItem !== "string";
1007
- }
1008
- function getToolId(toolItem) {
1009
- return typeof toolItem === "string" ? toolItem : toolItem.id;
1156
+ function isToolbarAction(item) {
1157
+ return item === "undo" || item === "redo";
1010
1158
  }
1011
1159
  function resolvePlacementStyle(placement, fallbackAnchor, fallbackOffsetX, fallbackOffsetY) {
1012
1160
  const anchor = placement?.anchor ?? fallbackAnchor;
@@ -1041,40 +1189,55 @@ function Tsdraw(props) {
1041
1189
  if (typeof window === "undefined") return "light";
1042
1190
  return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
1043
1191
  });
1044
- const toolItems = props.tools ?? DEFAULT_TOOL_IDS;
1045
- const customTools = react.useMemo(
1046
- () => toolItems.filter(isCustomTool),
1047
- [toolItems]
1048
- );
1049
- const toolDefinitions = react.useMemo(
1050
- () => customTools.map((tool) => tool.definition),
1192
+ const customTools = props.customTools ?? [];
1193
+ const toolbarPartIds = props.uiOptions?.toolbar?.parts ?? DEFAULT_TOOLBAR_PARTS;
1194
+ const customToolMap = react.useMemo(
1195
+ () => new Map(customTools.map((customTool) => [customTool.id, customTool])),
1051
1196
  [customTools]
1052
1197
  );
1053
- const toolbarItems = react.useMemo(
1054
- () => toolItems.map((tool) => {
1055
- if (typeof tool === "string") {
1056
- return {
1057
- id: tool,
1058
- label: DEFAULT_TOOL_LABELS[tool],
1059
- icon: (isActive) => getDefaultToolbarIcon(tool, isActive)
1060
- };
1198
+ const toolbarToolIds = react.useMemo(() => {
1199
+ const ids = /* @__PURE__ */ new Set();
1200
+ for (const toolbarPart of toolbarPartIds) {
1201
+ for (const item of toolbarPart) {
1202
+ if (isToolbarAction(item)) continue;
1203
+ if (item in DEFAULT_TOOL_LABELS || customToolMap.has(item)) {
1204
+ ids.add(item);
1205
+ }
1061
1206
  }
1062
- return {
1063
- id: tool.id,
1064
- label: tool.label,
1065
- icon: (isActive) => isActive && tool.iconSelected ? tool.iconSelected : tool.icon
1066
- };
1067
- }),
1068
- [toolItems]
1207
+ }
1208
+ return ids;
1209
+ }, [customToolMap, toolbarPartIds]);
1210
+ const toolDefinitions = react.useMemo(
1211
+ () => customTools.filter((customTool) => toolbarToolIds.has(customTool.id)).map((customTool) => customTool.definition),
1212
+ [customTools, toolbarToolIds]
1069
1213
  );
1070
1214
  const stylePanelToolIds = react.useMemo(
1071
- () => toolItems.filter((tool) => {
1072
- if (typeof tool === "string") return tool === "pen";
1073
- return tool.showStylePanel ?? false;
1074
- }).map(getToolId),
1075
- [toolItems]
1215
+ () => {
1216
+ const nextToolIds = /* @__PURE__ */ new Set();
1217
+ if (toolbarToolIds.has("pen")) {
1218
+ nextToolIds.add("pen");
1219
+ }
1220
+ for (const customTool of customTools) {
1221
+ if ((customTool.showStylePanel ?? false) && toolbarToolIds.has(customTool.id)) {
1222
+ nextToolIds.add(customTool.id);
1223
+ }
1224
+ }
1225
+ return [...nextToolIds];
1226
+ },
1227
+ [customTools, toolbarToolIds]
1076
1228
  );
1077
- const initialTool = props.initialToolId ?? toolbarItems[0]?.id ?? "pen";
1229
+ const firstToolbarTool = react.useMemo(() => {
1230
+ for (const toolbarPart of toolbarPartIds) {
1231
+ for (const item of toolbarPart) {
1232
+ if (isToolbarAction(item)) continue;
1233
+ if (item in DEFAULT_TOOL_LABELS || customToolMap.has(item)) {
1234
+ return item;
1235
+ }
1236
+ }
1237
+ }
1238
+ return void 0;
1239
+ }, [customToolMap, toolbarPartIds]);
1240
+ const initialTool = props.initialToolId ?? firstToolbarTool ?? "pen";
1078
1241
  const requestedTheme = props.theme ?? "light";
1079
1242
  react.useEffect(() => {
1080
1243
  if (requestedTheme !== "system" || typeof window === "undefined") return;
@@ -1099,7 +1262,12 @@ function Tsdraw(props) {
1099
1262
  canvasCursor: defaultCanvasCursor,
1100
1263
  cursorContext,
1101
1264
  toolOverlay,
1265
+ isPersistenceReady,
1102
1266
  showStylePanel,
1267
+ canUndo,
1268
+ canRedo,
1269
+ undo,
1270
+ redo,
1103
1271
  setTool,
1104
1272
  applyDrawStyle,
1105
1273
  handleResizePointerDown,
@@ -1112,7 +1280,7 @@ function Tsdraw(props) {
1112
1280
  stylePanelToolIds,
1113
1281
  onMount: props.onMount
1114
1282
  });
1115
- const toolbarPlacementStyle = resolvePlacementStyle(props.uiOptions?.toolbar?.placement, "bottom-center", 0, 16);
1283
+ const toolbarPlacementStyle = resolvePlacementStyle(props.uiOptions?.toolbar?.placement, "bottom-center", 0, 14);
1116
1284
  const stylePanelPlacementStyle = resolvePlacementStyle(props.uiOptions?.stylePanel?.placement, "top-right", 8, 8);
1117
1285
  const canvasCursor = props.uiOptions?.cursor?.getCursor?.(cursorContext) ?? defaultCanvasCursor;
1118
1286
  const defaultToolOverlay = /* @__PURE__ */ jsxRuntime.jsx(
@@ -1129,6 +1297,51 @@ function Tsdraw(props) {
1129
1297
  );
1130
1298
  const overlayNode = props.uiOptions?.overlays?.renderToolOverlay?.({ defaultOverlay: defaultToolOverlay, overlayState: toolOverlay, currentTool }) ?? defaultToolOverlay;
1131
1299
  const customElements = props.uiOptions?.customElements ?? [];
1300
+ const toolbarParts = react.useMemo(
1301
+ () => toolbarPartIds.map((toolbarPart, partIndex) => {
1302
+ const items = toolbarPart.map((item) => {
1303
+ if (item === "undo") {
1304
+ return {
1305
+ type: "action",
1306
+ id: "undo",
1307
+ label: "Undo",
1308
+ disabled: !canUndo,
1309
+ onSelect: undo
1310
+ };
1311
+ }
1312
+ if (item === "redo") {
1313
+ return {
1314
+ type: "action",
1315
+ id: "redo",
1316
+ label: "Redo",
1317
+ disabled: !canRedo,
1318
+ onSelect: redo
1319
+ };
1320
+ }
1321
+ if (item in DEFAULT_TOOL_LABELS) {
1322
+ return {
1323
+ type: "tool",
1324
+ id: item,
1325
+ label: DEFAULT_TOOL_LABELS[item],
1326
+ icon: (isActive) => getDefaultToolbarIcon(item, isActive)
1327
+ };
1328
+ }
1329
+ const customTool = customToolMap.get(item);
1330
+ if (!customTool) return null;
1331
+ return {
1332
+ type: "tool",
1333
+ id: customTool.id,
1334
+ label: customTool.label,
1335
+ icon: (isActive) => isActive && customTool.iconSelected ? customTool.iconSelected : customTool.icon
1336
+ };
1337
+ }).filter((nextItem) => nextItem != null);
1338
+ return {
1339
+ id: `toolbar-part-${partIndex.toString(36)}`,
1340
+ items
1341
+ };
1342
+ }).filter((part) => part.items.length > 0),
1343
+ [canRedo, canUndo, customToolMap, redo, toolbarPartIds, undo]
1344
+ );
1132
1345
  return /* @__PURE__ */ jsxRuntime.jsxs(
1133
1346
  "div",
1134
1347
  {
@@ -1172,7 +1385,7 @@ function Tsdraw(props) {
1172
1385
  /* @__PURE__ */ jsxRuntime.jsx(
1173
1386
  StylePanel,
1174
1387
  {
1175
- visible: showStylePanel,
1388
+ visible: isPersistenceReady && showStylePanel,
1176
1389
  style: stylePanelPlacementStyle,
1177
1390
  theme: resolvedTheme,
1178
1391
  drawColor,
@@ -1199,10 +1412,11 @@ function Tsdraw(props) {
1199
1412
  /* @__PURE__ */ jsxRuntime.jsx(
1200
1413
  Toolbar,
1201
1414
  {
1202
- items: toolbarItems,
1415
+ parts: toolbarParts,
1203
1416
  style: toolbarPlacementStyle,
1204
- currentTool,
1205
- onToolChange: setTool
1417
+ currentTool: isPersistenceReady ? currentTool : null,
1418
+ onToolChange: setTool,
1419
+ disabled: !isPersistenceReady
1206
1420
  }
1207
1421
  )
1208
1422
  ]