@tsdraw/react 0.5.1 → 0.6.1

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
@@ -203,23 +203,46 @@ function getDefaultToolbarIcon(toolId, isActive) {
203
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,8 +607,10 @@ 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
  });
613
+ if (disposed) return;
559
614
  persistenceChannel?.postMessage({
560
615
  type: "tsdraw:persisted",
561
616
  senderSessionId: sessionId
@@ -594,6 +649,7 @@ function useTsdrawCanvasController(options = {}) {
594
649
  const applyRemoteDocumentSnapshot = (document) => {
595
650
  ignorePersistenceChanges = true;
596
651
  editor.loadDocumentSnapshot(document);
652
+ editor.clearRedoHistory();
597
653
  reconcileSelectionAfterDocumentLoad();
598
654
  render();
599
655
  ignorePersistenceChanges = false;
@@ -626,6 +682,7 @@ function useTsdrawCanvasController(options = {}) {
626
682
  const handlePointerDown = (e) => {
627
683
  if (!canvas.contains(e.target)) return;
628
684
  isPointerActiveRef.current = true;
685
+ editor.beginHistoryEntry();
629
686
  canvas.setPointerCapture(e.pointerId);
630
687
  lastPointerClientRef.current = { x: e.clientX, y: e.clientY };
631
688
  updatePointerPreview(e.clientX, e.clientY);
@@ -750,6 +807,7 @@ function useTsdrawCanvasController(options = {}) {
750
807
  };
751
808
  render();
752
809
  refreshSelectionBounds(editor);
810
+ editor.endHistoryEntry();
753
811
  return;
754
812
  }
755
813
  if (drag.mode === "resize") {
@@ -758,6 +816,7 @@ function useTsdrawCanvasController(options = {}) {
758
816
  resizeRef.current = { handle: null, startBounds: null, startShapes: /* @__PURE__ */ new Map() };
759
817
  render();
760
818
  refreshSelectionBounds(editor);
819
+ editor.endHistoryEntry();
761
820
  return;
762
821
  }
763
822
  if (drag.mode === "move") {
@@ -765,6 +824,7 @@ function useTsdrawCanvasController(options = {}) {
765
824
  selectDragRef.current.mode = "none";
766
825
  render();
767
826
  refreshSelectionBounds(editor);
827
+ editor.endHistoryEntry();
768
828
  return;
769
829
  }
770
830
  if (drag.mode === "marquee") {
@@ -796,6 +856,7 @@ function useTsdrawCanvasController(options = {}) {
796
856
  pendingRemoteDocumentRef.current = null;
797
857
  applyRemoteDocumentSnapshot(pendingRemoteDocument);
798
858
  }
859
+ editor.endHistoryEntry();
799
860
  return;
800
861
  }
801
862
  }
@@ -807,8 +868,25 @@ function useTsdrawCanvasController(options = {}) {
807
868
  pendingRemoteDocumentRef.current = null;
808
869
  applyRemoteDocumentSnapshot(pendingRemoteDocument);
809
870
  }
871
+ editor.endHistoryEntry();
810
872
  };
811
873
  const handleKeyDown = (e) => {
874
+ const isMetaPressed = e.metaKey || e.ctrlKey;
875
+ const loweredKey = e.key.toLowerCase();
876
+ const isUndoOrRedoKey = loweredKey === "z" || loweredKey === "y";
877
+ if (isMetaPressed && isUndoOrRedoKey) {
878
+ const shouldRedo = loweredKey === "y" || loweredKey === "z" && e.shiftKey;
879
+ const changed = shouldRedo ? editor.redo() : editor.undo();
880
+ if (changed) {
881
+ e.preventDefault();
882
+ e.stopPropagation();
883
+ reconcileSelectionAfterDocumentLoad();
884
+ setSelectionRotationDeg(0);
885
+ render();
886
+ syncHistoryState();
887
+ return;
888
+ }
889
+ }
812
890
  editor.input.setModifiers(e.shiftKey, e.ctrlKey, e.metaKey);
813
891
  editor.tools.keyDown({ key: e.key });
814
892
  render();
@@ -819,40 +897,56 @@ function useTsdrawCanvasController(options = {}) {
819
897
  render();
820
898
  };
821
899
  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);
900
+ if (!persistenceKey) {
901
+ setIsPersistenceReady(true);
902
+ return;
834
903
  }
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;
904
+ try {
905
+ persistenceDb = new TsdrawLocalIndexedDb(persistenceKey);
906
+ const loaded = await persistenceDb.load(sessionId);
907
+ const snapshot = {};
908
+ if (loaded.records.length > 0) {
909
+ snapshot.document = { records: loaded.records };
910
+ }
911
+ if (loaded.state) {
912
+ snapshot.state = loaded.state;
913
+ }
914
+ if (snapshot.document || snapshot.state) {
915
+ applyLoadedSnapshot(snapshot);
916
+ }
917
+ editor.loadHistorySnapshot(loaded.history);
918
+ syncHistoryState();
919
+ persistenceActive = true;
920
+ persistenceChannel = new BroadcastChannel(`tsdraw:persistence:${persistenceKey}`);
921
+ persistenceChannel.onmessage = async (event) => {
922
+ const data = event.data;
923
+ if (data?.type !== "tsdraw:persisted" || data.senderSessionId === sessionId) return;
924
+ if (!persistenceDb || disposed) return;
925
+ const nextLoaded = await persistenceDb.load(sessionId);
926
+ if (nextLoaded.records.length > 0) {
927
+ const nextDocument = { records: nextLoaded.records };
928
+ if (isPointerActiveRef.current) {
929
+ pendingRemoteDocumentRef.current = nextDocument;
930
+ return;
931
+ }
932
+ applyRemoteDocumentSnapshot(nextDocument);
847
933
  }
848
- applyRemoteDocumentSnapshot(nextDocument);
934
+ };
935
+ } finally {
936
+ if (!disposed) {
937
+ setIsPersistenceReady(true);
849
938
  }
850
- };
939
+ }
851
940
  };
852
941
  const cleanupEditorListener = editor.listen(() => {
853
942
  if (ignorePersistenceChanges) return;
854
943
  schedulePersist();
855
944
  });
945
+ const cleanupHistoryListener = editor.listenHistory(() => {
946
+ syncHistoryState();
947
+ if (ignorePersistenceChanges) return;
948
+ schedulePersist();
949
+ });
856
950
  resize();
857
951
  const ro = new ResizeObserver(resize);
858
952
  ro.observe(container);
@@ -875,6 +969,26 @@ function useTsdrawCanvasController(options = {}) {
875
969
  currentToolRef.current = tool;
876
970
  },
877
971
  getCurrentTool: () => editor.getCurrentToolId(),
972
+ undo: () => {
973
+ const changed = editor.undo();
974
+ if (!changed) return false;
975
+ reconcileSelectionAfterDocumentLoad();
976
+ setSelectionRotationDeg(0);
977
+ render();
978
+ syncHistoryState();
979
+ return true;
980
+ },
981
+ redo: () => {
982
+ const changed = editor.redo();
983
+ if (!changed) return false;
984
+ reconcileSelectionAfterDocumentLoad();
985
+ setSelectionRotationDeg(0);
986
+ render();
987
+ syncHistoryState();
988
+ return true;
989
+ },
990
+ canUndo: () => editor.canUndo(),
991
+ canRedo: () => editor.canRedo(),
878
992
  applyDrawStyle: (partial) => {
879
993
  editor.setCurrentDrawStyle(partial);
880
994
  if (partial.color) setDrawColor(partial.color);
@@ -887,6 +1001,7 @@ function useTsdrawCanvasController(options = {}) {
887
1001
  disposed = true;
888
1002
  schedulePersistRef.current = null;
889
1003
  cleanupEditorListener();
1004
+ cleanupHistoryListener();
890
1005
  disposeMount?.();
891
1006
  ro.disconnect();
892
1007
  canvas.removeEventListener("pointerdown", handlePointerDown);
@@ -940,6 +1055,38 @@ function useTsdrawCanvasController(options = {}) {
940
1055
  },
941
1056
  [render]
942
1057
  );
1058
+ const undo = react.useCallback(() => {
1059
+ const editor = editorRef.current;
1060
+ if (!editor) return false;
1061
+ const changed = editor.undo();
1062
+ if (!changed) return false;
1063
+ const nextSelectedShapeIds = selectedShapeIdsRef.current.filter((shapeId) => editor.getShape(shapeId) != null);
1064
+ if (nextSelectedShapeIds.length !== selectedShapeIdsRef.current.length) {
1065
+ selectedShapeIdsRef.current = nextSelectedShapeIds;
1066
+ setSelectedShapeIds(nextSelectedShapeIds);
1067
+ }
1068
+ setSelectionRotationDeg(0);
1069
+ render();
1070
+ setCanUndo(editor.canUndo());
1071
+ setCanRedo(editor.canRedo());
1072
+ return true;
1073
+ }, [render]);
1074
+ const redo = react.useCallback(() => {
1075
+ const editor = editorRef.current;
1076
+ if (!editor) return false;
1077
+ const changed = editor.redo();
1078
+ if (!changed) return false;
1079
+ const nextSelectedShapeIds = selectedShapeIdsRef.current.filter((shapeId) => editor.getShape(shapeId) != null);
1080
+ if (nextSelectedShapeIds.length !== selectedShapeIdsRef.current.length) {
1081
+ selectedShapeIdsRef.current = nextSelectedShapeIds;
1082
+ setSelectedShapeIds(nextSelectedShapeIds);
1083
+ }
1084
+ setSelectionRotationDeg(0);
1085
+ render();
1086
+ setCanUndo(editor.canUndo());
1087
+ setCanRedo(editor.canRedo());
1088
+ return true;
1089
+ }, [render]);
943
1090
  const showToolOverlay = isPointerInsideCanvas && (currentTool === "pen" || currentTool === "eraser");
944
1091
  const canvasCursor = getCanvasCursor(currentTool, {
945
1092
  isMovingSelection,
@@ -978,14 +1125,19 @@ function useTsdrawCanvasController(options = {}) {
978
1125
  canvasCursor,
979
1126
  cursorContext,
980
1127
  toolOverlay,
1128
+ isPersistenceReady,
981
1129
  showStylePanel: stylePanelToolIdsRef.current.includes(currentTool),
1130
+ canUndo,
1131
+ canRedo,
1132
+ undo,
1133
+ redo,
982
1134
  setTool,
983
1135
  applyDrawStyle,
984
1136
  handleResizePointerDown,
985
1137
  handleRotatePointerDown
986
1138
  };
987
1139
  }
988
- var DEFAULT_TOOL_IDS = ["select", "pen", "eraser", "hand"];
1140
+ var DEFAULT_TOOLBAR_PARTS = [["undo", "redo"], ["select", "hand", "pen", "eraser"]];
989
1141
  var DEFAULT_TOOL_LABELS = {
990
1142
  select: "Select",
991
1143
  pen: "Pen",
@@ -1002,11 +1154,8 @@ function parseAnchor(anchor) {
1002
1154
  }
1003
1155
  return { vertical, horizontal };
1004
1156
  }
1005
- function isCustomTool(toolItem) {
1006
- return typeof toolItem !== "string";
1007
- }
1008
- function getToolId(toolItem) {
1009
- return typeof toolItem === "string" ? toolItem : toolItem.id;
1157
+ function isToolbarAction(item) {
1158
+ return item === "undo" || item === "redo";
1010
1159
  }
1011
1160
  function resolvePlacementStyle(placement, fallbackAnchor, fallbackOffsetX, fallbackOffsetY) {
1012
1161
  const anchor = placement?.anchor ?? fallbackAnchor;
@@ -1041,40 +1190,55 @@ function Tsdraw(props) {
1041
1190
  if (typeof window === "undefined") return "light";
1042
1191
  return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
1043
1192
  });
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),
1193
+ const customTools = props.customTools ?? [];
1194
+ const toolbarPartIds = props.uiOptions?.toolbar?.parts ?? DEFAULT_TOOLBAR_PARTS;
1195
+ const customToolMap = react.useMemo(
1196
+ () => new Map(customTools.map((customTool) => [customTool.id, customTool])),
1051
1197
  [customTools]
1052
1198
  );
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
- };
1199
+ const toolbarToolIds = react.useMemo(() => {
1200
+ const ids = /* @__PURE__ */ new Set();
1201
+ for (const toolbarPart of toolbarPartIds) {
1202
+ for (const item of toolbarPart) {
1203
+ if (isToolbarAction(item)) continue;
1204
+ if (item in DEFAULT_TOOL_LABELS || customToolMap.has(item)) {
1205
+ ids.add(item);
1206
+ }
1061
1207
  }
1062
- return {
1063
- id: tool.id,
1064
- label: tool.label,
1065
- icon: (isActive) => isActive && tool.iconSelected ? tool.iconSelected : tool.icon
1066
- };
1067
- }),
1068
- [toolItems]
1208
+ }
1209
+ return ids;
1210
+ }, [customToolMap, toolbarPartIds]);
1211
+ const toolDefinitions = react.useMemo(
1212
+ () => customTools.filter((customTool) => toolbarToolIds.has(customTool.id)).map((customTool) => customTool.definition),
1213
+ [customTools, toolbarToolIds]
1069
1214
  );
1070
1215
  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]
1216
+ () => {
1217
+ const nextToolIds = /* @__PURE__ */ new Set();
1218
+ if (toolbarToolIds.has("pen")) {
1219
+ nextToolIds.add("pen");
1220
+ }
1221
+ for (const customTool of customTools) {
1222
+ if ((customTool.showStylePanel ?? false) && toolbarToolIds.has(customTool.id)) {
1223
+ nextToolIds.add(customTool.id);
1224
+ }
1225
+ }
1226
+ return [...nextToolIds];
1227
+ },
1228
+ [customTools, toolbarToolIds]
1076
1229
  );
