@report-designer/designer 0.1.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 (253) hide show
  1. package/dist/band-metadata.d.ts +8 -0
  2. package/dist/band-metadata.d.ts.map +1 -0
  3. package/dist/band-metadata.js +75 -0
  4. package/dist/band-metadata.js.map +1 -0
  5. package/dist/component-factory.d.ts +9 -0
  6. package/dist/component-factory.d.ts.map +1 -0
  7. package/dist/component-factory.js +141 -0
  8. package/dist/component-factory.js.map +1 -0
  9. package/dist/component-palette-model.d.ts +14 -0
  10. package/dist/component-palette-model.d.ts.map +1 -0
  11. package/dist/component-palette-model.js +21 -0
  12. package/dist/component-palette-model.js.map +1 -0
  13. package/dist/components/Canvas.d.ts +5 -0
  14. package/dist/components/Canvas.d.ts.map +1 -0
  15. package/dist/components/Canvas.js +2428 -0
  16. package/dist/components/Canvas.js.map +1 -0
  17. package/dist/components/ConditionalFormatManager.d.ts +8 -0
  18. package/dist/components/ConditionalFormatManager.d.ts.map +1 -0
  19. package/dist/components/ConditionalFormatManager.js +135 -0
  20. package/dist/components/ConditionalFormatManager.js.map +1 -0
  21. package/dist/components/Designer.d.ts +22 -0
  22. package/dist/components/Designer.d.ts.map +1 -0
  23. package/dist/components/Designer.js +115 -0
  24. package/dist/components/Designer.js.map +1 -0
  25. package/dist/components/ExpressionEditor.d.ts +10 -0
  26. package/dist/components/ExpressionEditor.d.ts.map +1 -0
  27. package/dist/components/ExpressionEditor.js +203 -0
  28. package/dist/components/ExpressionEditor.js.map +1 -0
  29. package/dist/components/LeftPanel.d.ts +11 -0
  30. package/dist/components/LeftPanel.d.ts.map +1 -0
  31. package/dist/components/LeftPanel.js +551 -0
  32. package/dist/components/LeftPanel.js.map +1 -0
  33. package/dist/components/PropertyEditor.d.ts +6 -0
  34. package/dist/components/PropertyEditor.d.ts.map +1 -0
  35. package/dist/components/PropertyEditor.js +1002 -0
  36. package/dist/components/PropertyEditor.js.map +1 -0
  37. package/dist/components/RibbonToolbar.d.ts +3 -0
  38. package/dist/components/RibbonToolbar.d.ts.map +1 -0
  39. package/dist/components/RibbonToolbar.js +179 -0
  40. package/dist/components/RibbonToolbar.js.map +1 -0
  41. package/dist/components/TextFormatEditor.d.ts +13 -0
  42. package/dist/components/TextFormatEditor.d.ts.map +1 -0
  43. package/dist/components/TextFormatEditor.js +199 -0
  44. package/dist/components/TextFormatEditor.js.map +1 -0
  45. package/dist/components/TextStyleLibraryDialog.d.ts +8 -0
  46. package/dist/components/TextStyleLibraryDialog.d.ts.map +1 -0
  47. package/dist/components/TextStyleLibraryDialog.js +367 -0
  48. package/dist/components/TextStyleLibraryDialog.js.map +1 -0
  49. package/dist/components/canvas/DesignerCanvasFrame.d.ts +10 -0
  50. package/dist/components/canvas/DesignerCanvasFrame.d.ts.map +1 -0
  51. package/dist/components/canvas/DesignerCanvasFrame.js +22 -0
  52. package/dist/components/canvas/DesignerCanvasFrame.js.map +1 -0
  53. package/dist/components/chart/ChartAxesPanel.d.ts +12 -0
  54. package/dist/components/chart/ChartAxesPanel.d.ts.map +1 -0
  55. package/dist/components/chart/ChartAxesPanel.js +71 -0
  56. package/dist/components/chart/ChartAxesPanel.js.map +1 -0
  57. package/dist/components/chart/ChartDataPanel.d.ts +21 -0
  58. package/dist/components/chart/ChartDataPanel.d.ts.map +1 -0
  59. package/dist/components/chart/ChartDataPanel.js +80 -0
  60. package/dist/components/chart/ChartDataPanel.js.map +1 -0
  61. package/dist/components/chart/ChartLabelPanel.d.ts +12 -0
  62. package/dist/components/chart/ChartLabelPanel.d.ts.map +1 -0
  63. package/dist/components/chart/ChartLabelPanel.js +34 -0
  64. package/dist/components/chart/ChartLabelPanel.js.map +1 -0
  65. package/dist/components/chart/ChartLegendPanel.d.ts +12 -0
  66. package/dist/components/chart/ChartLegendPanel.d.ts.map +1 -0
  67. package/dist/components/chart/ChartLegendPanel.js +48 -0
  68. package/dist/components/chart/ChartLegendPanel.js.map +1 -0
  69. package/dist/components/chart/ChartPropertyPanel.d.ts +26 -0
  70. package/dist/components/chart/ChartPropertyPanel.d.ts.map +1 -0
  71. package/dist/components/chart/ChartPropertyPanel.js +119 -0
  72. package/dist/components/chart/ChartPropertyPanel.js.map +1 -0
  73. package/dist/components/chart/ChartThemePanel.d.ts +9 -0
  74. package/dist/components/chart/ChartThemePanel.d.ts.map +1 -0
  75. package/dist/components/chart/ChartThemePanel.js +21 -0
  76. package/dist/components/chart/ChartThemePanel.js.map +1 -0
  77. package/dist/components/chart/ChartTitlePanel.d.ts +10 -0
  78. package/dist/components/chart/ChartTitlePanel.d.ts.map +1 -0
  79. package/dist/components/chart/ChartTitlePanel.js +45 -0
  80. package/dist/components/chart/ChartTitlePanel.js.map +1 -0
  81. package/dist/components/chart/ChartTypeStylePanel.d.ts +11 -0
  82. package/dist/components/chart/ChartTypeStylePanel.d.ts.map +1 -0
  83. package/dist/components/chart/ChartTypeStylePanel.js +37 -0
  84. package/dist/components/chart/ChartTypeStylePanel.js.map +1 -0
  85. package/dist/components/chart/ColorPaletteEditor.d.ts +10 -0
  86. package/dist/components/chart/ColorPaletteEditor.d.ts.map +1 -0
  87. package/dist/components/chart/ColorPaletteEditor.js +18 -0
  88. package/dist/components/chart/ColorPaletteEditor.js.map +1 -0
  89. package/dist/components/chart/chart-options.d.ts +119 -0
  90. package/dist/components/chart/chart-options.d.ts.map +1 -0
  91. package/dist/components/chart/chart-options.js +217 -0
  92. package/dist/components/chart/chart-options.js.map +1 -0
  93. package/dist/components/dialogs/BandWizardDialog.d.ts +8 -0
  94. package/dist/components/dialogs/BandWizardDialog.d.ts.map +1 -0
  95. package/dist/components/dialogs/BandWizardDialog.js +54 -0
  96. package/dist/components/dialogs/BandWizardDialog.js.map +1 -0
  97. package/dist/components/dialogs/GroupWizardDialog.d.ts +8 -0
  98. package/dist/components/dialogs/GroupWizardDialog.d.ts.map +1 -0
  99. package/dist/components/dialogs/GroupWizardDialog.js +70 -0
  100. package/dist/components/dialogs/GroupWizardDialog.js.map +1 -0
  101. package/dist/components/dialogs/JsonDataSourceDialog.d.ts +8 -0
  102. package/dist/components/dialogs/JsonDataSourceDialog.d.ts.map +1 -0
  103. package/dist/components/dialogs/JsonDataSourceDialog.js +67 -0
  104. package/dist/components/dialogs/JsonDataSourceDialog.js.map +1 -0
  105. package/dist/components/dialogs/PageSetupDialog.d.ts +8 -0
  106. package/dist/components/dialogs/PageSetupDialog.d.ts.map +1 -0
  107. package/dist/components/dialogs/PageSetupDialog.js +145 -0
  108. package/dist/components/dialogs/PageSetupDialog.js.map +1 -0
  109. package/dist/components/dialogs/dialog-utils.d.ts +6 -0
  110. package/dist/components/dialogs/dialog-utils.d.ts.map +1 -0
  111. package/dist/components/dialogs/dialog-utils.js +37 -0
  112. package/dist/components/dialogs/dialog-utils.js.map +1 -0
  113. package/dist/components/events/EventEditorDialog.d.ts +43 -0
  114. package/dist/components/events/EventEditorDialog.d.ts.map +1 -0
  115. package/dist/components/events/EventEditorDialog.js +271 -0
  116. package/dist/components/events/EventEditorDialog.js.map +1 -0
  117. package/dist/components/events/EventScriptEditor.d.ts +41 -0
  118. package/dist/components/events/EventScriptEditor.d.ts.map +1 -0
  119. package/dist/components/events/EventScriptEditor.js +140 -0
  120. package/dist/components/events/EventScriptEditor.js.map +1 -0
  121. package/dist/components/events/event-editor-utils.d.ts +18 -0
  122. package/dist/components/events/event-editor-utils.d.ts.map +1 -0
  123. package/dist/components/events/event-editor-utils.js +66 -0
  124. package/dist/components/events/event-editor-utils.js.map +1 -0
  125. package/dist/components/events/event-script-monaco.d.ts +74 -0
  126. package/dist/components/events/event-script-monaco.d.ts.map +1 -0
  127. package/dist/components/events/event-script-monaco.js +282 -0
  128. package/dist/components/events/event-script-monaco.js.map +1 -0
  129. package/dist/components/events/event-script-templates.d.ts +7 -0
  130. package/dist/components/events/event-script-templates.d.ts.map +1 -0
  131. package/dist/components/events/event-script-templates.js +100 -0
  132. package/dist/components/events/event-script-templates.js.map +1 -0
  133. package/dist/components/expression/ExpressionMonacoEditor.d.ts +16 -0
  134. package/dist/components/expression/ExpressionMonacoEditor.d.ts.map +1 -0
  135. package/dist/components/expression/ExpressionMonacoEditor.js +63 -0
  136. package/dist/components/expression/ExpressionMonacoEditor.js.map +1 -0
  137. package/dist/components/expression/InlineExpressionEditor.d.ts +10 -0
  138. package/dist/components/expression/InlineExpressionEditor.d.ts.map +1 -0
  139. package/dist/components/expression/InlineExpressionEditor.js +33 -0
  140. package/dist/components/expression/InlineExpressionEditor.js.map +1 -0
  141. package/dist/components/expression/expression-monaco.d.ts +34 -0
  142. package/dist/components/expression/expression-monaco.d.ts.map +1 -0
  143. package/dist/components/expression/expression-monaco.js +87 -0
  144. package/dist/components/expression/expression-monaco.js.map +1 -0
  145. package/dist/components/panels/DesignerLeftPanel.d.ts +11 -0
  146. package/dist/components/panels/DesignerLeftPanel.d.ts.map +1 -0
  147. package/dist/components/panels/DesignerLeftPanel.js +8 -0
  148. package/dist/components/panels/DesignerLeftPanel.js.map +1 -0
  149. package/dist/components/panels/DesignerPropertyPanel.d.ts +6 -0
  150. package/dist/components/panels/DesignerPropertyPanel.d.ts.map +1 -0
  151. package/dist/components/panels/DesignerPropertyPanel.js +441 -0
  152. package/dist/components/panels/DesignerPropertyPanel.js.map +1 -0
  153. package/dist/components/panels/PanelSearchBox.d.ts +9 -0
  154. package/dist/components/panels/PanelSearchBox.d.ts.map +1 -0
  155. package/dist/components/panels/PanelSearchBox.js +5 -0
  156. package/dist/components/panels/PanelSearchBox.js.map +1 -0
  157. package/dist/components/properties/BandPropertyGrid.d.ts +6 -0
  158. package/dist/components/properties/BandPropertyGrid.d.ts.map +1 -0
  159. package/dist/components/properties/BandPropertyGrid.js +504 -0
  160. package/dist/components/properties/BandPropertyGrid.js.map +1 -0
  161. package/dist/components/properties/BoxStyleEditors.d.ts +59 -0
  162. package/dist/components/properties/BoxStyleEditors.d.ts.map +1 -0
  163. package/dist/components/properties/BoxStyleEditors.js +87 -0
  164. package/dist/components/properties/BoxStyleEditors.js.map +1 -0
  165. package/dist/components/properties/FontEditor.d.ts +28 -0
  166. package/dist/components/properties/FontEditor.d.ts.map +1 -0
  167. package/dist/components/properties/FontEditor.js +22 -0
  168. package/dist/components/properties/FontEditor.js.map +1 -0
  169. package/dist/components/ribbon/DesignerRibbon.d.ts +3 -0
  170. package/dist/components/ribbon/DesignerRibbon.d.ts.map +1 -0
  171. package/dist/components/ribbon/DesignerRibbon.js +193 -0
  172. package/dist/components/ribbon/DesignerRibbon.js.map +1 -0
  173. package/dist/components/richtext/RichTextInlineEditor.d.ts +15 -0
  174. package/dist/components/richtext/RichTextInlineEditor.d.ts.map +1 -0
  175. package/dist/components/richtext/RichTextInlineEditor.js +94 -0
  176. package/dist/components/richtext/RichTextInlineEditor.js.map +1 -0
  177. package/dist/components/shell/DesignerShell.d.ts +12 -0
  178. package/dist/components/shell/DesignerShell.d.ts.map +1 -0
  179. package/dist/components/shell/DesignerShell.js +124 -0
  180. package/dist/components/shell/DesignerShell.js.map +1 -0
  181. package/dist/components/shell/DesignerStatusBar.d.ts +3 -0
  182. package/dist/components/shell/DesignerStatusBar.d.ts.map +1 -0
  183. package/dist/components/shell/DesignerStatusBar.js +23 -0
  184. package/dist/components/shell/DesignerStatusBar.js.map +1 -0
  185. package/dist/components/tree/ReportTree.d.ts +3 -0
  186. package/dist/components/tree/ReportTree.d.ts.map +1 -0
  187. package/dist/components/tree/ReportTree.js +17 -0
  188. package/dist/components/tree/ReportTree.js.map +1 -0
  189. package/dist/data-source-fields.d.ts +14 -0
  190. package/dist/data-source-fields.d.ts.map +1 -0
  191. package/dist/data-source-fields.js +49 -0
  192. package/dist/data-source-fields.js.map +1 -0
  193. package/dist/data-source-paths.d.ts +10 -0
  194. package/dist/data-source-paths.d.ts.map +1 -0
  195. package/dist/data-source-paths.js +61 -0
  196. package/dist/data-source-paths.js.map +1 -0
  197. package/dist/expression/expression-catalog.d.ts +39 -0
  198. package/dist/expression/expression-catalog.d.ts.map +1 -0
  199. package/dist/expression/expression-catalog.js +127 -0
  200. package/dist/expression/expression-catalog.js.map +1 -0
  201. package/dist/expression/expression-preview.d.ts +11 -0
  202. package/dist/expression/expression-preview.d.ts.map +1 -0
  203. package/dist/expression/expression-preview.js +58 -0
  204. package/dist/expression/expression-preview.js.map +1 -0
  205. package/dist/expression/expression-validation.d.ts +12 -0
  206. package/dist/expression/expression-validation.d.ts.map +1 -0
  207. package/dist/expression/expression-validation.js +85 -0
  208. package/dist/expression/expression-validation.js.map +1 -0
  209. package/dist/expression/function-catalog.d.ts +21 -0
  210. package/dist/expression/function-catalog.d.ts.map +1 -0
  211. package/dist/expression/function-catalog.js +96 -0
  212. package/dist/expression/function-catalog.js.map +1 -0
  213. package/dist/i18n/DesignerI18nProvider.d.ts +11 -0
  214. package/dist/i18n/DesignerI18nProvider.d.ts.map +1 -0
  215. package/dist/i18n/DesignerI18nProvider.js +32 -0
  216. package/dist/i18n/DesignerI18nProvider.js.map +1 -0
  217. package/dist/i18n/index.d.ts +3 -0
  218. package/dist/i18n/index.d.ts.map +1 -0
  219. package/dist/i18n/index.js +2 -0
  220. package/dist/i18n/index.js.map +1 -0
  221. package/dist/i18n/messages.d.ts +5 -0
  222. package/dist/i18n/messages.d.ts.map +1 -0
  223. package/dist/i18n/messages.js +1383 -0
  224. package/dist/i18n/messages.js.map +1 -0
  225. package/dist/index.d.ts +21 -0
  226. package/dist/index.d.ts.map +1 -0
  227. package/dist/index.js +18 -0
  228. package/dist/index.js.map +1 -0
  229. package/dist/page-settings.d.ts +21 -0
  230. package/dist/page-settings.d.ts.map +1 -0
  231. package/dist/page-settings.js +66 -0
  232. package/dist/page-settings.js.map +1 -0
  233. package/dist/report-structure.d.ts +20 -0
  234. package/dist/report-structure.d.ts.map +1 -0
  235. package/dist/report-structure.js +219 -0
  236. package/dist/report-structure.js.map +1 -0
  237. package/dist/store/designer-store.d.ts +161 -0
  238. package/dist/store/designer-store.d.ts.map +1 -0
  239. package/dist/store/designer-store.js +1851 -0
  240. package/dist/store/designer-store.js.map +1 -0
  241. package/dist/table/table-structure.d.ts +50 -0
  242. package/dist/table/table-structure.d.ts.map +1 -0
  243. package/dist/table/table-structure.js +251 -0
  244. package/dist/table/table-structure.js.map +1 -0
  245. package/dist/text-style-application.d.ts +11 -0
  246. package/dist/text-style-application.d.ts.map +1 -0
  247. package/dist/text-style-application.js +135 -0
  248. package/dist/text-style-application.js.map +1 -0
  249. package/dist/text-style-bindings.d.ts +16 -0
  250. package/dist/text-style-bindings.d.ts.map +1 -0
  251. package/dist/text-style-bindings.js +549 -0
  252. package/dist/text-style-bindings.js.map +1 -0
  253. package/package.json +70 -0
