silvery 0.19.2 → 0.21.1

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 (172) hide show
  1. package/README.md +9 -4
  2. package/dist/Text-BRf-59j2.mjs +237 -0
  3. package/dist/Text-BRf-59j2.mjs.map +1 -0
  4. package/dist/ag-BeFC4S2N.mjs +8727 -0
  5. package/dist/ag-BeFC4S2N.mjs.map +1 -0
  6. package/dist/{animation-Cn64yepo.mjs → animation-N8MFybTk.mjs} +2 -2
  7. package/dist/animation-N8MFybTk.mjs.map +1 -0
  8. package/dist/{ansi-CLOitHKx.mjs → ansi-C6Qs1Wn2.mjs} +1 -1
  9. package/dist/{ansi-CLOitHKx.mjs.map → ansi-C6Qs1Wn2.mjs.map} +1 -1
  10. package/dist/{ansi-Cc33mW54.d.mts → ansi-CBkam1ty.d.mts} +1 -1
  11. package/dist/{ansi-Cc33mW54.d.mts.map → ansi-CBkam1ty.d.mts.map} +1 -1
  12. package/dist/ansi-OYRLxxZQ.mjs +10669 -0
  13. package/dist/ansi-OYRLxxZQ.mjs.map +1 -0
  14. package/dist/bound-term-BumfuXXW.d.mts +4902 -0
  15. package/dist/bound-term-BumfuXXW.d.mts.map +1 -0
  16. package/dist/{chunk-Vs_PY4HZ.mjs → chunk-hT5z_Zn9.mjs} +1 -3
  17. package/dist/cli-DG6zsfsS.mjs +4 -0
  18. package/dist/context-BU5LkkIy.mjs.map +1 -1
  19. package/dist/{devtools-DxkSLXDA.mjs → devtools-9zfhpFyG.mjs} +3 -3
  20. package/dist/{devtools-DxkSLXDA.mjs.map → devtools-9zfhpFyG.mjs.map} +1 -1
  21. package/dist/devtools-nmPUmYU_.mjs +2 -0
  22. package/dist/easing-B0oZKDki.d.mts +24 -0
  23. package/dist/easing-B0oZKDki.d.mts.map +1 -0
  24. package/dist/{eta-Bb3RH3wh.mjs → eta-DGOuC8yU.mjs} +5 -1
  25. package/dist/{eta-Bb3RH3wh.mjs.map → eta-DGOuC8yU.mjs.map} +1 -1
  26. package/dist/flexily-zero-adapter-CEJOcbNp.mjs +306 -0
  27. package/dist/flexily-zero-adapter-CEJOcbNp.mjs.map +1 -0
  28. package/dist/{flexily-zero-adapter-CMxXhdOL.mjs → flexily-zero-adapter-D6hcFgrH.mjs} +1 -1
  29. package/dist/image-1nRyKa60.mjs +5960 -0
  30. package/dist/image-1nRyKa60.mjs.map +1 -0
  31. package/dist/{index-D3saHouR.d.mts → index-2E1jYgak.d.mts} +1057 -1133
  32. package/dist/index-2E1jYgak.d.mts.map +1 -0
  33. package/dist/index-Bi4Jdz5g.d.mts +453 -0
  34. package/dist/index-Bi4Jdz5g.d.mts.map +1 -0
  35. package/dist/index-Dg1YaeJb.d.mts +336 -0
  36. package/dist/index-Dg1YaeJb.d.mts.map +1 -0
  37. package/dist/{index-BXslOebb.d.mts → index-DnuadDNL.d.mts} +5750 -4158
  38. package/dist/index-DnuadDNL.d.mts.map +1 -0
  39. package/dist/index.d.mts +8 -5
  40. package/dist/index.d.mts.map +1 -1
  41. package/dist/index.mjs +16 -12
  42. package/dist/index.mjs.map +1 -1
  43. package/dist/layout-engine-Ca_nbtfL.mjs +67 -0
  44. package/dist/layout-engine-Ca_nbtfL.mjs.map +1 -0
  45. package/dist/{layout-engine-B6Cdz1yZ.mjs → layout-engine-CgsoBRIn.mjs} +1 -1
  46. package/dist/layout-signals-Dch2EiCy.mjs +1111 -0
  47. package/dist/layout-signals-Dch2EiCy.mjs.map +1 -0
  48. package/dist/mouse-events-hnbJZRwK.mjs +1071 -0
  49. package/dist/mouse-events-hnbJZRwK.mjs.map +1 -0
  50. package/dist/{multi-progress-DAQC7eap.d.mts → multi-progress-Bg4ngK80.d.mts} +2 -2
  51. package/dist/{multi-progress-DAQC7eap.d.mts.map → multi-progress-Bg4ngK80.d.mts.map} +1 -1
  52. package/dist/{multi-progress-Bq9Oi_WI.mjs → multi-progress-CaXTuL9G.mjs} +3 -3
  53. package/dist/{multi-progress-Bq9Oi_WI.mjs.map → multi-progress-CaXTuL9G.mjs.map} +1 -1
  54. package/dist/{node-BeWlnCPY.mjs → node-BiFu8I9Y.mjs} +4 -4
  55. package/dist/node-BiFu8I9Y.mjs.map +1 -0
  56. package/dist/progress-B_UPy6zk.mjs +675 -0
  57. package/dist/progress-B_UPy6zk.mjs.map +1 -0
  58. package/dist/{progress-bar-CXE5Qfkd.mjs → progress-bar-DmIMPdL0.mjs} +4 -4
  59. package/dist/{progress-bar-CXE5Qfkd.mjs.map → progress-bar-DmIMPdL0.mjs.map} +1 -1
  60. package/dist/reconciler-NBDSEm8k.mjs +2178 -0
  61. package/dist/reconciler-NBDSEm8k.mjs.map +1 -0
  62. package/dist/render-string-B4h4SmK7.mjs +211 -0
  63. package/dist/render-string-B4h4SmK7.mjs.map +1 -0
  64. package/dist/{render-string-CDCeYkS3.mjs → render-string-BntLj7Xq.mjs} +1 -1
  65. package/dist/runtime.d.mts +3 -2
  66. package/dist/runtime.mjs +3 -3
  67. package/dist/{src-B5GjfG7g.mjs → schemes-DYt2ushj.mjs} +23 -1812
  68. package/dist/schemes-DYt2ushj.mjs.map +1 -0
  69. package/dist/{spinner-CGo34vyR.d.mts → spinner-CLgzJ_QF.d.mts} +2 -2
  70. package/dist/{spinner-CGo34vyR.d.mts.map → spinner-CLgzJ_QF.d.mts.map} +1 -1
  71. package/dist/{spinner-CeOmcuw_.mjs → spinner-Py8_-hn9.mjs} +23 -8
  72. package/dist/spinner-Py8_-hn9.mjs.map +1 -0
  73. package/dist/src-B9S_woYc.mjs +25024 -0
  74. package/dist/src-B9S_woYc.mjs.map +1 -0
  75. package/dist/src-CbhWmUnF.mjs +3928 -0
  76. package/dist/src-CbhWmUnF.mjs.map +1 -0
  77. package/dist/src-Dvq-s8iD.mjs +939 -0
  78. package/dist/src-Dvq-s8iD.mjs.map +1 -0
  79. package/dist/src-Oe6x5PrS.mjs +4621 -0
  80. package/dist/src-Oe6x5PrS.mjs.map +1 -0
  81. package/dist/{types-Bk2yw9Qj.mjs → src-WeA_J4BV.mjs} +34 -94
  82. package/dist/src-WeA_J4BV.mjs.map +1 -0
  83. package/dist/steps-DYrzCUCK.d.mts +202 -0
  84. package/dist/steps-DYrzCUCK.d.mts.map +1 -0
  85. package/dist/svg-DhxQkz-O.mjs +255 -0
  86. package/dist/svg-DhxQkz-O.mjs.map +1 -0
  87. package/dist/svg-Hk7lIl4F.d.mts +82 -0
  88. package/dist/svg-Hk7lIl4F.d.mts.map +1 -0
  89. package/dist/term.d.mts +3 -0
  90. package/dist/term.mjs +4 -0
  91. package/dist/theme.d.mts +95 -2
  92. package/dist/theme.d.mts.map +1 -0
  93. package/dist/theme.mjs +4 -3
  94. package/dist/{types-BH_v3iMT.d.mts → types-Bx-XZNbE.d.mts} +2 -15
  95. package/dist/types-Bx-XZNbE.d.mts.map +1 -0
  96. package/dist/ui/animation.d.mts +2 -1
  97. package/dist/ui/animation.mjs +1 -1
  98. package/dist/ui/ansi.d.mts +1 -1
  99. package/dist/ui/ansi.mjs +1 -1
  100. package/dist/ui/cli.d.mts +3 -3
  101. package/dist/ui/cli.mjs +5 -5
  102. package/dist/ui/display.d.mts +1 -1
  103. package/dist/ui/display.mjs.map +1 -1
  104. package/dist/ui/image.d.mts +2 -2
  105. package/dist/ui/image.mjs +2 -2
  106. package/dist/ui/input.d.mts +1 -1
  107. package/dist/ui/input.mjs.map +1 -1
  108. package/dist/ui/progress.d.mts +5 -249
  109. package/dist/ui/progress.mjs +5 -858
  110. package/dist/ui/react.d.mts +1 -1
  111. package/dist/ui/react.mjs +2 -2
  112. package/dist/ui/react.mjs.map +1 -1
  113. package/dist/ui/recording-chrome-react.d.mts +21 -0
  114. package/dist/ui/recording-chrome-react.d.mts.map +1 -0
  115. package/dist/ui/recording-chrome-react.mjs +105 -0
  116. package/dist/ui/recording-chrome-react.mjs.map +1 -0
  117. package/dist/ui/recording-chrome.d.mts +2 -0
  118. package/dist/ui/recording-chrome.mjs +2 -0
  119. package/dist/ui/utils.mjs +1 -1
  120. package/dist/ui/wrappers.d.mts +3 -3
  121. package/dist/ui/wrappers.mjs +2 -2
  122. package/dist/ui.d.mts +7 -6
  123. package/dist/ui.mjs +8 -7
  124. package/dist/{useLatest-Bg2x4bfP.d.mts → useLatest-DC8i7guK.d.mts} +5 -25
  125. package/dist/useLatest-DC8i7guK.d.mts.map +1 -0
  126. package/dist/useLayout-BKsQl2Or.mjs +424 -0
  127. package/dist/useLayout-BKsQl2Or.mjs.map +1 -0
  128. package/dist/{with-text-input-CRfoiFFG.d.mts → with-text-input-DG4f7JII.d.mts} +4 -55
  129. package/dist/with-text-input-DG4f7JII.d.mts.map +1 -0
  130. package/dist/wrapper-D7gNSsgf.mjs +3589 -0
  131. package/dist/wrapper-D7gNSsgf.mjs.map +1 -0
  132. package/dist/{wrappers-UTADQkSY.mjs → wrappers-CypAzrMO.mjs} +19 -161
  133. package/dist/wrappers-CypAzrMO.mjs.map +1 -0
  134. package/dist/yoga-adapter-1ex8r0ws.mjs +2 -0
  135. package/dist/{yoga-adapter-8oRGRw8V.mjs → yoga-adapter-SsEIqMc1.mjs} +28 -2
  136. package/dist/yoga-adapter-SsEIqMc1.mjs.map +1 -0
  137. package/package.json +63 -12
  138. package/dist/animation-Cn64yepo.mjs.map +0 -1
  139. package/dist/cli-BKp0YtBD.mjs +0 -4
  140. package/dist/devtools-9QY4teqI.mjs +0 -2
  141. package/dist/flexily-zero-adapter-BlQa46nr.mjs +0 -3385
  142. package/dist/flexily-zero-adapter-BlQa46nr.mjs.map +0 -1
  143. package/dist/image-CTII5QWI.mjs +0 -477
  144. package/dist/image-CTII5QWI.mjs.map +0 -1
  145. package/dist/index-BXslOebb.d.mts.map +0 -1
  146. package/dist/index-BnA7mNpo.d.mts +0 -175
  147. package/dist/index-BnA7mNpo.d.mts.map +0 -1
  148. package/dist/index-D3saHouR.d.mts.map +0 -1
  149. package/dist/layout-engine-ClUgv6jB.mjs +0 -50
  150. package/dist/layout-engine-ClUgv6jB.mjs.map +0 -1
  151. package/dist/node-BeWlnCPY.mjs.map +0 -1
  152. package/dist/reconciler-Cwgm8hRR.mjs +0 -8459
  153. package/dist/reconciler-Cwgm8hRR.mjs.map +0 -1
  154. package/dist/render-string-Darrg7ku.mjs +0 -5529
  155. package/dist/render-string-Darrg7ku.mjs.map +0 -1
  156. package/dist/spinner-CeOmcuw_.mjs.map +0 -1
  157. package/dist/src-B5GjfG7g.mjs.map +0 -1
  158. package/dist/src-CChwjk0Z.mjs +0 -738
  159. package/dist/src-CChwjk0Z.mjs.map +0 -1
  160. package/dist/src-CF-6UN01.mjs +0 -19434
  161. package/dist/src-CF-6UN01.mjs.map +0 -1
  162. package/dist/src-NCKb8kE5.mjs +0 -2660
  163. package/dist/src-NCKb8kE5.mjs.map +0 -1
  164. package/dist/types-BH_v3iMT.d.mts.map +0 -1
  165. package/dist/types-Bk2yw9Qj.mjs.map +0 -1
  166. package/dist/ui/progress.d.mts.map +0 -1
  167. package/dist/ui/progress.mjs.map +0 -1
  168. package/dist/useLatest-Bg2x4bfP.d.mts.map +0 -1
  169. package/dist/with-text-input-CRfoiFFG.d.mts.map +0 -1
  170. package/dist/wrappers-UTADQkSY.mjs.map +0 -1
  171. package/dist/yoga-adapter-8oRGRw8V.mjs.map +0 -1
  172. package/dist/yoga-adapter-D_CcxSt5.mjs +0 -2
