@sequent-org/moodboard 1.3.4 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/package.json +6 -1
  2. package/src/assets/icons/mindmap.svg +3 -0
  3. package/src/core/SaveManager.js +44 -15
  4. package/src/core/commands/MindmapStatePatchCommand.js +85 -0
  5. package/src/core/commands/UpdateContentCommand.js +47 -4
  6. package/src/core/flows/LayerAndViewportFlow.js +87 -14
  7. package/src/core/flows/ObjectLifecycleFlow.js +7 -2
  8. package/src/core/flows/SaveFlow.js +10 -7
  9. package/src/core/flows/TransformFlow.js +2 -2
  10. package/src/core/index.js +81 -11
  11. package/src/core/rendering/ObjectRenderer.js +7 -2
  12. package/src/grid/BaseGrid.js +65 -0
  13. package/src/grid/CrossGrid.js +89 -24
  14. package/src/grid/CrossGridZoomPhases.js +167 -0
  15. package/src/grid/DotGrid.js +117 -34
  16. package/src/grid/DotGridZoomPhases.js +214 -16
  17. package/src/grid/GridDiagnostics.js +80 -0
  18. package/src/grid/GridFactory.js +13 -11
  19. package/src/grid/LineGrid.js +176 -37
  20. package/src/grid/LineGridZoomPhases.js +163 -0
  21. package/src/grid/ScreenGridPhaseMachine.js +51 -0
  22. package/src/mindmap/MindmapCompoundContract.js +235 -0
  23. package/src/moodboard/ActionHandler.js +1 -0
  24. package/src/moodboard/DataManager.js +57 -0
  25. package/src/moodboard/bootstrap/MoodBoardUiFactory.js +21 -0
  26. package/src/moodboard/integration/MoodBoardEventBindings.js +26 -1
  27. package/src/moodboard/lifecycle/MoodBoardDestroyer.js +15 -0
  28. package/src/objects/MindmapObject.js +76 -0
  29. package/src/objects/ObjectFactory.js +3 -1
  30. package/src/services/BoardService.js +127 -31
  31. package/src/services/GridSnapResolver.js +60 -0
  32. package/src/services/MiroZoomLevels.js +39 -0
  33. package/src/services/SettingsApplier.js +0 -4
  34. package/src/services/ZoomPanController.js +51 -32
  35. package/src/tools/object-tools/PlacementTool.js +12 -3
  36. package/src/tools/object-tools/SelectTool.js +11 -1
  37. package/src/tools/object-tools/placement/GhostController.js +100 -1
  38. package/src/tools/object-tools/placement/PlacementEventsBridge.js +2 -0
  39. package/src/tools/object-tools/placement/PlacementInputRouter.js +2 -2
  40. package/src/tools/object-tools/selection/FileNameInlineEditorController.js +2 -2
  41. package/src/tools/object-tools/selection/InlineEditorController.js +15 -0
  42. package/src/tools/object-tools/selection/MindmapInlineEditorController.js +716 -0
  43. package/src/tools/object-tools/selection/SelectInputRouter.js +6 -0
  44. package/src/tools/object-tools/selection/SelectToolSetup.js +2 -0
  45. package/src/tools/object-tools/selection/TextEditorLifecycleRegistry.js +12 -16
  46. package/src/ui/ContextMenu.js +6 -6
  47. package/src/ui/DotGridDebugPanel.js +253 -0
  48. package/src/ui/HtmlTextLayer.js +1 -1
  49. package/src/ui/TextPropertiesPanel.js +2 -2
  50. package/src/ui/handles/GroupSelectionHandlesController.js +4 -1
  51. package/src/ui/handles/HandlesDomRenderer.js +1486 -15
  52. package/src/ui/handles/HandlesEventBridge.js +49 -5
  53. package/src/ui/handles/HandlesInteractionController.js +4 -4
  54. package/src/ui/mindmap/MindmapConnectionLayer.js +239 -0
  55. package/src/ui/mindmap/MindmapHtmlTextLayer.js +285 -0
  56. package/src/ui/mindmap/MindmapLayoutConfig.js +29 -0
  57. package/src/ui/mindmap/MindmapTextOverlayAdapter.js +144 -0
  58. package/src/ui/styles/toolbar.css +1 -0
  59. package/src/ui/styles/workspace.css +100 -0
  60. package/src/ui/toolbar/ToolbarActionRouter.js +35 -0
  61. package/src/ui/toolbar/ToolbarPopupsController.js +6 -6
  62. package/src/ui/toolbar/ToolbarRenderer.js +1 -0
  63. package/src/ui/toolbar/ToolbarStateController.js +1 -0
  64. package/src/utils/iconLoader.js +10 -4
