silvery 0.19.2 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/README.md +9 -4
  2. package/dist/Text-Lq0dmj8-.mjs +239 -0
  3. package/dist/Text-Lq0dmj8-.mjs.map +1 -0
  4. package/dist/UPNG-Bo33r8rA.mjs +3 -0
  5. package/dist/UPNG-DosRPdF4.mjs +5075 -0
  6. package/dist/UPNG-DosRPdF4.mjs.map +1 -0
  7. package/dist/__vite-browser-external-2447137e-D_JM6skp.mjs +6 -0
  8. package/dist/__vite-browser-external-2447137e-D_JM6skp.mjs.map +1 -0
  9. package/dist/{animation-Cn64yepo.mjs → animation-ZMN2_XKv.mjs} +2 -2
  10. package/dist/animation-ZMN2_XKv.mjs.map +1 -0
  11. package/dist/{ansi-Cc33mW54.d.mts → ansi-2Xn0yatP.d.mts} +1 -1
  12. package/dist/{ansi-Cc33mW54.d.mts.map → ansi-2Xn0yatP.d.mts.map} +1 -1
  13. package/dist/{ansi-CLOitHKx.mjs → ansi-D1KQMAbf.mjs} +1 -1
  14. package/dist/{ansi-CLOitHKx.mjs.map → ansi-D1KQMAbf.mjs.map} +1 -1
  15. package/dist/ansi-yC4RyBNY.mjs +22441 -0
  16. package/dist/ansi-yC4RyBNY.mjs.map +1 -0
  17. package/dist/apng-CR08rIaH.mjs +58 -0
  18. package/dist/apng-CR08rIaH.mjs.map +1 -0
  19. package/dist/apng-DaHfVaVI.mjs +3 -0
  20. package/dist/assets/resvgjs.darwin-arm64-BtufyGW1.node +0 -0
  21. package/dist/assets/skia.darwin-arm64-DQs5sT6N.node +0 -0
  22. package/dist/backend-B-WYLUib.mjs +13396 -0
  23. package/dist/backend-B-WYLUib.mjs.map +1 -0
  24. package/dist/backends-CUtan80W.mjs +3 -0
  25. package/dist/backends-DIVYzKqd.mjs +1083 -0
  26. package/dist/backends-DIVYzKqd.mjs.map +1 -0
  27. package/dist/bound-term-0sPrrzH1.d.mts +4640 -0
  28. package/dist/bound-term-0sPrrzH1.d.mts.map +1 -0
  29. package/dist/canvas-1v7dPT-_.mjs +3 -0
  30. package/dist/canvas-CSuPOMNt.mjs +1442 -0
  31. package/dist/canvas-CSuPOMNt.mjs.map +1 -0
  32. package/dist/{chunk-Vs_PY4HZ.mjs → chunk-BSw8zbkd.mjs} +1 -1
  33. package/dist/cli-dvo0r2fs.mjs +4 -0
  34. package/dist/compare-CQodSH4G.mjs +376 -0
  35. package/dist/compare-CQodSH4G.mjs.map +1 -0
  36. package/dist/compare-DHlcxEYA.mjs +3 -0
  37. package/dist/context-BU5LkkIy.mjs.map +1 -1
  38. package/dist/devtools-CJdt5H0X.mjs +2 -0
  39. package/dist/{devtools-DxkSLXDA.mjs → devtools-DcQjgyjL.mjs} +5 -4
  40. package/dist/{devtools-DxkSLXDA.mjs.map → devtools-DcQjgyjL.mjs.map} +1 -1
  41. package/dist/easing-BI-ASGMO.d.mts +24 -0
  42. package/dist/easing-BI-ASGMO.d.mts.map +1 -0
  43. package/dist/{eta-Bb3RH3wh.mjs → eta-CJlGH06n.mjs} +1 -1
  44. package/dist/{eta-Bb3RH3wh.mjs.map → eta-CJlGH06n.mjs.map} +1 -1
  45. package/dist/flexily-zero-adapter-C3Vj0fPt.mjs +306 -0
  46. package/dist/flexily-zero-adapter-C3Vj0fPt.mjs.map +1 -0
  47. package/dist/{flexily-zero-adapter-CMxXhdOL.mjs → flexily-zero-adapter-C4lW_Ov5.mjs} +1 -1
  48. package/dist/fonts-BFmhXDv7.mjs +88 -0
  49. package/dist/fonts-BFmhXDv7.mjs.map +1 -0
  50. package/dist/gif-C_AjaT9d.mjs +188 -0
  51. package/dist/gif-C_AjaT9d.mjs.map +1 -0
  52. package/dist/gif-DaC4XrxA.mjs +3 -0
  53. package/dist/gifenc-BOUT-KFB.mjs +730 -0
  54. package/dist/gifenc-BOUT-KFB.mjs.map +1 -0
  55. package/dist/image-C2Birh2x.mjs +1252 -0
  56. package/dist/image-C2Birh2x.mjs.map +1 -0
  57. package/dist/index-BUMxS65f.d.mts +453 -0
  58. package/dist/index-BUMxS65f.d.mts.map +1 -0
  59. package/dist/{index-D3saHouR.d.mts → index-CSQf13CI.d.mts} +1057 -1133
  60. package/dist/index-CSQf13CI.d.mts.map +1 -0
  61. package/dist/{index-BXslOebb.d.mts → index-Cl9KKjQ_.d.mts} +4919 -3921
  62. package/dist/index-Cl9KKjQ_.d.mts.map +1 -0
  63. package/dist/index-XbNrPhWl.d.mts +336 -0
  64. package/dist/index-XbNrPhWl.d.mts.map +1 -0
  65. package/dist/index.d.mts +8 -5
  66. package/dist/index.d.mts.map +1 -1
  67. package/dist/index.mjs +14 -12
  68. package/dist/index.mjs.map +1 -1
  69. package/dist/key-mapping-CS-YD_cD.mjs +132 -0
  70. package/dist/key-mapping-CS-YD_cD.mjs.map +1 -0
  71. package/dist/key-mapping-Yn-Jgrij.mjs +3 -0
  72. package/dist/{layout-engine-B6Cdz1yZ.mjs → layout-engine-C07LEXWT.mjs} +1 -1
  73. package/dist/layout-engine-C2px0RJE.mjs +67 -0
  74. package/dist/layout-engine-C2px0RJE.mjs.map +1 -0
  75. package/dist/layout-signals-Cnw6xk8Q.mjs +988 -0
  76. package/dist/layout-signals-Cnw6xk8Q.mjs.map +1 -0
  77. package/dist/mouse-events-Dki3ISIp.mjs +1044 -0
  78. package/dist/mouse-events-Dki3ISIp.mjs.map +1 -0
  79. package/dist/{multi-progress-Bq9Oi_WI.mjs → multi-progress-CIRjrzma.mjs} +3 -3
  80. package/dist/{multi-progress-Bq9Oi_WI.mjs.map → multi-progress-CIRjrzma.mjs.map} +1 -1
  81. package/dist/{multi-progress-DAQC7eap.d.mts → multi-progress-DHZ2xUT2.d.mts} +2 -2
  82. package/dist/{multi-progress-DAQC7eap.d.mts.map → multi-progress-DHZ2xUT2.d.mts.map} +1 -1
  83. package/dist/{node-BeWlnCPY.mjs → node-CjM5Rt-M.mjs} +4 -4
  84. package/dist/node-CjM5Rt-M.mjs.map +1 -0
  85. package/dist/playwright-D5YiZcNS.mjs +76397 -0
  86. package/dist/playwright-D5YiZcNS.mjs.map +1 -0
  87. package/dist/png-codec-Dp84742B.mjs +36 -0
  88. package/dist/png-codec-Dp84742B.mjs.map +1 -0
  89. package/dist/png-codec-QwOtJ8Zs.mjs +3 -0
  90. package/dist/progress-DB_Xo071.mjs +675 -0
  91. package/dist/progress-DB_Xo071.mjs.map +1 -0
  92. package/dist/{progress-bar-CXE5Qfkd.mjs → progress-bar-oJwq22CR.mjs} +4 -4
  93. package/dist/{progress-bar-CXE5Qfkd.mjs.map → progress-bar-oJwq22CR.mjs.map} +1 -1
  94. package/dist/rasterizer-BRXrDdWx.mjs +3 -0
  95. package/dist/rasterizer-CpEhJvdR.mjs +296 -0
  96. package/dist/rasterizer-CpEhJvdR.mjs.map +1 -0
  97. package/dist/reconciler-DldIJB93.mjs +2083 -0
  98. package/dist/reconciler-DldIJB93.mjs.map +1 -0
  99. package/dist/{render-string-CDCeYkS3.mjs → render-string-BcoCpjCB.mjs} +1 -1
  100. package/dist/{render-string-Darrg7ku.mjs → render-string-DkQacASz.mjs} +2707 -549
  101. package/dist/render-string-DkQacASz.mjs.map +1 -0
  102. package/dist/resvg-js-DkOndZI3.mjs +203 -0
  103. package/dist/resvg-js-DkOndZI3.mjs.map +1 -0
  104. package/dist/runtime.d.mts +3 -2
  105. package/dist/runtime.mjs +3 -3
  106. package/dist/schemes-JjNp4aSl.mjs +2611 -0
  107. package/dist/schemes-JjNp4aSl.mjs.map +1 -0
  108. package/dist/{spinner-CGo34vyR.d.mts → spinner-CZINHpkV.d.mts} +2 -2
  109. package/dist/{spinner-CGo34vyR.d.mts.map → spinner-CZINHpkV.d.mts.map} +1 -1
  110. package/dist/{spinner-CeOmcuw_.mjs → spinner-D9lrHr8s.mjs} +7 -7
  111. package/dist/spinner-D9lrHr8s.mjs.map +1 -0
  112. package/dist/src-5w9QR6_8.mjs +1071 -0
  113. package/dist/src-5w9QR6_8.mjs.map +1 -0
  114. package/dist/src-BNTToU7l.mjs +4387 -0
  115. package/dist/src-BNTToU7l.mjs.map +1 -0
  116. package/dist/{src-CF-6UN01.mjs → src-BR4xNwdG.mjs} +10436 -2622
  117. package/dist/src-BR4xNwdG.mjs.map +1 -0
  118. package/dist/{types-Bk2yw9Qj.mjs → src-DKp-_OFG.mjs} +34 -94
  119. package/dist/src-DKp-_OFG.mjs.map +1 -0
  120. package/dist/src-bt8wSrfJ.mjs +258 -0
  121. package/dist/src-bt8wSrfJ.mjs.map +1 -0
  122. package/dist/src-e33Y6kNJ.mjs +3 -0
  123. package/dist/src-iDwu25UD.mjs +1814 -0
  124. package/dist/src-iDwu25UD.mjs.map +1 -0
  125. package/dist/steps-Bp2uNqnn.d.mts +202 -0
  126. package/dist/steps-Bp2uNqnn.d.mts.map +1 -0
  127. package/dist/svg-15lZZzxq.mjs +486 -0
  128. package/dist/svg-15lZZzxq.mjs.map +1 -0
  129. package/dist/svg-Cz0UXcDj.mjs +255 -0
  130. package/dist/svg-Cz0UXcDj.mjs.map +1 -0
  131. package/dist/svg-DY72a4HK.mjs +3 -0
  132. package/dist/svg-g1D6ErwR.d.mts +82 -0
  133. package/dist/svg-g1D6ErwR.d.mts.map +1 -0
  134. package/dist/term.d.mts +3 -0
  135. package/dist/term.mjs +9 -0
  136. package/dist/term.mjs.map +1 -0
  137. package/dist/theme.d.mts +95 -2
  138. package/dist/theme.d.mts.map +1 -0
  139. package/dist/theme.mjs +9 -3
  140. package/dist/theme.mjs.map +1 -0
  141. package/dist/{types-BH_v3iMT.d.mts → types-kt_fKR37.d.mts} +2 -15
  142. package/dist/types-kt_fKR37.d.mts.map +1 -0
  143. package/dist/ui/animation.d.mts +2 -1
  144. package/dist/ui/animation.mjs +1 -1
  145. package/dist/ui/ansi.d.mts +1 -1
  146. package/dist/ui/ansi.mjs +1 -1
  147. package/dist/ui/cli.d.mts +3 -3
  148. package/dist/ui/cli.mjs +5 -5
  149. package/dist/ui/display.d.mts +1 -1
  150. package/dist/ui/image.d.mts +2 -2
  151. package/dist/ui/image.mjs +2 -2
  152. package/dist/ui/input.d.mts +1 -1
  153. package/dist/ui/input.mjs +4 -2
  154. package/dist/ui/input.mjs.map +1 -1
  155. package/dist/ui/progress.d.mts +5 -249
  156. package/dist/ui/progress.mjs +5 -858
  157. package/dist/ui/react.d.mts +1 -1
  158. package/dist/ui/react.mjs +2 -2
  159. package/dist/ui/recording-chrome-react.d.mts +21 -0
  160. package/dist/ui/recording-chrome-react.d.mts.map +1 -0
  161. package/dist/ui/recording-chrome-react.mjs +105 -0
  162. package/dist/ui/recording-chrome-react.mjs.map +1 -0
  163. package/dist/ui/recording-chrome.d.mts +2 -0
  164. package/dist/ui/recording-chrome.mjs +2 -0
  165. package/dist/ui/utils.mjs +1 -1
  166. package/dist/ui/wrappers.d.mts +3 -3
  167. package/dist/ui/wrappers.mjs +2 -2
  168. package/dist/ui.d.mts +7 -6
  169. package/dist/ui.mjs +8 -7
  170. package/dist/{useLatest-Bg2x4bfP.d.mts → useLatest-DRDDVwjh.d.mts} +5 -25
  171. package/dist/useLatest-DRDDVwjh.d.mts.map +1 -0
  172. package/dist/{with-text-input-CRfoiFFG.d.mts → with-text-input-YeohVLeo.d.mts} +4 -55
  173. package/dist/with-text-input-YeohVLeo.d.mts.map +1 -0
  174. package/dist/wrapper-C70ATkVv.mjs +3527 -0
  175. package/dist/wrapper-C70ATkVv.mjs.map +1 -0
  176. package/dist/{wrappers-UTADQkSY.mjs → wrappers-BCUYITrY.mjs} +5 -157
  177. package/dist/wrappers-BCUYITrY.mjs.map +1 -0
  178. package/dist/{yoga-adapter-8oRGRw8V.mjs → yoga-adapter-BnZX1PAY.mjs} +28 -2
  179. package/dist/yoga-adapter-BnZX1PAY.mjs.map +1 -0
  180. package/dist/yoga-adapter-DxgsQ_gg.mjs +2 -0
  181. package/dist/zipBundle-3nqeDRtm.mjs +3 -0
  182. package/dist/zipBundle-VNAYFmqJ.mjs +2003 -0
  183. package/dist/zipBundle-VNAYFmqJ.mjs.map +1 -0
  184. package/package.json +20 -9
  185. package/dist/animation-Cn64yepo.mjs.map +0 -1
  186. package/dist/cli-BKp0YtBD.mjs +0 -4
  187. package/dist/devtools-9QY4teqI.mjs +0 -2
  188. package/dist/flexily-zero-adapter-BlQa46nr.mjs +0 -3385
  189. package/dist/flexily-zero-adapter-BlQa46nr.mjs.map +0 -1
  190. package/dist/image-CTII5QWI.mjs +0 -477
  191. package/dist/image-CTII5QWI.mjs.map +0 -1
  192. package/dist/index-BXslOebb.d.mts.map +0 -1
  193. package/dist/index-BnA7mNpo.d.mts +0 -175
  194. package/dist/index-BnA7mNpo.d.mts.map +0 -1
  195. package/dist/index-D3saHouR.d.mts.map +0 -1
  196. package/dist/layout-engine-ClUgv6jB.mjs +0 -50
  197. package/dist/layout-engine-ClUgv6jB.mjs.map +0 -1
  198. package/dist/node-BeWlnCPY.mjs.map +0 -1
  199. package/dist/reconciler-Cwgm8hRR.mjs +0 -8459
  200. package/dist/reconciler-Cwgm8hRR.mjs.map +0 -1
  201. package/dist/render-string-Darrg7ku.mjs.map +0 -1
  202. package/dist/spinner-CeOmcuw_.mjs.map +0 -1
  203. package/dist/src-B5GjfG7g.mjs +0 -4305
  204. package/dist/src-B5GjfG7g.mjs.map +0 -1
  205. package/dist/src-CChwjk0Z.mjs +0 -738
  206. package/dist/src-CChwjk0Z.mjs.map +0 -1
  207. package/dist/src-CF-6UN01.mjs.map +0 -1
  208. package/dist/src-NCKb8kE5.mjs +0 -2660
  209. package/dist/src-NCKb8kE5.mjs.map +0 -1
  210. package/dist/types-BH_v3iMT.d.mts.map +0 -1
  211. package/dist/types-Bk2yw9Qj.mjs.map +0 -1
  212. package/dist/ui/progress.d.mts.map +0 -1
  213. package/dist/ui/progress.mjs.map +0 -1
  214. package/dist/useLatest-Bg2x4bfP.d.mts.map +0 -1
  215. package/dist/with-text-input-CRfoiFFG.d.mts.map +0 -1
  216. package/dist/wrappers-UTADQkSY.mjs.map +0 -1
  217. package/dist/yoga-adapter-8oRGRw8V.mjs.map +0 -1
  218. package/dist/yoga-adapter-D_CcxSt5.mjs +0 -2
