@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,212 +1,212 @@
1
- // Which resize handles a box offers, where they sit, and which layout keys
2
- // each one writes (ADR-0018 Karar 2 / Karar 4, §9/§10). No src/, DOM, or
3
- // history import — pure data in, data out, same discipline as geometry.js.
4
- //
5
- // Every size threshold here is an INJECTED number with NO default
6
- // (`requireMetrics` throws a real TypeError otherwise) — ADR-0018 §9.0:
7
- // "ölçülmemiş sabitlerin hiçbirinin varsayılanı yok". The four kademe
8
- // (degradation tier) boundaries are each their OWN metric rather than
9
- // derived from `handleSize` by some invented multiplier — the ADR's own
10
- // text only ever says "eşikleri enjekte" (thresholds are injected,
11
- // plural), never gives a formula relating them, so inventing one here
12
- // would be a design decision this file has no business making.
13
-
14
- import { rectContains } from "./geometry.js";
15
-
16
- /**
17
- * @typedef {object} HandleMetrics
18
- * @property {number} handleSize pt — a handle's own square footprint
19
- * @property {number} handleSlop pt — extra hit-test tolerance around a handle
20
- * @property {number} minBoxCx pt — at/below this, NO resize handle is shown at all (kademe 4)
21
- * @property {number} minBoxCy
22
- * @property {number} crampedCx pt — at/below this (but above minBoxCx), the x-axis's edge-middle handles (e/w) drop (kademe 2/3)
23
- * @property {number} crampedCy
24
- */
25
-
26
- /**
27
- * @typedef {object} HandleAffordances
28
- * @property {boolean} move body-drag (translate) is available
29
- * @property {string[]} resize handle names shown, a subset of ALL_HANDLES
30
- */
31
-
32
- /**
33
- * @typedef {object} Handle
34
- * @property {string} name one of ALL_HANDLES
35
- * @property {import("./geometry.js").Rect} rect pt, centered on the box's own edge/corner
36
- */
37
-
38
- export const ALL_HANDLES = /** @type {const} */ (["nw", "n", "ne", "e", "se", "s", "sw", "w"]);
39
- const CORNER_HANDLES = /** @type {const} */ (["nw", "ne", "se", "sw"]);
40
- /** @type {readonly string[]} */
41
- const X_MID_HANDLES = ["e", "w"];
42
- /** @type {readonly string[]} */
43
- const Y_MID_HANDLES = ["n", "s"];
44
-
45
- const METRICS_KEYS = /** @type {const} */ ([
46
- "handleSize",
47
- "handleSlop",
48
- "minBoxCx",
49
- "minBoxCy",
50
- "crampedCx",
51
- "crampedCy",
52
- ]);
53
-
54
- /**
55
- * @param {HandleMetrics} metrics
56
- * @returns {void}
57
- */
58
- export function requireMetrics(metrics) {
59
- for (const key of METRICS_KEYS) {
60
- if (typeof metrics?.[key] !== "number" || !Number.isFinite(metrics[key])) {
61
- throw new TypeError(
62
- `HandleMetrics.${key} is required and must be a finite number, got ${JSON.stringify(metrics?.[key])}`,
63
- );
64
- }
65
- }
66
- }
67
-
68
- /** @type {Record<(typeof ALL_HANDLES)[number], (layout: {x: number, y: number, cx: number, cy: number}) => {x: number, y: number}>} */
69
- const HANDLE_ANCHOR = {
70
- nw: (l) => ({ x: l.x, y: l.y }),
71
- n: (l) => ({ x: l.x + l.cx / 2, y: l.y }),
72
- ne: (l) => ({ x: l.x + l.cx, y: l.y }),
73
- e: (l) => ({ x: l.x + l.cx, y: l.y + l.cy / 2 }),
74
- se: (l) => ({ x: l.x + l.cx, y: l.y + l.cy }),
75
- s: (l) => ({ x: l.x + l.cx / 2, y: l.y + l.cy }),
76
- sw: (l) => ({ x: l.x, y: l.y + l.cy }),
77
- w: (l) => ({ x: l.x, y: l.y + l.cy / 2 }),
78
- };
79
-
80
- /** @type {Record<(typeof ALL_HANDLES)[number], string[]>} */
81
- const RESIZE_KEYS = {
82
- nw: ["x", "y", "cx", "cy"],
83
- ne: ["y", "cx", "cy"],
84
- se: ["cx", "cy"],
85
- sw: ["x", "cx", "cy"],
86
- n: ["y", "cy"],
87
- s: ["cy"],
88
- e: ["cx"],
89
- w: ["x", "cx"],
90
- };
91
- const BODY_KEYS = ["x", "y"];
92
-
93
- /**
94
- * The layout keys a handle's drag writes in ONE step. `sameKeySet`
95
- * (`src/history/history.js`) needs this identity to stay FIXED for the
96
- * whole gesture — see `src/commands/box.js:99-108`'s existing "full key
97
- * set, not just the changed ones" rule, of which this is the per-handle
98
- * generalization. NOT the uniform `{x,y,cx,cy}` for every handle: that
99
- * would let a NW drag's frame collapse with a completely unrelated SE
100
- * drag's frame, since `sameKeySet` alone could no longer tell them apart.
101
- *
102
- * @param {(typeof ALL_HANDLES)[number] | "body"} handle
103
- * @returns {string[]} a FRESH array — callers must not mutate the table
104
- */
105
- export function resizeKeys(handle) {
106
- if (handle === "body") {
107
- return [...BODY_KEYS];
108
- }
109
- const keys = RESIZE_KEYS[/** @type {(typeof ALL_HANDLES)[number]} */ (handle)];
110
- if (keys === undefined) {
111
- throw new TypeError(`resizeKeys: unknown handle ${JSON.stringify(handle)}`);
112
- }
113
- return [...keys];
114
- }
115
-
116
- /**
117
- * Which affordances a box offers right now — never both a resize handle
118
- * set AND `move: false` (a locked box loses BOTH, `BoxLayout.locked`'s own
119
- * doc comment: "a box-editing UI reads to withhold the move/resize
120
- * handle"). `rot !== 0` is deliberately NOT checked here: the renderer
121
- * never applies rotation (the box is drawn unrotated), so a handle placed
122
- * on the unrotated rect is consistent with what is actually on screen —
123
- * suppressing it would make 15 real corpus shapes uneditable for a reason
124
- * the user cannot see (ADR-0018 §4.4).
125
- *
126
- * @param {import("../model/node.js").BoxLayout} layout
127
- * @param {HandleMetrics} metrics
128
- * @returns {HandleAffordances}
129
- */
130
- export function affordancesFor(layout, metrics) {
131
- requireMetrics(metrics);
132
-
133
- if (layout.locked) {
134
- return { move: false, resize: [] };
135
- }
136
-
137
- // Kademe 4: below the absolute floor on either axis, no resize handle at
138
- // all — body move only, boyutlandırma bağlam menüsünden.
139
- if (layout.cx <= metrics.minBoxCx || layout.cy <= metrics.minBoxCy) {
140
- return { move: true, resize: [] };
141
- }
142
-
143
- const crampedX = layout.cx <= metrics.crampedCx;
144
- const crampedY = layout.cy <= metrics.crampedCy;
145
-
146
- // Kademe 3: both axes cramped — corners only.
147
- if (crampedX && crampedY) {
148
- return { move: true, resize: [...CORNER_HANDLES] };
149
- }
150
- // Kademe 2: one axis cramped — that axis's edge-middle handles drop.
151
- if (crampedX) {
152
- return { move: true, resize: ALL_HANDLES.filter((h) => !X_MID_HANDLES.includes(h)) };
153
- }
154
- if (crampedY) {
155
- return { move: true, resize: ALL_HANDLES.filter((h) => !Y_MID_HANDLES.includes(h)) };
156
- }
157
- // Kademe 1: both axes comfortable — all eight.
158
- return { move: true, resize: [...ALL_HANDLES] };
159
- }
160
-
161
- /**
162
- * The positioned rect for every resize handle `affordancesFor` currently
163
- * offers — nothing for "move" (its rect is `layout` itself, no computation
164
- * needed). Centering each handle ON the box's own edge/corner is what
165
- * makes a handle "dışa taşarak çizilir" (overflow outward) on a tiny box:
166
- * it is simply what centering does at any size, not a separate case.
167
- *
168
- * @param {import("../model/node.js").BoxLayout} layout
169
- * @param {HandleMetrics} metrics
170
- * @returns {Handle[]}
171
- */
172
- export function handleRects(layout, metrics) {
173
- requireMetrics(metrics);
174
- const { resize } = affordancesFor(layout, metrics);
175
- const half = metrics.handleSize / 2;
176
- return resize.map((name) => {
177
- const center = HANDLE_ANCHOR[/** @type {(typeof ALL_HANDLES)[number]} */ (name)](layout);
178
- return {
179
- name,
180
- rect: { x: center.x - half, y: center.y - half, cx: metrics.handleSize, cy: metrics.handleSize },
181
- };
182
- });
183
- }
184
-
185
- /**
186
- * Which resize handle (if any) sits under a page-space pt point, widened
187
- * by `handleSlop` on every side. Never reports "move" — a point that
188
- * misses every resize handle is the caller's cue to fall back to
189
- * `hit-test.js`'s box lookup (a click anywhere else in the box body is a
190
- * move, not this function's concern).
191
- *
192
- * @param {import("../model/node.js").BoxLayout} layout
193
- * @param {HandleMetrics} metrics
194
- * @param {number} x
195
- * @param {number} y
196
- * @returns {string | null}
197
- */
198
- export function handleAtPoint(layout, metrics, x, y) {
199
- requireMetrics(metrics);
200
- for (const handle of handleRects(layout, metrics)) {
201
- const expanded = {
202
- x: handle.rect.x - metrics.handleSlop,
203
- y: handle.rect.y - metrics.handleSlop,
204
- cx: handle.rect.cx + metrics.handleSlop * 2,
205
- cy: handle.rect.cy + metrics.handleSlop * 2,
206
- };
207
- if (rectContains(expanded, { x, y, cx: 0, cy: 0 })) {
208
- return handle.name;
209
- }
210
- }
211
- return null;
212
- }
1
+ // Which resize handles a box offers, where they sit, and which layout keys
2
+ // each one writes (ADR-0018 Karar 2 / Karar 4, §9/§10). No src/, DOM, or
3
+ // history import — pure data in, data out, same discipline as geometry.js.
4
+ //
5
+ // Every size threshold here is an INJECTED number with NO default
6
+ // (`requireMetrics` throws a real TypeError otherwise) — ADR-0018 §9.0:
7
+ // "ölçülmemiş sabitlerin hiçbirinin varsayılanı yok". The four kademe
8
+ // (degradation tier) boundaries are each their OWN metric rather than
9
+ // derived from `handleSize` by some invented multiplier — the ADR's own
10
+ // text only ever says "eşikleri enjekte" (thresholds are injected,
11
+ // plural), never gives a formula relating them, so inventing one here
12
+ // would be a design decision this file has no business making.
13
+
14
+ import { rectContains } from "./geometry.js";
15
+
16
+ /**
17
+ * @typedef {object} HandleMetrics
18
+ * @property {number} handleSize pt — a handle's own square footprint
19
+ * @property {number} handleSlop pt — extra hit-test tolerance around a handle
20
+ * @property {number} minBoxCx pt — at/below this, NO resize handle is shown at all (kademe 4)
21
+ * @property {number} minBoxCy
22
+ * @property {number} crampedCx pt — at/below this (but above minBoxCx), the x-axis's edge-middle handles (e/w) drop (kademe 2/3)
23
+ * @property {number} crampedCy
24
+ */
25
+
26
+ /**
27
+ * @typedef {object} HandleAffordances
28
+ * @property {boolean} move body-drag (translate) is available
29
+ * @property {string[]} resize handle names shown, a subset of ALL_HANDLES
30
+ */
31
+
32
+ /**
33
+ * @typedef {object} Handle
34
+ * @property {string} name one of ALL_HANDLES
35
+ * @property {import("./geometry.js").Rect} rect pt, centered on the box's own edge/corner
36
+ */
37
+
38
+ export const ALL_HANDLES = /** @type {const} */ (["nw", "n", "ne", "e", "se", "s", "sw", "w"]);
39
+ const CORNER_HANDLES = /** @type {const} */ (["nw", "ne", "se", "sw"]);
40
+ /** @type {readonly string[]} */
41
+ const X_MID_HANDLES = ["e", "w"];
42
+ /** @type {readonly string[]} */
43
+ const Y_MID_HANDLES = ["n", "s"];
44
+
45
+ const METRICS_KEYS = /** @type {const} */ ([
46
+ "handleSize",
47
+ "handleSlop",
48
+ "minBoxCx",
49
+ "minBoxCy",
50
+ "crampedCx",
51
+ "crampedCy",
52
+ ]);
53
+
54
+ /**
55
+ * @param {HandleMetrics} metrics
56
+ * @returns {void}
57
+ */
58
+ export function requireMetrics(metrics) {
59
+ for (const key of METRICS_KEYS) {
60
+ if (typeof metrics?.[key] !== "number" || !Number.isFinite(metrics[key])) {
61
+ throw new TypeError(
62
+ `HandleMetrics.${key} is required and must be a finite number, got ${JSON.stringify(metrics?.[key])}`,
63
+ );
64
+ }
65
+ }
66
+ }
67
+
68
+ /** @type {Record<(typeof ALL_HANDLES)[number], (layout: {x: number, y: number, cx: number, cy: number}) => {x: number, y: number}>} */
69
+ const HANDLE_ANCHOR = {
70
+ nw: (l) => ({ x: l.x, y: l.y }),
71
+ n: (l) => ({ x: l.x + l.cx / 2, y: l.y }),
72
+ ne: (l) => ({ x: l.x + l.cx, y: l.y }),
73
+ e: (l) => ({ x: l.x + l.cx, y: l.y + l.cy / 2 }),
74
+ se: (l) => ({ x: l.x + l.cx, y: l.y + l.cy }),
75
+ s: (l) => ({ x: l.x + l.cx / 2, y: l.y + l.cy }),
76
+ sw: (l) => ({ x: l.x, y: l.y + l.cy }),
77
+ w: (l) => ({ x: l.x, y: l.y + l.cy / 2 }),
78
+ };
79
+
80
+ /** @type {Record<(typeof ALL_HANDLES)[number], string[]>} */
81
+ const RESIZE_KEYS = {
82
+ nw: ["x", "y", "cx", "cy"],
83
+ ne: ["y", "cx", "cy"],
84
+ se: ["cx", "cy"],
85
+ sw: ["x", "cx", "cy"],
86
+ n: ["y", "cy"],
87
+ s: ["cy"],
88
+ e: ["cx"],
89
+ w: ["x", "cx"],
90
+ };
91
+ const BODY_KEYS = ["x", "y"];
92
+
93
+ /**
94
+ * The layout keys a handle's drag writes in ONE step. `sameKeySet`
95
+ * (`src/history/history.js`) needs this identity to stay FIXED for the
96
+ * whole gesture — see `src/commands/box.js:99-108`'s existing "full key
97
+ * set, not just the changed ones" rule, of which this is the per-handle
98
+ * generalization. NOT the uniform `{x,y,cx,cy}` for every handle: that
99
+ * would let a NW drag's frame collapse with a completely unrelated SE
100
+ * drag's frame, since `sameKeySet` alone could no longer tell them apart.
101
+ *
102
+ * @param {(typeof ALL_HANDLES)[number] | "body"} handle
103
+ * @returns {string[]} a FRESH array — callers must not mutate the table
104
+ */
105
+ export function resizeKeys(handle) {
106
+ if (handle === "body") {
107
+ return [...BODY_KEYS];
108
+ }
109
+ const keys = RESIZE_KEYS[/** @type {(typeof ALL_HANDLES)[number]} */ (handle)];
110
+ if (keys === undefined) {
111
+ throw new TypeError(`resizeKeys: unknown handle ${JSON.stringify(handle)}`);
112
+ }
113
+ return [...keys];
114
+ }
115
+
116
+ /**
117
+ * Which affordances a box offers right now — never both a resize handle
118
+ * set AND `move: false` (a locked box loses BOTH, `BoxLayout.locked`'s own
119
+ * doc comment: "a box-editing UI reads to withhold the move/resize
120
+ * handle"). `rot !== 0` is deliberately NOT checked here: the renderer
121
+ * never applies rotation (the box is drawn unrotated), so a handle placed
122
+ * on the unrotated rect is consistent with what is actually on screen —
123
+ * suppressing it would make 15 real corpus shapes uneditable for a reason
124
+ * the user cannot see (ADR-0018 §4.4).
125
+ *
126
+ * @param {import("../model/node.js").BoxLayout} layout
127
+ * @param {HandleMetrics} metrics
128
+ * @returns {HandleAffordances}
129
+ */
130
+ export function affordancesFor(layout, metrics) {
131
+ requireMetrics(metrics);
132
+
133
+ if (layout.locked) {
134
+ return { move: false, resize: [] };
135
+ }
136
+
137
+ // Kademe 4: below the absolute floor on either axis, no resize handle at
138
+ // all — body move only, boyutlandırma bağlam menüsünden.
139
+ if (layout.cx <= metrics.minBoxCx || layout.cy <= metrics.minBoxCy) {
140
+ return { move: true, resize: [] };
141
+ }
142
+
143
+ const crampedX = layout.cx <= metrics.crampedCx;
144
+ const crampedY = layout.cy <= metrics.crampedCy;
145
+
146
+ // Kademe 3: both axes cramped — corners only.
147
+ if (crampedX && crampedY) {
148
+ return { move: true, resize: [...CORNER_HANDLES] };
149
+ }
150
+ // Kademe 2: one axis cramped — that axis's edge-middle handles drop.
151
+ if (crampedX) {
152
+ return { move: true, resize: ALL_HANDLES.filter((h) => !X_MID_HANDLES.includes(h)) };
153
+ }
154
+ if (crampedY) {
155
+ return { move: true, resize: ALL_HANDLES.filter((h) => !Y_MID_HANDLES.includes(h)) };
156
+ }
157
+ // Kademe 1: both axes comfortable — all eight.
158
+ return { move: true, resize: [...ALL_HANDLES] };
159
+ }
160
+
161
+ /**
162
+ * The positioned rect for every resize handle `affordancesFor` currently
163
+ * offers — nothing for "move" (its rect is `layout` itself, no computation
164
+ * needed). Centering each handle ON the box's own edge/corner is what
165
+ * makes a handle "dışa taşarak çizilir" (overflow outward) on a tiny box:
166
+ * it is simply what centering does at any size, not a separate case.
167
+ *
168
+ * @param {import("../model/node.js").BoxLayout} layout
169
+ * @param {HandleMetrics} metrics
170
+ * @returns {Handle[]}
171
+ */
172
+ export function handleRects(layout, metrics) {
173
+ requireMetrics(metrics);
174
+ const { resize } = affordancesFor(layout, metrics);
175
+ const half = metrics.handleSize / 2;
176
+ return resize.map((name) => {
177
+ const center = HANDLE_ANCHOR[/** @type {(typeof ALL_HANDLES)[number]} */ (name)](layout);
178
+ return {
179
+ name,
180
+ rect: { x: center.x - half, y: center.y - half, cx: metrics.handleSize, cy: metrics.handleSize },
181
+ };
182
+ });
183
+ }
184
+
185
+ /**
186
+ * Which resize handle (if any) sits under a page-space pt point, widened
187
+ * by `handleSlop` on every side. Never reports "move" — a point that
188
+ * misses every resize handle is the caller's cue to fall back to
189
+ * `hit-test.js`'s box lookup (a click anywhere else in the box body is a
190
+ * move, not this function's concern).
191
+ *
192
+ * @param {import("../model/node.js").BoxLayout} layout
193
+ * @param {HandleMetrics} metrics
194
+ * @param {number} x
195
+ * @param {number} y
196
+ * @returns {string | null}
197
+ */
198
+ export function handleAtPoint(layout, metrics, x, y) {
199
+ requireMetrics(metrics);
200
+ for (const handle of handleRects(layout, metrics)) {
201
+ const expanded = {
202
+ x: handle.rect.x - metrics.handleSlop,
203
+ y: handle.rect.y - metrics.handleSlop,
204
+ cx: handle.rect.cx + metrics.handleSlop * 2,
205
+ cy: handle.rect.cy + metrics.handleSlop * 2,
206
+ };
207
+ if (rectContains(expanded, { x, y, cx: 0, cy: 0 })) {
208
+ return handle.name;
209
+ }
210
+ }
211
+ return null;
212
+ }
@@ -1,82 +1,82 @@
1
- // The box context menu's MODEL — not DOM (ADR-0018 Karar 8, §9/Karar 9).
2
- // No src/, DOM, or history import. `Overlay` (a later phase) is the one
3
- // place this turns into elements; every field here is plain data a test
4
- // can read without a document.
5
- //
6
- // The stack list is `boxesAtPoint`'s (hit-test.js) SECOND consumer — a
7
- // plain click is the first. One function, two callers: alt+tick cycling
8
- // through overlaps is deferred to P3b-b (§3.3), but the menu's own "boxes
9
- // at this point" submenu ships in P3b-a using the exact same hit-test.
10
-
11
- import { boxesAtPoint } from "./hit-test.js";
12
-
13
- /**
14
- * @typedef {object} MenuAction
15
- * @property {"action"} kind
16
- * @property {"cut" | "copy" | "paste" | "delete" | "front" | "back" | "forward" | "backward"} id
17
- * @property {boolean} enabled
18
- */
19
-
20
- /**
21
- * @typedef {object} MenuSeparator
22
- * @property {"separator"} kind
23
- */
24
-
25
- /**
26
- * @typedef {object} MenuStackSubmenu
27
- * @property {"submenu"} kind
28
- * @property {"stack"} id
29
- * @property {import("../model/node.js").NodeId[]} items top-to-bottom, `boxesAtPoint`'s own order
30
- */
31
-
32
- /** @typedef {MenuAction | MenuSeparator | MenuStackSubmenu} MenuItem */
33
-
34
- /**
35
- * The context-menu model for a right-click at `point` (page pt-space).
36
- * Labels are NOT included — this is a model, not presentation; the
37
- * (future) `Overlay` maps each `id` to its own localized label.
38
- *
39
- * Kes/Kopyala/Sil are `enabled` only when there IS a selection AND acting
40
- * on it cannot empty the document (`deleteBox` already refuses that,
41
- * `src/commands/box.js:51` — the menu surfaces the same rule as a
42
- * disabled item instead of a command that silently no-ops later). Yapıştır
43
- * is always enabled: whether the system clipboard actually holds
44
- * something is an async, DOM-observable fact a pure function cannot know;
45
- * the paste command itself already no-ops on empty content
46
- * (`src/input/clipboard.js`'s `handlePaste`).
47
- *
48
- * The stack submenu (§3.3) is present only when `boxesAtPoint` finds
49
- * something at `point` — an empty submenu behind a "▸" arrow is a dead
50
- * end no real menu offers, unlike a merely-disabled action.
51
- *
52
- * @param {import("../model/node.js").Doc} doc
53
- * @param {number} page
54
- * @param {{x: number, y: number}} point
55
- * @param {import("../model/node.js").NodeId[]} selection
56
- * @returns {MenuItem[]}
57
- */
58
- export function contextMenuModel(doc, page, point, selection) {
59
- const hasSelection = selection.length > 0;
60
- const wouldEmptyDoc = hasSelection && selection.length >= doc.content.length;
61
- const destructiveEnabled = hasSelection && !wouldEmptyDoc;
62
-
63
- /** @type {MenuItem[]} */
64
- const items = [
65
- { kind: "action", id: "cut", enabled: destructiveEnabled },
66
- { kind: "action", id: "copy", enabled: hasSelection },
67
- { kind: "action", id: "paste", enabled: true },
68
- { kind: "action", id: "delete", enabled: destructiveEnabled },
69
- { kind: "separator" },
70
- { kind: "action", id: "front", enabled: hasSelection },
71
- { kind: "action", id: "back", enabled: hasSelection },
72
- { kind: "action", id: "forward", enabled: hasSelection },
73
- { kind: "action", id: "backward", enabled: hasSelection },
74
- ];
75
-
76
- const stack = boxesAtPoint(doc, page, point.x, point.y);
77
- if (stack.length > 0) {
78
- items.push({ kind: "separator" }, { kind: "submenu", id: "stack", items: stack });
79
- }
80
-
81
- return items;
82
- }
1
+ // The box context menu's MODEL — not DOM (ADR-0018 Karar 8, §9/Karar 9).
2
+ // No src/, DOM, or history import. `Overlay` (a later phase) is the one
3
+ // place this turns into elements; every field here is plain data a test
4
+ // can read without a document.
5
+ //
6
+ // The stack list is `boxesAtPoint`'s (hit-test.js) SECOND consumer — a
7
+ // plain click is the first. One function, two callers: alt+tick cycling
8
+ // through overlaps is deferred to P3b-b (§3.3), but the menu's own "boxes
9
+ // at this point" submenu ships in P3b-a using the exact same hit-test.
10
+
11
+ import { boxesAtPoint } from "./hit-test.js";
12
+
13
+ /**
14
+ * @typedef {object} MenuAction
15
+ * @property {"action"} kind
16
+ * @property {"cut" | "copy" | "paste" | "delete" | "front" | "back" | "forward" | "backward"} id
17
+ * @property {boolean} enabled
18
+ */
19
+
20
+ /**
21
+ * @typedef {object} MenuSeparator
22
+ * @property {"separator"} kind
23
+ */
24
+
25
+ /**
26
+ * @typedef {object} MenuStackSubmenu
27
+ * @property {"submenu"} kind
28
+ * @property {"stack"} id
29
+ * @property {import("../model/node.js").NodeId[]} items top-to-bottom, `boxesAtPoint`'s own order
30
+ */
31
+
32
+ /** @typedef {MenuAction | MenuSeparator | MenuStackSubmenu} MenuItem */
33
+
34
+ /**
35
+ * The context-menu model for a right-click at `point` (page pt-space).
36
+ * Labels are NOT included — this is a model, not presentation; the
37
+ * (future) `Overlay` maps each `id` to its own localized label.
38
+ *
39
+ * Kes/Kopyala/Sil are `enabled` only when there IS a selection AND acting
40
+ * on it cannot empty the document (`deleteBox` already refuses that,
41
+ * `src/commands/box.js:51` — the menu surfaces the same rule as a
42
+ * disabled item instead of a command that silently no-ops later). Yapıştır
43
+ * is always enabled: whether the system clipboard actually holds
44
+ * something is an async, DOM-observable fact a pure function cannot know;
45
+ * the paste command itself already no-ops on empty content
46
+ * (`src/input/clipboard.js`'s `handlePaste`).
47
+ *
48
+ * The stack submenu (§3.3) is present only when `boxesAtPoint` finds
49
+ * something at `point` — an empty submenu behind a "▸" arrow is a dead
50
+ * end no real menu offers, unlike a merely-disabled action.
51
+ *
52
+ * @param {import("../model/node.js").Doc} doc
53
+ * @param {number} page
54
+ * @param {{x: number, y: number}} point
55
+ * @param {import("../model/node.js").NodeId[]} selection
56
+ * @returns {MenuItem[]}
57
+ */
58
+ export function contextMenuModel(doc, page, point, selection) {
59
+ const hasSelection = selection.length > 0;
60
+ const wouldEmptyDoc = hasSelection && selection.length >= doc.content.length;
61
+ const destructiveEnabled = hasSelection && !wouldEmptyDoc;
62
+
63
+ /** @type {MenuItem[]} */
64
+ const items = [
65
+ { kind: "action", id: "cut", enabled: destructiveEnabled },
66
+ { kind: "action", id: "copy", enabled: hasSelection },
67
+ { kind: "action", id: "paste", enabled: true },
68
+ { kind: "action", id: "delete", enabled: destructiveEnabled },
69
+ { kind: "separator" },
70
+ { kind: "action", id: "front", enabled: hasSelection },
71
+ { kind: "action", id: "back", enabled: hasSelection },
72
+ { kind: "action", id: "forward", enabled: hasSelection },
73
+ { kind: "action", id: "backward", enabled: hasSelection },
74
+ ];
75
+
76
+ const stack = boxesAtPoint(doc, page, point.x, point.y);
77
+ if (stack.length > 0) {
78
+ items.push({ kind: "separator" }, { kind: "submenu", id: "stack", items: stack });
79
+ }
80
+
81
+ return items;
82
+ }