sygnal 5.4.0 → 6.0.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 (316) hide show
  1. package/CHANGELOG.md +646 -0
  2. package/README.md +24 -9
  3. package/dist/astro/client.cjs.js +16 -9
  4. package/dist/astro/client.cjs.js.map +1 -1
  5. package/dist/astro/client.mjs +16 -9
  6. package/dist/astro/client.mjs.map +1 -1
  7. package/dist/astro/index.cjs.js +362 -91
  8. package/dist/astro/index.cjs.js.map +1 -1
  9. package/dist/astro/index.mjs +362 -92
  10. package/dist/astro/index.mjs.map +1 -1
  11. package/dist/astro/server.cjs.js +3396 -165
  12. package/dist/astro/server.cjs.js.map +1 -1
  13. package/dist/astro/server.mjs +3396 -165
  14. package/dist/astro/server.mjs.map +1 -1
  15. package/dist/devtools.cjs.js +1431 -0
  16. package/dist/devtools.cjs.js.map +1 -0
  17. package/dist/devtools.esm.js +1419 -0
  18. package/dist/devtools.esm.js.map +1 -0
  19. package/dist/diagnostics.cjs.js +3032 -296
  20. package/dist/diagnostics.cjs.js.map +1 -1
  21. package/dist/diagnostics.esm.js +3032 -296
  22. package/dist/diagnostics.esm.js.map +1 -1
  23. package/dist/element.cjs.js +230 -0
  24. package/dist/element.cjs.js.map +1 -0
  25. package/dist/element.esm.js +228 -0
  26. package/dist/element.esm.js.map +1 -0
  27. package/dist/guide/accessibility.md +257 -0
  28. package/dist/guide/adapters.md +221 -0
  29. package/dist/guide/behaviors.md +312 -0
  30. package/dist/guide/browser-sources.md +203 -0
  31. package/dist/guide/drag-and-drop.md +342 -0
  32. package/dist/guide/element-commands.md +306 -0
  33. package/dist/guide/error-boundaries.md +169 -0
  34. package/dist/guide/forms-reference.md +381 -0
  35. package/dist/guide/forms.md +250 -0
  36. package/dist/guide/http.md +299 -0
  37. package/dist/guide/inputs.md +173 -0
  38. package/dist/guide/persistence.md +245 -0
  39. package/dist/guide/recipes/carousel.md +155 -0
  40. package/dist/guide/recipes/charts.md +177 -0
  41. package/dist/guide/recipes/code-editor.md +135 -0
  42. package/dist/guide/recipes/data-grid.md +155 -0
  43. package/dist/guide/recipes/data-table.md +196 -0
  44. package/dist/guide/recipes/i18n.md +222 -0
  45. package/dist/guide/recipes/icons.md +115 -0
  46. package/dist/guide/recipes/overview.md +33 -0
  47. package/dist/guide/recipes/rich-text.md +154 -0
  48. package/dist/guide/resources.md +471 -0
  49. package/dist/guide/ssr.md +255 -0
  50. package/dist/guide/timers.md +178 -0
  51. package/dist/guide/ui/accordion.md +93 -0
  52. package/dist/guide/ui/combobox.md +144 -0
  53. package/dist/guide/ui/dialog.md +124 -0
  54. package/dist/guide/ui/disclosure.md +60 -0
  55. package/dist/guide/ui/menu.md +125 -0
  56. package/dist/guide/ui/overview.md +166 -0
  57. package/dist/guide/ui/popover.md +101 -0
  58. package/dist/guide/ui/select.md +114 -0
  59. package/dist/guide/ui/tabs.md +161 -0
  60. package/dist/guide/ui/toaster.md +152 -0
  61. package/dist/guide/ui/tooltip.md +103 -0
  62. package/dist/guide/undo.md +178 -0
  63. package/dist/guide/virtual-collections.md +183 -0
  64. package/dist/guide/web-components.md +179 -0
  65. package/dist/guide/widgets.md +217 -0
  66. package/dist/index.cjs.js +13178 -5769
  67. package/dist/index.cjs.js.map +1 -1
  68. package/dist/index.d.ts +2908 -276
  69. package/dist/index.esm.js +13149 -5779
  70. package/dist/index.esm.js.map +1 -1
  71. package/dist/jsx-dev-runtime.cjs.js +26 -334
  72. package/dist/jsx-dev-runtime.cjs.js.map +1 -1
  73. package/dist/jsx-dev-runtime.esm.js +19 -327
  74. package/dist/jsx-dev-runtime.esm.js.map +1 -1
  75. package/dist/jsx-runtime.cjs.js +26 -334
  76. package/dist/jsx-runtime.cjs.js.map +1 -1
  77. package/dist/jsx-runtime.esm.js +19 -327
  78. package/dist/jsx-runtime.esm.js.map +1 -1
  79. package/dist/jsx.cjs.js +10 -314
  80. package/dist/jsx.cjs.js.map +1 -1
  81. package/dist/jsx.esm.js +8 -315
  82. package/dist/jsx.esm.js.map +1 -1
  83. package/dist/react.cjs.js +117 -0
  84. package/dist/react.cjs.js.map +1 -0
  85. package/dist/react.esm.js +115 -0
  86. package/dist/react.esm.js.map +1 -0
  87. package/dist/shims/globalthis.cjs +20 -0
  88. package/dist/sygnal.min.js +1 -1
  89. package/dist/sygnal.min.js.map +1 -1
  90. package/dist/ui-combobox.cjs.js +169 -0
  91. package/dist/ui-combobox.cjs.js.map +1 -0
  92. package/dist/ui-combobox.esm.js +148 -0
  93. package/dist/ui-combobox.esm.js.map +1 -0
  94. package/dist/ui-menu.cjs.js +87 -0
  95. package/dist/ui-menu.cjs.js.map +1 -0
  96. package/dist/ui-menu.esm.js +66 -0
  97. package/dist/ui-menu.esm.js.map +1 -0
  98. package/dist/ui-select.cjs.js +104 -0
  99. package/dist/ui-select.cjs.js.map +1 -0
  100. package/dist/ui-select.esm.js +83 -0
  101. package/dist/ui-select.esm.js.map +1 -0
  102. package/dist/ui.cjs.js +738 -0
  103. package/dist/ui.cjs.js.map +1 -0
  104. package/dist/ui.esm.js +727 -0
  105. package/dist/ui.esm.js.map +1 -0
  106. package/dist/vike/ClientOnly.cjs.js +4 -14
  107. package/dist/vike/ClientOnly.cjs.js.map +1 -1
  108. package/dist/vike/ClientOnly.mjs +4 -14
  109. package/dist/vike/ClientOnly.mjs.map +1 -1
  110. package/dist/vike/{+config.js → config/+config.js} +9 -1
  111. package/dist/vike/config/+config.js.map +1 -0
  112. package/dist/vike/config/package.json +6 -0
  113. package/dist/vike/onRenderClient.cjs.js +165 -95
  114. package/dist/vike/onRenderClient.cjs.js.map +1 -1
  115. package/dist/vike/onRenderClient.mjs +165 -95
  116. package/dist/vike/onRenderClient.mjs.map +1 -1
  117. package/dist/vike/onRenderHtml.cjs.js +59 -24
  118. package/dist/vike/onRenderHtml.cjs.js.map +1 -1
  119. package/dist/vike/onRenderHtml.mjs +60 -25
  120. package/dist/vike/onRenderHtml.mjs.map +1 -1
  121. package/dist/vite/plugin.cjs.js +323 -88
  122. package/dist/vite/plugin.cjs.js.map +1 -1
  123. package/dist/vite/plugin.mjs +323 -89
  124. package/dist/vite/plugin.mjs.map +1 -1
  125. package/dist/zag.cjs.js +408 -0
  126. package/dist/zag.cjs.js.map +1 -0
  127. package/dist/zag.esm.js +405 -0
  128. package/dist/zag.esm.js.map +1 -0
  129. package/llms.txt +166 -103
  130. package/package.json +98 -14
  131. package/src/astro/client.ts +27 -16
  132. package/src/astro/index.d.ts +24 -0
  133. package/src/astro/index.ts +69 -3
  134. package/src/astro/server.ts +8 -2
  135. package/src/collection.ts +6 -93
  136. package/src/core/actions.ts +134 -0
  137. package/src/core/cell.ts +202 -0
  138. package/src/core/debug.ts +23 -0
  139. package/src/core/define.ts +242 -0
  140. package/src/core/hooks.ts +314 -0
  141. package/src/core/hosts/collection.ts +325 -0
  142. package/src/core/hosts/switchable.ts +146 -0
  143. package/src/core/instance.ts +592 -0
  144. package/src/core/markers/clientonly.ts +13 -0
  145. package/src/core/markers/lazy.ts +40 -0
  146. package/src/core/markers/portal.ts +96 -0
  147. package/src/core/markers/suspense.ts +41 -0
  148. package/src/core/markers/transition.ts +70 -0
  149. package/src/core/registry.ts +25 -0
  150. package/src/core/runtime.ts +573 -0
  151. package/src/core/statics.ts +104 -0
  152. package/src/core/teardown.ts +60 -0
  153. package/src/core/view.ts +37 -0
  154. package/src/cycle/dom/DocumentDOMSource.ts +13 -8
  155. package/src/cycle/dom/ElementFinder.ts +7 -9
  156. package/src/cycle/dom/EventDelegator.ts +174 -223
  157. package/src/cycle/dom/IsolateModule.ts +65 -35
  158. package/src/cycle/dom/MainDOMSource.ts +9 -2
  159. package/src/cycle/dom/PriorityQueue.ts +4 -2
  160. package/src/cycle/dom/SymbolTree.ts +18 -47
  161. package/src/cycle/dom/classNameModule.ts +9 -7
  162. package/src/cycle/dom/controlledInputModule.ts +23 -6
  163. package/src/cycle/dom/enrichEventStream.ts +25 -47
  164. package/src/cycle/dom/fragment.ts +7 -0
  165. package/src/cycle/dom/isolate.ts +30 -27
  166. package/src/cycle/dom/makeDOMDriver.ts +115 -56
  167. package/src/cycle/dom/mockDOMSource.ts +21 -1
  168. package/src/cycle/dom/modules.ts +16 -3
  169. package/src/cycle/dom/propsModule.ts +65 -0
  170. package/src/cycle/dom/selectModule.ts +22 -17
  171. package/src/cycle/dom/snabbdom.ts +1 -6
  172. package/src/cycle/dom/thunk.ts +3 -0
  173. package/src/cycle/dom/utils.ts +98 -0
  174. package/src/cycle/dom/viewTransition.ts +43 -0
  175. package/src/cycle/state/StateSource.ts +19 -3
  176. package/src/cycle/state/objIsEqual.ts +50 -0
  177. package/src/cycle/state/types.ts +0 -10
  178. package/src/defineComponent.ts +34 -0
  179. package/src/devtools.d.ts +151 -0
  180. package/src/devtools.ts +34 -0
  181. package/src/element.d.ts +59 -0
  182. package/src/element.ts +235 -0
  183. package/src/extra/backoff.ts +9 -0
  184. package/src/extra/behaviors.ts +169 -0
  185. package/src/extra/browserSignals.ts +23 -0
  186. package/src/extra/browserSources.ts +326 -0
  187. package/src/extra/command.ts +3 -3
  188. package/src/extra/controls.ts +47 -0
  189. package/src/extra/copyAsTest.ts +286 -0
  190. package/src/extra/devtools.ts +101 -31
  191. package/src/extra/devtoolsActions.ts +379 -0
  192. package/src/extra/devtoolsHook.ts +9 -0
  193. package/src/extra/devtoolsNext.ts +83 -0
  194. package/src/extra/diagnostics/checks/actionLog.ts +108 -0
  195. package/src/extra/diagnostics/checks/behaviors.ts +49 -0
  196. package/src/extra/diagnostics/checks/browserSources.ts +96 -0
  197. package/src/extra/diagnostics/checks/collections.ts +1 -1
  198. package/src/extra/diagnostics/checks/controls.ts +148 -0
  199. package/src/extra/diagnostics/checks/dataset.ts +57 -0
  200. package/src/extra/diagnostics/checks/dom.ts +20 -11
  201. package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
  202. package/src/extra/diagnostics/checks/events.ts +17 -2
  203. package/src/extra/diagnostics/checks/fetch.ts +135 -0
  204. package/src/extra/diagnostics/checks/forms.ts +163 -0
  205. package/src/extra/diagnostics/checks/index.ts +97 -4
  206. package/src/extra/diagnostics/checks/inspect.ts +140 -32
  207. package/src/extra/diagnostics/checks/next.ts +417 -0
  208. package/src/extra/diagnostics/checks/persist.ts +54 -0
  209. package/src/extra/diagnostics/checks/props.ts +10 -5
  210. package/src/extra/diagnostics/checks/public.d.ts +98 -7
  211. package/src/extra/diagnostics/checks/replies.ts +128 -0
  212. package/src/extra/diagnostics/checks/router.ts +100 -0
  213. package/src/extra/diagnostics/checks/rxjsHints.ts +1 -1
  214. package/src/extra/diagnostics/checks/shared.ts +86 -2
  215. package/src/extra/diagnostics/checks/shorthand.ts +115 -0
  216. package/src/extra/diagnostics/checks/sortable.ts +129 -0
  217. package/src/extra/diagnostics/checks/state.ts +100 -7
  218. package/src/extra/diagnostics/checks/statics.ts +48 -0
  219. package/src/extra/diagnostics/checks/strict.ts +10 -67
  220. package/src/extra/diagnostics/checks/timers.ts +80 -0
  221. package/src/extra/diagnostics/checks/viewTransitions.ts +47 -0
  222. package/src/extra/diagnostics/checks/virtual.ts +73 -0
  223. package/src/extra/diagnostics/checks/widgets.ts +109 -0
  224. package/src/extra/diagnostics/checks/wiring.ts +66 -10
  225. package/src/extra/diagnostics/codes.ts +268 -23
  226. package/src/extra/diagnostics/index.ts +34 -9
  227. package/src/extra/diagnostics/legacy.ts +17 -0
  228. package/src/extra/driverFactories.ts +83 -19
  229. package/src/extra/elementCommands.ts +53 -0
  230. package/src/extra/fetchDriver.ts +519 -0
  231. package/src/extra/focusWithin.ts +27 -0
  232. package/src/extra/form.ts +268 -0
  233. package/src/extra/formHelpers.ts +139 -0
  234. package/src/extra/head.ts +106 -0
  235. package/src/extra/hmr.ts +2 -3
  236. package/src/extra/owned.ts +24 -0
  237. package/src/extra/pager.ts +50 -0
  238. package/src/extra/persist.ts +133 -0
  239. package/src/extra/pwa.ts +1 -1
  240. package/src/extra/queryCache.ts +107 -0
  241. package/src/extra/reducers.ts +1 -1
  242. package/src/extra/reduxDevtools.ts +92 -0
  243. package/src/extra/replies.ts +54 -0
  244. package/src/extra/router.ts +323 -0
  245. package/src/extra/run.ts +90 -217
  246. package/src/extra/selection.ts +76 -0
  247. package/src/extra/socketDriver.ts +265 -0
  248. package/src/extra/sortable.ts +377 -0
  249. package/src/extra/ssr.ts +427 -160
  250. package/src/extra/standardSchema.ts +27 -0
  251. package/src/extra/testing.ts +2433 -175
  252. package/src/extra/timers.ts +95 -0
  253. package/src/extra/undo.ts +261 -0
  254. package/src/extra/viewTransitions.ts +38 -0
  255. package/src/extra/virtual.ts +513 -0
  256. package/src/extra/widget.ts +201 -0
  257. package/src/index.d.ts +2631 -117
  258. package/src/index.ts +25 -4
  259. package/src/jsx-runtime.ts +15 -1
  260. package/src/jsx.ts +2 -1
  261. package/src/lazy.ts +50 -7
  262. package/src/portal.ts +3 -1
  263. package/src/pragma/index.ts +256 -133
  264. package/src/react-peers.d.ts +5 -0
  265. package/src/react.d.ts +43 -0
  266. package/src/react.ts +110 -0
  267. package/src/shared.ts +48 -0
  268. package/src/slot.ts +1 -1
  269. package/src/suspense.ts +3 -1
  270. package/src/switchable.ts +6 -121
  271. package/src/transition.ts +3 -1
  272. package/src/ui/accordion.ts +64 -0
  273. package/src/ui/dialog.ts +129 -0
  274. package/src/ui/disclosure.ts +35 -0
  275. package/src/ui/popover.ts +45 -0
  276. package/src/ui/shared.ts +94 -0
  277. package/src/ui/tabs.ts +82 -0
  278. package/src/ui/toaster.ts +198 -0
  279. package/src/ui/tooltip.ts +70 -0
  280. package/src/ui/zag/combobox.ts +115 -0
  281. package/src/ui/zag/menu.ts +40 -0
  282. package/src/ui/zag/select.ts +54 -0
  283. package/src/ui/zag/shared.ts +43 -0
  284. package/src/ui-combobox.d.ts +20 -0
  285. package/src/ui-combobox.ts +6 -0
  286. package/src/ui-menu.d.ts +30 -0
  287. package/src/ui-menu.ts +6 -0
  288. package/src/ui-select.d.ts +17 -0
  289. package/src/ui-select.ts +6 -0
  290. package/src/ui-zag-types.d.ts +42 -0
  291. package/src/ui.d.ts +208 -0
  292. package/src/ui.ts +15 -0
  293. package/src/vike/+config.ts +9 -1
  294. package/src/vike/ClientOnly.ts +1 -1
  295. package/src/vike/onRenderClient.ts +144 -96
  296. package/src/vike/onRenderHtml.ts +69 -26
  297. package/src/vike/types.ts +7 -1
  298. package/src/vite/globalthis-shim.ts +18 -0
  299. package/src/vite/globalthis.ts +56 -0
  300. package/src/vite/plugin.d.ts +22 -3
  301. package/src/vite/plugin.ts +222 -24
  302. package/src/zag.d.ts +64 -0
  303. package/src/zag.ts +238 -0
  304. package/dist/vike/+config.cjs.js +0 -66
  305. package/dist/vike/+config.cjs.js.map +0 -1
  306. package/dist/vike/+config.js.map +0 -1
  307. package/src/component.ts +0 -2301
  308. package/src/cycle/isolate/index.ts +0 -196
  309. package/src/cycle/run/index.ts +0 -151
  310. package/src/cycle/run/internals.ts +0 -143
  311. package/src/cycle/state/Collection.ts +0 -184
  312. package/src/cycle/state/index.ts +0 -5
  313. package/src/cycle/state/pickCombine.ts +0 -201
  314. package/src/cycle/state/pickMerge.ts +0 -122
  315. package/src/cycle/state/withState.ts +0 -47
  316. package/src/pragma/fn.ts +0 -55