@@ -0,0 +1,988 @@
1
+ import { i as signal } from "./src-DKp-_OFG.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
+ decorationRects: signal(EMPTY_DECORATION_RECTS),
325
+ textContent: signal(node.textContent),
326
+ focused: signal(node.interactiveState?.focused ?? false)
327
+ };
328
+ signalMap.set(node, s);
329
+ }
330
+ return s;
331
+ }
332
+ /**
333
+ * Compute the content-box rect for a node — `scrollRect` minus border and
334
+ * padding (CSS content area in absolute terminal coordinates).
335
+ *
336
+ * Returns null when `scrollRect` is not yet populated (pre-layout) or when
337
+ * border + padding would shrink the area to zero/negative width or height
338
+ * (clipped/empty content area).
339
+ *
340
+ * The math is the canonical "border + padding" calculation that the layout
341
+ * engine uses internally. Lifted here so consumers (cursor positioning,
342
+ * popover anchors, selection overlays) read one signal instead of re-deriving
343
+ * the offsets at every call site.
344
+ */
345
+ function computeContentRect(node) {
346
+ const props = node.props;
347
+ const scroll = node.scrollRect;
348
+ if (!scroll) return null;
349
+ const padLeft = props?.paddingLeft ?? props?.paddingX ?? props?.padding ?? 0;
350
+ const padRight = props?.paddingRight ?? props?.paddingX ?? props?.padding ?? 0;
351
+ const padTop = props?.paddingTop ?? props?.paddingY ?? props?.padding ?? 0;
352
+ const padBottom = props?.paddingBottom ?? props?.paddingY ?? props?.padding ?? 0;
353
+ const borderLeft = props?.borderStyle ? 1 : 0;
354
+ const borderRight = props?.borderStyle ? 1 : 0;
355
+ const borderTop = props?.borderStyle ? 1 : 0;
356
+ const borderBottom = props?.borderStyle ? 1 : 0;
357
+ const x = scroll.x + borderLeft + padLeft;
358
+ const y = scroll.y + borderTop + padTop;
359
+ const width = scroll.width - borderLeft - borderRight - padLeft - padRight;
360
+ const height = scroll.height - borderTop - borderBottom - padTop - padBottom;
361
+ if (width <= 0 || height <= 0) return null;
362
+ return {
363
+ x,
364
+ y,
365
+ width,
366
+ height
367
+ };
368
+ }
369
+ /**
370
+ * Compute the absolute caret rect for a node based on its `cursorOffset`
371
+ * prop and current `contentRect`. Caret coordinates are content-area-relative
372
+ * (inside border + padding), so this delegates to `computeContentRect` for
373
+ * the origin instead of redoing the border/padding math here.
374
+ *
375
+ * Returns null when:
376
+ * - the node has no `cursorOffset` prop, OR
377
+ * - `scrollRect` is not yet populated (pre-layout), OR
378
+ * - the content box collapsed to zero/negative size (no place to draw).
379
+ *
380
+ * `computeContentRect` keeps cursor positioning and overlay anchoring on the
381
+ * same origin — Phase 4 / overlay-anchor consumers read `contentRect`
382
+ * directly and won't drift from where the caret lands. See bead
383
+ * `km-silvery.cursor-invariants` invariant 3.
384
+ */
385
+ function computeCursorRect(node) {
386
+ const offset = node.props?.cursorOffset;
387
+ if (!offset) return null;
388
+ const content = computeContentRect(node);
389
+ if (!content) return null;
390
+ return {
391
+ x: content.x + offset.col,
392
+ y: content.y + offset.row,
393
+ visible: offset.visible !== false,
394
+ shape: offset.shape
395
+ };
396
+ }
397
+ /**
398
+ * Compute the focused-node id for a node based on its `focused` BoxProp.
399
+ *
400
+ * Returns the node's `id` (preferred) or `testID` when `props.focused === true`,
401
+ * else `null`. This is the per-node value carried in
402
+ * `LayoutSignals.focusedNodeId` — the tree-walk lookup
403
+ * `findActiveFocusedNodeId(root)` picks the deepest non-null among all
404
+ * declarers (Phase 4a precedence rule).
405
+ *
406
+ * Identity priority: `id` > `testID`. Apps that want stable focus identity
407
+ * should set one of those props alongside `focused={true}`. When neither is
408
+ * set but `focused === true`, an opaque sentinel (`"__focused__"`) is
409
+ * returned so the signal is still observable as "something is focused" —
410
+ * downstream consumers should not depend on the sentinel value beyond
411
+ * non-null/null.
412
+ */
413
+ function computeFocusedNodeId(node) {
414
+ const props = node.props;
415
+ if (!props?.focused) return null;
416
+ if (typeof props.id === "string" && props.id.length > 0) return props.id;
417
+ if (typeof props.testID === "string" && props.testID.length > 0) return props.testID;
418
+ return "__focused__";
419
+ }
420
+ /**
421
+ * Resolve the `anchorRef` BoxProp into a string id. Accepts the shorthand
422
+ * `anchorRef="my-id"` and the structured `anchorRef={{ id: "my-id" }}` form.
423
+ *
424
+ * Returns `null` when no anchorRef is present, or when the prop is malformed
425
+ * (empty id string). Apps that need stable anchor identity should always pass
426
+ * a non-empty string.
427
+ */
428
+ function resolveAnchorId(node) {
429
+ const ref = node.props?.anchorRef;
430
+ if (!ref) return null;
431
+ if (typeof ref === "string") return ref.length > 0 ? ref : null;
432
+ const ar = ref;
433
+ if (typeof ar.id === "string" && ar.id.length > 0) return ar.id;
434
+ return null;
435
+ }
436
+ /**
437
+ * Compute the anchor rect for a Box that declares `anchorRef`. The registered
438
+ * rect is the Box's `contentRect` — the inner area inside border + padding
439
+ * — which is the canonical origin for placement math. Edge-specific rects
440
+ * (top/bottom/left/right) are derived by `placeFloating` at consumption time
441
+ * rather than baked into the registry.
442
+ *
443
+ * Returns `null` when:
444
+ * - the node has no `anchorRef` BoxProp (or it's empty), OR
445
+ * - `contentRect` is unavailable (pre-layout / clipped to zero size).
446
+ *
447
+ * Phase 4c of `km-silvery.view-as-layout-output` (overlay-anchor v1).
448
+ */
449
+ function computeAnchorRect(node) {
450
+ if (resolveAnchorId(node) === null) return null;
451
+ return computeContentRect(node);
452
+ }
453
+ /**
454
+ * Walk the tree and find the rect for an anchor by id. Returns `null` when
455
+ * no Box declares `anchorRef` with a matching id, or when the matching Box's
456
+ * `contentRect` is unavailable this frame (pre-layout / clipped).
457
+ *
458
+ * **Optional `edge` parameter** — when supplied, returns a 1-cell-thick rect
459
+ * along the requested edge of the anchor (`top`, `bottom`, `left`, `right`).
460
+ * Convenience for callers that want to draw against a specific edge without
461
+ * threading the full content rect through `placeFloating`. Without `edge`,
462
+ * returns the full content rect.
463
+ *
464
+ * **Walk order**: post-order (deepest-first). If two anchors share an id —
465
+ * the contract says they shouldn't, but the substrate doesn't enforce
466
+ * uniqueness — the deeper / later-rendered one wins. This matches the
467
+ * deepest-wins precedence used by cursor and focus walks.
468
+ *
469
+ * Per-node cost: one `props.anchorRef` check + one signal lookup (or one
470
+ * direct compute when no signal is allocated). Trees with no anchors return
471
+ * `null` after a single traversal.
472
+ *
473
+ * Phase 4c of `km-silvery.view-as-layout-output` (overlay-anchor v1).
474
+ */
475
+ function findAnchor(root, id, edge) {
476
+ let result = null;
477
+ function walk(node) {
478
+ for (const child of node.children) walk(child);
479
+ if (resolveAnchorId(node) !== id) return;
480
+ const s = signalMap.get(node);
481
+ const rect = s ? s.anchorRect() : computeAnchorRect(node);
482
+ if (rect) result = rect;
483
+ }
484
+ walk(root);
485
+ if (result === null || edge === void 0) return result;
486
+ const r = result;
487
+ switch (edge) {
488
+ case "top": return {
489
+ x: r.x,
490
+ y: r.y,
491
+ width: r.width,
492
+ height: 1
493
+ };
494
+ case "bottom": return {
495
+ x: r.x,
496
+ y: r.y + Math.max(0, r.height - 1),
497
+ width: r.width,
498
+ height: 1
499
+ };
500
+ case "left": return {
501
+ x: r.x,
502
+ y: r.y,
503
+ width: 1,
504
+ height: r.height
505
+ };
506
+ case "right": return {
507
+ x: r.x + Math.max(0, r.width - 1),
508
+ y: r.y,
509
+ width: 1,
510
+ height: r.height
511
+ };
512
+ }
513
+ }
514
+ /**
515
+ * Compute the resolved decoration rects for a node based on its `decorations`
516
+ * BoxProp. Each entry produces one `DecorationRect` whose `rects` may be empty
517
+ * when an anchor lookup fails.
518
+ *
519
+ * **Behavior by kind**:
520
+ * - `popover` / `tooltip`: requires `anchorId` + `placement` + `size`. The
521
+ * anchor rect is looked up via `findAnchor(root, anchorId)`; if found and
522
+ * all required fields are present, `placeFloating` produces the placed
523
+ * rect. Missing anchor or missing required fields → empty rect list.
524
+ * - `highlight`: the `rect` field, if present, is translated from
525
+ * content-relative coordinates into absolute terminal coordinates by
526
+ * adding the owning Box's `contentRect.{x, y}`. Missing rect or no
527
+ * contentRect → empty rect list.
528
+ *
529
+ * **Per-frame**: this runs in the layout-phase notify pass, so anchor rects
530
+ * are populated for the same frame. Anchors declared deeper in the tree
531
+ * resolve correctly because the function takes the root tree as input rather
532
+ * than relying on a separately-built map.
533
+ *
534
+ * Phase 4c of `km-silvery.view-as-layout-output` (overlay-anchor v1).
535
+ */
536
+ function computeDecorationRects(node, root) {
537
+ const decos = node.props?.decorations;
538
+ if (!decos || decos.length === 0) return EMPTY_DECORATION_RECTS;
539
+ const out = [];
540
+ const content = computeContentRect(node);
541
+ for (const d of decos) if (d.kind === "popover" || d.kind === "tooltip") {
542
+ if (!d.anchorId || !d.placement || !d.size) {
543
+ out.push({
544
+ kind: d.kind,
545
+ id: d.id,
546
+ rects: []
547
+ });
548
+ continue;
549
+ }
550
+ const anchor = findAnchor(root, d.anchorId);
551
+ if (!anchor) {
552
+ out.push({
553
+ kind: d.kind,
554
+ id: d.id,
555
+ rects: []
556
+ });
557
+ continue;
558
+ }
559
+ const placed = resolveFloatingPlacement(anchor, d.size, d.placement, {
560
+ offset: d.offset,
561
+ alignOffset: d.alignOffset,
562
+ collisionStrategy: d.collisionStrategy,
563
+ boundary: root.boxRect
564
+ });
565
+ out.push({
566
+ kind: d.kind,
567
+ id: d.id,
568
+ rects: placed ? [placed.rect] : []
569
+ });
570
+ } else if (d.kind === "highlight") {
571
+ if (!d.rect || !content) {
572
+ out.push({
573
+ kind: d.kind,
574
+ id: d.id,
575
+ rects: []
576
+ });
577
+ continue;
578
+ }
579
+ out.push({
580
+ kind: d.kind,
581
+ id: d.id,
582
+ rects: [{
583
+ x: content.x + d.rect.x,
584
+ y: content.y + d.rect.y,
585
+ width: d.rect.width,
586
+ height: d.rect.height
587
+ }]
588
+ });
589
+ }
590
+ return out.length === 0 ? EMPTY_DECORATION_RECTS : out;
591
+ }
592
+ /**
593
+ * Per-field equality on a list of `DecorationRect`. Used to skip
594
+ * `decorationRects` signal writes when nothing changed — mirrors the
595
+ * `selectionFragmentsEqual` pattern.
596
+ */
597
+ function decorationRectsEqual(a, b) {
598
+ if (a === b) return true;
599
+ if (a.length !== b.length) return false;
600
+ for (let i = 0; i < a.length; i++) {
601
+ const ai = a[i];
602
+ const bi = b[i];
603
+ if (ai.kind !== bi.kind) return false;
604
+ if (ai.id !== bi.id) return false;
605
+ if (ai.rects.length !== bi.rects.length) return false;
606
+ for (let j = 0; j < ai.rects.length; j++) if (!rectEqual$1(ai.rects[j] ?? null, bi.rects[j] ?? null)) return false;
607
+ }
608
+ return true;
609
+ }
610
+ /**
611
+ * Collect the textual content of a selection-declaring Box.
612
+ *
613
+ * The selection-fragment math operates on the rendered text content of the
614
+ * owning Box — `selectionIntent.{from,to}` are character offsets into this
615
+ * string. For Box nodes, the canonical content is the concatenation of
616
+ * descendant `silvery-text` nodes' `textContent` (in tree order), with `\n`
617
+ * separators between adjacent Text/Box children that introduce visual line
618
+ * breaks.
619
+ *
620
+ * v1 behaviour (kept intentionally minimal):
621
+ * - A Box with `silvery-text` children: concatenates `textContent` strings
622
+ * from those children. Two adjacent text children produce one logical
623
+ * line; if you want a line break, embed `\n` in the text.
624
+ * - A Box with mixed children: same — only `silvery-text` descendants
625
+ * contribute. Nested Box children don't add line breaks (they're treated
626
+ * as transparent for content purposes).
627
+ * - A `silvery-text` node directly carrying the prop: its own `textContent`
628
+ * is the content.
629
+ *
630
+ * This keeps the v1 model honest: declare `selectionIntent` on a Box (or
631
+ * Text) whose text content is the source of truth for the selection. Apps
632
+ * that want per-line semantics can split the selection across multiple
633
+ * intent declarations.
634
+ */
635
+ function collectSelectionText(node) {
636
+ if (node.type === "silvery-text") return node.textContent ?? "";
637
+ let out = "";
638
+ const stack = [node];
639
+ while (stack.length) {
640
+ const cur = stack.pop();
641
+ for (let i = cur.children.length - 1; i >= 0; i--) {
642
+ const child = cur.children[i];
643
+ if (child) stack.push(child);
644
+ }
645
+ if (cur === node) continue;
646
+ if (cur.type === "silvery-text" && cur.textContent !== void 0) out += cur.textContent;
647
+ }
648
+ return out;
649
+ }
650
+ /**
651
+ * Compute the geometric fragments for a node's `selectionIntent` — the list
652
+ * of rectangles (one per visual line spanned) that the selection renderer
653
+ * should paint with highlight bg this frame.
654
+ *
655
+ * Returns:
656
+ * - `[]` when the node has no `selectionIntent` prop, or when the intent is
657
+ * collapsed (`from === to`), or when the content rect is unavailable
658
+ * (pre-layout / clipped to zero size).
659
+ * - `[Rect]` for a single-visual-line selection.
660
+ * - `[Rect, Rect, ...]` for multi-line selections (split per visual line).
661
+ *
662
+ * **Geometry** (mirrors text-editor / ProseMirror conventions):
663
+ * - First line: from `(content.x + fromCol, content.y + fromLine)` to the
664
+ * end of the line. If single-line, runs to `toCol`.
665
+ * - Middle lines: full content-rect width, one row each.
666
+ * - Last line: from `(content.x, content.y + toLine)` to `toCol` chars.
667
+ *
668
+ * Coordinates are absolute terminal cells, matching `cursorRect`'s
669
+ * coordinate space. Width is in cells (one rect per visual line).
670
+ *
671
+ * **Soft-wrap awareness (Option B)**: when a wrap measurer is registered
672
+ * via `setWrapMeasurer({ wrapText })` AND the content rect width is known,
673
+ * this function splits on the measurer's per-visual-line slices — a
674
+ * 60-char paragraph wrapped at width 20 produces 3 fragments rather than
675
+ * one wide rectangle. The terminal runtime (`@silvery/ag-term`) registers
676
+ * its grapheme-aware `wrapText` at startup; pure `@silvery/ag` consumers
677
+ * (no terminal) fall back to `\n`-only splitting which preserves the
678
+ * pre-Option-B behavior bit-for-bit. See `wrap-measurer.ts` for the
679
+ * registry contract. Closes Phase 4b deferred wrap-spanning (bead
680
+ * `km-silvery.softwrap-selection-fragments`).
681
+ */
682
+ function computeSelectionFragments(node) {
683
+ const intent = node.props?.selectionIntent;
684
+ if (!intent) return EMPTY_FRAGMENTS;
685
+ if (intent.from >= intent.to) return EMPTY_FRAGMENTS;
686
+ const content = computeContentRect(node);
687
+ if (!content) return EMPTY_FRAGMENTS;
688
+ const text = collectSelectionText(node);
689
+ if (text.length === 0) return EMPTY_FRAGMENTS;
690
+ const measurer = getWrapMeasurer();
691
+ const visualLines = measurer !== null && content.width > 0 ? buildVisualLinesWithMeasurer(text, content.width, measurer.wrapText) : buildVisualLinesNewlineOnly(text);
692
+ const fragments = [];
693
+ for (let i = 0; i < visualLines.length; i++) {
694
+ const line = visualLines[i];
695
+ if (line.endOffset <= intent.from) continue;
696
+ if (line.startOffset >= intent.to) break;
697
+ const localFrom = Math.max(0, intent.from - line.startOffset);
698
+ const localTo = Math.min(line.text.length, intent.to - line.startOffset);
699
+ const width = Math.max(0, localTo - localFrom);
700
+ if (width === 0) continue;
701
+ fragments.push({
702
+ x: content.x + localFrom,
703
+ y: content.y + i,
704
+ width,
705
+ height: 1
706
+ });
707
+ }
708
+ return fragments.length === 0 ? EMPTY_FRAGMENTS : fragments;
709
+ }
710
+ /**
711
+ * Walk paragraphs (split on `\n`) through the registered wrap measurer to
712
+ * produce per-visual-line slices. When a paragraph fits within the width
713
+ * unchanged, the measurer returns `[]` — we synthesize a single-slice
714
+ * passthrough so the downstream loop sees uniform input.
715
+ *
716
+ * Maintains the invariant that visual-line offsets are monotone and cover
717
+ * the full input (including the `\n` terminator counted as a zero-width
718
+ * boundary so cross-paragraph selections stay aligned).
719
+ */
720
+ function buildVisualLinesWithMeasurer(text, width, wrapText) {
721
+ const out = [];
722
+ let paraStart = 0;
723
+ for (let i = 0; i <= text.length; i++) {
724
+ const isEnd = i === text.length;
725
+ const isNewline = !isEnd && text.charCodeAt(i) === 10;
726
+ if (!isEnd && !isNewline) continue;
727
+ const para = text.slice(paraStart, i);
728
+ const slices = wrapText(para, width);
729
+ if (slices.length === 0) out.push({
730
+ text: para,
731
+ startOffset: paraStart,
732
+ endOffset: paraStart + para.length
733
+ });
734
+ else for (const slice of slices) out.push({
735
+ text: slice.text,
736
+ startOffset: paraStart + slice.startOffset,
737
+ endOffset: paraStart + slice.endOffset
738
+ });
739
+ paraStart = i + 1;
740
+ }
741
+ return out;
742
+ }
743
+ /**
744
+ * Fallback: split on `\n` only. Preserves pre-Option-B geometry exactly so
745
+ * unit tests that exercise the framework-only layer (no terminal Term
746
+ * registered) keep passing without changes.
747
+ *
748
+ * The `endOffset` of each line is the position of the `\n` (or `text.length`
749
+ * for the trailing line) — this matches the convention used by
750
+ * `buildVisualLinesWithMeasurer`, where the newline is a zero-width
751
+ * paragraph boundary rather than a visual line of its own.
752
+ */
753
+ function buildVisualLinesNewlineOnly(text) {
754
+ const out = [];
755
+ let lineStart = 0;
756
+ for (let i = 0; i <= text.length; i++) if (i === text.length || text.charCodeAt(i) === 10) {
757
+ out.push({
758
+ text: text.slice(lineStart, i),
759
+ startOffset: lineStart,
760
+ endOffset: i
761
+ });
762
+ lineStart = i + 1;
763
+ }
764
+ return out;
765
+ }
766
+ /**
767
+ * Project AgNode.scrollState → ScrollStateSnapshot (the subset the virtualizer
768
+ * needs). Returns null if the node has no scroll state yet (non-scroll
769
+ * containers or fresh scroll containers pre-layout).
770
+ *
771
+ * Keeping this projection tight means callers can compare snapshots by
772
+ * per-field equality without pulling the mutable underlying object into
773
+ * consumer code.
774
+ */
775
+ function snapshotScrollState(node) {
776
+ const ss = node.scrollState;
777
+ if (!ss) return null;
778
+ return {
779
+ offset: ss.offset,
780
+ contentHeight: ss.contentHeight,
781
+ viewportHeight: ss.viewportHeight,
782
+ firstVisibleChild: ss.firstVisibleChild,
783
+ lastVisibleChild: ss.lastVisibleChild,
784
+ hiddenAbove: ss.hiddenAbove,
785
+ hiddenBelow: ss.hiddenBelow
786
+ };
787
+ }
788
+ /** Per-field equality check for ScrollStateSnapshot (skips allocation). */
789
+ function scrollStateEqual(a, b) {
790
+ if (a === b) return true;
791
+ if (!a || !b) return false;
792
+ 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;
793
+ }
794
+ /** Check whether a node has signals allocated (for testing). */
795
+ function hasLayoutSignals(node) {
796
+ return signalMap.has(node);
797
+ }
798
+ function hasObservedLayoutSignal(node, key) {
799
+ return observedLayoutSignals.get(node)?.has(key) ?? false;
800
+ }
801
+ /**
802
+ * Sync all rect signals from the node's current values.
803
+ *
804
+ * Called from notifyLayoutSubscribers after layout + scroll + sticky
805
+ * phases complete. Only syncs nodes that have signals allocated.
806
+ * Reference-equality check prevents unnecessary downstream updates.
807
+ */
808
+ function syncRectSignals(node) {
809
+ const props = node.props ?? void 0;
810
+ const hasCursorOffset = !!props?.cursorOffset;
811
+ const hasFocused = !!props?.focused;
812
+ const hasSelectionIntent = !!props?.selectionIntent;
813
+ const hasAnchorRef = !!props?.anchorRef;
814
+ const hasDecorations = !!(props?.decorations && props.decorations.length > 0);
815
+ const s = hasCursorOffset || hasFocused || hasSelectionIntent || hasAnchorRef || hasDecorations ? getLayoutSignals(node) : signalMap.get(node);
816
+ if (!s) return;
817
+ if (node.boxRect !== s.boxRect()) s.boxRect(node.boxRect);
818
+ if (node.scrollRect !== s.scrollRect()) s.scrollRect(node.scrollRect);
819
+ if (node.screenRect !== s.screenRect()) s.screenRect(node.screenRect);
820
+ const nextContentRect = computeContentRect(node);
821
+ if (!rectEqual$1(nextContentRect, s.contentRect())) s.contentRect(nextContentRect);
822
+ const nextCursorRect = computeCursorRect(node);
823
+ if (!cursorRectEqual(nextCursorRect, s.cursorRect())) s.cursorRect(nextCursorRect);
824
+ const nextFocusedId = computeFocusedNodeId(node);
825
+ if (nextFocusedId !== s.focusedNodeId()) s.focusedNodeId(nextFocusedId);
826
+ const nextFragments = computeSelectionFragments(node);
827
+ if (!selectionFragmentsEqual(nextFragments, s.selectionFragments())) s.selectionFragments(nextFragments);
828
+ const nextAnchorRect = computeAnchorRect(node);
829
+ if (!rectEqual$1(nextAnchorRect, s.anchorRect())) s.anchorRect(nextAnchorRect);
830
+ const nextScrollState = snapshotScrollState(node);
831
+ if (!scrollStateEqual(nextScrollState, s.scrollState())) s.scrollState(nextScrollState);
832
+ }
833
+ /**
834
+ * Second-pass sync for `decorationRects` — must run AFTER `syncRectSignals`
835
+ * has populated every anchor rect this frame, because decoration resolution
836
+ * calls `findAnchor(root, id)` and needs the freshest anchor rects.
837
+ *
838
+ * Walks the tree, recomputes per-node decoration rects, and writes the signal
839
+ * only when the result differs (per-field equality via `decorationRectsEqual`).
840
+ *
841
+ * Phase 4c of `km-silvery.view-as-layout-output` (overlay-anchor v1).
842
+ *
843
+ * Per-node cost: one `props.decorations` length check (zero-allocation
844
+ * short-circuit) + one signal lookup + one decoration recompute when present.
845
+ * Trees without decorations pay only the prop check at every node.
846
+ */
847
+ function syncDecorationRects(root) {
848
+ function walk(node) {
849
+ const props = node.props;
850
+ if (!!(props?.decorations && props.decorations.length > 0)) {
851
+ const s = getLayoutSignals(node);
852
+ const next = computeDecorationRects(node, root);
853
+ if (!decorationRectsEqual(next, s.decorationRects())) s.decorationRects(next);
854
+ } else {
855
+ const s = signalMap.get(node);
856
+ if (s && s.decorationRects().length > 0) s.decorationRects(EMPTY_DECORATION_RECTS);
857
+ }
858
+ for (const child of node.children) walk(child);
859
+ }
860
+ walk(root);
861
+ }
862
+ /**
863
+ * Promote the in-flight rect signals (`boxRect` / `scrollRect` / `screenRect`)
864
+ * to their committed counterparts (`boxRectCommitted` / etc.). Reactive
865
+ * `useBoxRect()` / `useScrollRect()` / `useScreenRect()` consumers subscribe
866
+ * to the committed signals — calling this advances them by one frame.
867
+ *
868
+ * Called by the runtime ONCE per event-batch commit, after the convergence
869
+ * loop has fully drained. Within a single batch, multiple convergence passes
870
+ * may write the in-flight signals (callback-form observers fire each time),
871
+ * but the committed signals advance only here. That's what lets a render
872
+ * which both READS `useBoxRect()` and WRITES a layout-affecting prop converge
873
+ * in one pass: the read returns the same value across every pass in the
874
+ * batch, so the write is idempotent.
875
+ *
876
+ * Reference equality on the underlying alien-signal write means a no-op
877
+ * commit (same rect as last frame) does not fire any subscribers — steady
878
+ * state pays no cost.
879
+ *
880
+ * The walker visits only nodes that already have allocated `LayoutSignals`
881
+ * (i.e. nodes with at least one consumer); a tree with no rect subscribers
882
+ * pays only the WeakMap probe per node.
883
+ *
884
+ * See bead `@km/silvery/use-deferred-box-rect-and-post-commit-observers`.
885
+ */
886
+ function commitLayoutSnapshot(root) {
887
+ function walk(node) {
888
+ const s = signalMap.get(node);
889
+ if (s) {
890
+ const nextBox = s.boxRect();
891
+ if (!rectEqual$1(nextBox, s.boxRectCommitted())) s.boxRectCommitted(nextBox);
892
+ const nextScroll = s.scrollRect();
893
+ if (!rectEqual$1(nextScroll, s.scrollRectCommitted())) s.scrollRectCommitted(nextScroll);
894
+ const nextScreen = s.screenRect();
895
+ if (!rectEqual$1(nextScreen, s.screenRectCommitted())) s.screenRectCommitted(nextScreen);
896
+ }
897
+ for (const child of node.children) walk(child);
898
+ }
899
+ walk(root);
900
+ }
901
+ /**
902
+ * Walk the tree and find the active caret rect — the caret to render this
903
+ * frame, applying the precedence + clipping rules locked by bead
904
+ * `km-silvery.cursor-invariants`. Returns null when no caret should be
905
+ * shown.
906
+ *
907
+ * **Precedence (invariant 1)**:
908
+ * 1. **Focused-editable wins**: a Box with `cursorOffset.visible !== false`
909
+ * AND `interactiveState.focused === true`. If multiple focused-editables
910
+ * exist (rare — typically one input is focused at a time), the deepest
911
+ * one in paint order wins.
912
+ * 2. **Otherwise deepest-in-paint-order**: if no node is focused-editable,
913
+ * fall back to the deepest visible declarer (post-order tree walk).
914
+ * This covers Ink-compat consumers and `useCursor` callers that don't
915
+ * participate in the focus tree.
916
+ * 3. **Otherwise null**: no visible caret declared anywhere.
917
+ *
918
+ * **Clipping (invariant 4)**: at each scroll/clip ancestor (a Box with
919
+ * `overflow="scroll"` / `"hidden"` / `overflowY="hidden"`), the caret's
920
+ * position is checked against the ancestor's `scrollRect`. If the caret
921
+ * falls outside the visible region, the caret is treated as not-present.
922
+ * Default behavior is **hide** (no caret ANSI emitted) — never clamp. A
923
+ * caret rect at the exact clip edge is treated as visible.
924
+ *
925
+ * Visited in tree order (depth-first, post-order). Per-node cost is one
926
+ * `props.cursorOffset` check + one signal lookup; trees without any caret
927
+ * declarer return null after a single traversal.
928
+ */
929
+ function findActiveCursorRect(root) {
930
+ let focusedResult = null;
931
+ let fallbackResult = null;
932
+ const clipStack = [];
933
+ function isClipped(rect) {
934
+ for (let i = clipStack.length - 1; i >= 0; i--) {
935
+ const clip = clipStack[i];
936
+ if (!clip) continue;
937
+ if (rect.x < clip.x || rect.y < clip.y || rect.x >= clip.x + clip.width || rect.y >= clip.y + clip.height) return true;
938
+ }
939
+ return false;
940
+ }
941
+ function isClipAncestor(node) {
942
+ const props = node.props;
943
+ if (!props) return false;
944
+ if (props.overflow === "scroll" || props.overflow === "hidden") return true;
945
+ if (props.overflowY === "hidden") return true;
946
+ return false;
947
+ }
948
+ function walk(node) {
949
+ const isClip = isClipAncestor(node);
950
+ if (isClip) clipStack.push(node.scrollRect ?? null);
951
+ for (const child of node.children) walk(child);
952
+ if (node.props?.cursorOffset) {
953
+ const s = signalMap.get(node);
954
+ const rect = s ? s.cursorRect() : computeCursorRect(node);
955
+ if (rect && rect.visible && !isClipped(rect)) {
956
+ fallbackResult = rect;
957
+ if (node.interactiveState?.focused) focusedResult = rect;
958
+ }
959
+ }
960
+ if (isClip) clipStack.pop();
961
+ }
962
+ walk(root);
963
+ return focusedResult ?? fallbackResult;
964
+ }
965
+ /**
966
+ * Sync textContent signal from the node's current value.
967
+ *
968
+ * Called from commitTextUpdate in the reconciler.
969
+ */
970
+ function syncTextContentSignal(node) {
971
+ const s = signalMap.get(node);
972
+ if (!s) return;
973
+ if (node.textContent !== s.textContent()) s.textContent(node.textContent);
974
+ }
975
+ /**
976
+ * Sync focused signal for a node.
977
+ *
978
+ * Called from FocusManager when focus changes.
979
+ */
980
+ function syncFocusedSignal(node, focused) {
981
+ const s = signalMap.get(node);
982
+ if (!s) return;
983
+ if (focused !== s.focused()) s.focused(focused);
984
+ }
985
+ //#endregion
986
+ export { hasObservedLayoutSignal as a, syncDecorationRects as c, syncTextContentSignal as d, getWrapMeasurer as f, hasLayoutSignals as i, syncFocusedSignal as l, rectEqual$1 as m, findActiveCursorRect as n, markObservedLayoutSignal as o, setWrapMeasurer as p, getLayoutSignals as r, observeLayoutSignal as s, commitLayoutSnapshot as t, syncRectSignals as u };
987
+
988
+ //# sourceMappingURL=layout-signals-Cnw6xk8Q.mjs.map