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,50 @@
1
+ /** Structural equality (to depth 5): the core's state/props/context diffing and STATE.watch (GS-6) */
2
+ export function objIsEqual(obj1: any, obj2?: any, maxDepth: number = 5, depth: number = 0): boolean {
3
+ // Past maxDepth the values count as different (false): the caller then treats them as changed
4
+ if (depth > maxDepth) {
5
+ return false;
6
+ }
7
+
8
+ // If both are the same object or are both exactly null or undefined
9
+ if (obj1 === obj2) {
10
+ return true;
11
+ }
12
+
13
+ // If either is not an object (null, undefined, or primitive), directly compare
14
+ if (typeof obj1 !== 'object' || obj1 === null || typeof obj2 !== 'object' || obj2 === null) {
15
+ return false;
16
+ }
17
+
18
+ // Special handling for arrays
19
+ if (Array.isArray(obj1) && Array.isArray(obj2)) {
20
+ if (obj1.length !== obj2.length) {
21
+ return false;
22
+ }
23
+ for (let i = 0; i < obj1.length; i++) {
24
+ if (!objIsEqual(obj1[i], obj2[i], maxDepth, depth + 1)) {
25
+ return false;
26
+ }
27
+ }
28
+ return true;
29
+ }
30
+
31
+ // Get keys of both objects
32
+ const keys1 = Object.keys(obj1);
33
+
34
+ // Check if the number of properties is different
35
+ if (keys1.length !== Object.keys(obj2).length) {
36
+ return false;
37
+ }
38
+
39
+ // Recursively check each property
40
+ for (const key of keys1) {
41
+ if (!(key in obj2)) {
42
+ return false;
43
+ }
44
+ if (!objIsEqual(obj1[key], obj2[key], maxDepth, depth + 1)) {
45
+ return false;
46
+ }
47
+ }
48
+
49
+ return true;
50
+ }
@@ -9,14 +9,4 @@ export type Lens<T, R> = {
9
9
  get: Getter<T, R>;
10
10
  set: Setter<T, R>;
11
11
  };
12
- export type ItemScopeFn = (key: string) => string | object;
13
- export type ItemKeyFn<S> = (state: S, index?: number) => string;
14
- export type ItemFactoryFn<S, So, Si> = (
15
- state: S,
16
- index?: number
17
- ) => MainFn<So, Si>;
18
12
  export type Scope<T, R> = string | number | Lens<T, R>;
