sygnal 7.0.0 → 7.2.0-preview.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 (343) hide show
  1. package/CHANGELOG.md +632 -0
  2. package/dist/agent-ui.cjs.js +592 -0
  3. package/dist/agent-ui.cjs.js.map +1 -0
  4. package/dist/agent-ui.esm.js +582 -0
  5. package/dist/agent-ui.esm.js.map +1 -0
  6. package/dist/api.cjs.js +63 -0
  7. package/dist/api.cjs.js.map +1 -0
  8. package/dist/api.esm.js +2 -0
  9. package/dist/api.esm.js.map +1 -0
  10. package/dist/astro/index.cjs.js +968 -4
  11. package/dist/astro/index.cjs.js.map +1 -1
  12. package/dist/astro/index.mjs +968 -4
  13. package/dist/astro/index.mjs.map +1 -1
  14. package/dist/astro/server.cjs.js +73 -45
  15. package/dist/astro/server.cjs.js.map +1 -1
  16. package/dist/astro/server.mjs +73 -45
  17. package/dist/astro/server.mjs.map +1 -1
  18. package/dist/canvas.cjs.js +2919 -0
  19. package/dist/canvas.cjs.js.map +1 -0
  20. package/dist/canvas.esm.js +2866 -0
  21. package/dist/canvas.esm.js.map +1 -0
  22. package/dist/devtools.cjs.js +38 -6
  23. package/dist/devtools.cjs.js.map +1 -1
  24. package/dist/devtools.esm.js +38 -6
  25. package/dist/devtools.esm.js.map +1 -1
  26. package/dist/diagnostics.cjs.js +224 -20
  27. package/dist/diagnostics.cjs.js.map +1 -1
  28. package/dist/diagnostics.esm.js +224 -20
  29. package/dist/diagnostics.esm.js.map +1 -1
  30. package/dist/guide/accessibility.md +1 -1
  31. package/dist/guide/ai-compose.md +150 -0
  32. package/dist/guide/behaviors.md +2 -1
  33. package/dist/guide/collaborative-state.md +265 -0
  34. package/dist/guide/element-commands.md +36 -2
  35. package/dist/guide/forms-reference.md +5 -5
  36. package/dist/guide/forms.md +3 -2
  37. package/dist/guide/http.md +28 -1
  38. package/dist/guide/islands.md +240 -0
  39. package/dist/guide/recipes/code-editor.md +1 -1
  40. package/dist/guide/renderers/agent-ui.md +138 -0
  41. package/dist/guide/renderers/custom-targets.md +126 -0
  42. package/dist/guide/renderers/overview.md +120 -0
  43. package/dist/guide/renderers/react-native.md +128 -0
  44. package/dist/guide/renderers/terminal.md +306 -0
  45. package/dist/guide/renderers/three.md +332 -0
  46. package/dist/guide/renderers/write-once.md +137 -0
  47. package/dist/guide/resources.md +2 -0
  48. package/dist/guide/sandboxed-components.md +223 -0
  49. package/dist/guide/timers.md +32 -5
  50. package/dist/guide/typed-apis.md +442 -0
  51. package/dist/guide/ui/accordion.md +11 -0
  52. package/dist/guide/ui/combobox.md +112 -8
  53. package/dist/guide/ui/dialog.md +96 -20
  54. package/dist/guide/ui/menu.md +57 -5
  55. package/dist/guide/ui/overview.md +8 -4
  56. package/dist/guide/ui/popover.md +69 -9
  57. package/dist/guide/ui/select.md +54 -4
  58. package/dist/guide/ui/tabs.md +1 -1
  59. package/dist/guide/ui/tooltip.md +7 -5
  60. package/dist/guide/virtual-collections.md +3 -0
  61. package/dist/guide/widgets.md +90 -1
  62. package/dist/host.cjs.js +2181 -0
  63. package/dist/host.cjs.js.map +1 -0
  64. package/dist/host.esm.js +2146 -0
  65. package/dist/host.esm.js.map +1 -0
  66. package/dist/index.cjs.js +3948 -586
  67. package/dist/index.cjs.js.map +1 -1
  68. package/dist/index.d.ts +584 -44
  69. package/dist/index.esm.js +3918 -586
  70. package/dist/index.esm.js.map +1 -1
  71. package/dist/primitives.cjs.js +143 -0
  72. package/dist/primitives.cjs.js.map +1 -0
  73. package/dist/primitives.esm.js +133 -0
  74. package/dist/primitives.esm.js.map +1 -0
  75. package/dist/react-native.cjs.js +248 -0
  76. package/dist/react-native.cjs.js.map +1 -0
  77. package/dist/react-native.esm.js +244 -0
  78. package/dist/react-native.esm.js.map +1 -0
  79. package/dist/replay.cjs.js +39 -0
  80. package/dist/replay.cjs.js.map +1 -0
  81. package/dist/replay.esm.js +2 -0
  82. package/dist/replay.esm.js.map +1 -0
  83. package/dist/sandbox-build.cjs.js +140 -0
  84. package/dist/sandbox-build.cjs.js.map +1 -0
  85. package/dist/sandbox-build.esm.js +137 -0
  86. package/dist/sandbox-build.esm.js.map +1 -0
  87. package/dist/sandbox-kernel.esm.js +212 -0
  88. package/dist/sandbox-kernel.esm.js.map +1 -0
  89. package/dist/sandbox-worker.cjs.js +857 -0
  90. package/dist/sandbox-worker.cjs.js.map +1 -0
  91. package/dist/sandbox-worker.esm.js +854 -0
  92. package/dist/sandbox-worker.esm.js.map +1 -0
  93. package/dist/sandbox.cjs.js +1425 -0
  94. package/dist/sandbox.cjs.js.map +1 -0
  95. package/dist/sandbox.esm.js +1406 -0
  96. package/dist/sandbox.esm.js.map +1 -0
  97. package/dist/sygnal.min.js +1 -1
  98. package/dist/sygnal.min.js.map +1 -1
  99. package/dist/terminal.cjs.js +1314 -0
  100. package/dist/terminal.cjs.js.map +1 -0
  101. package/dist/terminal.esm.js +1304 -0
  102. package/dist/terminal.esm.js.map +1 -0
  103. package/dist/three.cjs.js +1111 -0
  104. package/dist/three.cjs.js.map +1 -0
  105. package/dist/three.esm.js +1079 -0
  106. package/dist/three.esm.js.map +1 -0
  107. package/dist/ui-combobox.cjs.js +256 -18
  108. package/dist/ui-combobox.cjs.js.map +1 -1
  109. package/dist/ui-combobox.esm.js +256 -18
  110. package/dist/ui-combobox.esm.js.map +1 -1
  111. package/dist/ui-menu.cjs.js +150 -16
  112. package/dist/ui-menu.cjs.js.map +1 -1
  113. package/dist/ui-menu.esm.js +151 -17
  114. package/dist/ui-menu.esm.js.map +1 -1
  115. package/dist/ui-select.cjs.js +95 -9
  116. package/dist/ui-select.cjs.js.map +1 -1
  117. package/dist/ui-select.esm.js +95 -9
  118. package/dist/ui-select.esm.js.map +1 -1
  119. package/dist/ui.cjs.js +679 -173
  120. package/dist/ui.cjs.js.map +1 -1
  121. package/dist/ui.esm.js +679 -175
  122. package/dist/ui.esm.js.map +1 -1
  123. package/dist/vike/config/package.json +1 -1
  124. package/dist/vite/plugin.cjs.js +968 -4
  125. package/dist/vite/plugin.cjs.js.map +1 -1
  126. package/dist/vite/plugin.mjs +968 -4
  127. package/dist/vite/plugin.mjs.map +1 -1
  128. package/dist/worker-page.cjs.js +669 -0
  129. package/dist/worker-page.cjs.js.map +1 -0
  130. package/dist/worker-page.esm.js +665 -0
  131. package/dist/worker-page.esm.js.map +1 -0
  132. package/dist/worker.cjs.js +377 -0
  133. package/dist/worker.cjs.js.map +1 -0
  134. package/dist/worker.esm.js +374 -0
  135. package/dist/worker.esm.js.map +1 -0
  136. package/dist/yjs-testing.cjs.js +98 -0
  137. package/dist/yjs-testing.cjs.js.map +1 -0
  138. package/dist/yjs-testing.esm.js +77 -0
  139. package/dist/yjs-testing.esm.js.map +1 -0
  140. package/dist/yjs.cjs.js +923 -0
  141. package/dist/yjs.cjs.js.map +1 -0
  142. package/dist/yjs.esm.js +900 -0
  143. package/dist/yjs.esm.js.map +1 -0
  144. package/dist/zag.cjs.js +11 -1
  145. package/dist/zag.cjs.js.map +1 -1
  146. package/dist/zag.esm.js +11 -1
  147. package/dist/zag.esm.js.map +1 -1
  148. package/llms/ai.md +65 -0
  149. package/llms/collaboration.md +5 -0
  150. package/llms/graphics.md +10 -0
  151. package/llms/renderers.md +7 -0
  152. package/llms/replay.md +5 -0
  153. package/llms/sandbox.md +5 -0
  154. package/llms/typed-apis.md +5 -0
  155. package/llms/ui.md +10 -0
  156. package/llms-full.txt +444 -0
  157. package/llms.txt +37 -35
  158. package/package.json +139 -3
  159. package/src/agent-ui/mcp.ts +152 -0
  160. package/src/agent-ui/snapshot.ts +212 -0
  161. package/src/agent-ui.d.ts +106 -0
  162. package/src/agent-ui.ts +249 -0
  163. package/src/ai.d.ts +95 -2
  164. package/src/api.d.ts +303 -0
  165. package/src/api.ts +9 -0
  166. package/src/canvas/Canvas.ts +585 -0
  167. package/src/canvas/backend.ts +627 -0
  168. package/src/canvas/geometry.ts +187 -0
  169. package/src/canvas/gesture.ts +23 -0
  170. package/src/canvas/image.ts +197 -0
  171. package/src/canvas/kit.ts +130 -0
  172. package/src/canvas/paint.ts +260 -0
  173. package/src/canvas/pointer.ts +25 -0
  174. package/src/canvas/snap.ts +68 -0
  175. package/src/canvas/svg.ts +99 -0
  176. package/src/canvas.d.ts +373 -0
  177. package/src/canvas.ts +46 -0
  178. package/src/core/actions.ts +14 -3
  179. package/src/core/cell.ts +12 -4
  180. package/src/core/debug.ts +8 -3
  181. package/src/core/hooks.ts +16 -2
  182. package/src/core/hosts/collection.ts +1 -1
  183. package/src/core/hosts/switchable.ts +26 -8
  184. package/src/core/instance.ts +98 -45
  185. package/src/core/markers/portal.ts +4 -0
  186. package/src/core/registry.ts +4 -0
  187. package/src/core/runtime.ts +34 -25
  188. package/src/core/statics.ts +5 -5
  189. package/src/cycle/dom/EventDelegator.ts +37 -33
  190. package/src/cycle/dom/MainDOMSource.ts +16 -14
  191. package/src/cycle/dom/enrichEventStream.ts +13 -9
  192. package/src/cycle/dom/fromEvent.ts +6 -10
  193. package/src/cycle/dom/index.ts +11 -7
  194. package/src/cycle/dom/isolate.ts +13 -6
  195. package/src/cycle/dom/makeDOMDriver.ts +6 -2
  196. package/src/cycle/dom/utils.ts +9 -2
  197. package/src/cycle/run/adapt.ts +4 -12
  198. package/src/devtools.d.ts +9 -0
  199. package/src/devtools.ts +2 -1
  200. package/src/extra/ai/compose/catalog.ts +2 -1
  201. package/src/extra/ai/compose/decisions.ts +1 -2
  202. package/src/extra/ai/compose/generated.ts +234 -0
  203. package/src/extra/ai/compose/index.ts +6 -2
  204. package/src/extra/ai/compose/layout.ts +3 -1
  205. package/src/extra/ai/compose/llm.ts +7 -3
  206. package/src/extra/ai/compose/types.ts +2 -2
  207. package/src/extra/ai/transports/shared.ts +3 -1
  208. package/src/extra/api/contract.ts +485 -0
  209. package/src/extra/api/index.ts +26 -0
  210. package/src/extra/behaviors.ts +3 -3
  211. package/src/extra/controlled.ts +44 -0
  212. package/src/extra/copyAsTest.ts +22 -0
  213. package/src/extra/devtools.ts +5 -3
  214. package/src/extra/devtoolsActions.ts +8 -0
  215. package/src/extra/devtoolsNext.ts +1 -1
  216. package/src/extra/diagnostics/checks/actionLog.ts +8 -3
  217. package/src/extra/diagnostics/checks/behaviors.ts +25 -0
  218. package/src/extra/diagnostics/checks/elementCommands.ts +17 -5
  219. package/src/extra/diagnostics/checks/index.ts +1 -1
  220. package/src/extra/diagnostics/checks/next.ts +78 -4
  221. package/src/extra/diagnostics/checks/props.ts +6 -1
  222. package/src/extra/diagnostics/checks/replies.ts +11 -7
  223. package/src/extra/diagnostics/checks/timers.ts +8 -2
  224. package/src/extra/diagnostics/checks/virtual.ts +11 -0
  225. package/src/extra/diagnostics/codes.ts +51 -1
  226. package/src/extra/elementCommands.ts +20 -4
  227. package/src/extra/fetchDriver.ts +4 -1
  228. package/src/extra/fetchEventSource.ts +6 -2
  229. package/src/extra/field.ts +29 -0
  230. package/src/extra/form.ts +42 -16
  231. package/src/extra/frame.ts +54 -0
  232. package/src/extra/keymap.ts +41 -0
  233. package/src/extra/pager.ts +2 -0
  234. package/src/extra/persist.ts +2 -1
  235. package/src/extra/pointer.ts +262 -0
  236. package/src/extra/processForm.ts +34 -1
  237. package/src/extra/replay/address.ts +49 -0
  238. package/src/extra/replay/entropy.ts +99 -0
  239. package/src/extra/replay/format.ts +263 -0
  240. package/src/extra/replay/index.ts +8 -0
  241. package/src/extra/replay/inert.ts +22 -0
  242. package/src/extra/replay/recorder.ts +247 -0
  243. package/src/extra/replay/replayer.ts +234 -0
  244. package/src/extra/replay/report.ts +126 -0
  245. package/src/extra/replay/schema.ts +52 -0
  246. package/src/extra/replay/version.ts +3 -0
  247. package/src/extra/router.ts +7 -3
  248. package/src/extra/selection.ts +2 -0
  249. package/src/extra/socketDriver.ts +51 -6
  250. package/src/extra/ssr.ts +128 -42
  251. package/src/extra/testing.ts +295 -39
  252. package/src/extra/timers.ts +96 -7
  253. package/src/extra/undo.ts +2 -0
  254. package/src/extra/virtual.ts +25 -2
  255. package/src/extra/widget.ts +106 -17
  256. package/src/host/actions.ts +383 -0
  257. package/src/host/delegation.ts +101 -0
  258. package/src/host/dom.ts +553 -0
  259. package/src/host/driver.ts +185 -0
  260. package/src/host/selector.ts +119 -0
  261. package/src/host/style.ts +51 -0
  262. package/src/host.d.ts +298 -0
  263. package/src/host.ts +46 -0
  264. package/src/index.d.ts +302 -38
  265. package/src/index.ts +15 -3
  266. package/src/pragma/blocks.ts +211 -0
  267. package/src/pragma/index.ts +44 -10
  268. package/src/primitives.d.ts +54 -0
  269. package/src/primitives.ts +136 -0
  270. package/src/react-native.d.ts +57 -0
  271. package/src/react-native.ts +285 -0
  272. package/src/react-peers.d.ts +21 -3
  273. package/src/replay.d.ts +185 -0
  274. package/src/replay.ts +6 -0
  275. package/src/sandbox/build.ts +119 -0
  276. package/src/sandbox/create.ts +187 -0
  277. package/src/sandbox/engine.ts +267 -0
  278. package/src/sandbox/generated.ts +195 -0
  279. package/src/sandbox/kernel.ts +180 -0
  280. package/src/sandbox/mirror.ts +269 -0
  281. package/src/sandbox/protocol.ts +128 -0
  282. package/src/sandbox/quickjs.ts +74 -0
  283. package/src/sandbox/ssr.ts +41 -0
  284. package/src/sandbox/widget.ts +60 -0
  285. package/src/sandbox/worker-entry.ts +59 -0
  286. package/src/sandbox/worker.ts +137 -0
  287. package/src/sandbox-build.d.ts +12 -0
  288. package/src/sandbox-build.ts +6 -0
  289. package/src/sandbox.d.ts +299 -0
  290. package/src/sandbox.ts +25 -0
  291. package/src/shared.ts +12 -0
  292. package/src/terminal/paint.ts +712 -0
  293. package/src/terminal.d.ts +167 -0
  294. package/src/terminal.ts +530 -0
  295. package/src/three/assets.ts +170 -0
  296. package/src/three/host.ts +36 -0
  297. package/src/three/scene.ts +217 -0
  298. package/src/three/target.ts +252 -0
  299. package/src/three.d.ts +221 -0
  300. package/src/three.ts +30 -0
  301. package/src/ui/accordion.ts +21 -7
  302. package/src/ui/dialog.ts +192 -90
  303. package/src/ui/disclosure.ts +2 -0
  304. package/src/ui/dom.ts +30 -0
  305. package/src/ui/layers.ts +132 -0
  306. package/src/ui/popover.ts +120 -21
  307. package/src/ui/shared.ts +98 -12
  308. package/src/ui/stack.ts +18 -0
  309. package/src/ui/tabs.ts +2 -0
  310. package/src/ui/toaster.ts +16 -15
  311. package/src/ui/tooltip.ts +24 -8
  312. package/src/ui/tooltips.ts +26 -14
  313. package/src/ui/zag/combobox.ts +180 -13
  314. package/src/ui/zag/menu.ts +97 -17
  315. package/src/ui/zag/select.ts +15 -4
  316. package/src/ui/zag/shared.ts +60 -8
  317. package/src/ui-combobox.d.ts +16 -1
  318. package/src/ui-menu.d.ts +22 -7
  319. package/src/ui-zag-types.d.ts +13 -5
  320. package/src/ui.d.ts +62 -7
  321. package/src/ui.ts +2 -2
  322. package/src/vite/api.ts +186 -0
  323. package/src/vite/compile.ts +280 -0
  324. package/src/vite/plugin.d.ts +34 -0
  325. package/src/vite/plugin.ts +78 -4
  326. package/src/vite/sandbox.ts +16 -0
  327. package/src/worker/page.ts +396 -0
  328. package/src/worker/protocol.ts +48 -0
  329. package/src/worker/target.ts +282 -0
  330. package/src/worker-page.d.ts +42 -0
  331. package/src/worker-page.ts +12 -0
  332. package/src/worker.d.ts +65 -0
  333. package/src/worker.ts +16 -0
  334. package/src/yjs/driver.ts +253 -0
  335. package/src/yjs/mapping.ts +389 -0
  336. package/src/yjs/shared.ts +138 -0
  337. package/src/yjs/testing.ts +91 -0
  338. package/src/yjs-testing.d.ts +34 -0
  339. package/src/yjs-testing.ts +7 -0
  340. package/src/yjs.d.ts +109 -0
  341. package/src/yjs.ts +18 -0
  342. package/src/zag.d.ts +2 -0
  343. package/src/zag.ts +8 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,638 @@