@@ -0,0 +1,144 @@
1
+ import { Events } from '../../core/events/Events.js';
2
+
3
+ const MINDMAP_DRAG_THRESHOLD_PX = 4;
4
+
5
+ /**
6
+ * Изолирует mindmap-специфику HTML-слоя текста:
7
+ * - внешний вид overlay-элемента
8
+ * - вход в режим редактирования по клику на текст
9
+ */
10
+ export class MindmapTextOverlayAdapter {
11
+ supportsObject(objectData) {
12
+ return objectData?.type === 'mindmap';
13
+ }
14
+
15
+ getDefaultFontFamily(objectData) {
16
+ return objectData?.properties?.fontFamily || objectData?.fontFamily || 'Roboto, Arial, sans-serif';
17
+ }
18
+
19
+ applyElementStyles(el) {
20
+ el.classList.add('mb-text--mindmap');
21
+ el.style.pointerEvents = 'none';
22
+ el.style.cursor = 'default';
23
+ }
24
+
25
+ attachEditOnClick({ el, targetEl, objectId, objectData, eventBus }) {
26
+ const clickableEl = targetEl || el;
27
+ let pendingPointer = null;
28
+ let dragStarted = false;
29
+ let suppressNextClick = false;
30
+
31
+ const getCanvasEl = () => {
32
+ const doc = clickableEl?.ownerDocument || document;
33
+ return doc.querySelector('.moodboard-workspace__canvas canvas');
34
+ };
35
+
36
+ const dispatchMouseToCanvas = (type, sourceEvent, coords = null) => {
37
+ const canvas = getCanvasEl();
38
+ if (!canvas || !sourceEvent) return;
39
+ const point = coords || { clientX: sourceEvent.clientX, clientY: sourceEvent.clientY };
40
+ const evt = new MouseEvent(type, {
41
+ bubbles: true,
42
+ cancelable: true,
43
+ clientX: point.clientX,
44
+ clientY: point.clientY,
45
+ button: sourceEvent.button || 0,
46
+ buttons: sourceEvent.buttons || 0,
47
+ ctrlKey: !!sourceEvent.ctrlKey,
48
+ metaKey: !!sourceEvent.metaKey,
49
+ shiftKey: !!sourceEvent.shiftKey,
50
+ altKey: !!sourceEvent.altKey,
51
+ });
52
+ canvas.dispatchEvent(evt);
53
+ };
54
+
55
+ const onWindowMouseMove = (moveEvent) => {
56
+ if (!pendingPointer) return;
57
+ const dx = moveEvent.clientX - pendingPointer.clientX;
58
+ const dy = moveEvent.clientY - pendingPointer.clientY;
59
+ const movedEnough = Math.hypot(dx, dy) >= MINDMAP_DRAG_THRESHOLD_PX;
60
+ if (!dragStarted && movedEnough) {
61
+ dragStarted = true;
62
+ suppressNextClick = true;
63
+ dispatchMouseToCanvas('mousedown', pendingPointer, pendingPointer);
64
+ }
65
+ if (dragStarted) {
66
+ dispatchMouseToCanvas('mousemove', moveEvent);
67
+ }
68
+ };
69
+
70
+ const onWindowMouseUp = (upEvent) => {
71
+ if (!pendingPointer) return;
72
+ if (dragStarted) {
73
+ dispatchMouseToCanvas('mouseup', upEvent);
74
+ }
75
+ pendingPointer = null;
76
+ dragStarted = false;
77
+ window.removeEventListener('mousemove', onWindowMouseMove, true);
78
+ window.removeEventListener('mouseup', onWindowMouseUp, true);
79
+ };
80
+
81
+ const onTextMouseDown = (event) => {
82
+ if (event.button !== 0) return;
83
+ pendingPointer = {
84
+ clientX: event.clientX,
85
+ clientY: event.clientY,
86
+ button: event.button,
87
+ buttons: event.buttons || 1,
88
+ ctrlKey: !!event.ctrlKey,
89
+ metaKey: !!event.metaKey,
90
+ shiftKey: !!event.shiftKey,
91
+ altKey: !!event.altKey,
92
+ };
93
+ dragStarted = false;
94
+ window.addEventListener('mousemove', onWindowMouseMove, true);
95
+ window.addEventListener('mouseup', onWindowMouseUp, true);
96
+ };
97
+
98
+ const onTextClick = (event) => {
99
+ if (suppressNextClick) {
100
+ suppressNextClick = false;
101
+ event.preventDefault();
102
+ event.stopPropagation();
103
+ return;
104
+ }
105
+ event.preventDefault();
106
+ event.stopPropagation();
107
+ const actualContent = (typeof el?.dataset?.mbContent === 'string')
108
+ ? el.dataset.mbContent
109
+ : (objectData?.properties?.content || objectData?.content || '');
110
+ const posData = { objectId, position: null };
111
+ eventBus.emit(Events.Tool.GetObjectPosition, posData);
112
+ const mergedProperties = {
113
+ ...(objectData?.properties || {}),
114
+ content: actualContent,
115
+ };
116
+ if (Number.isFinite(objectData?.width) && !Number.isFinite(mergedProperties.width)) {
117
+ mergedProperties.width = objectData.width;
118
+ }
119
+ if (Number.isFinite(objectData?.height) && !Number.isFinite(mergedProperties.height)) {
120
+ mergedProperties.height = objectData.height;
121
+ }
122
+ eventBus.emit(Events.Tool.ObjectEdit, {
123
+ id: objectId,
124
+ type: 'mindmap',
125
+ position: posData.position || objectData?.position || { x: 0, y: 0 },
126
+ properties: mergedProperties,
127
+ caretClick: {
128
+ clientX: event.clientX,
129
+ clientY: event.clientY,
130
+ },
131
+ create: false,
132
+ });
133
+ };
134
+
135
+ clickableEl.addEventListener('mousedown', onTextMouseDown);
136
+ clickableEl.addEventListener('click', onTextClick);
137
+ return () => {
138
+ window.removeEventListener('mousemove', onWindowMouseMove, true);
139
+ window.removeEventListener('mouseup', onWindowMouseUp, true);
140
+ clickableEl.removeEventListener('mousedown', onTextMouseDown);
141
+ clickableEl.removeEventListener('click', onTextClick);
142
+ };
143
+ }
144
+ }
@@ -64,6 +64,7 @@
64
64
  .moodboard-toolbar__button--attachments:hover,