@@ -0,0 +1,1851 @@
1
+ import { create } from 'zustand';
2
+ import { createDefaultTemplate, getDefaultTextStyle, isRepeatOnEveryPageBandType, normalizeTemplate } from '@report-designer/core';
3
+ import { CommandDispatcher, registerCommand } from '@report-designer/core';
4
+ import { collectComponentNames, ensureTemplateBandNames, ensureTemplateComponentNames, getNextBandName, getNextComponentName, isComponentNameAvailable, normalizeComponentName, prepareBandForInsert, prepareComponentForInsert, } from '../report-structure';
5
+ import { applyDefaultTextStyle, applyTextStyleToComponent, canUseTextStyle, clearTextStyleReference, applyLocalTextComponentUpdates, normalizeLocalTextComponentUpdates, unbindTextStyleFromComponent, syncTextComponentStyle, } from '../text-style-application';
6
+ import { clearTableCell, clearTableCellStyle, copyTableCellStyle, deleteTableColumn, deleteTableRow, equalizeTableColumns, equalizeTableRows, insertTableColumn, insertTableRow, mergeTableCellRange, mergeTableCellRight, pasteTableCellStyle, setTableCellWidth, setTableStructure, splitTableCell, normalizeTable, updateTableRow, } from '../table/table-structure';
7
+ const DEFAULT_PAGE_WIDTH = 210; // A4
8
+ const DEFAULT_PAGE_HEIGHT = 297;
9
+ const UPDATE_COMPONENTS_COMMAND = 'update-components';
10
+ const INSERT_BAND_COMMAND = 'insert-band';
11
+ const DELETE_BAND_COMMAND = 'delete-band';
12
+ const MOVE_BAND_COMMAND = 'move-band';
13
+ const MOVE_COMPONENT_TO_BAND_COMMAND = 'move-component-to-band';
14
+ const DEFAULT_BAND_HEIGHTS = {
15
+ reportTitle: 40,
16
+ reportSummary: 30,
17
+ pageHeader: 20,
18
+ pageFooter: 20,
19
+ header: 20,
20
+ footer: 20,
21
+ columnHeader: 18,
22
+ columnFooter: 18,
23
+ groupHeader: 25,
24
+ groupFooter: 25,
25
+ data: 20,
26
+ hierarchicalData: 20,
27
+ overlay: 20,
28
+ };
29
+ if (!CommandDispatcher.isAllowed(UPDATE_COMPONENTS_COMMAND)) {
30
+ registerCommand(UPDATE_COMPONENTS_COMMAND, (state, payload) => applyComponentUpdates(state, payload.updates ?? []), (state, payload) => applyComponentUpdates(state, payload.previous ?? []));
31
+ }
32
+ if (!CommandDispatcher.isAllowed(INSERT_BAND_COMMAND)) {
33
+ registerCommand(INSERT_BAND_COMMAND, (state, payload) => insertBandInTemplate(state, payload.pageId, payload.afterBandId, payload.band), (state, payload) => ({
34
+ ...state,
35
+ pages: state.pages.map(page => page.id === payload.pageId
36
+ ? { ...page, bands: page.bands.filter(band => band.id !== payload.band.id) }
37
+ : page),
38
+ }));
39
+ }
40
+ if (!CommandDispatcher.isAllowed(DELETE_BAND_COMMAND)) {
41
+ registerCommand(DELETE_BAND_COMMAND, (state, payload) => ({
42
+ ...state,
43
+ pages: state.pages.map(page => page.id === payload.pageId
44
+ ? { ...page, bands: page.bands.filter(band => band.id !== payload.band.id) }
45
+ : page),
46
+ }), (state, payload) => ({
47
+ ...state,
48
+ pages: state.pages.map(page => {
49
+ if (page.id !== payload.pageId)
50
+ return page;
51
+ const index = Math.max(0, Math.min(payload.index ?? page.bands.length, page.bands.length));
52
+ return {
53
+ ...page,
54
+ bands: [
55
+ ...page.bands.slice(0, index),
56
+ payload.band,
57
+ ...page.bands.slice(index),
58
+ ],
59
+ };
60
+ }),
61
+ }));
62
+ }
63
+ if (!CommandDispatcher.isAllowed(MOVE_BAND_COMMAND)) {
64
+ registerCommand(MOVE_BAND_COMMAND, (state, payload) => moveBandInTemplate(state, payload.pageId, payload.bandId, payload.targetIndex), (state, payload) => moveBandInTemplate(state, payload.pageId, payload.bandId, payload.fromIndex));
65
+ }
66
+ if (!CommandDispatcher.isAllowed(MOVE_COMPONENT_TO_BAND_COMMAND)) {
67
+ registerCommand(MOVE_COMPONENT_TO_BAND_COMMAND, (state, payload) => moveComponentBetweenBandsInTemplate(state, {
68
+ pageId: payload.pageId,
69
+ fromBandId: payload.fromBandId,
70
+ toBandId: payload.toBandId,
71
+ componentId: payload.componentId,
72
+ x: payload.x,
73
+ y: payload.y,
74
+ }), (state, payload) => moveComponentBetweenBandsInTemplate(state, {
75
+ pageId: payload.pageId,
76
+ fromBandId: payload.toBandId,
77
+ toBandId: payload.fromBandId,
78
+ componentId: payload.componentId,
79
+ x: payload.prevX,
80
+ y: payload.prevY,
81
+ fallbackComponent: payload.component,
82
+ }));
83
+ }
84
+ export const useDesignerStore = create((set, get) => {
85
+ const dispatcher = new CommandDispatcher();
86
+ const updateSelectedTableComponents = (updater) => {
87
+ const { template, currentPageId, selectedComponentIds, dispatcher } = get();
88
+ if (selectedComponentIds.length === 0)
89
+ return;
90
+ const page = template.pages.find(p => p.id === currentPageId);
91
+ if (!page)
92
+ return;
93
+ const selected = new Set(selectedComponentIds);
94
+ const updates = [];
95
+ const previous = [];
96
+ for (const band of page.bands) {
97
+ for (const component of band.components) {
98
+ if (!selected.has(component.id) || component.type !== 'table')
99
+ continue;
100
+ const nextTable = updater(component);
101
+ if (JSON.stringify(nextTable) === JSON.stringify(component))
102
+ continue;
103
+ const target = {
104
+ pageId: currentPageId,
105
+ bandId: band.id,
106
+ componentId: component.id,
107
+ };
108
+ updates.push({
109
+ ...target,
110
+ updates: tableHistorySnapshot(nextTable),
111
+ });
112
+ previous.push({
113
+ ...target,
114
+ updates: tableHistorySnapshot(component),
115
+ });
116
+ }
117
+ }
118
+ if (updates.length === 0)
119
+ return;
120
+ const nextTemplate = dispatcher.execute(template, {
121
+ type: UPDATE_COMPONENTS_COMMAND,
122
+ payload: {
123
+ pageId: currentPageId,
124
+ updates,
125
+ previous,
126
+ },
127
+ execute: () => template,
128
+ undo: () => template,
129
+ });
130
+ set({ template: nextTemplate });
131
+ };
132
+ const updateSelectedTableCellComponent = (updates) => {
133
+ const { template, currentPageId, selectedTableCell, dispatcher } = get();
134
+ if (!selectedTableCell)
135
+ return;
136
+ const normalizedSelection = normalizeTableCellSelection(selectedTableCell);
137
+ const band = findBand(template, currentPageId, normalizedSelection.bandId);
138
+ const component = band?.components.find(item => item.id === normalizedSelection.tableId);
139
+ if (!component || component.type !== 'table')
140
+ return;
141
+ const nextTable = updateTableCell(component, normalizedSelection, updates);
142
+ if (JSON.stringify(nextTable) === JSON.stringify(component))
143
+ return;
144
+ const nextTemplate = dispatcher.execute(template, {
145
+ type: 'update-component',
146
+ payload: {
147
+ pageId: currentPageId,
148
+ bandId: normalizedSelection.bandId,
149
+ componentId: normalizedSelection.tableId,
150
+ updates: tableHistorySnapshot(nextTable),
151
+ previous: tableHistorySnapshot(component),
152
+ },
153
+ execute: () => template,
154
+ undo: () => template,
155
+ });
156
+ set({ template: nextTemplate });
157
+ };
158
+ return {
159
+ template: ensureTemplateComponentNames(ensureTemplateBandNames(createDefaultTemplate())),
160
+ currentPageId: '',
161
+ mode: 'design',
162
+ textStyleLibraryOpen: false,
163
+ conditionalFormatLibraryOpen: false,
164
+ selectedComponentIds: [],
165
+ selectedBandId: null,
166
+ pendingBandInsertType: null,
167
+ selectedTableRow: null,
168
+ selectedTableCell: null,
169
+ tableCellStyleClipboard: null,
170
+ pendingEventEditorTarget: null,
171
+ dataSources: {},
172
+ dispatcher,
173
+ clipboard: [],
174
+ reportUnit: 'mm',
175
+ zoom: 1,
176
+ loadTemplate: (template) => {
177
+ const normalizedTemplate = ensureTemplateComponentNames(ensureTemplateBandNames(normalizeTemplate(template)));
178
+ set({
179
+ template: normalizedTemplate,
180
+ currentPageId: normalizedTemplate.pages[0]?.id || '',
181
+ mode: 'design',
182
+ textStyleLibraryOpen: false,
183
+ conditionalFormatLibraryOpen: false,
184
+ selectedComponentIds: [],
185
+ selectedBandId: null,
186
+ pendingBandInsertType: null,
187
+ selectedTableRow: null,
188
+ selectedTableCell: null,
189
+ tableCellStyleClipboard: null,
190
+ pendingEventEditorTarget: null,
191
+ clipboard: [],
192
+ reportUnit: 'mm',
193
+ zoom: 1,
194
+ });
195
+ },
196
+ updateTemplate: (updater) => set(state => ({ template: updater(state.template) })),
197
+ replaceReportEvents: (events) => set(state => ({
198
+ template: { ...state.template, events: cleanEventMap(events) },
199
+ })),
200
+ replacePageEvents: (pageId, events) => set(state => ({
201
+ template: {
202
+ ...state.template,
203
+ pages: state.template.pages.map(page => page.id === pageId
204
+ ? { ...page, events: cleanEventMap(events) }
205
+ : page),
206
+ },
207
+ })),
208
+ replaceBandEvents: (pageId, bandId, events) => set(state => ({
209
+ template: {
210
+ ...state.template,
211
+ pages: state.template.pages.map(page => page.id === pageId ? {
212
+ ...page,
213
+ bands: page.bands.map(band => band.id === bandId ? {
214
+ ...band,
215
+ events: cleanEventMap(events),
216
+ } : band),
217
+ } : page),
218
+ },
219
+ })),
220
+ replaceComponentEvents: (pageId, bandId, componentId, events) => set(state => ({
221
+ template: {
222
+ ...state.template,
223
+ pages: state.template.pages.map(page => page.id === pageId ? {
224
+ ...page,
225
+ bands: page.bands.map(band => band.id === bandId ? {
226
+ ...band,
227
+ components: band.components.map(component => (component.id === componentId
228
+ ? { ...component, events: cleanEventMap(events) }
229
+ : component)),
230
+ } : band),
231
+ } : page),
232
+ },
233
+ })),
234
+ updateReportEvent: (eventName, event) => {
235
+ const events = { ...(get().template.events ?? {}), [eventName]: event };
236
+ get().replaceReportEvents(events);
237
+ },
238
+ updatePageEvent: (pageId, eventName, event) => {
239
+ const page = get().template.pages.find(item => item.id === pageId);
240
+ const events = { ...(page?.events ?? {}), [eventName]: event };
241
+ get().replacePageEvents(pageId, events);
242
+ },
243
+ updateBandEvent: (pageId, bandId, eventName, event) => {
244
+ const band = findBand(get().template, pageId, bandId);
245
+ const events = { ...(band?.events ?? {}), [eventName]: event };
246
+ get().replaceBandEvents(pageId, bandId, events);
247
+ },
248
+ updateComponentEvent: (pageId, bandId, componentId, eventName, event) => {
249
+ const band = findBand(get().template, pageId, bandId);
250
+ const component = band?.components.find(item => item.id === componentId);
251
+ const events = { ...(component?.events ?? {}), [eventName]: event };
252
+ get().replaceComponentEvents(pageId, bandId, componentId, events);
253
+ },
254
+ setCurrentPage: (pageId) => set({ currentPageId: pageId }),
255
+ setMode: (mode) => set({ mode }),
256
+ openTextStyleLibrary: () => set({ textStyleLibraryOpen: true }),
257
+ closeTextStyleLibrary: () => set({ textStyleLibraryOpen: false }),
258
+ openConditionalFormatLibrary: () => set({ conditionalFormatLibraryOpen: true }),
259
+ closeConditionalFormatLibrary: () => set({ conditionalFormatLibraryOpen: false }),
260
+ selectComponents: (componentIds) => set({ selectedComponentIds: componentIds, selectedTableRow: null, selectedTableCell: null }),
261
+ selectBand: (bandId) => set({ selectedBandId: bandId, selectedTableRow: null, selectedTableCell: null }),
262
+ beginBandInsert: (bandType) => set({
263
+ pendingBandInsertType: bandType,
264
+ selectedComponentIds: [],
265
+ selectedBandId: null,
266
+ selectedTableRow: null,
267
+ selectedTableCell: null,
268
+ }),
269
+ cancelBandInsert: () => set({ pendingBandInsertType: null }),
270
+ insertBandAfter: (pageId, afterBandId, bandType) => {
271
+ const { template, dispatcher, pendingBandInsertType } = get();
272
+ const page = template.pages.find(item => item.id === pageId);
273
+ const type = bandType ?? pendingBandInsertType;
274
+ if (!page || !type)
275
+ return;
276
+ const band = createBandForInsert(page.bands, type);
277
+ const newTemplate = dispatcher.execute(template, {
278
+ type: INSERT_BAND_COMMAND,
279
+ payload: { pageId, afterBandId, band },
280
+ execute: () => template,
281
+ undo: () => template,
282
+ });
283
+ set({
284
+ template: newTemplate,
285
+ selectedComponentIds: [],
286
+ selectedBandId: band.id,
287
+ selectedTableRow: null,
288
+ selectedTableCell: null,
289
+ pendingBandInsertType: null,
290
+ });
291
+ },
292
+ duplicateBandAfter: (pageId, bandId) => {
293
+ const { template, dispatcher } = get();
294
+ const page = template.pages.find(item => item.id === pageId);
295
+ const source = page?.bands.find(band => band.id === bandId);
296
+ if (!page || !source)
297
+ return;
298
+ const band = cloneBandForInsert(page.bands, source, template);
299
+ const newTemplate = dispatcher.execute(template, {
300
+ type: INSERT_BAND_COMMAND,
301
+ payload: { pageId, afterBandId: bandId, band },
302
+ execute: () => template,
303
+ undo: () => template,
304
+ });
305
+ set({
306
+ template: newTemplate,
307
+ selectedComponentIds: [],
308
+ selectedBandId: band.id,
309
+ selectedTableCell: null,
310
+ pendingBandInsertType: null,
311
+ });
312
+ },
313
+ moveBand: (pageId, bandId, targetIndex) => {
314
+ const { template, dispatcher } = get();
315
+ const page = template.pages.find(item => item.id === pageId);
316
+ const fromIndex = page?.bands.findIndex(band => band.id === bandId) ?? -1;
317
+ if (!page || fromIndex < 0)
318
+ return;
319
+ const normalizedTargetIndex = clampBandIndex(targetIndex, page.bands.length);
320
+ if (fromIndex === normalizedTargetIndex) {
321
+ set({
322
+ selectedBandId: bandId,
323
+ selectedComponentIds: [],
324
+ selectedTableRow: null,
325
+ selectedTableCell: null,
326
+ });
327
+ return;
328
+ }
329
+ const newTemplate = dispatcher.execute(template, {
330
+ type: MOVE_BAND_COMMAND,
331
+ payload: { pageId, bandId, fromIndex, targetIndex: normalizedTargetIndex },
332
+ execute: () => template,
333
+ undo: () => template,
334
+ });
335
+ set({
336
+ template: newTemplate,
337
+ selectedBandId: bandId,
338
+ selectedComponentIds: [],
339
+ selectedTableRow: null,
340
+ selectedTableCell: null,
341
+ pendingBandInsertType: null,
342
+ });
343
+ },
344
+ selectTableRow: (selection) => set({
345
+ selectedTableRow: selection,
346
+ selectedTableCell: null,
347
+ selectedComponentIds: selection ? [selection.tableId] : get().selectedComponentIds,
348
+ selectedBandId: selection ? null : get().selectedBandId,
349
+ }),
350
+ selectTableCell: (selection) => set({
351
+ selectedTableCell: selection ? normalizeTableCellSelection(selection) : null,
352
+ selectedTableRow: null,
353
+ selectedComponentIds: selection ? [selection.tableId] : get().selectedComponentIds,
354
+ selectedBandId: selection ? null : get().selectedBandId,
355
+ }),
356
+ openEventEditorTarget: (target) => set(state => ({
357
+ pendingEventEditorTarget: {
358
+ ...target,
359
+ requestId: target.nonce ?? Date.now(),
360
+ },
361
+ })),
362
+ consumeEventEditorTarget: (requestId) => set(state => (state.pendingEventEditorTarget?.requestId === requestId
363
+ ? { pendingEventEditorTarget: null }
364
+ : {})),
365
+ setDataSources: (data) => set({ dataSources: data }),
366
+ setReportUnit: (reportUnit) => set({ reportUnit }),
367
+ setZoom: (zoom) => set({ zoom }),
368
+ addComponent: (pageId, bandId, component) => {
369
+ const { template, dispatcher } = get();
370
+ const styledComponent = isTextComponent(component)
371
+ ? applyDefaultTextStyle(component, template.styles)
372
+ : component;
373
+ const normalizedComponent = prepareComponentForInsert(template, styledComponent);
374
+ const newTemplate = dispatcher.execute(template, {
375
+ type: 'add-component',
376
+ payload: { pageId, bandId, component: normalizedComponent },
377
+ execute: () => template,
378
+ undo: () => template,
379
+ });
380
+ set({ template: newTemplate, selectedComponentIds: [normalizedComponent.id], selectedBandId: undefined });
381
+ },
382
+ addComponentToPanel: (pageId, bandId, panelId, component) => {
383
+ const { template, dispatcher } = get();
384
+ const band = findBand(template, pageId, bandId);
385
+ const panel = band?.components.find(item => item.id === panelId && item.type === 'panel');
386
+ if (!panel)
387
+ return;
388
+ const styledComponent = isTextComponent(component)
389
+ ? applyDefaultTextStyle(component, template.styles)
390
+ : component;
391
+ const normalizedComponent = prepareComponentForInsert(template, styledComponent);
392
+ const previous = { ...panel, components: [...(panel.components ?? [])] };
393
+ const nextComponents = [...(panel.components ?? []), normalizedComponent];
394
+ const newTemplate = dispatcher.execute(template, {
395
+ type: 'update-component',
396
+ payload: { pageId, bandId, componentId: panelId, updates: { components: nextComponents }, previous },
397
+ execute: () => template,
398
+ undo: () => template,
399
+ });
400
+ set({ template: newTemplate, selectedComponentIds: [panelId], selectedBandId: undefined });
401
+ },
402
+ removeComponent: (pageId, bandId, componentId) => {
403
+ const { template, dispatcher } = get();
404
+ const band = findBand(template, pageId, bandId);
405
+ const comp = band?.components.find(c => c.id === componentId);
406
+ const newTemplate = dispatcher.execute(template, {
407
+ type: 'remove-component',
408
+ payload: { pageId, bandId, componentId, component: comp },
409
+ execute: () => template,
410
+ undo: () => template,
411
+ });
412
+ set({ template: newTemplate });
413
+ },
414
+ updateComponent: (pageId, bandId, componentId, updates, previous) => {
415
+ const { template, dispatcher } = get();
416
+ const band = findBand(template, pageId, bandId);
417
+ const comp = band ? findComponentInTree(band.components, componentId) : undefined;
418
+ if (!comp)
419
+ return;
420
+ const normalizedUpdates = comp && isTextComponent(comp)
421
+ ? normalizeLocalTextComponentUpdates(comp, updates)
422
+ : updates;
423
+ const sanitizedUpdates = sanitizeComponentUpdates(template, componentId, normalizedUpdates);
424
+ const changedUpdates = getChangedComponentUpdates(comp, sanitizedUpdates);
425
+ if (Object.keys(changedUpdates).length === 0)
426
+ return;
427
+ const prevData = previous || (comp ? { ...comp } : undefined);
428
+ const newTemplate = dispatcher.execute(template, {
429
+ type: UPDATE_COMPONENTS_COMMAND,
430
+ payload: {
431
+ updates: [{ pageId, bandId, componentId, updates: changedUpdates }],
432
+ previous: prevData ? [{ pageId, bandId, componentId, updates: prevData }] : [],
433
+ },
434
+ execute: () => template,
435
+ undo: () => template,
436
+ });
437
+ if (newTemplate !== template)
438
+ set({ template: newTemplate });
439
+ },
440
+ moveComponent: (pageId, bandId, componentId, x, y, prevX, prevY) => {
441
+ const { template, dispatcher } = get();
442
+ const newTemplate = dispatcher.execute(template, {
443
+ type: 'move-component',
444
+ payload: { pageId, bandId, componentId, x, y, prevX, prevY },
445
+ execute: () => template,
446
+ undo: () => template,
447
+ });
448
+ set({ template: newTemplate });
449
+ },
450
+ moveComponentToBand: (pageId, fromBandId, toBandId, componentId, x, y, prevX, prevY) => {
451
+ const { template, dispatcher } = get();
452
+ if (fromBandId === toBandId) {
453
+ get().moveComponent(pageId, fromBandId, componentId, x, y, prevX, prevY);
454
+ return;
455
+ }
456
+ const sourceBand = findBand(template, pageId, fromBandId);
457
+ const component = sourceBand?.components.find(item => item.id === componentId);
458
+ if (!component)
459
+ return;
460
+ const newTemplate = dispatcher.execute(template, {
461
+ type: MOVE_COMPONENT_TO_BAND_COMMAND,
462
+ payload: { pageId, fromBandId, toBandId, componentId, component, x, y, prevX, prevY },
463
+ execute: () => template,
464
+ undo: () => template,
465
+ });
466
+ set({
467
+ template: newTemplate,
468
+ selectedComponentIds: [componentId],
469
+ selectedBandId: null,
470
+ selectedTableRow: null,
471
+ selectedTableCell: null,
472
+ });
473
+ },
474
+ updateComponentSilent: (pageId, bandId, componentId, updates) => {
475
+ const { template } = get();
476
+ const newTemplate = applyComponentUpdates(template, [{ pageId, bandId, componentId, updates }]);
477
+ if (newTemplate !== template)
478
+ set({ template: newTemplate });
479
+ },
480
+ moveComponentSilent: (pageId, bandId, componentId, x, y) => {
481
+ const { template } = get();
482
+ const newTemplate = {
483
+ ...template,
484
+ pages: template.pages.map(p => {
485
+ if (p.id !== pageId)
486
+ return p;
487
+ return {
488
+ ...p,
489
+ bands: p.bands.map(b => {
490
+ if (b.id !== bandId)
491
+ return b;
492
+ return {
493
+ ...b,
494
+ components: b.components.map(c => {
495
+ if (c.id !== componentId)
496
+ return c;
497
+ return { ...c, x, y };
498
+ }),
499
+ };
500
+ }),
501
+ };
502
+ }),
503
+ };
504
+ set({ template: newTemplate });
505
+ },
506
+ resizeBand: (pageId, bandId, newHeight, oldHeight) => {
507
+ const { template, dispatcher } = get();
508
+ const band = findBand(template, pageId, bandId);
509
+ const normalizedHeight = normalizeBandHeight(newHeight, band);
510
+ const newTemplate = dispatcher.execute(template, {
511
+ type: 'resize-band',
512
+ payload: { pageId, bandId, newHeight: normalizedHeight, oldHeight },
513
+ execute: () => template,
514
+ undo: () => template,
515
+ });
516
+ set({ template: newTemplate });
517
+ },
518
+ resizeBandSilent: (pageId, bandId, newHeight) => {
519
+ const { template } = get();
520
+ const newTemplate = {
521
+ ...template,
522
+ pages: template.pages.map(p => {
523
+ if (p.id !== pageId)
524
+ return p;
525
+ return {
526
+ ...p,
527
+ bands: p.bands.map(b => {
528
+ if (b.id !== bandId)
529
+ return b;
530
+ return { ...b, height: normalizeBandHeight(newHeight, b) };
531
+ }),
532
+ };
533
+ }),
534
+ };
535
+ set({ template: newTemplate });
536
+ },
537
+ undo: () => {
538
+ const { template, dispatcher } = get();
539
+ const newTemplate = dispatcher.undo(template);
540
+ if (newTemplate)
541
+ set({ template: newTemplate });
542
+ },
543
+ redo: () => {
544
+ const { template, dispatcher } = get();
545
+ const newTemplate = dispatcher.redo(template);
546
+ if (newTemplate)
547
+ set({ template: newTemplate });
548
+ },
549
+ canUndo: () => get().dispatcher.canUndo(),
550
+ canRedo: () => get().dispatcher.canRedo(),
551
+ alignComponents: (alignment) => {
552
+ const { template, currentPageId, selectedComponentIds, dispatcher } = get();
553
+ if (selectedComponentIds.length < 2)
554
+ return;
555
+ // Find all selected components in the same band
556
+ const page = template.pages.find(p => p.id === currentPageId);
557
+ if (!page)
558
+ return;
559
+ // Group by band
560
+ const byBand = new Map();
561
+ for (const id of selectedComponentIds) {
562
+ for (const band of page.bands) {
563
+ if (band.components.some(c => c.id === id)) {
564
+ if (!byBand.has(band.id))
565
+ byBand.set(band.id, []);
566
+ byBand.get(band.id).push(id);
567
+ break;
568
+ }
569
+ }
570
+ }
571
+ let newTemplate = template;
572
+ for (const [bandId, compIds] of byBand) {
573
+ const band = page.bands.find(b => b.id === bandId);
574
+ if (!band)
575
+ continue;
576
+ const comps = compIds.map(id => band.components.find(c => c.id === id)).filter(Boolean);
577
+ if (comps.length < 2)
578
+ continue;
579
+ const updates = [];
580
+ const previous = [];
581
+ switch (alignment) {
582
+ case 'left': {
583
+ const minX = Math.min(...comps.map(c => c.x));
584
+ for (const c of comps) {
585
+ previous.push({ x: c.x });
586
+ updates.push({ x: minX });
587
+ }
588
+ break;
589
+ }
590
+ case 'right': {
591
+ const maxX = Math.max(...comps.map(c => c.x + c.width));
592
+ for (const c of comps) {
593
+ previous.push({ x: c.x });
594
+ updates.push({ x: maxX - c.width });
595
+ }
596
+ break;
597
+ }
598
+ case 'center-h': {
599
+ const minX = Math.min(...comps.map(c => c.x));
600
+ const maxX = Math.max(...comps.map(c => c.x + c.width));
601
+ const center = (minX + maxX) / 2;
602
+ for (const c of comps) {
603
+ previous.push({ x: c.x });
604
+ updates.push({ x: center - c.width / 2 });
605
+ }
606
+ break;
607
+ }
608
+ case 'top': {
609
+ const minY = Math.min(...comps.map(c => c.y));
610
+ for (const c of comps) {
611
+ previous.push({ y: c.y });
612
+ updates.push({ y: minY });
613
+ }
614
+ break;
615
+ }
616
+ case 'bottom': {
617
+ const maxY = Math.max(...comps.map(c => c.y + c.height));
618
+ for (const c of comps) {
619
+ previous.push({ y: c.y });
620
+ updates.push({ y: maxY - c.height });
621
+ }
622
+ break;
623
+ }
624
+ case 'center-v': {
625
+ const minY = Math.min(...comps.map(c => c.y));
626
+ const maxY = Math.max(...comps.map(c => c.y + c.height));
627
+ const center = (minY + maxY) / 2;
628
+ for (const c of comps) {
629
+ previous.push({ y: c.y });
630
+ updates.push({ y: center - c.height / 2 });
631
+ }
632
+ break;
633
+ }
634
+ case 'distribute-h': {
635
+ if (comps.length < 3)
636
+ continue;
637
+ const sorted = [...comps].sort((a, b) => a.x - b.x);
638
+ const minLeft = sorted[0].x;
639
+ const maxRight = sorted[sorted.length - 1].x + sorted[sorted.length - 1].width;
640
+ const totalWidth = sorted.reduce((sum, c) => sum + c.width, 0);
641
+ const gap = (maxRight - minLeft - totalWidth) / (sorted.length - 1);
642
+ let cursor = minLeft;
643
+ for (let i = 0; i < sorted.length; i += 1) {
644
+ const c = sorted[i];
645
+ previous.push({ x: c.x });
646
+ updates.push({ x: Math.round(cursor * 10) / 10 });
647
+ cursor += c.width + gap;
648
+ }
649
+ break;
650
+ }
651
+ case 'distribute-v': {
652
+ if (comps.length < 3)
653
+ continue;
654
+ const sorted = [...comps].sort((a, b) => a.y - b.y);
655
+ const minTop = sorted[0].y;
656
+ const maxBottom = sorted[sorted.length - 1].y + sorted[sorted.length - 1].height;
657
+ const totalHeight = sorted.reduce((sum, c) => sum + c.height, 0);
658
+ const gap = (maxBottom - minTop - totalHeight) / (sorted.length - 1);
659
+ let cursor = minTop;
660
+ for (let i = 0; i < sorted.length; i += 1) {
661
+ const c = sorted[i];
662
+ previous.push({ y: c.y });
663
+ updates.push({ y: Math.round(cursor * 10) / 10 });
664
+ cursor += c.height + gap;
665
+ }
666
+ break;
667
+ }
668
+ }
669
+ if (updates.length > 0) {
670
+ newTemplate = dispatcher.execute(newTemplate, {
671
+ type: 'align-components',
672
+ payload: { pageId: currentPageId, bandId, componentIds: compIds, alignment, updates, previous },
673
+ execute: () => newTemplate,
674
+ undo: () => newTemplate,
675
+ });
676
+ }
677
+ }
678
+ set({ template: newTemplate });
679
+ },
680
+ sizeComponents: (sizeMode) => {
681
+ const { template, currentPageId, selectedComponentIds, dispatcher } = get();
682
+ if (selectedComponentIds.length < 2)
683
+ return;
684
+ const page = template.pages.find(p => p.id === currentPageId);
685
+ if (!page)
686
+ return;
687
+ // Group by band
688
+ const byBand = new Map();
689
+ for (const id of selectedComponentIds) {
690
+ for (const band of page.bands) {
691
+ if (band.components.some(c => c.id === id)) {
692
+ if (!byBand.has(band.id))
693
+ byBand.set(band.id, []);
694
+ byBand.get(band.id).push(id);
695
+ break;
696
+ }
697
+ }
698
+ }
699
+ let newTemplate = template;
700
+ for (const [bandId, compIds] of byBand) {
701
+ const band = page.bands.find(b => b.id === bandId);
702
+ if (!band)
703
+ continue;
704
+ const comps = compIds.map(id => band.components.find(c => c.id === id)).filter(Boolean);
705
+ if (comps.length < 2)
706
+ continue;
707
+ const first = comps[0];
708
+ const updates = [];
709
+ const previous = [];
710
+ switch (sizeMode) {
711
+ case 'same-width':
712
+ for (const c of comps) {
713
+ previous.push({ width: c.width });
714
+ updates.push({ width: first.width });
715
+ }
716
+ break;
717
+ case 'same-height':
718
+ for (const c of comps) {
719
+ previous.push({ height: c.height });
720
+ updates.push({ height: first.height });
721
+ }
722
+ break;
723
+ case 'same-size':
724
+ for (const c of comps) {
725
+ previous.push({ width: c.width, height: c.height });
726
+ updates.push({ width: first.width, height: first.height });
727
+ }
728
+ break;
729
+ }
730
+ if (updates.length > 0) {
731
+ newTemplate = dispatcher.execute(newTemplate, {
732
+ type: 'size-components',
733
+ payload: { pageId: currentPageId, bandId, componentIds: compIds, sizeMode, updates, previous },
734
+ execute: () => newTemplate,
735
+ undo: () => newTemplate,
736
+ });
737
+ }
738
+ }
739
+ set({ template: newTemplate });
740
+ },
741
+ bringToFront: () => {
742
+ const { template, currentPageId, selectedComponentIds, dispatcher } = get();
743
+ if (selectedComponentIds.length === 0)
744
+ return;
745
+ const page = template.pages.find(p => p.id === currentPageId);
746
+ if (!page)
747
+ return;
748
+ let newTemplate = template;
749
+ for (const band of page.bands) {
750
+ const bandComps = band.components.filter(c => selectedComponentIds.includes(c.id));
751
+ if (bandComps.length === 0)
752
+ continue;
753
+ const orderedComps = [...bandComps].sort((a, b) => selectedComponentIds.indexOf(a.id) - selectedComponentIds.indexOf(b.id));
754
+ const zOrders = band.components.map(c => c.zOrder ?? 0);
755
+ const maxZ = zOrders.length > 0 ? Math.max(...zOrders) : 0;
756
+ newTemplate = dispatcher.execute(newTemplate, {
757
+ type: 'align-components',
758
+ payload: {
759
+ pageId: currentPageId,
760
+ bandId: band.id,
761
+ componentIds: orderedComps.map(comp => comp.id),
762
+ alignment: 'bring-to-front',
763
+ updates: orderedComps.map((comp, index) => ({ zOrder: maxZ + index + 1 })),
764
+ previous: orderedComps.map(comp => ({ zOrder: comp.zOrder })),
765
+ },
766
+ execute: () => newTemplate,
767
+ undo: () => newTemplate,
768
+ });
769
+ }
770
+ set({ template: newTemplate });
771
+ },
772
+ sendToBack: () => {
773
+ const { template, currentPageId, selectedComponentIds, dispatcher } = get();
774
+ if (selectedComponentIds.length === 0)
775
+ return;
776
+ const page = template.pages.find(p => p.id === currentPageId);
777
+ if (!page)
778
+ return;
779
+ let newTemplate = template;
780
+ for (const band of page.bands) {
781
+ const bandComps = band.components.filter(c => selectedComponentIds.includes(c.id));
782
+ if (bandComps.length === 0)
783
+ continue;
784
+ const orderedComps = [...bandComps].sort((a, b) => selectedComponentIds.indexOf(a.id) - selectedComponentIds.indexOf(b.id));
785
+ const zOrders = band.components.map(c => c.zOrder ?? 0);
786
+ const minZ = zOrders.length > 0 ? Math.min(...zOrders) : 0;
787
+ newTemplate = dispatcher.execute(newTemplate, {
788
+ type: 'align-components',
789
+ payload: {
790
+ pageId: currentPageId,
791
+ bandId: band.id,
792
+ componentIds: orderedComps.map(comp => comp.id),
793
+ alignment: 'send-to-back',
794
+ updates: orderedComps.map((comp, index) => ({ zOrder: minZ - orderedComps.length + index })),
795
+ previous: orderedComps.map(comp => ({ zOrder: comp.zOrder })),
796
+ },
797
+ execute: () => newTemplate,
798
+ undo: () => newTemplate,
799
+ });
800
+ }
801
+ set({ template: newTemplate });
802
+ },
803
+ deleteSelected: () => {
804
+ const { template, currentPageId, selectedComponentIds, selectedBandId, dispatcher } = get();
805
+ const page = template.pages.find(p => p.id === currentPageId);
806
+ if (!page)
807
+ return;
808
+ if (selectedBandId && selectedComponentIds.length === 0) {
809
+ const bandIndex = page.bands.findIndex(band => band.id === selectedBandId);
810
+ const band = bandIndex >= 0 ? page.bands[bandIndex] : undefined;
811
+ if (!band)
812
+ return;
813
+ const newTemplate = dispatcher.execute(template, {
814
+ type: DELETE_BAND_COMMAND,
815
+ payload: { pageId: currentPageId, band, index: bandIndex },
816
+ execute: () => template,
817
+ undo: () => template,
818
+ });
819
+ set({
820
+ template: newTemplate,
821
+ selectedBandId: null,
822
+ selectedComponentIds: [],
823
+ selectedTableRow: null,
824
+ selectedTableCell: null,
825
+ });
826
+ return;
827
+ }
828
+ let newTemplate = template;
829
+ for (const band of page.bands) {
830
+ const components = band.components.filter(c => selectedComponentIds.includes(c.id));
831
+ if (components.length === 0)
832
+ continue;
833
+ for (const component of components) {
834
+ newTemplate = dispatcher.execute(newTemplate, {
835
+ type: 'remove-component',
836
+ payload: { pageId: currentPageId, bandId: band.id, componentId: component.id, component },
837
+ execute: () => newTemplate,
838
+ undo: () => newTemplate,
839
+ });
840
+ }
841
+ }
842
+ set({ template: newTemplate, selectedComponentIds: [] });
843
+ },
844
+ copySelected: () => {
845
+ const { template, currentPageId, selectedComponentIds } = get();
846
+ const page = template.pages.find(p => p.id === currentPageId);
847
+ if (!page)
848
+ return;
849
+ const comps = [];
850
+ for (const band of page.bands) {
851
+ for (const comp of band.components) {
852
+ if (selectedComponentIds.includes(comp.id)) {
853
+ comps.push({ ...comp });
854
+ }
855
+ }
856
+ }
857
+ set({ clipboard: comps });
858
+ },
859
+ cutSelected: () => {
860
+ get().copySelected();
861
+ get().deleteSelected();
862
+ },
863
+ duplicateSelected: () => {
864
+ get().copySelected();
865
+ get().pasteClipboard();
866
+ },
867
+ pasteClipboard: () => {
868
+ const { template, currentPageId, selectedComponentIds, clipboard, dispatcher } = get();
869
+ if (clipboard.length === 0)
870
+ return;
871
+ const page = template.pages.find(p => p.id === currentPageId);
872
+ if (!page)
873
+ return;
874
+ const pasteBandId = selectedComponentIds.length > 0
875
+ ? page.bands.find(b => b.components.some(c => c.id === selectedComponentIds[0]))?.id
876
+ : page.bands.find(b => b.type === 'data')?.id ?? page.bands[0]?.id;
877
+ if (!pasteBandId)
878
+ return;
879
+ const newIds = [];
880
+ let newTemplate = template;
881
+ for (const comp of clipboard) {
882
+ const preparedComp = prepareComponentForInsert(newTemplate, { ...comp, name: comp.name });
883
+ const clonedComp = cloneComponentTreeWithNewIds(preparedComp);
884
+ const component = {
885
+ ...clonedComp,
886
+ x: comp.x + 5,
887
+ y: comp.y + 5,
888
+ };
889
+ newTemplate = dispatcher.execute(newTemplate, {
890
+ type: 'add-component',
891
+ payload: { pageId: currentPageId, bandId: pasteBandId, component },
892
+ execute: () => newTemplate,
893
+ undo: () => newTemplate,
894
+ });
895
+ newIds.push(component.id);
896
+ }
897
+ set({ template: newTemplate, selectedComponentIds: newIds });
898
+ },
899
+ getClipboard: () => get().clipboard,
900
+ moveSelectedBy: (dx, dy) => {
901
+ const { template, currentPageId, selectedComponentIds, dispatcher } = get();
902
+ const page = template.pages.find(p => p.id === currentPageId);
903
+ if (!page || selectedComponentIds.length === 0)
904
+ return;
905
+ let newTemplate = template;
906
+ for (const band of page.bands) {
907
+ const comps = band.components.filter(comp => selectedComponentIds.includes(comp.id));
908
+ if (comps.length === 0)
909
+ continue;
910
+ const updates = comps.map(comp => ({
911
+ x: Math.round((comp.x + dx) * 10) / 10,
912
+ y: Math.round((comp.y + dy) * 10) / 10,
913
+ }));
914
+ const previous = comps.map(comp => ({ x: comp.x, y: comp.y }));
915
+ newTemplate = dispatcher.execute(newTemplate, {
916
+ type: 'align-components',
917
+ payload: { pageId: currentPageId, bandId: band.id, componentIds: comps.map(comp => comp.id), alignment: 'move', updates, previous },
918
+ execute: () => newTemplate,
919
+ undo: () => newTemplate,
920
+ });
921
+ }
922
+ set({ template: newTemplate });
923
+ },
924
+ resizeSelectedBy: (dw, dh) => {
925
+ const { template, currentPageId, selectedComponentIds, dispatcher } = get();
926
+ const page = template.pages.find(p => p.id === currentPageId);
927
+ if (!page || selectedComponentIds.length === 0)
928
+ return;
929
+ let newTemplate = template;
930
+ for (const band of page.bands) {
931
+ const comps = band.components.filter(comp => selectedComponentIds.includes(comp.id));
932
+ if (comps.length === 0)
933
+ continue;
934
+ const updates = comps.map(comp => ({
935
+ width: Math.max(1, Math.round((comp.width + dw) * 10) / 10),
936
+ height: Math.max(1, Math.round((comp.height + dh) * 10) / 10),
937
+ }));
938
+ const previous = comps.map(comp => ({ width: comp.width, height: comp.height }));
939
+ newTemplate = dispatcher.execute(newTemplate, {
940
+ type: 'size-components',
941
+ payload: { pageId: currentPageId, bandId: band.id, componentIds: comps.map(comp => comp.id), sizeMode: 'nudge-size', updates, previous },
942
+ execute: () => newTemplate,
943
+ undo: () => newTemplate,
944
+ });
945
+ }
946
+ set({ template: newTemplate });
947
+ },
948
+ toggleSelectedFontStyle: (style) => {
949
+ const { template, currentPageId, selectedComponentIds } = get();
950
+ const newTemplate = mapSelectedComponents(template, currentPageId, selectedComponentIds, comp => {
951
+ const font = comp.font;
952
+ if (!font)
953
+ return comp;
954
+ const fontUpdates = { [style]: !font[style] };
955
+ if (isTextComponent(comp)) {
956
+ return applyLocalTextComponentUpdates(comp, { font: fontUpdates });
957
+ }
958
+ return { ...comp, font: { ...font, ...fontUpdates } };
959
+ });
960
+ set({ template: newTemplate });
961
+ },
962
+ updateSelectedTable: (updates) => {
963
+ updateSelectedTableComponents(table => setTableStructure(table, updates));
964
+ },
965
+ updateSelectedTableRow: (updates) => {
966
+ const { selectedTableRow } = get();
967
+ if (!selectedTableRow)
968
+ return;
969
+ updateSelectedTableComponents(table => updateTableRow(table, selectedTableRow.row, updates));
970
+ },
971
+ updateSelectedTableCell: (updates) => {
972
+ updateSelectedTableCellComponent(updates);
973
+ },
974
+ setSelectedTableCellWidth: (row, column, width) => {
975
+ updateSelectedTableComponents(table => setTableCellWidth(table, row, column, width));
976
+ },
977
+ applySelectedStyle: (styleId) => {
978
+ const { template, currentPageId, selectedComponentIds } = get();
979
+ const style = styleId ? template.styles.find(item => item.id === styleId) : undefined;
980
+ const newTemplate = mapSelectedComponents(template, currentPageId, selectedComponentIds, comp => {
981
+ if (!canUseTextStyle(comp))
982
+ return comp;
983
+ if (!styleId || !style) {
984
+ return clearTextStyleReference(comp);
985
+ }
986
+ return applyTextStyleToComponent(comp, style);
987
+ });
988
+ set({ template: newTemplate });
989
+ },
990
+ unbindSelectedStyle: () => {
991
+ const { template, currentPageId, selectedComponentIds } = get();
992
+ const newTemplate = mapSelectedComponents(template, currentPageId, selectedComponentIds, comp => {
993
+ if (!canUseTextStyle(comp))
994
+ return comp;
995
+ return unbindTextStyleFromComponent(comp, template.styles);
996
+ });
997
+ set({ template: newTemplate });
998
+ },
999
+ createTextStyle: (style) => {
1000
+ const { template } = get();
1001
+ const nextStyle = createTextStyleDraft(template, style);
1002
+ set({ template: { ...template, styles: [...template.styles, nextStyle] } });
1003
+ return nextStyle.id;
1004
+ },
1005
+ duplicateTextStyle: (styleId) => {
1006
+ const { template } = get();
1007
+ const style = template.styles.find(item => item.id === styleId);
1008
+ if (!style)
1009
+ return undefined;
1010
+ const duplicate = cloneTextStyle(style, {
1011
+ id: createTextStyleId(),
1012
+ name: `${style.name} Copy`,
1013
+ isDefault: false,
1014
+ });
1015
+ set({ template: { ...template, styles: [...template.styles, duplicate] } });
1016
+ return duplicate.id;
1017
+ },
1018
+ renameTextStyle: (styleId, name) => {
1019
+ const { template } = get();
1020
+ set({
1021
+ template: {
1022
+ ...template,
1023
+ styles: template.styles.map(style => (style.id === styleId ? { ...style, name } : style)),
1024
+ },
1025
+ });
1026
+ },
1027
+ updateTextStyle: (styleId, updates) => {
1028
+ const { template } = get();
1029
+ const styles = template.styles.map(style => (style.id === styleId ? mergeTextStyle(style, updates) : style));
1030
+ const nextTemplate = syncTextStyleReferencesInTemplate({ ...template, styles }, styleId);
1031
+ set({ template: nextTemplate });
1032
+ },
1033
+ deleteTextStyle: (styleId) => {
1034
+ const { template } = get();
1035
+ const nextTemplate = clearTextStyleReferencesInTemplate({
1036
+ ...template,
1037
+ styles: template.styles.filter(style => style.id !== styleId),
1038
+ }, styleId);
1039
+ set({ template: nextTemplate });
1040
+ },
1041
+ setDefaultTextStyle: (styleId) => {
1042
+ const { template } = get();
1043
+ set({
1044
+ template: {
1045
+ ...template,
1046
+ styles: template.styles.map(style => ({
1047
+ ...style,
1048
+ isDefault: styleId ? style.id === styleId : false,
1049
+ })),
1050
+ },
1051
+ });
1052
+ },
1053
+ syncTextStyleReferences: (styleId) => {
1054
+ const { template } = get();
1055
+ set({ template: syncTextStyleReferencesInTemplate(template, styleId) });
1056
+ },
1057
+ getTextStyleUsageCount: (styleId) => {
1058
+ const { template } = get();
1059
+ return countTextStyleUsage(template, styleId);
1060
+ },
1061
+ applySelectedConditionalFormat: (formatId) => {
1062
+ const { template, currentPageId, selectedComponentIds } = get();
1063
+ const exists = formatId ? template.conditionalFormats.some(item => item.id === formatId) : false;
1064
+ const nextFormatId = exists ? formatId : undefined;
1065
+ const newTemplate = mapSelectedComponents(template, currentPageId, selectedComponentIds, component => {
1066
+ const nextComponent = { ...component };
1067
+ if (nextFormatId) {
1068
+ nextComponent.conditionalFormat = nextFormatId;
1069
+ }
1070
+ else {
1071
+ delete nextComponent.conditionalFormat;
1072
+ }
1073
+ return nextComponent;
1074
+ });
1075
+ set({ template: newTemplate });
1076
+ },
1077
+ createConditionalFormat: (format) => {
1078
+ const { template } = get();
1079
+ const nextFormat = createConditionalFormatDraft(template, format);
1080
+ set({ template: { ...template, conditionalFormats: [...template.conditionalFormats, nextFormat] } });
1081
+ return nextFormat.id;
1082
+ },
1083
+ duplicateConditionalFormat: (formatId) => {
1084
+ const { template } = get();
1085
+ const format = template.conditionalFormats.find(item => item.id === formatId);
1086
+ if (!format)
1087
+ return undefined;
1088
+ const duplicate = cloneConditionalFormat(format, {
1089
+ id: createConditionalFormatId(),
1090
+ name: `${format.name} Copy`,
1091
+ });
1092
+ set({ template: { ...template, conditionalFormats: [...template.conditionalFormats, duplicate] } });
1093
+ return duplicate.id;
1094
+ },
1095
+ updateConditionalFormat: (formatId, updates) => {
1096
+ const { template } = get();
1097
+ set({
1098
+ template: {
1099
+ ...template,
1100
+ conditionalFormats: template.conditionalFormats.map(format => (format.id === formatId ? mergeConditionalFormat(format, updates) : format)),
1101
+ },
1102
+ });
1103
+ },
1104
+ deleteConditionalFormat: (formatId) => {
1105
+ const { template } = get();
1106
+ const nextTemplate = clearConditionalFormatReferencesInTemplate({
1107
+ ...template,
1108
+ conditionalFormats: template.conditionalFormats.filter(format => format.id !== formatId),
1109
+ }, formatId);
1110
+ set({ template: nextTemplate });
1111
+ },
1112
+ insertSelectedTableColumn: (afterColumn) => {
1113
+ updateSelectedTableComponents(table => insertTableColumn(table, afterColumn));
1114
+ },
1115
+ deleteSelectedTableColumn: (columnIndex) => {
1116
+ updateSelectedTableComponents(table => deleteTableColumn(table, columnIndex));
1117
+ },
1118
+ insertSelectedTableRow: (afterRow) => {
1119
+ updateSelectedTableComponents(table => insertTableRow(table, afterRow));
1120
+ },
1121
+ deleteSelectedTableRow: (rowIndex) => {
1122
+ updateSelectedTableComponents(table => deleteTableRow(table, rowIndex));
1123
+ },
1124
+ mergeSelectedTableCellRight: (row, column) => {
1125
+ updateSelectedTableComponents(table => mergeTableCellRight(table, row, column));
1126
+ },
1127
+ mergeSelectedTableCellRange: () => {
1128
+ const { selectedTableCell } = get();
1129
+ if (!selectedTableCell)
1130
+ return;
1131
+ const selection = normalizeTableCellSelection(selectedTableCell);
1132
+ updateSelectedTableComponents(table => mergeTableCellRange(table, selection.startRow, selection.startColumn, selection.endRow, selection.endColumn));
1133
+ },
1134
+ splitSelectedTableCell: (row, column) => {
1135
+ updateSelectedTableComponents(table => splitTableCell(table, row, column));
1136
+ },
1137
+ clearSelectedTableCell: (row, column) => {
1138
+ updateSelectedTableComponents(table => clearTableCell(table, row, column));
1139
+ },
1140
+ clearSelectedTableCellStyle: (row, column) => {
1141
+ updateSelectedTableComponents(table => clearTableCellStyle(table, row, column));
1142
+ },
1143
+ copySelectedTableCellStyle: (row, column) => {
1144
+ const { template, currentPageId, selectedComponentIds } = get();
1145
+ const page = template.pages.find(item => item.id === currentPageId);
1146
+ const selected = new Set(selectedComponentIds);
1147
+ const table = page?.bands
1148
+ .flatMap(band => band.components)
1149
+ .find(component => selected.has(component.id) && component.type === 'table');
1150
+ if (!table)
1151
+ return;
1152
+ set({ tableCellStyleClipboard: copyTableCellStyle(table, row, column) });
1153
+ },
1154
+ pasteSelectedTableCellStyle: (row, column) => {
1155
+ const { tableCellStyleClipboard } = get();
1156
+ updateSelectedTableComponents(table => pasteTableCellStyle(table, row, column, tableCellStyleClipboard));
1157
+ },
1158
+ equalizeSelectedTableColumns: () => {
1159
+ updateSelectedTableComponents(table => equalizeTableColumns(table));
1160
+ },
1161
+ equalizeSelectedTableRows: () => {
1162
+ updateSelectedTableComponents(table => equalizeTableRows(table));
1163
+ },
1164
+ addBand: (pageId, band) => {
1165
+ const { template } = get();
1166
+ const newBand = prepareBandForInsert(template, {
1167
+ ...band,
1168
+ id: `band_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
1169
+ components: [],
1170
+ });
1171
+ const newTemplate = {
1172
+ ...template,
1173
+ pages: template.pages.map(p => {
1174
+ if (p.id !== pageId)
1175
+ return p;
1176
+ return { ...p, bands: [...p.bands, newBand] };
1177
+ }),
1178
+ };
1179
+ set({ template: newTemplate });
1180
+ },
1181
+ deleteBand: (pageId, bandId) => {
1182
+ const { template, dispatcher } = get();
1183
+ const page = template.pages.find(item => item.id === pageId);
1184
+ const bandIndex = page?.bands.findIndex(band => band.id === bandId) ?? -1;
1185
+ const band = page && bandIndex >= 0 ? page.bands[bandIndex] : undefined;
1186
+ if (!band)
1187
+ return;
1188
+ const newTemplate = dispatcher.execute(template, {
1189
+ type: DELETE_BAND_COMMAND,
1190
+ payload: { pageId, band, index: bandIndex },
1191
+ execute: () => template,
1192
+ undo: () => template,
1193
+ });
1194
+ set({
1195
+ template: newTemplate,
1196
+ selectedBandId: get().selectedBandId === bandId ? null : get().selectedBandId,
1197
+ selectedComponentIds: [],
1198
+ selectedTableRow: null,
1199
+ selectedTableCell: null,
1200
+ });
1201
+ },
1202
+ addPage: () => {
1203
+ const { template } = get();
1204
+ const existingBands = template.pages.flatMap(page => page.bands);
1205
+ const pageHeaderBand = { ...createBandForInsert(existingBands, 'pageHeader'), height: 20 };
1206
+ const dataBand = { ...createBandForInsert([...existingBands, pageHeaderBand], 'data'), height: 50 };
1207
+ const pageFooterBand = { ...createBandForInsert([...existingBands, pageHeaderBand, dataBand], 'pageFooter'), height: 20 };
1208
+ const newPage = {
1209
+ id: `page_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
1210
+ width: DEFAULT_PAGE_WIDTH,
1211
+ height: DEFAULT_PAGE_HEIGHT,
1212
+ margins: { top: 10, right: 10, bottom: 10, left: 10 },
1213
+ orientation: 'portrait',
1214
+ bands: [pageHeaderBand, dataBand, pageFooterBand],
1215
+ };
1216
+ set({ template: { ...template, pages: [...template.pages, newPage] }, currentPageId: newPage.id, selectedComponentIds: [] });
1217
+ },
1218
+ deletePage: (pageId) => {
1219
+ const { template, currentPageId } = get();
1220
+ if (template.pages.length <= 1)
1221
+ return;
1222
+ const newPages = template.pages.filter(p => p.id !== pageId);
1223
+ set({
1224
+ template: { ...template, pages: newPages },
1225
+ currentPageId: currentPageId === pageId ? newPages[0].id : currentPageId,
1226
+ selectedComponentIds: [],
1227
+ });
1228
+ },
1229
+ setPageSettings: (pageId, settings) => {
1230
+ const { template } = get();
1231
+ const newTemplate = {
1232
+ ...template,
1233
+ pages: template.pages.map(p => {
1234
+ if (p.id !== pageId)
1235
+ return p;
1236
+ return { ...p, ...settings };
1237
+ }),
1238
+ };
1239
+ set({ template: newTemplate });
1240
+ },
1241
+ setFontBold: (bold) => {
1242
+ const { template, currentPageId, selectedComponentIds } = get();
1243
+ const newTemplate = mapSelectedComponents(template, currentPageId, selectedComponentIds, comp => {
1244
+ const font = comp.font;
1245
+ if (!font)
1246
+ return comp;
1247
+ if (isTextComponent(comp)) {
1248
+ return applyLocalTextComponentUpdates(comp, { font: { bold } });
1249
+ }
1250
+ return { ...comp, font: { ...font, bold } };
1251
+ });
1252
+ set({ template: newTemplate });
1253
+ },
1254
+ setFontSize: (size) => {
1255
+ const { template, currentPageId, selectedComponentIds } = get();
1256
+ const newTemplate = mapSelectedComponents(template, currentPageId, selectedComponentIds, comp => {
1257
+ const font = comp.font;
1258
+ if (!font)
1259
+ return comp;
1260
+ if (isTextComponent(comp)) {
1261
+ return applyLocalTextComponentUpdates(comp, { font: { size } });
1262
+ }
1263
+ return { ...comp, font: { ...font, size } };
1264
+ });
1265
+ set({ template: newTemplate });
1266
+ },
1267
+ setTextAlign: (align) => {
1268
+ const { template, currentPageId, selectedComponentIds } = get();
1269
+ const newTemplate = mapSelectedComponents(template, currentPageId, selectedComponentIds, comp => {
1270
+ if (comp.textAlign === undefined)
1271
+ return comp;
1272
+ if (isTextComponent(comp)) {
1273
+ return applyLocalTextComponentUpdates(comp, { textAlign: align });
1274
+ }
1275
+ return { ...comp, textAlign: align };
1276
+ });
1277
+ set({ template: newTemplate });
1278
+ },
1279
+ setBorderAll: (enabled) => {
1280
+ const { template, currentPageId, selectedComponentIds } = get();
1281
+ const newTemplate = mapSelectedComponents(template, currentPageId, selectedComponentIds, comp => {
1282
+ const border = comp.border;
1283
+ if (!border)
1284
+ return comp;
1285
+ const borderUpdates = {
1286
+ style: enabled ? 'solid' : 'none',
1287
+ width: enabled ? (border.width || 0.2) : 0,
1288
+ sides: { top: enabled, right: enabled, bottom: enabled, left: enabled },
1289
+ };
1290
+ if (isTextComponent(comp)) {
1291
+ return applyLocalTextComponentUpdates(comp, { border: borderUpdates });
1292
+ }
1293
+ return {
1294
+ ...comp,
1295
+ border: {
1296
+ ...border,
1297
+ ...borderUpdates,
1298
+ },
1299
+ };
1300
+ });
1301
+ set({ template: newTemplate });
1302
+ },
1303
+ getSelectedFont: () => {
1304
+ const { template, currentPageId, selectedComponentIds } = get();
1305
+ if (selectedComponentIds.length !== 1)
1306
+ return null;
1307
+ const page = template.pages.find(p => p.id === currentPageId);
1308
+ if (!page)
1309
+ return null;
1310
+ for (const band of page.bands) {
1311
+ for (const comp of band.components) {
1312
+ if (selectedComponentIds.includes(comp.id) && comp.font) {
1313
+ const f = comp.font;
1314
+ return { family: f.family, size: f.size, bold: f.bold, italic: f.italic, underline: f.underline, strikethrough: f.strikethrough, color: f.color };
1315
+ }
1316
+ }
1317
+ }
1318
+ return null;
1319
+ },
1320
+ getSelectedTextAlign: () => {
1321
+ const { template, currentPageId, selectedComponentIds } = get();
1322
+ if (selectedComponentIds.length !== 1)
1323
+ return null;
1324
+ const page = template.pages.find(p => p.id === currentPageId);
1325
+ if (!page)
1326
+ return null;
1327
+ for (const band of page.bands) {
1328
+ for (const comp of band.components) {
1329
+ if (selectedComponentIds.includes(comp.id) && comp.textAlign !== undefined) {
1330
+ return comp.textAlign;
1331
+ }
1332
+ }
1333
+ }
1334
+ return null;
1335
+ },
1336
+ };
1337
+ });
1338
+ function findBand(template, pageId, bandId) {
1339
+ const page = template.pages.find(p => p.id === pageId);
1340
+ if (!page)
1341
+ return undefined;
1342
+ return page.bands.find(b => b.id === bandId);
1343
+ }
1344
+ function findComponentInTree(components, componentId) {
1345
+ for (const component of components) {
1346
+ if (component.id === componentId)
1347
+ return component;
1348
+ const children = getNestedComponents(component);
1349
+ if (!children)
1350
+ continue;
1351
+ const child = findComponentInTree(children, componentId);
1352
+ if (child)
1353
+ return child;
1354
+ }
1355
+ return undefined;
1356
+ }
1357
+ function getNestedComponents(component) {
1358
+ if (!('components' in component))
1359
+ return undefined;
1360
+ const children = component.components;
1361
+ return Array.isArray(children) ? children : undefined;
1362
+ }
1363
+ function withNestedComponents(component, components) {
1364
+ return { ...component, components };
1365
+ }
1366
+ function cloneComponentTreeWithNewIds(component) {
1367
+ let cloned = {
1368
+ ...component,
1369
+ id: createComponentInstanceId(component.type),
1370
+ };
1371
+ const children = getNestedComponents(component);
1372
+ if (!children)
1373
+ return cloned;
1374
+ cloned = withNestedComponents(cloned, children.map(cloneComponentTreeWithNewIds));
1375
+ return cloned;
1376
+ }
1377
+ function createComponentInstanceId(type) {
1378
+ return `${type}_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1379
+ }
1380
+ function getMinimumBandHeight(band) {
1381
+ if (!band || band.components.length === 0)
1382
+ return 5;
1383
+ const componentBottom = Math.max(...band.components.map(component => component.y + component.height));
1384
+ return Math.max(5, Math.round(componentBottom * 10) / 10);
1385
+ }
1386
+ function normalizeBandHeight(height, band) {
1387
+ const numericHeight = Number.isFinite(height) ? height : 5;
1388
+ return Math.max(getMinimumBandHeight(band), Math.round(numericHeight * 10) / 10);
1389
+ }
1390
+ function insertBandInTemplate(template, pageId, afterBandId, band) {
1391
+ return {
1392
+ ...template,
1393
+ pages: template.pages.map(page => {
1394
+ if (page.id !== pageId)
1395
+ return page;
1396
+ const targetIndex = page.bands.findIndex(item => item.id === afterBandId);
1397
+ if (targetIndex < 0)
1398
+ return { ...page, bands: [...page.bands, band] };
1399
+ return {
1400
+ ...page,
1401
+ bands: [
1402
+ ...page.bands.slice(0, targetIndex + 1),
1403
+ band,
1404
+ ...page.bands.slice(targetIndex + 1),
1405
+ ],
1406
+ };
1407
+ }),
1408
+ };
1409
+ }
1410
+ function clampBandIndex(index, length) {
1411
+ if (length <= 0)
1412
+ return 0;
1413
+ if (!Number.isFinite(index))
1414
+ return length - 1;
1415
+ return Math.max(0, Math.min(Math.round(index), length - 1));
1416
+ }
1417
+ function moveBandInTemplate(template, pageId, bandId, targetIndex) {
1418
+ return {
1419
+ ...template,
1420
+ pages: template.pages.map(page => {
1421
+ if (page.id !== pageId)
1422
+ return page;
1423
+ const fromIndex = page.bands.findIndex(band => band.id === bandId);
1424
+ if (fromIndex < 0)
1425
+ return page;
1426
+ const normalizedTargetIndex = clampBandIndex(targetIndex, page.bands.length);
1427
+ if (fromIndex === normalizedTargetIndex)
1428
+ return page;
1429
+ const bands = [...page.bands];
1430
+ const [band] = bands.splice(fromIndex, 1);
1431
+ bands.splice(normalizedTargetIndex, 0, band);
1432
+ return { ...page, bands };
1433
+ }),
1434
+ };
1435
+ }
1436
+ function moveComponentBetweenBandsInTemplate(template, payload) {
1437
+ return {
1438
+ ...template,
1439
+ pages: template.pages.map(page => {
1440
+ if (page.id !== payload.pageId)
1441
+ return page;
1442
+ const sourceBand = page.bands.find(band => band.id === payload.fromBandId);
1443
+ const targetBand = page.bands.find(band => band.id === payload.toBandId);
1444
+ const sourceComponent = sourceBand?.components.find(component => component.id === payload.componentId);
1445
+ const component = sourceComponent ?? payload.fallbackComponent;
1446
+ if (!targetBand || !component)
1447
+ return page;
1448
+ const movedComponent = { ...component, x: payload.x, y: payload.y };
1449
+ return {
1450
+ ...page,
1451
+ bands: page.bands.map(band => {
1452
+ const withoutMoved = band.components.filter(item => item.id !== payload.componentId);
1453
+ if (band.id !== payload.toBandId) {
1454
+ return band.components.length === withoutMoved.length ? band : { ...band, components: withoutMoved };
1455
+ }
1456
+ return { ...band, components: [...withoutMoved, movedComponent] };
1457
+ }),
1458
+ };
1459
+ }),
1460
+ };
1461
+ }
1462
+ function createBandForInsert(existingBands, type) {
1463
+ const takenNames = new Set(existingBands.map(band => band.name?.trim()).filter(Boolean));
1464
+ const band = {
1465
+ id: `band_${type}_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
1466
+ type,
1467
+ name: getNextBandName(type, takenNames),
1468
+ height: DEFAULT_BAND_HEIGHTS[type] ?? 20,
1469
+ components: [],
1470
+ behavior: createBandBehavior(type),
1471
+ };
1472
+ if (type === 'data' || type === 'hierarchicalData') {
1473
+ band.dataBand = {};
1474
+ }
1475
+ if (type === 'groupHeader' || type === 'groupFooter') {
1476
+ band.group = {};
1477
+ }
1478
+ return band;
1479
+ }
1480
+ function cloneBandForInsert(existingBands, source, template) {
1481
+ const nextBand = createBandForInsert(existingBands, source.type);
1482
+ const takenNames = collectComponentNames(template);
1483
+ return {
1484
+ ...source,
1485
+ id: nextBand.id,
1486
+ name: nextBand.name,
1487
+ components: source.components.map(component => cloneComponentForBandCopy(component, takenNames)),
1488
+ behavior: source.behavior ? { ...source.behavior } : nextBand.behavior,
1489
+ dataBand: source.dataBand ? { ...source.dataBand, sort: source.dataBand.sort ? [...source.dataBand.sort] : undefined } : nextBand.dataBand,
1490
+ group: source.group ? { ...source.group } : nextBand.group,
1491
+ events: source.events ? { ...source.events } : undefined,
1492
+ };
1493
+ }
1494
+ function cloneComponentForBandCopy(component, takenNames) {
1495
+ const cloned = JSON.parse(JSON.stringify(component));
1496
+ cloned.id = `component_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1497
+ cloned.name = getNextComponentName(cloned.type, takenNames);
1498
+ if ('components' in cloned && Array.isArray(cloned.components)) {
1499
+ cloned.components = cloned.components
1500
+ ?.map(child => cloneComponentForBandCopy(child, takenNames));
1501
+ }
1502
+ return cloned;
1503
+ }
1504
+ function sanitizeComponentUpdates(template, componentId, updates) {
1505
+ if (!Object.prototype.hasOwnProperty.call(updates, 'name')) {
1506
+ return updates;
1507
+ }
1508
+ const name = normalizeComponentName(updates.name);
1509
+ const { name: _ignoredName, ...restUpdates } = updates;
1510
+ if (!isComponentNameAvailable(template, name, componentId)) {
1511
+ return restUpdates;
1512
+ }
1513
+ return { ...updates, name };
1514
+ }
1515
+ function createBandBehavior(type) {
1516
+ return {
1517
+ enabled: true,
1518
+ printOn: 'allPages',
1519
+ printIfEmpty: true,
1520
+ printOnAllPages: isRepeatOnEveryPageBandType(type),
1521
+ keepTogether: false,
1522
+ canBreak: type === 'data' || type === 'hierarchicalData',
1523
+ breakIfLessThan: undefined,
1524
+ printAtBottom: type === 'pageFooter',
1525
+ autoGrow: true,
1526
+ autoShrink: false,
1527
+ };
1528
+ }
1529
+ function applyComponentUpdates(template, updates) {
1530
+ if (updates.length === 0)
1531
+ return template;
1532
+ const updatesByBand = new Map();
1533
+ for (const update of updates) {
1534
+ let bandUpdates = updatesByBand.get(update.pageId);
1535
+ if (!bandUpdates) {
1536
+ bandUpdates = new Map();
1537
+ updatesByBand.set(update.pageId, bandUpdates);
1538
+ }
1539
+ const componentUpdates = bandUpdates.get(update.bandId) ?? [];
1540
+ componentUpdates.push(update);
1541
+ bandUpdates.set(update.bandId, componentUpdates);
1542
+ }
1543
+ let nextPages = template.pages;
1544
+ for (const [pageId, bandUpdates] of updatesByBand) {
1545
+ const pageIndex = nextPages.findIndex(page => page.id === pageId);
1546
+ if (pageIndex < 0)
1547
+ continue;
1548
+ const page = nextPages[pageIndex];
1549
+ let nextBands = page.bands;
1550
+ for (const [bandId, componentUpdates] of bandUpdates) {
1551
+ const bandIndex = nextBands.findIndex(band => band.id === bandId);
1552
+ if (bandIndex < 0)
1553
+ continue;
1554
+ const band = nextBands[bandIndex];
1555
+ const updatesByComponent = new Map(componentUpdates.map(update => [update.componentId, update]));
1556
+ const nextComponents = applyComponentUpdatesInTree(band.components, updatesByComponent);
1557
+ if (nextComponents === band.components)
1558
+ continue;
1559
+ if (nextBands === page.bands)
1560
+ nextBands = page.bands.slice();
1561
+ nextBands[bandIndex] = { ...band, components: nextComponents };
1562
+ }
1563
+ if (nextBands === page.bands)
1564
+ continue;
1565
+ if (nextPages === template.pages)
1566
+ nextPages = template.pages.slice();
1567
+ nextPages[pageIndex] = { ...page, bands: nextBands };
1568
+ }
1569
+ return nextPages === template.pages ? template : { ...template, pages: nextPages };
1570
+ }
1571
+ function applyComponentUpdatesInTree(components, updatesByComponent) {
1572
+ let componentsChanged = false;
1573
+ const nextComponents = components.map(component => {
1574
+ const update = updatesByComponent.get(component.id);
1575
+ let nextComponent = component;
1576
+ if (update) {
1577
+ const changedUpdates = getChangedComponentUpdates(component, update.updates);
1578
+ if (Object.keys(changedUpdates).length > 0) {
1579
+ nextComponent = { ...component, ...changedUpdates };
1580
+ componentsChanged = true;
1581
+ }
1582
+ }
1583
+ const children = getNestedComponents(nextComponent);
1584
+ if (!children)
1585
+ return nextComponent;
1586
+ const nextChildren = applyComponentUpdatesInTree(children, updatesByComponent);
1587
+ if (nextChildren !== children) {
1588
+ nextComponent = withNestedComponents(nextComponent, nextChildren);
1589
+ componentsChanged = true;
1590
+ }
1591
+ return nextComponent;
1592
+ });
1593
+ return componentsChanged ? nextComponents : components;
1594
+ }
1595
+ function getChangedComponentUpdates(component, updates) {
1596
+ const changed = {};
1597
+ for (const [field, value] of Object.entries(updates)) {
1598
+ if (!Object.is(component[field], value)) {
1599
+ changed[field] = value;
1600
+ }
1601
+ }
1602
+ return changed;
1603
+ }
1604
+ function cleanEventMap(events) {
1605
+ const next = {};
1606
+ for (const [key, event] of Object.entries(events)) {
1607
+ if (event.enabled || event.script.trim()) {
1608
+ next[key] = event;
1609
+ }
1610
+ }
1611
+ return Object.keys(next).length ? next : undefined;
1612
+ }
1613
+ function isTextComponent(component) {
1614
+ return component.type === 'text';
1615
+ }
1616
+ function createTextStyleId() {
1617
+ return `text-style_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1618
+ }
1619
+ const FALLBACK_TEXT_STYLE_FONT = {
1620
+ family: 'Arial',
1621
+ size: 10,
1622
+ bold: false,
1623
+ italic: false,
1624
+ underline: false,
1625
+ strikethrough: false,
1626
+ color: '#000000',
1627
+ };
1628
+ const FALLBACK_TEXT_STYLE_BORDER = {
1629
+ style: 'none',
1630
+ width: 0,
1631
+ color: '#000000',
1632
+ sides: { top: false, right: false, bottom: false, left: false },
1633
+ };
1634
+ function cloneTextStyle(style, overrides) {
1635
+ const baseFont = style.font ?? FALLBACK_TEXT_STYLE_FONT;
1636
+ const baseBorder = style.border ?? FALLBACK_TEXT_STYLE_BORDER;
1637
+ const overrideBorder = overrides?.border;
1638
+ return {
1639
+ ...style,
1640
+ ...overrides,
1641
+ font: { ...baseFont, ...overrides?.font },
1642
+ border: {
1643
+ ...baseBorder,
1644
+ ...overrideBorder,
1645
+ sides: {
1646
+ ...FALLBACK_TEXT_STYLE_BORDER.sides,
1647
+ ...baseBorder.sides,
1648
+ ...overrideBorder?.sides,
1649
+ },
1650
+ },
1651
+ padding: overrides && 'padding' in overrides
1652
+ ? (overrides.padding ? { ...overrides.padding } : undefined)
1653
+ : (style.padding ? { ...style.padding } : undefined),
1654
+ format: overrides && 'format' in overrides
1655
+ ? (overrides.format ? { ...(style.format ?? { type: 'none' }), ...overrides.format } : undefined)
1656
+ : (style.format ? { ...style.format } : undefined),
1657
+ };
1658
+ }
1659
+ function createTextStyleDraft(template, style) {
1660
+ const baseStyle = getDefaultTextStyle(template.styles)
1661
+ ?? template.styles[0]
1662
+ ?? {
1663
+ id: 'text-normal',
1664
+ name: 'Normal',
1665
+ category: 'text',
1666
+ font: { family: 'Arial', size: 10, bold: false, italic: false, underline: false, strikethrough: false, color: '#000000' },
1667
+ border: { style: 'none', width: 0, color: '#000000', sides: { top: false, right: false, bottom: false, left: false } },
1668
+ backgroundColor: 'transparent',
1669
+ textAlign: 'left',
1670
+ verticalAlign: 'top',
1671
+ padding: { top: 0, right: 0, bottom: 0, left: 0 },
1672
+ canGrow: false,
1673
+ canShrink: false,
1674
+ isDefault: false,
1675
+ };
1676
+ return cloneTextStyle(baseStyle, {
1677
+ ...style,
1678
+ id: createTextStyleId(),
1679
+ name: style?.name ?? 'Text Style',
1680
+ category: 'text',
1681
+ isDefault: false,
1682
+ });
1683
+ }
1684
+ function mergeTextStyle(style, updates) {
1685
+ return cloneTextStyle(style, updates);
1686
+ }
1687
+ function mapAllStyleComponents(template, mapper) {
1688
+ return {
1689
+ ...template,
1690
+ pages: template.pages.map(page => ({
1691
+ ...page,
1692
+ bands: page.bands.map(band => ({
1693
+ ...band,
1694
+ components: band.components.map(component => (canUseTextStyle(component) ? mapper(component) : component)),
1695
+ })),
1696
+ })),
1697
+ };
1698
+ }
1699
+ function syncTextStyleReferencesInTemplate(template, styleId) {
1700
+ const stylesById = new Map(template.styles.map(style => [style.id, style]));
1701
+ return mapAllStyleComponents(template, component => {
1702
+ if (!component.style)
1703
+ return component;
1704
+ if (styleId && component.style !== styleId)
1705
+ return component;
1706
+ const style = stylesById.get(component.style);
1707
+ if (!style) {
1708
+ return clearTextStyleReference(component);
1709
+ }
1710
+ return syncTextComponentStyle(component, style);
1711
+ });
1712
+ }
1713
+ function clearTextStyleReferencesInTemplate(template, styleId) {
1714
+ return mapAllStyleComponents(template, component => (component.style === styleId ? clearTextStyleReference(component) : component));
1715
+ }
1716
+ function countTextStyleUsage(template, styleId) {
1717
+ let count = 0;
1718
+ for (const page of template.pages) {
1719
+ for (const band of page.bands) {
1720
+ for (const component of band.components) {
1721
+ if (canUseTextStyle(component) && component.style === styleId) {
1722
+ count += 1;
1723
+ }
1724
+ }
1725
+ }
1726
+ }
1727
+ return count;
1728
+ }
1729
+ function createConditionalFormatId() {
1730
+ return `cf_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1731
+ }
1732
+ function createConditionRuleId() {
1733
+ return `rule_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
1734
+ }
1735
+ function cloneConditionRule(rule) {
1736
+ return {
1737
+ ...rule,
1738
+ overrides: { ...(rule.overrides ?? {}) },
1739
+ };
1740
+ }
1741
+ function cloneConditionalFormat(format, overrides) {
1742
+ return {
1743
+ ...format,
1744
+ ...overrides,
1745
+ applyTo: overrides?.applyTo ? [...overrides.applyTo] : [...(format.applyTo ?? [])],
1746
+ rules: (overrides?.rules ?? format.rules ?? []).map(cloneConditionRule),
1747
+ };
1748
+ }
1749
+ function createConditionalFormatDraft(template, format) {
1750
+ const nextIndex = template.conditionalFormats.length + 1;
1751
+ return cloneConditionalFormat({
1752
+ id: createConditionalFormatId(),
1753
+ name: format?.name ?? `Conditional Format ${nextIndex}`,
1754
+ applyTo: [],
1755
+ rules: [{
1756
+ id: createConditionRuleId(),
1757
+ expression: 'true',
1758
+ conditionType: 'expression',
1759
+ enabled: true,
1760
+ breakIfTrue: false,
1761
+ overrides: {},
1762
+ }],
1763
+ }, format);
1764
+ }
1765
+ function mergeConditionalFormat(format, updates) {
1766
+ return cloneConditionalFormat(format, updates);
1767
+ }
1768
+ function clearConditionalFormatReferencesInTemplate(template, formatId) {
1769
+ return {
1770
+ ...template,
1771
+ pages: template.pages.map(page => ({
1772
+ ...page,
1773
+ bands: page.bands.map(band => ({
1774
+ ...band,
1775
+ components: band.components.map(component => {
1776
+ if (component.conditionalFormat !== formatId)
1777
+ return component;
1778
+ const nextComponent = { ...component };
1779
+ delete nextComponent.conditionalFormat;
1780
+ return nextComponent;
1781
+ }),
1782
+ })),
1783
+ })),
1784
+ };
1785
+ }
1786
+ function mapSelectedComponents(template, pageId, selectedComponentIds, mapper) {
1787
+ if (selectedComponentIds.length === 0)
1788
+ return template;
1789
+ const selected = new Set(selectedComponentIds);
1790
+ return {
1791
+ ...template,
1792
+ pages: template.pages.map(page => {
1793
+ if (page.id !== pageId)
1794
+ return page;
1795
+ return {
1796
+ ...page,
1797
+ bands: page.bands.map(band => ({
1798
+ ...band,
1799
+ components: band.components.map(component => (selected.has(component.id) ? mapper(component) : component)),
1800
+ })),
1801
+ };
1802
+ }),
1803
+ };
1804
+ }
1805
+ function normalizeTableCellSelection(selection) {
1806
+ const startRow = Math.min(selection.startRow, selection.endRow);
1807
+ const endRow = Math.max(selection.startRow, selection.endRow);
1808
+ const startColumn = Math.min(selection.startColumn, selection.endColumn);
1809
+ const endColumn = Math.max(selection.startColumn, selection.endColumn);
1810
+ return { ...selection, startRow, startColumn, endRow, endColumn };
1811
+ }
1812
+ function updateTableCell(table, selection, updates) {
1813
+ const normalized = normalizeTable(table);
1814
+ const normalizedSelection = normalizeTableCellSelection(selection);
1815
+ const rows = (normalized.rows ?? []).map((row, rowIndex) => ({
1816
+ ...row,
1817
+ cells: row.cells.map((cell, columnIndex) => {
1818
+ const selected = rowIndex >= normalizedSelection.startRow
1819
+ && rowIndex <= normalizedSelection.endRow
1820
+ && columnIndex >= normalizedSelection.startColumn
1821
+ && columnIndex <= normalizedSelection.endColumn;
1822
+ if (!selected)
1823
+ return cell;
1824
+ return clampTableCellSpan({ ...cell, ...updates }, cell, updates, normalized, rowIndex, columnIndex);
1825
+ }),
1826
+ }));
1827
+ return normalizeTable({ ...normalized, rows });
1828
+ }
1829
+ function tableHistorySnapshot(table) {
1830
+ return {
1831
+ ...table,
1832
+ rows: table.rows,
1833
+ cells: table.cells,
1834
+ columns: table.columns,
1835
+ binding: table.binding,
1836
+ dataSource: table.dataSource,
1837
+ rowHeight: table.rowHeight,
1838
+ };
1839
+ }
1840
+ function clampTableCellSpan(cell, existing, updates, table, rowIndex, columnIndex) {
1841
+ const rowCount = table.rows?.length ?? table.rowCount ?? 1;
1842
+ const columnCount = table.rows?.[rowIndex]?.cells.length ?? table.columnCount ?? 1;
1843
+ const shouldWriteRowSpan = existing.rowSpan !== undefined || updates.rowSpan !== undefined;
1844
+ const shouldWriteColSpan = existing.colSpan !== undefined || updates.colSpan !== undefined;
1845
+ return {
1846
+ ...cell,
1847
+ ...(shouldWriteRowSpan ? { rowSpan: Math.max(1, Math.min(cell.rowSpan ?? 1, rowCount - rowIndex)) } : {}),
1848
+ ...(shouldWriteColSpan ? { colSpan: Math.max(1, Math.min(cell.colSpan ?? 1, columnCount - columnIndex)) } : {}),
1849
+ };
1850
+ }
1851
+ //# sourceMappingURL=designer-store.js.map