@tsdraw/react 0.6.2 → 0.8.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
@@ -100,66 +100,106 @@ function SelectionOverlay({
100
100
  }
101
101
  var STYLE_COLORS = Object.entries(core.DEFAULT_COLORS).filter(([key]) => key !== "white").map(([value]) => ({ value }));
102
102
  var STYLE_DASHES = ["draw", "solid", "dashed", "dotted"];
103
+ var STYLE_FILLS = ["none", "blank", "semi", "solid"];
103
104
  var STYLE_SIZES = ["s", "m", "l", "xl"];
104
105
  function StylePanel({
105
106
  visible,
107
+ parts,
108
+ customParts,
106
109
  style,
107
110
  theme,
108
111
  drawColor,
109
112
  drawDash,
113
+ drawFill,
110
114
  drawSize,
111
115
  onColorSelect,
112
116
  onDashSelect,
117
+ onFillSelect,
113
118
  onSizeSelect
114
119
  }) {
115
- if (!visible) return null;
116
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "tsdraw-style-panel", style, "aria-label": "Draw style panel", children: [
117
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "tsdraw-style-colors", children: STYLE_COLORS.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
118
- "button",
119
- {
120
- type: "button",
121
- className: "tsdraw-style-color",
122
- "data-active": drawColor === item.value ? "true" : void 0,
123
- "aria-label": `Color ${item.value}`,
124
- title: item.value,
125
- onClick: () => onColorSelect(item.value),
126
- children: /* @__PURE__ */ jsxRuntime.jsx(
127
- "span",
128
- {
129
- className: "tsdraw-style-color-dot",
130
- style: { background: core.resolveThemeColor(item.value, theme) }
131
- }
132
- )
133
- },
134
- item.value
135
- )) }),
136
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "tsdraw-style-section", children: STYLE_DASHES.map((dash) => /* @__PURE__ */ jsxRuntime.jsx(
137
- "button",
138
- {
139
- type: "button",
140
- className: "tsdraw-style-row",
141
- "data-active": drawDash === dash ? "true" : void 0,
142
- "aria-label": `Stroke ${dash}`,
143
- title: dash,
144
- onClick: () => onDashSelect(dash),
145
- children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "tsdraw-style-preview", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: `tsdraw-style-preview-line tsdraw-style-preview-line--${dash}` }) })
146
- },
147
- dash
148
- )) }),
149
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "tsdraw-style-section", children: STYLE_SIZES.map((size) => /* @__PURE__ */ jsxRuntime.jsx(
150
- "button",
151
- {
152
- type: "button",
153
- className: "tsdraw-style-row",
154
- "data-active": drawSize === size ? "true" : void 0,
155
- "aria-label": `Thickness ${size}`,
156
- title: size,
157
- onClick: () => onSizeSelect(size),
158
- children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "tsdraw-style-preview", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: `tsdraw-style-size tsdraw-style-size--${size}` }) })
159
- },
160
- size
161
- )) })
162
- ] });
120
+ if (!visible || parts.length === 0) return null;
121
+ const context = {
122
+ drawColor,
123
+ drawDash,
124
+ drawFill,
125
+ drawSize,
126
+ onColorSelect,
127
+ onDashSelect,
128
+ onFillSelect,
129
+ onSizeSelect
130
+ };
131
+ const customPartMap = new Map((customParts ?? []).map((customPart) => [customPart.id, customPart]));
132
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "tsdraw-style-panel", style, "aria-label": "Draw style panel", children: parts.map((part) => {
133
+ if (part === "colors") {
134
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "tsdraw-style-colors", children: STYLE_COLORS.map((item) => /* @__PURE__ */ jsxRuntime.jsx(
135
+ "button",
136
+ {
137
+ type: "button",
138
+ className: "tsdraw-style-color",
139
+ "data-active": drawColor === item.value ? "true" : void 0,
140
+ "aria-label": `Color ${item.value}`,
141
+ title: item.value,
142
+ onClick: () => onColorSelect(item.value),
143
+ children: /* @__PURE__ */ jsxRuntime.jsx(
144
+ "span",
145
+ {
146
+ className: "tsdraw-style-color-dot",
147
+ style: { background: core.resolveThemeColor(item.value, theme) }
148
+ }
149
+ )
150
+ },
151
+ item.value
152
+ )) }, part);
153
+ }
154
+ if (part === "dashes") {
155
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "tsdraw-style-section", children: STYLE_DASHES.map((dash) => /* @__PURE__ */ jsxRuntime.jsx(
156
+ "button",
157
+ {
158
+ type: "button",
159
+ className: "tsdraw-style-row",
160
+ "data-active": drawDash === dash ? "true" : void 0,
161
+ "aria-label": `Stroke ${dash}`,
162
+ title: dash,
163
+ onClick: () => onDashSelect(dash),
164
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "tsdraw-style-preview", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: `tsdraw-style-preview-line tsdraw-style-preview-line--${dash}` }) })
165
+ },
166
+ dash
167
+ )) }, part);
168
+ }
169
+ if (part === "fills") {
170
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "tsdraw-style-section", children: STYLE_FILLS.map((fill) => /* @__PURE__ */ jsxRuntime.jsx(
171
+ "button",
172
+ {
173
+ type: "button",
174
+ className: "tsdraw-style-row",
175
+ "data-active": drawFill === fill ? "true" : void 0,
176
+ "aria-label": `Fill ${fill}`,
177
+ title: fill,
178
+ onClick: () => onFillSelect(fill),
179
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "tsdraw-style-preview", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: `tsdraw-style-fill tsdraw-style-fill--${fill}` }) })
180
+ },
181
+ fill
182
+ )) }, part);
183
+ }
184
+ if (part === "sizes") {
185
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "tsdraw-style-section", children: STYLE_SIZES.map((size) => /* @__PURE__ */ jsxRuntime.jsx(
186
+ "button",
187
+ {
188
+ type: "button",
189
+ className: "tsdraw-style-row",
190
+ "data-active": drawSize === size ? "true" : void 0,
191
+ "aria-label": `Thickness ${size}`,
192
+ title: size,
193
+ onClick: () => onSizeSelect(size),
194
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "tsdraw-style-preview", children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: `tsdraw-style-size tsdraw-style-size--${size}` }) })
195
+ },
196
+ size
197
+ )) }, part);
198
+ }
199
+ const customPart = customPartMap.get(part);
200
+ if (!customPart) return null;
201
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "tsdraw-style-section tsdraw-style-section--custom", children: customPart.render(context) }, part);
202
+ }) });
163
203
  }
