@sametalmdr/word-editor 0.2.0-alpha → 0.3.0-alpha

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 (82) hide show
  1. package/CHANGELOG.md +108 -70
  2. package/LICENSE +21 -21
  3. package/README.md +286 -247
  4. package/dist/canvas/box-clipboard.js +238 -238
  5. package/dist/canvas/box-selection.js +80 -80
  6. package/dist/canvas/canvas-editor.js +502 -502
  7. package/dist/canvas/controller.js +717 -717
  8. package/dist/canvas/geometry.js +131 -131
  9. package/dist/canvas/gesture.d.ts +0 -1
  10. package/dist/canvas/gesture.js +289 -289
  11. package/dist/canvas/handles.js +212 -212
  12. package/dist/canvas/menu.js +82 -82
  13. package/dist/canvas/mode-controller.js +81 -81
  14. package/dist/canvas/nudge.js +108 -108
  15. package/dist/canvas/overlay.js +149 -149
  16. package/dist/canvas/snap.js +132 -132
  17. package/dist/canvas/viewport.js +112 -112
  18. package/dist/canvas/z-order.js +171 -171
  19. package/dist/commands/alignment.js +150 -150
  20. package/dist/commands/box.js +272 -272
  21. package/dist/commands/clipboard.js +93 -93
  22. package/dist/commands/graphemes.js +112 -112
  23. package/dist/commands/marks.js +153 -153
  24. package/dist/commands/paragraph-props.js +176 -176
  25. package/dist/commands/size.js +368 -368
  26. package/dist/commands/text.js +291 -291
  27. package/dist/docx.d.ts +1 -0
  28. package/dist/docx.js +15 -13
  29. package/dist/editor/editing-host.js +434 -434
  30. package/dist/element.js +41 -41
  31. package/dist/fonts/OFL-Carlito.txt +94 -0
  32. package/dist/fonts/calibri-metrics.d.ts +27 -0
  33. package/dist/fonts/calibri-metrics.js +88 -0
  34. package/dist/history/history.js +433 -433
  35. package/dist/index.js +63 -63
  36. package/dist/input/beforeinput.js +224 -224
  37. package/dist/input/clipboard.js +107 -107
  38. package/dist/input/composition.js +341 -341
  39. package/dist/input/keymap.js +54 -54
  40. package/dist/io/ooxml/custgeom.js +72 -72
  41. package/dist/io/ooxml/drawingml.js +232 -232
  42. package/dist/io/ooxml/edits.js +67 -67
  43. package/dist/io/ooxml/export-splice.js +880 -880
  44. package/dist/io/ooxml/export.js +365 -365
  45. package/dist/io/ooxml/fill.d.ts +93 -8
  46. package/dist/io/ooxml/fill.js +618 -562
  47. package/dist/io/ooxml/fit.d.ts +252 -0
  48. package/dist/io/ooxml/fit.js +598 -0
  49. package/dist/io/ooxml/import-gate.js +200 -200
  50. package/dist/io/ooxml/import.js +691 -691
  51. package/dist/io/ooxml/inflate.js +126 -126
  52. package/dist/io/ooxml/media.js +87 -87
  53. package/dist/io/ooxml/ns.d.ts +2 -0
  54. package/dist/io/ooxml/ns.js +26 -21
  55. package/dist/io/ooxml/numbering.js +290 -290
  56. package/dist/io/ooxml/opc-write.js +171 -171
  57. package/dist/io/ooxml/provenance.js +93 -93
  58. package/dist/io/ooxml/recipe.js +708 -708
  59. package/dist/io/ooxml/relationships.js +59 -59
  60. package/dist/io/ooxml/run-fonts.d.ts +89 -0
  61. package/dist/io/ooxml/run-fonts.js +378 -0
  62. package/dist/io/ooxml/slots.d.ts +4 -0
  63. package/dist/io/ooxml/slots.js +582 -581
  64. package/dist/io/ooxml/styles-resolve.js +257 -257
  65. package/dist/io/ooxml/units.js +43 -43
  66. package/dist/io/ooxml/value.js +120 -120
  67. package/dist/io/ooxml/xml-scan.js +616 -616
  68. package/dist/io/ooxml/zip-container.js +228 -228
  69. package/dist/io/ooxml/zip-reject.js +106 -106
  70. package/dist/measure/placement.js +83 -83
  71. package/dist/measure/report.js +75 -75
  72. package/dist/model/ids.js +70 -70
  73. package/dist/model/normalize.js +146 -146
  74. package/dist/model/position.js +283 -283
  75. package/dist/model/transaction.js +209 -209
  76. package/dist/platform/capabilities.js +25 -25
  77. package/dist/react/index.js +21 -21
  78. package/dist/selection/controller.js +315 -315
  79. package/dist/selection/dom-map.js +334 -334
  80. package/dist/state/editor-state.js +109 -109
  81. package/dist/styles.css +66 -66
  82. package/package.json +94 -93