19
- export type InternalInstances<Si> = {
20
- dict: Map<string, Si>;
21
- arr: Array<Si & {_key: string}>;
22
- };
@@ -0,0 +1,34 @@
1
+ /**
2
+ * PLAN-4.6 (D162): `defineComponent(opts)` builds a component from an options object, for code
3
+ * that makes components from data (generators, wrappers). It returns an ordinary function
4
+ * component: a new function that calls `view` with the one view argument, with the other options
5
+ * assigned as its statics. There is no separate instantiation path: the core reads the statics
6
+ * like any function component's.
7
+ *
8
+ * const Counter = defineComponent({ name: 'Counter', view, intent, model, initialState: { n: 0 } })
9
+ *
10
+ * `name` becomes `componentName` (diagnostics, devtools, uid()); without it, the view's
11
+ * componentName or function name (G-343: not the `view` an inline `view: () => …` is given;
12
+ * 'Component' then). Statics already on the view are copied, and the options override them. The
13
+ * view function itself is not changed, so one view can back several definitions; the dev checks
14
+ * read it from `__sygnalView` (its arity: positional views).
15
+ */
16
+ export function defineComponent(opts: Record<string, any>): any {
17
+ if (!opts || typeof opts !== 'object') {
18
+ throw new TypeError('defineComponent(opts): pass an options object, e.g. defineComponent({ view, model, initialState })')
19
+ }
20
+ const {view, name, ...statics} = opts
21
+ if (typeof view !== 'function') {
22
+ throw new TypeError(`defineComponent(${name ? `{ name: '${name}' }` : 'opts'}): view must be a function (the view: ({ state, context, ...props }) => vnode)`)
23
+ }
24
+ const C: any = (args: any) => view(args)
25
+ for (const k of Object.keys(view)) C[k] = view[k]
26
+ const componentName = name || view.componentName || (view.name && view.name != 'view' ? view.name : 'Component')
27
+ C.componentName = componentName
28
+ try { Object.defineProperty(C, 'name', {value: componentName, configurable: true}) } catch (_) { /* name not configurable */ }
29
+ for (const k of Object.keys(statics)) if (statics[k] !== undefined) C[k] = statics[k]
30
+ Object.defineProperty(C, '__sygnalView', {value: view.__sygnalView || view, configurable: true})
31
+ return C
32
+ }
33
+
34
+ export default defineComponent
@@ -0,0 +1,151 @@
1
+ // 'sygnal/devtools' (D77): importing it installs the DevTools bridge
2
+ // (window.__SYGNAL_DEVTOOLS__) in a browser, for the Sygnal DevTools extension.
3
+ // Dev only: sygnal/vite injects it in dev; without Vite, import it before run().
4
+ // Self-contained (like the other dev entries' types); the same shape as
5
+ // SygnalDevTools in 'sygnal'.
6
+ // PLAN-4 3-E (GS-10): it also records the action log and writes "Copy as test" tests.
7
+
8
+ export interface SygnalDevTools {
9
+ /** true while the browser extension is connected */
10
+ readonly connected: boolean
11
+ /** Diagnostics collected so far (same as getDiagnostics() from 'sygnal') */
12
+ getDiagnostics(): Array<{ code: string; severity: string; message: string; [key: string]: any }>
13
+ /** The app graph; present only when the 'sygnal/diagnostics' dev entry is loaded */
14
+ inspect?(): any
15
+ /** PLAN-4 3-E: defaults for "Copy as test" from the extension panel (componentImport, drivers, ...) */
16
+ configureCopyAsTest(options: CopyAsTestOptions): void
17
+ /** PLAN-4 3-E: one instance's recorded session (as getSession()) */
18
+ getSession(target?: Exclude<SessionTarget, SessionRecording>): SessionRecording
19
+ }
20
+
21
+ /** The bridge singleton of this entry (also window.__SYGNAL_DEVTOOLS__ once installed). */
22
+ export declare function getDevTools(): SygnalDevTools
23
+ /** Install the bridge (done on import; idempotent). Undefined outside a browser. */
24
+ export declare function installDevTools(): SygnalDevTools | undefined
25
+
26
+ // ── PLAN-4 3-E (GS-10): the action log, "Copy as test", the Redux DevTools bridge ──
27
+
28
+ export type DevtoolsActionCause = 'intent' | 'next' | 'reply' | 'built-in' | 'simulateAction' | 'behavior'
29
+
30
+ /** One recorded action (the entry records every component instance's actions) */
31
+ export interface DevtoolsAction {
32
+ /** Sequence number (session-wide) */
33
+ seq: number
34
+ /** The action name (a behavior's actions are namespaced, `pager.NEXT`) */
35
+ type: string
36
+ /** Its data, as the model got it (a DOM event for a DOM intent stream) */
37
+ data: any
38
+ /** The component's name */
39
+ component: string
40
+ /** The instance's id */
41
+ instance: string
42
+ /** The parent instance's id (null: a root) */
43
+ parent: string | null
44
+ /** Sinks that produced a value for it (live: STATE fills in when its reducer runs) */
45
+ sinks: string[]
46
+ cause: DevtoolsActionCause
47
+ /** ms since the session started */
48
+ at: number
49
+ /** The instance's state before / after the action's STATE reducer, when it changed the state */
50
+ before?: any
51
+ after?: any
52
+ /** A reply action (or RESOURCE): the source that delivered it; 'fetch' when it is makeFetchDriver's */
53
+ replySink?: string
54
+ replyKind?: 'fetch' | 'other'
55
+ /** An intent action whose data is the instance's own state (an intent over STATE.stream) */
56
+ echo?: true
57
+ }
58
+
59
+ export interface ActionFilter {
60
+ component?: string
61
+ instance?: string | number
62
+ type?: string | RegExp
63
+ cause?: DevtoolsActionCause | DevtoolsActionCause[]
64
+ }
65
+
66
+ /** One instance's recorded session, as plain data (what copyAsTest() writes the test from) */
67
+ export interface SessionRecording {
68
+ version: 1
69
+ component: string
70
+ instance: string
71
+ /** The state when the session started for this instance */
72
+ initialState?: any
73
+ /** The component's own initialState (renderComponent's default) */
74
+ definitionInitialState?: any
75
+ finalState: any
76
+ /** The action names the component can be sent (model keys, behavior actions) */
77
+ actionNames?: string[]
78
+ /** Source names beyond DOM / EVENTS / STATE / LOG / CHILD / PARENT / READY */
79
+ drivers: string[]
80
+ /** Those of `drivers` renderComponent fakes (makeFetchDriver sources) */
81
+ fakeable: string[]
82
+ /** The instance's own actions, in order */
83
+ actions: Array<Pick<DevtoolsAction, 'type' | 'data' | 'cause' | 'sinks' | 'at' | 'replySink' | 'replyKind' | 'echo'>>
84
+ /** State changes in descendant instances a replay at this instance can't reproduce */
85
+ foreign: Array<{ type: string; component: string; instance: string; cause: DevtoolsActionCause }>
86
+ truncated?: boolean
87
+ }
88
+
89
+ export interface CopyAsTestOptions {
90
+ /** The import line(s) for the component (default: `import <Name> from './<Name>.js'`) */
91
+ componentImport?: string
92
+ /** The component's identifier in the test (default: its recorded name) */
93
+ componentName?: string
94
+ /** More import lines (drivers, helpers) */
95
+ imports?: string[]
96
+ /** Drivers for renderComponent, as source code by sink name: { DND: 'mockDragDriver().driver' } */
97
+ drivers?: Record<string, string>
98
+ /** More renderComponent options, as source code: 'strict: true' */
99
+ renderOptions?: string
100
+ /** The test's name */
101
+ testName?: string
102
+ /** Adds a `// @vitest-environment <env>` first line */
103
+ environment?: string
104
+ }
105
+
106
+ export interface CopyAsTestResult {
107
+ /** The test file */
108
+ code: string
109
+ /** true when the test asserts the final state */
110
+ complete: boolean
111
+ /** What was left out and why (also comments in the code) */
112
+ warnings: string[]
113
+ /** Actions written as simulateAction / t.respond / t.fail */
114
+ replayed: number
115
+ }
116
+
117
+ /**
118
+ * What a session is taken from: undefined (the newest root), an instance id, a component, run()'s
119
+ * result, or a SessionRecording from getSession()
120
+ */
121
+ export type SessionTarget = undefined | string | number | ((...args: any[]) => any) | { sources: any } | SessionRecording
122
+
123
+ /** Start recording (done on import in a browser; idempotent). Returns a function that stops it. */
124
+ export declare function recordActions(): () => void
125
+ /** true while the action log records */
126
+ export declare function isRecording(): boolean
127
+ /** Start a new session: forget the recorded actions */
128
+ export declare function clearActions(): void
129
+ /** The session's actions, oldest first, optionally filtered */
130
+ export declare function getActions(filter?: ActionFilter): DevtoolsAction[]
131
+ /** Subscribe to the log; returns unsubscribe */
132
+ export declare function onAction(fn: (action: DevtoolsAction | null, kind: 'add' | 'update' | 'reset') => void): () => void
133
+ /** One instance's session as plain data */
134
+ export declare function getSession(target?: Exclude<SessionTarget, SessionRecording>): SessionRecording
135
+ /** "Copy as test": a Vitest + renderComponent test that replays the session */
136
+ export declare function copyAsTest(target?: SessionTarget, options?: CopyAsTestOptions): string
137
+ /** "Copy as test", with what it left out */
138
+ export declare function copyAsTestResult(target?: SessionTarget, options?: CopyAsTestOptions): CopyAsTestResult
139
+
140
+ export interface ReduxDevtoolsOptions {
141
+ /** The instance name in the extension (default 'Sygnal') */
142
+ name?: string
143
+ /** Which actions to send (default: all but 'built-in' ones) */
144
+ filter?: (action: DevtoolsAction) => boolean
145
+ }
146
+ /**
147
+ * Send the recorded actions and the root's state to the Redux DevTools extension
148
+ * (window.__REDUX_DEVTOOLS_EXTENSION__); jump to state / action replaces the root's state.
149
+ * Returns a disconnect function (a no-op without the extension).
150
+ */
151
+ export declare function connectReduxDevtools(target?: Exclude<SessionTarget, SessionRecording>, options?: ReduxDevtoolsOptions): () => void
@@ -0,0 +1,34 @@
1
+ // 'sygnal/devtools' (D77): the DevTools bridge for the browser extension, a dev-only
2
+ // entry. Importing it installs window.__SYGNAL_DEVTOOLS__ (run() did this before 6.0),
3
+ // so import it before run(): components created earlier are not in the tree.
4
+ // sygnal/vite injects it in dev (`vite`, not `vite build`); opt out with
5
+ // sygnal({ devtools: false }). Production builds never contain it.
6
+ // PLAN-4 3-E (GS-10): it also records the action log (in a browser on import; elsewhere
7
+ // after recordActions()), and turns a session into a renderComponent test (copyAsTest).
8
+ import {installDevTools} from './extra/devtools'
9
+ import {getSession, isRecording} from './extra/devtoolsActions'
10
+ import type {SessionRecording} from './extra/devtoolsActions'
11
+ import {sessionToTest} from './extra/copyAsTest'
12
+ import type {CopyAsTestOptions, CopyAsTestResult} from './extra/copyAsTest'
13
+
14
+ installDevTools()
15
+
16
+ export {getDevTools, installDevTools} from './extra/devtools'
17
+ export {recordActions, clearActions, getActions, onAction, getSession, isRecording} from './extra/devtoolsActions'
18
+ export {connectReduxDevtools} from './extra/reduxDevtools'
19
+
20
+ const isSession = (x: any): x is SessionRecording => !!x && typeof x == 'object' && x.version === 1 && Array.isArray(x.actions)
21
+
22
+ /**
23
+ * "Copy as test", with what was left out: the session of `target` (undefined: the newest root;
24
+ * an instance id; a component; run()'s result; or a SessionRecording from getSession()).
25
+ */
26
+ export function copyAsTestResult(target?: any, options?: CopyAsTestOptions): CopyAsTestResult {
27
+ if (!isSession(target) && !isRecording()) throw new Error('[Sygnal DevTools] copyAsTest: nothing was recorded: import sygnal/devtools before run() (in a browser), or call recordActions() first')
28
+ return sessionToTest(isSession(target) ? target : getSession(target), options)
29
+ }
30
+
31
+ /** "Copy as test": a Vitest + renderComponent test replaying the recorded session (see copyAsTestResult). */
32
+ export function copyAsTest(target?: any, options?: CopyAsTestOptions): string {
33
+ return copyAsTestResult(target, options).code
34
+ }
@@ -0,0 +1,59 @@
1
+ // 'sygnal/element' (PLAN-4 GS-13, D127): publish a Sygnal component as a custom element.
2
+ // Self-contained (like the other entries' types).
3
+
4
+ export type ElementPropType =
5
+ | StringConstructor
6
+ | NumberConstructor
7
+ | BooleanConstructor
8
+ | ObjectConstructor
9
+ | ArrayConstructor
10
+
11
+ /** The property type a prop declaration gives (attributes are parsed to it). */
12
+ export type ElementPropValue<T> =
13
+ T extends BooleanConstructor ? boolean :
14
+ T extends NumberConstructor ? number | undefined :
15
+ T extends StringConstructor ? string | undefined :
16
+ T extends ArrayConstructor ? any[] | undefined :
17
+ T extends ObjectConstructor ? Record<string, any> | undefined :
18
+ never
19
+
20
+ /** The element's prop properties for a `props` option (an array means all String). */
21
+ export type ElementProps<P> =
22
+ P extends readonly (infer K extends string)[] ? { [N in K]: string | undefined } :
23
+ P extends Record<string, ElementPropType> ? { -readonly [N in keyof P]: ElementPropValue<P[N]> } :
24
+ {}
25
+
26
+ export interface ElementOptions<P extends readonly string[] | Record<string, ElementPropType> = readonly string[] | Record<string, ElementPropType>> {
27
+ /**
28
+ * Properties fed into state: camelCase names; each also has a kebab-case attribute
29
+ * (`dueDate` ↔ `due-date`). String/Number attributes are parsed, Boolean is presence,
30
+ * Object/Array are JSON. An array of names means all String.
31
+ */
32
+ props?: P
33
+ /**
34
+ * Sink name → CustomEvent name, e.g. `{ PARENT: 'task-picked' }`. The event bubbles and is
35
+ * composed; `detail` is the sink's value. Use lowercase names (React 19 listens with
36
+ * `on<exact-name>`, e.g. `ontask-picked`). DOM, STATE and EVENTS can't be used.
37
+ */
38
+ events?: Record<string, string>
39
+ /** Render into a shadow root (true = 'open') */
40
+ shadow?: boolean | 'open' | 'closed'
41
+ /** Adopted into the shadow root, shared by every instance (ignored without shadow) */
42
+ styles?: string | CSSStyleSheet | Array<string | CSSStyleSheet>
43
+ }
44
+
45
+ export interface SygnalElementConstructor<P = {}> {
46
+ new (): HTMLElement & ElementProps<P>
47
+ prototype: HTMLElement & ElementProps<P>
48
+ readonly observedAttributes: string[]
49
+ }
50
+
51
+ /**
52
+ * Define `tag` as a custom element that runs `Component` (a plain function component) as
53
+ * its own app. Define it before a host framework renders the tag, so props arrive as
54
+ * properties. Calling it again for the same tag hot-swaps the component in every live
55
+ * element, keeping each one's state (HMR), and returns the same constructor.
56
+ */
57
+ export declare function defineElement<
58
+ const P extends readonly string[] | Record<string, ElementPropType> = {}
59
+ >(tag: string, Component: (...args: any[]) => any, options?: ElementOptions<P>): SygnalElementConstructor<P>
package/src/element.ts ADDED
@@ -0,0 +1,235 @@
1
+ // 'sygnal/element' (PLAN-4 GS-13, D127): publish a Sygnal component as a custom element.
2
+ //
3
+ // defineElement('task-board', TaskBoard, {
4
+ // props: { title: String, tasks: Array, readonly: Boolean }, // or ['a', 'b'] (all String)
5
+ // events: { PARENT: 'task-picked' }, // sink name → CustomEvent name
6
+ // shadow: true, // true | 'open' | 'closed'
7
+ // styles: '.title { color: green }', // string | CSSStyleSheet | array
8
+ // })
9
+ //
10
+ // Each connected element is its own app: run(Component) mounted on a div inside the element
11
+ // (or its shadow root). Props are in state from the first render (the component's
12
+ // initialState, overlaid with the props); later property/attribute changes are merged into
13
+ // state. Sinks listed in `events` are dispatched as CustomEvents (bubbles + composed; the
14
+ // PARENT wrapper is unwrapped to its value). Removing the element disposes the app; a move
15
+ // (disconnect + connect in the same task) keeps it running. Calling defineElement again for
16
+ // the same tag (HMR) swaps the component in every live element, keeping each one's state.
17
+ //
18
+ // Separate entry: rollup rewrites './index' to the external 'sygnal' (0 B in the core bundle).
19
+ import {run} from './index';
20
+ import {owned} from './extra/owned';
21
+
22
+ export type ElementPropType =
23
+ | StringConstructor
24
+ | NumberConstructor
25
+ | BooleanConstructor
26
+ | ObjectConstructor
27
+ | ArrayConstructor;
28
+
29
+ export interface ElementOptions {
30
+ /** Properties fed into state (camelCase; the attribute is kebab-case). An array means all String. */
31
+ props?: readonly string[] | Record<string, ElementPropType>;
32
+ /** Sink name → CustomEvent name, e.g. { PARENT: 'task-picked' } */
33
+ events?: Record<string, string>;
34
+ /** Render into a shadow root (true = 'open') */
35
+ shadow?: boolean | 'open' | 'closed';
36
+ /** Adopted into the shadow root, shared by every instance (ignored without shadow) */
37
+ styles?: string | CSSStyleSheet | Array<string | CSSStyleSheet>;
38
+ }
39
+
40
+ // Sinks that are the app's own drivers, never events
41
+ const RESERVED_SINKS = ['DOM', 'STATE', 'EVENTS'];
42
+
43
+ // Hot-swap functions of the tags this module defined
44
+ const swaps = new WeakMap<CustomElementConstructor, (Component: any) => void>();
45
+
46
+ const kebab = (name: string) => name.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase());
47
+
48
+ function parseAttribute(type: ElementPropType, value: string | null): any {
49
+ if (type === Boolean) return value !== null;
50
+ if (value === null) return undefined;
51
+ if (type === Number) return Number(value);
52
+ if (type === String) return value;
53
+ return JSON.parse(value); // Object, Array (throws on bad JSON)
54
+ }
55
+
56
+ function toSheet(style: string | CSSStyleSheet): CSSStyleSheet {
57
+ if (typeof style !== 'string') return style;
58
+ const sheet = new CSSStyleSheet();
59
+ sheet.replaceSync(style);
60
+ return sheet;
61
+ }
62
+
63
+ const devMode = () => (globalThis as any).__SYGNAL_DEV__ === true;
64
+
65
+ export function defineElement(tag: string, Component: any, options: ElementOptions = {}): CustomElementConstructor {
66
+ const prior = customElements.get(tag);
67
+ const swap = prior && swaps.get(prior);
68
+ if (swap) {
69
+ swap(Component); // HMR: the defining module ran again
70
+ return prior!;
71
+ }
72
+ if (typeof Component !== 'function') {
73
+ throw new TypeError(`defineElement('${tag}'): pass a function component`);
74
+ }
75
+
76
+ const types: Record<string, ElementPropType> = Array.isArray(options.props)
77
+ ? Object.fromEntries(options.props.map((name: string) => [name, String]))
78
+ : {...(options.props as Record<string, ElementPropType> | undefined)};
79
+ const propOfAttribute: Record<string, string> = Object.fromEntries(Object.keys(types).map((name) => [kebab(name), name]));
80
+ const events = {...options.events};
81
+ for (const sink of RESERVED_SINKS) {
82
+ if (sink in events) throw new TypeError(`defineElement('${tag}'): '${sink}' is a driver of the element's app, not an event sink`);
83
+ }
84
+ const shadowMode = options.shadow ? (options.shadow === 'closed' ? 'closed' : 'open') : undefined;
85
+ const sheets = shadowMode ? ([] as Array<string | CSSStyleSheet>).concat(options.styles || []).map(toSheet) : [];
86
+ // Props that hide a member of HTMLElement (e.g. title, hidden): SYG644 once per tag (D131)
87
+ const shadowedMembers = Object.keys(types).filter((name) => name in HTMLElement.prototype);
88
+ let warned = false;
89
+
90
+ const live = new Set<SygnalElement>();
91
+ let instances = 0;
92
+
93
+ class SygnalElement extends HTMLElement {
94
+ static observedAttributes = Object.keys(propOfAttribute);
95
+
96
+ #props: Record<string, any> = {};
97
+ #app: any = undefined;
98
+ #root: HTMLElement | ShadowRoot = this;
99
+ #uid = `${tag}-${++instances}`;
100
+
101
+ static {
102
+ for (const name of Object.keys(types)) {
103
+ Object.defineProperty(this.prototype, name, {
104
+ configurable: true,
105
+ enumerable: true,
106
+ get(this: SygnalElement) {
107
+ return this.#props[name];
108
+ },
109
+ set(this: SygnalElement, value: any) {
110
+ this.#setProp(name, value);
111
+ },
112
+ });
113
+ }
114
+ swaps.set(this, (Next: any) => {
115
+ Component = Next;
116
+ live.forEach((element) => element.#swap());
117
+ });
118
+ }
119
+
120
+ constructor() {
121
+ super();
122
+ if (shadowMode) {
123
+ const root = this.attachShadow({mode: shadowMode});
124
+ root.adoptedStyleSheets = sheets;
125
+ this.#root = root;
126
+ }
127
+ // Properties set before the tag was defined are own properties that hide the
128
+ // accessors: move them onto the accessors
129
+ for (const name of Object.keys(types)) {
130
+ if (Object.prototype.hasOwnProperty.call(this, name)) {
131
+ const value = (this as any)[name];
132
+ delete (this as any)[name];
133
+ this.#setProp(name, value);
134
+ }
135
+ }
136
+ }
137
+
138
+ attributeChangedCallback(attribute: string, _old: string | null, value: string | null) {
139
+ const name = propOfAttribute[attribute];
140
+ try {
141
+ this.#setProp(name, parseAttribute(types[name], value));
142
+ } catch (_) {
143
+ // bad JSON in an Object/Array attribute: keep the previous value
144
+ }
145
+ }
146
+
147
+ connectedCallback() {
148
+ if (this.#app) return; // moved, not removed: keep running
149
+ // (G-216: a hot swap elsewhere on the page is that app's own, so no need to wait it out)
150
+ if (shadowedMembers.length && !warned) {
151
+ // D131: SYG644 through the diagnostics core when diagnostics are on; else a console
152
+ // warning in dev
153
+ const message = `props hide the HTMLElement members of the same name (${shadowedMembers.join(', ')})`;
154
+ const fix = 'Rename them to keep the native behaviour';
155
+ let reported: any;
156
+ try {
157
+ reported = (globalThis as any).__SYGNAL_DIAGNOSTICS__?.report('SYG644', {
158
+ severity: 'warn', component: `<${tag}>`, message, fix, data: {tag, members: shadowedMembers},
159
+ });
160
+ } catch (e) {
161
+ reported = true; // mode 'error': thrown after the element starts
162
+ queueMicrotask(() => { throw e; });
163
+ }
164
+ if (reported || devMode()) warned = true;
165
+ if (!reported && devMode()) console.warn(`[sygnal/element] <${tag}>: ${message}. ${fix}.`);
166
+ }
167
+ const mount = document.createElement('div');
168
+ this.#root.appendChild(mount);
169
+ this.#app = run(this.#component(), this.#eventDrivers(), {mountPoint: mount as any, uid: this.#uid});
170
+ this.#releaseDevtools();
171
+ live.add(this);
172
+ }
173
+
174
+ disconnectedCallback() {
175
+ // Deferred: a move is a disconnect + connect in the same task
176
+ queueMicrotask(() => {
177
+ if (this.isConnected || !this.#app) return;
178
+ this.#app.dispose();
179
+ this.#app = undefined;
180
+ live.delete(this);
181
+ this.#root.replaceChildren();
182
+ });
183
+ }
184
+
185
+ // Hot swap (defineElement again for this tag): the new component, this element's state
186
+ #swap() {
187
+ if (!this.#app) return;
188
+ // (the state through the runtime API, 04 §3.9)
189
+ this.#app.hmr(this.#component(), (this.#app as any).__runtime.getState());
190
+ this.#releaseDevtools();
191
+ }
192
+
193
+ #setProp(name: string, value: any) {
194
+ this.#props[name] = value;
195
+ const set = (state: any) => ({...state, [name]: value});
196
+ (this.#app as any)?.__runtime?.setState('root', set);
197
+ }
198
+
199
+ // A per-instance copy of the component whose initialState includes the current props
200
+ // (owned: the dev statics freeze leaves the host's values alone, G-275)
201
+ #component() {
202
+ const wrapped = Object.assign((...args: any[]) => Component(...args), Component, {
203
+ initialState: owned({...Component.initialState, ...this.#props}),
204
+ });
205
+ return Object.defineProperty(wrapped, 'name', {value: Component.name || tag});
206
+ }
207
+
208
+ #eventDrivers() {
209
+ return Object.fromEntries(
210
+ Object.entries(events).map(([sink, eventName]) => [
211
+ sink,
212
+ (sink$: any) => {
213
+ sink$.addListener({
214
+ next: (value: any) => {
215
+ // PARENT values arrive wrapped as { name, component, value }
216
+ const detail = sink === 'PARENT' ? value?.value : value;
217
+ this.dispatchEvent(new CustomEvent(eventName, {detail, bubbles: true, composed: true}));
218
+ },
219
+ });
220
+ },
221
+ ])
222
+ );
223
+ }
224
+
225
+ // run() registers the first live app for DevTools time travel (G-212). An element gives
226
+ // the slot back, so a host app that starts later gets it
227
+ #releaseDevtools() {
228
+ const w = window as any;
229
+ if (w.__SYGNAL_DEVTOOLS_APP__ === this.#app || w.__SYGNAL_DEVTOOLS_APP__?.sinks === this.#app.sinks) w.__SYGNAL_DEVTOOLS_APP__ = undefined;
230
+ }
231
+ }
232
+
233
+ customElements.define(tag, SygnalElement);
234
+ return SygnalElement;
235
+ }
@@ -0,0 +1,9 @@
1
+ /*
2
+ * PLAN-3: the retry delay shared by makeSocketDriver's `reconnect` and makeFetchDriver's `retry`.
3
+ * Delay of retry n (from 0): `min(maxDelayMs, delayMs * 2^n)`, varied by ±`jitter` (a fraction;
4
+ * `false` = none). Defaults `{ delayMs: 500, maxDelayMs: 10000, jitter: 0.2 }`.
5
+ */
6
+ export const backoff = (p: {delayMs?: number; maxDelayMs?: number; jitter?: number | boolean}, n: number) => {
7
+ const j = p.jitter === false ? 0 : p.jitter == null || p.jitter === true ? 0.2 : +p.jitter;
8
+ return Math.min(p.maxDelayMs ?? 10000, (p.delayMs ?? 500) * 2 ** n) * (1 + j * (2 * Math.random() - 1));
9
+ };