164
204
  function ToolOverlay({
165
205
  visible,
@@ -199,6 +239,8 @@ function ToolOverlay({
199
239
  function getDefaultToolbarIcon(toolId, isActive) {
200
240
  if (toolId === "select") return /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconPointer, { size: 16, stroke: 1.8, fill: isActive ? "currentColor" : "none" });
201
241
  if (toolId === "pen") return /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconPencil, { size: 16, stroke: 1.8, fill: isActive ? "currentColor" : "none" });
242
+ if (toolId === "square") return /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconSquare, { size: 16, stroke: 1.8, fill: isActive ? "currentColor" : "none" });
243
+ if (toolId === "circle") return /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconCircle, { size: 16, stroke: 1.8, fill: isActive ? "currentColor" : "none" });
202
244
  if (toolId === "eraser") return /* @__PURE__ */ jsxRuntime.jsx(iconsReact.IconEraser, { size: 16, stroke: 1.8, fill: isActive ? "currentColor" : "none" });
203
245
  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
246
  return null;
@@ -255,6 +297,249 @@ function getCanvasCursor(currentTool, state) {
255
297
  return state.showToolOverlay ? "none" : "crosshair";
256
298
  }
257
299
 
300
+ // src/canvas/touchInteractions.ts
301
+ var TAP_MAX_DURATION_MS = 100;
302
+ var DOUBLE_TAP_INTERVAL_MS = 100;
303
+ var TAP_MOVE_TOLERANCE = 14;
304
+ var PINCH_MODE_ZOOM_DISTANCE = 24;
305
+ var PINCH_MODE_PAN_DISTANCE = 16;
306
+ var PINCH_MODE_SWITCH_TO_ZOOM_DISTANCE = 64;
307
+ function createTouchInteractionController(editor, canvas, handlers) {
308
+ const activeTouchPoints = /* @__PURE__ */ new Map();
309
+ const touchTapState = {
310
+ active: false,
311
+ startTime: 0,
312
+ maxTouchCount: 0,
313
+ moved: false,
314
+ startPoints: /* @__PURE__ */ new Map(),
315
+ lastTapAtByCount: {}
316
+ };
317
+ const touchCameraState = {
318
+ active: false,
319
+ mode: "not-sure",
320
+ previousCenter: { x: 0, y: 0 },
321
+ initialCenter: { x: 0, y: 0 },
322
+ previousDistance: 1,
323
+ initialDistance: 1,
324
+ previousAngle: 0
325
+ };
326
+ const isTouchPointer = (event) => event.pointerType === "touch";
327
+ const endTouchCameraGesture = () => {
328
+ touchCameraState.active = false;
329
+ touchCameraState.mode = "not-sure";
330
+ touchCameraState.previousDistance = 1;
331
+ touchCameraState.initialDistance = 1;
332
+ touchCameraState.previousAngle = 0;
333
+ };
334
+ const maybeHandleTouchTapGesture = () => {
335
+ if (activeTouchPoints.size > 0) return;
336
+ if (!touchTapState.active) return;
337
+ const elapsed = performance.now() - touchTapState.startTime;
338
+ if (!touchTapState.moved && elapsed <= TAP_MAX_DURATION_MS && (touchTapState.maxTouchCount === 2 || touchTapState.maxTouchCount === 3)) {
339
+ const fingerCount = touchTapState.maxTouchCount;
340
+ const now = performance.now();
341
+ const previousTapTime = touchTapState.lastTapAtByCount[fingerCount] ?? 0;
342
+ const isDoubleTap = previousTapTime > 0 && now - previousTapTime <= DOUBLE_TAP_INTERVAL_MS;
343
+ if (isDoubleTap) {
344
+ touchTapState.lastTapAtByCount[fingerCount] = 0;
345
+ if (fingerCount === 2) {
346
+ if (handlers.runUndo()) handlers.refreshView();
347
+ } else if (handlers.runRedo()) handlers.refreshView();
348
+ } else touchTapState.lastTapAtByCount[fingerCount] = now;
349
+ }
350
+ touchTapState.active = false;
351
+ touchTapState.startPoints.clear();
352
+ touchTapState.maxTouchCount = 0;
353
+ touchTapState.moved = false;
354
+ };
355
+ const beginTouchCameraGesture = () => {
356
+ const points = [...activeTouchPoints.values()];
357
+ if (points.length !== 2) return;
358
+ handlers.cancelActivePointerInteraction();
359
+ const first = points[0];
360
+ const second = points[1];
361
+ const center = { x: (first.x + second.x) / 2, y: (first.y + second.y) / 2 };
362
+ const distance = Math.hypot(second.x - first.x, second.y - first.y);
363
+ const angle = Math.atan2(second.y - first.y, second.x - first.x);
364
+ touchCameraState.active = true;
365
+ touchCameraState.mode = "not-sure";
366
+ touchCameraState.previousCenter = center;
367
+ touchCameraState.initialCenter = center;
368
+ touchCameraState.previousDistance = Math.max(1, distance);
369
+ touchCameraState.initialDistance = Math.max(1, distance);
370
+ touchCameraState.previousAngle = angle;
371
+ };
372
+ const updateTouchCameraGesture = () => {
373
+ if (!touchCameraState.active) return false;
374
+ const points = [...activeTouchPoints.values()];
375
+ if (points.length !== 2) {
376
+ endTouchCameraGesture();
377
+ return false;
378
+ }
379
+ const first = points[0];
380
+ const second = points[1];
381
+ const center = { x: (first.x + second.x) / 2, y: (first.y + second.y) / 2 };
382
+ const distance = Math.max(1, Math.hypot(second.x - first.x, second.y - first.y));
383
+ const angle = Math.atan2(second.y - first.y, second.x - first.x);
384
+ const centerDx = center.x - touchCameraState.previousCenter.x;
385
+ const centerDy = center.y - touchCameraState.previousCenter.y;
386
+ const touchDistance = Math.abs(distance - touchCameraState.initialDistance);
387
+ const originDistance = Math.hypot(center.x - touchCameraState.initialCenter.x, center.y - touchCameraState.initialCenter.y);
388
+ if (touchCameraState.mode === "not-sure") {
389
+ if (touchDistance > PINCH_MODE_ZOOM_DISTANCE) touchCameraState.mode = "zooming";
390
+ else if (originDistance > PINCH_MODE_PAN_DISTANCE) touchCameraState.mode = "panning";
391
+ } else if (touchCameraState.mode === "panning" && touchDistance > PINCH_MODE_SWITCH_TO_ZOOM_DISTANCE) touchCameraState.mode = "zooming";
392
+ const canvasRect = canvas.getBoundingClientRect();
393
+ const centerOnCanvasX = center.x - canvasRect.left;
394
+ const centerOnCanvasY = center.y - canvasRect.top;
395
+ editor.panBy(centerDx, centerDy);
396
+ if (touchCameraState.mode === "zooming") {
397
+ const zoomFactor = distance / touchCameraState.previousDistance;
398
+ editor.zoomAt(zoomFactor, centerOnCanvasX, centerOnCanvasY);
399
+ editor.rotateAt(angle - touchCameraState.previousAngle, centerOnCanvasX, centerOnCanvasY);
400
+ }
401
+ touchCameraState.previousCenter = center;
402
+ touchCameraState.previousDistance = distance;
403
+ touchCameraState.previousAngle = angle;
404
+ handlers.refreshView();
405
+ return true;
406
+ };
407
+ const handlePointerDown = (event) => {
408
+ if (!isTouchPointer(event)) return false;
409
+ activeTouchPoints.set(event.pointerId, { x: event.clientX, y: event.clientY });
410
+ if (!touchTapState.active) {
411
+ touchTapState.active = true;
412
+ touchTapState.startTime = performance.now();
413
+ touchTapState.maxTouchCount = activeTouchPoints.size;
414
+ touchTapState.moved = false;
415
+ touchTapState.startPoints.clear();
416
+ } else {
417
+ touchTapState.maxTouchCount = Math.max(touchTapState.maxTouchCount, activeTouchPoints.size);
418
+ }
419
+ touchTapState.startPoints.set(event.pointerId, { x: event.clientX, y: event.clientY });
420
+ if (activeTouchPoints.size === 2) {
421
+ beginTouchCameraGesture();
422
+ return true;
423
+ }
424
+ return false;
425
+ };
426
+ const handlePointerMove = (event) => {
427
+ if (!isTouchPointer(event)) return false;
428
+ if (activeTouchPoints.has(event.pointerId)) activeTouchPoints.set(event.pointerId, { x: event.clientX, y: event.clientY });
429
+ const tapStart = touchTapState.startPoints.get(event.pointerId);
430
+ if (tapStart) {
431
+ const moved = Math.hypot(event.clientX - tapStart.x, event.clientY - tapStart.y);
432
+ if (moved > TAP_MOVE_TOLERANCE) touchTapState.moved = true;
433
+ }
434
+ return updateTouchCameraGesture();
435
+ };
436
+ const handlePointerUpOrCancel = (event) => {
437
+ if (!isTouchPointer(event)) return false;
438
+ const wasCameraGestureActive = touchCameraState.active;
439
+ activeTouchPoints.delete(event.pointerId);
440
+ touchTapState.startPoints.delete(event.pointerId);
441
+ if (activeTouchPoints.size < 2) endTouchCameraGesture();
442
+ maybeHandleTouchTapGesture();
443
+ return wasCameraGestureActive;
444
+ };
445
+ let gestureLastScale = 1;
446
+ let gestureActive = false;
447
+ const handleGestureEvent = (event, container) => {
448
+ if (!container.contains(event.target)) return;
449
+ event.preventDefault();
450
+ const gestureEvent = event;
451
+ if (gestureEvent.scale == null) return;
452
+ if (event.type === "gesturestart") {
453
+ gestureLastScale = gestureEvent.scale;
454
+ gestureActive = true;
455
+ return;
456
+ }
457
+ if (event.type === "gestureend") {
458
+ gestureActive = false;
459
+ gestureLastScale = 1;
460
+ return;
461
+ }
462
+ if (event.type === "gesturechange" && gestureActive) {
463
+ const zoomFactor = gestureEvent.scale / gestureLastScale;
464
+ gestureLastScale = gestureEvent.scale;
465
+ const canvasRect = canvas.getBoundingClientRect();
466
+ const cx = (gestureEvent.clientX ?? canvasRect.left + canvasRect.width / 2) - canvasRect.left;
467
+ const cy = (gestureEvent.clientY ?? canvasRect.top + canvasRect.height / 2) - canvasRect.top;
468
+ editor.zoomAt(zoomFactor, cx, cy);
469
+ handlers.refreshView();
470
+ }
471
+ };
472
+ const reset = () => {
473
+ activeTouchPoints.clear();
474
+ touchTapState.active = false;
475
+ touchTapState.startPoints.clear();
476
+ endTouchCameraGesture();
477
+ };
478
+ return {
479
+ handlePointerDown,
480
+ handlePointerMove,
481
+ handlePointerUpOrCancel,
482
+ handleGestureEvent,
483
+ reset,
484
+ isCameraGestureActive: () => touchCameraState.active,
485
+ isTrackpadZoomActive: () => gestureActive
486
+ };
487
+ }
488
+
489
+ // src/canvas/keyboardShortcuts.ts
490
+ var TOOL_SHORTCUTS = {
491
+ v: "select",
492
+ h: "hand",
493
+ e: "eraser",
494
+ p: "pen",
495
+ b: "pen",
496
+ d: "pen",
497
+ x: "pen",
498
+ r: "square",
499
+ o: "circle",
500
+ c: "circle"
501
+ };
502
+ function isEditableTarget(eventTarget) {
503
+ const element = eventTarget;
504
+ if (!element) return false;
505
+ if (element.isContentEditable) return true;
506
+ const tagName = element.tagName;
507
+ return tagName === "INPUT" || tagName === "TEXTAREA" || tagName === "SELECT";
508
+ }
509
+ function handleKeyboardShortcutKeyDown(event, handlers) {
510
+ if (isEditableTarget(event.target)) return;
511
+ const loweredKey = event.key.toLowerCase();
512
+ const isMetaPressed = event.metaKey || event.ctrlKey;
513
+ if (isMetaPressed && (loweredKey === "z" || loweredKey === "y")) {
514
+ const shouldRedo = loweredKey === "y" || loweredKey === "z" && event.shiftKey;
515
+ if (handlers.runHistoryShortcut(shouldRedo)) {
516
+ event.preventDefault();
517
+ event.stopPropagation();
518
+ return;
519
+ }
520
+ }
521
+ if (!isMetaPressed && !event.altKey) {
522
+ const nextToolId = TOOL_SHORTCUTS[loweredKey];
523
+ if (nextToolId && handlers.isToolAvailable(nextToolId)) {
524
+ handlers.setToolFromShortcut(nextToolId);
525
+ event.preventDefault();
526
+ return;
527
+ }
528
+ }
529
+ if (event.key === "Delete" || event.key === "Backspace") {
530
+ if (handlers.deleteSelection()) {
531
+ event.preventDefault();
532
+ event.stopPropagation();
533
+ return;
534
+ }
535
+ }
536
+ handlers.dispatchKeyDown(event);
537
+ }
538
+ function handleKeyboardShortcutKeyUp(event, handlers) {
539
+ if (isEditableTarget(event.target)) return;
540
+ handlers.dispatchKeyUp(event);
541
+ }
542
+
258
543
  // src/persistence/localIndexedDb.ts
259
544
  var DATABASE_PREFIX = "tsdraw_v1_";
260
545
  var DATABASE_VERSION = 2;
@@ -378,25 +663,35 @@ function getOrCreateSessionId() {
378
663
 
379
664
  // src/canvas/useTsdrawCanvasController.ts
380
665
  function toScreenRect(editor, bounds) {
381
- const { x, y, zoom } = editor.viewport;
666
+ const topLeft = core.pageToScreen(editor.viewport, bounds.minX, bounds.minY);
667
+ const topRight = core.pageToScreen(editor.viewport, bounds.maxX, bounds.minY);
668
+ const bottomLeft = core.pageToScreen(editor.viewport, bounds.minX, bounds.maxY);
669
+ const bottomRight = core.pageToScreen(editor.viewport, bounds.maxX, bounds.maxY);
670
+ const minX = Math.min(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x);
671
+ const minY = Math.min(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y);
672
+ const maxX = Math.max(topLeft.x, topRight.x, bottomLeft.x, bottomRight.x);
673
+ const maxY = Math.max(topLeft.y, topRight.y, bottomLeft.y, bottomRight.y);
382
674
  return {
383
- left: bounds.minX * zoom + x,
384
- top: bounds.minY * zoom + y,
385
- width: (bounds.maxX - bounds.minX) * zoom,
386
- height: (bounds.maxY - bounds.minY) * zoom
675
+ left: minX,
676
+ top: minY,
677
+ width: Math.max(0, maxX - minX),
678
+ height: Math.max(0, maxY - minY)
387
679
  };
388
680
  }
389
681
  function resolveDrawColor(colorStyle, theme) {
390
682
  return core.resolveThemeColor(colorStyle, theme);
391
683
  }
684
+ var ZOOM_WHEEL_CAP = 10;
392
685
  function useTsdrawCanvasController(options = {}) {
393
- const stylePanelToolIds = options.stylePanelToolIds ?? ["pen"];
394
- const stylePanelToolIdsRef = react.useRef(stylePanelToolIds);
395
686
  const onMountRef = react.useRef(options.onMount);
396
687
  const containerRef = react.useRef(null);
397
688
  const canvasRef = react.useRef(null);
398
689
  const editorRef = react.useRef(null);
399
690
  const dprRef = react.useRef(1);
691
+ const penDetectedRef = react.useRef(false);
692
+ const penModeRef = react.useRef(false);
693
+ const lastPointerDownWithRef = react.useRef("mouse");
694
+ const activePointerIdsRef = react.useRef(/* @__PURE__ */ new Set());
400
695
  const lastPointerClientRef = react.useRef(null);
401
696
  const currentToolRef = react.useRef(options.initialTool ?? "pen");
402
697
  const selectedShapeIdsRef = react.useRef([]);
@@ -426,6 +721,7 @@ function useTsdrawCanvasController(options = {}) {
426
721
  const [currentTool, setCurrentToolState] = react.useState(options.initialTool ?? "pen");
427
722
  const [drawColor, setDrawColor] = react.useState("black");
428
723
  const [drawDash, setDrawDash] = react.useState("draw");
724
+ const [drawFill, setDrawFill] = react.useState("none");
429
725
  const [drawSize, setDrawSize] = react.useState("m");
430
726
  const [selectedShapeIds, setSelectedShapeIds] = react.useState([]);
431
727
  const [selectionBrush, setSelectionBrush] = react.useState(null);
@@ -442,9 +738,6 @@ function useTsdrawCanvasController(options = {}) {
442
738
  react.useEffect(() => {
443
739
  currentToolRef.current = currentTool;
444
740
  }, [currentTool]);
445
- react.useEffect(() => {
446
- stylePanelToolIdsRef.current = stylePanelToolIds;
447
- }, [stylePanelToolIds]);
448
741
  react.useEffect(() => {
449
742
  onMountRef.current = options.onMount;
450
743
  }, [options.onMount]);
@@ -456,7 +749,7 @@ function useTsdrawCanvasController(options = {}) {
456
749
  }, [selectionRotationDeg]);
457
750
  react.useEffect(() => {
458
751
  schedulePersistRef.current?.();
459
- }, [selectedShapeIds, currentTool, drawColor, drawDash, drawSize]);
752
+ }, [selectedShapeIds, currentTool, drawColor, drawDash, drawFill, drawSize]);
460
753
  const render = react.useCallback(() => {
461
754
  const canvas = canvasRef.current;
462
755
  const editor = editorRef.current;
@@ -592,6 +885,7 @@ function useTsdrawCanvasController(options = {}) {
592
885
  const initialStyle = editor.getCurrentDrawStyle();
593
886
  setDrawColor(initialStyle.color);
594
887
  setDrawDash(initialStyle.dash);
888
+ setDrawFill(initialStyle.fill);
595
889
  setDrawSize(initialStyle.size);
596
890
  const resize = () => {
597
891
  const dpr = window.devicePixelRatio ?? 1;
@@ -654,9 +948,9 @@ function useTsdrawCanvasController(options = {}) {
654
948
  }
655
949
  refreshSelectionBounds(editor, nextSelectedShapeIds);
656
950
  };
657
- const applyRemoteDocumentSnapshot = (document) => {
951
+ const applyRemoteDocumentSnapshot = (document2) => {
658
952
  ignorePersistenceChanges = true;
659
- editor.loadDocumentSnapshot(document);
953
+ editor.loadDocumentSnapshot(document2);
660
954
  editor.clearRedoHistory();
661
955
  reconcileSelectionAfterDocumentLoad();
662
956
  render();
@@ -673,6 +967,7 @@ function useTsdrawCanvasController(options = {}) {
673
967
  const nextDrawStyle = editor.getCurrentDrawStyle();
674
968
  setDrawColor(nextDrawStyle.color);
675
969
  setDrawDash(nextDrawStyle.dash);
970
+ setDrawFill(nextDrawStyle.fill);
676
971
  setDrawSize(nextDrawStyle.size);
677
972
  setSelectionRotationDeg(0);
678
973
  render();
@@ -687,8 +982,95 @@ function useTsdrawCanvasController(options = {}) {
687
982
  const coalesced = e.getCoalescedEvents?.();
688
983
  return coalesced && coalesced.length > 0 ? coalesced : [e];
689
984
  };
985
+ const applyDocumentChangeResult = (changed) => {
986
+ if (!changed) return false;
987
+ reconcileSelectionAfterDocumentLoad();
988
+ setSelectionRotationDeg(0);
989
+ render();
990
+ syncHistoryState();
991
+ return true;
992
+ };
993
+ const normalizeWheelDelta = (event) => {
994
+ let deltaX = event.deltaX;
995
+ let deltaY = event.deltaY;
996
+ let deltaZoom = 0;
997
+ if (event.ctrlKey || event.metaKey || event.altKey) {
998
+ const clamped = Math.abs(deltaY) > ZOOM_WHEEL_CAP ? ZOOM_WHEEL_CAP * Math.sign(deltaY) : deltaY;
999
+ deltaZoom = -clamped / 100;
1000
+ } else if (event.shiftKey && !navigator.userAgent.includes("Mac") && !navigator.userAgent.includes("iPhone") && !navigator.userAgent.includes("iPad")) {
1001
+ deltaX = deltaY;
1002
+ deltaY = 0;
1003
+ }
1004
+ return { x: -deltaX, y: -deltaY, z: deltaZoom };
1005
+ };
1006
+ const deleteCurrentSelection = () => {
1007
+ const selectedIds = selectedShapeIdsRef.current;
1008
+ if (selectedIds.length === 0) return false;
1009
+ editor.beginHistoryEntry();
1010
+ editor.deleteShapes(selectedIds);
1011
+ editor.endHistoryEntry();
1012
+ setSelectedShapeIds([]);
1013
+ selectedShapeIdsRef.current = [];
1014
+ setSelectionBounds(null);
1015
+ setSelectionBrush(null);
1016
+ setSelectionRotationDeg(0);
1017
+ render();
1018
+ syncHistoryState();
1019
+ return true;
1020
+ };
1021
+ const cancelActivePointerInteraction = () => {
1022
+ if (!isPointerActiveRef.current) return;
1023
+ isPointerActiveRef.current = false;
1024
+ lastPointerClientRef.current = null;
1025
+ editor.input.pointerUp();
1026
+ if (currentToolRef.current === "select") {
1027
+ const dragMode = selectDragRef.current.mode;
1028
+ if (dragMode === "rotate") setIsRotatingSelection(false);
1029
+ if (dragMode === "resize") setIsResizingSelection(false);
1030
+ if (dragMode === "move") setIsMovingSelection(false);
1031
+ if (dragMode === "marquee") setSelectionBrush(null);
1032
+ selectDragRef.current.mode = "none";
1033
+ } else {
1034
+ editor.tools.pointerUp();
1035
+ }
1036
+ editor.endHistoryEntry();
1037
+ render();
1038
+ refreshSelectionBounds(editor);
1039
+ };
1040
+ const touchInteractions = createTouchInteractionController(editor, canvas, {
1041
+ cancelActivePointerInteraction,
1042
+ refreshView: () => {
1043
+ render();
1044
+ refreshSelectionBounds(editor);
1045
+ },
1046
+ runUndo: () => applyDocumentChangeResult(editor.undo()),
1047
+ runRedo: () => applyDocumentChangeResult(editor.redo())
1048
+ });
1049
+ const isDrawingTool = (tool) => tool !== "select" && tool !== "hand";
1050
+ const hasRealPressure = (pressure) => pressure != null && pressure > 0 && pressure !== 0.5;
690
1051
  const handlePointerDown = (e) => {
691
1052
  if (!canvas.contains(e.target)) return;
1053
+ if (!penDetectedRef.current && (e.pointerType === "pen" || hasRealPressure(e.pressure))) {
1054
+ penDetectedRef.current = true;
1055
+ penModeRef.current = true;
1056
+ }
1057
+ lastPointerDownWithRef.current = e.pointerType;
1058
+ activePointerIdsRef.current.add(e.pointerId);
1059
+ const startedCameraGesture = touchInteractions.handlePointerDown(e);
1060
+ if (startedCameraGesture || touchInteractions.isCameraGestureActive()) {
1061
+ e.preventDefault();
1062
+ if (!canvas.hasPointerCapture(e.pointerId)) {
1063
+ canvas.setPointerCapture(e.pointerId);
1064
+ }
1065
+ return;
1066
+ }
1067
+ const allowPointerDown = !penModeRef.current || e.pointerType !== "touch" || !isDrawingTool(currentToolRef.current);
1068
+ if (!allowPointerDown) {
1069
+ return;
1070
+ }
1071
+ if (activePointerIdsRef.current.size > 1) {
1072
+ return;
1073
+ }
692
1074
  isPointerActiveRef.current = true;
693
1075
  editor.beginHistoryEntry();
694
1076
  canvas.setPointerCapture(e.pointerId);
@@ -697,7 +1079,7 @@ function useTsdrawCanvasController(options = {}) {
697
1079
  const first = sampleEvents(e)[0];
698
1080
  const { x, y } = getPagePoint(first);
699
1081
  const pressure = first.pressure ?? 0.5;
700
- const isPen = first.pointerType === "pen" || first.pointerType === "touch";
1082
+ const isPen = first.pointerType === "pen" || hasRealPressure(first.pressure);
701
1083
  if (currentToolRef.current === "select") {
702
1084
  const hit = core.getTopShapeAtPoint(editor, { x, y });
703
1085
  const isHitSelected = !!(hit && selectedShapeIdsRef.current.includes(hit.id));
@@ -737,6 +1119,16 @@ function useTsdrawCanvasController(options = {}) {
737
1119
  refreshSelectionBounds(editor);
738
1120
  };
739
1121
  const handlePointerMove = (e) => {
1122
+ if (!penDetectedRef.current && (e.pointerType === "pen" || hasRealPressure(e.pressure))) {
1123
+ penDetectedRef.current = true;
1124
+ penModeRef.current = true;
1125
+ }
1126
+ if (touchInteractions.handlePointerMove(e)) {
1127
+ e.preventDefault();
1128
+ return;
1129
+ }
1130
+ if (penModeRef.current && e.pointerType === "touch" && isDrawingTool(currentToolRef.current) && !isPointerActiveRef.current) return;
1131
+ if (activePointerIdsRef.current.size > 1) return;
740
1132
  updatePointerPreview(e.clientX, e.clientY);
741
1133
  const prevClient = lastPointerClientRef.current;
742
1134
  const dx = prevClient ? e.clientX - prevClient.x : 0;
@@ -745,7 +1137,7 @@ function useTsdrawCanvasController(options = {}) {
745
1137
  for (const sample of sampleEvents(e)) {
746
1138
  const { x, y } = getPagePoint(sample);
747
1139
  const pressure = sample.pressure ?? 0.5;
748
- const isPen = sample.pointerType === "pen" || sample.pointerType === "touch";
1140
+ const isPen = sample.pointerType === "pen" || hasRealPressure(sample.pressure);
749
1141
  editor.input.pointerMove(x, y, pressure, isPen);
750
1142
  }
751
1143
  if (currentToolRef.current === "select") {
@@ -795,6 +1187,13 @@ function useTsdrawCanvasController(options = {}) {
795
1187
  refreshSelectionBounds(editor);
796
1188
  };
797
1189
  const handlePointerUp = (e) => {
1190
+ activePointerIdsRef.current.delete(e.pointerId);
1191
+ const hadTouchCameraGesture = touchInteractions.handlePointerUpOrCancel(e);
1192
+ if (hadTouchCameraGesture || touchInteractions.isCameraGestureActive()) {
1193
+ e.preventDefault();
1194
+ return;
1195
+ }
1196
+ if (!isPointerActiveRef.current) return;
798
1197
  isPointerActiveRef.current = false;
799
1198
  lastPointerClientRef.current = null;
800
1199
  updatePointerPreview(e.clientX, e.clientY);
@@ -878,7 +1277,10 @@ function useTsdrawCanvasController(options = {}) {
878
1277
  }
879
1278
  editor.endHistoryEntry();
880
1279
  };
881
- const handlePointerCancel = () => {
1280
+ const handlePointerCancel = (e) => {
1281
+ activePointerIdsRef.current.delete(e.pointerId);
1282
+ const hadTouchCameraGesture = touchInteractions.handlePointerUpOrCancel(e);
1283
+ if (hadTouchCameraGesture || touchInteractions.isCameraGestureActive()) return;
882
1284
  if (!isPointerActiveRef.current) return;
883
1285
  isPointerActiveRef.current = false;
884
1286
  lastPointerClientRef.current = null;
@@ -907,31 +1309,58 @@ function useTsdrawCanvasController(options = {}) {
907
1309
  applyRemoteDocumentSnapshot(pending);
908
1310
  }
909
1311
  };
910
- const handleKeyDown = (e) => {
911
- const isMetaPressed = e.metaKey || e.ctrlKey;
912
- const loweredKey = e.key.toLowerCase();
913
- const isUndoOrRedoKey = loweredKey === "z" || loweredKey === "y";
914
- if (isMetaPressed && isUndoOrRedoKey) {
915
- const shouldRedo = loweredKey === "y" || loweredKey === "z" && e.shiftKey;
916
- const changed = shouldRedo ? editor.redo() : editor.undo();
917
- if (changed) {
918
- e.preventDefault();
919
- e.stopPropagation();
920
- reconcileSelectionAfterDocumentLoad();
921
- setSelectionRotationDeg(0);
922
- render();
923
- syncHistoryState();
924
- return;
925
- }
1312
+ const handleWheel = (e) => {
1313
+ if (!container.contains(e.target)) return;
1314
+ e.preventDefault();
1315
+ if (touchInteractions.isTrackpadZoomActive()) return;
1316
+ const delta = normalizeWheelDelta(e);
1317
+ if (delta.z !== 0) {
1318
+ const rect = canvas.getBoundingClientRect();
1319
+ const pointX = e.clientX - rect.left;
1320
+ const pointY = e.clientY - rect.top;
1321
+ editor.zoomAt(Math.exp(delta.z), pointX, pointY);
1322
+ } else {
1323
+ editor.panBy(delta.x, delta.y);
926
1324
  }
927
- editor.input.setModifiers(e.shiftKey, e.ctrlKey, e.metaKey);
928
- editor.tools.keyDown({ key: e.key });
929
1325
  render();
1326
+ refreshSelectionBounds(editor);
1327
+ };
1328
+ const handleGestureEvent = (e) => {
1329
+ touchInteractions.handleGestureEvent(e, container);
1330
+ };
1331
+ const handleKeyDown = (e) => {
1332
+ handleKeyboardShortcutKeyDown(e, {
1333
+ isToolAvailable: (tool) => editor.tools.hasTool(tool),
1334
+ setToolFromShortcut: (tool) => {
1335
+ editor.setCurrentTool(tool);
1336
+ setCurrentToolState(tool);
1337
+ currentToolRef.current = tool;
1338
+ if (tool !== "select") resetSelectUi();
1339
+ render();
1340
+ },
1341
+ runHistoryShortcut: (shouldRedo) => applyDocumentChangeResult(shouldRedo ? editor.redo() : editor.undo()),
1342
+ deleteSelection: () => currentToolRef.current === "select" ? deleteCurrentSelection() : false,
1343
+ dispatchKeyDown: (event) => {
1344
+ editor.input.setModifiers(event.shiftKey, event.ctrlKey, event.metaKey);
1345
+ editor.tools.keyDown({ key: event.key });
1346
+ render();
1347
+ },
1348
+ dispatchKeyUp: () => void 0
1349
+ });
930
1350
  };
931
1351
  const handleKeyUp = (e) => {
932
- editor.input.setModifiers(e.shiftKey, e.ctrlKey, e.metaKey);
933
- editor.tools.keyUp({ key: e.key });
934
- render();
1352
+ handleKeyboardShortcutKeyUp(e, {
1353
+ isToolAvailable: () => false,
1354
+ setToolFromShortcut: () => void 0,
1355
+ runHistoryShortcut: () => false,
1356
+ deleteSelection: () => false,
1357
+ dispatchKeyDown: () => void 0,
1358
+ dispatchKeyUp: (event) => {
1359
+ editor.input.setModifiers(event.shiftKey, event.ctrlKey, event.metaKey);
1360
+ editor.tools.keyUp({ key: event.key });
1361
+ render();
1362
+ }
1363
+ });
935
1364
  };
936
1365
  const initializePersistence = async () => {
937
1366
  if (!persistenceKey) {
@@ -1008,6 +1437,10 @@ function useTsdrawCanvasController(options = {}) {
1008
1437
  const ro = new ResizeObserver(resize);
1009
1438
  ro.observe(container);
1010
1439
  canvas.addEventListener("pointerdown", handlePointerDown);
1440
+ container.addEventListener("wheel", handleWheel, { passive: false });
1441
+ document.addEventListener("gesturestart", handleGestureEvent);
1442
+ document.addEventListener("gesturechange", handleGestureEvent);
1443
+ document.addEventListener("gestureend", handleGestureEvent);
1011
1444
  window.addEventListener("pointermove", handlePointerMove);
1012
1445
  window.addEventListener("pointerup", handlePointerUp);
1013
1446
  window.addEventListener("pointercancel", handlePointerCancel);
@@ -1051,6 +1484,7 @@ function useTsdrawCanvasController(options = {}) {
1051
1484
  editor.setCurrentDrawStyle(partial);
1052
1485
  if (partial.color) setDrawColor(partial.color);
1053
1486
  if (partial.dash) setDrawDash(partial.dash);
1487
+ if (partial.fill) setDrawFill(partial.fill);
1054
1488
  if (partial.size) setDrawSize(partial.size);
1055
1489
  render();
1056
1490
  }
@@ -1063,13 +1497,19 @@ function useTsdrawCanvasController(options = {}) {
1063
1497
  disposeMount?.();
1064
1498
  ro.disconnect();
1065
1499
  canvas.removeEventListener("pointerdown", handlePointerDown);
1500
+ container.removeEventListener("wheel", handleWheel);
1501
+ document.removeEventListener("gesturestart", handleGestureEvent);
1502
+ document.removeEventListener("gesturechange", handleGestureEvent);
1503
+ document.removeEventListener("gestureend", handleGestureEvent);
1066
1504
  window.removeEventListener("pointermove", handlePointerMove);
1067
1505
  window.removeEventListener("pointerup", handlePointerUp);
1068
1506
  window.removeEventListener("pointercancel", handlePointerCancel);
1069
1507
  window.removeEventListener("keydown", handleKeyDown);
1070
1508
  window.removeEventListener("keyup", handleKeyUp);
1071
1509
  isPointerActiveRef.current = false;
1510
+ activePointerIdsRef.current.clear();
1072
1511
  pendingRemoteDocumentRef.current = null;
1512
+ touchInteractions.reset();
1073
1513
  persistenceChannel?.close();
1074
1514
  void persistenceDb?.close();
1075
1515
  editorRef.current = null;
@@ -1080,6 +1520,7 @@ function useTsdrawCanvasController(options = {}) {
1080
1520
  options.persistenceKey,
1081
1521
  options.toolDefinitions,
1082
1522
  refreshSelectionBounds,
1523
+ resetSelectUi,
1083
1524
  render,
1084
1525
  updatePointerPreview
1085
1526
  ]);
@@ -1108,6 +1549,7 @@ function useTsdrawCanvasController(options = {}) {
1108
1549
  editor.setCurrentDrawStyle(partial);
1109
1550
  if (partial.color) setDrawColor(partial.color);
1110
1551
  if (partial.dash) setDrawDash(partial.dash);
1552
+ if (partial.fill) setDrawFill(partial.fill);
1111
1553
  if (partial.size) setDrawSize(partial.size);
1112
1554
  render();
1113
1555
  },
@@ -1175,6 +1617,7 @@ function useTsdrawCanvasController(options = {}) {
1175
1617
  currentTool,
1176
1618
  drawColor,
1177
1619
  drawDash,
1620
+ drawFill,
1178
1621
  drawSize,
1179
1622
  selectedShapeIds,
1180
1623
  selectionBrush,
@@ -1184,7 +1627,6 @@ function useTsdrawCanvasController(options = {}) {
1184
1627
  cursorContext,
1185
1628
  toolOverlay,
1186
1629
  isPersistenceReady,
1187
- showStylePanel: stylePanelToolIdsRef.current.includes(currentTool),
1188
1630
  canUndo,
1189
1631
  canRedo,
1190
1632
  undo,
@@ -1195,12 +1637,21 @@ function useTsdrawCanvasController(options = {}) {
1195
1637
  handleRotatePointerDown
1196
1638
  };
1197
1639
  }
1198
- var DEFAULT_TOOLBAR_PARTS = [["undo", "redo"], ["select", "hand", "pen", "eraser"]];
1640
+ var DEFAULT_TOOLBAR_PARTS = [["undo", "redo"], ["select", "hand", "pen", "eraser", "square", "circle"]];
1199
1641
  var EMPTY_CUSTOM_TOOLS = [];
1200
1642
  var EMPTY_CUSTOM_ELEMENTS = [];
1643
+ var EMPTY_STYLE_PANEL_PARTS = [];
1644
+ var EMPTY_STYLE_PANEL_CUSTOM_PARTS = [];
1645
+ var DEFAULT_STYLE_PANEL_PARTS_BY_TOOL = {
1646
+ pen: ["colors", "dashes", "sizes"],
1647
+ square: ["colors", "dashes", "fills", "sizes"],
1648
+ circle: ["colors", "dashes", "fills", "sizes"]
1649
+ };
1201
1650
  var DEFAULT_TOOL_LABELS = {
1202
1651
  select: "Select",
1203
1652
  pen: "Pen",
1653
+ square: "Rectangle",
1654
+ circle: "Ellipse",
1204
1655
  eraser: "Eraser",
1205
1656
  hand: "Hand"
1206
1657
  };
@@ -1272,21 +1723,6 @@ function Tsdraw(props) {
1272
1723
  () => customTools.filter((customTool) => toolbarToolIds.has(customTool.id)).map((customTool) => customTool.definition),
1273
1724
  [customTools, toolbarToolIds]
1274
1725
  );
1275
- const stylePanelToolIds = react.useMemo(
1276
- () => {
1277
- const nextToolIds = /* @__PURE__ */ new Set();
1278
- if (toolbarToolIds.has("pen")) {
1279
- nextToolIds.add("pen");
1280
- }
1281
- for (const customTool of customTools) {
1282
- if ((customTool.showStylePanel ?? false) && toolbarToolIds.has(customTool.id)) {
1283
- nextToolIds.add(customTool.id);
1284
- }
1285
- }
1286
- return [...nextToolIds];
1287
- },
1288
- [customTools, toolbarToolIds]
1289
- );
1290
1726
  const firstToolbarTool = react.useMemo(() => {
1291
1727
  for (const toolbarPart of toolbarPartIds) {
1292
1728
  for (const item of toolbarPart) {
@@ -1315,6 +1751,7 @@ function Tsdraw(props) {
1315
1751
  currentTool,
1316
1752
  drawColor,
1317
1753
  drawDash,
1754
+ drawFill,
1318
1755
  drawSize,
1319
1756
  selectedShapeIds,
1320
1757
  selectionBrush,
@@ -1324,7 +1761,6 @@ function Tsdraw(props) {
1324
1761
  cursorContext,
1325
1762
  toolOverlay,
1326
1763
  isPersistenceReady,
1327
- showStylePanel,
1328
1764
  canUndo,
1329
1765
  canRedo,
1330
1766
  undo,
@@ -1338,11 +1774,12 @@ function Tsdraw(props) {
1338
1774
  initialTool,
1339
1775
  theme: resolvedTheme,
1340
1776
  persistenceKey: props.persistenceKey,
1341
- stylePanelToolIds,
1342
1777
  onMount: props.onMount
1343
1778
  });
1344
1779
  const toolbarPlacementStyle = resolvePlacementStyle(props.uiOptions?.toolbar?.placement, "bottom-center", 0, 14);
1345
1780
  const stylePanelPlacementStyle = resolvePlacementStyle(props.uiOptions?.stylePanel?.placement, "top-right", 8, 8);
1781
+ const isToolbarHidden = props.uiOptions?.toolbar?.hide === true;
1782
+ const isStylePanelHidden = props.uiOptions?.stylePanel?.hide === true;
1346
1783
  const canvasCursor = props.uiOptions?.cursor?.getCursor?.(cursorContext) ?? defaultCanvasCursor;
1347
1784
  const defaultToolOverlay = /* @__PURE__ */ jsxRuntime.jsx(
1348
1785
  ToolOverlay,
@@ -1364,9 +1801,24 @@ function Tsdraw(props) {
1364
1801
  const onDashSelect = react.useCallback((dash) => {
1365
1802
  applyDrawStyle({ dash });
1366
1803
  }, [applyDrawStyle]);
1804
+ const onFillSelect = react.useCallback((fill) => {
1805
+ applyDrawStyle({ fill });
1806
+ }, [applyDrawStyle]);
1367
1807
  const onSizeSelect = react.useCallback((size) => {
1368
1808
  applyDrawStyle({ size });
1369
1809
  }, [applyDrawStyle]);
1810
+ const activeCustomTool = customToolMap.get(currentTool);
1811
+ const stylePanelParts = react.useMemo(
1812
+ () => {
1813
+ const fromCustomTool = activeCustomTool?.stylePanel?.parts;
1814
+ if (fromCustomTool && fromCustomTool.length > 0) return fromCustomTool;
1815
+ if (activeCustomTool?.stylePanel?.customParts && activeCustomTool.stylePanel.customParts.length > 0) return activeCustomTool.stylePanel.customParts.map((customPart) => customPart.id);
1816
+ if (currentTool in DEFAULT_STYLE_PANEL_PARTS_BY_TOOL) return DEFAULT_STYLE_PANEL_PARTS_BY_TOOL[currentTool] ?? EMPTY_STYLE_PANEL_PARTS;
1817
+ return EMPTY_STYLE_PANEL_PARTS;
1818
+ },
1819
+ [activeCustomTool, currentTool]
1820
+ );
1821
+ const stylePanelCustomParts = activeCustomTool?.stylePanel?.customParts ?? EMPTY_STYLE_PANEL_CUSTOM_PARTS;
1370
1822
  const toolbarParts = react.useMemo(
1371
1823
  () => toolbarPartIds.map((toolbarPart, partIndex) => {
1372
1824
  const items = toolbarPart.map((item) => {
@@ -1455,14 +1907,18 @@ function Tsdraw(props) {
1455
1907
  /* @__PURE__ */ jsxRuntime.jsx(
1456
1908
  StylePanel,
1457
1909
  {
1458
- visible: isPersistenceReady && showStylePanel,
1910
+ visible: !isStylePanelHidden && isPersistenceReady && stylePanelParts.length > 0,
1911
+ parts: stylePanelParts,
1912
+ customParts: stylePanelCustomParts,
1459
1913
  style: stylePanelPlacementStyle,
1460
1914
  theme: resolvedTheme,
1461
1915
  drawColor,
1462
1916
  drawDash,
1917
+ drawFill,
1463
1918
  drawSize,
1464
1919
  onColorSelect,
1465
1920
  onDashSelect,
1921
+ onFillSelect,
1466
1922
  onSizeSelect
1467
1923
  }
1468
1924
  ),
@@ -1479,7 +1935,7 @@ function Tsdraw(props) {
1479
1935
  },
1480
1936
  customElement.id
1481
1937
  )),
1482
- /* @__PURE__ */ jsxRuntime.jsx(
1938
+ !isToolbarHidden ? /* @__PURE__ */ jsxRuntime.jsx(
1483
1939
  Toolbar,
1484
1940
  {
1485
1941
  parts: toolbarParts,
@@ -1488,7 +1944,7 @@ function Tsdraw(props) {
1488
1944
  onToolChange: setTool,
1489
1945
  disabled: !isPersistenceReady
1490
1946
  }
1491
- )
1947
+ ) : null
1492
1948
  ]
1493
1949
  }
1494
1950
  );