@@ -0,0 +1,95 @@
1
+ import {makeReplies} from './replies';
2
+
3
+ /*
4
+ * makeTimerDriver() (PLAN-4 GS-7, D114): timers declared per component instance from state,
5
+ * answered with reply actions (./replies.ts). Registered: `run(App, { TIMER: makeTimerDriver() })`.
6
+ * The core has no timer code (0 B): it sends a component's `timers` static to the source marked
7
+ * `__sygnalStatic: 'timers'` (initStatics), stamped with the sender (`__emitterId`).
8
+ *
9
+ * - Declaration: `C.timers = (state) => ({ [name]: spec | falsy })`, spec one of
10
+ * `{ every: ms, action }`, `{ after: ms, action }`, `{ frame: 'ACTION' }`, each with an
11
+ * optional `background: true`.
12
+ * - Diffed per (sender, name), comparing specs structurally: a new name starts, a removed or
13
+ * falsy one stops, a changed spec restarts (an equal one keeps running).
14
+ * - `every`: drift-free; tick n is due at start + n * every (each delay is computed from the
15
+ * start, not chained). A late tick (a throttled background tab) coalesces the missed ones:
16
+ * n jumps. Data `{ n, t }`: the tick number from 1, `t` = Date.now().
17
+ * - `after`: fires once, data `{ t }`. It doesn't fire again while the same spec stays declared.
18
+ * - `frame`: every animation frame (requestAnimationFrame; setTimeout 16 ms without it), data
19
+ * `{ t, dt }`: Date.now() and the ms since the previous frame (0 on the first).
20
+ * - A hidden Switchable page's timers stop unless `background: true` (the core sends only those
21
+ * while hidden) and start again from scratch when it is shown.
22
+ * - A disposed instance's timers stop; app dispose stops all. SSR runs no drivers: no timers.
23
+ * - An invalid spec (non-positive `every`, negative `after`, no action) doesn't start; the dev
24
+ * entry reports SYG422 through the `timerSpec` diagnostics hook.
25
+ */
26
+
27
+ const g: any = globalThis;
28
+ const now = () => Date.now();
29
+
30
+ const valid = (s: any) => s.frame
31
+ ? typeof s.frame == 'string'
32
+ : typeof s.action == 'string' && !!s.action && (s.every == null ? s.after >= 0 : s.after == null && s.every > 0) && (s.every || s.after) < 1 / 0;
33
+
34
+ // starts one timer; `e` (its entry) gets `d` once an `after` has fired. Returns the stop function.
35
+ // The next tick / frame is scheduled before the action is sent, so an action that stops (or
36
+ // changes) the timer clears the pending one.
37
+ const start = (s: any, send: (type: string, data: any) => void, e: any): (() => void) => {
38
+ let id: any, n = 0, last: any;
39
+ const t0 = now(), every = s.every, raf = s.frame && g.requestAnimationFrame;
40
+ const later = (ms: number) => id = raf ? requestAnimationFrame(tick) : setTimeout(tick, ms);
41
+ // a frame's `ts` is requestAnimationFrame's timestamp (the fallback passes none: the clock)
42
+ const tick = (ts = now()) => {
43
+ const t = now();
44
+ if (s.frame) {
45
+ later(16);
46
+ send(s.frame, {t, dt: last == null ? 0 : ts - last});
47
+ last = ts;
48
+ } else if (every) {
49
+ n = Math.max(n + 1, Math.floor((t - t0) / every));
50
+ later(t0 + (n + 1) * every - t);
51
+ send(s.action, {n, t});
52
+ } else {
53
+ e.d = 1;
54
+ send(s.action, {t});
55
+ }
56
+ };
57
+ later(s.frame ? 16 : every || s.after);
58
+ return () => raf ? cancelAnimationFrame(id) : clearTimeout(id);
59
+ };
60
+
61
+ /**
62
+ * The driver over `runners` (sender → { c: component name, on: { [name]: entry } }, an entry
63
+ * being { k: the spec's JSON, s: the spec, x: stop, ok: started, d: an `after` that fired }).
64
+ * The testing fake passes its own map to list the active timers (t.timers).
65
+ */
66
+ export const timerDriver = (runners: Map<any, any>) => (sink$: any) => {
67
+ const update = (id: any, d: any, c?: any) => {
68
+ let r = runners.get(id);
69
+ if (!r) runners.set(id, r = {c, on: {}});
70
+ const on = r.on;
71
+ for (const n in on) if (on[n].k != JSON.stringify(d?.[n])) on[n].x(), delete on[n];
72
+ for (const n in d) {
73
+ const s = d[n];
74
+ if (!s || on[n]) continue;
75
+ const e: any = on[n] = {k: JSON.stringify(s), s, x: () => {}};
76
+ if (e.ok = valid(s)) e.x = start(s, (type, data) => reply(id, type, data), e);
77
+ else g.__SYGNAL_DIAGNOSTICS__?.timerSpec?.(n, s, c);
78
+ }
79
+ if (!d) runners.delete(id);
80
+ };
81
+ const stopAll = () => runners.forEach((_, id) => update(id, 0));
82
+ const {replies, reply} = makeReplies(id => update(id, 0));
83
+ sink$.addListener({
84
+ next: (v: any) => { if (v && v.__emitterId !== undefined && 'timers' in v) update(v.__emitterId, v.timers, v.__emitterName); },
85
+ error: () => {},
86
+ complete: stopAll,
87
+ });
88
+ return {...replies, __sygnalStatic: 'timers', dispose: stopAll};
89
+ };
90
+
91
+ /**
92
+ * Timers declared by components (`C.timers = (state) => ({ tick: state.running && { every: 100,
93
+ * action: 'TICK' } })`), delivered as their own actions: `run(App, { TIMER: makeTimerDriver() })`.
94
+ */
95
+ export const makeTimerDriver = () => timerDriver(new Map());
@@ -0,0 +1,261 @@
1
+ /*
2
+ * PLAN-4 GS-8: undo / redo.
3
+ *
4
+ * Editor.model = undoable({ TYPE: ..., LOAD: ... }, { key: 'doc', coalesceMs: 500, resetOn: ['LOAD'] })
5
+ * → state.history = { past, future }; adds UNDO and REDO (the host's intent triggers them)
6
+ * Editor.uses = { history: undo({ key: 'doc', undo: UndoButton, redo: RedoButton }) }
7
+ * → the same as a behavior: state.history also has canUndo / canRedo, the actions are
8
+ * 'history.UNDO' / 'history.REDO', and the controls trigger them
9
+ *
10
+ * Every STATE reducer (function entries, `STATE` of object entries, 'ACTION | STATE' keys) is
11
+ * wrapped: when its result has a different state[key], the old state[key] is pushed onto
12
+ * `past` (at most `limit`, default 100) and `future` is cleared. With `track`, only those
13
+ * actions are recorded; built-in actions (INITIALIZE ...) only when tracked. A change by the
14
+ * same action within `coalesceMs` of the previous one joins that step (typing). With
15
+ * `coalesce` (4-G1, D143) only the listed actions join (`coalesceMs` then defaults to 500);
16
+ * every other action is always its own step (two quick clicks on Larger are two). `resetOn`
17
+ * actions clear the history and are not recorded. Snapshots are references: reducers must
18
+ * return new objects (they do in Sygnal). A model's own UNDO / REDO entry runs after the
19
+ * built-in one (as a host entry for a behavior action does, D123).
20
+ *
21
+ * Gestures (3-H G-447): another behavior in the host's `uses` that declares `undoStep` (its
22
+ * actions that complete a step: sortable's DROPPED) makes its other actions gesture steps. Their
23
+ * changes aren't recorded; `history.base` holds the value from before the gesture until the
24
+ * completing action records it as one entry. Works in either `uses` order (undo first: the
25
+ * behavior's actions get host entries that run after them). undo() as a behavior only:
26
+ * undoable() can't see `uses`. 3-L: a recorded action, UNDO or REDO during a gesture first
27
+ * records the value from before it (G-473: it stays reachable); a gesture step that brings the
28
+ * value back to where the gesture started (a cancel) leaves nothing pending, nor does a gesture
29
+ * action after the value was changed outside the gesture (G-475 / G-479). `track` / `coalesce`
30
+ * naming any of a gesture behavior's actions apply to its steps; `resetOn` naming one of them
31
+ * resets the history at that action (G-477). 3-O: an untracked gesture is like an untracked
32
+ * action, its changes are never recorded, nor pending (G-506); drops join only when `coalesce`
33
+ * names a gesture action (G-507); REDO mid-gesture records the value from before the gesture in
34
+ * place of the current one, as a recorded action does (G-508); "back where it started" compares
35
+ * arrays and plain objects shallowly (G-509). 3-U: with `key: [...]` every comparison is key by key
36
+ * (G-529 / G-530), a key the state didn't have stays out on UNDO / REDO, and `key: []` throws (G-531).
37
+ *
38
+ * SYG226 (dev, warn): a `track` / `resetOn` / `coalesce` name with no model entry (nor an action
39
+ * of a behavior in the host's `uses`).
40
+ */
41
+ import { defineBehavior } from './behaviors'
42
+ import { ABORT, isAbort } from '../shared'
43
+
44
+ const BUILT_IN = /^(BOOTSTRAP|INITIALIZE|DISPOSE|READY|RESOURCE)$/
45
+ // the last recorded change of a history, keyed by its `past` array (which a behavior's calculated
46
+ // fields keep while they copy the history object): [action, time] (coalescing; never in state)
47
+ const last = new WeakMap<object, [string, number]>()
48
+ const reported = new WeakSet<object>()
49
+
50
+ export interface UndoOptions { key: string | string[], limit?: number, track?: string[], coalesce?: string[], coalesceMs?: number, resetOn?: string[] }
51
+
52
+ const actionOf = (a: string) => a.split('|')[0].trim()
53
+
54
+ // 3-U G-531: `key: []` keeps nothing (a definition error, in every build)
55
+ const keyed = (o: UndoOptions) => {
56
+ if (Array.isArray(o?.key) && !o.key.length) throw new Error("undo: `key: []` names no state key; use key: 'name' or key: ['a', 'b']")
57
+ }
58
+
59
+ // SYG226: report through the diagnostics core when it is enabled (dev); nothing in production
60
+ const check = (model: any, o: UndoOptions, component?: any, skip?: any) => {
61
+ const known = new Set(Object.keys(model || {}).map(actionOf))
62
+ for (const a of [...(o.track || []), ...(o.resetOn || []), ...(o.coalesce || [])]) {
63
+ const i = a.indexOf('.'), m = i > 0 && skip?.[a.slice(0, i)]?.model
64
+ // a behavior's action (G-477: one it doesn't have is reported)
65
+ if (known.has(a) || m && a.slice(i + 1) in m) continue
66
+ try {
67
+ (globalThis as any).__SYGNAL_DIAGNOSTICS__?.report('SYG226', {
68
+ severity: 'warn', component, data: {action: a},
69
+ message: `undo ${o.track?.includes(a) ? 'track' : o.resetOn?.includes(a) ? 'resetOn' : 'coalesce'} names '${a}', which has no model entry`,
70
+ fix: `Use the name of a model entry, or add '${a}' to the model`,
71
+ })
72
+ } catch (e) { queueMicrotask(() => { throw e }) }
73
+ }
74
+ }
75
+
76
+ // a history without the pending gesture base
77
+ const settled = (h: any) => { const {base: _, ...o} = h; return o }
78
+ // the same value (a gesture step that put everything back: a cancel): arrays and plain objects
79
+ // compare their entries by identity (G-509), anything else is itself
80
+ const plain = (x: any) => Array.isArray(x) ? 1 : x && typeof x == 'object' && Object.getPrototypeOf(x) === Object.prototype ? 2 : 0
81
+ const same1 = (a: any, b: any) => {
82
+ const k = a !== b && plain(a) && plain(a) == plain(b) && Object.keys(a)
83
+ return a === b || !!k && k.length == Object.keys(b).length && k.every(i => i in b && a[i] === b[i])
84
+ }
85
+
86
+ const wrap = (model: any, o: UndoOptions, hk: string, ns: string, S = 'STATE', g: any = {}): any => {
87
+ const {key, limit = 100, track, coalesce, coalesceMs = coalesce ? 500 : 0, resetOn = []} = o, keys = typeof key == 'string' ? null : key
88
+ const out: any = {}
89
+ const hist = (s: any) => s?.[hk] || {past: [], future: []}
90
+ // the value undo keeps: state[key], or (3-P G-515: `key: ['todo', 'done']`) an object of those
91
+ // keys the state has; values compare key by key (3-U G-529 / G-530: no shared identity, no cache).
92
+ // 3-W G-536: a key the state lacks and one it has as `undefined` differ for an array key (a key
93
+ // that appears or goes is a change); for a string key both are the value `undefined`. Primitive
94
+ // state (before the first object) has none of the keys
95
+ const val = (s: any) => {
96
+ if (!keys) return s?.[key as string]
97
+ const v: any = {}
98
+ for (const k of keys) if (s && typeof s == 'object' && k in s) v[k] = s[k]
99
+ return v
100
+ }
101
+ const by = (a: any, b: any, f: any) => !!a && !!b && keys!.every(k => k in a == k in b && f(a[k], b[k]))
102
+ const eq = (a: any, b: any) => keys ? a === b || by(a, b, (x: any, y: any) => x === y) : a === b
103
+ const same = (a: any, b: any) => keys ? by(a, b, same1) : same1(a, b)
104
+ // G-531: a key the snapshot didn't have is left out (not set to undefined); G-536: so is a string
105
+ // key whose snapshot is undefined
106
+ const set = (s: any, v: any) => {
107
+ const r = keys ? {...s, ...v} : {...s, [key as string]: v}
108
+ for (const k of keys || [key as string]) if (keys ? !(k in v) : v === undefined) delete r[k]
109
+ return r
110
+ }
111
+ // one more entry in `past` (`join`: the change joins the last one)
112
+ const rec = (r: any, h: any, v: any, name: string, joins: any) => {
113
+ const at = Date.now(), prev = last.get(h.past)
114
+ const join = joins && prev && prev[0] == name && at - prev[1] < coalesceMs && h.past.length
115
+ const nh = {...settled(h), past: join ? h.past : [...h.past, v].slice(-limit), future: []}
116
+ last.set(nh.past, [name, at])
117
+ return {...r, [hk]: nh}
118
+ }
119
+ // 3-H G-447: a gesture's actions (`g`: another behavior's actions, 1 a step, 2 the one that
120
+ // completes it, its `undoStep`). A step's change isn't recorded: `base` keeps [the value before
121
+ // the gesture's first step, the value after its last]; the completing action records that
122
+ // first value as one entry. A gesture that ends without it (cancelled) records nothing. The
123
+ // pre-action state comes from the handler's props (`state`): as the host entry after the
124
+ // behavior's (undo before it in `uses`), the reducer gets the behavior's result. `all`: the
125
+ // gesture behavior's actions (G-477: `track` / `coalesce` naming any of them cover its steps);
126
+ // `n` its key in `uses`, `end` its internal `undoEnd(slice)`: true when the slice an action
127
+ // left ended the gesture without completing it (3-P G-510: sortable's Escape, unmount, reset)
128
+ const gw = (f: any, [kind, all, n, end]: any, name: string) => {
129
+ const has = (l?: string[]) => !!l && all.some((x: string) => l.includes(x))
130
+ // G-507: drops join only when `coalesce` names the gesture (two separate drags are two steps)
131
+ const tracked = !track || has(track), joins = coalesceMs > 0 && has(coalesce), reset = resetOn.includes(name)
132
+ return (s: any, ...x: any[]) => {
133
+ const r0 = f ? f(s, ...x) : s, r = isAbort(r0) ? s : r0, pre = x[2]?.state || s, none = f ? r0 : ABORT
134
+ if (!r || typeof r != 'object' || !pre) return none
135
+ const h = hist(r), b = h.base, v = val(r), p = val(pre)
136
+ // G-477: resetOn names this gesture action
137
+ if (reset) return h.past.length || h.future.length || b ? {...r, [hk]: {...settled(h), past: [], future: []}} : none
138
+ // G-506: an untracked gesture is an untracked change: nothing recorded, nothing pending
139
+ if (!tracked) return none
140
+ // the base holds while the value is the one the gesture's last step left (G-475: else it
141
+ // was changed outside the gesture, e.g. restored, and the base is stale)
142
+ const on = b && eq(b[1], p) && b, stop = kind == 1 && end?.(r[n])
143
+ if (kind == 1 && !stop) {
144
+ if (eq(v, p)) return b && !on ? {...r, [hk]: settled(h)} : none
145
+ const b0 = on ? on[0] : p
146
+ // back where the gesture started (Escape, an item moved back): nothing pending (G-479);
147
+ // the base is this behavior's (G-514: a step joins another's pending base and takes it)
148
+ return {...r, [hk]: same(b0, v) ? settled(h) : {...h, base: [b0, v, n]}}
149
+ }
150
+ // G-514: only the behavior that opened the base completes it
151
+ if (b && b[2] != null && b[2] !== n || !b && !stop) return none
152
+ // G-510: ended without completing: what it leaves (the item where it is, or a restore after
153
+ // the value changed) is one step from the value before the gesture, unless it is that value
154
+ if (stop) {
155
+ const b0 = on ? on[0] : p
156
+ return same(b0, v) ? b ? {...r, [hk]: settled(h)} : none : rec(r, h, b0, name, 0)
157
+ }
158
+ return !on || eq(b[0], v) ? {...r, [hk]: settled(h)} : rec(r, h, b[0], name, joins)
159
+ }
160
+ }
161
+ // G-473: a pending gesture's base, recorded before another change: the value from before the
162
+ // gesture stays reachable (its steps join that change's entry)
163
+ const before = (h: any, v: any) => { const b = h.base; return b && eq(b[1], v) && !eq(b[0], v) ? [b[0]] : null }
164
+ for (const a in g) if (!(a in model)) out[a] = {[S]: gw(null, g[a], a)}
165
+ for (const a in model) {
166
+ const e = model[a], [name, sink] = a.split('|').map(x => x.trim())
167
+ if (g[a]) {
168
+ const f = typeof e == 'function' ? e : e?.[S]
169
+ out[a] = typeof e == 'function' ? gw(e, g[a], a) : {...e, [S]: gw(typeof f == 'function' ? f : null, g[a], a)}
170
+ continue
171
+ }
172
+ const reset = resetOn.includes(name), joins = coalesceMs > 0 && (!coalesce || coalesce.includes(name))
173
+ const f0 = sink ? (sink == S ? e : null) : typeof e == 'function' ? e : e?.[S]
174
+ // a constant STATE value isn't a reducer: the entry is left alone (G-214)
175
+ if (f0 != null && typeof f0 != 'function') { out[a] = e; continue }
176
+ const f = f0
177
+ // an entry without a STATE reducer is left alone, except a resetOn object entry (gets one)
178
+ if (!(f || reset && !sink) || !(reset || (track ? track.includes(name) : !BUILT_IN.test(name)))) { out[a] = e; continue }
179
+ const w = (s: any, ...x: any[]) => {
180
+ const r = f ? f(s, ...x) : s, h = hist(s)
181
+ if (!r || typeof r != 'object' || isAbort(r)) return r
182
+ if (reset) return h.past.length || h.future.length || h.base ? {...r, [hk]: {...settled(h), past: [], future: []}} : r
183
+ const v = val(s)
184
+ if (r === s || eq(val(r), v)) return r
185
+ const b0 = before(h, v)
186
+ return rec(r, h, b0 ? b0[0] : v, name, !b0 && joins)
187
+ }
188
+ out[a] = sink || typeof e == 'function' ? w : {...e, [S]: w}
189
+ }
190
+ // mid-gesture, the value from before the gesture stands in for the current one: UNDO goes back
191
+ // to it (G-473: the drag so far is the step undone), REDO records it (G-508: the drag so far
192
+ // joins the redone step, as with a recorded action; at most `limit`)
193
+ const step = (u: boolean) => (s: any) => {
194
+ const h = hist(s), v = val(s), b = before(h, v), {past, future} = h
195
+ if (!(u ? b || past.length : future.length)) return ABORT
196
+ return {...set(s, u ? b ? b[0] : past[past.length - 1] : future[0]), [hk]: {...settled(h),
197
+ past: u ? b ? past : past.slice(0, -1) : [...past, b ? b[0] : v].slice(-limit),
198
+ future: u ? [v, ...future] : future.slice(1)}}
199
+ }
200
+ const add = (a: string, f: any) => {
201
+ const own = out[ns + a]
202
+ if (!own) return out[ns + a] = f
203
+ const g = typeof own == 'function' ? own : own[S]
204
+ // the model's own entry runs after the built-in step (its ABORT keeps the step)
205
+ const both = typeof g != 'function' ? f : (s: any, ...x: any[]) => {
206
+ const r = f(s, ...x), q = g(isAbort(r) ? s : r, ...x)
207
+ return isAbort(q) ? r : q
208
+ }
209
+ out[ns + a] = typeof own == 'function' ? both : {...own, [S]: both}
210
+ }
211
+ add('UNDO', step(true))
212
+ add('REDO', step(false))
213
+ return out
214
+ }
215
+
216
+ /**
217
+ * Wraps a model's STATE reducers so `state[key]` gets undo / redo (GS-8): `state.history =
218
+ * { past, future }`, plus UNDO and REDO entries for the intent to trigger. Options: `key` (the
219
+ * state key to snapshot), `limit` (100), `track` (only these actions), `coalesceMs` (join rapid
220
+ * changes by one action), `coalesce` (only these actions join; default window 500 ms), `resetOn`
221
+ * (actions that clear the history). Returns a new model.
222
+ */
223
+ export const undoable = (model: any, options: UndoOptions): any => {
224
+ keyed(options)
225
+ check(model, options)
226
+ return wrap(model, options, 'history', '')
227
+ }
228
+
229
+ /**
230
+ * undoable() as a behavior (GS-8 + GS-1): `uses = { history: undo({ key: 'doc', undo: Undo,
231
+ * redo: Redo }) }` gives `state.history = { past, future, canUndo, canRedo }` and the actions
232
+ * 'history.UNDO' / 'history.REDO' (the `undo` / `redo` controls trigger them; so can a host
233
+ * intent action of that name). The other options are undoable()'s; `track` and `resetOn` name
234
+ * the host's actions.
235
+ */
236
+ export const undo = (options: UndoOptions & { undo?: any, redo?: any }): any => {
237
+ keyed(options)
238
+ const b = defineBehavior({
239
+ initialState: {past: [], future: []},
240
+ intent: ({DOM}: any, {undo, redo}: any) => ({
241
+ ...(undo && {UNDO: DOM.click(undo)}),
242
+ ...(redo && {REDO: DOM.click(redo)}),
243
+ }),
244
+ // placeholders (no change): they make the actions behavior-owned; the full-state UNDO / REDO
245
+ // that wrap() adds for 'key.UNDO' runs after them, as a host entry would (D123)
246
+ model: {UNDO: () => ABORT, REDO: () => ABORT},
247
+ calculated: {canUndo: (h: any) => h.past.length > 0, canRedo: (h: any) => h.future.length > 0},
248
+ })(options), merge = b.merge
249
+ b.merge = (c: any, k: string) => {
250
+ const u = c.view?.uses, g: any = {}
251
+ if (!reported.has(b)) reported.add(b), check(c.model, options, c, u)
252
+ // the gestures of the host's other behaviors (sortable's drag: one step per drop), in either `uses` order
253
+ for (const n in u) {
254
+ const s = u[n]?.undoStep, all = Object.keys(u[n]?.model || {}).map(a => n + '.' + a)
255
+ if (s) for (const a in u[n].model) g[n + '.' + a] = [s.includes(a) ? 2 : 1, all, n, u[n].undoEnd]
256
+ }
257
+ c.model = wrap(c.model || {}, options, k, k + '.', c.stateSourceName, g)
258
+ merge(c, k)
259
+ }
260
+ return b
261
+ }
@@ -0,0 +1,38 @@
1
+ import type {Stream} from 'xstream';
2
+ import type {VNode} from '../cycle/dom/snabbdom';
3
+ import {makeDOMDriver, DOMDriverOptions} from '../cycle/dom/makeDOMDriver';
4
+ import {viewTransition$} from '../cycle/dom/viewTransition';
5
+
6
+ /**
7
+ * PLAN-4 GS-12 (D129): a DOM driver that can patch inside `document.startViewTransition()`.
8
+ * It is `makeDOMDriver(mountPoint, options)`, as run()'s own DOM driver, plus the hook: the patch caused by an action listed in a component's
9
+ * `viewTransitions` static (`Board.viewTransitions = ['MOVE']`, `App.viewTransitions = ['ROUTE']`)
10
+ * runs as a View Transition. Opt-in, so the hook costs nothing in apps that don't import it.
11
+ *
12
+ * run(App, { DOM: makeViewTransitionDOMDriver('#root') })
13
+ */
14
+ export function makeViewTransitionDOMDriver(mountPoint: string | Element | DocumentFragment = '#root', options: DOMDriverOptions = {}) {
15
+ const inner: any = makeDOMDriver(mountPoint, options);
16
+ return (vnode$: Stream<VNode>, name?: string) => {
17
+ let source: any;
18
+ // the flags live on the driver's IsolateModule (the core sets `vt` there; it is per app),
19
+ // which exists once the inner driver has run: read lazily, at the first vnode
20
+ source = inner(viewTransition$(vnode$, () => source?._isolateModule), name);
21
+ const im = source._isolateModule;
22
+ // SYG645 (dev): this app's DOM driver can do View Transitions
23
+ im.vtDriver = 1;
24
+ // a request is consumed by the next vnode (viewTransition$ resets it), however late: a render
25
+ // may take long. It expires when the page has been idle for 100 ms without one, since a listed
26
+ // action whose new state renders the same view (the first ROUTE reply, say) sends no vnode and
27
+ // must not animate a later, unrelated patch. A slow render keeps the main thread busy, so the
28
+ // expiry timer then runs late: it is re-armed instead (late by more than 30 ms = busy)
29
+ let on = 0, t: any;
30
+ const expire = (due: number): any => t = setTimeout(() => Date.now() - due > 30 ? expire(Date.now() + 100) : on = 0, 100);
31
+ Object.defineProperty(im, 'vt', {
32
+ configurable: true,
33
+ get: () => on,
34
+ set: (v: any) => { clearTimeout(t); (on = v ? 1 : 0) && expire(Date.now() + 100); },
35
+ });
36
+ return source;
37
+ };
38
+ }