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,1044 @@
1
+ import { O as displayWidthAnsi, rt as wrapText } from "./ansi-yC4RyBNY.mjs";
2
+ import { createLogger } from "loggily";
3
+ //#region packages/ag/src/focus-queries.ts
4
+ /** Check if a node has the focusable prop set to true (or truthy). */
5
+ function isFocusable(node) {
6
+ if (node.hidden) return false;
7
+ const props = node.props;
8
+ return Boolean(props.focusable) && props.display !== "none";
9
+ }
10
+ /** Check if a node creates a focus scope (isolated Tab cycle). */
11
+ function isFocusScope(node) {
12
+ const props = node.props;
13
+ return Boolean(props.focusScope);
14
+ }
15
+ /**
16
+ * Walk up from node to nearest ancestor (or self) with focusable prop.
17
+ * Useful for mouse clicks — find the focusable target from a deep text node.
18
+ */
19
+ function findFocusableAncestor(node) {
20
+ let current = node;
21
+ while (current) {
22
+ if (isFocusable(current)) return current;
23
+ current = current.parent;
24
+ }
25
+ return null;
26
+ }
27
+ /**
28
+ * DFS traversal of focusable nodes in tab order, optionally scoped.
29
+ *
30
+ * When scope is provided, only nodes within that scope subtree are included.
31
+ * If a focusScope node is encountered during traversal, its children are
32
+ * skipped (they belong to a different scope), unless that scope IS the
33
+ * provided scope node.
34
+ */
35
+ function getTabOrder(root, scope) {
36
+ const result = [];
37
+ const walkRoot = scope ?? root;
38
+ function walk(node) {
39
+ if (node.hidden) return;
40
+ if (node.props.display === "none") return;
41
+ if (node !== walkRoot && isFocusScope(node)) {
42
+ if (isFocusable(node)) result.push(node);
43
+ return;
44
+ }
45
+ if (isFocusable(node)) result.push(node);
46
+ for (const child of node.children) walk(child);
47
+ }
48
+ walk(walkRoot);
49
+ return result;
50
+ }
51
+ /**
52
+ * Walk up from a node to find the nearest ancestor (or self) with focusScope prop.
53
+ * Returns the testID of the enclosing scope, or null if none found.
54
+ */
55
+ function findEnclosingScope(node) {
56
+ let current = node;
57
+ while (current) {
58
+ if (isFocusScope(current)) {
59
+ const props = current.props;
60
+ return typeof props.testID === "string" ? props.testID : null;
61
+ }
62
+ current = current.parent;
63
+ }
64
+ return null;
65
+ }
66
+ /**
67
+ * Find a node by testID in the subtree rooted at root.
68
+ * DFS, returns the first match.
69
+ */
70
+ function findByTestID(root, testID) {
71
+ if (root.props.testID === testID) return root;
72
+ for (const child of root.children) {
73
+ const found = findByTestID(child, testID);
74
+ if (found) return found;
75
+ }
76
+ return null;
77
+ }
78
+ /**
79
+ * Compute center point of a Rect.
80
+ */
81
+ function rectCenter(rect) {
82
+ return {
83
+ cx: rect.x + rect.width / 2,
84
+ cy: rect.y + rect.height / 2
85
+ };
86
+ }
87
+ /**
88
+ * Check if a candidate point falls within a 45-degree cone from source
89
+ * in the given direction (tvOS-style spatial navigation).
90
+ *
91
+ * The cone extends from the center of the source rect in the specified
92
+ * direction with a 45-degree half-angle (90-degree total aperture).
93
+ */
94
+ function isInCone(source, candidate, direction) {
95
+ const dx = candidate.cx - source.cx;
96
+ const dy = candidate.cy - source.cy;
97
+ switch (direction) {
98
+ case "up":
99
+ if (dy >= 0) return false;
100
+ return Math.abs(dx) <= Math.abs(dy);
101
+ case "down":
102
+ if (dy <= 0) return false;
103
+ return Math.abs(dx) <= Math.abs(dy);
104
+ case "left":
105
+ if (dx >= 0) return false;
106
+ return Math.abs(dy) <= Math.abs(dx);
107
+ case "right":
108
+ if (dx <= 0) return false;
109
+ return Math.abs(dy) <= Math.abs(dx);
110
+ }
111
+ }
112
+ /**
113
+ * Euclidean distance between two points.
114
+ */
115
+ function distance(a, b) {
116
+ const dx = a.cx - b.cx;
117
+ const dy = a.cy - b.cy;
118
+ return Math.sqrt(dx * dx + dy * dy);
119
+ }
120
+ /**
121
+ * Find the nearest focusable candidate in a given direction using
122
+ * 45-degree cone heuristic (tvOS-style spatial navigation).
123
+ *
124
+ * From the center of the source rect, draw a cone in the target direction.
125
+ * Filter candidates whose center falls within the cone. Pick the closest
126
+ * by Euclidean distance.
127
+ *
128
+ * @param from - The currently focused node
129
+ * @param direction - Direction to search
130
+ * @param candidates - All focusable nodes to consider
131
+ * @param layoutFn - Function to get screen rect for a node (null if not laid out)
132
+ */
133
+ function findSpatialTarget(from, direction, candidates, layoutFn) {
134
+ const sourceRect = layoutFn(from);
135
+ if (!sourceRect) return null;
136
+ const source = rectCenter(sourceRect);
137
+ let best = null;
138
+ let bestDist = Infinity;
139
+ for (const candidate of candidates) {
140
+ if (candidate === from) continue;
141
+ const candidateRect = layoutFn(candidate);
142
+ if (!candidateRect) continue;
143
+ const target = rectCenter(candidateRect);
144
+ if (!isInCone(source, target, direction)) continue;
145
+ const dist = distance(source, target);
146
+ if (dist < bestDist) {
147
+ bestDist = dist;
148
+ best = candidate;
149
+ }
150
+ }
151
+ return best;
152
+ }
153
+ /**
154
+ * Check if a node has an explicit nextFocus{Direction} override prop.
155
+ *
156
+ * These props allow components to declare explicit focus targets for
157
+ * spatial navigation, overriding the cone heuristic.
158
+ *
159
+ * @param node - The node to check
160
+ * @param direction - Direction string: "up", "down", "left", "right"
161
+ * @returns The testID of the explicit target, or null
162
+ */
163
+ function getExplicitFocusLink(node, direction) {
164
+ const value = node.props[`nextFocus${direction.charAt(0).toUpperCase()}${direction.slice(1)}`];
165
+ return typeof value === "string" ? value : null;
166
+ }
167
+ //#endregion
168
+ //#region packages/ag/src/interactive-signals.ts
169
+ /**
170
+ * Ensure a node has an InteractiveState object, creating one if needed.
171
+ * Returns the (possibly newly created) state.
172
+ */
173
+ function ensureInteractiveState(node) {
174
+ if (!node.interactiveState) node.interactiveState = {
175
+ hovered: false,
176
+ armed: false,
177
+ selected: false,
178
+ focused: false,
179
+ dropTarget: false
180
+ };
181
+ return node.interactiveState;
182
+ }
183
+ /**
184
+ * Set the hovered state. Returns true if the value changed.
185
+ */
186
+ function setHovered(node, value) {
187
+ const state = ensureInteractiveState(node);
188
+ if (state.hovered === value) return false;
189
+ state.hovered = value;
190
+ return true;
191
+ }
192
+ /**
193
+ * Set the armed state (pointer-down, awaiting click). Returns true if the value changed.
194
+ */
195
+ function setArmed(node, value) {
196
+ const state = ensureInteractiveState(node);
197
+ if (state.armed === value) return false;
198
+ state.armed = value;
199
+ return true;
200
+ }
201
+ /**
202
+ * Set the focused state. Returns true if the value changed.
203
+ */
204
+ function setFocused(node, value) {
205
+ const state = ensureInteractiveState(node);
206
+ if (state.focused === value) return false;
207
+ state.focused = value;
208
+ return true;
209
+ }
210
+ //#endregion
211
+ //#region packages/ag/src/tree-utils.ts
212
+ /**
213
+ * Collect the ancestor path from target to root (inclusive).
214
+ */
215
+ function getAncestorPath(node) {
216
+ const path = [];
217
+ let current = node;
218
+ while (current) {
219
+ path.push(current);
220
+ current = current.parent;
221
+ }
222
+ return path;
223
+ }
224
+ /**
225
+ * Check if a point is inside a rect.
226
+ */
227
+ function pointInRect(x, y, rect) {
228
+ return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height;
229
+ }
230
+ //#endregion
231
+ //#region packages/ag-term/src/mouse-events.ts
232
+ /**
233
+ * DOM-level Mouse Events for silvery
234
+ *
235
+ * Provides React DOM-compatible mouse event infrastructure:
236
+ * - SilveryMouseEvent / SilveryWheelEvent synthetic event objects
237
+ * - Tree-based hit testing using scrollRect (replaces manual HitRegistry)
238
+ * - Event dispatch with bubbling (target → root, stopPropagation support)
239
+ * - Double-click detection (300ms / 2-cell threshold)
240
+ * - mouseenter/mouseleave tracking (no bubble, like DOM spec)
241
+ */
242
+ const mouseLog = createLogger("silvery:mouse");
243
+ /**
244
+ * Create a synthetic mouse event.
245
+ *
246
+ * Modifier keys are merged from two sources:
247
+ * - SGR mouse protocol: reports Ctrl, Alt/Meta, Shift (reliable)
248
+ * - Keyboard tracking: reports Super/Cmd, Hyper, CapsLock, NumLock (via Kitty protocol)
249
+ *
250
+ * `metaKey` = keyboard-tracked Super (Cmd on macOS). SGR "meta" maps to `altKey`.
251
+ */
252
+ function createMouseEvent(type, x, y, target, parsed, keyboardMods) {
253
+ let propagationStopped = false;
254
+ let defaultPrevented = false;
255
+ const metaKey = keyboardMods?.super ?? false;
256
+ if (type === "click" || type === "mousedown") mouseLog.debug?.(`createMouseEvent(${type}) metaKey=${metaKey} keyboardMods.super=${keyboardMods?.super}`);
257
+ else if (type === "wheel") {
258
+ const targetId = target.props.id ?? "";
259
+ mouseLog.debug?.(`createMouseEvent(wheel) x=${x} y=${y} delta=${parsed.delta ?? 0} target=${target.type}#${targetId}`);
260
+ } else if (type === "mouseup") mouseLog.debug?.(`createMouseEvent(mouseup) x=${x} y=${y} button=${parsed.button}`);
261
+ return {
262
+ type,
263
+ x,
264
+ y,
265
+ clientX: parsed.clientX,
266
+ clientY: parsed.clientY,
267
+ button: parsed.button,
268
+ altKey: parsed.meta,
269
+ ctrlKey: parsed.ctrl,
270
+ metaKey,
271
+ shiftKey: parsed.shift,
272
+ timeStamp: parsed.receivedAt ?? performance.now(),
273
+ inputBatchId: parsed.inputBatchId,
274
+ target,
275
+ currentTarget: target,
276
+ nativeEvent: parsed,
277
+ get propagationStopped() {
278
+ return propagationStopped;
279
+ },
280
+ get defaultPrevented() {
281
+ return defaultPrevented;
282
+ },
283
+ stopPropagation() {
284
+ propagationStopped = true;
285
+ },
286
+ preventDefault() {
287
+ defaultPrevented = true;
288
+ }
289
+ };
290
+ }
291
+ /**
292
+ * Create a synthetic wheel event.
293
+ */
294
+ function createWheelEvent(x, y, target, parsed, keyboardMods) {
295
+ const base = createMouseEvent("wheel", x, y, target, parsed, keyboardMods);
296
+ base.deltaY = parsed.delta ?? 0;
297
+ base.deltaX = 0;
298
+ return base;
299
+ }
300
+ /** Position property on a Box that takes the node out of normal flow. */
301
+ function isAbsolutePositioned(node) {
302
+ return node.props.position === "absolute";
303
+ }
304
+ /**
305
+ * Geometry-based hit test for absolute-positioned descendants.
306
+ *
307
+ * Walks the whole subtree rooted at `node` in tree order. For each
308
+ * absolute-positioned descendant whose scrollRect contains (x, y), recurse
309
+ * into it as a standalone hit-test (which finds the deepest in-flow child
310
+ * under that absolute) and track the latest-in-tree hit — that one paints
311
+ * on top (third pass in render order uses natural child order, so later =
312
+ * higher z).
313
+ *
314
+ * Respects pointerEvents="none" on the absolute root and its ancestors,
315
+ * and overflow:hidden/scroll clipping on ancestors up to `node`.
316
+ *
317
+ * Returns null if no absolute descendant covers (x, y).
318
+ */
319
+ function hitTestAbsoluteDescendants(node, x, y, ancestorClipRect) {
320
+ let result = null;
321
+ for (const child of node.children) {
322
+ const cp = child.props;
323
+ if (cp.pointerEvents === "none") continue;
324
+ let childClip = ancestorClipRect;
325
+ if (cp.overflow === "hidden" || cp.overflow === "scroll") {
326
+ const cr = child.scrollRect;
327
+ if (cr) childClip = childClip ? intersectRect(childClip, cr) : cr;
328
+ }
329
+ if (isAbsolutePositioned(child) && child.scrollRect) {
330
+ if (!(ancestorClipRect && !pointInRect(x, y, ancestorClipRect)) && pointInRect(x, y, child.scrollRect)) {
331
+ const hit = hitTestAbsoluteDescendants(child, x, y, null) ?? hitTestInFlow(child, x, y);
332
+ if (hit) result = hit;
333
+ }
334
+ }
335
+ const deeper = hitTestAbsoluteDescendants(child, x, y, childClip);
336
+ if (deeper) result = deeper;
337
+ }
338
+ return result;
339
+ }
340
+ /** Compute the intersection of two rects; returns a zero-size rect if disjoint. */
341
+ function intersectRect(a, b) {
342
+ const x1 = Math.max(a.x, b.x);
343
+ const y1 = Math.max(a.y, b.y);
344
+ const x2 = Math.min(a.x + a.width, b.x + b.width);
345
+ const y2 = Math.min(a.y + a.height, b.y + b.height);
346
+ return {
347
+ x: x1,
348
+ y: y1,
349
+ width: Math.max(0, x2 - x1),
350
+ height: Math.max(0, y2 - y1)
351
+ };
352
+ }
353
+ /**
354
+ * In-flow (non-absolute) DFS hit test. Used by both `hitTest` after the
355
+ * absolute pass, and by `hitTestAbsoluteDescendants` when recursing into a
356
+ * matched absolute to find the deepest in-flow descendant under it.
357
+ *
358
+ * Skips absolute children — they're handled by the absolute pass at the
359
+ * entry point (`hitTest`).
360
+ */
361
+ function hitTestInFlow(node, x, y) {
362
+ const rect = node.scrollRect;
363
+ if (!rect) return null;
364
+ if (!pointInRect(x, y, rect)) return null;
365
+ const props = node.props;
366
+ if (props.pointerEvents === "none") return null;
367
+ const clips = props.overflow === "hidden" || props.overflow === "scroll";
368
+ for (let i = node.children.length - 1; i >= 0; i--) {
369
+ const child = node.children[i];
370
+ if (isAbsolutePositioned(child)) continue;
371
+ if (clips) {
372
+ if (child.scrollRect && !pointInRect(x, y, rect)) continue;
373
+ }
374
+ const hit = hitTestInFlow(child, x, y);
375
+ if (hit) return hit;
376
+ }
377
+ if (node.type === "silvery-text") for (let i = node.children.length - 1; i >= 0; i--) {
378
+ const child = node.children[i];
379
+ if (child.inlineRects) {
380
+ for (const inlineRect of child.inlineRects) if (pointInRect(x, y, inlineRect)) return child;
381
+ }
382
+ }
383
+ return node;
384
+ }
385
+ /**
386
+ * Tree-based hit test: find the deepest node whose scrollRect contains (x, y).
387
+ *
388
+ * Uses reverse child order (last sibling wins = highest z-order, like DOM).
389
+ * Respects overflow:hidden clipping and pointerEvents="none".
390
+ *
391
+ * ### Absolute-positioned nodes escape parent bounds
392
+ *
393
+ * Absolute descendants participate in hit-testing by GEOMETRY, not by
394
+ * tree order / parent rect containment. An absolute child can be placed
395
+ * outside its parent's bounding rect (e.g., a popover anchored near a
396
+ * viewport edge); it still occupies screen cells at its own geometry and
397
+ * must be hittable.
398
+ *
399
+ * The hit test runs an "absolute pass" first that walks the whole subtree
400
+ * for absolute descendants and returns the latest-in-tree hit (matching
401
+ * the three-pass render order where absolute children paint on top of
402
+ * normal + sticky content). If no absolute descendant covers the point,
403
+ * it falls through to standard in-flow DFS.
404
+ *
405
+ * A recursive sub-call (via `hitTest(absolute, ...)`) would re-run the
406
+ * absolute pass on that absolute's subtree — which is correct: nested
407
+ * absolutes also need geometry-based hit testing.
408
+ */
409
+ function hitTest(node, x, y) {
410
+ const absHit = hitTestAbsoluteDescendants(node, x, y, null);
411
+ if (absHit) return absHit;
412
+ return hitTestInFlow(node, x, y);
413
+ }
414
+ /**
415
+ * Resolve the effective userSelect value for a node.
416
+ * "auto" inherits from parent; root defaults to "text".
417
+ */
418
+ function resolveUserSelect(node) {
419
+ let current = node;
420
+ while (current) {
421
+ const value = current.props.userSelect;
422
+ if (value === "none" || value === "text" || value === "contain") return value;
423
+ current = current.parent;
424
+ }
425
+ return "text";
426
+ }
427
+ /**
428
+ * Selection hit test: find the deepest node whose text is selectable at (x, y).
429
+ *
430
+ * Unlike pointer hitTest, this:
431
+ * - Ignores pointerEvents (a node with pointerEvents="none" can still be selectable)
432
+ * - Respects userSelect (a node with userSelect="none" is not a selection target)
433
+ */
434
+ function selectionHitTest(node, x, y) {
435
+ return selectionHitTestInner(node, x, y, true);
436
+ }
437
+ function selectionCellFromPoint(x, y) {
438
+ return {
439
+ col: Math.max(0, Math.floor(x)),
440
+ row: Math.max(0, Math.floor(y))
441
+ };
442
+ }
443
+ /**
444
+ * Find the nearest selectable rendered-text cell inside a container rect.
445
+ *
446
+ * This is the terminal-target equivalent of the browser's
447
+ * caretPositionFromPoint behavior for text-containing boxes: a mousedown in
448
+ * padding or interior whitespace can still start text selection by snapping to
449
+ * nearby rendered text, while genuinely text-free containers return null.
450
+ */
451
+ function nearestSelectableCellFromPoint(buffer, rect, x, y) {
452
+ if (rect.width <= 0 || rect.height <= 0) return null;
453
+ const left = Math.max(0, Math.floor(rect.x));
454
+ const right = Math.min(buffer.width - 1, Math.ceil(rect.x + rect.width) - 1);
455
+ const top = Math.max(0, Math.floor(rect.y));
456
+ const bottom = Math.min(buffer.height - 1, Math.ceil(rect.y + rect.height) - 1);
457
+ if (left > right || top > bottom) return null;
458
+ const pointerCol = Math.max(0, Math.floor(x));
459
+ const pointerRow = Math.max(0, Math.floor(y));
460
+ const rows = [];
461
+ for (let row = top; row <= bottom; row++) {
462
+ let first = -1;
463
+ let last = -1;
464
+ let nearest = -1;
465
+ let nearestDistance = Number.POSITIVE_INFINITY;
466
+ for (let col = left; col <= right; col++) {
467
+ if (!buffer.isCellSelectable(col, row)) continue;
468
+ if (first === -1) first = col;
469
+ last = col;
470
+ const distance = Math.abs(col - pointerCol);
471
+ if (distance < nearestDistance) {
472
+ nearest = col;
473
+ nearestDistance = distance;
474
+ }
475
+ }
476
+ if (first !== -1) rows.push({
477
+ row,
478
+ first,
479
+ last,
480
+ nearest
481
+ });
482
+ }
483
+ if (rows.length === 0) return null;
484
+ const firstRow = rows[0];
485
+ const lastRow = rows[rows.length - 1];
486
+ const sameRow = rows.find((candidate) => candidate.row === pointerRow);
487
+ if (sameRow) {
488
+ if (pointerCol < sameRow.first) return {
489
+ col: sameRow.first,
490
+ row: sameRow.row
491
+ };
492
+ if (pointerCol > sameRow.last) return {
493
+ col: Math.min(right, sameRow.last + 1),
494
+ row: sameRow.row
495
+ };
496
+ return {
497
+ col: sameRow.nearest,
498
+ row: sameRow.row
499
+ };
500
+ }
501
+ if (pointerRow < firstRow.row) return {
502
+ col: firstRow.first,
503
+ row: firstRow.row
504
+ };
505
+ if (pointerRow > lastRow.row) return {
506
+ col: lastRow.last,
507
+ row: lastRow.row
508
+ };
509
+ for (let i = 0; i < rows.length; i++) {
510
+ const candidate = rows[i];
511
+ if (candidate.row > pointerRow) return {
512
+ col: candidate.first,
513
+ row: candidate.row
514
+ };
515
+ }
516
+ return {
517
+ col: lastRow.last,
518
+ row: lastRow.row
519
+ };
520
+ }
521
+ /**
522
+ * Resolve the semantic selection anchor for a terminal pointer position.
523
+ *
524
+ * This is the single owner for mousedown selection semantics:
525
+ * - exact selectable glyph / empty rendered line hits
526
+ * - `userSelect="none"` pointer targets that block document selection
527
+ * - nearest rendered text-cell fallback from blank padding inside text containers
528
+ * - `userSelect="contain"` / document boundary discovery
529
+ * - Shift/raw buffer selection that bypasses document scopes
530
+ */
531
+ function resolveSelectionAnchorFromPoint(options) {
532
+ const { root, buffer, x, y } = options;
533
+ const forceBufferSelection = options.forceBufferSelection === true;
534
+ const downCell = selectionCellFromPoint(x, y);
535
+ let anchorCell = downCell;
536
+ if (!root) return forceBufferSelection ? {
537
+ node: null,
538
+ cell: anchorCell,
539
+ downCell,
540
+ boundaries: [],
541
+ forceBufferSelection
542
+ } : null;
543
+ const pointerTarget = hitTest(root, x, y);
544
+ const pointerBlocksSelection = pointerTarget !== null && resolveUserSelect(pointerTarget) === "none";
545
+ let selectedNode = !pointerBlocksSelection ? selectionHitTest(root, x, y) : null;
546
+ if (selectedNode === null && !forceBufferSelection && !pointerBlocksSelection && pointerTarget !== null && buffer) {
547
+ let current = pointerTarget;
548
+ while (current && selectedNode === null) {
549
+ const rect = current.scrollRect;
550
+ const nearest = rect ? nearestSelectableCellFromPoint(buffer, rect, x, y) : null;
551
+ if (!nearest) {
552
+ current = current.parent;
553
+ continue;
554
+ }
555
+ const nearestHit = selectionHitTest(root, nearest.col, nearest.row);
556
+ if (nearestHit) {
557
+ anchorCell = nearest;
558
+ selectedNode = nearestHit;
559
+ }
560
+ current = current.parent;
561
+ }
562
+ }
563
+ if (selectedNode === null && !forceBufferSelection) return null;
564
+ return {
565
+ node: selectedNode,
566
+ cell: anchorCell,
567
+ downCell,
568
+ boundaries: selectedNode ? findSelectionBoundaries(selectedNode) : [],
569
+ forceBufferSelection
570
+ };
571
+ }
572
+ function selectionHitTestInner(node, x, y, allowRowFallback) {
573
+ const rect = node.scrollRect;
574
+ if (!rect) return null;
575
+ if (!pointInRect(x, y, rect)) return null;
576
+ const props = node.props;
577
+ if (resolveUserSelect(node) === "none") return null;
578
+ const clips = props.overflow === "hidden" || props.overflow === "scroll";
579
+ for (let i = node.children.length - 1; i >= 0; i--) {
580
+ const child = node.children[i];
581
+ const childRect = child.scrollRect;
582
+ if (clips) {
583
+ if (childRect && !pointInRect(x, y, rect)) continue;
584
+ }
585
+ if (childRect && pointInRect(x, y, childRect)) {
586
+ if (resolveUserSelect(child) === "none") return null;
587
+ const hit = selectionHitTestInner(child, x, y, false);
588
+ if (hit) return hit;
589
+ continue;
590
+ }
591
+ const hit = selectionHitTestInner(child, x, y, false);
592
+ if (hit) return hit;
593
+ }
594
+ if (node.type === "silvery-text") for (let i = node.children.length - 1; i >= 0; i--) {
595
+ const child = node.children[i];
596
+ if (child.inlineRects) {
597
+ for (const inlineRect of child.inlineRects) if (pointInRect(x, y, inlineRect)) return child;
598
+ }
599
+ }
600
+ if (node.type === "silvery-text") return pointHitsRenderedTextRow(node, y) ? node : null;
601
+ return allowRowFallback ? findTextNodeOnRow(node, y) : null;
602
+ }
603
+ function nodeSelectionScope(node) {
604
+ const rect = node.scrollRect;
605
+ if (!rect) return null;
606
+ if (rect.width <= 0 || rect.height <= 0) return null;
607
+ if (node.type === "silvery-text") {
608
+ const textBounds = renderedTextBounds(node);
609
+ if (textBounds === null) return null;
610
+ return {
611
+ top: rect.y,
612
+ bottom: rect.y + textBounds.height - 1,
613
+ left: rect.x,
614
+ right: rect.x + textBounds.width - 1
615
+ };
616
+ }
617
+ return {
618
+ top: rect.y,
619
+ bottom: rect.y + rect.height - 1,
620
+ left: rect.x,
621
+ right: rect.x + rect.width - 1
622
+ };
623
+ }
624
+ function collectText(node) {
625
+ if (node.type === "silvery-text" && node.textContent !== void 0) return node.textContent;
626
+ let out = "";
627
+ for (const child of node.children) out += collectText(child);
628
+ return out;
629
+ }
630
+ function renderedTextLines(node) {
631
+ const rect = node.scrollRect;
632
+ if (!rect || rect.width <= 0) return [];
633
+ const text = collectText(node);
634
+ if (text.length === 0) return [];
635
+ const wrap = node.props.wrap;
636
+ return wrap !== false && wrap !== "truncate" && wrap !== "truncate-end" && wrap !== "clip" ? wrapText(text, rect.width, true, false) : text.split("\n");
637
+ }
638
+ function renderedTextBounds(node) {
639
+ const rect = node.scrollRect;
640
+ if (!rect || rect.width <= 0 || rect.height <= 0) return null;
641
+ const lines = renderedTextLines(node);
642
+ if (lines.length === 0) return null;
643
+ let maxWidth = 0;
644
+ for (const line of lines) maxWidth = Math.max(maxWidth, Math.min(rect.width, displayWidthAnsi(line)));
645
+ if (maxWidth <= 0) return null;
646
+ return {
647
+ width: maxWidth,
648
+ height: Math.min(rect.height, lines.length)
649
+ };
650
+ }
651
+ function pointHitsRenderedTextRow(node, y) {
652
+ const rect = node.scrollRect;
653
+ if (!rect) return false;
654
+ const row = y - rect.y;
655
+ if (row < 0 || row >= rect.height) return false;
656
+ return renderedTextLines(node)[row] !== void 0;
657
+ }
658
+ function findTextNodeOnRow(node, y) {
659
+ for (let i = node.children.length - 1; i >= 0; i--) {
660
+ const child = node.children[i];
661
+ const hit = findTextNodeOnRow(child, y);
662
+ if (hit) return hit;
663
+ }
664
+ return node.type === "silvery-text" && pointHitsRenderedTextRow(node, y) ? node : null;
665
+ }
666
+ /**
667
+ * Return the selectable document-ancestor chain for a node, nearest first.
668
+ *
669
+ * This is the DOM-like selection path: ordinary selectable nodes create
670
+ * semantic selection regions, while `userSelect="contain"` marks a CSS-style
671
+ * hard containment boundary that selection must not escape.
672
+ */
673
+ function findSelectionBoundaries(node) {
674
+ const boundaries = [];
675
+ let current = node;
676
+ while (current) {
677
+ if (resolveUserSelect(current) !== "none") {
678
+ const scope = nodeSelectionScope(current);
679
+ if (scope) {
680
+ const props = current.props;
681
+ boundaries.push({
682
+ node: current,
683
+ scope,
684
+ hardContain: props.userSelect === "contain"
685
+ });
686
+ }
687
+ }
688
+ current = current.parent;
689
+ }
690
+ return boundaries;
691
+ }
692
+ /** Map event type to the handler prop name */
693
+ const EVENT_HANDLER_MAP = {
694
+ click: "onClick",
695
+ dblclick: "onDoubleClick",
696
+ tripleclick: "onTripleClick",
697
+ mousedown: "onMouseDown",
698
+ mouseup: "onMouseUp",
699
+ mousemove: "onMouseMove",
700
+ mouseenter: "onMouseEnter",
701
+ mouseleave: "onMouseLeave",
702
+ wheel: "onWheel"
703
+ };
704
+ /**
705
+ * Dispatch a mouse event through the render tree with DOM-style bubbling.
706
+ *
707
+ * Bubbles from target → root, calling the appropriate handler on each node.
708
+ * stopPropagation() halts bubbling. mouseenter/mouseleave do NOT bubble (DOM spec).
709
+ */
710
+ function dispatchMouseEvent(event) {
711
+ const handlerProp = EVENT_HANDLER_MAP[event.type];
712
+ if (!handlerProp) return;
713
+ if (event.type === "mouseenter" || event.type === "mouseleave") {
714
+ const handler = event.target.props[handlerProp];
715
+ if (handler) {
716
+ const mutableEvent = event;
717
+ mutableEvent.currentTarget = event.target;
718
+ handler(event);
719
+ }
720
+ return;
721
+ }
722
+ const path = getAncestorPath(event.target);
723
+ for (const node of path) {
724
+ if (event.propagationStopped) break;
725
+ const handler = node.props[handlerProp];
726
+ if (handler) {
727
+ const mutableEvent = event;
728
+ mutableEvent.currentTarget = node;
729
+ handler(event);
730
+ }
731
+ }
732
+ }
733
+ function createClickCountState() {
734
+ return {
735
+ lastClickTime: 0,
736
+ lastClickX: -999,
737
+ lastClickY: -999,
738
+ lastClickButton: -1,
739
+ count: 0
740
+ };
741
+ }
742
+ /** @deprecated Use `createClickCountState()` instead. */
743
+ const createDoubleClickState = createClickCountState;
744
+ const MULTI_CLICK_TIME_MS = 300;
745
+ const MULTI_CLICK_DISTANCE = 2;
746
+ /**
747
+ * Determine the consecutive-click count for the current click.
748
+ *
749
+ * Returns 1 for a fresh click, 2 for a double-click, 3 for a triple-click.
750
+ * Subsequent clicks restart the chain at 1.
751
+ *
752
+ * Updates `state` so the next call sees the right history.
753
+ */
754
+ function checkClickCount(state, x, y, button, now = Date.now()) {
755
+ const timeDelta = now - state.lastClickTime;
756
+ const dx = Math.abs(x - state.lastClickX);
757
+ const dy = Math.abs(y - state.lastClickY);
758
+ const inChain = button === state.lastClickButton && timeDelta <= MULTI_CLICK_TIME_MS && dx <= MULTI_CLICK_DISTANCE && dy <= MULTI_CLICK_DISTANCE;
759
+ let count;
760
+ if (!inChain || state.count >= 3) count = 1;
761
+ else if (state.count === 1) count = 2;
762
+ else count = 3;
763
+ state.lastClickTime = now;
764
+ state.lastClickX = x;
765
+ state.lastClickY = y;
766
+ state.lastClickButton = button;
767
+ state.count = count;
768
+ return count;
769
+ }
770
+ /**
771
+ * Check if a click qualifies as a double-click. Backwards-compatible
772
+ * wrapper around `checkClickCount`.
773
+ *
774
+ * @deprecated Use `checkClickCount` and inspect the returned count
775
+ * (`=== 2` for dblclick, `=== 3` for tripleclick).
776
+ */
777
+ function checkDoubleClick(state, x, y, button, now = Date.now()) {
778
+ return checkClickCount(state, x, y, button, now) === 2;
779
+ }
780
+ /**
781
+ * Compute mouseenter/mouseleave transitions between two ancestor paths.
782
+ *
783
+ * Returns { entered, left } — arrays of nodes that were entered or left.
784
+ * Mirrors the DOM spec: fire mouseleave on nodes in prevPath not in nextPath,
785
+ * and mouseenter on nodes in nextPath not in prevPath.
786
+ */
787
+ function computeEnterLeave(prevPath, nextPath) {
788
+ const prevSet = new Set(prevPath);
789
+ const nextSet = new Set(nextPath);
790
+ return {
791
+ entered: nextPath.filter((n) => !prevSet.has(n)),
792
+ left: prevPath.filter((n) => !nextSet.has(n))
793
+ };
794
+ }
795
+ function createMouseEventProcessor(options) {
796
+ return {
797
+ doubleClick: createDoubleClickState(),
798
+ hoverPath: [],
799
+ mouseDownTarget: null,
800
+ mouseCaptureTarget: null,
801
+ outsideCaptureReleaseTimer: null,
802
+ outsideCaptureReleaseMouse: null,
803
+ focusManager: options?.focusManager,
804
+ keyboardModifiers: {
805
+ super: false,
806
+ hyper: false,
807
+ capsLock: false,
808
+ numLock: false
809
+ },
810
+ lastClickPrevented: false,
811
+ lastPointer: null
812
+ };
813
+ }
814
+ function findMouseCaptureTarget(node) {
815
+ let current = node;
816
+ while (current) {
817
+ if (current.props.mouseCapture === true) return current;
818
+ current = current.parent;
819
+ }
820
+ return null;
821
+ }
822
+ const MOUSE_CAPTURE_OUTSIDE_GRACE_MS = 2e3;
823
+ function mouseUpParsed(parsed) {
824
+ return parsed.action === "up" ? parsed : {
825
+ ...parsed,
826
+ action: "up"
827
+ };
828
+ }
829
+ /**
830
+ * Update keyboard modifier state from a parsed key event.
831
+ * Call this for every keyboard event so mouse events can include accurate modifiers.
832
+ */
833
+ function updateKeyboardModifiers(state, key) {
834
+ const isRelease = key.eventType === "release";
835
+ const prevSuper = state.keyboardModifiers.super;
836
+ if (key.super !== void 0) state.keyboardModifiers.super = isRelease ? false : key.super;
837
+ if (key.hyper !== void 0) state.keyboardModifiers.hyper = isRelease ? false : key.hyper;
838
+ if (key.capsLock !== void 0) state.keyboardModifiers.capsLock = key.capsLock;
839
+ if (key.numLock !== void 0) state.keyboardModifiers.numLock = key.numLock;
840
+ if (state.keyboardModifiers.super !== prevSuper) mouseLog.debug?.(`keyboardModifiers.super: ${prevSuper} → ${state.keyboardModifiers.super} (key.super=${key.super}, eventType=${key.eventType})`);
841
+ }
842
+ function releaseMousePress(state, parsed) {
843
+ let defaultPrevented = false;
844
+ const dispatchTarget = state.mouseCaptureTarget;
845
+ const releaseParsed = mouseUpParsed(parsed);
846
+ cancelOutsideCaptureRelease(state);
847
+ if (state.mouseDownTarget) setArmed(state.mouseDownTarget, false);
848
+ if (dispatchTarget) {
849
+ const event = createMouseEvent("mouseup", releaseParsed.x, releaseParsed.y, dispatchTarget, releaseParsed, state.keyboardModifiers);
850
+ dispatchMouseEvent(event);
851
+ defaultPrevented = event.defaultPrevented;
852
+ }
853
+ state.lastClickPrevented = false;
854
+ state.mouseDownTarget = null;
855
+ state.mouseCaptureTarget = null;
856
+ return defaultPrevented;
857
+ }
858
+ function cancelOutsideCaptureRelease(state) {
859
+ if (state.outsideCaptureReleaseTimer !== null) clearTimeout(state.outsideCaptureReleaseTimer);
860
+ state.outsideCaptureReleaseTimer = null;
861
+ state.outsideCaptureReleaseMouse = null;
862
+ }
863
+ function scheduleOutsideCaptureRelease(state, parsed) {
864
+ state.outsideCaptureReleaseMouse = parsed;
865
+ if (state.outsideCaptureReleaseTimer !== null) return;
866
+ state.outsideCaptureReleaseTimer = setTimeout(() => {
867
+ const outsideMouse = state.outsideCaptureReleaseMouse ?? parsed;
868
+ releaseMousePress(state, outsideMouse);
869
+ clearHoverPath(state, outsideMouse);
870
+ }, MOUSE_CAPTURE_OUTSIDE_GRACE_MS);
871
+ }
872
+ function clearHoverPath(state, parsed) {
873
+ for (const node of state.hoverPath.slice().reverse()) {
874
+ setHovered(node, false);
875
+ dispatchMouseEvent(createMouseEvent("mouseleave", parsed.x, parsed.y, node, parsed, state.keyboardModifiers));
876
+ }
877
+ state.hoverPath = [];
878
+ state.lastPointer = null;
879
+ }
880
+ /**
881
+ * Re-resolve the hover path at the last known pointer coordinates and
882
+ * dispatch enter/leave for any nodes that changed. Use after a layout
883
+ * change that didn't come from a mouse event — most importantly:
884
+ *
885
+ * - Wheel scrolls (content shifts under a stationary cursor)
886
+ * - Async content arrival (transcript leaf appended, list re-flowed)
887
+ * - Programmatic re-layout (resize, theme switch)
888
+ *
889
+ * Without this, hover bg / hover-armed popovers stick to whatever AgNode
890
+ * was under the cursor when the last mouse event fired, even after the
891
+ * tree under that coordinate changed. Symptoms: persistent hover bg on
892
+ * rows that have scrolled out from under the pointer; popover targets
893
+ * arming on rows the cursor isn't over anymore.
894
+ *
895
+ * Idempotent — when nothing changed, no events fire and `state.hoverPath`
896
+ * stays identity-equal. Safe to call every render commit.
897
+ *
898
+ * Bead: @km/code/sticky-hover-residue.
899
+ */
900
+ function refreshHoverPath(state, root) {
901
+ if (state.lastPointer === null) return;
902
+ if (state.mouseCaptureTarget) return;
903
+ const { x, y } = state.lastPointer;
904
+ const target = hitTest(root, x, y);
905
+ const newPath = target ? getAncestorPath(target) : [];
906
+ const { entered, left } = computeEnterLeave(state.hoverPath, newPath);
907
+ if (entered.length === 0 && left.length === 0) return;
908
+ const synthetic = {
909
+ x,
910
+ y,
911
+ button: 0,
912
+ action: "move",
913
+ coordinateMode: "cell",
914
+ shift: false,
915
+ meta: false,
916
+ ctrl: false
917
+ };
918
+ for (const node of left) {
919
+ setHovered(node, false);
920
+ dispatchMouseEvent(createMouseEvent("mouseleave", x, y, node, synthetic, state.keyboardModifiers));
921
+ }
922
+ for (const node of entered.reverse()) {
923
+ setHovered(node, true);
924
+ dispatchMouseEvent(createMouseEvent("mouseenter", x, y, node, synthetic, state.keyboardModifiers));
925
+ }
926
+ state.hoverPath = newPath;
927
+ }
928
+ /**
929
+ * Process a raw ParsedMouse event and dispatch DOM-level events on the render tree.
930
+ *
931
+ * Call this for every SGR mouse event received. It handles:
932
+ * - mousedown / mouseup
933
+ * - click (on mouseup if same target as mousedown)
934
+ * - dblclick (based on timing)
935
+ * - mousemove + mouseenter/mouseleave
936
+ * - wheel
937
+ */
938
+ function processMouseEvent(state, parsed, root) {
939
+ const { x, y, action } = parsed;
940
+ state.lastPointer = {
941
+ x,
942
+ y
943
+ };
944
+ const target = hitTest(root, x, y);
945
+ if (action === "move") {
946
+ const nodeType = target?.type ?? "null";
947
+ const nodeId = target ? target.props.id ?? "" : "";
948
+ let enterAncestor = "";
949
+ if (target) {
950
+ let n = target;
951
+ while (n) {
952
+ if ("onMouseEnter" in n.props) {
953
+ enterAncestor = `${n.type}#${n.props.id ?? ""}`;
954
+ break;
955
+ }
956
+ n = n.parent;
957
+ }
958
+ }
959
+ const newPath = target ? getAncestorPath(target) : [];
960
+ const { entered } = computeEnterLeave(state.hoverPath, newPath);
961
+ mouseLog.debug?.(`move x=${x} y=${y} target=${nodeType}#${nodeId} enterAncestor=${enterAncestor || "none"} entered=${entered.length} prevPath=${state.hoverPath.length}`);
962
+ }
963
+ let defaultPrevented = false;
964
+ if (target) cancelOutsideCaptureRelease(state);
965
+ if (!target) {
966
+ if (action === "move") {
967
+ if (state.mouseCaptureTarget) scheduleOutsideCaptureRelease(state, parsed);
968
+ else defaultPrevented = releaseMousePress(state, parsed);
969
+ clearHoverPath(state, parsed);
970
+ return defaultPrevented;
971
+ }
972
+ if (action === "up") defaultPrevented = releaseMousePress(state, parsed);
973
+ return defaultPrevented;
974
+ }
975
+ if (action === "down") {
976
+ state.mouseDownTarget = target;
977
+ state.mouseCaptureTarget = findMouseCaptureTarget(target);
978
+ setArmed(target, true);
979
+ if (state.focusManager) {
980
+ const focusable = findFocusableAncestor(target);
981
+ if (focusable) state.focusManager.focus(focusable, "mouse");
982
+ }
983
+ const event = createMouseEvent("mousedown", x, y, target, parsed, state.keyboardModifiers);
984
+ dispatchMouseEvent(event);
985
+ if (event.defaultPrevented) defaultPrevented = true;
986
+ } else if (action === "up") {
987
+ const dispatchTarget = state.mouseCaptureTarget ?? target;
988
+ if (state.mouseDownTarget) setArmed(state.mouseDownTarget, false);
989
+ state.lastClickPrevented = false;
990
+ dispatchMouseEvent(createMouseEvent("mouseup", x, y, dispatchTarget, parsed, state.keyboardModifiers));
991
+ if (state.mouseDownTarget) {
992
+ const count = checkClickCount(state.doubleClick, x, y, parsed.button);
993
+ const clickEvent = createMouseEvent("click", x, y, dispatchTarget, parsed, state.keyboardModifiers);
994
+ clickEvent.detail = count;
995
+ dispatchMouseEvent(clickEvent);
996
+ if (clickEvent.defaultPrevented) {
997
+ defaultPrevented = true;
998
+ state.lastClickPrevented = true;
999
+ }
1000
+ if (count >= 2) {
1001
+ const dblEvent = createMouseEvent("dblclick", x, y, dispatchTarget, parsed, state.keyboardModifiers);
1002
+ dblEvent.detail = 2;
1003
+ dispatchMouseEvent(dblEvent);
1004
+ if (dblEvent.defaultPrevented) {
1005
+ defaultPrevented = true;
1006
+ state.lastClickPrevented = true;
1007
+ }
1008
+ }
1009
+ if (count === 3) {
1010
+ const tripleEvent = createMouseEvent("tripleclick", x, y, dispatchTarget, parsed, state.keyboardModifiers);
1011
+ tripleEvent.detail = 3;
1012
+ dispatchMouseEvent(tripleEvent);
1013
+ if (tripleEvent.defaultPrevented) {
1014
+ defaultPrevented = true;
1015
+ state.lastClickPrevented = true;
1016
+ }
1017
+ }
1018
+ }
1019
+ state.mouseDownTarget = null;
1020
+ state.mouseCaptureTarget = null;
1021
+ } else if (action === "move") {
1022
+ dispatchMouseEvent(createMouseEvent("mousemove", x, y, state.mouseCaptureTarget ?? target, parsed, state.keyboardModifiers));
1023
+ const newPath = getAncestorPath(target);
1024
+ const { entered, left } = computeEnterLeave(state.hoverPath, newPath);
1025
+ for (const node of left) {
1026
+ setHovered(node, false);
1027
+ dispatchMouseEvent(createMouseEvent("mouseleave", x, y, node, parsed, state.keyboardModifiers));
1028
+ }
1029
+ for (const node of entered.reverse()) {
1030
+ setHovered(node, true);
1031
+ dispatchMouseEvent(createMouseEvent("mouseenter", x, y, node, parsed, state.keyboardModifiers));
1032
+ }
1033
+ state.hoverPath = newPath;
1034
+ } else if (action === "wheel") {
1035
+ const event = createWheelEvent(x, y, target, parsed, state.keyboardModifiers);
1036
+ dispatchMouseEvent(event);
1037
+ if (event.defaultPrevented) defaultPrevented = true;
1038
+ }
1039
+ return defaultPrevented;
1040
+ }
1041
+ //#endregion
1042
+ export { findSpatialTarget as C, findFocusableAncestor as S, getTabOrder as T, getAncestorPath as _, createDoubleClickState as a, findByTestID as b, createWheelEvent as c, hitTest as d, processMouseEvent as f, updateKeyboardModifiers as g, selectionHitTest as h, createClickCountState as i, dispatchMouseEvent as l, resolveSelectionAnchorFromPoint as m, checkDoubleClick as n, createMouseEvent as o, refreshHoverPath as p, computeEnterLeave as r, createMouseEventProcessor as s, checkClickCount as t, findSelectionBoundaries as u, setArmed as v, getExplicitFocusLink as w, findEnclosingScope as x, setFocused as y };
1043
+
1044
+ //# sourceMappingURL=mouse-events-Dki3ISIp.mjs.map