2
2
 
3
3
  All notable changes to Sygnal are listed here. Versions follow [semantic versioning](https://semver.org). Releases before 5.4.0 are described in the [GitHub releases](https://github.com/tpresley/sygnal/releases) and tags.
4
4
 
5
+ ## 7.2.0-preview.0 — dev preview
6
+
7
+ A dev preview (`npm install sygnal@next sygnal-check@next`). Its new subpaths and the `compile` option are
8
+ experimental, and their APIs may change before 7.2.0. Everything is additive except the entries under
9
+ **Changed**: `processForm()` arrays, the `form` behavior's checkbox-group order, and `sygnal/ui`'s
10
+ dialog, popover and tooltip behaviour (each marked **Behaviour change** or state-visible). An app that
11
+ uses none of these behaves as on 7.1.0.
12
+
13
+ **New (experimental).**
14
+ - **Renderer targets:** the same components on other hosts than the browser DOM.
15
+ - `sygnal/host` is the kit: a browser-free host tree, `makeHostDriver(doc)`, selectors and per-target
16
+ stylesheets, and browser-like default actions (`activate`, `pressKey`, `fill`, …).
17
+ - `sygnal/terminal` renders in a terminal: `runTerminal(App)`, and `createTerminal()` for tests.
18
+ - `sygnal/agent-ui` renders for an LLM agent: an accessibility snapshot with refs, `ui_*` tools that
19
+ become DOM events, and `serveMcp`.
20
+ - `registerFieldTags()` lets a target's own form-field tags re-sync their value like `input`.
21
+ - Guides: "Beyond the Browser", with "Writing a Renderer Target". Example: `examples/renderer-targets`.
22
+ - **React Native:** `sygnal/react-native` runs an app on React Native: `<SygnalView app={App} />`.
23
+ - Native callbacks arrive as DOM events, so intents don't change.
24
+ - Only the path to a changed element re-renders (a tap in a 100-item list re-renders 2 elements, not 303).
25
+ - Stylesheets are compiled with `StyleSheet.create`. A `List` with a `Collection` becomes a `FlatList`.
26
+ - `react` and `react-native` are optional peer dependencies.
27
+ - **Primitives:** `sygnal/primitives` adds `View`, `Text`, `Pressable`, `TextInput`, `Switch`, `Image`,
28
+ `ScrollView` and `List`. They render as HTML on the web, the terminal and agent-ui, and as native
29
+ components on React Native.
30
+ - `registerFieldTags()` takes a kind (`{ tag, kind: 'checkbox' }`).
31
+ - Guides: "Write Once: Web, Native, Terminal" and "React Native". Example: `examples/write-once`.
32
+ - **Typed APIs:** `sygnal/api` builds typed requests from an API contract for the existing HTTP and
33
+ socket drivers, with no new driver.
34
+ - Clients: `orpcClient`, `openApiClient` and `honoClient`.
35
+ - Version skew: `contractHash` / `contractSkew` / `isContractSkew` / `contractFetch`, reported as SYG690.
36
+ - Testing: `fixture` / `errorFixture` for `renderComponent`.
37
+ - Function-first: `query` / `mutation` / `stream` in `*.server.ts` files are the API.
38
+ `serverFunctions()` builds the oRPC router.
39
+ - `sygnal({ api: { handler, contracts, functions } })` in `sygnal/vite` does three things:
40
+ - serves the API in dev;
41
+ - computes the contract hash at build time, so the client ships no schemas
42
+ (the example: 73.7 → 47.7 kB gzip);
43
+ - turns `*.server.ts` imports into client stubs. Tests check that no server code reaches the bundle.
44
+ - Guide: "Typed APIs". Example: `examples/typed-api`.
45
+ - **Compiled views:** `sygnal({ compile: true | { views, ssr, blocks, test } })` in `sygnal/vite`
46
+ (Vite 8) compiles view JSX at build time.
47
+ - Static element data is built once, and dynamic keys are routed once.
48
+ - `.map()` rows and component roots become template-cloned blocks.
49
+ - Server rendering becomes string templates (4.7–5.5× on the benchmark table).
50
+ - Same vnodes, DOM and HTML as without it. Tests, devtools and other renderers see ordinary vnodes.
51
+ - Off by default. `magic-string` is a new dependency of the plugin only.
52
+ - Guide: "Compiled Views".
53
+
54
+ - **Smaller bundles with Vite 8:** `src/extra/widget.ts` set a property on a module-level function at
55
+ load, which rolldown keeps, so every Vite 8 app shipped the widget module whether or not it called
56
+ `defineWidget()` (kanban: 42,975 → 42,073 B gzip, −902 B). The assignment moved into
57
+ `defineWidget()`.
58
+ - **Faster dispatch in Node:** the debug check read `process.env.SYGNAL_DEBUG` on every action,
59
+ most of the per-action cost in Node (SSR, tests, headless apps): 665 → 105 ns per action. In Node
60
+ the variable is now read once, when Sygnal loads (`SYGNAL_DEBUG=true node server.js`, as
61
+ documented); in the browser `window.SYGNAL_DEBUG` is still read on each call.
62
+
63
+ - **Cheaper Collection items:** each item instance holds less, and its intent's sources are no longer
64
+ built with `Object.defineProperty`. On the benchmark's 1k-row Collection:
65
+ - Each item keeps 104 JS objects instead of 160 (3.3 KB instead of 5.1 KB).
66
+ - The core's own time drops by 18–26% for create, append and replace (10k rows: 66.6 → 49.6 ms) and
67
+ by 19% for selecting a row (1.6 → 1.3 ms).
68
+ - Two observable changes come with it:
69
+ - An intent's `sources` object is a Proxy. Its own enumerable properties are still the source names,
70
+ so spread, `Object.keys`, `in` and destructuring work as before, and writing to a source name still
71
+ throws a TypeError. `Object.getOwnPropertyDescriptor(sources, 'DOM')` now reports a read-only data
72
+ property instead of a getter.
73
+ - An event stream's helpers (`.value()`, `.checked()`, `.data()`, `.target()`, `.key()`,
74
+ `.detail()`) are now methods shared by every stream. Call them on the stream:
75
+ `DOM.input('.x').value()`. A helper taken off its stream (`const { value } = DOM.input('.x')`) no
76
+ longer knows the stream. Nothing in the docs or examples does this.
77
+ - **Imperative islands:** a widget can take JSX children: `defineWidget({ children: true, sync })`.
78
+ - The children (a 3D scene's objects, a map's markers, a canvas's shapes) are rendered by Sygnal as
79
+ elements, so components, `Collection`, isolation and intents work inside. The widget draws them its
80
+ own way and dispatches DOM events on them: a raycast hit is a `click` on the cube's element, and
81
+ `DOM.click('.cube')` takes it.
82
+ - The children are visually hidden but accessible: Enter and Space click a focused child, and
83
+ `focus(instance, child)` tells the widget where to draw the ring. `children: 'hidden'` opts out.
84
+ - `sync` runs only when the children changed. `tags` keeps a widget's vocabulary (`marker`, `line`)
85
+ out of the pragma's SVG handling.
86
+ - `value: { write, read?, equals?, strict? }` makes the `value` prop controlled: Sygnal writes only the
87
+ app's own changes, never echoes of the widget's, and `strict` writes back an edit the model refused.
88
+ - Guide: "Imperative Islands" (three.js, canvas 2D, CodeMirror). Research: `dev-plans/research/canvas-three`.
89
+ - **three.js:** `sygnal/three` renders three.js scenes from JSX. `<Scene>` takes meshes, geometries,
90
+ materials, lights and cameras as children (every three.js class, lowercased; `extend()` for more).
91
+ - A click or hover on the canvas is a raycast, dispatched as a DOM event on the element hit, with the
92
+ point it hit: `DOM.click('.cube')` in the cube's own intent.
93
+ - `animate={{ rotation: (t) => [0, t, 0] }}` moves objects every frame with no action, render or patch.
94
+ - Keyboard: meshes with a role and `tabIndex` are focusable buttons; Enter clicks them.
95
+ - Loaded assets as tags: `<Model src="/tree.glb" />` (GLTF by default, any loader with `loader`) and
96
+ `<Texture src="/wood.jpg" />`. A `<Suspense>` boundary shows its fallback until they arrive (inside
97
+ the Scene: a 3D fallback). Cached per URL and freed with the last element using them; a failed load
98
+ renders the component's `onError`.
99
+ - Dragging: `capture="xz"` keeps the pointer on the object from the press to the release, off the
100
+ object and off the canvas, with each move's point on a drag plane; `el.setPointerCapture()` works
101
+ too. `draggable` sends the HTML drag events, so `processDrag` works on scene objects.
102
+ - A controlled camera (`Orbit`, `camera`), disposal of what it made, `createThreeHost()` for a whole app
103
+ as a scene on `sygnal/host`, and `sceneOf(el)` for tests in jsdom (headless raycasts, no WebGL).
104
+ - `three` (0.170 or later) is an optional peer dependency. About 6 KB gzip (GLTFLoader loads when a
105
+ model first needs it), nothing for an app that doesn't import it. Guide: "three.js". Example:
106
+ `examples/three-cubes`.
107
+ - **2D canvas apps:** `sygnal/canvas` is an infinite canvas for editors, whiteboards and diagrams.
108
+ Research and the maintainer's decisions: `dev-plans/research/canvas-apps`.
109
+ - Shapes are data in state: `<shapes items={state.shapes} />` in a `<Canvas>` paints, culls and
110
+ hit-tests them all with one element. A hit is a DOM event on it with `event.hit` = the shape's id.
111
+ JSX nodes (`rect`, `ellipse`, `text`, `line`, `path`, `frame`, `space="screen"`) are for the few
112
+ things with behaviour of their own (handles, pins).
113
+ - One box-based vocabulary everywhere (`x y width height rotation radius parent`): an ellipse is its
114
+ box, not SVG's `cx cy rx ry`.
115
+ - Pointer capture by default (`capture="all"`): the pressed shape keeps every move and the up, with
116
+ `delta` from the press. A controlled camera with pan and zoom built in (wheel, ctrl-wheel and
117
+ trackpad pinch, two-finger touch pinch, space-drag, middle-drag), reported once a frame as `camerachange`.
118
+ - Painting: a static layer plus a live layer (the app passes `active={ids}`), translation blits for
119
+ big moving sets, and a camera cache: a pan is a copy of the cached bitmap, a zoom gesture scales it
120
+ and repaints sharp at rest. At 10,000 shapes a pan step costs 3.5 ms against React + react-konva's 3.7 ms (12.1 ms before the cache), and a drag of all 10,000 through the action queue 8.7 ms against 8.9.
121
+ - The kit: `gesture`, `snap` / `snapTargets`, `handles`, `resize`, `rotation`, `turn`, `scaleInto`,
122
+ `selectionBox`, `positionsOf` (an id → index map a drag's new arrays share), `fit`, `union`, `toWorld` / `toScreen`, `exportSVG`, and `history(field)`, which routes
123
+ to `sygnal/yjs`'s per-user UndoManager when the field is shared (one undo step per gesture).
124
+ - `follow={{ ids, from }}`: the transient escape hatch for very large selections (the canvas drags them
125
+ with no action per move; the app commits on release).
126
+ - Accessibility: each data layer is projected into a list of buttons (the nodes in view) that screen
127
+ readers and the `sygnal/agent-ui` snapshot read; activating one presses the node.
128
+ - SYG262 (dev): a data layer whose nodes all changed identity with equal fields (a reducer that
129
+ rebuilds every shape), which makes every frame a full repaint.
130
+ - The painter is a backend (`canvas2d()`, the default). No dependencies; 15 KB min+gzip, nothing for an
131
+ app that doesn't import it. Guide: "Canvas". Example: `examples/figma-lite`.
132
+ - **Keyboard shortcuts:** `keymap(DOM, { 'mod+z': 'UNDO', delete: 'DELETE', left: ['NUDGE', { dx: -1 }] })`
133
+ (from `sygnal`) gives one action stream per name, default prevented and never while typing in a field.
134
+ - **One pointer router:** `sygnal/three` and `sygnal/canvas` share `src/extra/pointer.ts` (capture as
135
+ `sygnal/three` defines it; a 2D scene's background target and implicit capture are options of it).
136
+ - **Collaborative state:** `sygnal/yjs` shares one key of the state between users through a Yjs
137
+ document. `uses = { board: shared() }` and `run(Board, { SHARED: makeYjsDriver({ doc, provider }) })`.
138
+ - Reducers don't change: each new `state.board` is diffed into the document (`board.LOCAL`, once per
139
+ flush), and other users' changes arrive as `board.REMOTE`, applied only while there's no unsynced
140
+ local edit. DevTools hides both, as it hides `INITIALIZE`.
141
+ - Arrays of items with `id` merge concurrent adds, edits, moves and removes (order keys from
142
+ `fractional-indexing`). `shape` declares fields (`'text'` merges typing by character); without one
143
+ the kind is inferred, and an empty array is reported in dev (SYG261).
144
+ - The first value seeds an empty document after the provider's first sync (`seed: 'whenSynced'`; or
145
+ `'ifEmpty'`). Undo per user (`{ SHARED: { undo: 'board' } }`), presence, and `{ connect }` to work
146
+ offline. `undo()` on a shared key is SYG260 (dev and sygnal-check).
147
+ - `sygnal/yjs/testing`: `createNetwork()`, an in-memory network of peers for tests and demos.
148
+ - `yjs` and `y-protocols` are optional peer dependencies; Sygnal ships no network code (any Yjs
149
+ provider works). About 5.6 KB gzip, nothing for an app that doesn't import it. Guide:
150
+ "Collaborative State". Research: `dev-plans/research/crdt-state`.
151
+ - **Sandboxed components:** `sygnal/sandbox` runs a component the app didn't write (a model's output,
152
+ a plugin, a marketplace widget) in QuickJS-in-WASM, with no access to the page. It renders into the
153
+ real DOM through an allowlist sanitizer, and its events come back as validated data.
154
+ - `<Sandboxed kernel={kernel} source={cjs} emits={{ ANSWER: Schema }} input={...} />` and
155
+ `createSandbox(options)`. `emits` is required: only declared `EVENTS` types whose data passes their
156
+ Standard Schema reach the page (SYG692 in development otherwise).
157
+ - Two tiers behind one option: `isolation: 'worker'` (the default; a watchdog terminates a stuck
158
+ worker) and `'thread'` (synchronous events, for model-written code, not hostile code).
159
+ - Defaults: a WebAssembly instance per sandbox capped at 64 MB (ArrayBuffers included), a 32 MB heap,
160
+ a 256 KB stack (at most 384 KB), 1 s to boot, 100 ms per event, 50 % CPU.
161
+ - Capabilities are off by default: `fetch` (an allowlist; no cookies, no redirects), `navigate`, and
162
+ `focus` (only while focus is already inside the box). Guest classes are kept; `classPrefix`
163
+ isolates them.
164
+ - The guest kernel is built from the app's installed packages: `import kernel from
165
+ 'sygnal/sandbox/kernel?raw'` in `sygnal/vite`, or `buildSandboxKernel()` from `sygnal/sandbox/build`.
166
+ SSR: `renderToHTML()`.
167
+ - `quickjs-emscripten-core` and `@jitl/quickjs-wasmfile-release-sync` are optional peer dependencies.
168
+ Guide: "Sandboxed Components".
169
+ - **Generated parts in `compose`** (experimental, outside semver even after 7.2.0 until real-model
170
+ success rates are measured): `compose.generated(runtime, { data, emits })` lets the model write a part's
171
+ component as code when no prepared part fits; it runs in `sygnal/sandbox`. Research:
172
+ `dev-plans/research/compose-sandbox`.
173
+ - `generatedRuntime({ kernel, compile })` from `sygnal/sandbox` compiles, checks and runs the code;
174
+ `sucraseCompiler(() => import('sucrase'))` is a `compile` on the app's own sucrase.
175
+ - `gen.part` goes in the catalogs that may have one (never a default), `gen.instructions` in
176
+ `compose.llm()`'s instructions, `fix: gen.repair()` in `uses`; `gen.events(CHILD, type)` gives the
177
+ host the validated events.
178
+ - Defaults: `isolation: 'worker'`, `maxUses: 3`, `maxCode: 4000`, `maxAttempts: 2`, the check before
179
+ mount on, no capabilities.
180
+ - Partial code never mounts while a reply streams; a part keeps its node key from the preview to the
181
+ final layout. A failure becomes one small repair request (the code and the error); a part the model
182
+ can't fix shows a fallback in its own place.
183
+ - `gen.card(type)` is the `render_component` chat card: the one-part case, as `chat({ ask })`'s direct
184
+ catalog.
185
+ - The check before mount loads nothing on the page: JSX event handlers on the page, and the kernel now
186
+ warns at boot about an intent action with no model entry (SYG101). sygnal-check is an opt-in `check`
187
+ where Node runs.
188
+ - `createSandbox({ onReject })` and `emitGate(emits, deliver, reject)` report dropped events.
189
+ - Text params take `format: 'code'`: the prompt calls them source code, and `asked_for` items keep
190
+ their own length.
191
+
192
+ - **Record and replay:** `sygnal/replay` records a session and replays it as a test. Research and the
193
+ maintainer's decisions: `dev-plans/research/record-replay/`.
194
+ - `startRecording({ redact, dropNoops, vnodes })` records the next app: its inputs (DOM events,
195
+ driver replies, timers), a checkpoint per patch, the Sygnal version and the component and action
196
+ names (format version 1, `RECORDING_SCHEMA`). Redaction is on by default and follows where a value
197
+ came from: typed text and field values are masked unless `redact` lets them through.
198
+ - `replay(Component, log, options)` (also on the main entry) runs the log under `renderComponent`,
199
+ drops the code's own inputs, compares every checkpoint and reports the first divergence. Version
200
+ and name mismatches are warnings.
201
+ - `replayTest`, `bugReport`, `timeline` and `finalState` turn a recording into a test, a report or a
202
+ state. DevTools' Copy as test has a `{ mode: 'replay' }`.
203
+ - **Handlers get `now()`, `random()` and `uuid()`** in their 4th argument; renderComponent's `now`,
204
+ `random` and `uuid` options fix them (a fixed clock, a seeded random, ordered ids), and a replay
205
+ feeds them from the log. This is the canonical form: `sygnal-check` warns about `Date.now()`,
206
+ `new Date()`, `Math.random()` and `crypto.randomUUID()` in a STATE reducer (SYG258) and, under
207
+ `--strict`, in any handler (SYG509), with `--fix`. Docs and examples use it.
208
+ - Core hooks: an `onAction` layer that returns `false` drops the action (any `false` wins);
209
+ `onDispatch(inst, type, data, cause, event)` runs at enqueue with the DOM event being delivered;
210
+ the runtime API's `setState` reaches `onAction` as a `'setState'` action. About 140 B in the
211
+ kanban gate. Guide: "Record and Replay".
212
+ - **Off the main thread:** `sygnal/worker` and `sygnal/worker/page` run a whole app in a module Web
213
+ Worker with its code unchanged. Research: `dev-plans/research/web-worker/`.
214
+ - `createWorkerTarget(port)` runs the app on a `sygnal/host` tree in the worker and sends one message
215
+ of patch ops per flush, frame-coalesced. `remote(name, methods)` is a driver that stays on the page
216
+ (`localStorage`, the router).
217
+ - `startBridge(worker, mount, { drivers, preventDefault, coalesce })` applies the ops and sends input
218
+ back. An intent's declared `preventDefault` is applied at once on the page. A field's value from
219
+ the worker that is about older text is rebased on the user's edits since (the caret is kept), and a
220
+ submit waits until the form's last edit is acknowledged. Pointer moves, scroll and wheel go once a
221
+ frame.
222
+ - A click costs about one frame more; the page stays at 60 fps while the app works (10,000 todos: a
223
+ key pressed while toggling them paints in 16 ms instead of 128). Not yet: element commands,
224
+ browser-owned attributes, widgets, DevTools and HMR. Guide: "Running off the Main Thread".
225
+ Example: `examples/todomvc-worker` (`examples/todomvc`'s files).
226
+ - **Terminal apps (`sygnal/terminal`, on `sygnal/host`):** from building a full-screen TUI
227
+ (`examples/tui-tracker`, an issue tracker: list and detail panes, tabs, a keymap, a modal confirm, a
228
+ validated form with every control, async data). Research: `dev-plans/research/tui-spike/`.
229
+ - **Full-screen layouts:** the terminal stylesheet takes `display: 'flex'`, `width` / `height` (cells
230
+ or `%`), `flex`, `gap`, `padding`, `border` (`'round'`, `'single'`, `'double'`, `'heavy'`, `true`;
231
+ titled by `aria-label`, heavy when focused), `overflow: 'auto'` (a scrolling box with a scrollbar
232
+ that keeps the focus in view) and colours. Box properties come only from the stylesheet, so a web
233
+ view's inline styles don't change the terminal layout. `createTerminal({ height })`.
234
+ - **Controls:** a native `<select>` opens an option list (Enter, Space, Alt+↓, F4; letters jump).
235
+ `<dialog>` (`show`, `showModal`, `close`, `requestClose`, `closedby`) and the Popover API
236
+ (`popover="auto"` light dismiss, `popovertarget`, `commandfor` / `command` invokers): a modal traps
237
+ focus, Escape is its close request, the focus goes back to the opener; the terminal draws modals
238
+ centered over a dimmed page and popovers anchored.
239
+ - **Text editing:** a caret and selection (`selectionStart` / `selectionEnd` / `setSelectionRange()` /
240
+ `select()`), typing at the caret, word deletes, Shift selection, `<textarea>` lines, cancelable
241
+ `beforeinput` and `input` with their `inputType`. Readline keys in a field (Ctrl+A E B F D K U W,
242
+ Alt+B F D; the keydown goes out first, so `keymap()` can take a key). Bracketed paste is one
243
+ `paste` event and one insertion.
244
+ - **Mouse** (`runTerminal({ mouse: true })`): click, double click, wheel scrolling of the box under
245
+ the pointer, the caret placed in a field.
246
+ - The `TERMINAL` sink (`'exit'`, `{ title }`, `'bell'`), a key-hint footer (`footer`), and emoji
247
+ width asked from the terminal at startup (`emojiWidth: 'auto'`, the default; `1` or `2` fixes it).
248
+ - **Tests:** `term.run(App, drivers, runOptions)` runs the app with the terminal's `DOM` and
249
+ `TERMINAL` drivers; `await term.settle({ timeout, quiet })` waits until the runtime has flushed and
250
+ the tree has been unchanged for `quiet` ms (20), advancing fake timers, and rejects after `timeout`
251
+ (2 s) with the frame. `term.paste()`, `term.click()`, `term.wheel()`, `hitTest()`, `term.keyHint()`,
252
+ `term.commands`; `Frame` has `focusLine`, `focus`, `caret`, `hits`.
253
+ - `sygnal/host`: `:focus-visible`, `:focus-within`, `:checked`, `:disabled`, `:enabled`, `:open`,
254
+ `:modal`, `:popover-open`; `scrollTop` / `scrollIntoView()`; `getRootNode()`; listener `{ once }`;
255
+ `inert` and `autofocus`; `showPicker()`, `selectedIndex`. `HostDocument.addEventListener` and a
256
+ real capture phase (document capture → root delegates → target → bubbling), with
257
+ `stopPropagation` / `stopImmediatePropagation`; `defaultView.CustomEvent`; the host driver's events
258
+ carry `ownerTarget`, as the DOM driver's do.
259
+ - **Games on `sygnal/canvas`:** from building a platformer (`examples/platformer`). Research:
260
+ `dev-plans/research/canvas-game/`.
261
+ - A frame timer's `step` (a fixed timestep, `max` steps per frame, vsync snapping) and `keys`
262
+ (sampled with the frame as `held` / `pressed` / `released`, released on blur, not in text
263
+ fields): `{ frame: 'STEP', step: 1000 / 60, max: 5, keys: { left: 'ArrowLeft' } }`. SYG422 covers
264
+ the new fields.
265
+ - `<shapes live>` (every node repaints each frame, no projection), layer order that holds with live
266
+ layers, `parallax={f}`, `<Canvas a11y={0}>`.
267
+ - **Images and sprites:** an `image` node (`src` or a loaded `image`, a crop `sx sy sw sh`, `fit`,
268
+ `flipX` / `flipY`, `smoothing: false`, `alt`), loaded once per URL and shared by every canvas, painted
269
+ when it arrives (`imageload` / `imageerror`), in hit testing, the projection and `exportSVG`.
270
+ `<Image src>` inside `<Canvas>` makes `<Suspense>` wait and sends a failed load to `onError`.
271
+ `preloadImage`, `clearImages`, and sprite sheets with `frameOf(sheet, index)` and
272
+ `frameAt(frames, t, every, loop)`.
273
+ - **`follow(camera, target, { deadzone, offset, lerp, dt, bounds, round })`:** a pure follow camera,
274
+ as Phaser's and Godot's behave; it returns the same camera when nothing moves.
275
+ - **A scrolling camera without a full-bitmap copy:** in a game scene (a `live` or `parallax`
276
+ layer) each run of static layers is its own canvas element that the browser shifts; only newly
277
+ exposed strips are painted, and after 30 still frames they fold back onto the main canvas. The
278
+ platformer level goes from 0.68 to 0.46 ms per frame (plain JS: 0.47). `PaintStats` gains
279
+ `scrolled` / `strips`; a backend may implement `refresh()`.
280
+ - **Text wrapping:** text wraps to its `width` (`wrap: 'word' | 'char' | 'none'`, `\n`, `maxLines`
281
+ with an ellipsis, `align`, `lineHeight`), a missing height is its lines' height, and
282
+ `layoutText(text, options)` returns the lines and size. Hit boxes and `exportSVG` follow it.
283
+ - `t.frames(n)` in `renderComponent`; `settle()` ignores the frame loop; `replay()` works under fake
284
+ timers.
285
+ - The canvas bundle is 22.4 KB min+gzip (was 17.3 KB); nothing for an app that doesn't import it.
286
+ - **`sygnal/ui`: a controlled `open`.** `state.<key>.open` decides whether a dialog or popover is open.
287
+ - A host reducer that writes `open` (any action, a parent, `setState`) opens or closes the element,
288
+ through the new `APPLY` action. A host entry for `OPEN` / `CLOSE` / `CANCEL` / `CLOSED` / `TOGGLED`
289
+ that keeps the old value keeps the element as it was. The `open` option is the start value
290
+ (Radix's `defaultOpen`).
291
+ - A dialog rendered only while open gives the focus back to its opener when it is removed.
292
+ - **`sygnal/ui`: dismissal through the state,** as Radix's `DismissableLayer`. Giving the `open`
293
+ option (even `false`) makes a dialog or popover controlled.
294
+ - Escape, a press outside and focus leaving become close requests (a popover's new `DISMISS`, a
295
+ dialog's `CANCEL`, with the event as data), one per interaction. A host entry that returns
296
+ `previousState` keeps it open, with no frame where it is hidden.
297
+ - One layer stack per page: Escape goes to the top layer only; layers under a modal get no presses;
298
+ a touch dismisses at its click, so a scroll that starts outside doesn't; a layer opened inside
299
+ another (or in a `<Portal>` rendered in it) counts as inside it; a press on a layer's own trigger
300
+ is not outside. Open Menus, Selects and Comboboxes are layers too: the first Escape closes the Zag
301
+ part, the next the popover.
302
+ - A controlled dialog's `closedby` says what dismisses it; uncontrolled popovers keep the browser's
303
+ light dismiss.
304
+ - **`sygnal/ui` additions:**
305
+ - `popover({ trigger })` (Radix's `Popover.Trigger`): the trigger's clicks toggle the popover
306
+ through the model. `TOGGLE` takes `true` / `false`, or no data to flip it.
307
+ - `popoverAttrs(state, uid)` (`trigger`, `popover`) and `dialogAttrs(state, uid)` (`trigger`,
308
+ `dialog`, `title`, `description`, `close`) give Radix's ARIA and `data-state`. The dialog guide's
309
+ "Alert dialogs" section shows `role="alertdialog"`.
310
+ - `dialog`, `popover`, `tooltip` and `tooltips` work inside a `<Portal>`. In the core, an `ELEMENT`
311
+ command whose target isn't in the component's own DOM is looked for in its portals' content, and
312
+ a spec command can be marked `optional` (no SYG640).
313
+ - The parts use their element's own document, root and window, so they work in shadow roots,
314
+ iframes and on `sygnal/host`: every part runs in `sygnal/terminal`, the dialog and `<Toaster />`
315
+ included.
316
+ - `defineBehavior({ triggers })` declares which option an action's built-in trigger comes from
317
+ (`{ OPEN: 'trigger' }`); the `sygnal/ui` parts, `pager`, `selection` and `undo` declare it. **SYG118**
318
+ (warning; dev and `sygnal-check`): a host intent for `<key>.<ACTION>` replaces such a trigger, so
319
+ the option's clicks stop working. (An intent action with the same name replaces the built-in
320
+ trigger; the docs used to say it adds one.)
321
+ - **Menu, Select, Combobox:**
322
+ - Widget form fields: a widget that declares `field: true | { event, read }` (`defineWidget`, and
323
+ `fromZag` through its options) is one form field named by its `name` prop, for the `form` behavior
324
+ (value, touched on blur) and `processForm()` (an array when `multiple`). Select and Combobox are
325
+ form fields.
326
+ - Combobox `multiple` shows chips before the input, as React Aria's TagGroup: one tab stop (a grid
327
+ with a roving tabindex), Left / Right / Home / End between chips, Delete / Backspace remove and
328
+ move the focus, removals announced (`removedMessage`), `removeHint`, `removeLabel`, `chipsLabel`,
329
+ `chips={false}`, and the `remove` command.
330
+ - Combobox `allowCustomValue` commits the typed text on blur or on Enter with no active option (an
331
+ item's label picks the item, an empty input clears), as React Aria's `allowsCustomValue`.
332
+ `loading` while the items load: a value no item has yet shows nothing, the listbox is `aria-busy`
333
+ and `empty` waits. `empty="No results"` shows and announces a row.
334
+ - Groups in Select and Combobox: `items={[{ group: 'Fruit', items: ['Apple', 'Pear'] }]}`.
335
+ - Menu checkbox and radio items (`checked-change`), link items and submenus (ArrowRight / Enter /
336
+ hover open, ArrowLeft / Escape close; a pick in one is the Menu's `select`).
337
+ **Changed.**
338
+ - `processForm()` gives a `<select multiple>` as an array of its selected values (`[]` when none),
339
+ as the `form` behavior's `CHANGE` and a `multiple` Select or Combobox already do, leaving out
340
+ disabled options as FormData does. Checkboxes that share a name (more than one in the form, disabled
341
+ ones included) give an array of the checked, enabled boxes' values in document order (`[]` when none
342
+ is checked), like `FormData.getAll()`. Both hold on the DOM and on a `sygnal/host` form (terminal,
343
+ worker, agent-ui); a lone checkbox and radios are unchanged. Both gave the last selected or checked
344
+ value, and no entry at all when none was. To keep the old value, take the array's last item:
345
+ `data.tags.at(-1)`. Type the field as an array: `processForm<{ tags: string[] }>(DOM.select('form'))`
346
+ (`FormData`'s fields may now be any type). A group that sometimes renders one box gives a string
347
+ then: read it as `[].concat(data.tags ?? [])`, or use the `form` behavior, whose array fields stay
348
+ arrays.
349
+ - **The `form` behavior keeps a checkbox group's array in document order,** as FormData and
350
+ `processForm()` do, instead of the order the boxes were checked in (checking c then b gives
351
+ `['a', 'b', 'c']`). Values no rendered box has keep their places. A checkbox's `form.CHANGE` carries
352
+ `order`, the group's values as rendered. Unchecking and rechecking a box now returns the form-level
353
+ `dirty` to false.
354
+ - **`sygnal/ui` slice shapes (state-visible):** `dialog()` and `popover()` are keyed like `tabs` and
355
+ `disclosure`: the slices are `{ id, open, returnValue }` and `{ id, open }`, and the `id` option sets
356
+ the id prefix (default: the key in `uses`). A reducer that drops `id` gets it back.
357
+ - **Behaviour change: dialog Escape and cancel.** While a modal dialog behavior is open, the dialog has
358
+ `closedby="none"`: Escape becomes `CANCEL` through the model, an Android back gesture no longer
359
+ closes it unless the host sets `closedby`, and its `cancel` event is always prevented, so the state
360
+ decides. `CLOSE` sets the state at once (before, only the close event did).
361
+ - **Behaviour change: tooltips.** `tooltip()` / `tooltips()` show on focus only when it is
362
+ `:focus-visible` (React Aria's rule), so a mouse click or tap no longer shows the tip. A pointerdown
363
+ on the trigger hides the tip and cancels a pending show, and it stays hidden while the pointer is
364
+ still over the trigger (Radix); the state gains `pressed`. `DialogActions.SYNC` is typed `boolean`.
365
+ - **Behaviour change: `sygnal/host` capture listeners** run in a capture phase before the others
366
+ (before, they ran with them).
367
+ - Menu, Select and Combobox lists are `position: fixed` by default, as in Radix, so an
368
+ `overflow: hidden` or scrolling ancestor no longer clips them. Inside a `transform`, `filter` or
369
+ `contain` ancestor, use `positioning={{ strategy: 'absolute' }}`.
370
+ - **`sygnal/terminal` defaults** (each documented with how to change it): the mouse is off (the
371
+ terminal keeps its own text selection; `mouse: true`); Ctrl+D in a field deletes forward and Ctrl+A
372
+ goes to the line start, as in readline (Ctrl+C or a `TERMINAL` `'exit'` quits); console output is
373
+ buffered on a TTY and written after the terminal is restored (`console: 'pass'`); the screen redraws
374
+ in place in one synchronized update. Cell widths come from Sygnal's own tables (CJK two cells,
375
+ combining marks none, emoji per `emojiWidth`).
376
+ - **`sygnal/host` focus and keys:** a radio group, a roving `tabindex` list and a tab list are one Tab
377
+ stop. Enter / Space on a non-native `[tabindex]` element click it only when no component listens to
378
+ `keydown` on it or an ancestor (app-wide `DOM.select('document')` and `keymap()` listeners don't
379
+ count), so an app that handles those keys gets one action, as on the web. A `tabindex="-1"` element
380
+ is no longer added to the focus order. `select()` selects, setting `value` puts the caret at the end,
381
+ `dataset` values are strings, closed `<dialog>` / hidden `[popover]` / closed `<details>` content
382
+ counts as hidden, and while a modal is open only its contents are focusable.
383
+ - **`sygnal/canvas` text:** a `text` node with a `width` now wraps instead of overflowing on one line.
384
+ A JSX `<text>` without width and height no longer has its measured size written into its node
385
+ (`event.node.width` is undefined for it). `measureText` is the natural, unwrapped size.
386
+ - DevTools keeps before / after states for the newest 500 actions (was 5,000: the memory of a 60 fps
387
+ game).
388
+
389
+ **Fixed.**
390
+ - A component or `<Collection>` written directly inside a literal `<svg>` lost its props (the pragma
391
+ turned them into SVG attributes).
392
+ - An element above an unchanged component was patched again (its hooks ran) whenever its parent
393
+ re-injected another child.
394
+ - `sygnal/agent-ui`: a role-less container the app listens to no longer hides the controls inside it.
395
+ - `src/` can be served by Vite directly (its types are re-exported with `export type`).
396
+ - `<Collection>` / `<VirtualCollection>`: a new array whose elements keep their keys in place (a drag,
397
+ an edit) keeps the key index (an identity check per element instead of a map of every key), and
398
+ `<VirtualCollection>` then keeps its row list without a pass over the keys. A 10,000-row list that
399
+ changes in one place per frame no longer costs a pass over 10,000 keys.
400
+
401
+ - **Runs in a Web Worker:** `run()` no longer reads the `window` global (`src/cycle/run/adapt.ts` uses
402
+ `globalThis`), so an app can run in a module worker on `sygnal/host` (research:
403
+ `dev-plans/research/web-worker/`).
404
+ - `sygnal/canvas`: a canvas sized by CSS painted nothing until its first change (the first
405
+ ResizeObserver report cleared the bitmap and the backend kept its cached paint). `check-live` now
406
+ fails on a canvas demo that is one flat colour.
407
+ - `sygnal/canvas` and `sygnal/three` painted every other frame under a frame timer: a frame's paint
408
+ now runs at the end of that frame, after its flush (`atFrameEnd`, 0 B in the core). Also: no blit
409
+ bitmap reallocated every frame for ≥ 1,000 live nodes that don't translate; SYG262 skips `live`
410
+ layers and `active` nodes.
411
+ - `sygnal/replay` crashed on a checkpoint when a view had a number next to text.
412
+ - `accordion()` driven from state: numbers in a host-written `expanded` never opened (and a click
413
+ added a duplicate), a string `expanded: '12'` opened panels `1`, `2` and `12`, and
414
+ `collapsible: false` was lost when a host reducer replaced the slice.
415
+ - `dialog()` in Firefox and WebKit: Escape after an earlier `'ok'` close reported `returnValue: 'ok'`
416
+ again, so the app committed the cancelled edit. `OPEN` now clears `returnValue`.
417
+ - Tooltips: in Chromium and Firefox a clicked trigger kept its tip after the pointer left; in WebKit a
418
+ tip on a dialog trigger stayed open under the modal.
419
+ - Select and Combobox in a form: the `form` behavior saw their picks, a picked value is submitted
420
+ exactly once, and a form reset puts their first value back. A Combobox shows its value's label when
421
+ the items arrive after the value, after a refused pick and after Zag's own input writes; its
422
+ nameless input no longer sends `form.CHANGE { name: '' }` per keystroke. Select's hidden `<select>`
423
+ keeps an option for a selected value no longer in `items`. A checked Menu item's name no longer
424
+ includes its `✓`, and Menu's `label` is optional (an icon-only trigger named by `aria-label`).
425
+ - `sygnal/terminal`: a crash, `SIGTERM` or `SIGHUP` restores the terminal (raw mode, cursor, alternate
426
+ screen, mouse and paste modes) before the error is printed. Painting is about 16× faster (a 120×50
427
+ frame: 46 → 2.9 ms; an arrow key with the app's render: 40 → 5.9 ms), so held keys no longer lag.
428
+ Emoji no longer break box borders in xterm.js terminals (VS Code); wrapped lines no longer start
429
+ with the space they broke at; nothing redraws after `stop()`; `<textarea>` `rows` / `cols` set as
430
+ properties are read.
431
+
432
+ **From Temper's feedback (2026-10).** Friction points from building an app with Sygnal; assessment
433
+ and decisions in `dev-plans/research/temper-feedback-2026-10/`.
434
+ - **`simulateEvent` returns a promise.** `await t.simulateEvent('.retry', 'click')` resolves once the
435
+ event is delivered and the tree has rendered what it caused, so `t.query()` after it sees the new
436
+ view (right after the call it still shows the old one). It never rejects, and the fake clock moves
437
+ only while it is awaited. The type changes from `void` to `Promise<void>`.
438
+ - **Test DOM selectors:** the `+` and `~` combinators, and `nextElementSibling` /
439
+ `previousElementSibling` on mock elements. `t.queryAll('.a, .b')` lists in document order, which
440
+ the testing guide now shows for order checks.
441
+ - **SYG112** (a reply action with no model entry) names the ancestor that has the entry: a reply goes
442
+ only to its sender. The HTTP guide and the explanation show two ways to share a loader (a tagged
443
+ resource re-requested with `{ invalidate: tag }`, or handing the request to the owner via `EVENTS`).
444
+ - **SYG640** (an element command matched nothing) says when a matching element is rendered outside
445
+ the sender's view, and its fix, the explanation and the element-commands guide show the hand-off to
446
+ an ancestor (`EVENTS: event('NAME_FIRST')`, the ancestor sends `ELEMENT`).
447
+ - **New SYG436** (dev): a numeric `<VirtualCollection estimateSize>` more than 1.3× away from the
448
+ median of the first 10 measured rows. Too low an estimate leaves blank space at the end of a fast
449
+ scroll; the guide says so.
450
+ - **Switchable:** the guide's new "Pages with entry side effects" section. Every page in `of` is
451
+ created at mount and runs `BOOTSTRAP` then (7.1's per-page `state` map doesn't change that), so a
452
+ page whose start does something (a logout page) belongs in a Switchable that holds only the current
453
+ page, keyed by it: `<Switchable key={page} of={{ [page]: PAGES[page] }} current={page} />`. No new
454
+ prop (D172 stands).
455
+ - **Behaviour change: `t.simulateAction` fails on a name its target doesn't handle.** It reaches one
456
+ instance, the rendered component, like `next()`. A name with no model entry there (nor a behavior
457
+ action or a built-in) used to be dropped silently, and a test aimed at a child's action passed
458
+ without running it. It now fails the test (at the call when nothing is queued, else through the
459
+ next wait), naming the rendered components that handle it. To reach a child, pass
460
+ `{ component: Child }` (the first rendered instance; `within` picks a Collection item). Three of
461
+ Sygnal's own tests relied on the silent drop and were fixed: one form test that never reset its
462
+ form, one effect test written in a removed 6.0 key form, and one that used an unknown name as "an
463
+ action that sends nothing".
464
+ - **File inputs in tests:** `t.simulateEvent(sel, 'change', { files: [file] })` gives the target a
465
+ FileList (with `dom: 'real'`, a real one where `DataTransfer` exists, else defined on the element),
466
+ so a `form()` file field or an intent's `e.target.files` sees the file. With `dom: 'real'`, a
467
+ `target` property the DOM rejects (jsdom's `files`, a read-only property) used to be dropped
468
+ silently; it is now defined on the element, and the test fails naming the property only if that
469
+ fails too. The testing guide's new "File inputs" section shows the form upload flow
470
+ (`t.requests` → `multipart`, `t.progress`, `t.respond`).
471
+ - Size: 0 B in the kanban gate (the changes are in the test harness, the dev diagnostics and
472
+ `<VirtualCollection>`'s dev hook, which kanban doesn't import).
473
+
474
+ **Measured impact.** Size: the kanban gate is 42,731 B with `nativeGlobalThis: false` (7.1.0: 42,982 B;
475
+ budget 43,000 B, 269 B headroom). The widget fix saves 902 B. The core hooks for compiled views and
476
+ renderer field tags cost about 100 B, the cheaper Collection items about 260 B, and the record/replay hooks
477
+ with the handlers' `now()` / `random()` / `uuid()` about 140 B.
478
+ The new subpaths add nothing to an app that doesn't import them. The perf count gate is within its
479
+ limits.
480
+
481
+ **Companion packages.**
482
+ - **`sygnal-check`:** SYG302 also reports a default import from `xstream/extra/<name>` (the module
483
+ object under Node ESM and Vite 8: `dropRepeats()` "is not a function" after `tsc` and the checker
484
+ passed); `--fix` rewrites the eight extras `sygnal` exports (`import { dropRepeats } from 'sygnal'`).
485
+ New **SYG303**: an intent that composes `STATE.stream` with `dropRepeats()` (info; a warning under
486
+ `--strict`), pointing at `STATE.watch(fn)`.
487
+ - **`sygnal-check`:** incremental checking. A check session (`createCheckSession()`,
488
+ `checkFiles(files, { cache, session })`) reuses the previous check's model for the files a change
489
+ can't affect and replays the per-file rules' findings for them; the language server and
490
+ `--graph=html --watch` use one per project root. Same findings as a fresh check (a seeded edit
491
+ fuzz over the examples and fixtures checks it). On a 20k-line project a one-file edit re-checks in
492
+ about 40 ms instead of 130 ms; keystroke to diagnostics in the language server is gated at 300 ms
493
+ (was 400 ms), 150 ms debounce included. Faster in general: identifier lookups use a table per
494
+ scope, SYG702 no longer scans every element per field (it was quadratic on large apps), the
495
+ installed Sygnal is looked up once per directory, and the CLI uses Node's compile cache
496
+ (Node ≥ 22.1). A rule can declare `perFile` (README "Writing a rule"). SYG691: a procedure name that
497
+ isn't in an `orpcClient` contract, with a "did you mean". `shared()` from `sygnal/yjs` is a known
498
+ behavior: its key in `initialState` is not SYG127 (the behavior marker `ownsState: false`, also read
499
+ from a hand-written factory), and SYG260 reports `undo()` / `undoable()` on a shared key.
500
+ - **`sygnal-check`:** **SYG258** (warning): `Date.now()`, `new Date()`, `Math.random()` or
501
+ `crypto.randomUUID()` in a STATE reducer; **SYG509** (`--strict`) the same in any handler. `--fix`
502
+ rewrites them to the handler's `now()` / `random()` / `uuid()`. **SYG118**: a host intent that
503
+ replaces a trigger a behavior built from an option (the first-party parts, and `defineBehavior()`
504
+ calls it can follow). **SYG116** is also static: a non-STATE sink entry that is a function returning
505
+ `event()` (`EVENTS: (s) => event('TOAST', …)` sends nothing; write `event('TOAST', (s) => …)`).
506
+ **SYG141** also flags `events('change')` / `events('input')` on a Menu, Select or Combobox, with the
507
+ right event name as a quick fix. SYG422 checks a frame timer's `step` and `keys`. The first-party
508
+ behavior table knows the popover's `trigger` and `id` options and `DISMISS`, the dialog's `id`, and
509
+ `tooltips`.
510
+
511
+ ## 7.1.0 — 2026-10-20
512
+
513
+ Fixes and additions from building an app (Temper's Tune UI) on 7.0 ([PLAN-13](dev-plans/PLAN-13.md)):
514
+ an invalidation / navigation race, declaration statics that see the context, per-page Switchable
515
+ state, SSE resume and dead-stream detection, reply helpers for hand-written drivers, and more
516
+ precise `sygnal-check` findings. All additive.
517
+
518
+ **Measured impact.** Size: the kanban gate is (a) 42,982 B with `nativeGlobalThis: false` (7.0.0:
519
+ 42,844 B; budget 43,000 B, 18 B headroom) and (b) 38,987 B by default. Statics with context cost
520
+ 19 B and the per-page Switchable map 126 B (it reuses the owner's binding watch for a page whose
521
+ record is deleted); everything else is in drivers, the dev diagnostics entry, the test helper or
522
+ `sygnal-check`, and adds nothing to an app that doesn't use it. The perf count gate is within its
523
+ limits.
524
+
525
+ **Companion packages.**
526
+ - **`sygnal-check` 0.12.0:** SYG117 (`next()` to an action with no entry here), SYG302 (`import xs
527
+ from 'xstream'`, with `--fix`), SYG429 (a Switchable state map key that names no page); reply names
528
+ through request helpers and consts; SYG256 sees past helper calls in other fields; `next` followed
529
+ into helpers, imported ones included; context reads in the statics count for SYG170; `replyKeys`
530
+ in the config file. Its caret range doesn't reach 0.12.0 from `^0.11.0`, so update the dev
531
+ dependency.
532
+ - **`create-sygnal-app` 2.10.0:** templates depend on `sygnal` ^7.1.0 and `sygnal-check` ^0.12.0, use
533
+ a system font stack (no Google Fonts) and scaffold a `.gitignore`.
534
+ - **VS Code extension 0.9.0:** bundles `sygnal-check` 0.12.0.
535
+ - **Claude Code plugin 0.2.1:** the `sygnal-dev` skill covers 7.1 (statics with context, per-page
536
+ Switchable state, SSE resume, reply helpers, `import { xs } from 'sygnal'`).
537
+
538
+ ### Added
539
+
540
+ - **SYG117: `next()` to an action the component has no entry for (PLAN-13 N-1).** `next('X')`
541
+ reaches only its own component's model, so a child's `next('ENTER')` for an action only its
542
+ parent handles silently did nothing. The dev checks (`sygnal/diagnostics`, 0 production bytes)
543
+ warn on the first such `next()` per component and name, naming the ancestor that handles it and
544
+ the fix (send it up with `PARENT` or `EVENTS`), or the closest entry for a typo. `sygnal-check`
545
+ reports a literal `next('X')` with no entry `X` in the same component (with a rename fix for a typo).
546
+
547
+ - **sygnal-check reads reply names through request helpers and consts (PLAN-13 C-1).** An object
548
+ argument with a reply key of a call a sink returns is a request (`req('/a', { ok: 'GOT_A' })`,
549
+ `api.get_run(args, { ok: 'RUN' })`, any callee), a returned const is followed (`HTTP: () => SNAP`),
550
+ and so are `connections` specs and SSE `events` maps held in consts. SYG102 no longer reports
551
+ their reply actions as unreachable, SYG112 checks the names, SYG152 and `--graph` see the requests.
552
+
553
+ - **SYG256 sees past helper calls in other fields (PLAN-13 C-2).** A call in the STATE reducer
554
+ stopped the proof, so a leave-guard like `EDIT: { STATE: (s, d) => ({ ...s, dirty: true, edit:
555
+ { ...s.edit, [keyOf(s)]: d } }), ROUTER: (s) => s.dirty ? ABORT : … }` went unreported. A call
556
+ (or a function) inside a plain field's value now only makes that field unknown; calls in a test,
557
+ a spread or a computed key of the written object, and any assignment, still keep the check out.
558
+
559
+ - **create-sygnal-app: no Google Fonts, and a `.gitignore` (PLAN-13 T-1).** The vite, vite-pwa and
560
+ astro templates loaded Inter from Google Fonts; every template now uses a system font stack and
561
+ loads nothing remote. No template wrote a `.gitignore` (npm leaves it out of the package): each
562
+ ships `gitignore`, scaffolded as `.gitignore` (`node_modules`, `dist`, `.vite`, `.astro`,
563
+ `*.local`, `dev-dist` for the PWA). The release smoke checks both.
564
+
565
+ - **SYG302: `import xs from 'xstream'` (PLAN-13 X-1).** Under Node ESM and Vite 8 (a `"type":
566
+ "module"` package) xstream's CommonJS default import is the module object, so a custom driver's
567
+ `xs.create` is undefined. The docs, llms.txt and the skill use `import { xs } from 'sygnal'`
568
+ (the accessibility guide's sample was the last default import), the custom-drivers guide says
569
+ why, and `sygnal-check` reports the default import (info) with a `--fix`.
570
+
571
+ - **`makeReplies()` and `senderOf()` for drivers you write (PLAN-13 D-1).** The reply-action
572
+ protocol `makeFetchDriver`, `driverFromAsync` and the socket driver use is exported and typed
573
+ (`ReplySender`, `ReplyChannel`): spread `replies` into the source, `reply(senderOf(req), type,
574
+ data)` as often as needed (progress, then a result), and abort a disposed sender's work in
575
+ `onStop`. The custom-drivers guide has an upload driver that replies more than once.
576
+
577
+ - **SSE event ids, resuming and dead streams (PLAN-13 S-1).** On a `connections` spec: `meta:
578
+ true` gives SSE `message` / `events` actions `{ data, id, event }`; `resume: true | 'param'`
579
+ keeps the last event id and makes a reconnect the driver makes (after EventSource gave up, e.g. a
580
+ proxy 502, or a non-2xx on the `headers` path) resume from it, as `Last-Event-ID` over fetch or
581
+ `?lastEventId=` (or `?param=`) on the EventSource URL; `staleAfterMs` (SSE and WebSocket) closes
582
+ a connection that stays silent that long as a drop (`close` gets `stale: true`) so the reconnect
583
+ policy applies; over fetch, heartbeat comments count. `t.push(sink, data, { id })` gives a test
584
+ event its id. No bytes for apps without a socket driver.
585
+
586
+ - **Declaration statics see the context (PLAN-13 F-1).** `resources`, `timers`, `connections`,
587
+ `head` and `browser` get `(state, context)` and are computed again when the component's context
588
+ changes (by identity, D603; an unrelated write re-runs nothing). A routed page reads `route` from
589
+ context (`App.context = { route: (s) => s.route }`, `TaskPage.resources = { task: (s, { route })
590
+ => … }`) instead of keeping a copy of the params in its own state. Server rendering passes the
591
+ context to `resources` and `head` too; the types take it as the second parameter; `sygnal-check`
592
+ counts context reads in the statics for SYG170. Core: +19 B (kanban gzip).
593
+
594
+ - **Per-page Switchable state (PLAN-13 F-3).** `state` takes a map from page names to bindings
595
+ (`state={{ projects: 'pages.projects', settings: byId('teams', state.teamId) }}`): each listed
596
+ page binds its own key, path, lens or byId selection with a tag child's lifetime rules (another
597
+ target makes only that page again; a deleted target disposes it on its last state), and the
598
+ pages not listed share the owner's state. A key that names no page is the new **SYG429** (dev
599
+ checks and `sygnal-check`, with a rename fix). Types infer the page names from `of`. Core:
600
+ +126 B (kanban gzip), leaving 18 B of the 43,000 B budget with F-1.
601
+
602
+ - **Deep-link tests start on their route (PLAN-13 F-2).** `renderComponent(App, { router, url })`
603
+ applies the root's `route` entry's STATE reducer to the initial state with the `url`'s route
604
+ before the first render: `initialState`'s `router.current()` ran at module load, so a test used
605
+ to render `/` first and send its requests. The entry's other sinks (a guard's redirect) still run
606
+ once, when the router answers. Tests only; no core change.
607
+
608
+ - **sygnal-check follows `next` into helpers (PLAN-13 C-3).** A model that passes `next` to a
609
+ function (`advance(state, next)`), imported or in the same file and with any parameter name, has
610
+ that function's `next('X')` calls counted as its own, 3 calls deep: SYG102 sees the targets, and
611
+ SYG117 reports a bad one in the helper's file. A helper it can't read (a package) makes the
612
+ targets unknown, so SYG102 drops to info. The language server re-checks the component when the
613
+ helper changes.
614
+
615
+ - **`replyKeys` in sygnal-check's config (PLAN-13 C-4).** A custom driver's own reply keys
616
+ (`cancelled: 'UP_CANCELLED'`) can be listed in `sygnal-check.config.json`, for every sink
617
+ (`["cancelled"]`) or per sink (`{ "UPLOAD": ["progress", "cancelled"] }`): their actions count for
618
+ SYG102 and are checked by SYG112. Without the setting, a request key the checker doesn't know
619
+ whose value names a model entry makes that entry's SYG102 info, pointing at `replyKeys`.
620
+
621
+ ### Fixed
622
+
623
+ - **Follow-ups from trying the PLAN-13 build in Temper.** A Switchable page with a per-page
624
+ `state` map no longer reports SYG106 for the Switchable's own `state` prop (every page did, so
625
+ `expectNoDiagnostics()` failed). The `sygnal-check` command line reads `replyKeys` (check,
626
+ `--fix`, `--graph`, `--base`; the API and the language server already did). SSE `resume` replaces
627
+ an id parameter the URL already has instead of adding a second one. An `every` timer ticks once
628
+ per interval under a test clock that freezes `Date.now()` while timers run (Playwright's
629
+ `setFixedTime()`): its drift-free delays had grown by one interval per tick (1, 3, 6, 10 s).
630
+ - **An invalidation no longer resends a resource's old request after a navigation (PLAN-13 R-1).**
631
+ When a write's `ok` action navigated (`ROUTER`) and its `invalidates` (a URL prefix or a tag)
632
+ matched a resource of a lens- or key-bound page, the page's new request was sent and then the
633
+ invalidation sent the old one in the same lane, cancelling the new one: the new page showed the
634
+ old record. A load now stops when the resource's request changed (or it was paused) while it
635
+ wrote its loading state; refresh, polling and focus refetches go through the same path.
636
+
5
637
  ## 7.0.0 — 2026-10-19
6
638
 
7
639
  **Read [Migrating to 7.0](https://sygnal.js.org/guide/migrating-to-7/) before upgrading.** 7.0