@@ -1,272 +1,272 @@
1
- import { containerIndexById, layoutOf } from "../model/node.js";
2
- import { insertContainerStep, removeContainerStep, setLayoutStep } from "../model/step.js";
3
- import { createTransaction } from "../model/transaction.js";
4
-
5
- /**
6
- * The five user-visible box operations, as commands over the three container
7
- * steps (ADR-0007) — move/resize/setZ all become `setLayoutStep`, since intent
8
- * lives in `Transaction.origin`, not in a wider step vocabulary.
9
- *
10
- * None of these touch text selection: a box operation and a caret position are
11
- * orthogonal in this model, so every command here carries `state.selection`
12
- * through unchanged rather than requiring a `BoxSelection` type to exist yet.
13
- */
14
-
15
- /**
16
- * Adds a new container to the document.
17
- * @param {import("../state/editor-state.js").EditorState} state
18
- * @param {import("../model/node.js").Container} container
19
- * @param {import("../model/node.js").BoxLayout | null} [layout]
20
- * @param {number} [index] defaults to the end of the document
21
- * @returns {import("../model/transaction.js").Transaction | null}
22
- */
23
- export function createBox(state, container, layout = null, index) {
24
- const { doc, selection } = state;
25
- const at = index ?? doc.content.length;
26
-
27
- const tr = createTransaction(doc, selection, "format");
28
- tr.step(insertContainerStep(at, container, layout));
29
- if (!tr.changesDoc) {
30
- return null;
31
- }
32
- return tr.build();
33
- }
34
-
35
- /**
36
- * Removes a container and its geometry together.
37
- * @param {import("../state/editor-state.js").EditorState} state
38
- * @param {import("../model/node.js").NodeId} containerId
39
- * @returns {import("../model/transaction.js").Transaction | null}
40
- */
41
- export function deleteBox(state, containerId) {
42
- const { doc, selection } = state;
43
- if (containerIndexById(doc, containerId) === -1) {
44
- return null;
45
- }
46
- // `normalizeDoc` asserts a document holds at least one container and states
47
- // explicitly that keeping it that way is the command layer's job — this is
48
- // that job. Without the guard the assertion fires in dev and, worse, in
49
- // production (where `devAssert` is a no-op) a zero-container document would
50
- // reach the renderer.
51
- if (doc.content.length === 1) {
52
- return null;
53
- }
54
-
55
- const tr = createTransaction(doc, selection, "format");
56
- tr.step(removeContainerStep(containerId));
57
- if (!tr.changesDoc) {
58
- return null;
59
- }
60
- return tr.build();
61
- }
62
-
63
- /**
64
- * Whether any key in `patch` differs from the container's current layout.
65
- *
66
- * Needed because `patchLayout` (model/node.js) always builds a fresh layout
67
- * object whether or not values changed, so `TransactionBuilder.changesDoc`
68
- * cannot tell a real move from a stationary one (mouse down, mouse up, no
69
- * movement) — that would write a spurious history entry.
70
- *
71
- * @param {import("../model/node.js").BoxLayout} current
72
- * @param {Partial<import("../model/node.js").BoxLayout>} patch
73
- * @returns {boolean}
74
- */
75
- function changesAnything(current, patch) {
76
- return /** @type {import("../model/node.js").LayoutKey[]} */ (Object.keys(patch)).some(
77
- (key) => current[key] !== patch[key],
78
- );
79
- }
80
-
81
- /**
82
- * @param {import("../state/editor-state.js").EditorState} state
83
- * @param {import("../model/node.js").NodeId} containerId
84
- * @param {Partial<import("../model/node.js").BoxLayout>} patch
85
- * @param {import("../model/transaction.js").TransactionOrigin} origin
86
- * @returns {import("../model/transaction.js").Transaction | null}
87
- */
88
- function setLayout(state, containerId, patch, origin) {
89
- const { doc, selection } = state;
90
- const current = layoutOf(doc, containerId);
91
- if (current === null) {
92
- return null; // nothing to patch — this container is content, not a box
93
- }
94
-
95
- if (!changesAnything(current, patch)) {
96
- return null;
97
- }
98
-
99
- // The patch keeps the operation's FULL key set — `{x,y}` for a move even when
100
- // only x moved this frame. Narrowing it to just the differing keys looks
101
- // tidier and is a real bug: `HistoryEngine` collapses a drag only while the
102
- // key set stays identical, so one perfectly horizontal pointer move (y
103
- // unchanged) would emit `{x}`, break the collapse, and split the drag into two
104
- // undo entries mid-gesture. Dragging in a straight line is not an edge case.
105
- //
106
- // Emitting an unchanged key is harmless in both directions: applying it is
107
- // idempotent, and `invertSetLayout` restores that key from `docBefore`, where
108
- // it already held this value.
109
- const tr = createTransaction(doc, selection, origin);
110
- tr.step(setLayoutStep(containerId, patch));
111
- return tr.build();
112
- }
113
-
114
- /**
115
- * Moves a box. `origin: "pointer"` — a drag calls this once per pointer move,
116
- * and `HistoryEngine` collapses a run of same-container, same-key-set
117
- * `SetLayoutStep`s from this origin into a single undo entry (ADR-0007).
118
- * @param {import("../state/editor-state.js").EditorState} state
119
- * @param {import("../model/node.js").NodeId} containerId
120
- * @param {number} x
121
- * @param {number} y
122
- * @returns {import("../model/transaction.js").Transaction | null}
123
- */
124
- export function moveBox(state, containerId, x, y) {
125
- return setLayout(state, containerId, { x, y }, "pointer");
126
- }
127
-
128
- /**
129
- * Resizes a box. Same collapsing behaviour as `moveBox`, keyed on `{cx, cy}`
130
- * instead of `{x, y}` — a resize drag never collapses into a move drag or
131
- * vice versa, because the key sets differ.
132
- * @param {import("../state/editor-state.js").EditorState} state
133
- * @param {import("../model/node.js").NodeId} containerId
134
- * @param {number} cx
135
- * @param {number} cy
136
- * @returns {import("../model/transaction.js").Transaction | null}
137
- */
138
- export function resizeBox(state, containerId, cx, cy) {
139
- return setLayout(state, containerId, { cx, cy }, "pointer");
140
- }
141
-
142
- /**
143
- * Moves a box to another page. Shares `moveBox`'s coalescing key set only
144
- * incidentally if a drag ever patches `page` alongside `x`/`y` in the same
145
- * step — ordinary page reassignment is a discrete action, so `origin:
146
- * "format"` (never coalesced), matching `setBoxZ` below.
147
- * @param {import("../state/editor-state.js").EditorState} state
148
- * @param {import("../model/node.js").NodeId} containerId
149
- * @param {number} page
150
- * @returns {import("../model/transaction.js").Transaction | null}
151
- */
152
- export function setBoxPage(state, containerId, page) {
153
- return setLayout(state, containerId, { page }, "format");
154
- }
155
-
156
- /**
157
- * Reorders a box's stacking. A discrete action (a menu command or keyboard
158
- * shortcut, never a drag), so `origin: "format"` — never coalesced.
159
- * @param {import("../state/editor-state.js").EditorState} state
160
- * @param {import("../model/node.js").NodeId} containerId
161
- * @param {number} z
162
- * @returns {import("../model/transaction.js").Transaction | null}
163
- */
164
- export function setBoxZ(state, containerId, z) {
165
- return setLayout(state, containerId, { z }, "format");
166
- }
167
-
168
- /**
169
- * @typedef {object} BoxTransformEntry
170
- * @property {import("../model/node.js").NodeId} containerId
171
- * @property {Partial<import("../model/node.js").BoxLayout>} patch
172
- */
173
-
174
- /**
175
- * The shared engine behind `transformBoxes`/`setBoxZOrder`: every entry in
176
- * one call becomes its OWN `setLayoutStep`, all in ONE transaction —
177
- * deliberately a NEW function rather than a change to `setLayout` above,
178
- * which `moveBox`/`resizeBox` must stay byte-identical to (HIST1/HIST5/
179
- * HIST6's `find` strings, and their monotonic `expectedMinBreaks` 9/8/7,
180
- * are measured against those two functions' current bodies).
181
- *
182
- * Unlike `setLayout` (one box, one step, skipped entirely when its patch
183
- * is a no-op), an entry here is never dropped individually just because
184
- * ITS OWN patch didn't change anything this frame — ADR-0018 Karar 2's
185
- * "transformBoxes bir karedeki HER girdiyi yazar" rule. A multi-box drag's
186
- * frame shape (step count, and each step's key set) has to stay identical
187
- * from frame to frame for `HistoryEngine.canCoalesce`'s `sameKeySet` check
188
- * to keep collapsing the gesture into one undo entry; dropping one entry
189
- * because it happened not to move this frame would shrink the step count
190
- * and split the drag mid-gesture — the exact bug `setLayout`'s own comment
191
- * already documents for a single box's unchanged key, generalized to N.
192
- *
193
- * The WHOLE call is a no-op (returns null) when nothing in the batch
194
- * changed, or when any entry names something that is not a box — writing
195
- * N-1 of N valid entries would silently narrow the gesture's own roster.
196
- *
197
- * @param {import("../state/editor-state.js").EditorState} state
198
- * @param {BoxTransformEntry[]} entries
199
- * @param {import("../model/transaction.js").TransactionOrigin} origin
200
- * @returns {import("../model/transaction.js").Transaction | null}
201
- */
202
- function transformLayouts(state, entries, origin) {
203
- const { doc, selection } = state;
204
-
205
- const current = entries.map((entry) => layoutOf(doc, entry.containerId));
206
- if (current.some((layout) => layout === null)) {
207
- return null;
208
- }
209
-
210
- const anyChanged = entries.some((entry, i) =>
211
- changesAnything(/** @type {import("../model/node.js").BoxLayout} */ (current[i]), entry.patch),
212
- );
213
- if (!anyChanged) {
214
- return null;
215
- }
216
-
217
- const tr = createTransaction(doc, selection, origin);
218
- for (const entry of entries) {
219
- tr.step(setLayoutStep(entry.containerId, entry.patch));
220
- }
221
- return tr.build();
222
- }
223
-
224
- /**
225
- * Moves/resizes N boxes in ONE transaction — the command a corner-handle
226
- * drag needs and `moveBox`/`resizeBox` cannot give: those write exactly
227
- * `{x,y}`/`{cx,cy}`, never a corner's full `{x,y,cx,cy}` in one step
228
- * (ADR-0018 Karar 2). `origin: "pointer"` — coalesces the same way a
229
- * single-box drag does, keyed on each step's own patch (see
230
- * `src/canvas/handles.js`'s `resizeKeys` for the fixed key set a given
231
- * handle's drag must keep writing for the whole gesture).
232
- * @param {import("../state/editor-state.js").EditorState} state
233
- * @param {BoxTransformEntry[]} entries
234
- * @returns {import("../model/transaction.js").Transaction | null}
235
- */
236
- export function transformBoxes(state, entries) {
237
- return transformLayouts(state, entries, "pointer");
238
- }
239
-
240
- /**
241
- * `transformBoxes` for exactly one box — the corner-handle case itself.
242
- * @param {import("../state/editor-state.js").EditorState} state
243
- * @param {import("../model/node.js").NodeId} containerId
244
- * @param {Partial<import("../model/node.js").BoxLayout>} patch
245
- * @returns {import("../model/transaction.js").Transaction | null}
246
- */
247
- export function transformBox(state, containerId, patch) {
248
- return transformBoxes(state, [{ containerId, patch }]);
249
- }
250
-
251
- /**
252
- * @typedef {object} BoxZOrderEntry
253
- * @property {import("../model/node.js").NodeId} containerId
254
- * @property {number} z
255
- */
256
-
257
- /**
258
- * Reorders N boxes' stacking in ONE transaction (a "bring to front" over a
259
- * multi-selection touches more than one box's z at once). `origin:
260
- * "format"` — a menu/keyboard action, never coalesced, matching the
261
- * existing single-box `setBoxZ`.
262
- * @param {import("../state/editor-state.js").EditorState} state
263
- * @param {BoxZOrderEntry[]} entries
264
- * @returns {import("../model/transaction.js").Transaction | null}
265
- */
266
- export function setBoxZOrder(state, entries) {
267
- return transformLayouts(
268
- state,
269
- entries.map((entry) => ({ containerId: entry.containerId, patch: { z: entry.z } })),
270
- "format",
271
- );
272
- }
1
+ import { containerIndexById, layoutOf } from "../model/node.js";
2
+ import { insertContainerStep, removeContainerStep, setLayoutStep } from "../model/step.js";
3
+ import { createTransaction } from "../model/transaction.js";
4
+
5
+ /**
6
+ * The five user-visible box operations, as commands over the three container
7
+ * steps (ADR-0007) — move/resize/setZ all become `setLayoutStep`, since intent
8
+ * lives in `Transaction.origin`, not in a wider step vocabulary.
9
+ *
10
+ * None of these touch text selection: a box operation and a caret position are
11
+ * orthogonal in this model, so every command here carries `state.selection`
12
+ * through unchanged rather than requiring a `BoxSelection` type to exist yet.
13
+ */
14
+
15
+ /**
16
+ * Adds a new container to the document.
17
+ * @param {import("../state/editor-state.js").EditorState} state
18
+ * @param {import("../model/node.js").Container} container
19
+ * @param {import("../model/node.js").BoxLayout | null} [layout]
20
+ * @param {number} [index] defaults to the end of the document
21
+ * @returns {import("../model/transaction.js").Transaction | null}
22
+ */
23
+ export function createBox(state, container, layout = null, index) {
24
+ const { doc, selection } = state;
25
+ const at = index ?? doc.content.length;
26
+
27
+ const tr = createTransaction(doc, selection, "format");
28
+ tr.step(insertContainerStep(at, container, layout));
29
+ if (!tr.changesDoc) {
30
+ return null;
31
+ }
32
+ return tr.build();
33
+ }
34
+
35
+ /**
36
+ * Removes a container and its geometry together.
37
+ * @param {import("../state/editor-state.js").EditorState} state
38
+ * @param {import("../model/node.js").NodeId} containerId
39
+ * @returns {import("../model/transaction.js").Transaction | null}
40
+ */
41
+ export function deleteBox(state, containerId) {
42
+ const { doc, selection } = state;
43
+ if (containerIndexById(doc, containerId) === -1) {
44
+ return null;
45
+ }
46
+ // `normalizeDoc` asserts a document holds at least one container and states
47
+ // explicitly that keeping it that way is the command layer's job — this is
48
+ // that job. Without the guard the assertion fires in dev and, worse, in
49
+ // production (where `devAssert` is a no-op) a zero-container document would
50
+ // reach the renderer.
51
+ if (doc.content.length === 1) {
52
+ return null;
53
+ }
54
+
55
+ const tr = createTransaction(doc, selection, "format");
56
+ tr.step(removeContainerStep(containerId));
57
+ if (!tr.changesDoc) {
58
+ return null;
59
+ }
60
+ return tr.build();
61
+ }
62
+
63
+ /**
64
+ * Whether any key in `patch` differs from the container's current layout.
65
+ *
66
+ * Needed because `patchLayout` (model/node.js) always builds a fresh layout
67
+ * object whether or not values changed, so `TransactionBuilder.changesDoc`
68
+ * cannot tell a real move from a stationary one (mouse down, mouse up, no
69
+ * movement) — that would write a spurious history entry.
70
+ *
71
+ * @param {import("../model/node.js").BoxLayout} current
72
+ * @param {Partial<import("../model/node.js").BoxLayout>} patch
73
+ * @returns {boolean}
74
+ */
75
+ function changesAnything(current, patch) {
76
+ return /** @type {import("../model/node.js").LayoutKey[]} */ (Object.keys(patch)).some(
77
+ (key) => current[key] !== patch[key],
78
+ );
79
+ }
80
+
81
+ /**
82
+ * @param {import("../state/editor-state.js").EditorState} state
83
+ * @param {import("../model/node.js").NodeId} containerId
84
+ * @param {Partial<import("../model/node.js").BoxLayout>} patch
85
+ * @param {import("../model/transaction.js").TransactionOrigin} origin
86
+ * @returns {import("../model/transaction.js").Transaction | null}
87
+ */
88
+ function setLayout(state, containerId, patch, origin) {
89
+ const { doc, selection } = state;
90
+ const current = layoutOf(doc, containerId);
91
+ if (current === null) {
92
+ return null; // nothing to patch — this container is content, not a box
93
+ }
94
+
95
+ if (!changesAnything(current, patch)) {
96
+ return null;
97
+ }
98
+
99
+ // The patch keeps the operation's FULL key set — `{x,y}` for a move even when
100
+ // only x moved this frame. Narrowing it to just the differing keys looks
101
+ // tidier and is a real bug: `HistoryEngine` collapses a drag only while the
102
+ // key set stays identical, so one perfectly horizontal pointer move (y
103
+ // unchanged) would emit `{x}`, break the collapse, and split the drag into two
104
+ // undo entries mid-gesture. Dragging in a straight line is not an edge case.
105
+ //
106
+ // Emitting an unchanged key is harmless in both directions: applying it is
107
+ // idempotent, and `invertSetLayout` restores that key from `docBefore`, where
108
+ // it already held this value.
109
+ const tr = createTransaction(doc, selection, origin);
110
+ tr.step(setLayoutStep(containerId, patch));
111
+ return tr.build();
112
+ }
113
+
114
+ /**
115
+ * Moves a box. `origin: "pointer"` — a drag calls this once per pointer move,
116
+ * and `HistoryEngine` collapses a run of same-container, same-key-set
117
+ * `SetLayoutStep`s from this origin into a single undo entry (ADR-0007).
118
+ * @param {import("../state/editor-state.js").EditorState} state
119
+ * @param {import("../model/node.js").NodeId} containerId
120
+ * @param {number} x
121
+ * @param {number} y
122
+ * @returns {import("../model/transaction.js").Transaction | null}
123
+ */
124
+ export function moveBox(state, containerId, x, y) {
125
+ return setLayout(state, containerId, { x, y }, "pointer");
126
+ }
127
+
128
+ /**
129
+ * Resizes a box. Same collapsing behaviour as `moveBox`, keyed on `{cx, cy}`
130
+ * instead of `{x, y}` — a resize drag never collapses into a move drag or
131
+ * vice versa, because the key sets differ.
132
+ * @param {import("../state/editor-state.js").EditorState} state
133
+ * @param {import("../model/node.js").NodeId} containerId
134
+ * @param {number} cx
135
+ * @param {number} cy
136
+ * @returns {import("../model/transaction.js").Transaction | null}
137
+ */
138
+ export function resizeBox(state, containerId, cx, cy) {
139
+ return setLayout(state, containerId, { cx, cy }, "pointer");
140
+ }
141
+
142
+ /**
143
+ * Moves a box to another page. Shares `moveBox`'s coalescing key set only
144
+ * incidentally if a drag ever patches `page` alongside `x`/`y` in the same
145
+ * step — ordinary page reassignment is a discrete action, so `origin:
146
+ * "format"` (never coalesced), matching `setBoxZ` below.
147
+ * @param {import("../state/editor-state.js").EditorState} state
148
+ * @param {import("../model/node.js").NodeId} containerId
149
+ * @param {number} page
150
+ * @returns {import("../model/transaction.js").Transaction | null}
151
+ */
152
+ export function setBoxPage(state, containerId, page) {
153
+ return setLayout(state, containerId, { page }, "format");
154
+ }
155
+
156
+ /**
157
+ * Reorders a box's stacking. A discrete action (a menu command or keyboard
158
+ * shortcut, never a drag), so `origin: "format"` — never coalesced.
159
+ * @param {import("../state/editor-state.js").EditorState} state
160
+ * @param {import("../model/node.js").NodeId} containerId
161
+ * @param {number} z
162
+ * @returns {import("../model/transaction.js").Transaction | null}
163
+ */
164
+ export function setBoxZ(state, containerId, z) {
165
+ return setLayout(state, containerId, { z }, "format");
166
+ }
167
+
168
+ /**
169
+ * @typedef {object} BoxTransformEntry
170
+ * @property {import("../model/node.js").NodeId} containerId
171
+ * @property {Partial<import("../model/node.js").BoxLayout>} patch
172
+ */
173
+
174
+ /**
175
+ * The shared engine behind `transformBoxes`/`setBoxZOrder`: every entry in
176
+ * one call becomes its OWN `setLayoutStep`, all in ONE transaction —
177
+ * deliberately a NEW function rather than a change to `setLayout` above,
178
+ * which `moveBox`/`resizeBox` must stay byte-identical to (HIST1/HIST5/
179
+ * HIST6's `find` strings, and their monotonic `expectedMinBreaks` 9/8/7,
180
+ * are measured against those two functions' current bodies).
181
+ *
182
+ * Unlike `setLayout` (one box, one step, skipped entirely when its patch
183
+ * is a no-op), an entry here is never dropped individually just because
184
+ * ITS OWN patch didn't change anything this frame — ADR-0018 Karar 2's
185
+ * "transformBoxes bir karedeki HER girdiyi yazar" rule. A multi-box drag's
186
+ * frame shape (step count, and each step's key set) has to stay identical
187
+ * from frame to frame for `HistoryEngine.canCoalesce`'s `sameKeySet` check
188
+ * to keep collapsing the gesture into one undo entry; dropping one entry
189
+ * because it happened not to move this frame would shrink the step count
190
+ * and split the drag mid-gesture — the exact bug `setLayout`'s own comment
191
+ * already documents for a single box's unchanged key, generalized to N.
192
+ *
193
+ * The WHOLE call is a no-op (returns null) when nothing in the batch
194
+ * changed, or when any entry names something that is not a box — writing
195
+ * N-1 of N valid entries would silently narrow the gesture's own roster.
196
+ *
197
+ * @param {import("../state/editor-state.js").EditorState} state
198
+ * @param {BoxTransformEntry[]} entries
199
+ * @param {import("../model/transaction.js").TransactionOrigin} origin
200
+ * @returns {import("../model/transaction.js").Transaction | null}
201
+ */
202
+ function transformLayouts(state, entries, origin) {
203
+ const { doc, selection } = state;
204
+
205
+ const current = entries.map((entry) => layoutOf(doc, entry.containerId));
206
+ if (current.some((layout) => layout === null)) {
207
+ return null;
208
+ }
209
+
210
+ const anyChanged = entries.some((entry, i) =>
211
+ changesAnything(/** @type {import("../model/node.js").BoxLayout} */ (current[i]), entry.patch),
212
+ );
213
+ if (!anyChanged) {
214
+ return null;
215
+ }
216
+
217
+ const tr = createTransaction(doc, selection, origin);
218
+ for (const entry of entries) {
219
+ tr.step(setLayoutStep(entry.containerId, entry.patch));
220
+ }
221
+ return tr.build();
222
+ }
223
+
224
+ /**
225
+ * Moves/resizes N boxes in ONE transaction — the command a corner-handle
226
+ * drag needs and `moveBox`/`resizeBox` cannot give: those write exactly
227
+ * `{x,y}`/`{cx,cy}`, never a corner's full `{x,y,cx,cy}` in one step
228
+ * (ADR-0018 Karar 2). `origin: "pointer"` — coalesces the same way a
229
+ * single-box drag does, keyed on each step's own patch (see
230
+ * `src/canvas/handles.js`'s `resizeKeys` for the fixed key set a given
231
+ * handle's drag must keep writing for the whole gesture).
232
+ * @param {import("../state/editor-state.js").EditorState} state
233
+ * @param {BoxTransformEntry[]} entries
234
+ * @returns {import("../model/transaction.js").Transaction | null}
235
+ */
236
+ export function transformBoxes(state, entries) {
237
+ return transformLayouts(state, entries, "pointer");
238
+ }
239
+
240
+ /**
241
+ * `transformBoxes` for exactly one box — the corner-handle case itself.
242
+ * @param {import("../state/editor-state.js").EditorState} state
243
+ * @param {import("../model/node.js").NodeId} containerId
244
+ * @param {Partial<import("../model/node.js").BoxLayout>} patch
245
+ * @returns {import("../model/transaction.js").Transaction | null}
246
+ */
247
+ export function transformBox(state, containerId, patch) {
248
+ return transformBoxes(state, [{ containerId, patch }]);
249
+ }
250
+
251
+ /**
252
+ * @typedef {object} BoxZOrderEntry
253
+ * @property {import("../model/node.js").NodeId} containerId
254
+ * @property {number} z
255
+ */
256
+
257
+ /**
258
+ * Reorders N boxes' stacking in ONE transaction (a "bring to front" over a
259
+ * multi-selection touches more than one box's z at once). `origin:
260
+ * "format"` — a menu/keyboard action, never coalesced, matching the
261
+ * existing single-box `setBoxZ`.
262
+ * @param {import("../state/editor-state.js").EditorState} state
263
+ * @param {BoxZOrderEntry[]} entries
264
+ * @returns {import("../model/transaction.js").Transaction | null}
265
+ */
266
+ export function setBoxZOrder(state, entries) {
267
+ return transformLayouts(
268
+ state,
269
+ entries.map((entry) => ({ containerId: entry.containerId, patch: { z: entry.z } })),
270
+ "format",
271
+ );
272
+ }