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,2083 @@
1
+ import { d as syncTextContentSignal } from "./layout-signals-Cnw6xk8Q.mjs";
2
+ import { k as warnOnce, t as init_src } from "./src-BNTToU7l.mjs";
3
+ import { A as getActiveLineHeight, D as displayWidth, rt as wrapText, z as isSoftBreakPoint } from "./ansi-yC4RyBNY.mjs";
4
+ import { a as requireCapability, n as getConstants, r as getLayoutEngine } from "./layout-engine-C2px0RJE.mjs";
5
+ import { createContext } from "react";
6
+ import { createLogger } from "loggily";
7
+ import Reconciler from "react-reconciler";
8
+ import { DefaultEventPriority, DiscreteEventPriority, NoEventPriority } from "react-reconciler/constants.js";
9
+ //#region packages/ag/src/dirty-tracking.ts
10
+ /**
11
+ * Nodes with any content/style dirty flag. Written by reconciler,
12
+ * read by render phase for targeted subtree entry.
13
+ */
14
+ const contentDirtyNodes = /* @__PURE__ */ new Set();
15
+ /**
16
+ * Nodes where ONLY style props changed (no content, layout, or children changes).
17
+ * These are eligible for the style-only fast path in the render phase, which
18
+ * updates cell styles without re-collecting text or re-computing layout.
19
+ *
20
+ * A node is style-only when commitUpdate classifies contentChanged="style"
21
+ * AND layoutChanged=false. The render phase checks this set to decide whether
22
+ * to use restyleRegion() instead of full renderText()/renderBox().
23
+ */
24
+ const styleOnlyDirtyNodes = /* @__PURE__ */ new Set();
25
+ /**
26
+ * Nodes where scrollTo/scrollOffset changed. These don't affect Flexily layout
27
+ * dimensions, but the scroll, sticky, scrollRect, and notify phases must still
28
+ * run to update visible children positions.
29
+ *
30
+ * Written by reconciler (host-config.ts commitUpdate), read by ag.ts
31
+ * layout-on-demand gate.
32
+ */
33
+ const scrollDirtyNodes = /* @__PURE__ */ new Set();
34
+ /** Mark a node as content-dirty. Called when content/style flags are set. */
35
+ function trackContentDirty(node) {
36
+ contentDirtyNodes.add(node);
37
+ }
38
+ /**
39
+ * Mark a node as style-only dirty. Called when commitUpdate sees
40
+ * contentChanged="style" AND layoutChanged=false.
41
+ * If a node is later marked with contentDirty, the render phase ignores
42
+ * the style-only flag (full path takes precedence).
43
+ */
44
+ function trackStyleOnlyDirty(node) {
45
+ styleOnlyDirtyNodes.add(node);
46
+ }
47
+ /** Mark a node as scroll-dirty. Called when scrollTo/scrollOffset props change. */
48
+ function trackScrollDirty(node) {
49
+ scrollDirtyNodes.add(node);
50
+ }
51
+ /** O(1) check: are there any scroll-dirty nodes? */
52
+ function hasScrollDirty() {
53
+ return scrollDirtyNodes.size > 0;
54
+ }
55
+ /** Clear all dirty tracking. Called after each render pass completes. */
56
+ function clearDirtyTracking() {
57
+ contentDirtyNodes.clear();
58
+ styleOnlyDirtyNodes.clear();
59
+ scrollDirtyNodes.clear();
60
+ }
61
+ //#endregion
62
+ //#region packages/ag/src/epoch.ts
63
+ /**
64
+ * The current render epoch. Incremented after each render pass.
65
+ * Reconciler stamps dirty nodes with this value; render phase checks equality.
66
+ */
67
+ let renderEpoch = 0;
68
+ /** Get the current render epoch value. */
69
+ function getRenderEpoch() {
70
+ return renderEpoch;
71
+ }
72
+ /**
73
+ * Advance the render epoch. Called once at the end of each render pass.
74
+ * All nodes stamped with the old epoch instantly become "not dirty".
75
+ */
76
+ function advanceRenderEpoch() {
77
+ renderEpoch++;
78
+ }
79
+ /**
80
+ * Check if an epoch stamp matches the current render epoch (i.e., "is dirty").
81
+ */
82
+ function isCurrentEpoch(epoch) {
83
+ return epoch === renderEpoch;
84
+ }
85
+ /**
86
+ * Check if a specific dirty bit is set for the current epoch.
87
+ * Returns true if dirtyEpoch matches the current render epoch AND the bit is set.
88
+ */
89
+ function isDirty(dirtyBits, dirtyEpoch, bit) {
90
+ return dirtyEpoch === renderEpoch && (dirtyBits & bit) !== 0;
91
+ }
92
+ /**
93
+ * Check if ANY dirty bit is set for the current epoch.
94
+ */
95
+ function isAnyDirty(dirtyBits, dirtyEpoch) {
96
+ return dirtyEpoch === renderEpoch && dirtyBits !== 0;
97
+ }
98
+ //#endregion
99
+ //#region packages/scope/src/trace.ts
100
+ /**
101
+ * @silvery/scope/trace — opt-in leak detector for scopes and disposables.
102
+ *
103
+ * Gated by `SILVERY_SCOPE_TRACE=1`. When enabled, every `createScope()`
104
+ * and `disposable()` call is recorded with its creation stack; dispose
105
+ * unregisters; at process exit any remaining entries are logged.
106
+ *
107
+ * Zero overhead when disabled — the public functions are no-ops behind
108
+ * an early-return guard. The trace registry isn't even allocated.
109
+ *
110
+ * Why: silvery's `Scope` (Phase 0/1/2 of `km-silvery.lifecycle-scope`)
111
+ * makes resource ownership explicit, but adoption is opt-in. This is the
112
+ * runtime backstop for what the ESLint `no-raw-lifecycle` rule can't see
113
+ * (dynamic call sites, third-party paths). Together they make
114
+ * convention-driven leaks structurally impossible.
115
+ *
116
+ * Usage in tests / CI:
117
+ * ```bash
118
+ * SILVERY_SCOPE_TRACE=1 bun run test
119
+ * ```
120
+ *
121
+ * The detector logs to stderr at process exit (or `getTraceSnapshot()`
122
+ * is callable any time for in-test assertions). Production builds — no
123
+ * env var set — skip every code path.
124
+ *
125
+ * @packageDocumentation
126
+ */
127
+ const TRACE_ENV = "SILVERY_SCOPE_TRACE";
128
+ function envEnabled() {
129
+ try {
130
+ return !!globalThis.process?.env?.[TRACE_ENV];
131
+ } catch {
132
+ return false;
133
+ }
134
+ }
135
+ const traceEnabled = envEnabled();
136
+ const live = traceEnabled ? /* @__PURE__ */ new Map() : null;
137
+ /** Internal — register a handle on creation. No-op when tracing is off. */
138
+ function _trackCreate(handle, kind, name) {
139
+ if (!live) return;
140
+ const stack = (/* @__PURE__ */ new Error("(creation stack)")).stack ?? "";
141
+ live.set(handle, {
142
+ kind,
143
+ name,
144
+ createdAt: stack
145
+ });
146
+ }
147
+ /** Internal — unregister a handle on dispose. No-op when tracing is off. */
148
+ function _trackDispose(handle) {
149
+ if (!live) return;
150
+ live.delete(handle);
151
+ }
152
+ /** Force the at-exit report to fire now (for tests / manual diagnostics).
153
+ * Always logs the count, even if zero. No-op when tracing is off. */
154
+ function reportTraceLeaks() {
155
+ if (!live) return 0;
156
+ const count = live.size;
157
+ if (count === 0) {
158
+ console.error("[silvery:scope:trace] no undisposed handles");
159
+ return 0;
160
+ }
161
+ console.error(`[silvery:scope:trace] ${count} undisposed handle(s):`);
162
+ for (const entry of live.values()) {
163
+ const label = entry.kind + (entry.name ? `(${entry.name})` : "");
164
+ console.error(` - ${label}`);
165
+ console.error(entry.createdAt);
166
+ }
167
+ return count;
168
+ }
169
+ /**
170
+ * Print a per-scope handle-delta diagnostic at scope close. Called from
171
+ * `Scope[Symbol.asyncDispose]()` AFTER the inherited stack drains. Per
172
+ * km-silvery.lifecycle-leak-detection Phase 2: previously the trace only
173
+ * fired at process exit, which left in-test scope-close leaks invisible
174
+ * until the worker terminated.
175
+ *
176
+ * `pre` and `post` are snapshot counts of `getAdoptedHandles(scope).length`
177
+ * before and after scope-close. The delta is reported to stderr when
178
+ * tracing is enabled and the scope did NOT balance.
179
+ */
180
+ function reportScopeDelta(scopeName, pre, post) {
181
+ if (!traceEnabled) return;
182
+ if (post === 0) return;
183
+ const label = scopeName ? `Scope(${scopeName})` : "Scope";
184
+ console.error(`[silvery:scope:trace] ${label} close-delta: ${pre} adopted → ${post} undisposed (${pre - post} disposed cleanly)`);
185
+ }
186
+ if (traceEnabled) {
187
+ const proc = globalThis.process;
188
+ if (proc?.on) proc.on("exit", () => {
189
+ reportTraceLeaks();
190
+ });
191
+ }
192
+ //#endregion
193
+ //#region packages/scope/src/handle.ts
194
+ /**
195
+ * Module-private set of every legitimate handle the factory has produced.
196
+ * `WeakSet` keys by reference, so disposed-and-GC'd handles drop out
197
+ * automatically; this is the canonical source of truth for "is this a real
198
+ * silvery handle?" Forged values (created via `as TickHandle` or by hand)
199
+ * are NOT in this set, so `adoptHandle()` and `Scope.use()` reject them.
200
+ */
201
+ const branded = /* @__PURE__ */ new WeakSet();
202
+ const metadata = /* @__PURE__ */ new WeakMap();
203
+ /**
204
+ * Global count of handles currently alive (created but not yet disposed).
205
+ * Incremented in `defineHandle().create()`, decremented once on first
206
+ * dispose (idempotent). WeakSet alone cannot give a count; this integer
207
+ * counter is the parallel deterministic accounting layer.
208
+ *
209
+ * Used by tests to assert structural lifecycle invariants without GC:
210
+ * - After dispose-all: `getActiveHandleCount() === 0`
211
+ * - During N creates: `getActiveHandleCount() === N`
212
+ *
213
+ * Not per-scope (use `getAdoptedHandles(scope)` for per-scope accounting).
214
+ */
215
+ let _activeHandleCount = 0;
216
+ /** True iff `value` was minted by `defineHandle(...).create(...)`. */
217
+ function isBrandedHandle(value) {
218
+ return typeof value === "object" && value !== null && branded.has(value);
219
+ }
220
+ function defineHandle(kind) {
221
+ Symbol(`silvery.handle:${kind}`);
222
+ return {
223
+ kind,
224
+ create(_value, dispose) {
225
+ const handle = Object.create(null);
226
+ let _disposed = false;
227
+ Object.defineProperty(handle, Symbol.asyncDispose, {
228
+ value: async () => {
229
+ if (!_disposed) {
230
+ _disposed = true;
231
+ _activeHandleCount--;
232
+ }
233
+ await dispose();
234
+ },
235
+ enumerable: false,
236
+ writable: false,
237
+ configurable: false
238
+ });
239
+ Object.defineProperty(handle, Symbol.dispose, {
240
+ value: () => {
241
+ if (!_disposed) {
242
+ _disposed = true;
243
+ _activeHandleCount--;
244
+ }
245
+ dispose();
246
+ },
247
+ enumerable: false,
248
+ writable: false,
249
+ configurable: false
250
+ });
251
+ branded.add(handle);
252
+ metadata.set(handle, { kind });
253
+ _activeHandleCount++;
254
+ return handle;
255
+ }
256
+ };
257
+ }
258
+ /**
259
+ * Finalise a handle's public surface and freeze it. Call this from the
260
+ * resource factory AFTER attaching all public properties (e.g. `iterable`,
261
+ * `emitted`) and BEFORE returning to the consumer.
262
+ *
263
+ * @param handle The branded handle returned by `<defineHandle().create>`.
264
+ * @param surface Public properties to attach (each becomes non-enumerable,
265
+ * non-writable, non-configurable so the freeze is real).
266
+ *
267
+ * @example
268
+ * ```ts
269
+ * const handle = Tick.create(internal, stop)
270
+ * finaliseHandle(handle, { iterable, emitted: () => count })
271
+ * return handle as TickHandle
272
+ * ```
273
+ */
274
+ function finaliseHandle(handle, surface) {
275
+ if (!branded.has(handle)) throw new TypeError("finaliseHandle: not a branded handle (call defineHandle().create first)");
276
+ for (const key of Object.keys(surface)) Object.defineProperty(handle, key, {
277
+ value: surface[key],
278
+ enumerable: true,
279
+ writable: false,
280
+ configurable: false
281
+ });
282
+ for (const sym of Object.getOwnPropertySymbols(surface)) Object.defineProperty(handle, sym, {
283
+ value: surface[sym],
284
+ enumerable: false,
285
+ writable: false,
286
+ configurable: false
287
+ });
288
+ Object.freeze(handle);
289
+ return handle;
290
+ }
291
+ /**
292
+ * Per-scope handle accounting. Keyed by `Scope` instance via WeakMap so we
293
+ * don't have to modify the Scope class signature for the minimum-invasive
294
+ * Phase 1 prototype.
295
+ */
296
+ const ownedHandles = /* @__PURE__ */ new WeakMap();
297
+ const handleOrigins = /* @__PURE__ */ new WeakMap();
298
+ /**
299
+ * Adopt a `Handle` into the given scope's ownership registry. The handle:
300
+ *
301
+ * - is rejected if it isn't in the module-private `branded` WeakSet
302
+ * (forged values fail here)
303
+ * - is rejected if already owned by a different scope
304
+ * - is registered with `scope.use(...)` for LIFO disposal
305
+ * - subscribes to early manual disposal so the registry stays accurate
306
+ * if the consumer calls `handle[Symbol.asyncDispose]()` directly
307
+ *
308
+ * Idempotent: adopting the same handle twice into the same scope is a no-op.
309
+ */
310
+ function adoptHandle(scope, handle) {
311
+ if (scope.disposed) throw new ReferenceError("Cannot adopt handle into a disposed scope");
312
+ if (!branded.has(handle)) throw new TypeError("adoptHandle: value is not a silvery handle (forged via 'as' or wrong factory). Use the resource's createX(scope, ...) factory.");
313
+ let owned = ownedHandles.get(scope);
314
+ if (!owned) {
315
+ owned = /* @__PURE__ */ new Set();
316
+ ownedHandles.set(scope, owned);
317
+ }
318
+ const existingOwner = handleOrigins.get(handle);
319
+ if (existingOwner && existingOwner !== scope) throw new TypeError("Handle already owned by another scope; create a new handle for this scope instead");
320
+ if (owned.has(handle)) return;
321
+ owned.add(handle);
322
+ handleOrigins.set(handle, scope);
323
+ const existing = metadata.get(handle);
324
+ if (existing && !existing.createdAt) metadata.set(handle, {
325
+ ...existing,
326
+ createdAt: captureCreationStack()
327
+ });
328
+ let disposedFlag = false;
329
+ const removeFromRegistry = () => {
330
+ if (disposedFlag) return;
331
+ disposedFlag = true;
332
+ owned.delete(handle);
333
+ handleOrigins.delete(handle);
334
+ };
335
+ scope.use({ [Symbol.asyncDispose]: async () => {
336
+ try {
337
+ if (!disposedFlag) await handle[Symbol.asyncDispose]();
338
+ } finally {
339
+ removeFromRegistry();
340
+ }
341
+ } });
342
+ }
343
+ /**
344
+ * Snapshot the current set of handles still adopted into this scope.
345
+ * Empty when the scope was never used or all handles are disposed.
346
+ */
347
+ function getAdoptedHandles(scope) {
348
+ const owned = ownedHandles.get(scope);
349
+ if (!owned || owned.size === 0) return [];
350
+ const result = [];
351
+ for (const h of owned) {
352
+ const meta = metadata.get(h);
353
+ result.push({
354
+ kind: meta?.kind ?? "unknown",
355
+ createdAt: meta?.createdAt
356
+ });
357
+ }
358
+ return result;
359
+ }
360
+ function captureCreationStack() {
361
+ return ((/* @__PURE__ */ new Error("(handle adoption)")).stack ?? "").split("\n").slice(3).join("\n");
362
+ }
363
+ //#endregion
364
+ //#region packages/scope/src/index.ts
365
+ /**
366
+ * @silvery/scope — Structured concurrency scopes for silvery apps.
367
+ *
368
+ * `Scope` is a subclass of TC39's `AsyncDisposableStack` that adds:
369
+ * - an `AbortSignal` that aborts on disposal (and links to a parent's signal)
370
+ * - a `child(name?)` method that creates child scopes with cascade disposal
371
+ * - an overridden `[Symbol.asyncDispose]()` that disposes children before the
372
+ * inherited user disposer stack
373
+ *
374
+ * All disposer-stack semantics (LIFO, async-await, idempotent dispose,
375
+ * `SuppressedError` on multi-throw, post-dispose `ReferenceError`) come
376
+ * from `AsyncDisposableStack` directly.
377
+ *
378
+ * @example
379
+ * ```ts
380
+ * using scope = createScope("app")
381
+ * const proc = scope.use(disposable(
382
+ * child_process.spawn("claude"),
383
+ * p => p.kill("SIGTERM"),
384
+ * ))
385
+ * ```
386
+ *
387
+ * @packageDocumentation
388
+ */
389
+ var Scope = class Scope extends AsyncDisposableStack {
390
+ signal;
391
+ name;
392
+ #children = /* @__PURE__ */ new Set();
393
+ #parent;
394
+ constructor(parent, name) {
395
+ super();
396
+ this.name = name;
397
+ this.#parent = parent;
398
+ _trackCreate(this, "scope", name);
399
+ const controller = new AbortController();
400
+ this.signal = controller.signal;
401
+ this.defer(() => controller.abort());
402
+ if (parent) {
403
+ if (parent.disposed) throw new ReferenceError("Cannot create child of disposed scope");
404
+ if (parent.signal.aborted) controller.abort();
405
+ else {
406
+ const onAbort = () => controller.abort();
407
+ parent.signal.addEventListener("abort", onAbort, { once: true });
408
+ this.defer(() => parent.signal.removeEventListener("abort", onAbort));
409
+ }
410
+ parent.#children.add(this);
411
+ }
412
+ }
413
+ /** Create a child scope. Child's signal aborts when this scope's signal does. */
414
+ child(name) {
415
+ return new Scope(this, name);
416
+ }
417
+ /**
418
+ * Schedule a one-shot callback owned by this scope. Disposing the scope
419
+ * clears the timer if it has not fired yet. The returned function cancels
420
+ * the timer early and is idempotent.
421
+ */
422
+ timeout(callback, ms, opts) {
423
+ let active = true;
424
+ const timer = setTimeout(() => {
425
+ if (!active) return;
426
+ active = false;
427
+ callback();
428
+ }, ms);
429
+ if (opts?.unref === true) timer.unref?.();
430
+ const cancel = () => {
431
+ if (!active) return;
432
+ active = false;
433
+ clearTimeout(timer);
434
+ };
435
+ this.defer(cancel);
436
+ return cancel;
437
+ }
438
+ /**
439
+ * Schedule a repeating callback owned by this scope. Disposing the scope
440
+ * clears the interval. The returned function cancels the interval early
441
+ * and is idempotent.
442
+ */
443
+ interval(callback, ms, opts) {
444
+ let active = true;
445
+ const timer = setInterval(() => {
446
+ if (active) callback();
447
+ }, ms);
448
+ if (opts?.unref === true) timer.unref?.();
449
+ const cancel = () => {
450
+ if (!active) return;
451
+ active = false;
452
+ clearInterval(timer);
453
+ };
454
+ this.defer(cancel);
455
+ return cancel;
456
+ }
457
+ /**
458
+ * Cancellation-aware sleep. Resolves when the timer fires, when the scope
459
+ * is disposed, or immediately if the scope is already aborted.
460
+ */
461
+ sleep(ms, opts) {
462
+ return new Promise((resolve) => {
463
+ if (this.signal.aborted) {
464
+ resolve();
465
+ return;
466
+ }
467
+ let done = false;
468
+ let cancelTimer = null;
469
+ const finish = () => {
470
+ if (done) return;
471
+ done = true;
472
+ cancelTimer?.();
473
+ this.signal.removeEventListener("abort", finish);
474
+ resolve();
475
+ };
476
+ this.signal.addEventListener("abort", finish, { once: true });
477
+ cancelTimer = this.timeout(finish, ms, opts);
478
+ this.defer(finish);
479
+ });
480
+ }
481
+ /**
482
+ * A debounced wrapper around `fn`, owned by this scope. Each call cancels the
483
+ * pending invocation and reschedules `ms` out, so only the last call in a
484
+ * burst fires. Uses a SINGLE timer slot — no per-call defer accumulation
485
+ * (unlike repeatedly calling {@link timeout}), which is what makes it the
486
+ * right primitive for keystroke/hover debounces. Disposing the scope cancels
487
+ * any pending call; the returned function exposes `.cancel()` to drop a
488
+ * pending call early. Resolves the structured-concurrency adoption need for
489
+ * cancel-and-reschedule timers (@km/all/structured-concurrency).
490
+ */
491
+ debounce(fn, ms, opts) {
492
+ let timer;
493
+ const cancel = () => {
494
+ if (timer !== void 0) {
495
+ clearTimeout(timer);
496
+ timer = void 0;
497
+ }
498
+ };
499
+ this.defer(cancel);
500
+ const debounced = (...args) => {
501
+ if (this.signal.aborted) return;
502
+ cancel();
503
+ timer = setTimeout(() => {
504
+ timer = void 0;
505
+ fn(...args);
506
+ }, ms);
507
+ if (opts?.unref === true) timer.unref?.();
508
+ };
509
+ debounced.cancel = cancel;
510
+ return debounced;
511
+ }
512
+ /**
513
+ * Adopt an opaque {@link Handle} (from `defineHandle()`) into this scope's
514
+ * ownership registry. The handle is added to the inherited disposer stack
515
+ * (LIFO disposal) and tracked separately so {@link assertScopeBalance}
516
+ * can detect leaked handles per-scope without depending on global GC.
517
+ *
518
+ * See `./handle.ts` for the brand-+-registry pattern.
519
+ */
520
+ adoptHandle(handle) {
521
+ adoptHandle(this, handle);
522
+ }
523
+ /**
524
+ * Adopt an `AsyncDisposable` for LIFO teardown.
525
+ *
526
+ * Branded handles (from `defineHandle()`) are routed through
527
+ * {@link adoptHandle} so per-scope accounting catches them.
528
+ * Non-branded `AsyncDisposable` values use the inherited stack directly.
529
+ *
530
+ * This closes the pro/Kimi-flagged "scope.use(handle) bypasses ownership"
531
+ * hole — a forged or hand-rolled `AsyncDisposable` claiming to be a
532
+ * branded handle still goes through the runtime authenticity gate in
533
+ * `adoptHandle`.
534
+ */
535
+ use(value) {
536
+ if (value !== null && value !== void 0 && typeof value === "object" && isBrandedHandle(value)) {
537
+ adoptHandle(this, value);
538
+ return value;
539
+ }
540
+ return super.use(value);
541
+ }
542
+ /**
543
+ * Dispose children first, then the inherited user disposer stack.
544
+ * Collects errors across the tree and surfaces them as `SuppressedError`.
545
+ *
546
+ * When `SILVERY_SCOPE_TRACE=1`, prints a per-scope handle-delta to
547
+ * stderr if any adopted handles remained undisposed after the inherited
548
+ * stack ran (km-silvery.lifecycle-leak-detection Phase 2).
549
+ */
550
+ async [Symbol.asyncDispose]() {
551
+ if (this.disposed) return;
552
+ const errors = [];
553
+ const preCount = getAdoptedHandles(this).length;
554
+ const children = [...this.#children].reverse();
555
+ this.#children.clear();
556
+ for (const c of children) try {
557
+ await c[Symbol.asyncDispose]();
558
+ } catch (e) {
559
+ errors.push(e);
560
+ }
561
+ try {
562
+ await super[Symbol.asyncDispose]();
563
+ } catch (e) {
564
+ errors.push(e);
565
+ }
566
+ const postCount = getAdoptedHandles(this).length;
567
+ reportScopeDelta(this.name, preCount, postCount);
568
+ if (this.#parent) this.#parent.#children.delete(this);
569
+ _trackDispose(this);
570
+ if (errors.length === 1) throw errors[0];
571
+ if (errors.length > 1) throw errors.reduce((suppressed, e) => new SuppressedError(e, suppressed, "multiple dispose errors"));
572
+ }
573
+ /**
574
+ * `AsyncDisposableStack.move()` returns a plain stack that loses Scope's
575
+ * `signal`, `name`, and child registry. Throw rather than silently
576
+ * corrupting invariants. Create a new scope and re-register resources
577
+ * explicitly if you need to relocate ownership.
578
+ */
579
+ move() {
580
+ throw new TypeError("Scope.move() is not supported — create a new scope and re-register resources explicitly");
581
+ }
582
+ };
583
+ /** Create a root scope. Use `scope.child(name?)` for descendants. */
584
+ function createScope(name) {
585
+ return new Scope(void 0, name);
586
+ }
587
+ const SINK_KEY = Symbol.for("@silvery/scope/disposeErrorSink");
588
+ const defaultSink = (error, context) => {
589
+ const name = context.scope?.name ?? "?";
590
+ console.error(`[scope dispose error] phase=${context.phase} scope=${name}`, error);
591
+ };
592
+ const sinkHost = globalThis;
593
+ if (!sinkHost[SINK_KEY]) sinkHost[SINK_KEY] = { sink: defaultSink };
594
+ /**
595
+ * Report a disposal failure from a fire-and-forget context (React unmount,
596
+ * signal handler, app-exit hook). Best-effort; never throws.
597
+ */
598
+ function reportDisposeError(error, context) {
599
+ try {
600
+ sinkHost[SINK_KEY].sink(error, context);
601
+ } catch {}
602
+ }
603
+ //#endregion
604
+ //#region packages/ag-term/src/pipeline/collect-text.ts
605
+ /**
606
+ * Collect plain text from a node tree, applying internal_transform.
607
+ *
608
+ * This is the base traversal used by:
609
+ * - measure-phase.ts (fit-content measurement)
610
+ * - render-text.ts (DOM-level truncation budget)
611
+ *
612
+ * Does NOT filter hidden or display:none nodes — those are handled by
613
+ * the layout engine (display:none gets 0x0 size) or by other phases.
614
+ */
615
+ function collectPlainText(node) {
616
+ if (node.textContent !== void 0) return node.textContent;
617
+ let result = "";
618
+ for (let i = 0; i < node.children.length; i++) {
619
+ const child = node.children[i];
620
+ let childText = collectPlainText(child);
621
+ if (childText.length > 0 && child.props.internal_transform) childText = child.props.internal_transform(childText, i);
622
+ result += childText;
623
+ }
624
+ return result;
625
+ }
626
+ /**
627
+ * Collect plain text from a node tree, skipping hidden children.
628
+ *
629
+ * Used by the reconciler's Yoga measure function where hidden nodes
630
+ * (from Suspense) must not contribute to measured size.
631
+ *
632
+ * Identical to collectPlainText except for the hidden check.
633
+ */
634
+ function collectPlainTextSkipHidden(node) {
635
+ if (node.textContent !== void 0) return node.textContent;
636
+ let result = "";
637
+ for (let i = 0; i < node.children.length; i++) {
638
+ const child = node.children[i];
639
+ if (child.hidden) continue;
640
+ let childText = collectPlainTextSkipHidden(child);
641
+ if (childText.length > 0 && child.props.internal_transform) childText = child.props.internal_transform(childText, i);
642
+ result += childText;
643
+ }
644
+ return result;
645
+ }
646
+ //#endregion
647
+ //#region packages/ag-term/src/pipeline/measure-stats.ts
648
+ /**
649
+ * Profiling counters for measure function performance analysis (dev only).
650
+ *
651
+ * Shared between @silvery/ag-react/reconciler/nodes (where measure happens)
652
+ * and @silvery/ag-term/pipeline/layout-phase (where stats are logged).
653
+ *
654
+ * Lives in @silvery/ag-term to keep the @silvery/ag-term barrel React-free.
655
+ */
656
+ const measureStats = {
657
+ calls: 0,
658
+ cacheHits: 0,
659
+ textCollects: 0,
660
+ displayWidthCalls: 0,
661
+ reset() {
662
+ this.calls = 0;
663
+ this.cacheHits = 0;
664
+ this.textCollects = 0;
665
+ this.displayWidthCalls = 0;
666
+ }
667
+ };
668
+ //#endregion
669
+ //#region packages/ag-react/src/reconciler/helpers.ts
670
+ /**
671
+ * Reconciler Helper Functions
672
+ *
673
+ * Utility functions for props comparison and change detection
674
+ * used by the React reconciler during updates.
675
+ */
676
+ /**
677
+ * Set of layout-affecting props.
678
+ */
679
+ const LAYOUT_PROPS = new Set([
680
+ "width",
681
+ "height",
682
+ "minWidth",
683
+ "minHeight",
684
+ "maxWidth",
685
+ "maxHeight",
686
+ "flexDirection",
687
+ "flexWrap",
688
+ "justifyContent",
689
+ "alignItems",
690
+ "alignContent",
691
+ "alignSelf",
692
+ "flexGrow",
693
+ "flexShrink",
694
+ "flexBasis",
695
+ "padding",
696
+ "paddingX",
697
+ "paddingY",
698
+ "paddingTop",
699
+ "paddingBottom",
700
+ "paddingLeft",
701
+ "paddingRight",
702
+ "margin",
703
+ "marginX",
704
+ "marginY",
705
+ "marginTop",
706
+ "marginBottom",
707
+ "marginLeft",
708
+ "marginRight",
709
+ "gap",
710
+ "columnGap",
711
+ "rowGap",
712
+ "borderStyle",
713
+ "borderTop",
714
+ "borderBottom",
715
+ "borderLeft",
716
+ "borderRight",
717
+ "display",
718
+ "position",
719
+ "top",
720
+ "left",
721
+ "bottom",
722
+ "right",
723
+ "aspectRatio",
724
+ "overflow",
725
+ "overflowX",
726
+ "overflowY",
727
+ "cols",
728
+ "rows"
729
+ ]);
730
+ /**
731
+ * Set of content props that affect layout dimensions (trigger contentDirty + Flexily markDirty()).
732
+ * wrap changes text line count; internal_transform changes text width.
733
+ */
734
+ const TEXT_CONTENT_PROPS = new Set(["wrap", "internal_transform"]);
735
+ /**
736
+ * Set of style props that affect content (paint) but NOT layout dimensions.
737
+ * borderColor, color, bold, etc. don't change how much space a node takes.
738
+ * borderStyle is also a layout prop (affects border widths), but it's included
739
+ * here so stylePropsDirty is set — otherwise border add/remove doesn't trigger
740
+ * renderBox to draw/clear border characters.
741
+ */
742
+ const STYLE_PROPS = new Set([
743
+ "color",
744
+ "backgroundColor",
745
+ "bold",
746
+ "italic",
747
+ "underline",
748
+ "underlineStyle",
749
+ "underlineColor",
750
+ "overline",
751
+ "strikethrough",
752
+ "inverse",
753
+ "borderColor",
754
+ "borderBackgroundColor",
755
+ "borderTopBackgroundColor",
756
+ "borderBottomBackgroundColor",
757
+ "borderLeftBackgroundColor",
758
+ "borderRightBackgroundColor",
759
+ "borderStyle",
760
+ "outlineStyle",
761
+ "outlineColor",
762
+ "outlineDimColor",
763
+ "outlineTop",
764
+ "outlineBottom",
765
+ "outlineLeft",
766
+ "outlineRight",
767
+ "theme"
768
+ ]);
769
+ /** Shared singleton for the no-changes fast path (identity check). */
770
+ const NO_CHANGES = {
771
+ anyChanged: false,
772
+ layoutChanged: false,
773
+ contentChanged: false
774
+ };
775
+ /**
776
+ * Classify all prop changes in a single pass over the union of old and new keys.
777
+ *
778
+ * Replaces the previous 3-pass approach (propsEqual + layoutPropsChanged +
779
+ * contentPropsChanged) with one iteration. Includes an identity fast path
780
+ * and early exit when both layout and content flags are fully determined.
781
+ */
782
+ function classifyPropChanges(oldProps, newProps) {
783
+ if (oldProps === newProps) return NO_CHANGES;
784
+ const keysA = Object.keys(oldProps);
785
+ const keysB = Object.keys(newProps);
786
+ const sameKeyCount = keysA.length === keysB.length;
787
+ let layoutChanged = false;
788
+ let contentChanged = false;
789
+ let anyChanged = false;
790
+ for (const key of keysA) if (oldProps[key] !== newProps[key]) {
791
+ anyChanged = true;
792
+ if (LAYOUT_PROPS.has(key)) layoutChanged = true;
793
+ if (contentChanged !== "text") {
794
+ if (key === "children") {
795
+ const oldIsPrimitive = typeof oldProps[key] === "string" || typeof oldProps[key] === "number";
796
+ const newIsPrimitive = typeof newProps[key] === "string" || typeof newProps[key] === "number";
797
+ if (oldIsPrimitive || newIsPrimitive) contentChanged = "text";
798
+ } else if (TEXT_CONTENT_PROPS.has(key)) contentChanged = "text";
799
+ else if (contentChanged !== "style" && STYLE_PROPS.has(key)) contentChanged = "style";
800
+ }
801
+ if (layoutChanged && contentChanged === "text") break;
802
+ }
803
+ if (!sameKeyCount) {
804
+ for (const key of keysB) if (!(key in oldProps)) {
805
+ anyChanged = true;
806
+ if (LAYOUT_PROPS.has(key)) layoutChanged = true;
807
+ if (contentChanged !== "text") {
808
+ if (key === "children") {
809
+ if (typeof newProps[key] === "string" || typeof newProps[key] === "number") contentChanged = "text";
810
+ } else if (TEXT_CONTENT_PROPS.has(key)) contentChanged = "text";
811
+ else if (contentChanged !== "style" && STYLE_PROPS.has(key)) contentChanged = "style";
812
+ }
813
+ if (layoutChanged && contentChanged === "text") break;
814
+ }
815
+ }
816
+ if (!anyChanged && !sameKeyCount) anyChanged = true;
817
+ if (!anyChanged) return NO_CHANGES;
818
+ return {
819
+ anyChanged,
820
+ layoutChanged,
821
+ contentChanged
822
+ };
823
+ }
824
+ //#endregion
825
+ //#region packages/ag-react/src/reconciler/nodes.ts
826
+ /**
827
+ * Node Creation and Layout Application
828
+ *
829
+ * Functions for creating SilveryNodes and applying layout properties.
830
+ */
831
+ const measureLog = createLogger("silvery:measure");
832
+ /**
833
+ * Create a new SilveryNode with a fresh layout node.
834
+ */
835
+ function createNode(type, props, measurer) {
836
+ const layoutNode = getLayoutEngine().createNode();
837
+ const epoch = getRenderEpoch();
838
+ const node = {
839
+ type,
840
+ props,
841
+ children: [],
842
+ parent: null,
843
+ layoutNode,
844
+ boxRect: null,
845
+ scrollRect: null,
846
+ screenRect: null,
847
+ prevLayout: null,
848
+ prevScrollRect: null,
849
+ prevScreenRect: null,
850
+ layoutChangedThisFrame: epoch,
851
+ dirtyBits: 31,
852
+ dirtyEpoch: epoch
853
+ };
854
+ if (type === "silvery-box") applyBoxProps(layoutNode, props);
855
+ else if (type === "silvery-text") applyTextFlexItemProps(layoutNode, props);
856
+ else if (type === "silvery-viewport") applyViewportProps(layoutNode, props);
857
+ else if (type === "silvery-island") applyIslandProps(layoutNode, props);
858
+ if (type === "silvery-text") {
859
+ let cachedText = null;
860
+ const measureCache = /* @__PURE__ */ new Map();
861
+ layoutNode.setMeasureFunc((width, widthMode, height, heightMode) => {
862
+ measureStats.calls++;
863
+ measureLog.debug?.(`measure "${collectPlainTextSkipHidden(node).slice(0, 40)}" width=${width} widthMode=${widthMode} height=${height} heightMode=${heightMode}`);
864
+ const cacheKey = `${width}|${widthMode}|${height}|${heightMode}`;
865
+ const cached = measureCache.get(cacheKey);
866
+ if (cached && cachedText !== null && !isDirty(node.dirtyBits, node.dirtyEpoch, 1)) {
867
+ measureStats.cacheHits++;
868
+ return cached;
869
+ }
870
+ let text;
871
+ if (cachedText !== null && !isDirty(node.dirtyBits, node.dirtyEpoch, 1)) text = cachedText;
872
+ else {
873
+ measureStats.textCollects++;
874
+ const newText = collectPlainTextSkipHidden(node);
875
+ if (newText !== cachedText) measureCache.clear();
876
+ text = newText;
877
+ cachedText = text;
878
+ node.dirtyBits &= -2;
879
+ }
880
+ if (!text) return {
881
+ width: 0,
882
+ height: 0
883
+ };
884
+ const cachedAfterCollect = measureCache.get(cacheKey);
885
+ if (cachedAfterCollect) {
886
+ measureStats.cacheHits++;
887
+ return cachedAfterCollect;
888
+ }
889
+ const lines = text.split("\n");
890
+ const isMinContentQuery = widthMode === "min-content";
891
+ const maxWidth = widthMode === "undefined" || Number.isNaN(width) ? Number.POSITIVE_INFINITY : isMinContentQuery ? 0 : width;
892
+ const { wrap } = node.props;
893
+ const isTruncate = wrap === "truncate" || wrap === "truncate-start" || wrap === "truncate-middle" || wrap === "truncate-end" || wrap === "clip" || wrap === false;
894
+ const isHardWrap = wrap === "hard";
895
+ const isWrapTruncate = wrap === "wrap-truncate";
896
+ const internalTransform = node.props.internal_transform;
897
+ let totalHeight = 0;
898
+ let actualWidth = 0;
899
+ const dw = measurer ? measurer.displayWidth.bind(measurer) : displayWidth;
900
+ const wt = measurer ? measurer.wrapText.bind(measurer) : wrapText;
901
+ const lh = getActiveLineHeight();
902
+ let renderedLineIndex = 0;
903
+ const widthFor = (line) => {
904
+ if (!internalTransform) return dw(line);
905
+ return dw(internalTransform(line, renderedLineIndex));
906
+ };
907
+ for (const line of lines) {
908
+ measureStats.displayWidthCalls++;
909
+ const lineWidth = dw(line);
910
+ if (isTruncate) {
911
+ totalHeight += lh;
912
+ if (isMinContentQuery) actualWidth = Math.max(actualWidth, lineWidth > 0 ? 1 : 0);
913
+ else actualWidth = Math.max(actualWidth, widthFor(line));
914
+ renderedLineIndex++;
915
+ } else if (isHardWrap) if (isMinContentQuery) {
916
+ totalHeight += lh;
917
+ actualWidth = Math.max(actualWidth, lineWidth > 0 ? 1 : 0);
918
+ renderedLineIndex++;
919
+ } else if (lineWidth <= maxWidth) {
920
+ totalHeight += lh;
921
+ actualWidth = Math.max(actualWidth, widthFor(line));
922
+ renderedLineIndex++;
923
+ } else if (Number.isFinite(maxWidth) && maxWidth > 0) {
924
+ const wrappedCount = Math.ceil(lineWidth / maxWidth);
925
+ totalHeight += wrappedCount * lh;
926
+ actualWidth = Math.max(actualWidth, maxWidth);
927
+ renderedLineIndex += wrappedCount;
928
+ } else {
929
+ totalHeight += lh;
930
+ actualWidth = Math.max(actualWidth, widthFor(line));
931
+ renderedLineIndex++;
932
+ }
933
+ else if (isMinContentQuery) {
934
+ let longestSegment = 0;
935
+ let segmentWidth = 0;
936
+ for (let pos = 0; pos < line.length; pos++) {
937
+ const ch = line[pos];
938
+ if (ch === " " || ch === " " || ch === "-") {
939
+ if (segmentWidth > longestSegment) longestSegment = segmentWidth;
940
+ segmentWidth = 0;
941
+ continue;
942
+ }
943
+ segmentWidth += dw(ch);
944
+ if (isSoftBreakPoint(ch)) {
945
+ if (segmentWidth > longestSegment) longestSegment = segmentWidth;
946
+ segmentWidth = 0;
947
+ }
948
+ }
949
+ if (segmentWidth > longestSegment) longestSegment = segmentWidth;
950
+ totalHeight += lh;
951
+ actualWidth = Math.max(actualWidth, longestSegment);
952
+ renderedLineIndex++;
953
+ } else if (lineWidth <= maxWidth) {
954
+ totalHeight += lh;
955
+ actualWidth = Math.max(actualWidth, widthFor(line));
956
+ renderedLineIndex++;
957
+ } else {
958
+ const wrapped = wt(line, maxWidth, false, true, isWrapTruncate);
959
+ totalHeight += wrapped.length * lh;
960
+ for (const wl of wrapped) {
961
+ actualWidth = Math.max(actualWidth, widthFor(wl));
962
+ renderedLineIndex++;
963
+ }
964
+ }
965
+ }
966
+ let resultHeight = Math.max(lh, totalHeight);
967
+ if (heightMode === "exactly" && Number.isFinite(height)) resultHeight = height;
968
+ else if (heightMode === "at-most" && Number.isFinite(height)) resultHeight = Math.min(resultHeight, height);
969
+ const result = {
970
+ width: isMinContentQuery ? actualWidth : isTruncate && true ? actualWidth : Math.min(actualWidth, maxWidth),
971
+ height: resultHeight
972
+ };
973
+ measureCache.set(cacheKey, result);
974
+ return result;
975
+ });
976
+ }
977
+ return node;
978
+ }
979
+ /**
980
+ * Create the root node for the Silvery tree.
981
+ * Root is always column (document flow is top-to-bottom), regardless of
982
+ * flexily's default flexDirection.
983
+ */
984
+ function createRootNode() {
985
+ const node = createNode("silvery-root", {});
986
+ const c = getConstants();
987
+ node.layoutNode.setFlexDirection(c.FLEX_DIRECTION_COLUMN);
988
+ return node;
989
+ }
990
+ /**
991
+ * Create a virtual text node (for nested text elements).
992
+ * Virtual text nodes don't have layout nodes and don't participate in layout.
993
+ * They're used when Text is nested inside another Text.
994
+ */
995
+ function createVirtualTextNode(props) {
996
+ return {
997
+ type: "silvery-text",
998
+ props,
999
+ children: [],
1000
+ parent: null,
1001
+ layoutNode: null,
1002
+ boxRect: null,
1003
+ scrollRect: null,
1004
+ screenRect: null,
1005
+ prevLayout: null,
1006
+ prevScrollRect: null,
1007
+ prevScreenRect: null,
1008
+ layoutChangedThisFrame: -1,
1009
+ dirtyBits: 23,
1010
+ dirtyEpoch: getRenderEpoch(),
1011
+ isRawText: false,
1012
+ inlineRects: null
1013
+ };
1014
+ }
1015
+ /**
1016
+ * Apply ViewportProps to a viewport node's layout node.
1017
+ *
1018
+ * A `<Viewport>` is a leaf with fixed `cols`×`rows` — no flex children, no
1019
+ * measure function. We pin width/height from the props so the parent layout
1020
+ * positions the viewport rect deterministically; if the parent rect is
1021
+ * narrower, flexbox + parent overflow="hidden" clips at paint time.
1022
+ *
1023
+ * See bead `@km/silvery/15513-surface-nested-composition-primitive`.
1024
+ */
1025
+ function applyViewportProps(layoutNode, props, oldProps) {
1026
+ if (props.cols !== void 0) layoutNode.setWidth(props.cols);
1027
+ else if (oldProps?.cols !== void 0) layoutNode.setWidthAuto();
1028
+ if (props.rows !== void 0) layoutNode.setHeight(props.rows);
1029
+ else if (oldProps?.rows !== void 0) layoutNode.setHeightAuto();
1030
+ }
1031
+ /**
1032
+ * Apply IslandProps to an island node's layout node.
1033
+ *
1034
+ * `<Island>` is a leaf — guest content lives off the AgNode's `islandState`
1035
+ * slot, blitted by the render phase at boxRect. Layout-dim resolution:
1036
+ * explicit `width` / `height` wins; otherwise fall back to `cols` / `rows`.
1037
+ *
1038
+ * See bead `@km/silvery/15646-islands` for the two-phase resize protocol
1039
+ * (guest acks via `IslandSizeOwner` after host writes new dims).
1040
+ */
1041
+ function applyIslandProps(layoutNode, props, oldProps) {
1042
+ const c = getConstants();
1043
+ const wasRemoved = (prop) => oldProps?.[prop] !== void 0 && props[prop] === void 0;
1044
+ if (props.width !== void 0) {
1045
+ if (typeof props.width === "string" && props.width.endsWith("%")) layoutNode.setWidthPercent(Number.parseFloat(props.width));
1046
+ else if (typeof props.width === "number") layoutNode.setWidth(props.width);
1047
+ } else if (props.cols !== void 0) layoutNode.setWidth(props.cols);
1048
+ else if (wasRemoved("width") || wasRemoved("cols")) layoutNode.setWidthAuto();
1049
+ if (props.height !== void 0) {
1050
+ if (typeof props.height === "string" && props.height.endsWith("%")) layoutNode.setHeightPercent(Number.parseFloat(props.height));
1051
+ else if (typeof props.height === "number") layoutNode.setHeight(props.height);
1052
+ } else if (props.rows !== void 0) layoutNode.setHeight(props.rows);
1053
+ else if (wasRemoved("height") || wasRemoved("rows")) layoutNode.setHeightAuto();
1054
+ if (props.flexGrow !== void 0) layoutNode.setFlexGrow(props.flexGrow);
1055
+ else if (wasRemoved("flexGrow")) layoutNode.setFlexGrow(0);
1056
+ if (props.flexShrink !== void 0) layoutNode.setFlexShrink(props.flexShrink);
1057
+ else if (wasRemoved("flexShrink")) layoutNode.setFlexShrink(1);
1058
+ if (props.flexBasis !== void 0) {
1059
+ if (typeof props.flexBasis === "string" && props.flexBasis.endsWith("%")) layoutNode.setFlexBasisPercent(Number.parseFloat(props.flexBasis));
1060
+ else if (props.flexBasis === "auto") layoutNode.setFlexBasisAuto();
1061
+ else if (typeof props.flexBasis === "number") layoutNode.setFlexBasis(props.flexBasis);
1062
+ } else if (wasRemoved("flexBasis")) layoutNode.setFlexBasisAuto();
1063
+ if (props.alignSelf !== void 0) if (props.alignSelf === "auto") layoutNode.setAlignSelf(c.ALIGN_AUTO);
1064
+ else layoutNode.setAlignSelf(alignToConstant(props.alignSelf));
1065
+ else if (wasRemoved("alignSelf")) layoutNode.setAlignSelf(c.ALIGN_AUTO);
1066
+ if (props.minWidth !== void 0) {
1067
+ if (typeof props.minWidth === "string" && props.minWidth.endsWith("%")) layoutNode.setMinWidthPercent(Number.parseFloat(props.minWidth));
1068
+ else if (typeof props.minWidth === "number") layoutNode.setMinWidth(props.minWidth);
1069
+ } else if (wasRemoved("minWidth")) layoutNode.setMinWidth(0);
1070
+ if (props.minHeight !== void 0) {
1071
+ if (typeof props.minHeight === "string" && props.minHeight.endsWith("%")) layoutNode.setMinHeightPercent(Number.parseFloat(props.minHeight));
1072
+ else if (typeof props.minHeight === "number") layoutNode.setMinHeight(props.minHeight);
1073
+ } else if (wasRemoved("minHeight")) layoutNode.setMinHeight(0);
1074
+ if (props.maxWidth !== void 0) {
1075
+ if (typeof props.maxWidth === "string" && props.maxWidth.endsWith("%")) layoutNode.setMaxWidthPercent(Number.parseFloat(props.maxWidth));
1076
+ else if (typeof props.maxWidth === "number") layoutNode.setMaxWidth(props.maxWidth);
1077
+ } else if (wasRemoved("maxWidth")) layoutNode.setMaxWidth(Number.POSITIVE_INFINITY);
1078
+ if (props.maxHeight !== void 0) {
1079
+ if (typeof props.maxHeight === "string" && props.maxHeight.endsWith("%")) layoutNode.setMaxHeightPercent(Number.parseFloat(props.maxHeight));
1080
+ else if (typeof props.maxHeight === "number") layoutNode.setMaxHeight(props.maxHeight);
1081
+ } else if (wasRemoved("maxHeight")) layoutNode.setMaxHeight(Number.POSITIVE_INFINITY);
1082
+ }
1083
+ /**
1084
+ * Apply TextFlexItemProps to a Text node's layout node.
1085
+ *
1086
+ * Text is a leaf flex item — it accepts the subset of FlexboxProps that
1087
+ * affect how it participates as a flex item (sizing, growth, shrink),
1088
+ * not the props that affect how it lays out children. This is the
1089
+ * canonical CSS escape hatch: use `flexShrink={0}` to keep a Text rigid,
1090
+ * or `minWidth={0}` to let it shrink fully under a tight parent.
1091
+ *
1092
+ * See `TextFlexItemProps` in @silvery/ag/types.
1093
+ */
1094
+ function applyTextFlexItemProps(layoutNode, props, oldProps) {
1095
+ const c = getConstants();
1096
+ const wasRemoved = (prop) => oldProps?.[prop] !== void 0 && props[prop] === void 0;
1097
+ if (props.flexGrow !== void 0) layoutNode.setFlexGrow(props.flexGrow);
1098
+ else if (wasRemoved("flexGrow")) layoutNode.setFlexGrow(0);
1099
+ if (props.flexShrink !== void 0) layoutNode.setFlexShrink(props.flexShrink);
1100
+ else if (wasRemoved("flexShrink")) layoutNode.setFlexShrink(1);
1101
+ if (props.flexBasis !== void 0) {
1102
+ if (typeof props.flexBasis === "string" && props.flexBasis.endsWith("%")) layoutNode.setFlexBasisPercent(Number.parseFloat(props.flexBasis));
1103
+ else if (props.flexBasis === "auto") layoutNode.setFlexBasisAuto();
1104
+ else if (typeof props.flexBasis === "number") layoutNode.setFlexBasis(props.flexBasis);
1105
+ } else if (wasRemoved("flexBasis")) layoutNode.setFlexBasisAuto();
1106
+ if (props.alignSelf !== void 0) if (props.alignSelf === "auto") layoutNode.setAlignSelf(c.ALIGN_AUTO);
1107
+ else layoutNode.setAlignSelf(alignToConstant(props.alignSelf));
1108
+ else if (wasRemoved("alignSelf")) layoutNode.setAlignSelf(c.ALIGN_AUTO);
1109
+ if (props.minWidth !== void 0) {
1110
+ if (typeof props.minWidth === "string" && props.minWidth.endsWith("%")) layoutNode.setMinWidthPercent(Number.parseFloat(props.minWidth));
1111
+ else if (typeof props.minWidth === "number") layoutNode.setMinWidth(props.minWidth);
1112
+ } else if (wasRemoved("minWidth")) layoutNode.setMinWidth(0);
1113
+ if (props.minHeight !== void 0) {
1114
+ if (typeof props.minHeight === "string" && props.minHeight.endsWith("%")) layoutNode.setMinHeightPercent(Number.parseFloat(props.minHeight));
1115
+ else if (typeof props.minHeight === "number") layoutNode.setMinHeight(props.minHeight);
1116
+ } else if (wasRemoved("minHeight")) layoutNode.setMinHeight(0);
1117
+ if (props.maxWidth !== void 0) {
1118
+ if (typeof props.maxWidth === "string" && props.maxWidth.endsWith("%")) layoutNode.setMaxWidthPercent(Number.parseFloat(props.maxWidth));
1119
+ else if (typeof props.maxWidth === "number") layoutNode.setMaxWidth(props.maxWidth);
1120
+ } else if (wasRemoved("maxWidth")) layoutNode.setMaxWidth(Number.POSITIVE_INFINITY);
1121
+ if (props.maxHeight !== void 0) {
1122
+ if (typeof props.maxHeight === "string" && props.maxHeight.endsWith("%")) layoutNode.setMaxHeightPercent(Number.parseFloat(props.maxHeight));
1123
+ else if (typeof props.maxHeight === "number") layoutNode.setMaxHeight(props.maxHeight);
1124
+ } else if (wasRemoved("maxHeight")) layoutNode.setMaxHeight(Number.POSITIVE_INFINITY);
1125
+ }
1126
+ /**
1127
+ * Parse a `fitWidth` entry from user-facing form (number | string) to the
1128
+ * engine's FitWidthLane shape (number | { value, unit: "cqi" | "cqmin" }).
1129
+ *
1130
+ * Accepted strings: `"100cqi"`, `"50cqmin"` — decimal and integer values both
1131
+ * fine. Numbers pass through unchanged. Invalid strings throw with a clear
1132
+ * pointer to the bug — the layout engine consumes only the parsed form, so
1133
+ * parse-fail at the React seam is the right place.
1134
+ */
1135
+ function parseFitWidthEntry(entry) {
1136
+ if (typeof entry === "number") return entry;
1137
+ const cqiMatch = entry.match(/^(\d+(?:\.\d+)?)cqi$/);
1138
+ if (cqiMatch) return {
1139
+ value: Number.parseFloat(cqiMatch[1]),
1140
+ unit: "cqi"
1141
+ };
1142
+ const cqminMatch = entry.match(/^(\d+(?:\.\d+)?)cqmin$/);
1143
+ if (cqminMatch) return {
1144
+ value: Number.parseFloat(cqminMatch[1]),
1145
+ unit: "cqmin"
1146
+ };
1147
+ throw new Error(`<Box fitWidth>: invalid lane entry ${JSON.stringify(entry)}. Expected a number (cells) or a string like "100cqi" / "50cqmin".`);
1148
+ }
1149
+ /**
1150
+ * Apply BoxProps to a layout node.
1151
+ * This maps Ink/Silvery props to the layout engine API.
1152
+ */
1153
+ function applyBoxProps(layoutNode, props, oldProps) {
1154
+ const c = getConstants();
1155
+ const wasRemoved = (prop) => oldProps?.[prop] !== void 0 && props[prop] === void 0;
1156
+ if (props.width !== void 0) {
1157
+ if (typeof props.width === "string" && props.width.endsWith("%")) layoutNode.setWidthPercent(Number.parseFloat(props.width));
1158
+ else if (typeof props.width === "number") layoutNode.setWidth(props.width);
1159
+ else if (props.width === "auto") layoutNode.setWidthAuto();
1160
+ else if (props.width === "fit-content") layoutNode.setWidthFitContent();
1161
+ else if (props.width === "snug-content") layoutNode.setWidthSnugContent();
1162
+ } else if (wasRemoved("width")) layoutNode.setWidthAuto();
1163
+ if (props.height !== void 0) {
1164
+ if (typeof props.height === "string" && props.height.endsWith("%")) layoutNode.setHeightPercent(Number.parseFloat(props.height));
1165
+ else if (typeof props.height === "number") layoutNode.setHeight(props.height);
1166
+ else if (props.height === "auto") layoutNode.setHeightAuto();
1167
+ } else if (wasRemoved("height")) layoutNode.setHeightAuto();
1168
+ if (props.minWidth !== void 0) {
1169
+ if (typeof props.minWidth === "string" && props.minWidth.endsWith("%")) layoutNode.setMinWidthPercent(Number.parseFloat(props.minWidth));
1170
+ else if (typeof props.minWidth === "number") layoutNode.setMinWidth(props.minWidth);
1171
+ } else if (wasRemoved("minWidth")) layoutNode.setMinWidth(0);
1172
+ if (props.minHeight !== void 0) {
1173
+ if (typeof props.minHeight === "string" && props.minHeight.endsWith("%")) layoutNode.setMinHeightPercent(Number.parseFloat(props.minHeight));
1174
+ else if (typeof props.minHeight === "number") layoutNode.setMinHeight(props.minHeight);
1175
+ } else if (wasRemoved("minHeight")) layoutNode.setMinHeight(0);
1176
+ if (props.maxWidth !== void 0) {
1177
+ if (typeof props.maxWidth === "string" && props.maxWidth.endsWith("%")) layoutNode.setMaxWidthPercent(Number.parseFloat(props.maxWidth));
1178
+ else if (typeof props.maxWidth === "number") layoutNode.setMaxWidth(props.maxWidth);
1179
+ } else if (wasRemoved("maxWidth")) layoutNode.setMaxWidth(Number.POSITIVE_INFINITY);
1180
+ if (props.maxHeight !== void 0) {
1181
+ if (typeof props.maxHeight === "string" && props.maxHeight.endsWith("%")) layoutNode.setMaxHeightPercent(Number.parseFloat(props.maxHeight));
1182
+ else if (typeof props.maxHeight === "number") layoutNode.setMaxHeight(props.maxHeight);
1183
+ } else if (wasRemoved("maxHeight")) layoutNode.setMaxHeight(Number.POSITIVE_INFINITY);
1184
+ if (props.flexGrow !== void 0) layoutNode.setFlexGrow(props.flexGrow);
1185
+ else if (wasRemoved("flexGrow")) layoutNode.setFlexGrow(0);
1186
+ if (props.flexShrink !== void 0) layoutNode.setFlexShrink(props.flexShrink);
1187
+ else if (wasRemoved("flexShrink")) layoutNode.setFlexShrink(1);
1188
+ if (props.flexBasis !== void 0) {
1189
+ if (typeof props.flexBasis === "string" && props.flexBasis.endsWith("%")) layoutNode.setFlexBasisPercent(Number.parseFloat(props.flexBasis));
1190
+ else if (props.flexBasis === "auto") layoutNode.setFlexBasisAuto();
1191
+ else if (typeof props.flexBasis === "number") layoutNode.setFlexBasis(props.flexBasis);
1192
+ } else if (wasRemoved("flexBasis")) layoutNode.setFlexBasisAuto();
1193
+ if (props.flexDirection !== void 0) {
1194
+ const directionMap = {
1195
+ row: c.FLEX_DIRECTION_ROW,
1196
+ column: c.FLEX_DIRECTION_COLUMN,
1197
+ "row-reverse": c.FLEX_DIRECTION_ROW_REVERSE,
1198
+ "column-reverse": c.FLEX_DIRECTION_COLUMN_REVERSE
1199
+ };
1200
+ layoutNode.setFlexDirection(directionMap[props.flexDirection] ?? c.FLEX_DIRECTION_ROW);
1201
+ } else if (wasRemoved("flexDirection")) layoutNode.setFlexDirection(c.FLEX_DIRECTION_ROW);
1202
+ if (props.flexWrap !== void 0) {
1203
+ const wrapMap = {
1204
+ nowrap: c.WRAP_NO_WRAP,
1205
+ wrap: c.WRAP_WRAP,
1206
+ "wrap-reverse": c.WRAP_WRAP_REVERSE
1207
+ };
1208
+ layoutNode.setFlexWrap(wrapMap[props.flexWrap] ?? c.WRAP_NO_WRAP);
1209
+ } else if (wasRemoved("flexWrap")) layoutNode.setFlexWrap(c.WRAP_NO_WRAP);
1210
+ if (props.alignItems !== void 0) layoutNode.setAlignItems(alignToConstant(props.alignItems));
1211
+ else if (wasRemoved("alignItems")) layoutNode.setAlignItems(c.ALIGN_STRETCH);
1212
+ if (props.alignSelf !== void 0) if (props.alignSelf === "auto") layoutNode.setAlignSelf(c.ALIGN_AUTO);
1213
+ else layoutNode.setAlignSelf(alignToConstant(props.alignSelf));
1214
+ else if (wasRemoved("alignSelf")) layoutNode.setAlignSelf(c.ALIGN_AUTO);
1215
+ if (props.alignContent !== void 0) layoutNode.setAlignContent(alignToConstant(props.alignContent));
1216
+ else if (wasRemoved("alignContent")) layoutNode.setAlignContent(c.ALIGN_FLEX_START);
1217
+ if (props.justifyContent !== void 0) layoutNode.setJustifyContent(justifyToConstant(props.justifyContent));
1218
+ else if (wasRemoved("justifyContent")) layoutNode.setJustifyContent(c.JUSTIFY_FLEX_START);
1219
+ applySpacing(layoutNode, "padding", props);
1220
+ applySpacing(layoutNode, "margin", props);
1221
+ if (props.gap !== void 0) layoutNode.setGap(c.GUTTER_ALL, props.gap);
1222
+ else if (wasRemoved("gap")) layoutNode.setGap(c.GUTTER_ALL, 0);
1223
+ if (props.columnGap !== void 0) layoutNode.setGap(c.GUTTER_COLUMN, props.columnGap);
1224
+ else if (wasRemoved("columnGap")) layoutNode.setGap(c.GUTTER_COLUMN, 0);
1225
+ if (props.rowGap !== void 0) layoutNode.setGap(c.GUTTER_ROW, props.rowGap);
1226
+ else if (wasRemoved("rowGap")) layoutNode.setGap(c.GUTTER_ROW, 0);
1227
+ if (props.display !== void 0) layoutNode.setDisplay(props.display === "none" ? c.DISPLAY_NONE : c.DISPLAY_FLEX);
1228
+ else if (wasRemoved("display")) layoutNode.setDisplay(c.DISPLAY_FLEX);
1229
+ if (props.position !== void 0) if (props.position === "absolute") layoutNode.setPositionType(c.POSITION_TYPE_ABSOLUTE);
1230
+ else if (props.position === "static") layoutNode.setPositionType(c.POSITION_TYPE_STATIC);
1231
+ else layoutNode.setPositionType(c.POSITION_TYPE_RELATIVE);
1232
+ else if (wasRemoved("position")) layoutNode.setPositionType(c.POSITION_TYPE_RELATIVE);
1233
+ if (props.position !== "static") {
1234
+ applyPositionOffset(layoutNode, c.EDGE_TOP, props.top);
1235
+ applyPositionOffset(layoutNode, c.EDGE_LEFT, props.left);
1236
+ applyPositionOffset(layoutNode, c.EDGE_BOTTOM, props.bottom);
1237
+ applyPositionOffset(layoutNode, c.EDGE_RIGHT, props.right);
1238
+ }
1239
+ if (props.aspectRatio !== void 0) layoutNode.setAspectRatio(props.aspectRatio);
1240
+ else if (wasRemoved("aspectRatio")) layoutNode.setAspectRatio(NaN);
1241
+ const effectiveOverflow = props.overflow ?? (props.overflowX === "hidden" || props.overflowY === "hidden" ? "hidden" : void 0);
1242
+ if (effectiveOverflow !== void 0) if (effectiveOverflow === "hidden") layoutNode.setOverflow(c.OVERFLOW_HIDDEN);
1243
+ else if (effectiveOverflow === "scroll") layoutNode.setOverflow(c.OVERFLOW_HIDDEN);
1244
+ else layoutNode.setOverflow(c.OVERFLOW_VISIBLE);
1245
+ else if (wasRemoved("overflow") || wasRemoved("overflowX") || wasRemoved("overflowY")) layoutNode.setOverflow(c.OVERFLOW_VISIBLE);
1246
+ if (props.containerType !== void 0) {
1247
+ requireCapability("containerQueries", "<Box containerType>");
1248
+ layoutNode.setContainerType(props.containerType === "inline-size" ? 1 : 0);
1249
+ } else if (wasRemoved("containerType")) layoutNode.setContainerType(0);
1250
+ if (props.containSize !== void 0) {
1251
+ requireCapability("containSize", "<Box containSize>");
1252
+ layoutNode.setContainSize(props.containSize);
1253
+ } else if (wasRemoved("containSize")) layoutNode.setContainSize(false);
1254
+ if (props.containerQueries !== void 0) requireCapability("containerQueries", "<Box containerQueries>");
1255
+ if (props.fitWidth !== void 0) {
1256
+ requireCapability("fitWidth", "<Box fitWidth>");
1257
+ layoutNode.setFitWidth(props.fitWidth.map(parseFitWidthEntry));
1258
+ } else if (wasRemoved("fitWidth")) layoutNode.setFitWidth(void 0);
1259
+ if (props.borderStyle) {
1260
+ const borderWidth = getActiveLineHeight() > 1 ? 0 : 1;
1261
+ if (props.borderTop !== false) layoutNode.setBorder(c.EDGE_TOP, borderWidth);
1262
+ else layoutNode.setBorder(c.EDGE_TOP, 0);
1263
+ if (props.borderBottom !== false) layoutNode.setBorder(c.EDGE_BOTTOM, borderWidth);
1264
+ else layoutNode.setBorder(c.EDGE_BOTTOM, 0);
1265
+ if (props.borderLeft !== false) layoutNode.setBorder(c.EDGE_LEFT, borderWidth);
1266
+ else layoutNode.setBorder(c.EDGE_LEFT, 0);
1267
+ if (props.borderRight !== false) layoutNode.setBorder(c.EDGE_RIGHT, borderWidth);
1268
+ else layoutNode.setBorder(c.EDGE_RIGHT, 0);
1269
+ } else {
1270
+ layoutNode.setBorder(c.EDGE_TOP, 0);
1271
+ layoutNode.setBorder(c.EDGE_BOTTOM, 0);
1272
+ layoutNode.setBorder(c.EDGE_LEFT, 0);
1273
+ layoutNode.setBorder(c.EDGE_RIGHT, 0);
1274
+ }
1275
+ }
1276
+ /**
1277
+ * Apply padding or margin to a layout node.
1278
+ */
1279
+ function applySpacing(layoutNode, type, props) {
1280
+ const c = getConstants();
1281
+ const set = type === "padding" ? layoutNode.setPadding.bind(layoutNode) : layoutNode.setMargin.bind(layoutNode);
1282
+ const all = props[type];
1283
+ const x = props[`${type}X`];
1284
+ const yy = props[`${type}Y`];
1285
+ const top = props[`${type}Top`];
1286
+ const bottom = props[`${type}Bottom`];
1287
+ const left = props[`${type}Left`];
1288
+ const right = props[`${type}Right`];
1289
+ set(c.EDGE_TOP, top ?? yy ?? all ?? 0);
1290
+ set(c.EDGE_BOTTOM, bottom ?? yy ?? all ?? 0);
1291
+ set(c.EDGE_LEFT, left ?? x ?? all ?? 0);
1292
+ set(c.EDGE_RIGHT, right ?? x ?? all ?? 0);
1293
+ }
1294
+ /**
1295
+ * Apply a position offset (top/left/bottom/right) to a layout node.
1296
+ * Supports both numeric (absolute) and percentage string values.
1297
+ */
1298
+ function applyPositionOffset(layoutNode, edge, value) {
1299
+ if (value === void 0) {
1300
+ layoutNode.setPosition(edge, NaN);
1301
+ return;
1302
+ }
1303
+ if (typeof value === "string" && value.endsWith("%")) layoutNode.setPositionPercent(edge, Number.parseFloat(value));
1304
+ else if (typeof value === "number") layoutNode.setPosition(edge, value);
1305
+ }
1306
+ /**
1307
+ * Convert align value to layout constant.
1308
+ */
1309
+ function alignToConstant(align) {
1310
+ const c = getConstants();
1311
+ return {
1312
+ "flex-start": c.ALIGN_FLEX_START,
1313
+ "flex-end": c.ALIGN_FLEX_END,
1314
+ center: c.ALIGN_CENTER,
1315
+ stretch: c.ALIGN_STRETCH,
1316
+ baseline: c.ALIGN_BASELINE,
1317
+ "space-between": c.ALIGN_SPACE_BETWEEN,
1318
+ "space-around": c.ALIGN_SPACE_AROUND,
1319
+ "space-evenly": c.ALIGN_SPACE_EVENLY
1320
+ }[align] ?? c.ALIGN_STRETCH;
1321
+ }
1322
+ /**
1323
+ * Convert justify value to layout constant.
1324
+ */
1325
+ function justifyToConstant(justify) {
1326
+ const c = getConstants();
1327
+ return {
1328
+ "flex-start": c.JUSTIFY_FLEX_START,
1329
+ "flex-end": c.JUSTIFY_FLEX_END,
1330
+ center: c.JUSTIFY_CENTER,
1331
+ "space-between": c.JUSTIFY_SPACE_BETWEEN,
1332
+ "space-around": c.JUSTIFY_SPACE_AROUND,
1333
+ "space-evenly": c.JUSTIFY_SPACE_EVENLY
1334
+ }[justify] ?? c.JUSTIFY_FLEX_START;
1335
+ }
1336
+ //#endregion
1337
+ //#region packages/ag-react/src/reconciler/host-config.ts
1338
+ /**
1339
+ * React Reconciler Host Config
1340
+ *
1341
+ * Defines how React creates, updates, and manages SilveryNodes.
1342
+ * This is the bridge between React's reconciliation algorithm
1343
+ * and our custom terminal node tree.
1344
+ */
1345
+ init_src();
1346
+ const log = createLogger("silvery:reconciler");
1347
+ const mountLog = createLogger("silvery:mount");
1348
+ const DEBUG_PROP_NAMES = [
1349
+ "id",
1350
+ "testID",
1351
+ "data-component",
1352
+ "display",
1353
+ "position",
1354
+ "flexDirection",
1355
+ "width",
1356
+ "height",
1357
+ "minWidth",
1358
+ "minHeight",
1359
+ "flexGrow",
1360
+ "flexShrink",
1361
+ "overflow",
1362
+ "overflowX",
1363
+ "overflowY",
1364
+ "overflowIndicator",
1365
+ "scrollTo",
1366
+ "scrollOffset",
1367
+ "scrollbar",
1368
+ "scrollbarVisibility",
1369
+ "follow",
1370
+ "onWheel",
1371
+ "onClick",
1372
+ "onMouseDown",
1373
+ "onMouseUp",
1374
+ "onMouseMove"
1375
+ ];
1376
+ function hostTypeLabel(type) {
1377
+ if (type === "silvery-box") return "Box";
1378
+ if (type === "silvery-text") return "Text";
1379
+ if (type === "silvery-viewport") return "Viewport";
1380
+ if (type === "silvery-island") return "Island";
1381
+ return type;
1382
+ }
1383
+ function getDebugComponentName(node) {
1384
+ const props = node.props;
1385
+ const explicitName = props["data-component"];
1386
+ if (typeof explicitName === "string" && explicitName.length > 0) return explicitName;
1387
+ const base = hostTypeLabel(node.type);
1388
+ const testID = props.testID;
1389
+ if (typeof testID === "string" && testID.length > 0) return `${base}#${testID}`;
1390
+ const id = props.id;
1391
+ if (typeof id === "string" && id.length > 0) return `${base}#${id}`;
1392
+ return base;
1393
+ }
1394
+ function summarizeDebugProp(value) {
1395
+ if (typeof value === "function") return true;
1396
+ if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
1397
+ }
1398
+ function summarizeDebugProps(props) {
1399
+ const summary = {};
1400
+ for (const key of DEBUG_PROP_NAMES) {
1401
+ const value = summarizeDebugProp(props[key]);
1402
+ if (value !== void 0) summary[key] = value;
1403
+ }
1404
+ for (const key of Object.keys(props)) {
1405
+ if (!key.startsWith("data-") || key === "data-component") continue;
1406
+ const value = summarizeDebugProp(props[key]);
1407
+ if (value !== void 0) summary[key] = value;
1408
+ }
1409
+ return summary;
1410
+ }
1411
+ function logNodeLifecycle(event, node) {
1412
+ const props = node.props;
1413
+ const component = getDebugComponentName(node);
1414
+ const text = node.textContent;
1415
+ mountLog.debug?.(`${event} ${component}`, {
1416
+ event,
1417
+ component,
1418
+ type: node.type,
1419
+ props: summarizeDebugProps(props),
1420
+ propKeys: Object.keys(props).filter((key) => key !== "children").sort(),
1421
+ ...text ? { text: text.length > 80 ? `${text.slice(0, 77)}...` : text } : {}
1422
+ });
1423
+ }
1424
+ function logUnmountSubtree(node) {
1425
+ logNodeLifecycle("unmount", node);
1426
+ for (const child of node.children) logUnmountSubtree(child);
1427
+ }
1428
+ /**
1429
+ * Normalize Ink intrinsic element types to Silvery equivalents.
1430
+ * Ink uses `ink-box` / `ink-text` as intrinsic element names;
1431
+ * Silvery uses `silvery-box` / `silvery-text`.
1432
+ */
1433
+ function normalizeNodeType(type) {
1434
+ if (type === "ink-box") return "silvery-box";
1435
+ if (type === "ink-text") return "silvery-text";
1436
+ return type;
1437
+ }
1438
+ /**
1439
+ * Callback invoked when a node is removed from the tree.
1440
+ * Used by the app layer to coordinate focus cleanup — if the focused element
1441
+ * is within a removed subtree, focus must be cleared to prevent dangling
1442
+ * references and broken navigation (indexOf → -1, hasFocusWithin lies).
1443
+ */
1444
+ let onNodeRemovedCallback = null;
1445
+ /**
1446
+ * Register a callback to be called when any node is removed from the tree.
1447
+ * Returns a cleanup function to unregister. Only one callback at a time.
1448
+ */
1449
+ function setOnNodeRemoved(callback) {
1450
+ onNodeRemovedCallback = callback;
1451
+ }
1452
+ const NODE_SCOPES_KEY = Symbol.for("@silvery/ag-react/reconciler/nodeScopes");
1453
+ const nodeScopes = globalThis[NODE_SCOPES_KEY] ?? (globalThis[NODE_SCOPES_KEY] = /* @__PURE__ */ new WeakMap());
1454
+ /**
1455
+ * Dispose any scope attached to `node` and to every descendant. Called
1456
+ * from the reconciler's unmount paths. Fire-and-forget per the design
1457
+ * contract: react commit is synchronous, scope dispose is async, so we
1458
+ * kick off the promise and route rejections through `reportDisposeError`.
1459
+ *
1460
+ * Walks the subtree synchronously so all slots are detached *before* any
1461
+ * dispose promise resolves — this prevents a re-entrant render from
1462
+ * observing a partially torn-down tree with live scope slots.
1463
+ */
1464
+ function disposeSubtreeScopes(node) {
1465
+ const scope = nodeScopes.get(node);
1466
+ if (scope) {
1467
+ nodeScopes.delete(node);
1468
+ scope[Symbol.asyncDispose]().catch((error) => reportDisposeError(error, {
1469
+ phase: "react-unmount",
1470
+ scope
1471
+ }));
1472
+ }
1473
+ for (const child of node.children) disposeSubtreeScopes(child);
1474
+ }
1475
+ /**
1476
+ * Mark this node and all ancestors as having dirty content/layout.
1477
+ * Used to enable fast-path subtree skipping in renderPhase.
1478
+ */
1479
+ function markSubtreeDirty(node) {
1480
+ const epoch = getRenderEpoch();
1481
+ while (node && !isDirty(node.dirtyBits, node.dirtyEpoch, 16)) {
1482
+ if (node.dirtyEpoch !== epoch) {
1483
+ node.dirtyBits = 16;
1484
+ node.dirtyEpoch = epoch;
1485
+ } else node.dirtyBits |= 16;
1486
+ node = node.parent;
1487
+ }
1488
+ }
1489
+ /**
1490
+ * When a child change (append/remove/insert/text-update) occurs inside a
1491
+ * virtual text subtree (no layoutNode), the nearest layout ancestor must be
1492
+ * notified so its measure function re-collects descendant text and the layout
1493
+ * engine recalculates dimensions. Without this, the measure cache stays stale
1494
+ * and renderPhase renders at the wrong size / doesn't clear old content.
1495
+ *
1496
+ * No-op when the node already has a layoutNode (normal path handles it).
1497
+ */
1498
+ function markLayoutAncestorDirty(node) {
1499
+ if (node.layoutNode) return;
1500
+ let ancestor = node.parent;
1501
+ while (ancestor && !ancestor.layoutNode) ancestor = ancestor.parent;
1502
+ if (ancestor?.layoutNode) {
1503
+ const epoch = getRenderEpoch();
1504
+ if (ancestor.dirtyEpoch !== epoch) {
1505
+ ancestor.dirtyBits = 3;
1506
+ ancestor.dirtyEpoch = epoch;
1507
+ } else ancestor.dirtyBits |= 3;
1508
+ ancestor.layoutNode.markDirty();
1509
+ trackContentDirty(ancestor);
1510
+ }
1511
+ }
1512
+ const BOX_INSIDE_TEXT_WARNING_ID = "silvery/ag-react:box-in-text";
1513
+ let currentUpdatePriority = NoEventPriority;
1514
+ /**
1515
+ * Run a callback with DiscreteEventPriority so React treats state
1516
+ * updates inside it as user-interaction priority (synchronous commit).
1517
+ * Use this for keyboard input handling to prevent React's concurrent
1518
+ * scheduler from deferring the commit.
1519
+ */
1520
+ function runWithDiscreteEvent(fn) {
1521
+ const prev = currentUpdatePriority;
1522
+ currentUpdatePriority = DiscreteEventPriority;
1523
+ try {
1524
+ fn();
1525
+ } finally {
1526
+ currentUpdatePriority = prev;
1527
+ }
1528
+ }
1529
+ /**
1530
+ * The React Reconciler host config.
1531
+ * This defines how React creates, updates, and manages our custom SilveryNodes.
1532
+ */
1533
+ const hostConfig = {
1534
+ rendererPackageName: "@silvery/ag-react",
1535
+ rendererVersion: "0.0.1",
1536
+ supportsMutation: true,
1537
+ supportsPersistence: false,
1538
+ supportsHydration: false,
1539
+ isPrimaryRenderer: true,
1540
+ scheduleTimeout: setTimeout,
1541
+ cancelTimeout: clearTimeout,
1542
+ noTimeout: -1,
1543
+ supportsMicrotasks: true,
1544
+ scheduleMicrotask: queueMicrotask,
1545
+ getRootHostContext() {
1546
+ return { isInsideText: false };
1547
+ },
1548
+ getChildHostContext(parentHostContext, type) {
1549
+ const normalizedType = normalizeNodeType(type);
1550
+ const isInsideText = parentHostContext.isInsideText || normalizedType === "silvery-text";
1551
+ if (isInsideText === parentHostContext.isInsideText) return parentHostContext;
1552
+ return { isInsideText };
1553
+ },
1554
+ createInstance(type, props, _rootContainer, hostContext) {
1555
+ type = normalizeNodeType(type);
1556
+ if ("style" in props && props.style && typeof props.style === "object") props = {
1557
+ ...props.style,
1558
+ ...props
1559
+ };
1560
+ if (type === "silvery-box" && hostContext.isInsideText) {
1561
+ if (process.env.NODE_ENV !== "production") warnOnce(BOX_INSIDE_TEXT_WARNING_ID, () => log.warn?.("<Box> cannot be nested inside <Text>. This produces undefined layout behavior."));
1562
+ }
1563
+ if (type === "silvery-text" && hostContext.isInsideText) {
1564
+ const node = createVirtualTextNode(props);
1565
+ logNodeLifecycle("mount", node);
1566
+ return node;
1567
+ }
1568
+ const node = createNode(type, props);
1569
+ logNodeLifecycle("mount", node);
1570
+ return node;
1571
+ },
1572
+ createTextInstance(text, _rootContainer, hostContext) {
1573
+ const epoch = getRenderEpoch();
1574
+ const node = {
1575
+ type: "silvery-text",
1576
+ props: { children: text },
1577
+ children: [],
1578
+ parent: null,
1579
+ layoutNode: null,
1580
+ boxRect: null,
1581
+ scrollRect: null,
1582
+ screenRect: null,
1583
+ prevLayout: null,
1584
+ prevScrollRect: null,
1585
+ prevScreenRect: null,
1586
+ layoutChangedThisFrame: -1,
1587
+ dirtyBits: 23,
1588
+ dirtyEpoch: epoch,
1589
+ textContent: text,
1590
+ isRawText: true
1591
+ };
1592
+ logNodeLifecycle("mount", node);
1593
+ return node;
1594
+ },
1595
+ appendChild(parentInstance, child) {
1596
+ const existingIndex = parentInstance.children.indexOf(child);
1597
+ if (existingIndex !== -1) {
1598
+ parentInstance.children.splice(existingIndex, 1);
1599
+ if (parentInstance.layoutNode && child.layoutNode) parentInstance.layoutNode.removeChild(child.layoutNode);
1600
+ }
1601
+ child.parent = parentInstance;
1602
+ parentInstance.children.push(child);
1603
+ if (parentInstance.layoutNode && child.layoutNode) {
1604
+ const layoutIndex = parentInstance.children.filter((c) => c.layoutNode !== null).length - 1;
1605
+ parentInstance.layoutNode.insertChild(child.layoutNode, layoutIndex);
1606
+ }
1607
+ {
1608
+ const epoch = getRenderEpoch();
1609
+ const bits = 9;
1610
+ parentInstance.dirtyBits = parentInstance.dirtyEpoch !== epoch ? bits : parentInstance.dirtyBits | bits;
1611
+ parentInstance.dirtyEpoch = epoch;
1612
+ }
1613
+ parentInstance.layoutNode?.markDirty();
1614
+ trackContentDirty(parentInstance);
1615
+ markLayoutAncestorDirty(parentInstance);
1616
+ markSubtreeDirty(parentInstance);
1617
+ },
1618
+ appendInitialChild(parentInstance, child) {
1619
+ child.parent = parentInstance;
1620
+ parentInstance.children.push(child);
1621
+ if (parentInstance.layoutNode && child.layoutNode) {
1622
+ const layoutIndex = parentInstance.children.filter((c) => c.layoutNode !== null).length - 1;
1623
+ parentInstance.layoutNode.insertChild(child.layoutNode, layoutIndex);
1624
+ }
1625
+ },
1626
+ appendChildToContainer(container, child) {
1627
+ const existingIndex = container.root.children.indexOf(child);
1628
+ if (existingIndex !== -1) {
1629
+ container.root.children.splice(existingIndex, 1);
1630
+ if (container.root.layoutNode && child.layoutNode) container.root.layoutNode.removeChild(child.layoutNode);
1631
+ }
1632
+ child.parent = container.root;
1633
+ container.root.children.push(child);
1634
+ if (container.root.layoutNode && child.layoutNode) {
1635
+ const layoutIndex = container.root.children.filter((c) => c.layoutNode !== null).length - 1;
1636
+ container.root.layoutNode.insertChild(child.layoutNode, layoutIndex);
1637
+ }
1638
+ {
1639
+ const epoch = getRenderEpoch();
1640
+ const bits = 9;
1641
+ container.root.dirtyBits = container.root.dirtyEpoch !== epoch ? bits : container.root.dirtyBits | bits;
1642
+ container.root.dirtyEpoch = epoch;
1643
+ }
1644
+ container.root.layoutNode?.markDirty();
1645
+ trackContentDirty(container.root);
1646
+ markSubtreeDirty(container.root);
1647
+ },
1648
+ removeChild(parentInstance, child) {
1649
+ const index = parentInstance.children.indexOf(child);
1650
+ if (index !== -1) {
1651
+ onNodeRemovedCallback?.(child);
1652
+ logUnmountSubtree(child);
1653
+ disposeSubtreeScopes(child);
1654
+ parentInstance.children.splice(index, 1);
1655
+ if (parentInstance.layoutNode && child.layoutNode) {
1656
+ parentInstance.layoutNode.removeChild(child.layoutNode);
1657
+ child.layoutNode.free();
1658
+ }
1659
+ child.parent = null;
1660
+ {
1661
+ const epoch = getRenderEpoch();
1662
+ const bits = 9;
1663
+ parentInstance.dirtyBits = parentInstance.dirtyEpoch !== epoch ? bits : parentInstance.dirtyBits | bits;
1664
+ parentInstance.dirtyEpoch = epoch;
1665
+ }
1666
+ parentInstance.layoutNode?.markDirty();
1667
+ trackContentDirty(parentInstance);
1668
+ markLayoutAncestorDirty(parentInstance);
1669
+ markSubtreeDirty(parentInstance);
1670
+ }
1671
+ },
1672
+ removeChildFromContainer(container, child) {
1673
+ const index = container.root.children.indexOf(child);
1674
+ if (index !== -1) {
1675
+ onNodeRemovedCallback?.(child);
1676
+ logUnmountSubtree(child);
1677
+ disposeSubtreeScopes(child);
1678
+ container.root.children.splice(index, 1);
1679
+ if (container.root.layoutNode && child.layoutNode) {
1680
+ container.root.layoutNode.removeChild(child.layoutNode);
1681
+ child.layoutNode.free();
1682
+ }
1683
+ child.parent = null;
1684
+ {
1685
+ const epoch = getRenderEpoch();
1686
+ const bits = 9;
1687
+ container.root.dirtyBits = container.root.dirtyEpoch !== epoch ? bits : container.root.dirtyBits | bits;
1688
+ container.root.dirtyEpoch = epoch;
1689
+ }
1690
+ container.root.layoutNode?.markDirty();
1691
+ trackContentDirty(container.root);
1692
+ markSubtreeDirty(container.root);
1693
+ }
1694
+ },
1695
+ insertBefore(parentInstance, child, beforeChild) {
1696
+ const existingIndex = parentInstance.children.indexOf(child);
1697
+ if (existingIndex !== -1) {
1698
+ parentInstance.children.splice(existingIndex, 1);
1699
+ if (parentInstance.layoutNode && child.layoutNode) parentInstance.layoutNode.removeChild(child.layoutNode);
1700
+ }
1701
+ const beforeIndex = parentInstance.children.indexOf(beforeChild);
1702
+ if (beforeIndex !== -1) {
1703
+ child.parent = parentInstance;
1704
+ parentInstance.children.splice(beforeIndex, 0, child);
1705
+ if (parentInstance.layoutNode && child.layoutNode) {
1706
+ const layoutIndex = parentInstance.children.slice(0, beforeIndex).filter((c) => c.layoutNode !== null).length;
1707
+ parentInstance.layoutNode.insertChild(child.layoutNode, layoutIndex);
1708
+ }
1709
+ {
1710
+ const epoch = getRenderEpoch();
1711
+ const bits = 9;
1712
+ parentInstance.dirtyBits = parentInstance.dirtyEpoch !== epoch ? bits : parentInstance.dirtyBits | bits;
1713
+ parentInstance.dirtyEpoch = epoch;
1714
+ }
1715
+ parentInstance.layoutNode?.markDirty();
1716
+ trackContentDirty(parentInstance);
1717
+ markLayoutAncestorDirty(parentInstance);
1718
+ markSubtreeDirty(parentInstance);
1719
+ }
1720
+ },
1721
+ insertInContainerBefore(container, child, beforeChild) {
1722
+ const existingIndex = container.root.children.indexOf(child);
1723
+ if (existingIndex !== -1) {
1724
+ container.root.children.splice(existingIndex, 1);
1725
+ if (container.root.layoutNode && child.layoutNode) container.root.layoutNode.removeChild(child.layoutNode);
1726
+ }
1727
+ const beforeIndex = container.root.children.indexOf(beforeChild);
1728
+ if (beforeIndex !== -1) {
1729
+ child.parent = container.root;
1730
+ container.root.children.splice(beforeIndex, 0, child);
1731
+ if (container.root.layoutNode && child.layoutNode) {
1732
+ const layoutIndex = container.root.children.slice(0, beforeIndex).filter((c) => c.layoutNode !== null).length;
1733
+ container.root.layoutNode.insertChild(child.layoutNode, layoutIndex);
1734
+ }
1735
+ {
1736
+ const epoch = getRenderEpoch();
1737
+ const bits = 9;
1738
+ container.root.dirtyBits = container.root.dirtyEpoch !== epoch ? bits : container.root.dirtyBits | bits;
1739
+ container.root.dirtyEpoch = epoch;
1740
+ }
1741
+ container.root.layoutNode?.markDirty();
1742
+ trackContentDirty(container.root);
1743
+ markSubtreeDirty(container.root);
1744
+ }
1745
+ },
1746
+ prepareUpdate(_instance, _type, oldProps, newProps) {
1747
+ return classifyPropChanges(oldProps, newProps).anyChanged;
1748
+ },
1749
+ commitUpdate(instance, _type, oldProps, newProps, _finishedWork) {
1750
+ if ("style" in oldProps && oldProps.style && typeof oldProps.style === "object") oldProps = {
1751
+ ...oldProps.style,
1752
+ ...oldProps
1753
+ };
1754
+ if ("style" in newProps && newProps.style && typeof newProps.style === "object") newProps = {
1755
+ ...newProps.style,
1756
+ ...newProps
1757
+ };
1758
+ const { anyChanged, layoutChanged, contentChanged } = classifyPropChanges(oldProps, newProps);
1759
+ if (!anyChanged) {
1760
+ instance.props = newProps;
1761
+ return;
1762
+ }
1763
+ if (layoutChanged) {
1764
+ if (instance.layoutNode) {
1765
+ if (instance.type === "silvery-text") applyTextFlexItemProps(instance.layoutNode, newProps, oldProps);
1766
+ else if (instance.type === "silvery-viewport") applyViewportProps(instance.layoutNode, newProps, oldProps);
1767
+ else if (instance.type === "silvery-island") applyIslandProps(instance.layoutNode, newProps, oldProps);
1768
+ else applyBoxProps(instance.layoutNode, newProps, oldProps);
1769
+ instance.layoutNode.markDirty();
1770
+ }
1771
+ }
1772
+ if (contentChanged) {
1773
+ const epoch = getRenderEpoch();
1774
+ let bits = 2;
1775
+ if (contentChanged === "text") {
1776
+ bits |= 1;
1777
+ if (instance.layoutNode) instance.layoutNode.markDirty();
1778
+ }
1779
+ if (oldProps.backgroundColor !== newProps.backgroundColor) bits |= 4;
1780
+ if (oldProps.borderStyle && !newProps.borderStyle) bits |= 4;
1781
+ if (oldProps.theme !== newProps.theme) bits |= 5;
1782
+ instance.dirtyBits = instance.dirtyEpoch !== epoch ? bits : instance.dirtyBits | bits;
1783
+ instance.dirtyEpoch = epoch;
1784
+ }
1785
+ if (contentChanged) trackContentDirty(instance);
1786
+ if (contentChanged === "style" && !layoutChanged && !isDirty(instance.dirtyBits, instance.dirtyEpoch, 4) && !isDirty(instance.dirtyBits, instance.dirtyEpoch, 1) && !isDirty(instance.dirtyBits, instance.dirtyEpoch, 8)) trackStyleOnlyDirty(instance);
1787
+ instance.props = newProps;
1788
+ logNodeLifecycle("update", instance);
1789
+ const scrollToChanged = oldProps.scrollTo !== newProps.scrollTo;
1790
+ const scrollOffsetChanged = oldProps.scrollOffset !== newProps.scrollOffset;
1791
+ if (scrollToChanged || scrollOffsetChanged) trackScrollDirty(instance);
1792
+ if (layoutChanged || contentChanged || scrollToChanged || scrollOffsetChanged) {
1793
+ markLayoutAncestorDirty(instance);
1794
+ markSubtreeDirty(instance);
1795
+ }
1796
+ },
1797
+ commitTextUpdate(textInstance, _oldText, newText) {
1798
+ textInstance.textContent = newText;
1799
+ syncTextContentSignal(textInstance);
1800
+ textInstance.props = { children: newText };
1801
+ const epoch = getRenderEpoch();
1802
+ const bits = 3;
1803
+ textInstance.dirtyBits = textInstance.dirtyEpoch !== epoch ? bits : textInstance.dirtyBits | bits;
1804
+ textInstance.dirtyEpoch = epoch;
1805
+ trackContentDirty(textInstance);
1806
+ markLayoutAncestorDirty(textInstance);
1807
+ markSubtreeDirty(textInstance);
1808
+ },
1809
+ finalizeInitialChildren() {
1810
+ return false;
1811
+ },
1812
+ prepareForCommit() {
1813
+ return null;
1814
+ },
1815
+ resetAfterCommit(container) {
1816
+ const budgetEnv = typeof process !== "undefined" ? process.env.SILVERY_RENDER_BUDGET_MS : void 0;
1817
+ const budget = budgetEnv !== void 0 ? Number(budgetEnv) : 0;
1818
+ if (budget > 0 && Number.isFinite(budget)) {
1819
+ const start = performance.now();
1820
+ container.onRender();
1821
+ const elapsed = performance.now() - start;
1822
+ if (elapsed > budget) console.warn(`[silvery] render commit exceeded budget: ${elapsed.toFixed(0)}ms > ${budget}ms — see @km/board/projection-stability-improvements for cascade-prevention guidance`);
1823
+ } else container.onRender();
1824
+ },
1825
+ getPublicInstance(instance) {
1826
+ return instance;
1827
+ },
1828
+ shouldSetTextContent() {
1829
+ return false;
1830
+ },
1831
+ clearContainer(container) {
1832
+ for (const child of container.root.children) {
1833
+ onNodeRemovedCallback?.(child);
1834
+ logUnmountSubtree(child);
1835
+ }
1836
+ disposeSubtreeScopes(container.root);
1837
+ for (const child of container.root.children) if (container.root.layoutNode && child.layoutNode) {
1838
+ container.root.layoutNode.removeChild(child.layoutNode);
1839
+ child.layoutNode.free();
1840
+ }
1841
+ container.root.children = [];
1842
+ {
1843
+ const epoch = getRenderEpoch();
1844
+ const bits = 9;
1845
+ container.root.dirtyBits = container.root.dirtyEpoch !== epoch ? bits : container.root.dirtyBits | bits;
1846
+ container.root.dirtyEpoch = epoch;
1847
+ }
1848
+ container.root.layoutNode?.markDirty();
1849
+ trackContentDirty(container.root);
1850
+ markSubtreeDirty(container.root);
1851
+ },
1852
+ preparePortalMount() {},
1853
+ getCurrentEventPriority() {
1854
+ if (currentUpdatePriority !== NoEventPriority) return currentUpdatePriority;
1855
+ return DefaultEventPriority;
1856
+ },
1857
+ getInstanceFromNode() {
1858
+ return null;
1859
+ },
1860
+ beforeActiveInstanceBlur() {},
1861
+ afterActiveInstanceBlur() {},
1862
+ prepareScopeUpdate() {},
1863
+ getInstanceFromScope() {
1864
+ return null;
1865
+ },
1866
+ detachDeletedInstance(node) {
1867
+ disposeSubtreeScopes(node);
1868
+ },
1869
+ setCurrentUpdatePriority(newPriority) {
1870
+ currentUpdatePriority = newPriority;
1871
+ },
1872
+ getCurrentUpdatePriority() {
1873
+ return currentUpdatePriority;
1874
+ },
1875
+ resolveUpdatePriority() {
1876
+ if (currentUpdatePriority !== NoEventPriority) return currentUpdatePriority;
1877
+ return DefaultEventPriority;
1878
+ },
1879
+ maySuspendCommit() {
1880
+ return false;
1881
+ },
1882
+ NotPendingTransition: null,
1883
+ HostTransitionContext: createContext(null),
1884
+ resetFormInstance() {},
1885
+ requestPostPaintCallback() {},
1886
+ shouldAttemptEagerTransition() {
1887
+ return false;
1888
+ },
1889
+ trackSchedulerEvent() {},
1890
+ resolveEventType() {
1891
+ return null;
1892
+ },
1893
+ resolveEventTimeStamp() {
1894
+ return -1.1;
1895
+ },
1896
+ preloadInstance() {
1897
+ return true;
1898
+ },
1899
+ startSuspendingCommit() {},
1900
+ suspendInstance() {},
1901
+ waitForCommitToBeReady() {
1902
+ return null;
1903
+ },
1904
+ hideInstance(instance) {
1905
+ instance.hidden = true;
1906
+ const epoch = getRenderEpoch();
1907
+ const bits = 3;
1908
+ instance.dirtyBits = instance.dirtyEpoch !== epoch ? bits : instance.dirtyBits | bits;
1909
+ instance.dirtyEpoch = epoch;
1910
+ if (instance.layoutNode) instance.layoutNode.markDirty();
1911
+ trackContentDirty(instance);
1912
+ if (instance.parent) {
1913
+ if (instance.parent.dirtyEpoch !== epoch) {
1914
+ instance.parent.dirtyBits = 1;
1915
+ instance.parent.dirtyEpoch = epoch;
1916
+ } else instance.parent.dirtyBits |= 1;
1917
+ trackContentDirty(instance.parent);
1918
+ }
1919
+ markLayoutAncestorDirty(instance);
1920
+ markSubtreeDirty(instance);
1921
+ },
1922
+ unhideInstance(instance, _props) {
1923
+ instance.hidden = false;
1924
+ const epoch = getRenderEpoch();
1925
+ const bits = 3;
1926
+ instance.dirtyBits = instance.dirtyEpoch !== epoch ? bits : instance.dirtyBits | bits;
1927
+ instance.dirtyEpoch = epoch;
1928
+ if (instance.layoutNode) instance.layoutNode.markDirty();
1929
+ trackContentDirty(instance);
1930
+ if (instance.parent) {
1931
+ if (instance.parent.dirtyEpoch !== epoch) {
1932
+ instance.parent.dirtyBits = 1;
1933
+ instance.parent.dirtyEpoch = epoch;
1934
+ } else instance.parent.dirtyBits |= 1;
1935
+ trackContentDirty(instance.parent);
1936
+ }
1937
+ markLayoutAncestorDirty(instance);
1938
+ markSubtreeDirty(instance);
1939
+ },
1940
+ hideTextInstance(textInstance) {
1941
+ textInstance.hidden = true;
1942
+ const epoch = getRenderEpoch();
1943
+ const bits = 3;
1944
+ textInstance.dirtyBits = textInstance.dirtyEpoch !== epoch ? bits : textInstance.dirtyBits | bits;
1945
+ textInstance.dirtyEpoch = epoch;
1946
+ trackContentDirty(textInstance);
1947
+ if (textInstance.parent) {
1948
+ if (textInstance.parent.dirtyEpoch !== epoch) {
1949
+ textInstance.parent.dirtyBits = 1;
1950
+ textInstance.parent.dirtyEpoch = epoch;
1951
+ } else textInstance.parent.dirtyBits |= 1;
1952
+ trackContentDirty(textInstance.parent);
1953
+ }
1954
+ markLayoutAncestorDirty(textInstance);
1955
+ markSubtreeDirty(textInstance);
1956
+ },
1957
+ unhideTextInstance(textInstance, _text) {
1958
+ textInstance.hidden = false;
1959
+ const epoch = getRenderEpoch();
1960
+ const bits = 3;
1961
+ textInstance.dirtyBits = textInstance.dirtyEpoch !== epoch ? bits : textInstance.dirtyBits | bits;
1962
+ textInstance.dirtyEpoch = epoch;
1963
+ trackContentDirty(textInstance);
1964
+ if (textInstance.parent) {
1965
+ if (textInstance.parent.dirtyEpoch !== epoch) {
1966
+ textInstance.parent.dirtyBits = 1;
1967
+ textInstance.parent.dirtyEpoch = epoch;
1968
+ } else textInstance.parent.dirtyBits |= 1;
1969
+ trackContentDirty(textInstance.parent);
1970
+ }
1971
+ markLayoutAncestorDirty(textInstance);
1972
+ markSubtreeDirty(textInstance);
1973
+ }
1974
+ };
1975
+ //#endregion
1976
+ //#region packages/ag-react/src/reconciler/index.ts
1977
+ /**
1978
+ * Silvery React Reconciler
1979
+ *
1980
+ * Custom React reconciler that builds a tree of SilveryNodes, each with a Yoga layout node.
1981
+ * This is the core of Silvery's architecture - separating structure (React reconciliation)
1982
+ * from content (terminal rendering).
1983
+ *
1984
+ * The reconciler creates SilveryNodes during React's reconciliation phase,
1985
+ * but actual terminal content is rendered later after Yoga computes layout.
1986
+ */
1987
+ /**
1988
+ * Create the React reconciler instance.
1989
+ */
1990
+ const reconciler = Reconciler(hostConfig);
1991
+ /**
1992
+ * Create a container for rendering.
1993
+ */
1994
+ function createContainer(onRender) {
1995
+ return {
1996
+ root: createRootNode(),
1997
+ onRender
1998
+ };
1999
+ }
2000
+ /**
2001
+ * Create a React fiber root for a container (wraps the 10-argument reconciler call).
2002
+ *
2003
+ * Pass `options.onUncaughtError` to route React-thrown render errors back to
2004
+ * the host's panic path. Without it, render errors are silently swallowed
2005
+ * (the default `() => {}` callback) and the only surface for them is whatever
2006
+ * the host's `console.error` capture decides to do — typically an altscreen
2007
+ * overlay the user can't copy-paste or screenshot reliably.
2008
+ */
2009
+ function createFiberRoot(container, options = {}) {
2010
+ return reconciler.createContainer(container, 1, null, false, null, "", options.onUncaughtError ?? (() => {}), options.onCaughtError ?? (() => {}), options.onRecoverableError ?? (() => {}), null);
2011
+ }
2012
+ /**
2013
+ * Get the root SilveryNode from a container.
2014
+ */
2015
+ function getContainerRoot(container) {
2016
+ return container.root;
2017
+ }
2018
+ /**
2019
+ * Synchronously unmount a fiber root and scrub the container so it can't
2020
+ * keep its closure-captured RenderInstance alive afterward.
2021
+ *
2022
+ * Why both steps are needed:
2023
+ *
2024
+ * 1. `createFiberRoot` uses `ConcurrentRoot` (mode 1). React's async
2025
+ * `updateContainer(null, fiberRoot, ...)` does NOT run layout-effect
2026
+ * cleanups before returning — useLayoutEffect / useBoxRect /
2027
+ * useBoxMetrics / signal-effect disposers are scheduled but may not
2028
+ * fire promptly. That keeps signal subscriptions alive past unmount,
2029
+ * which keeps the React tree reachable, which keeps the host
2030
+ * `RenderInstance` reachable. `updateContainerSync` + `flushSyncWork`
2031
+ * forces all cleanups to run inline.
2032
+ *
2033
+ * 2. Even after the React tree is detached, the `FiberRoot` keeps a
2034
+ * pointer to its `containerInfo` (our `Container`) for some time, and
2035
+ * `Container.onRender` typically closes over the entire enclosing
2036
+ * `RenderInstance`. Without nulling `onRender` and scrubbing the root
2037
+ * AgNode, the instance graph is still reachable through the FiberRoot's
2038
+ * container pointer.
2039
+ *
2040
+ * Call this in every unmount path that uses ConcurrentRoot. The previous
2041
+ * (async) pattern leaked across mount/unmount cycles in tests and likely
2042
+ * in production long-lived host applications too.
2043
+ *
2044
+ * Safe to call multiple times — `releaseContainer` is idempotent (the
2045
+ * scrub fields are nulled and `layoutNode.free()` is best-effort).
2046
+ *
2047
+ * @param fiberRoot — opaque React FiberRoot returned by `createFiberRoot`
2048
+ * @param container — the `Container` paired with that fiberRoot
2049
+ */
2050
+ function unmountFiberRoot(fiberRoot, container) {
2051
+ reconciler.updateContainerSync(null, fiberRoot, null, null);
2052
+ reconciler.flushSyncWork();
2053
+ releaseContainer(container);
2054
+ }
2055
+ /**
2056
+ * Scrub a Container so it can't retain its enclosing render state after
2057
+ * the React tree has been unmounted. See {@link unmountFiberRoot} for the
2058
+ * full rationale; call this directly only if you've already run a sync
2059
+ * unmount through the reconciler and just need the post-commit scrub.
2060
+ */
2061
+ function releaseContainer(container) {
2062
+ disposeSubtreeScopes(container.root);
2063
+ container.onRender = () => {};
2064
+ const root = container.root;
2065
+ root.children = [];
2066
+ root.parent = null;
2067
+ root.boxRect = null;
2068
+ root.scrollRect = null;
2069
+ root.screenRect = null;
2070
+ root.prevLayout = null;
2071
+ root.prevScrollRect = null;
2072
+ root.prevScreenRect = null;
2073
+ if (root.layoutNode) {
2074
+ try {
2075
+ root.layoutNode.free();
2076
+ } catch {}
2077
+ root.layoutNode = null;
2078
+ }
2079
+ }
2080
+ //#endregion
2081
+ export { trackContentDirty as S, isAnyDirty as _, unmountFiberRoot as a, clearDirtyTracking as b, setOnNodeRemoved as c, createScope as d, reportDisposeError as f, getRenderEpoch as g, advanceRenderEpoch as h, reconciler as i, measureStats as l, finaliseHandle as m, createFiberRoot as n, hostConfig as o, defineHandle as p, getContainerRoot as r, runWithDiscreteEvent as s, createContainer as t, collectPlainText as u, isCurrentEpoch as v, hasScrollDirty as x, isDirty as y };
2082
+
2083
+ //# sourceMappingURL=reconciler-DldIJB93.mjs.map