65
65
  .moodboard-toolbar__button--emoji:hover,
66
66
  .moodboard-toolbar__button--frame:hover,
67
+ .moodboard-toolbar__button--mindmap:hover,
67
68
  .moodboard-toolbar__button--clear:hover,
68
69
  .moodboard-toolbar__button--undo:hover:not(:disabled),
69
70
  .moodboard-toolbar__button--redo:hover:not(:disabled) { background: #80D8FF !important; }
@@ -234,6 +234,37 @@
234
234
  letter-spacing: normal;
235
235
  }
236
236
 
237
+ .mb-text--mindmap {
238
+ display: flex;
239
+ align-items: center;
240
+ justify-content: flex-start;
241
+ text-align: left;
242
+ pointer-events: none;
243
+ cursor: default;
244
+ color: #212121;
245
+ font-family: 'Roboto', Arial, sans-serif;
246
+ font-weight: 400;
247
+ padding-top: 50px;
248
+ padding-bottom: 50px;
249
+ padding-left: 50px;
250
+ padding-right: 50px;
251
+ }
252
+
253
+ .mb-text--mindmap-content {
254
+ pointer-events: auto;
255
+ display: inline-block;
256
+ max-width: 100%;
257
+ white-space: pre;
258
+ word-break: normal;
259
+ overflow-wrap: normal;
260
+ cursor: default;
261
+ font-weight: 400;
262
+ }
263
+
264
+ .mb-text--mindmap-content.is-placeholder {
265
+ opacity: 0.45;
266
+ }
267
+
237
268
  /* HTML handles layer */
238
269
  .moodboard-html-handles {
239
270
  position: absolute;
@@ -297,6 +328,75 @@
297
328
  justify-content: center;
298
329
  }