1077
- const initialTool = props.initialToolId ?? toolbarItems[0]?.id ?? "pen";
1230
+ const firstToolbarTool = react.useMemo(() => {
1231
+ for (const toolbarPart of toolbarPartIds) {
1232
+ for (const item of toolbarPart) {
1233
+ if (isToolbarAction(item)) continue;
1234
+ if (item in DEFAULT_TOOL_LABELS || customToolMap.has(item)) {
1235
+ return item;
1236
+ }
1237
+ }
1238
+ }
1239
+ return void 0;
1240
+ }, [customToolMap, toolbarPartIds]);
1241
+ const initialTool = props.initialToolId ?? firstToolbarTool ?? "pen";
1078
1242
  const requestedTheme = props.theme ?? "light";
1079
1243
  react.useEffect(() => {
1080
1244
  if (requestedTheme !== "system" || typeof window === "undefined") return;
@@ -1099,7 +1263,12 @@ function Tsdraw(props) {
1099
1263
  canvasCursor: defaultCanvasCursor,
1100
1264
  cursorContext,
1101
1265
  toolOverlay,
1266
+ isPersistenceReady,
1102
1267
  showStylePanel,
1268
+ canUndo,
1269
+ canRedo,
1270
+ undo,
1271
+ redo,
1103
1272
  setTool,
1104
1273
  applyDrawStyle,
1105
1274
  handleResizePointerDown,
@@ -1129,6 +1298,51 @@ function Tsdraw(props) {
1129
1298
  );
1130
1299
  const overlayNode = props.uiOptions?.overlays?.renderToolOverlay?.({ defaultOverlay: defaultToolOverlay, overlayState: toolOverlay, currentTool }) ?? defaultToolOverlay;
1131
1300
  const customElements = props.uiOptions?.customElements ?? [];
1301
+ const toolbarParts = react.useMemo(
1302
+ () => toolbarPartIds.map((toolbarPart, partIndex) => {
1303
+ const items = toolbarPart.map((item) => {
1304
+ if (item === "undo") {
1305
+ return {
1306
+ type: "action",
1307
+ id: "undo",
1308
+ label: "Undo",
1309
+ disabled: !canUndo,
1310
+ onSelect: undo
1311
+ };
1312
+ }
1313
+ if (item === "redo") {
1314
+ return {
1315
+ type: "action",
1316
+ id: "redo",
1317
+ label: "Redo",
1318
+ disabled: !canRedo,
1319
+ onSelect: redo
1320
+ };
1321
+ }
1322
+ if (item in DEFAULT_TOOL_LABELS) {
1323
+ return {
1324
+ type: "tool",
1325
+ id: item,
1326
+ label: DEFAULT_TOOL_LABELS[item],
1327
+ icon: (isActive) => getDefaultToolbarIcon(item, isActive)
1328
+ };
1329
+ }
1330
+ const customTool = customToolMap.get(item);
1331
+ if (!customTool) return null;
1332
+ return {
1333
+ type: "tool",
1334
+ id: customTool.id,
1335
+ label: customTool.label,
1336
+ icon: (isActive) => isActive && customTool.iconSelected ? customTool.iconSelected : customTool.icon
1337
+ };
1338
+ }).filter((nextItem) => nextItem != null);
1339
+ return {
1340
+ id: `toolbar-part-${partIndex.toString(36)}`,
1341
+ items
1342
+ };
1343
+ }).filter((part) => part.items.length > 0),
1344
+ [canRedo, canUndo, customToolMap, redo, toolbarPartIds, undo]
1345
+ );
1132
1346
  return /* @__PURE__ */ jsxRuntime.jsxs(
1133
1347
  "div",
1134
1348
  {
@@ -1172,7 +1386,7 @@ function Tsdraw(props) {
1172
1386
  /* @__PURE__ */ jsxRuntime.jsx(
1173
1387
  StylePanel,
1174
1388
  {
1175
- visible: showStylePanel,
1389
+ visible: isPersistenceReady && showStylePanel,
1176
1390
  style: stylePanelPlacementStyle,
1177
1391
  theme: resolvedTheme,
1178
1392
  drawColor,
@@ -1199,10 +1413,11 @@ function Tsdraw(props) {
1199
1413
  /* @__PURE__ */ jsxRuntime.jsx(
1200
1414
  Toolbar,
1201
1415
  {
1202
- items: toolbarItems,
1416
+ parts: toolbarParts,
1203
1417
  style: toolbarPlacementStyle,
1204
- currentTool,
1205
- onToolChange: setTool
1418
+ currentTool: isPersistenceReady ? currentTool : null,
1419
+ onToolChange: setTool,
1420
+ disabled: !isPersistenceReady
1206
1421
  }
1207
1422
  )
1208
1423
  ]