@@ -0,0 +1,1111 @@
1
+ import { i as signal } from "./src-WeA_J4BV.mjs";
2
+ //#region packages/ag/src/types.ts
3
+ /**
4
+ * Check if two rects are equal (same position and size).
5
+ */
6
+ function rectEqual$1(a, b) {
7
+ if (a === b) return true;
8
+ if (!a || !b) return false;
9
+ return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
10
+ }
11
+ //#endregion
12
+ //#region packages/ag/src/wrap-measurer.ts
13
+ let _measurer = null;
14
+ /**
15
+ * Register the active wrap measurer. Pass `null` to clear (test teardown,
16
+ * or a Term disposing its runtime).
17
+ *
18
+ * Idempotent: setting the same reference twice is a no-op. Setting a new
19
+ * reference replaces the previous one — there's no stack. v1 assumes a
20
+ * single Term-per-process consumer; multi-Term setups need a different
21
+ * dispatch (see file header).
22
+ */
23
+ function setWrapMeasurer(m) {
24
+ _measurer = m;
25
+ }
26
+ /**
27
+ * Read the active wrap measurer, or `null` if none is registered.
28
+ *
29
+ * Geometry helpers (`computeSelectionFragments` is the v1 consumer) call
30
+ * this at compute-time — not at module-load — so the registration order
31
+ * doesn't matter. The fragment helper falls back to `\n`-split when this
32
+ * returns null.
33
+ */
34
+ function getWrapMeasurer() {
35
+ return _measurer;
36
+ }
37
+ //#endregion
38
+ //#region packages/ag/src/place-floating.ts
39
+ function splitPlacement(placement) {
40
+ const dashIdx = placement.indexOf("-");
41
+ return {
42
+ side: placement.slice(0, dashIdx),
43
+ align: placement.slice(dashIdx + 1)
44
+ };
45
+ }
46
+ function oppositePlacement(placement) {
47
+ const { side, align } = splitPlacement(placement);
48
+ return `${{
49
+ top: "bottom",
50
+ bottom: "top",
51
+ left: "right",
52
+ right: "left"
53
+ }[side]}-${align}`;
54
+ }
55
+ function rectFitsWithin(rect, boundary) {
56
+ return rect.x >= boundary.x && rect.y >= boundary.y && rect.x + rect.width <= boundary.x + boundary.width && rect.y + rect.height <= boundary.y + boundary.height;
57
+ }
58
+ function clampAxis(value, min, max) {
59
+ if (max < min) return min;
60
+ return Math.min(max, Math.max(min, value));
61
+ }
62
+ function shiftIntoBoundary(rect, boundary) {
63
+ return {
64
+ x: clampAxis(rect.x, boundary.x, boundary.x + boundary.width - rect.width),
65
+ y: clampAxis(rect.y, boundary.y, boundary.y + boundary.height - rect.height),
66
+ width: rect.width,
67
+ height: rect.height
68
+ };
69
+ }
70
+ function rectEqual(a, b) {
71
+ return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;
72
+ }
73
+ function sideOverflow(rect, boundary, side) {
74
+ switch (side) {
75
+ case "top": return Math.max(0, boundary.y - rect.y);
76
+ case "bottom": return Math.max(0, rect.y + rect.height - (boundary.y + boundary.height));
77
+ case "left": return Math.max(0, boundary.x - rect.x);
78
+ case "right": return Math.max(0, rect.x + rect.width - (boundary.x + boundary.width));
79
+ }
80
+ }
81
+ /**
82
+ * Compute the absolute rect at which a floating decoration should be painted
83
+ * relative to its anchor.
84
+ *
85
+ * Inputs:
86
+ * - `anchor`: the anchor's rect (typically `findAnchor(root, id)` →
87
+ * `contentRect`). Origin in the same absolute terminal cell space as
88
+ * other rect signals.
89
+ * - `target.{width, height}`: intrinsic size of the floating decoration.
90
+ * Both must be `>= 0` — this function does not enforce minimums; callers
91
+ * pass through what their renderer asked for.
92
+ * - `placement`: 12-placement vocabulary. See module docstring for the
93
+ * visual reference.
94
+ *
95
+ * Output: `Rect` in the same coordinate space as `anchor`. Width/height equal
96
+ * `target.width`/`target.height` exactly (no clamping, no shifting).
97
+ *
98
+ * **Pure**: no allocation other than the result rect, no I/O, no logging.
99
+ * Suitable for property tests and SILVERY_STRICT cross-checks.
100
+ */
101
+ function placeFloating(anchor, target, placement, options = {}) {
102
+ const { x: ax, y: ay, width: aw, height: ah } = anchor;
103
+ const tw = target.width;
104
+ const th = target.height;
105
+ const { side, align } = splitPlacement(placement);
106
+ let x = 0;
107
+ let y = 0;
108
+ if (side === "top" || side === "bottom") {
109
+ y = side === "top" ? ay - th : ay + ah;
110
+ if (align === "start") x = ax;
111
+ else if (align === "end") x = ax + aw - tw;
112
+ else x = ax + Math.round((aw - tw) / 2);
113
+ } else {
114
+ x = side === "left" ? ax - tw : ax + aw;
115
+ if (align === "start") y = ay;
116
+ else if (align === "end") y = ay + ah - th;
117
+ else y = ay + Math.round((ah - th) / 2);
118
+ }
119
+ const offset = options.offset ?? 0;
120
+ const alignOffset = options.alignOffset ?? 0;
121
+ if (side === "top") y -= offset;
122
+ else if (side === "bottom") y += offset;
123
+ else if (side === "left") x -= offset;
124
+ else x += offset;
125
+ if (side === "top" || side === "bottom") x += alignOffset;
126
+ else y += alignOffset;
127
+ return {
128
+ x,
129
+ y,
130
+ width: tw,
131
+ height: th
132
+ };
133
+ }
134
+ /**
135
+ * Resolve a floating rect with optional viewport collision handling.
136
+ *
137
+ * This is the collision-aware peer of `placeFloating`. The fixed-placement
138
+ * helper remains intentionally simple and deterministic; this function adds
139
+ * the behavior needed by declarative popovers/tooltips: gap offsets, alignment
140
+ * nudges, side flipping, viewport shifting, and hide-on-overflow.
141
+ */
142
+ function resolveFloatingPlacement(anchor, target, placement, options = {}) {
143
+ const boundary = options.boundary ?? null;
144
+ const strategy = options.collisionStrategy ?? "none";
145
+ const requested = placeFloating(anchor, target, placement, options);
146
+ if (!boundary || strategy === "none") return {
147
+ rect: requested,
148
+ placement,
149
+ flipped: false,
150
+ shifted: false
151
+ };
152
+ if (strategy === "hide") return rectFitsWithin(requested, boundary) ? {
153
+ rect: requested,
154
+ placement,
155
+ flipped: false,
156
+ shifted: false
157
+ } : null;
158
+ let finalPlacement = placement;
159
+ let rect = requested;
160
+ let flipped = false;
161
+ if (strategy === "flip" || strategy === "flip-then-shift") {
162
+ const side = splitPlacement(placement).side;
163
+ const requestedSideOverflow = sideOverflow(requested, boundary, side);
164
+ if (requestedSideOverflow > 0) {
165
+ const candidatePlacement = oppositePlacement(placement);
166
+ const candidate = placeFloating(anchor, target, candidatePlacement, options);
167
+ const candidateSide = splitPlacement(candidatePlacement).side;
168
+ if (sideOverflow(candidate, boundary, candidateSide) < requestedSideOverflow) {
169
+ finalPlacement = candidatePlacement;
170
+ rect = candidate;
171
+ flipped = true;
172
+ }
173
+ }
174
+ }
175
+ let shifted = false;
176
+ if (strategy === "shift" || strategy === "flip-then-shift") {
177
+ const shiftedRect = shiftIntoBoundary(rect, boundary);
178
+ shifted = !rectEqual(shiftedRect, rect);
179
+ rect = shiftedRect;
180
+ }
181
+ return {
182
+ rect,
183
+ placement: finalPlacement,
184
+ flipped,
185
+ shifted
186
+ };
187
+ }
188
+ //#endregion
189
+ //#region packages/ag/src/layout-signals.ts
190
+ /**
191
+ * withLayoutSignals — reactive signal layer for AgNode layout outputs.
192
+ *
193
+ * Composable plugin that wraps an AgNode with reactive signals for layout
194
+ * rects, text content, and focus state. Engine-agnostic — works with
195
+ * Flexily, Yoga, or any future layout engine.
196
+ *
197
+ * Signals are WeakMap-backed and lazily created. Nodes without subscribers
198
+ * pay zero cost. After layout completes, the pipeline calls `syncSignals()`
199
+ * to propagate imperative state into signals.
200
+ *
201
+ * ## Usage
202
+ *
203
+ * ```ts
204
+ * import { getLayoutSignals, syncSignals } from "@silvery/ag/layout-signals"
205
+ *
206
+ * // Get (or create) signals for a node
207
+ * const signals = getLayoutSignals(node)
208
+ * signals.boxRect() // read current rect
209
+ * signals.textContent() // read current text
210
+ *
211
+ * // After layout/reconciler mutations, sync imperative → reactive
212
+ * syncSignals(node)
213
+ * ```
214
+ *
215
+ * ## Three-layer stack
216
+ *
217
+ * Layer 0: alien-signals (signal, computed, effect)
218
+ * Layer 1: getLayoutSignals() — this module (@silvery/ag, framework-agnostic)
219
+ * Layer 2: useSignal(signal) — @silvery/ag-react (React bridge)
220
+ * Layer 3: useBoxRect(), useAgNode() — semantic convenience hooks
221
+ */
222
+ const permanentlyObservedLayoutSignals = /* @__PURE__ */ new WeakMap();
223
+ const observedLayoutSignals = /* @__PURE__ */ new WeakMap();
224
+ const retainedLayoutSignals = /* @__PURE__ */ new WeakMap();
225
+ function addObservedLayoutSignal(node, key) {
226
+ let observed = observedLayoutSignals.get(node);
227
+ if (!observed) {
228
+ observed = /* @__PURE__ */ new Set();
229
+ observedLayoutSignals.set(node, observed);
230
+ }
231
+ observed.add(key);
232
+ }
233
+ function markObservedLayoutSignal(node, key) {
234
+ addObservedLayoutSignal(node, key);
235
+ let permanent = permanentlyObservedLayoutSignals.get(node);
236
+ if (!permanent) {
237
+ permanent = /* @__PURE__ */ new Set();
238
+ permanentlyObservedLayoutSignals.set(node, permanent);
239
+ }
240
+ permanent.add(key);
241
+ }
242
+ function observeLayoutSignal(node, key) {
243
+ addObservedLayoutSignal(node, key);
244
+ let retained = retainedLayoutSignals.get(node);
245
+ if (!retained) {
246
+ retained = /* @__PURE__ */ new Map();
247
+ retainedLayoutSignals.set(node, retained);
248
+ }
249
+ retained.set(key, (retained.get(key) ?? 0) + 1);
250
+ let released = false;
251
+ return () => {
252
+ if (released) return;
253
+ released = true;
254
+ const current = retainedLayoutSignals.get(node);
255
+ if (!current) return;
256
+ const next = (current.get(key) ?? 0) - 1;
257
+ if (next > 0) {
258
+ current.set(key, next);
259
+ return;
260
+ }
261
+ current.delete(key);
262
+ if (current.size === 0) retainedLayoutSignals.delete(node);
263
+ if (!permanentlyObservedLayoutSignals.get(node)?.has(key)) observedLayoutSignals.get(node)?.delete(key);
264
+ };
265
+ }
266
+ function cursorRectEqual(a, b) {
267
+ if (a === b) return true;
268
+ if (!a || !b) return false;
269
+ return a.x === b.x && a.y === b.y && a.visible === b.visible && a.shape === b.shape;
270
+ }
271
+ /**
272
+ * Per-field equality on a list of rects. Used to skip selection-fragment
273
+ * signal writes when nothing changed — mirrors the rect-tuple equality
274
+ * pattern used for boxRect/scrollRect/screenRect/contentRect/cursorRect.
275
+ *
276
+ * Reference equality is checked first (the common no-op path); only when
277
+ * lengths match do we walk the entries. An empty array is the canonical
278
+ * "no fragments" state — the equality path treats `[]` and `[]` as equal
279
+ * by length-zero, so collapsed/no-selection nodes don't churn the signal.
280
+ */
281
+ function selectionFragmentsEqual(a, b) {
282
+ if (a === b) return true;
283
+ if (a.length !== b.length) return false;
284
+ for (let i = 0; i < a.length; i++) if (!rectEqual$1(a[i] ?? null, b[i] ?? null)) return false;
285
+ return true;
286
+ }
287
+ /**
288
+ * Stable empty-rects sentinel — `selectionFragments` defaults to this when a
289
+ * node has no `selectionIntent` declared. Reusing the same array reference
290
+ * means subscribers see reference-stable "no selection" frames and can skip
291
+ * downstream re-computation. The array is frozen so accidental mutation
292
+ * never corrupts the sentinel.
293
+ */
294
+ const EMPTY_FRAGMENTS = Object.freeze([]);
295
+ /**
296
+ * Stable empty-decorations sentinel — `decorationRects` defaults to this when
297
+ * a node has no `decorations` BoxProp. Same reference-stability story as
298
+ * `EMPTY_FRAGMENTS`.
299
+ */
300
+ const EMPTY_DECORATION_RECTS = Object.freeze([]);
301
+ const signalMap = /* @__PURE__ */ new WeakMap();
302
+ /**
303
+ * Get or create layout signals for a node.
304
+ *
305
+ * Lazily created on first access. Automatically garbage-collected
306
+ * when the node is removed from the tree (WeakMap semantics).
307
+ */
308
+ function getLayoutSignals(node) {
309
+ let s = signalMap.get(node);
310
+ if (!s) {
311
+ s = {
312
+ boxRect: signal(node.boxRect),
313
+ scrollRect: signal(node.scrollRect),
314
+ screenRect: signal(node.screenRect),
315
+ boxRectCommitted: signal(node.boxRect),
316
+ scrollRectCommitted: signal(node.scrollRect),
317
+ screenRectCommitted: signal(node.screenRect),
318
+ contentRect: signal(computeContentRect(node)),
319
+ cursorRect: signal(computeCursorRect(node)),
320
+ focusedNodeId: signal(computeFocusedNodeId(node)),
321
+ selectionFragments: signal(computeSelectionFragments(node)),
322
+ scrollState: signal(snapshotScrollState(node)),
323
+ anchorRect: signal(computeAnchorRect(node)),
324
+ parkRect: signal(computeParkRect(node)),
325
+ decorationRects: signal(EMPTY_DECORATION_RECTS),
326
+ textContent: signal(node.textContent),
327
+ focused: signal(node.interactiveState?.focused ?? false)
328
+ };
329
+ signalMap.set(node, s);
330
+ }
331
+ return s;
332
+ }
333
+ /**
334
+ * Compute the content-box rect for a node — `scrollRect` minus border and
335
+ * padding (CSS content area in absolute terminal coordinates).
336
+ *
337
+ * Returns null when `scrollRect` is not yet populated (pre-layout) or when
338
+ * border + padding would shrink the area to zero/negative width or height
339
+ * (clipped/empty content area).
340
+ *
341
+ * The math is the canonical "border + padding" calculation that the layout
342
+ * engine uses internally. Lifted here so consumers (cursor positioning,
343
+ * popover anchors, selection overlays) read one signal instead of re-deriving
344
+ * the offsets at every call site.
345
+ */
346
+ function computeContentRect(node) {
347
+ const props = node.props;
348
+ const scroll = node.scrollRect;
349
+ if (!scroll) return null;
350
+ const padLeft = props?.paddingLeft ?? props?.paddingX ?? props?.padding ?? 0;
351
+ const padRight = props?.paddingRight ?? props?.paddingX ?? props?.padding ?? 0;
352
+ const padTop = props?.paddingTop ?? props?.paddingY ?? props?.padding ?? 0;
353
+ const padBottom = props?.paddingBottom ?? props?.paddingY ?? props?.padding ?? 0;
354
+ const borderLeft = props?.borderStyle ? 1 : 0;
355
+ const borderRight = props?.borderStyle ? 1 : 0;
356
+ const borderTop = props?.borderStyle ? 1 : 0;
357
+ const borderBottom = props?.borderStyle ? 1 : 0;
358
+ const x = scroll.x + borderLeft + padLeft;
359
+ const y = scroll.y + borderTop + padTop;
360
+ const width = scroll.width - borderLeft - borderRight - padLeft - padRight;
361
+ const height = scroll.height - borderTop - borderBottom - padTop - padBottom;
362
+ if (width <= 0 || height <= 0) return null;
363
+ return {
364
+ x,
365
+ y,
366
+ width,
367
+ height
368
+ };
369
+ }
370
+ /**
371
+ * Compute the absolute caret rect for a node based on its `cursorOffset`
372
+ * prop and current `contentRect`. Caret coordinates are content-area-relative
373
+ * (inside border + padding), so this delegates to `computeContentRect` for
374
+ * the origin instead of redoing the border/padding math here.
375
+ *
376
+ * Returns null when:
377
+ * - the node has no `cursorOffset` prop, OR
378
+ * - `scrollRect` is not yet populated (pre-layout), OR
379
+ * - the content box collapsed to zero/negative size (no place to draw).
380
+ *
381
+ * `computeContentRect` keeps cursor positioning and overlay anchoring on the
382
+ * same origin — Phase 4 / overlay-anchor consumers read `contentRect`
383
+ * directly and won't drift from where the caret lands. See bead
384
+ * `km-silvery.cursor-invariants` invariant 3.
385
+ */
386
+ function computeCursorRect(node) {
387
+ const offset = node.props?.cursorOffset;
388
+ if (!offset) return null;
389
+ const content = computeContentRect(node);
390
+ if (!content) return null;
391
+ return {
392
+ x: content.x + offset.col,
393
+ y: content.y + offset.row,
394
+ visible: offset.visible !== false,
395
+ shape: offset.shape
396
+ };
397
+ }
398
+ /**
399
+ * Compute the focused-node id for a node based on its `focused` BoxProp.
400
+ *
401
+ * Returns the node's `id` (preferred) or `testID` when `props.focused === true`,
402
+ * else `null`. This is the per-node value carried in
403
+ * `LayoutSignals.focusedNodeId` — the tree-walk lookup
404
+ * `findActiveFocusedNodeId(root)` picks the deepest non-null among all
405
+ * declarers (Phase 4a precedence rule).
406
+ *
407
+ * Identity priority: `id` > `testID`. Apps that want stable focus identity
408
+ * should set one of those props alongside `focused={true}`. When neither is
409
+ * set but `focused === true`, an opaque sentinel (`"__focused__"`) is
410
+ * returned so the signal is still observable as "something is focused" —
411
+ * downstream consumers should not depend on the sentinel value beyond
412
+ * non-null/null.
413
+ */
414
+ function computeFocusedNodeId(node) {
415
+ const props = node.props;
416
+ if (!props?.focused) return null;
417
+ if (typeof props.id === "string" && props.id.length > 0) return props.id;
418
+ if (typeof props.testID === "string" && props.testID.length > 0) return props.testID;
419
+ return "__focused__";
420
+ }
421
+ /**
422
+ * Resolve the `anchorRef` BoxProp into a string id. Accepts the shorthand
423
+ * `anchorRef="my-id"` and the structured `anchorRef={{ id: "my-id" }}` form.
424
+ *
425
+ * Returns `null` when no anchorRef is present, or when the prop is malformed
426
+ * (empty id string). Apps that need stable anchor identity should always pass
427
+ * a non-empty string.
428
+ */
429
+ function resolveAnchorId(node) {
430
+ const ref = node.props?.anchorRef;
431
+ if (!ref) return null;
432
+ if (typeof ref === "string") return ref.length > 0 ? ref : null;
433
+ const ar = ref;
434
+ if (typeof ar.id === "string" && ar.id.length > 0) return ar.id;
435
+ return null;
436
+ }
437
+ /**
438
+ * Compute the anchor rect for a Box that declares `anchorRef`. The registered
439
+ * rect is the Box's `contentRect` — the inner area inside border + padding
440
+ * — which is the canonical origin for placement math. Edge-specific rects
441
+ * (top/bottom/left/right) are derived by `placeFloating` at consumption time
442
+ * rather than baked into the registry.
443
+ *
444
+ * Returns `null` when:
445
+ * - the node has no `anchorRef` BoxProp (or it's empty), OR
446
+ * - `contentRect` is unavailable (pre-layout / clipped to zero size).
447
+ *
448
+ * Phase 4c of `km-silvery.view-as-layout-output` (overlay-anchor v1).
449
+ */
450
+ function computeAnchorRect(node) {
451
+ if (resolveAnchorId(node) === null) return null;
452
+ return computeContentRect(node);
453
+ }
454
+ /**
455
+ * Compute the absolute HARDWARE-PARK rect for a node from its `parkOffset` prop
456
+ * and current `contentRect`. The park rect is the 1×1 cell where a managed
457
+ * terminal frame parks (then hides) the hardware cursor when this node owns the
458
+ * frame — position-only (the visible caret is `cursorOffset`/`computeCursorRect`'s
459
+ * job, so `visible`/`shape` on the offset are ignored here).
460
+ *
461
+ * UNLIKE `computeCursorRect`, this is consumed **non-focus-gated** (see
462
+ * `findActiveParkRect`): an editable declares its input cell whether or not it
463
+ * is focused, so a managed frame ALWAYS has a benign park cell and never falls
464
+ * back to the box origin or home (@km/code/v0.2/19702). Mirrors
465
+ * `computeAnchorRect`'s content-origin delegation.
466
+ *
467
+ * Returns null when the node has no `parkOffset` prop or `contentRect` is
468
+ * unavailable (pre-layout / clipped to zero size).
469
+ */
470
+ function computeParkRect(node) {
471
+ const offset = node.props?.parkOffset;
472
+ if (!offset) return null;
473
+ const content = computeContentRect(node);
474
+ if (!content) return null;
475
+ return {
476
+ x: content.x + offset.col,
477
+ y: content.y + offset.row,
478
+ width: 1,
479
+ height: 1
480
+ };
481
+ }
482
+ /**
483
+ * Walk the tree and return the deepest declared hardware-park rect, or null when
484
+ * no node declares `parkOffset`. Post-order deepest-wins (mirrors `findAnchor`),
485
+ * and deliberately **non-focus-gated and non-visibility-gated** — the park
486
+ * target exists regardless of focus so `managedCursorSuffix` always has a benign
487
+ * cell to park the hardware cursor at, never the box origin / `home(0,0)`
488
+ * (@km/code/v0.2/19702). Signal-or-compute per node so it works before signals
489
+ * are allocated (first frame after mount).
490
+ */
491
+ function findActiveParkRect(root) {
492
+ let result = null;
493
+ function walk(node) {
494
+ for (const child of node.children) walk(child);
495
+ if (!node.props?.parkOffset) return;
496
+ const s = signalMap.get(node);
497
+ const rect = s ? s.parkRect() : computeParkRect(node);
498
+ if (rect) result = rect;
499
+ }
500
+ walk(root);
501
+ return result;
502
+ }
503
+ /**
504
+ * Walk the tree and find the rect for an anchor by id. Returns `null` when
505
+ * no Box declares `anchorRef` with a matching id, or when the matching Box's
506
+ * `contentRect` is unavailable this frame (pre-layout / clipped).
507
+ *
508
+ * **Optional `edge` parameter** — when supplied, returns a 1-cell-thick rect
509
+ * along the requested edge of the anchor (`top`, `bottom`, `left`, `right`).
510
+ * Convenience for callers that want to draw against a specific edge without
511
+ * threading the full content rect through `placeFloating`. Without `edge`,
512
+ * returns the full content rect.
513
+ *
514
+ * **Walk order**: post-order (deepest-first). If two anchors share an id —
515
+ * the contract says they shouldn't, but the substrate doesn't enforce
516
+ * uniqueness — the deeper / later-rendered one wins. This matches the
517
+ * deepest-wins precedence used by cursor and focus walks.
518
+ *
519
+ * Per-node cost: one `props.anchorRef` check + one signal lookup (or one
520
+ * direct compute when no signal is allocated). Trees with no anchors return
521
+ * `null` after a single traversal.
522
+ *
523
+ * Phase 4c of `km-silvery.view-as-layout-output` (overlay-anchor v1).
524
+ */
525
+ function findAnchor(root, id, edge) {
526
+ let result = null;
527
+ function walk(node) {
528
+ for (const child of node.children) walk(child);
529
+ if (resolveAnchorId(node) !== id) return;
530
+ const s = signalMap.get(node);
531
+ const rect = s ? s.anchorRect() : computeAnchorRect(node);
532
+ if (rect) result = rect;
533
+ }
534
+ walk(root);
535
+ if (result === null || edge === void 0) return result;
536
+ const r = result;
537
+ switch (edge) {
538
+ case "top": return {
539
+ x: r.x,
540
+ y: r.y,
541
+ width: r.width,
542
+ height: 1
543
+ };
544
+ case "bottom": return {
545
+ x: r.x,
546
+ y: r.y + Math.max(0, r.height - 1),
547
+ width: r.width,
548
+ height: 1
549
+ };
550
+ case "left": return {
551
+ x: r.x,
552
+ y: r.y,
553
+ width: 1,
554
+ height: r.height
555
+ };
556
+ case "right": return {
557
+ x: r.x + Math.max(0, r.width - 1),
558
+ y: r.y,
559
+ width: 1,
560
+ height: r.height
561
+ };
562
+ }
563
+ }
564
+ /**
565
+ * Compute the resolved decoration rects for a node based on its `decorations`
566
+ * BoxProp. Each entry produces one `DecorationRect` whose `rects` may be empty
567
+ * when an anchor lookup fails.
568
+ *
569
+ * **Behavior by kind**:
570
+ * - `popover` / `tooltip`: requires `anchorId` + `placement` + `size`. The
571
+ * anchor rect is looked up via `findAnchor(root, anchorId)`; if found and
572
+ * all required fields are present, `placeFloating` produces the placed
573
+ * rect. Missing anchor or missing required fields → empty rect list.
574
+ * - `highlight`: the `rect` field, if present, is translated from
575
+ * content-relative coordinates into absolute terminal coordinates by
576
+ * adding the owning Box's `contentRect.{x, y}`. Missing rect or no
577
+ * contentRect → empty rect list.
578
+ *
579
+ * **Per-frame**: this runs in the layout-phase notify pass, so anchor rects
580
+ * are populated for the same frame. Anchors declared deeper in the tree
581
+ * resolve correctly because the function takes the root tree as input rather
582
+ * than relying on a separately-built map.
583
+ *
584
+ * Phase 4c of `km-silvery.view-as-layout-output` (overlay-anchor v1).
585
+ */
586
+ function computeDecorationRects(node, root) {
587
+ const decos = node.props?.decorations;
588
+ if (!decos || decos.length === 0) return EMPTY_DECORATION_RECTS;
589
+ const out = [];
590
+ const content = computeContentRect(node);
591
+ for (const d of decos) if (d.kind === "popover" || d.kind === "tooltip") {
592
+ if (!d.anchorId || !d.placement || !d.size) {
593
+ out.push({
594
+ kind: d.kind,
595
+ id: d.id,
596
+ rects: []
597
+ });
598
+ continue;
599
+ }
600
+ const anchor = findAnchor(root, d.anchorId);
601
+ if (!anchor) {
602
+ out.push({
603
+ kind: d.kind,
604
+ id: d.id,
605
+ rects: []
606
+ });
607
+ continue;
608
+ }
609
+ const placed = resolveFloatingPlacement(anchor, d.size, d.placement, {
610
+ offset: d.offset,
611
+ alignOffset: d.alignOffset,
612
+ collisionStrategy: d.collisionStrategy,
613
+ boundary: root.boxRect
614
+ });
615
+ out.push({
616
+ kind: d.kind,
617
+ id: d.id,
618
+ rects: placed ? [placed.rect] : []
619
+ });
620
+ } else if (d.kind === "highlight") {
621
+ if (!d.rect || !content) {
622
+ out.push({
623
+ kind: d.kind,
624
+ id: d.id,
625
+ rects: []
626
+ });
627
+ continue;
628
+ }
629
+ out.push({
630
+ kind: d.kind,
631
+ id: d.id,
632
+ rects: [{
633
+ x: content.x + d.rect.x,
634
+ y: content.y + d.rect.y,
635
+ width: d.rect.width,
636
+ height: d.rect.height
637
+ }]
638
+ });
639
+ }
640
+ return out.length === 0 ? EMPTY_DECORATION_RECTS : out;
641
+ }
642
+ /**
643
+ * Per-field equality on a list of `DecorationRect`. Used to skip
644
+ * `decorationRects` signal writes when nothing changed — mirrors the
645
+ * `selectionFragmentsEqual` pattern.
646
+ */
647
+ function decorationRectsEqual(a, b) {
648
+ if (a === b) return true;
649
+ if (a.length !== b.length) return false;
650
+ for (let i = 0; i < a.length; i++) {
651
+ const ai = a[i];
652
+ const bi = b[i];
653
+ if (ai.kind !== bi.kind) return false;
654
+ if (ai.id !== bi.id) return false;
655
+ if (ai.rects.length !== bi.rects.length) return false;
656
+ for (let j = 0; j < ai.rects.length; j++) if (!rectEqual$1(ai.rects[j] ?? null, bi.rects[j] ?? null)) return false;
657
+ }
658
+ return true;
659
+ }
660
+ /**
661
+ * Collect the textual content of a selection-declaring Box.
662
+ *
663
+ * The selection-fragment math operates on the rendered text content of the
664
+ * owning Box — `selectionIntent.{from,to}` are character offsets into this
665
+ * string. For Box nodes, the canonical content is the concatenation of
666
+ * descendant `silvery-text` nodes' `textContent` (in tree order), with `\n`
667
+ * separators between adjacent Text/Box children that introduce visual line
668
+ * breaks.
669
+ *
670
+ * v1 behaviour (kept intentionally minimal):
671
+ * - A Box with `silvery-text` children: concatenates `textContent` strings
672
+ * from those children. Two adjacent text children produce one logical
673
+ * line; if you want a line break, embed `\n` in the text.
674
+ * - A Box with mixed children: same — only `silvery-text` descendants
675
+ * contribute. Nested Box children don't add line breaks (they're treated
676
+ * as transparent for content purposes).
677
+ * - A `silvery-text` node directly carrying the prop: its own `textContent`
678
+ * is the content.
679
+ *
680
+ * This keeps the v1 model honest: declare `selectionIntent` on a Box (or
681
+ * Text) whose text content is the source of truth for the selection. Apps
682
+ * that want per-line semantics can split the selection across multiple
683
+ * intent declarations.
684
+ */
685
+ function collectSelectionText(node) {
686
+ if (node.type === "silvery-text") return node.textContent ?? "";
687
+ let out = "";
688
+ const stack = [node];
689
+ while (stack.length) {
690
+ const cur = stack.pop();
691
+ for (let i = cur.children.length - 1; i >= 0; i--) {
692
+ const child = cur.children[i];
693
+ if (child) stack.push(child);
694
+ }
695
+ if (cur === node) continue;
696
+ if (cur.type === "silvery-text" && cur.textContent !== void 0) out += cur.textContent;
697
+ }
698
+ return out;
699
+ }
700
+ /**
701
+ * Compute the geometric fragments for a node's `selectionIntent` — the list
702
+ * of rectangles (one per visual line spanned) that the selection renderer
703
+ * should paint with highlight bg this frame.
704
+ *
705
+ * Returns:
706
+ * - `[]` when the node has no `selectionIntent` prop, or when the intent is
707
+ * collapsed (`from === to`), or when the content rect is unavailable
708
+ * (pre-layout / clipped to zero size).
709
+ * - `[Rect]` for a single-visual-line selection.
710
+ * - `[Rect, Rect, ...]` for multi-line selections (split per visual line).
711
+ *
712
+ * **Geometry** (mirrors text-editor / ProseMirror conventions):
713
+ * - First line: from `(content.x + fromCol, content.y + fromLine)` to the
714
+ * end of the line. If single-line, runs to `toCol`.
715
+ * - Middle lines: full content-rect width, one row each.
716
+ * - Last line: from `(content.x, content.y + toLine)` to `toCol` chars.
717
+ *
718
+ * Coordinates are absolute terminal cells, matching `cursorRect`'s
719
+ * coordinate space. Width is in cells (one rect per visual line).
720
+ *
721
+ * **Soft-wrap awareness (Option B)**: when a wrap measurer is registered
722
+ * via `setWrapMeasurer({ wrapText })` AND the content rect width is known,
723
+ * this function splits on the measurer's per-visual-line slices — a
724
+ * 60-char paragraph wrapped at width 20 produces 3 fragments rather than
725
+ * one wide rectangle. The terminal runtime (`@silvery/ag-term`) registers
726
+ * its grapheme-aware `wrapText` at startup; pure `@silvery/ag` consumers
727
+ * (no terminal) fall back to `\n`-only splitting which preserves the
728
+ * pre-Option-B behavior bit-for-bit. See `wrap-measurer.ts` for the
729
+ * registry contract. Closes Phase 4b deferred wrap-spanning (bead
730
+ * `km-silvery.softwrap-selection-fragments`).
731
+ */
732
+ function computeSelectionFragments(node) {
733
+ const intent = node.props?.selectionIntent;
734
+ if (!intent) return EMPTY_FRAGMENTS;
735
+ if (intent.from >= intent.to) return EMPTY_FRAGMENTS;
736
+ const content = computeContentRect(node);
737
+ if (!content) return EMPTY_FRAGMENTS;
738
+ const text = collectSelectionText(node);
739
+ if (text.length === 0) return EMPTY_FRAGMENTS;
740
+ const measurer = getWrapMeasurer();
741
+ const visualLines = measurer !== null && content.width > 0 ? buildVisualLinesWithMeasurer(text, content.width, measurer.wrapText) : buildVisualLinesNewlineOnly(text);
742
+ const fragments = [];
743
+ for (let i = 0; i < visualLines.length; i++) {
744
+ const line = visualLines[i];
745
+ if (line.endOffset <= intent.from) continue;
746
+ if (line.startOffset >= intent.to) break;
747
+ const localFrom = Math.max(0, intent.from - line.startOffset);
748
+ const localTo = Math.min(line.text.length, intent.to - line.startOffset);
749
+ const width = Math.max(0, localTo - localFrom);
750
+ if (width === 0) continue;
751
+ fragments.push({
752
+ x: content.x + localFrom,
753
+ y: content.y + i,
754
+ width,
755
+ height: 1
756
+ });
757
+ }
758
+ return fragments.length === 0 ? EMPTY_FRAGMENTS : fragments;
759
+ }
760
+ /**
761
+ * Walk paragraphs (split on `\n`) through the registered wrap measurer to
762
+ * produce per-visual-line slices. When a paragraph fits within the width
763
+ * unchanged, the measurer returns `[]` — we synthesize a single-slice
764
+ * passthrough so the downstream loop sees uniform input.
765
+ *
766
+ * Maintains the invariant that visual-line offsets are monotone and cover
767
+ * the full input (including the `\n` terminator counted as a zero-width
768
+ * boundary so cross-paragraph selections stay aligned).
769
+ */
770
+ function buildVisualLinesWithMeasurer(text, width, wrapText) {
771
+ const out = [];
772
+ let paraStart = 0;
773
+ for (let i = 0; i <= text.length; i++) {
774
+ const isEnd = i === text.length;
775
+ const isNewline = !isEnd && text.charCodeAt(i) === 10;
776
+ if (!isEnd && !isNewline) continue;
777
+ const para = text.slice(paraStart, i);
778
+ const slices = wrapText(para, width);
779
+ if (slices.length === 0) out.push({
780
+ text: para,
781
+ startOffset: paraStart,
782
+ endOffset: paraStart + para.length
783
+ });
784
+ else for (const slice of slices) out.push({
785
+ text: slice.text,
786
+ startOffset: paraStart + slice.startOffset,
787
+ endOffset: paraStart + slice.endOffset
788
+ });
789
+ paraStart = i + 1;
790
+ }
791
+ return out;
792
+ }
793
+ /**
794
+ * Fallback: split on `\n` only. Preserves pre-Option-B geometry exactly so
795
+ * unit tests that exercise the framework-only layer (no terminal Term
796
+ * registered) keep passing without changes.
797
+ *
798
+ * The `endOffset` of each line is the position of the `\n` (or `text.length`
799
+ * for the trailing line) — this matches the convention used by
800
+ * `buildVisualLinesWithMeasurer`, where the newline is a zero-width
801
+ * paragraph boundary rather than a visual line of its own.
802
+ */
803
+ function buildVisualLinesNewlineOnly(text) {
804
+ const out = [];
805
+ let lineStart = 0;
806
+ for (let i = 0; i <= text.length; i++) if (i === text.length || text.charCodeAt(i) === 10) {
807
+ out.push({
808
+ text: text.slice(lineStart, i),
809
+ startOffset: lineStart,
810
+ endOffset: i
811
+ });
812
+ lineStart = i + 1;
813
+ }
814
+ return out;
815
+ }
816
+ /**
817
+ * Project AgNode.scrollState → ScrollStateSnapshot (the subset the virtualizer
818
+ * needs). Returns null if the node has no scroll state yet (non-scroll
819
+ * containers or fresh scroll containers pre-layout).
820
+ *
821
+ * Keeping this projection tight means callers can compare snapshots by
822
+ * per-field equality without pulling the mutable underlying object into
823
+ * consumer code.
824
+ */
825
+ function snapshotScrollState(node) {
826
+ const ss = node.scrollState;
827
+ if (!ss) return null;
828
+ return {
829
+ offset: ss.offset,
830
+ contentHeight: ss.contentHeight,
831
+ viewportHeight: ss.viewportHeight,
832
+ firstVisibleChild: ss.firstVisibleChild,
833
+ lastVisibleChild: ss.lastVisibleChild,
834
+ hiddenAbove: ss.hiddenAbove,
835
+ hiddenBelow: ss.hiddenBelow
836
+ };
837
+ }
838
+ /** Per-field equality check for ScrollStateSnapshot (skips allocation). */
839
+ function scrollStateEqual(a, b) {
840
+ if (a === b) return true;
841
+ if (!a || !b) return false;
842
+ return a.offset === b.offset && a.contentHeight === b.contentHeight && a.viewportHeight === b.viewportHeight && a.firstVisibleChild === b.firstVisibleChild && a.lastVisibleChild === b.lastVisibleChild && a.hiddenAbove === b.hiddenAbove && a.hiddenBelow === b.hiddenBelow;
843
+ }
844
+ /** Check whether a node has signals allocated (for testing). */
845
+ function hasLayoutSignals(node) {
846
+ return signalMap.has(node);
847
+ }
848
+ function hasObservedLayoutSignal(node, key) {
849
+ return observedLayoutSignals.get(node)?.has(key) ?? false;
850
+ }
851
+ /**
852
+ * Sync all rect signals from the node's current values.
853
+ *
854
+ * Called from notifyLayoutSubscribers after layout + scroll + sticky
855
+ * phases complete. Only syncs nodes that have signals allocated.
856
+ * Reference-equality check prevents unnecessary downstream updates.
857
+ */
858
+ function syncRectSignals(node) {
859
+ const props = node.props ?? void 0;
860
+ const hasCursorOffset = !!props?.cursorOffset;
861
+ const hasFocused = !!props?.focused;
862
+ const hasSelectionIntent = !!props?.selectionIntent;
863
+ const hasAnchorRef = !!props?.anchorRef;
864
+ const hasDecorations = !!(props?.decorations && props.decorations.length > 0);
865
+ const s = hasCursorOffset || hasFocused || hasSelectionIntent || hasAnchorRef || hasDecorations ? getLayoutSignals(node) : signalMap.get(node);
866
+ if (!s) return;
867
+ if (node.boxRect !== s.boxRect()) s.boxRect(node.boxRect);
868
+ if (node.scrollRect !== s.scrollRect()) s.scrollRect(node.scrollRect);
869
+ if (node.screenRect !== s.screenRect()) s.screenRect(node.screenRect);
870
+ const nextContentRect = computeContentRect(node);
871
+ if (!rectEqual$1(nextContentRect, s.contentRect())) s.contentRect(nextContentRect);
872
+ const nextCursorRect = computeCursorRect(node);
873
+ if (!cursorRectEqual(nextCursorRect, s.cursorRect())) s.cursorRect(nextCursorRect);
874
+ const nextFocusedId = computeFocusedNodeId(node);
875
+ if (nextFocusedId !== s.focusedNodeId()) s.focusedNodeId(nextFocusedId);
876
+ const nextFragments = computeSelectionFragments(node);
877
+ if (!selectionFragmentsEqual(nextFragments, s.selectionFragments())) s.selectionFragments(nextFragments);
878
+ const nextAnchorRect = computeAnchorRect(node);
879
+ if (!rectEqual$1(nextAnchorRect, s.anchorRect())) s.anchorRect(nextAnchorRect);
880
+ const nextParkRect = computeParkRect(node);
881
+ if (!rectEqual$1(nextParkRect, s.parkRect())) s.parkRect(nextParkRect);
882
+ const nextScrollState = snapshotScrollState(node);
883
+ if (!scrollStateEqual(nextScrollState, s.scrollState())) s.scrollState(nextScrollState);
884
+ }
885
+ /**
886
+ * Second-pass sync for `decorationRects` — must run AFTER `syncRectSignals`
887
+ * has populated every anchor rect this frame, because decoration resolution
888
+ * calls `findAnchor(root, id)` and needs the freshest anchor rects.
889
+ *
890
+ * Walks the tree, recomputes per-node decoration rects, and writes the signal
891
+ * only when the result differs (per-field equality via `decorationRectsEqual`).
892
+ *
893
+ * Phase 4c of `km-silvery.view-as-layout-output` (overlay-anchor v1).
894
+ *
895
+ * Per-node cost: one `props.decorations` length check (zero-allocation
896
+ * short-circuit) + one signal lookup + one decoration recompute when present.
897
+ * Trees without decorations pay only the prop check at every node.
898
+ */
899
+ function syncDecorationRects(root) {
900
+ function walk(node) {
901
+ const props = node.props;
902
+ if (!!(props?.decorations && props.decorations.length > 0)) {
903
+ const s = getLayoutSignals(node);
904
+ const next = computeDecorationRects(node, root);
905
+ if (!decorationRectsEqual(next, s.decorationRects())) s.decorationRects(next);
906
+ } else {
907
+ const s = signalMap.get(node);
908
+ if (s && s.decorationRects().length > 0) s.decorationRects(EMPTY_DECORATION_RECTS);
909
+ }
910
+ for (const child of node.children) walk(child);
911
+ }
912
+ walk(root);
913
+ }
914
+ /**
915
+ * Promote the in-flight rect signals (`boxRect` / `scrollRect` / `screenRect`)
916
+ * to their committed counterparts (`boxRectCommitted` / etc.). Reactive
917
+ * `useBoxRect()` / `useScrollRect()` / `useScreenRect()` consumers subscribe
918
+ * to the committed signals — calling this advances them by one frame.
919
+ *
920
+ * Called by the runtime ONCE per event-batch commit, after the convergence
921
+ * loop has fully drained. Within a single batch, multiple convergence passes
922
+ * may write the in-flight signals (callback-form observers fire each time),
923
+ * but the committed signals advance only here. That's what lets a render
924
+ * which both READS `useBoxRect()` and WRITES a layout-affecting prop converge
925
+ * in one pass: the read returns the same value across every pass in the
926
+ * batch, so the write is idempotent.
927
+ *
928
+ * Reference equality on the underlying alien-signal write means a no-op
929
+ * commit (same rect as last frame) does not fire any subscribers — steady
930
+ * state pays no cost.
931
+ *
932
+ * The walker visits only nodes that already have allocated `LayoutSignals`
933
+ * (i.e. nodes with at least one consumer); a tree with no rect subscribers
934
+ * pays only the WeakMap probe per node.
935
+ *
936
+ * **Return value**: `true` when the commit promoted at least one committed
937
+ * rect signal — i.e. it fired at least one reactive subscriber's forceUpdate.
938
+ * Those forceUpdates are React-scheduled (deferred-lane) updates that do NOT
939
+ * surface as the runtime's `pendingRerender` flag and are NOT drained by
940
+ * `reconciler.flushSyncWork()`; only a subsequent `doRender()`
941
+ * (`updateContainerSync`) processes them. The runtime's post-commit drain uses
942
+ * this return so it can run the documented "exactly one additional pass" that
943
+ * paints the subscriber update WITHIN the same event — instead of leaking a
944
+ * stale frame to a later macrotask (the @si/render/19436 boxSize signature).
945
+ *
946
+ * See bead `@km/silvery/use-deferred-box-rect-and-post-commit-observers`.
947
+ */
948
+ function commitLayoutSnapshot(root) {
949
+ let promoted = false;
950
+ function walk(node) {
951
+ const s = signalMap.get(node);
952
+ if (s) {
953
+ const nextBox = s.boxRect();
954
+ if (!rectEqual$1(nextBox, s.boxRectCommitted())) {
955
+ s.boxRectCommitted(nextBox);
956
+ if (hasObservedLayoutSignal(node, "boxRect") || hasObservedLayoutSignal(node, "boxSize")) promoted = true;
957
+ }
958
+ const nextScroll = s.scrollRect();
959
+ if (!rectEqual$1(nextScroll, s.scrollRectCommitted())) {
960
+ s.scrollRectCommitted(nextScroll);
961
+ if (hasObservedLayoutSignal(node, "scrollRect")) promoted = true;
962
+ }
963
+ const nextScreen = s.screenRect();
964
+ if (!rectEqual$1(nextScreen, s.screenRectCommitted())) {
965
+ s.screenRectCommitted(nextScreen);
966
+ if (hasObservedLayoutSignal(node, "screenRect")) promoted = true;
967
+ }
968
+ }
969
+ for (const child of node.children) walk(child);
970
+ }
971
+ walk(root);
972
+ return promoted;
973
+ }
974
+ /**
975
+ * Walk the tree and find the active caret rect — the caret to render this
976
+ * frame, applying the precedence + clipping rules locked by bead
977
+ * `km-silvery.cursor-invariants`. Returns null when no cursor owner is
978
+ * present; focused hidden owners return a rect with `visible: false`.
979
+ *
980
+ * **Precedence (invariant 1)**:
981
+ * 1. **Focused cursor owner wins**: a Box with `cursorOffset` AND either
982
+ * `focused === true` or `interactiveState.focused === true` wins even
983
+ * when `cursorOffset.visible === false`. If the rect is visible,
984
+ * renderers show it; if it is hidden, renderers move there and hide the
985
+ * hardware cursor. Either way it suppresses non-focused fallback cursors.
986
+ * 2. **Otherwise deepest visible in paint order**: if no node is a focused
987
+ * cursor owner, fall back to the deepest visible declarer (post-order
988
+ * tree walk). This covers Ink-compat consumers and `useCursor` callers
989
+ * that don't participate in the focus tree.
990
+ * 3. **Otherwise null**: no visible cursor owner is present.
991
+ *
992
+ * **Clipping (invariant 4)**: at each scroll/clip ancestor (a Box with
993
+ * `overflow="scroll"` / `"hidden"` / `overflowY="hidden"`), the caret's
994
+ * position is checked against the ancestor's `scrollRect`. If the caret
995
+ * falls outside the visible region, the caret is treated as not-present.
996
+ * Default behavior is **hide** (no caret ANSI emitted) — never clamp. A
997
+ * caret rect at the exact clip edge is treated as visible.
998
+ *
999
+ * Visited in tree order (depth-first, post-order). Per-node cost is one
1000
+ * `props.cursorOffset` check + one signal lookup; trees without any cursor
1001
+ * declarer return null after a single traversal.
1002
+ */
1003
+ function findActiveCursorRect(root) {
1004
+ return findActiveCursorRectWithProvenance(root)?.rect ?? null;
1005
+ }
1006
+ /**
1007
+ * Walk the tree once and return the active cursor rect WITH its provenance.
1008
+ * `findActiveCursorRect` is the value-only projection of this. See
1009
+ * `CursorProvenance` for the source categories and why the terminal layer needs
1010
+ * them (one walk, no parallel island-blind re-derivation in the compositing
1011
+ * gate). Precedence and clipping match `findActiveCursorRect`'s documented
1012
+ * invariants exactly — this IS that walk.
1013
+ */
1014
+ function findActiveCursorRectWithProvenance(root) {
1015
+ let focusedResult = null;
1016
+ let focusedSuppressesFallback = false;
1017
+ let fallbackResult = null;
1018
+ let fallbackProvenance = "declarative-fallback";
1019
+ const clipStack = [];
1020
+ function isClipped(rect) {
1021
+ for (let i = clipStack.length - 1; i >= 0; i--) {
1022
+ const clip = clipStack[i];
1023
+ if (!clip) continue;
1024
+ if (rect.x < clip.x || rect.y < clip.y || rect.x >= clip.x + clip.width || rect.y >= clip.y + clip.height) return true;
1025
+ }
1026
+ return false;
1027
+ }
1028
+ function isClipAncestor(node) {
1029
+ const props = node.props;
1030
+ if (!props) return false;
1031
+ if (props.overflow === "scroll" || props.overflow === "hidden") return true;
1032
+ if (props.overflowY === "hidden") return true;
1033
+ return false;
1034
+ }
1035
+ function walk(node) {
1036
+ const isClip = isClipAncestor(node);
1037
+ if (isClip) clipStack.push(node.scrollRect ?? null);
1038
+ for (const child of node.children) walk(child);
1039
+ const props = node.props;
1040
+ if (props?.cursorOffset) {
1041
+ const s = signalMap.get(node);
1042
+ const rect = s ? s.cursorRect() : computeCursorRect(node);
1043
+ const isFocusedCursorOwner = props.focused === true || node.interactiveState?.focused === true;
1044
+ if (rect) {
1045
+ const clipped = isClipped(rect);
1046
+ if (isFocusedCursorOwner) {
1047
+ focusedSuppressesFallback = true;
1048
+ if (!clipped) focusedResult = rect;
1049
+ }
1050
+ if (!clipped) {
1051
+ if (rect.visible) {
1052
+ fallbackResult = rect;
1053
+ fallbackProvenance = "declarative-fallback";
1054
+ }
1055
+ }
1056
+ } else if (isFocusedCursorOwner) focusedSuppressesFallback = true;
1057
+ }
1058
+ if (node.type === "silvery-island" && node.islandState?.cursorActive) {
1059
+ const out = node.islandState.handle?.output;
1060
+ const cur = out?.cursor;
1061
+ const base = node.screenRect ?? node.boxRect;
1062
+ if (cur && out?.cursorVisible && base) {
1063
+ const rect = {
1064
+ x: base.x + cur.col,
1065
+ y: base.y + cur.row,
1066
+ visible: true,
1067
+ shape: cur.style
1068
+ };
1069
+ if (!focusedSuppressesFallback && !isClipped(rect)) {
1070
+ fallbackResult = rect;
1071
+ fallbackProvenance = "island";
1072
+ }
1073
+ }
1074
+ }
1075
+ if (isClip) clipStack.pop();
1076
+ }
1077
+ walk(root);
1078
+ if (focusedResult) return {
1079
+ rect: focusedResult,
1080
+ provenance: "focused-declarative"
1081
+ };
1082
+ if (focusedSuppressesFallback || !fallbackResult) return null;
1083
+ return {
1084
+ rect: fallbackResult,
1085
+ provenance: fallbackProvenance
1086
+ };
1087
+ }
1088
+ /**
1089
+ * Sync textContent signal from the node's current value.
1090
+ *
1091
+ * Called from commitTextUpdate in the reconciler.
1092
+ */
1093
+ function syncTextContentSignal(node) {
1094
+ const s = signalMap.get(node);
1095
+ if (!s) return;
1096
+ if (node.textContent !== s.textContent()) s.textContent(node.textContent);
1097
+ }
1098
+ /**
1099
+ * Sync focused signal for a node.
1100
+ *
1101
+ * Called from FocusManager when focus changes.
1102
+ */
1103
+ function syncFocusedSignal(node, focused) {
1104
+ const s = signalMap.get(node);
1105
+ if (!s) return;
1106
+ if (focused !== s.focused()) s.focused(focused);
1107
+ }
1108
+ //#endregion
1109
+ export { rectEqual$1 as _, findActiveParkRect as a, hasObservedLayoutSignal as c, syncDecorationRects as d, syncFocusedSignal as f, setWrapMeasurer as g, getWrapMeasurer as h, findActiveCursorRectWithProvenance as i, markObservedLayoutSignal as l, syncTextContentSignal as m, computeContentRect as n, getLayoutSignals as o, syncRectSignals as p, findActiveCursorRect as r, hasLayoutSignals as s, commitLayoutSnapshot as t, observeLayoutSignal as u };
1110
+
1111
+ //# sourceMappingURL=layout-signals-Dch2EiCy.mjs.map