299
330
 
331
+ .mb-mindmap-side-btn {
332
+ position: absolute;
333
+ width: 24px;
334
+ height: 24px;
335
+ border: none;
336
+ border-radius: 50%;
337
+ transform: translate(-50%, -50%);
338
+ display: inline-flex;
339
+ align-items: center;
340
+ justify-content: center;
341
+ background: rgba(107, 114, 128, 0.65);
342
+ color: #ffffff;
343
+ cursor: pointer;
344
+ pointer-events: auto;
345
+ z-index: 13;
346
+ padding: 0;
347
+ }
348
+
349
+ .mb-mindmap-side-btn:hover {
350
+ background: #80D8FF;
351
+ }
352
+
353
+ .mb-mindmap-side-btn::before,
354
+ .mb-mindmap-side-btn::after {
355
+ content: '';
356
+ position: absolute;
357
+ left: 50%;
358
+ top: 50%;
359
+ background: #ffffff;
360
+ border-radius: 999px;
361
+ transform: translate(-50%, -50%);
362
+ }
363
+
364
+ .mb-mindmap-side-btn::before {
365
+ width: 12px;
366
+ height: 2px;
367
+ }
368
+
369
+ .mb-mindmap-side-btn::after {
370
+ width: 2px;
371
+ height: 12px;
372
+ }
373
+
374
+ .mb-mindmap-side-btn--down {
375
+ font-size: 0;
376
+ }
377
+
378
+ .mb-mindmap-side-btn--down::before,
379
+ .mb-mindmap-side-btn--down::after {
380
+ display: block;
381
+ content: '';
382
+ position: absolute;
383
+ left: 50%;
384
+ top: calc(50% + 1px);
385
+ width: 8px;
386
+ height: 2px;
387
+ background: #ffffff;
388
+ border-radius: 999px;
389
+ transform-origin: center;
390
+ }
391
+
392
+ .mb-mindmap-side-btn--down::before {
393
+ transform: translate(-88%, -36%) rotate(35deg);
394
+ }
395
+
396
+ .mb-mindmap-side-btn--down::after {
397
+ transform: translate(-12%, -36%) rotate(-35deg);
398
+ }
399
+
300
400
  .mb-revit-show-in-model {
301
401
  position: absolute;
302
402
  transform: translate(-50%, -100%);
@@ -1,4 +1,6 @@
1
1
  import { Events } from '../../core/events/Events.js';
2
+ import { MINDMAP_LAYOUT } from '../mindmap/MindmapLayoutConfig.js';
3
+ import { createRootMindmapIntentMetadata } from '../../mindmap/MindmapCompoundContract.js';
2
4
 
3
5
  export class ToolbarActionRouter {
4
6
  constructor(toolbar) {
@@ -76,6 +78,39 @@ export class ToolbarActionRouter {
76
78
  return true;
77
79
  }
78
80
 
81
+ if (toolType === 'mindmap-add') {
82
+ const mindmapWidth = MINDMAP_LAYOUT.width;
83
+ const mindmapHeight = MINDMAP_LAYOUT.height;
84
+ this.toolbar.animateButton(button);
85
+ this.toolbar.closeShapesPopup();
86
+ this.toolbar.closeDrawPopup();
87
+ this.toolbar.closeEmojiPopup();
88
+ this.toolbar.eventBus.emit(Events.Keyboard.ToolSelect, { tool: 'place' });
89
+ this.toolbar.placeSelectedButtonId = 'mindmap';
90
+ this.toolbar.setActiveToolbarButton('place');
91
+ this.toolbar.eventBus.emit(Events.Place.Set, {
92
+ type: 'mindmap',
93
+ size: { width: mindmapWidth, height: mindmapHeight },
94
+ properties: {
95
+ mindmap: createRootMindmapIntentMetadata(),
96
+ fontSize: MINDMAP_LAYOUT.fontSize,
97
+ width: mindmapWidth,
98
+ height: mindmapHeight,
99
+ capsuleBaseWidth: mindmapWidth,
100
+ capsuleBaseHeight: mindmapHeight,
101
+ paddingX: MINDMAP_LAYOUT.paddingX,
102
+ paddingY: MINDMAP_LAYOUT.paddingY,
103
+ maxLineChars: MINDMAP_LAYOUT.maxLineChars,
104
+ textColor: 0x212121,
105
+ strokeColor: 0x2563EB,
106
+ fillColor: 0x3B82F6,
107
+ fillAlpha: 0.25,
108
+ strokeWidth: 1
109
+ }
110
+ });
111
+ return true;
112
+ }
113
+
79
114
  if (toolType === 'frame') {
80
115
  this.toolbar.animateButton(button);
81
116
  this.toolbar.toggleFramePopup(button);
@@ -183,8 +183,8 @@ export class ToolbarPopupsController {
183
183
  const buttonRect = anchorButton.getBoundingClientRect();
184
184
  const top = buttonRect.top - toolbarRect.top - 4;
185
185
  const left = this.toolbar.element.offsetWidth + 8;
186
- this.toolbar.shapesPopupEl.style.top = `${top}px`;
187
- this.toolbar.shapesPopupEl.style.left = `${left}px`;
186
+ this.toolbar.shapesPopupEl.style.top = `${Math.round(top)}px`;
187
+ this.toolbar.shapesPopupEl.style.left = `${Math.round(left)}px`;
188
188
  this.toolbar.shapesPopupEl.style.display = 'block';
189
189
  }
190
190
 
@@ -363,8 +363,8 @@ export class ToolbarPopupsController {
363
363
  const buttonRect = anchorButton.getBoundingClientRect();
364
364
  const top = buttonRect.top - toolbarRect.top - 4;
365
365
  const left = this.toolbar.element.offsetWidth + 8;
366
- this.toolbar.drawPopupEl.style.top = `${top}px`;
367
- this.toolbar.drawPopupEl.style.left = `${left}px`;
366
+ this.toolbar.drawPopupEl.style.top = `${Math.round(top)}px`;
367
+ this.toolbar.drawPopupEl.style.left = `${Math.round(left)}px`;
368
368
  this.toolbar.drawPopupEl.style.display = 'block';
369
369
  }
370
370
 
@@ -652,8 +652,8 @@ export class ToolbarPopupsController {
652
652
  const minTop = 8;
653
653
  const maxTop = Math.max(minTop, containerHeight - popupHeight - 8);
654
654
  const top = Math.min(Math.max(minTop, desiredTop), maxTop);
655
- this.toolbar.emojiPopupEl.style.top = `${top}px`;
656
- this.toolbar.emojiPopupEl.style.left = `${left}px`;
655
+ this.toolbar.emojiPopupEl.style.top = `${Math.round(top)}px`;
656
+ this.toolbar.emojiPopupEl.style.left = `${Math.round(left)}px`;
657
657
  this.toolbar.emojiPopupEl.style.visibility = 'visible';
658
658
  }
659
659
 
@@ -25,6 +25,7 @@ export class ToolbarRenderer {
25
25
 
26
26
  const existingTools = [
27
27
  { id: 'frame', iconName: 'frame', title: 'Добавить фрейм', type: 'frame' },
28
+ { id: 'mindmap', iconName: 'mindmap', title: 'Схема', type: 'mindmap-add' },
28
29
  { id: 'divider', type: 'divider' },
29
30
  { id: 'undo', iconName: 'undo', title: 'Отменить (Ctrl+Z)', type: 'undo', disabled: true },
30
31
  { id: 'redo', iconName: 'redo', title: 'Повторить (Ctrl+Y)', type: 'redo', disabled: true }
@@ -31,6 +31,7 @@ export class ToolbarStateController {
31
31
  attachments: 'attachments',
32
32
  shapes: 'shapes',
33
33
  emoji: 'emoji',
34
+ mindmap: 'mindmap',
34
35
  null: 'image'
35
36
  };
36
37
  btnId = placeButtonMap[this.toolbar.placeSelectedButtonId] || 'shapes';
@@ -28,14 +28,15 @@ export class IconLoader {
28
28
  import('../assets/icons/frame.svg?raw'),
29
29
  import('../assets/icons/clear.svg?raw'),
30
30
  import('../assets/icons/undo.svg?raw'),
31
- import('../assets/icons/redo.svg?raw')
31
+ import('../assets/icons/redo.svg?raw'),
32
+ import('../assets/icons/mindmap.svg?raw')
32
33
  ]);
33
34
 
34
35
  // Сохраняем иконки в кэш
35
36
  const iconNames = [
36
37
  'select', 'pan', 'text-add', 'note', 'image', 'shapes',
37
38
  'pencil', 'comments', 'attachments', 'emoji', 'frame',
38
- 'clear', 'undo', 'redo'
39
+ 'clear', 'undo', 'redo', 'mindmap'
39
40
  ];
40
41
 
41
42
  iconNames.forEach((name, index) => {
@@ -134,6 +135,10 @@ export class IconLoader {
134
135
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
135
136
  <path d="M15 14L20 9L15 4M4 20V13C4 11.9391 4.42143 10.9217 5.17157 10.1716C5.92172 9.42143 6.93913 9 8 9H20"
136
137
  fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
138
+ </svg>`,
139
+ 'mindmap': `<?xml version="1.0" encoding="UTF-8"?>
140
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
141
+ <path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M19 2a3 3 0 1 1 0 6c-.463 0-.9-.109-1.291-.296l-1.19 1.19A3.992 3.992 0 0 1 18 12a3.991 3.991 0 0 1-1.48 3.105l1.189 1.19A2.984 2.984 0 0 1 19 16a3 3 0 1 1-3 3c0-.463.108-.9.295-1.291l-1.748-1.748A4.106 4.106 0 0 1 14 16h-4c-.186 0-.37-.014-.549-.04l-1.747 1.75c.187.392.296.828.296 1.291a3 3 0 1 1-3-3c.462 0 .899.108 1.29.295l1.19-1.19A3.992 3.992 0 0 1 6 12a3.99 3.99 0 0 1 1.48-3.106l-1.19-1.19A2.982 2.982 0 0 1 5 8a3 3 0 1 1 3-3c0 .463-.109.899-.296 1.29l1.748 1.748C9.632 8.014 9.814 8 10 8h4c.185 0 .368.013.547.037l1.748-1.747A2.983 2.983 0 0 1 16 5a3 3 0 0 1 3-3ZM5 18a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm14 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-9-8a2 2 0 1 0 0 4h4a2 2 0 1 0 0-4h-4ZM5 4a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm14 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z"/>
137
142
  </svg>`
138
143
  };
139
144
 
@@ -187,7 +192,7 @@ export class IconLoader {
187
192
  const iconNames = [
188
193
  'select', 'pan', 'text-add', 'note', 'image', 'shapes',
189
194
  'pencil', 'comments', 'attachments', 'emoji', 'frame',
190
- 'clear', 'undo', 'redo'
195
+ 'clear', 'undo', 'redo', 'mindmap'
191
196
  ];
192
197
 
193
198
  iconNames.forEach(name => {
@@ -219,7 +224,8 @@ export class IconLoader {
219
224
  'frame': '<svg width="20" height="20" viewBox="0 0 20 20"><rect x="2" y="2" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2"/></svg>',
220
225
  'clear': '<svg width="20" height="20" viewBox="0 0 20 20"><path d="M3 6H17L16 18H4L3 6Z" fill="currentColor"/></svg>',
221
226
  'undo': '<svg width="20" height="20" viewBox="0 0 20 20"><path d="M8 4L3 9L8 14" stroke="currentColor" stroke-width="2" fill="none"/></svg>',
222
- 'redo': '<svg width="20" height="20" viewBox="0 0 20 20"><path d="M12 4L17 9L12 14" stroke="currentColor" stroke-width="2" fill="none"/></svg>'
227
+ 'redo': '<svg width="20" height="20" viewBox="0 0 20 20"><path d="M12 4L17 9L12 14" stroke="currentColor" stroke-width="2" fill="none"/></svg>',
228
+ 'mindmap': '<svg width="20" height="20" viewBox="0 0 20 20"><circle cx="4" cy="4" r="2" fill="currentColor"/><circle cx="16" cy="4" r="2" fill="currentColor"/><circle cx="4" cy="16" r="2" fill="currentColor"/><circle cx="16" cy="16" r="2" fill="currentColor"/><rect x="7" y="8" width="6" height="4" rx="2" fill="currentColor"/><path d="M6 5L8 8M14 8L16 5M8 12L6 15M12 12L14 15" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>'
223
229
  };
224
230
 
225
231
  return fallbacks[iconName] || fallbacks['select'];