sygnal 5.3.7 → 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.
- package/CHANGELOG.md +646 -0
- package/README.md +77 -47
- package/dist/astro/client.cjs.js +33 -8114
- package/dist/astro/client.cjs.js.map +1 -0
- package/dist/astro/client.mjs +33 -8114
- package/dist/astro/client.mjs.map +1 -0
- package/dist/astro/index.cjs.js +1039 -9
- package/dist/astro/index.cjs.js.map +1 -0
- package/dist/astro/index.mjs +1038 -9
- package/dist/astro/index.mjs.map +1 -0
- package/dist/astro/server.cjs.js +3400 -166
- package/dist/astro/server.cjs.js.map +1 -0
- package/dist/astro/server.mjs +3400 -166
- package/dist/astro/server.mjs.map +1 -0
- package/dist/devtools.cjs.js +1431 -0
- package/dist/devtools.cjs.js.map +1 -0
- package/dist/devtools.esm.js +1419 -0
- package/dist/devtools.esm.js.map +1 -0
- package/dist/diagnostics.cjs.js +4033 -0
- package/dist/diagnostics.cjs.js.map +1 -0
- package/dist/diagnostics.esm.js +4003 -0
- package/dist/diagnostics.esm.js.map +1 -0
- package/dist/element.cjs.js +230 -0
- package/dist/element.cjs.js.map +1 -0
- package/dist/element.esm.js +228 -0
- package/dist/element.esm.js.map +1 -0
- package/dist/guide/accessibility.md +257 -0
- package/dist/guide/adapters.md +221 -0
- package/dist/guide/behaviors.md +312 -0
- package/dist/guide/browser-sources.md +203 -0
- package/dist/guide/drag-and-drop.md +342 -0
- package/dist/guide/element-commands.md +306 -0
- package/dist/guide/error-boundaries.md +169 -0
- package/dist/guide/forms-reference.md +381 -0
- package/dist/guide/forms.md +250 -0
- package/dist/guide/http.md +299 -0
- package/dist/guide/inputs.md +173 -0
- package/dist/guide/persistence.md +245 -0
- package/dist/guide/recipes/carousel.md +155 -0
- package/dist/guide/recipes/charts.md +177 -0
- package/dist/guide/recipes/code-editor.md +135 -0
- package/dist/guide/recipes/data-grid.md +155 -0
- package/dist/guide/recipes/data-table.md +196 -0
- package/dist/guide/recipes/i18n.md +222 -0
- package/dist/guide/recipes/icons.md +115 -0
- package/dist/guide/recipes/overview.md +33 -0
- package/dist/guide/recipes/rich-text.md +154 -0
- package/dist/guide/resources.md +471 -0
- package/dist/guide/ssr.md +255 -0
- package/dist/guide/timers.md +178 -0
- package/dist/guide/ui/accordion.md +93 -0
- package/dist/guide/ui/combobox.md +144 -0
- package/dist/guide/ui/dialog.md +124 -0
- package/dist/guide/ui/disclosure.md +60 -0
- package/dist/guide/ui/menu.md +125 -0
- package/dist/guide/ui/overview.md +166 -0
- package/dist/guide/ui/popover.md +101 -0
- package/dist/guide/ui/select.md +114 -0
- package/dist/guide/ui/tabs.md +161 -0
- package/dist/guide/ui/toaster.md +152 -0
- package/dist/guide/ui/tooltip.md +103 -0
- package/dist/guide/undo.md +178 -0
- package/dist/guide/virtual-collections.md +183 -0
- package/dist/guide/web-components.md +179 -0
- package/dist/guide/widgets.md +217 -0
- package/dist/index.cjs.js +15283 -6279
- package/dist/index.cjs.js.map +1 -0
- package/dist/index.d.ts +3739 -370
- package/dist/index.esm.js +15230 -6274
- package/dist/index.esm.js.map +1 -0
- package/dist/jsx-dev-runtime.cjs.js +27 -329
- package/dist/jsx-dev-runtime.cjs.js.map +1 -0
- package/dist/jsx-dev-runtime.esm.js +20 -322
- package/dist/jsx-dev-runtime.esm.js.map +1 -0
- package/dist/jsx-runtime.cjs.js +27 -329
- package/dist/jsx-runtime.cjs.js.map +1 -0
- package/dist/jsx-runtime.esm.js +20 -322
- package/dist/jsx-runtime.esm.js.map +1 -0
- package/dist/jsx.cjs.js +11 -309
- package/dist/jsx.cjs.js.map +1 -0
- package/dist/jsx.esm.js +9 -310
- package/dist/jsx.esm.js.map +1 -0
- package/dist/react.cjs.js +117 -0
- package/dist/react.cjs.js.map +1 -0
- package/dist/react.esm.js +115 -0
- package/dist/react.esm.js.map +1 -0
- package/dist/shims/globalthis.cjs +20 -0
- package/dist/sygnal.min.js +2 -1
- package/dist/sygnal.min.js.map +1 -0
- package/dist/ui-combobox.cjs.js +169 -0
- package/dist/ui-combobox.cjs.js.map +1 -0
- package/dist/ui-combobox.esm.js +148 -0
- package/dist/ui-combobox.esm.js.map +1 -0
- package/dist/ui-menu.cjs.js +87 -0
- package/dist/ui-menu.cjs.js.map +1 -0
- package/dist/ui-menu.esm.js +66 -0
- package/dist/ui-menu.esm.js.map +1 -0
- package/dist/ui-select.cjs.js +104 -0
- package/dist/ui-select.cjs.js.map +1 -0
- package/dist/ui-select.esm.js +83 -0
- package/dist/ui-select.esm.js.map +1 -0
- package/dist/ui.cjs.js +738 -0
- package/dist/ui.cjs.js.map +1 -0
- package/dist/ui.esm.js +727 -0
- package/dist/ui.esm.js.map +1 -0
- package/dist/vike/ClientOnly.cjs.js +5 -14
- package/dist/vike/ClientOnly.cjs.js.map +1 -0
- package/dist/vike/ClientOnly.mjs +5 -14
- package/dist/vike/ClientOnly.mjs.map +1 -0
- package/dist/vike/{+config.js → config/+config.js} +10 -1
- package/dist/vike/config/+config.js.map +1 -0
- package/dist/vike/config/package.json +6 -0
- package/dist/vike/onRenderClient.cjs.js +166 -95
- package/dist/vike/onRenderClient.cjs.js.map +1 -0
- package/dist/vike/onRenderClient.mjs +166 -95
- package/dist/vike/onRenderClient.mjs.map +1 -0
- package/dist/vike/onRenderHtml.cjs.js +60 -24
- package/dist/vike/onRenderHtml.cjs.js.map +1 -0
- package/dist/vike/onRenderHtml.mjs +61 -25
- package/dist/vike/onRenderHtml.mjs.map +1 -0
- package/dist/vite/plugin.cjs.js +926 -65
- package/dist/vite/plugin.cjs.js.map +1 -0
- package/dist/vite/plugin.mjs +925 -65
- package/dist/vite/plugin.mjs.map +1 -0
- package/dist/zag.cjs.js +408 -0
- package/dist/zag.cjs.js.map +1 -0
- package/dist/zag.esm.js +405 -0
- package/dist/zag.esm.js.map +1 -0
- package/llms.txt +313 -0
- package/package.json +109 -16
- package/src/astro/client.ts +40 -18
- package/src/astro/index.d.ts +48 -1
- package/src/astro/index.ts +106 -9
- package/src/astro/server.ts +11 -3
- package/src/collection.ts +6 -90
- package/src/core/actions.ts +134 -0
- package/src/core/cell.ts +202 -0
- package/src/core/debug.ts +23 -0
- package/src/core/define.ts +242 -0
- package/src/core/hooks.ts +314 -0
- package/src/core/hosts/collection.ts +325 -0
- package/src/core/hosts/switchable.ts +146 -0
- package/src/core/instance.ts +592 -0
- package/src/core/markers/clientonly.ts +13 -0
- package/src/core/markers/lazy.ts +40 -0
- package/src/core/markers/portal.ts +96 -0
- package/src/core/markers/suspense.ts +41 -0
- package/src/core/markers/transition.ts +70 -0
- package/src/core/registry.ts +25 -0
- package/src/core/runtime.ts +573 -0
- package/src/core/statics.ts +104 -0
- package/src/core/teardown.ts +60 -0
- package/src/core/view.ts +37 -0
- package/src/cycle/dom/DocumentDOMSource.ts +13 -8
- package/src/cycle/dom/ElementFinder.ts +7 -9
- package/src/cycle/dom/EventDelegator.ts +174 -223
- package/src/cycle/dom/IsolateModule.ts +65 -35
- package/src/cycle/dom/MainDOMSource.ts +13 -2
- package/src/cycle/dom/PriorityQueue.ts +4 -2
- package/src/cycle/dom/SymbolTree.ts +18 -47
- package/src/cycle/dom/classNameModule.ts +76 -0
- package/src/cycle/dom/controlledInputModule.ts +50 -0
- package/src/cycle/dom/enrichEventStream.ts +29 -45
- package/src/cycle/dom/fragment.ts +7 -0
- package/src/cycle/dom/isolate.ts +30 -27
- package/src/cycle/dom/makeDOMDriver.ts +115 -57
- package/src/cycle/dom/mockDOMSource.ts +83 -9
- package/src/cycle/dom/modules.ts +21 -4
- package/src/cycle/dom/propsModule.ts +65 -0
- package/src/cycle/dom/selectModule.ts +22 -17
- package/src/cycle/dom/snabbdom.ts +1 -6
- package/src/cycle/dom/thunk.ts +3 -0
- package/src/cycle/dom/utils.ts +98 -0
- package/src/cycle/dom/viewTransition.ts +43 -0
- package/src/cycle/state/StateSource.ts +20 -4
- package/src/cycle/state/objIsEqual.ts +50 -0
- package/src/cycle/state/types.ts +0 -10
- package/src/defineComponent.ts +34 -0
- package/src/devtools.d.ts +151 -0
- package/src/devtools.ts +34 -0
- package/src/element.d.ts +59 -0
- package/src/element.ts +235 -0
- package/src/extra/backoff.ts +9 -0
- package/src/extra/behaviors.ts +169 -0
- package/src/extra/browserSignals.ts +23 -0
- package/src/extra/browserSources.ts +326 -0
- package/src/extra/command.ts +3 -3
- package/src/extra/controls.ts +47 -0
- package/src/extra/copyAsTest.ts +286 -0
- package/src/extra/devtools.ts +109 -31
- package/src/extra/devtoolsActions.ts +379 -0
- package/src/extra/devtoolsHook.ts +9 -0
- package/src/extra/devtoolsNext.ts +83 -0
- package/src/extra/diagnostics/checks/actionLog.ts +108 -0
- package/src/extra/diagnostics/checks/behaviors.ts +49 -0
- package/src/extra/diagnostics/checks/browserSources.ts +96 -0
- package/src/extra/diagnostics/checks/collections.ts +67 -0
- package/src/extra/diagnostics/checks/controls.ts +148 -0
- package/src/extra/diagnostics/checks/dataset.ts +57 -0
- package/src/extra/diagnostics/checks/dom.ts +250 -0
- package/src/extra/diagnostics/checks/elementCommands.ts +175 -0
- package/src/extra/diagnostics/checks/events.ts +83 -0
- package/src/extra/diagnostics/checks/fetch.ts +135 -0
- package/src/extra/diagnostics/checks/forms.ts +163 -0
- package/src/extra/diagnostics/checks/index.ts +202 -0
- package/src/extra/diagnostics/checks/inspect.ts +427 -0
- package/src/extra/diagnostics/checks/next.ts +417 -0
- package/src/extra/diagnostics/checks/persist.ts +54 -0
- package/src/extra/diagnostics/checks/props.ts +56 -0
- package/src/extra/diagnostics/checks/public.d.ts +302 -0
- package/src/extra/diagnostics/checks/replies.ts +128 -0
- package/src/extra/diagnostics/checks/router.ts +100 -0
- package/src/extra/diagnostics/checks/rxjsHints.ts +88 -0
- package/src/extra/diagnostics/checks/shared.ts +203 -0
- package/src/extra/diagnostics/checks/shorthand.ts +115 -0
- package/src/extra/diagnostics/checks/sortable.ts +129 -0
- package/src/extra/diagnostics/checks/state.ts +148 -0
- package/src/extra/diagnostics/checks/statics.ts +48 -0
- package/src/extra/diagnostics/checks/strict.ts +55 -0
- package/src/extra/diagnostics/checks/timers.ts +80 -0
- package/src/extra/diagnostics/checks/viewTransitions.ts +47 -0
- package/src/extra/diagnostics/checks/virtual.ts +73 -0
- package/src/extra/diagnostics/checks/widgets.ts +109 -0
- package/src/extra/diagnostics/checks/wiring.ts +121 -0
- package/src/extra/diagnostics/codes.ts +477 -0
- package/src/extra/diagnostics/index.ts +353 -0
- package/src/extra/diagnostics/legacy.ts +76 -0
- package/src/extra/driverFactories.ts +172 -60
- package/src/extra/elementCommands.ts +53 -0
- package/src/extra/eventDriver.ts +4 -0
- package/src/extra/fetchDriver.ts +519 -0
- package/src/extra/flatten.ts +75 -0
- package/src/extra/focusWithin.ts +27 -0
- package/src/extra/form.ts +268 -0
- package/src/extra/formHelpers.ts +139 -0
- package/src/extra/head.ts +106 -0
- package/src/extra/hmr.ts +2 -3
- package/src/extra/owned.ts +24 -0
- package/src/extra/pager.ts +50 -0
- package/src/extra/persist.ts +133 -0
- package/src/extra/pwa.ts +1 -1
- package/src/extra/queryCache.ts +107 -0
- package/src/extra/reducers.ts +13 -6
- package/src/extra/reduxDevtools.ts +92 -0
- package/src/extra/replies.ts +54 -0
- package/src/extra/router.ts +323 -0
- package/src/extra/run.ts +94 -210
- package/src/extra/selection.ts +76 -0
- package/src/extra/socketDriver.ts +265 -0
- package/src/extra/sortable.ts +377 -0
- package/src/extra/ssr.ts +427 -160
- package/src/extra/standardSchema.ts +27 -0
- package/src/extra/testing.ts +3158 -190
- package/src/extra/timers.ts +95 -0
- package/src/extra/undo.ts +261 -0
- package/src/extra/viewTransitions.ts +38 -0
- package/src/extra/virtual.ts +513 -0
- package/src/extra/widget.ts +201 -0
- package/src/extra/xstreamExtras.ts +269 -0
- package/src/index.d.ts +3012 -87
- package/src/index.ts +32 -10
- package/src/jsx-runtime.ts +15 -1
- package/src/jsx.ts +2 -1
- package/src/lazy.ts +50 -7
- package/src/portal.ts +3 -1
- package/src/pragma/index.ts +262 -134
- package/src/react-peers.d.ts +5 -0
- package/src/react.d.ts +43 -0
- package/src/react.ts +110 -0
- package/src/shared.ts +48 -0
- package/src/slot.ts +1 -1
- package/src/suspense.ts +3 -1
- package/src/switchable.ts +6 -119
- package/src/transition.ts +3 -1
- package/src/ui/accordion.ts +64 -0
- package/src/ui/dialog.ts +129 -0
- package/src/ui/disclosure.ts +35 -0
- package/src/ui/popover.ts +45 -0
- package/src/ui/shared.ts +94 -0
- package/src/ui/tabs.ts +82 -0
- package/src/ui/toaster.ts +198 -0
- package/src/ui/tooltip.ts +70 -0
- package/src/ui/zag/combobox.ts +115 -0
- package/src/ui/zag/menu.ts +40 -0
- package/src/ui/zag/select.ts +54 -0
- package/src/ui/zag/shared.ts +43 -0
- package/src/ui-combobox.d.ts +20 -0
- package/src/ui-combobox.ts +6 -0
- package/src/ui-menu.d.ts +30 -0
- package/src/ui-menu.ts +6 -0
- package/src/ui-select.d.ts +17 -0
- package/src/ui-select.ts +6 -0
- package/src/ui-zag-types.d.ts +42 -0
- package/src/ui.d.ts +208 -0
- package/src/ui.ts +15 -0
- package/src/vike/+config.ts +9 -1
- package/src/vike/ClientOnly.ts +1 -1
- package/src/vike/onRenderClient.ts +144 -96
- package/src/vike/onRenderHtml.ts +69 -26
- package/src/vike/types.ts +27 -4
- package/src/vite/globalthis-shim.ts +18 -0
- package/src/vite/globalthis.ts +56 -0
- package/src/vite/plugin.d.ts +114 -2
- package/src/vite/plugin.ts +920 -56
- package/src/zag.d.ts +64 -0
- package/src/zag.ts +238 -0
- package/dist/astro/astro/client.d.ts +0 -5
- package/dist/astro/astro/index.d.ts +0 -16
- package/dist/astro/astro/server.d.ts +0 -12
- package/dist/astro/client.d.ts +0 -5
- package/dist/astro/client.esm.js +0 -4665
- package/dist/astro/collection.d.ts +0 -12
- package/dist/astro/component.d.ts +0 -22
- package/dist/astro/cycle/dom/BodyDOMSource.d.ts +0 -10
- package/dist/astro/cycle/dom/DOMSource.d.ts +0 -11
- package/dist/astro/cycle/dom/DocumentDOMSource.d.ts +0 -12
- package/dist/astro/cycle/dom/ElementFinder.d.ts +0 -8
- package/dist/astro/cycle/dom/EventDelegator.d.ts +0 -34
- package/dist/astro/cycle/dom/IsolateModule.d.ts +0 -23
- package/dist/astro/cycle/dom/MainDOMSource.d.ts +0 -32
- package/dist/astro/cycle/dom/PriorityQueue.d.ts +0 -7
- package/dist/astro/cycle/dom/ScopeChecker.d.ts +0 -9
- package/dist/astro/cycle/dom/SymbolTree.d.ts +0 -9
- package/dist/astro/cycle/dom/VNodeWrapper.d.ts +0 -8
- package/dist/astro/cycle/dom/enrichEventStream.d.ts +0 -24
- package/dist/astro/cycle/dom/fromEvent.d.ts +0 -6
- package/dist/astro/cycle/dom/index.d.ts +0 -8
- package/dist/astro/cycle/dom/isolate.d.ts +0 -9
- package/dist/astro/cycle/dom/makeDOMDriver.d.ts +0 -11
- package/dist/astro/cycle/dom/mockDOMSource.d.ts +0 -17
- package/dist/astro/cycle/dom/modules.d.ts +0 -5
- package/dist/astro/cycle/dom/snabbdom.d.ts +0 -22
- package/dist/astro/cycle/dom/styleModule.d.ts +0 -13
- package/dist/astro/cycle/dom/thunk.d.ts +0 -11
- package/dist/astro/cycle/dom/utils.d.ts +0 -8
- package/dist/astro/cycle/isolate/index.d.ts +0 -42
- package/dist/astro/cycle/run/adapt.d.ts +0 -6
- package/dist/astro/cycle/run/index.d.ts +0 -36
- package/dist/astro/cycle/run/internals.d.ts +0 -8
- package/dist/astro/cycle/run/types.d.ts +0 -49
- package/dist/astro/cycle/state/Collection.d.ts +0 -29
- package/dist/astro/cycle/state/StateSource.d.ts +0 -20
- package/dist/astro/cycle/state/index.d.ts +0 -5
- package/dist/astro/cycle/state/pickCombine.d.ts +0 -3
- package/dist/astro/cycle/state/pickMerge.d.ts +0 -3
- package/dist/astro/cycle/state/types.d.ts +0 -18
- package/dist/astro/cycle/state/withState.d.ts +0 -17
- package/dist/astro/extra/classes.d.ts +0 -7
- package/dist/astro/extra/devtools.d.ts +0 -78
- package/dist/astro/extra/dragDriver.d.ts +0 -20
- package/dist/astro/extra/driverFactories.d.ts +0 -12
- package/dist/astro/extra/eventDriver.d.ts +0 -9
- package/dist/astro/extra/exactState.d.ts +0 -1
- package/dist/astro/extra/hmr.d.ts +0 -17
- package/dist/astro/extra/logDriver.d.ts +0 -2
- package/dist/astro/extra/processDrag.d.ts +0 -19
- package/dist/astro/extra/processForm.d.ts +0 -15
- package/dist/astro/extra/run.d.ts +0 -13
- package/dist/astro/extra/xstreamCompat.d.ts +0 -5
- package/dist/astro/index.d.ts +0 -19
- package/dist/astro/index.esm.js +0 -27
- package/dist/astro/jsx-dev-runtime.d.ts +0 -1
- package/dist/astro/jsx-runtime.d.ts +0 -4
- package/dist/astro/jsx.d.ts +0 -2
- package/dist/astro/pragma/fn.d.ts +0 -7
- package/dist/astro/pragma/index.d.ts +0 -7
- package/dist/astro/pragma/is.d.ts +0 -10
- package/dist/astro/server.d.ts +0 -12
- package/dist/astro/server.esm.js +0 -25
- package/dist/astro/switchable.d.ts +0 -7
- package/dist/collection.d.ts +0 -12
- package/dist/component.d.ts +0 -22
- package/dist/cycle/dom/BodyDOMSource.d.ts +0 -10
- package/dist/cycle/dom/DOMSource.d.ts +0 -11
- package/dist/cycle/dom/DocumentDOMSource.d.ts +0 -12
- package/dist/cycle/dom/ElementFinder.d.ts +0 -8
- package/dist/cycle/dom/EventDelegator.d.ts +0 -34
- package/dist/cycle/dom/IsolateModule.d.ts +0 -23
- package/dist/cycle/dom/MainDOMSource.d.ts +0 -32
- package/dist/cycle/dom/PriorityQueue.d.ts +0 -7
- package/dist/cycle/dom/ScopeChecker.d.ts +0 -9
- package/dist/cycle/dom/SymbolTree.d.ts +0 -9
- package/dist/cycle/dom/VNodeWrapper.d.ts +0 -8
- package/dist/cycle/dom/enrichEventStream.d.ts +0 -24
- package/dist/cycle/dom/fromEvent.d.ts +0 -6
- package/dist/cycle/dom/index.d.ts +0 -8
- package/dist/cycle/dom/isolate.d.ts +0 -9
- package/dist/cycle/dom/makeDOMDriver.d.ts +0 -11
- package/dist/cycle/dom/mockDOMSource.d.ts +0 -17
- package/dist/cycle/dom/modules.d.ts +0 -5
- package/dist/cycle/dom/snabbdom.d.ts +0 -22
- package/dist/cycle/dom/styleModule.d.ts +0 -13
- package/dist/cycle/dom/thunk.d.ts +0 -11
- package/dist/cycle/dom/utils.d.ts +0 -8
- package/dist/cycle/isolate/index.d.ts +0 -42
- package/dist/cycle/run/adapt.d.ts +0 -6
- package/dist/cycle/run/index.d.ts +0 -36
- package/dist/cycle/run/internals.d.ts +0 -8
- package/dist/cycle/run/types.d.ts +0 -49
- package/dist/cycle/state/Collection.d.ts +0 -29
- package/dist/cycle/state/StateSource.d.ts +0 -20
- package/dist/cycle/state/index.d.ts +0 -5
- package/dist/cycle/state/pickCombine.d.ts +0 -3
- package/dist/cycle/state/pickMerge.d.ts +0 -3
- package/dist/cycle/state/types.d.ts +0 -18
- package/dist/cycle/state/withState.d.ts +0 -17
- package/dist/extra/classes.d.ts +0 -7
- package/dist/extra/devtools.d.ts +0 -78
- package/dist/extra/dragDriver.d.ts +0 -20
- package/dist/extra/driverFactories.d.ts +0 -12
- package/dist/extra/eventDriver.d.ts +0 -9
- package/dist/extra/exactState.d.ts +0 -1
- package/dist/extra/hmr.d.ts +0 -17
- package/dist/extra/logDriver.d.ts +0 -2
- package/dist/extra/processDrag.d.ts +0 -19
- package/dist/extra/processForm.d.ts +0 -15
- package/dist/extra/run.d.ts +0 -13
- package/dist/extra/xstreamCompat.d.ts +0 -5
- package/dist/jsx-dev-runtime.d.ts +0 -1
- package/dist/jsx-dev-runtime.js +0 -224
- package/dist/jsx-runtime.d.ts +0 -4
- package/dist/jsx-runtime.js +0 -224
- package/dist/jsx.d.ts +0 -2
- package/dist/pragma/fn.d.ts +0 -7
- package/dist/pragma/index.d.ts +0 -7
- package/dist/pragma/is.d.ts +0 -10
- package/dist/switchable.d.ts +0 -7
- package/dist/vike/+config.cjs.js +0 -65
- package/src/component.ts +0 -2256
- package/src/cycle/isolate/index.ts +0 -196
- package/src/cycle/run/index.ts +0 -151
- package/src/cycle/run/internals.ts +0 -143
- package/src/cycle/state/Collection.ts +0 -174
- package/src/cycle/state/index.ts +0 -5
- package/src/cycle/state/pickCombine.ts +0 -167
- package/src/cycle/state/pickMerge.ts +0 -122
- package/src/cycle/state/withState.ts +0 -47
- package/src/pragma/fn.ts +0 -55
package/dist/index.d.ts
CHANGED
|
@@ -1,39 +1,542 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
import
|
|
5
|
-
import
|
|
1
|
+
import { Options } from 'snabbdom/build/init.js';
|
|
2
|
+
import { VNode, VNodeData } from 'snabbdom/build/vnode.js';
|
|
3
|
+
export { VNode, VNodeData } from 'snabbdom/build/vnode.js';
|
|
4
|
+
import { Module } from 'snabbdom/build/modules/module.js';
|
|
5
|
+
import xsDefault, { Stream, MemoryStream } from 'xstream';
|
|
6
|
+
export { MemoryStream, Stream } from 'xstream';
|
|
7
|
+
export { h } from 'snabbdom/build/h.js';
|
|
8
|
+
export { default as debounce } from 'xstream/extra/debounce.js';
|
|
9
|
+
export { default as throttle } from 'xstream/extra/throttle.js';
|
|
10
|
+
export { default as delay } from 'xstream/extra/delay.js';
|
|
11
|
+
export { default as dropRepeats } from 'xstream/extra/dropRepeats.js';
|
|
12
|
+
export { default as sampleCombine } from 'xstream/extra/sampleCombine.js';
|
|
13
|
+
export { default as flattenConcurrently } from 'xstream/extra/flattenConcurrently.js';
|
|
14
|
+
export { default as flattenSequentially } from 'xstream/extra/flattenSequentially.js';
|
|
15
|
+
export { default as concat } from 'xstream/extra/concat.js';
|
|
16
|
+
|
|
17
|
+
type FantasyObserver<T> = {
|
|
18
|
+
next(x: T): void;
|
|
19
|
+
error(err: any): void;
|
|
20
|
+
complete(c?: any): void;
|
|
21
|
+
};
|
|
22
|
+
type FantasySubscription = {
|
|
23
|
+
unsubscribe(): void;
|
|
24
|
+
};
|
|
25
|
+
type FantasyObservable<T> = {
|
|
26
|
+
subscribe(observer: FantasyObserver<T>): FantasySubscription;
|
|
27
|
+
};
|
|
28
|
+
type Driver<Si, So> = Si extends void ? (() => So) : ((stream: Si) => So);
|
|
29
|
+
|
|
30
|
+
type Predicate = (ev: Event) => boolean;
|
|
31
|
+
type Comparator = Record<string, unknown>;
|
|
32
|
+
type PreventDefaultOpt = boolean | Predicate | Comparator;
|
|
33
|
+
|
|
34
|
+
interface EnrichedEventStream<T = Event> extends Stream<T> {
|
|
35
|
+
value(): EnrichedEventStream<string>;
|
|
36
|
+
value<R>(fn: (val: string) => R): EnrichedEventStream<R>;
|
|
37
|
+
checked(): EnrichedEventStream<boolean>;
|
|
38
|
+
checked<R>(fn: (val: boolean) => R): EnrichedEventStream<R>;
|
|
39
|
+
data(name: string): EnrichedEventStream<string | undefined>;
|
|
40
|
+
data<R>(name: string, fn: (val: string | undefined) => R): EnrichedEventStream<R>;
|
|
41
|
+
target(): EnrichedEventStream<EventTarget | null>;
|
|
42
|
+
target<R>(fn: (el: EventTarget | null) => R): EnrichedEventStream<R>;
|
|
43
|
+
key(): EnrichedEventStream<string>;
|
|
44
|
+
key<R>(fn: (key: string) => R): EnrichedEventStream<R>;
|
|
45
|
+
/** e.detail: a CustomEvent's payload (a widget's emit(name, detail), a web component's event) */
|
|
46
|
+
detail<D = any>(): EnrichedEventStream<D>;
|
|
47
|
+
detail<R, D = any>(fn: (detail: D) => R): EnrichedEventStream<R>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
declare class DocumentDOMSource {
|
|
51
|
+
private _name;
|
|
52
|
+
private _selector;
|
|
53
|
+
constructor(_name: string, selector?: string);
|
|
54
|
+
select(selector: string): DocumentDOMSource;
|
|
55
|
+
elements(): MemoryStream<Array<Document | Element>>;
|
|
56
|
+
element(): MemoryStream<Document | Element | null>;
|
|
57
|
+
events<K extends keyof DocumentEventMap>(eventType: K, options?: EventsFnOptions, bubbles?: boolean): EnrichedEventStream<DocumentEventMap[K]>;
|
|
58
|
+
events(eventType: string, options?: EventsFnOptions, bubbles?: boolean): EnrichedEventStream<Event>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
declare class BodyDOMSource {
|
|
62
|
+
private _name;
|
|
63
|
+
constructor(_name: string);
|
|
64
|
+
select(selector: string): BodyDOMSource;
|
|
65
|
+
elements(): MemoryStream<Array<HTMLBodyElement>>;
|
|
66
|
+
element(): MemoryStream<HTMLBodyElement>;
|
|
67
|
+
events<K extends keyof HTMLBodyElementEventMap>(eventType: K, options?: EventsFnOptions, bubbles?: boolean): Stream<HTMLBodyElementEventMap[K]>;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface EventsFnOptions {
|
|
71
|
+
useCapture?: boolean;
|
|
72
|
+
passive?: boolean;
|
|
73
|
+
bubbles?: boolean;
|
|
74
|
+
preventDefault?: PreventDefaultOpt;
|
|
75
|
+
}
|
|
76
|
+
type DOMSource = MainDOMSource | DocumentDOMSource | BodyDOMSource;
|
|
77
|
+
|
|
78
|
+
interface Scope$1 {
|
|
79
|
+
type: 'sibling' | 'total' | 'selector';
|
|
80
|
+
scope: string;
|
|
81
|
+
}
|
|
82
|
+
type IsolateSink<T extends VNode> = (s: Stream<T>, scope: string) => Stream<T>;
|
|
83
|
+
|
|
84
|
+
interface CycleDOMEvent extends Event {
|
|
85
|
+
propagationHasBeenStopped: boolean;
|
|
86
|
+
ownerTarget: Element;
|
|
87
|
+
}
|
|
88
|
+
declare class EventDelegator {
|
|
89
|
+
private rootElement$;
|
|
90
|
+
isolateModule: IsolateModule;
|
|
91
|
+
private virtualListeners;
|
|
92
|
+
private origin;
|
|
93
|
+
private domListeners;
|
|
94
|
+
private nonBubblingListeners;
|
|
95
|
+
private domListenersToAdd;
|
|
96
|
+
private nonBubblingListenersToAdd;
|
|
97
|
+
private virtualNonBubblingListener;
|
|
98
|
+
constructor(rootElement$: Stream<Element>, isolateModule: IsolateModule);
|
|
99
|
+
addEventListener(eventType: string, namespace: Array<Scope$1>, options: EventsFnOptions, bubbles?: boolean): Stream<Event>;
|
|
100
|
+
removeElement(element: Element): void;
|
|
101
|
+
private eachSet;
|
|
102
|
+
private insertListener;
|
|
103
|
+
private removeListener;
|
|
104
|
+
private getVirtualListeners;
|
|
105
|
+
private setupDOMListener;
|
|
106
|
+
private setupNonBubblingListener;
|
|
107
|
+
private resetEventListeners;
|
|
108
|
+
private putNonBubblingListener;
|
|
109
|
+
private onEvent;
|
|
110
|
+
private bubble;
|
|
111
|
+
private doBubbleStep;
|
|
112
|
+
private patchEvent;
|
|
113
|
+
private mutateEventCurrentTarget;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
declare class IsolateModule {
|
|
117
|
+
private namespaceTree;
|
|
118
|
+
private namespaceByElement;
|
|
119
|
+
private eventDelegator;
|
|
120
|
+
private vnodesBeingRemoved;
|
|
121
|
+
constructor();
|
|
122
|
+
setEventDelegator(del: EventDelegator): void;
|
|
123
|
+
private insertElement;
|
|
124
|
+
private removeElement;
|
|
125
|
+
getElements(namespace: Array<Scope$1>): Array<Element>;
|
|
126
|
+
getRootElement(elm: Element): Element | undefined;
|
|
127
|
+
getNamespace(elm: Element): Array<Scope$1> | undefined;
|
|
128
|
+
createModule(): {
|
|
129
|
+
pre(): void;
|
|
130
|
+
create(emptyVNode: VNode, vNode: VNode): void;
|
|
131
|
+
update(oldVNode: VNode, vNode: VNode): void;
|
|
132
|
+
destroy(vNode: VNode): void;
|
|
133
|
+
remove(vNode: VNode, cb: Function): void;
|
|
134
|
+
post(): void;
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
interface SpecialSelector {
|
|
139
|
+
body: BodyDOMSource;
|
|
140
|
+
document: DocumentDOMSource;
|
|
141
|
+
}
|
|
142
|
+
declare class MainDOMSource {
|
|
143
|
+
private _rootElement$;
|
|
144
|
+
private _sanitation$;
|
|
145
|
+
private _namespace;
|
|
146
|
+
_isolateModule: IsolateModule;
|
|
147
|
+
private _eventDelegator;
|
|
148
|
+
private _name;
|
|
149
|
+
constructor(_rootElement$: Stream<Element>, _sanitation$: Stream<null>, _namespace: Array<Scope$1>, _isolateModule: IsolateModule, _eventDelegator: EventDelegator, _name: string);
|
|
150
|
+
private _elements;
|
|
151
|
+
elements(): MemoryStream<Array<Element>>;
|
|
152
|
+
element(): MemoryStream<Element>;
|
|
153
|
+
get namespace(): Array<Scope$1>;
|
|
154
|
+
select<T extends keyof SpecialSelector>(selector: T): SpecialSelector[T];
|
|
155
|
+
select(selector: string): MainDOMSource;
|
|
156
|
+
events<K extends keyof HTMLElementEventMap>(eventType: K, options?: EventsFnOptions, bubbles?: boolean): EnrichedEventStream<HTMLElementEventMap[K]>;
|
|
157
|
+
events(eventType: string, options?: EventsFnOptions, bubbles?: boolean): EnrichedEventStream<Event>;
|
|
158
|
+
dispose(): void;
|
|
159
|
+
isolateSource: (source: MainDOMSource, scope: string) => MainDOMSource;
|
|
160
|
+
isolateSink: IsolateSink<VNode>;
|
|
161
|
+
isolateValue: (node: any, scope: string) => any;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
interface DOMDriverOptions {
|
|
165
|
+
modules?: Array<Partial<Module>>;
|
|
166
|
+
reportSnabbdomError?(err: unknown): void;
|
|
167
|
+
snabbdomOptions?: Options;
|
|
168
|
+
}
|
|
169
|
+
declare function makeDOMDriver(container: string | Element | DocumentFragment, options?: DOMDriverOptions): Driver<Stream<VNode>, MainDOMSource>;
|
|
170
|
+
|
|
171
|
+
type Reducer$1<T> = (state: T | undefined) => T | undefined;
|
|
172
|
+
type Getter<T, R> = (state: T | undefined) => R | undefined;
|
|
173
|
+
type Setter<T, R> = (state: T | undefined, childState: R | undefined) => T | undefined;
|
|
174
|
+
type Lens$1<T, R> = {
|
|
175
|
+
get: Getter<T, R>;
|
|
176
|
+
set: Setter<T, R>;
|
|
177
|
+
};
|
|
178
|
+
type Scope<T, R> = string | number | Lens$1<T, R>;
|
|
179
|
+
|
|
180
|
+
declare function isolateSource<T, R>(source: StateSource<T>, scope: Scope<T, R>): StateSource<R>;
|
|
181
|
+
declare function isolateSink<T, R>(innerReducer$: Stream<Reducer$1<R>>, scope: Scope<T, R>): Stream<Reducer$1<T>>;
|
|
182
|
+
/**
|
|
183
|
+
* Represents a piece of application state dynamically changing over time.
|
|
184
|
+
*/
|
|
185
|
+
declare class StateSource<S> {
|
|
186
|
+
stream: MemoryStream<S>;
|
|
187
|
+
private _stream;
|
|
188
|
+
private _name;
|
|
189
|
+
private _end?;
|
|
190
|
+
constructor(stream: Stream<S>, name: string, end?: Stream<any>, raw?: any);
|
|
191
|
+
/**
|
|
192
|
+
* Selects a part (or scope) of the state object and returns a new StateSource
|
|
193
|
+
* dynamically representing that selected part of the state.
|
|
194
|
+
*/
|
|
195
|
+
select<R>(scope: Scope<S, R>): StateSource<R>;
|
|
196
|
+
/**
|
|
197
|
+
* PLAN-4 GS-6: selector(state) whenever it changes structurally (objIsEqual). The current
|
|
198
|
+
* value is the baseline, not a change, unless { immediate: true }. Ends with the state stream
|
|
199
|
+
* or, for a component's own STATE source, when the component is disposed
|
|
200
|
+
*/
|
|
201
|
+
watch<R>(selector: (state: S) => R, { immediate }?: {
|
|
202
|
+
immediate?: boolean;
|
|
203
|
+
}): Stream<R>;
|
|
204
|
+
isolateSource: typeof isolateSource;
|
|
205
|
+
isolateSink: typeof isolateSink;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Types for the 'sygnal/diagnostics' entry (runtime consistency checks).
|
|
210
|
+
*
|
|
211
|
+
* import 'sygnal/diagnostics' // registers all checks (side effect)
|
|
212
|
+
*
|
|
213
|
+
* Checks only run while diagnostics are on: run(App, drivers, { diagnostics: 'warn' }),
|
|
214
|
+
* or globalThis.__SYGNAL_DEV__ (set by the Vite plugin in dev).
|
|
215
|
+
*/
|
|
216
|
+
|
|
217
|
+
type DiagnosticSeverity$1 = 'error' | 'warn' | 'info'
|
|
218
|
+
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// inspect (PLAN-1 workstream 2B): a machine-readable app graph. The same shape
|
|
221
|
+
// is produced at runtime (inspect(), getDevTools().inspect(), renderComponent's
|
|
222
|
+
// t.inspect()) and statically (`sygnal-check --graph --json`); JSON Schema:
|
|
223
|
+
// sygnal-check/schema/inspect.schema.json. Fields one side cannot know are
|
|
224
|
+
// null (or omitted).
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
/** How an action is dispatched. */
|
|
228
|
+
type InspectActionTrigger = 'intent' | 'next' | 'reply' | 'builtin' | 'unknown'
|
|
229
|
+
|
|
230
|
+
interface InspectAction {
|
|
231
|
+
name: string
|
|
232
|
+
/**
|
|
233
|
+
* 'intent': returned by the component's intent. 'builtin': BOOTSTRAP,
|
|
234
|
+
* INITIALIZE, DISPOSE or READY. 'reply': named as a reply action by a request
|
|
235
|
+
* (`ok: 'NAME'` / `error: 'NAME'`) or a `connections` entry (statically: a
|
|
236
|
+
* string literal; at runtime: a request the instance was seen sending).
|
|
237
|
+
* 'next': dispatched with next() (statically: a next('NAME') literal; at
|
|
238
|
+
* runtime: a model-only action whose STATE reducer was seen running).
|
|
239
|
+
* 'unknown': none of these is known.
|
|
240
|
+
*/
|
|
241
|
+
trigger: InspectActionTrigger
|
|
242
|
+
/** sinks of the model entry (STATE, EVENTS, EFFECT, PARENT, custom drivers); [] without a model entry */
|
|
243
|
+
sinks: string[]
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
interface InspectChild {
|
|
247
|
+
name: string
|
|
248
|
+
via: 'tag' | 'collection' | 'switchable' | 'slot'
|
|
249
|
+
/** Collection: the `from` state field, when known */
|
|
250
|
+
from?: string | null
|
|
251
|
+
/** runtime: number of live instances behind this entry (e.g. Collection items) */
|
|
252
|
+
count?: number
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
interface InspectSelector {
|
|
256
|
+
/** CSS selector passed to DOM.select() (chained selects joined with a space) */
|
|
257
|
+
selector: string
|
|
258
|
+
/** event types listened to; null when unknown (runtime, real DOM) */
|
|
259
|
+
events: string[] | null
|
|
260
|
+
/**
|
|
261
|
+
* whether it matches an element the component itself renders; null when unknown.
|
|
262
|
+
* static: the class/id is in the view source. renderComponent: an element of the latest
|
|
263
|
+
* render matches (a conditionally rendered element that is hidden right now gives false).
|
|
264
|
+
* real DOM: true once a DOM check saw it match, false after SYG103/SYG104, else null.
|
|
265
|
+
*/
|
|
266
|
+
matched: boolean | null
|
|
267
|
+
/** the child component whose (isolated) elements it matches instead, if any */
|
|
268
|
+
isolationHit: string | null
|
|
269
|
+
/** PLAN-4 CT-1: the control this selector is (its key), when it is `[data-control="<Key>"]` */
|
|
270
|
+
control?: string
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** PLAN-4 CT-1: a control (controls()) a component renders */
|
|
274
|
+
interface InspectControl {
|
|
275
|
+
/** the control's key (rendered as data-control="<name>") */
|
|
276
|
+
name: string
|
|
277
|
+
/** the intrinsic tag of a tag spec; null for a spec object */
|
|
278
|
+
element: string | null
|
|
279
|
+
/** a spec object's kind (e.g. 'widget') */
|
|
280
|
+
kind?: string
|
|
281
|
+
/** whether the component's intent listens to it; null when unknown */
|
|
282
|
+
listened: boolean | null
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
interface InspectDiagnostic {
|
|
286
|
+
code: string
|
|
287
|
+
severity: DiagnosticSeverity$1
|
|
288
|
+
component?: string
|
|
289
|
+
message: string
|
|
290
|
+
fix?: string
|
|
291
|
+
docsUrl?: string
|
|
292
|
+
data?: any
|
|
293
|
+
/** static only */
|
|
294
|
+
file?: string
|
|
295
|
+
line?: number
|
|
296
|
+
column?: number
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
interface InspectComponent {
|
|
300
|
+
name: string
|
|
301
|
+
/** runtime: instance number (as a string); static: 'file:line' of the definition */
|
|
302
|
+
id: string
|
|
303
|
+
/** runtime: the parent instance's id; static: null (see the parents' `children`) */
|
|
304
|
+
parentId: string | null
|
|
305
|
+
/** static only: source file, relative to the working directory */
|
|
306
|
+
file?: string
|
|
307
|
+
/**
|
|
308
|
+
* runtime: how the instance was created; static: 'root' when no scanned
|
|
309
|
+
* component renders it, else how it is first rendered
|
|
310
|
+
*/
|
|
311
|
+
kind: 'root' | 'child' | 'collection-item' | 'switchable'
|
|
312
|
+
actions: InspectAction[]
|
|
313
|
+
/** state keys (runtime: current state; static: initialState) */
|
|
314
|
+
stateKeys: string[]
|
|
315
|
+
/** calculated field names */
|
|
316
|
+
calculated: string[]
|
|
317
|
+
/** context fields this component provides (Component.context) */
|
|
318
|
+
contextProvides: string[]
|
|
319
|
+
/** context fields its view reads (static only; null at runtime) */
|
|
320
|
+
contextConsumes?: string[] | null
|
|
321
|
+
/** EVENTS types this component emits */
|
|
322
|
+
eventsEmitted: string[]
|
|
323
|
+
/** EVENTS types this component selects ('*' = EVENTS.select() without a type) */
|
|
324
|
+
eventsSelected: string[]
|
|
325
|
+
children: InspectChild[]
|
|
326
|
+
selectors: InspectSelector[]
|
|
327
|
+
/** PLAN-4 CT-1: the controls it renders (runtime: seen in its renders); omitted when none */
|
|
328
|
+
controls?: InspectControl[]
|
|
329
|
+
/** static only, PLAN-4 GS-2 (G-224): the element commands (ELEMENT sink) its model sends; omitted when none */
|
|
330
|
+
commands?: InspectCommand[]
|
|
331
|
+
/** static only, PLAN-4 GS-7 (G-224): the literal timer specs of its `timers` static; omitted when none */
|
|
332
|
+
timers?: InspectTimer[]
|
|
333
|
+
diagnostics: InspectDiagnostic[]
|
|
334
|
+
/** runtime, PLAN-3 5-3: the instance's `resources` and their state */
|
|
335
|
+
resources?: InspectResource[]
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** PLAN-4 GS-2: an element command a model entry sends (static inspect, sygnal-check --graph) */
|
|
339
|
+
interface InspectCommand {
|
|
340
|
+
/** the model entry that sends it */
|
|
341
|
+
action: string
|
|
342
|
+
/** the command's method (its first key) */
|
|
343
|
+
method: string
|
|
344
|
+
/** the control's key or the selector; null when not static */
|
|
345
|
+
target: string | null
|
|
346
|
+
/** the control it targets (its key) */
|
|
347
|
+
control?: string
|
|
348
|
+
/** intent actions listening on the target for a native event the command causes (close, toggle...) */
|
|
349
|
+
triggers?: string[]
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** PLAN-4 GS-7: a literal timer spec of a `timers` static (static inspect, sygnal-check --graph) */
|
|
353
|
+
interface InspectTimer {
|
|
354
|
+
name: string
|
|
355
|
+
every?: number | null
|
|
356
|
+
after?: number | null
|
|
357
|
+
/** the action a frame timer dispatches */
|
|
358
|
+
frame?: string | null
|
|
359
|
+
action?: string | null
|
|
360
|
+
background?: true
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** PLAN-3 5-3: a resource of a component instance (runtime inspect()) */
|
|
364
|
+
interface InspectResource {
|
|
365
|
+
name: string
|
|
366
|
+
status: 'idle' | 'loading' | 'success' | 'error'
|
|
367
|
+
refreshing: boolean
|
|
368
|
+
/** `data` is set (a success, or kept through a refetch) */
|
|
369
|
+
hasData: boolean
|
|
370
|
+
/** the error message while 'error' */
|
|
371
|
+
error?: string
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** PLAN-3 5-3: a makeFetchDriver cache entry (runtime inspect(), t.cache) */
|
|
375
|
+
interface InspectCacheEntry {
|
|
376
|
+
key: string
|
|
377
|
+
age?: number
|
|
378
|
+
stale: boolean
|
|
379
|
+
subscribers: number
|
|
380
|
+
data: any
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
interface InspectGraph {
|
|
384
|
+
version: 1
|
|
385
|
+
/** which implementation produced the graph */
|
|
386
|
+
source?: 'runtime' | 'static'
|
|
387
|
+
components: InspectComponent[]
|
|
388
|
+
/** EVENTS bus: type -> names of the components that emit / select it */
|
|
389
|
+
events: Record<string, { emitters: string[]; selectors: string[] }>
|
|
390
|
+
/** diagnostics not tied to a listed component */
|
|
391
|
+
diagnostics: InspectDiagnostic[]
|
|
392
|
+
/** runtime, PLAN-3 5-3: makeFetchDriver cache entries, by sink name (empty when no cache) */
|
|
393
|
+
cache?: Record<string, InspectCacheEntry[]>
|
|
394
|
+
/** runtime, PLAN-4 2-C (GS-10): with `inspect({ actions })`, the most recent actions, oldest first */
|
|
395
|
+
recentActions?: InspectRecentAction[]
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** PLAN-4 2-C (GS-10): one recent action (inspect({ actions })) */
|
|
399
|
+
interface InspectRecentAction {
|
|
400
|
+
type: string
|
|
401
|
+
/** the action's data, when it is JSON-safe and small (a DOM event is left out) */
|
|
402
|
+
data?: any
|
|
403
|
+
/** the component's name */
|
|
404
|
+
component: string
|
|
405
|
+
/** the instance's id (InspectComponent.id) */
|
|
406
|
+
instance: string
|
|
407
|
+
/** the sinks that produced a value for it (not ABORT) */
|
|
408
|
+
sinks: string[]
|
|
409
|
+
cause: 'intent' | 'next' | 'reply' | 'built-in' | 'simulateAction' | 'behavior'
|
|
410
|
+
/** ms since the dev entry started recording (or the last resetChecks()) */
|
|
411
|
+
at: number
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
interface InspectOptions {
|
|
415
|
+
/** only these component instances (ids as in InspectComponent.id) */
|
|
416
|
+
ids?: Array<string | number>
|
|
417
|
+
/** selector details by component id (renderComponent passes its mock-DOM view) */
|
|
418
|
+
selectors?: Record<string, InspectSelector[]>
|
|
419
|
+
/** diagnostics to attach (default: the devtools' collected diagnostics, when available) */
|
|
420
|
+
diagnostics?: Array<{ code: string; severity: DiagnosticSeverity$1; component?: string; message: string; [key: string]: any }>
|
|
421
|
+
/** PLAN-4 2-C: add `recentActions`: true for every kept one (the last 200), a number for the last n */
|
|
422
|
+
actions?: boolean | number
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
interface ThunkData extends VNodeData {
|
|
426
|
+
fn(): VNode;
|
|
427
|
+
args: Array<any>;
|
|
428
|
+
}
|
|
429
|
+
interface Thunk extends VNode {
|
|
430
|
+
data: ThunkData;
|
|
431
|
+
}
|
|
432
|
+
declare function thunk(sel: string, fn: Function, args: Array<any>): Thunk;
|
|
433
|
+
declare function thunk(sel: string, key: any, fn: Function, args: Array<any>): Thunk;
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Optional simulated-event hub (used by renderComponent's simulateEvent): a
|
|
437
|
+
* stream of `{type, event, match(path)}`; each source's events(type) also emits
|
|
438
|
+
* the hub events whose `match` accepts its selector path (isolation scopes
|
|
439
|
+
* included as '.___scope' segments).
|
|
440
|
+
*/
|
|
441
|
+
type MockEventHub = Stream<{
|
|
442
|
+
type: string;
|
|
443
|
+
event: any;
|
|
444
|
+
match: (path: string[]) => boolean;
|
|
445
|
+
}>;
|
|
446
|
+
/**
|
|
447
|
+
* Optional listener callback (renderComponent): called with `live` undefined when events() is
|
|
448
|
+
* called (SYG103/104 checks), then with true / false when that hub listener is subscribed /
|
|
449
|
+
* unsubscribed (simulateEvent waits for a just-mounted child's listeners, G-039).
|
|
450
|
+
*/
|
|
451
|
+
type MockOnEvents = (path: string[], eventType: string, live?: boolean) => void;
|
|
452
|
+
type MockConfig = {
|
|
453
|
+
[name: string]: FantasyObservable<any> | MockConfig;
|
|
454
|
+
};
|
|
455
|
+
declare class MockedDOMSource {
|
|
456
|
+
private _mockConfig;
|
|
457
|
+
private _hub?;
|
|
458
|
+
_path: string[];
|
|
459
|
+
private _onEvents?;
|
|
460
|
+
private _elements;
|
|
461
|
+
constructor(_mockConfig: MockConfig, _hub?: MockEventHub | undefined, _path?: string[], _onEvents?: MockOnEvents | undefined);
|
|
462
|
+
elements(): any;
|
|
463
|
+
element(): any;
|
|
464
|
+
events(eventType: string, options?: EventsFnOptions, bubbles?: boolean): any;
|
|
465
|
+
select(selector: any): MockedDOMSource;
|
|
466
|
+
isolateSource(source: MockedDOMSource, scope: string): MockedDOMSource;
|
|
467
|
+
/**
|
|
468
|
+
* PLAN-4.6: isolateSink for one vnode (a copy, with the scope class). 4-I G-558: a fragment (a
|
|
469
|
+
* Collection's, `<>…</>`) has no element: each of its top-level elements gets the class (as the
|
|
470
|
+
* real driver's scoped()); a text vnode is left as it is
|
|
471
|
+
*/
|
|
472
|
+
isolateValue(vnode: any, scope: string): any;
|
|
473
|
+
isolateSink(sink: any, scope: string): any;
|
|
474
|
+
}
|
|
475
|
+
declare function mockDOMSource(mockConfig: MockConfig, hub?: MockEventHub, onEvents?: MockOnEvents): MockedDOMSource;
|
|
6
476
|
|
|
7
|
-
|
|
8
|
-
|
|
477
|
+
declare const ABORT: unique symbol
|
|
478
|
+
type ABORT = typeof ABORT
|
|
9
479
|
|
|
10
|
-
|
|
480
|
+
type DriverSpec<SOURCE = any, SINK = any> = {
|
|
11
481
|
source: SOURCE;
|
|
12
482
|
sink: SINK;
|
|
13
483
|
}
|
|
14
484
|
|
|
15
|
-
|
|
485
|
+
type DriverSpecs = Record<string, DriverSpec<any, any>>
|
|
16
486
|
|
|
17
|
-
|
|
487
|
+
type CycleDriver<SINK = any, SOURCE = any> =
|
|
18
488
|
SINK extends void
|
|
19
489
|
? (() => SOURCE)
|
|
20
490
|
: ((sink$: Stream<SINK>) => SOURCE)
|
|
21
491
|
|
|
22
|
-
|
|
492
|
+
type DriverFactories<DRIVERS extends DriverSpecs = DriverSpecs> = {
|
|
23
493
|
[DRIVER_KEY in keyof DRIVERS]: CycleDriver<DRIVERS[DRIVER_KEY]['sink'], DRIVERS[DRIVER_KEY]['source']>
|
|
24
494
|
}
|
|
25
495
|
|
|
26
496
|
/**
|
|
27
497
|
* A function that takes component properties and returns a JSX element.
|
|
28
|
-
* State
|
|
498
|
+
* State and context are always provided by the framework at runtime. In JSX the element
|
|
499
|
+
* takes the component's own props plus an optional `state` slice name or lens instead
|
|
500
|
+
* (see `JSX.LibraryManagedAttributes` / `ElementProps`).
|
|
29
501
|
*/
|
|
30
502
|
type ComponentProps<STATE, PROPS, CONTEXT> = (
|
|
31
|
-
props:
|
|
32
|
-
state: STATE,
|
|
33
|
-
context: CONTEXT,
|
|
34
|
-
peers: { [peer: string]: JSX.Element | JSX.Element[] }
|
|
503
|
+
props: ViewProps<STATE, PROPS, CONTEXT>
|
|
35
504
|
) => JSX.Element
|
|
36
505
|
|
|
506
|
+
/**
|
|
507
|
+
* PLAN-4 GS-9: `uid()` is a stable id string for this component instance (from its position in
|
|
508
|
+
* the tree: the same in renderToString and after hydration); `uid('name')` derives one from it,
|
|
509
|
+
* e.g. `<input id={uid('email')} />` with `<label for={uid('email')}>`.
|
|
510
|
+
*/
|
|
511
|
+
type UidFunction = (name?: string) => string
|
|
512
|
+
|
|
513
|
+
/** The first argument of a component's view: its props plus `state`, `context`, `children`, `slots` and `uid`. */
|
|
514
|
+
type ViewProps<STATE = any, PROPS = {}, CONTEXT = {}> =
|
|
515
|
+
PROPS & { state: STATE; context: CONTEXT; children?: JSX.Element | JSX.Element[]; slots?: Record<string, JSX.Element[]>; uid: UidFunction }
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* The `state` prop a parent passes to a sub-component in JSX: the name of a field of the
|
|
519
|
+
* parent's state (`state="editor"`) or a lens. Without it the child shares the parent's state.
|
|
520
|
+
*/
|
|
521
|
+
type StateProp = string | Lense<any, any>
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* The JSX attributes of a component whose view takes PROPS: `state` becomes an optional
|
|
525
|
+
* slice name or lens, and the framework-provided `context` and `slots` are not passed.
|
|
526
|
+
* `resetState` (6.0): for an `isolatedState` child bound with `state`, replace the slice with
|
|
527
|
+
* the child's `initialState` when it is created (by default an existing slice is kept and
|
|
528
|
+
* `initialState` only seeds a missing one). Read at creation, like `state`; not a prop of the child.
|
|
529
|
+
*/
|
|
530
|
+
type ElementProps<PROPS> =
|
|
531
|
+
0 extends (1 & PROPS) ? PROPS
|
|
532
|
+
: 'state' extends keyof PROPS ? WithoutViewOnlyProps<PROPS> & { state?: StateProp; resetState?: boolean }
|
|
533
|
+
: PROPS
|
|
534
|
+
|
|
535
|
+
/** PROPS without `state`, `context`, `slots` and `uid` (keeps optionality and index signatures, unlike Omit). */
|
|
536
|
+
type WithoutViewOnlyProps<PROPS> = {
|
|
537
|
+
[KEY in keyof PROPS as KEY extends 'state' | 'context' | 'slots' | 'uid' ? never : KEY]: PROPS[KEY]
|
|
538
|
+
}
|
|
539
|
+
|
|
37
540
|
type NextFunction<ACTIONS = any> = ACTIONS extends object
|
|
38
541
|
? <ACTION_KEY extends keyof ACTIONS>(
|
|
39
542
|
action: ACTION_KEY,
|
|
@@ -42,7 +545,7 @@ type NextFunction<ACTIONS = any> = ACTIONS extends object
|
|
|
42
545
|
) => void
|
|
43
546
|
: (action: string, data?: any, delay?: number) => void
|
|
44
547
|
|
|
45
|
-
type ReducerExtras<PROPS, CONTEXT> = PROPS & { context: CONTEXT; children?: JSX.Element | JSX.Element[]; slots?: Record<string, JSX.Element[]
|
|
548
|
+
type ReducerExtras<PROPS, CONTEXT> = PROPS & { context: CONTEXT; children?: JSX.Element | JSX.Element[]; slots?: Record<string, JSX.Element[]>; uid: UidFunction }
|
|
46
549
|
|
|
47
550
|
type Reducer<STATE, PROPS, ACTIONS = any, DATA = any, RETURN = any, CONTEXT = {}> = (
|
|
48
551
|
state: STATE,
|
|
@@ -51,25 +554,97 @@ type Reducer<STATE, PROPS, ACTIONS = any, DATA = any, RETURN = any, CONTEXT = {}
|
|
|
51
554
|
props: ReducerExtras<PROPS, CONTEXT>
|
|
52
555
|
) => RETURN | ABORT | undefined
|
|
53
556
|
|
|
54
|
-
|
|
557
|
+
type ExactShape<EXPECTED, ACTUAL extends EXPECTED> = ACTUAL &
|
|
55
558
|
Record<Exclude<keyof ACTUAL, keyof EXPECTED>, never>
|
|
56
559
|
|
|
57
560
|
type StateOnlyReducer<STATE, RETURN = any> = (
|
|
58
561
|
state: STATE
|
|
59
562
|
) => RETURN | ABORT
|
|
60
563
|
|
|
61
|
-
|
|
564
|
+
type Event$1<DATA = any> = { type: string; data: DATA }
|
|
565
|
+
|
|
566
|
+
// ── EVENTS registry ────────────────────────────────────────────────
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Registry of global EVENTS bus event names and their payload types.
|
|
570
|
+
*
|
|
571
|
+
* Empty by default, which keeps the EVENTS bus untyped (`any`). Augment it to
|
|
572
|
+
* type-check `EVENTS.select()`, `event()`, `emit()` and raw EVENTS sink returns:
|
|
573
|
+
*
|
|
574
|
+
* declare module 'sygnal' {
|
|
575
|
+
* interface SygnalEvents {
|
|
576
|
+
* DELETE_LANE: { laneId: string }
|
|
577
|
+
* RESET: void // no payload: event('RESET')
|
|
578
|
+
* }
|
|
579
|
+
* }
|
|
580
|
+
*
|
|
581
|
+
* Once the registry has at least one entry, unregistered event names are type errors.
|
|
582
|
+
* The augmenting file must be a module (have at least one import or export).
|
|
583
|
+
*/
|
|
584
|
+
interface SygnalEvents {}
|
|
585
|
+
|
|
586
|
+
/** Valid event names: `string` while the registry is empty, otherwise the registered names. */
|
|
587
|
+
type EventName = keyof SygnalEvents extends never ? string : keyof SygnalEvents & string
|
|
588
|
+
|
|
589
|
+
/** Payload type of a registered event (`any` while the registry is empty). */
|
|
590
|
+
type EventPayload<TYPE extends string = string> = keyof SygnalEvents extends never
|
|
591
|
+
? any
|
|
592
|
+
: TYPE extends keyof SygnalEvents ? SygnalEvents[TYPE] : never
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* An event object as put on the EVENTS bus. While the registry is empty this is `Event<any>`;
|
|
596
|
+
* otherwise it is the union of `{ type, data }` for every registered event.
|
|
597
|
+
*/
|
|
598
|
+
type RegisteredEvent = keyof SygnalEvents extends never
|
|
599
|
+
? Event$1<any>
|
|
600
|
+
: { [TYPE in keyof SygnalEvents & string]: { type: TYPE; data: SygnalEvents[TYPE] } }[keyof SygnalEvents & string]
|
|
601
|
+
|
|
602
|
+
/** The `{ type, data }` object produced by `event(type, ...)` / `emit(type, ...)`. */
|
|
603
|
+
type EmittedEvent<TYPE extends string = string> = keyof SygnalEvents extends never
|
|
604
|
+
? { type: TYPE; data: any }
|
|
605
|
+
// TYPE is every registered name when the name argument isn't registered (inference falls
|
|
606
|
+
// back to the constraint): any registered event, so only the clear "not assignable to
|
|
607
|
+
// parameter" error is reported, not a second one on the model entry
|
|
608
|
+
: [EventName] extends [TYPE] ? RegisteredEvent
|
|
609
|
+
: { type: TYPE; data: EventPayload<TYPE> }
|
|
610
|
+
|
|
611
|
+
/** The `next()` function as seen by an event payload function. */
|
|
612
|
+
type EventNextFunction = (action: string, data?: any, delay?: number) => void
|
|
613
|
+
|
|
614
|
+
/** Payload function for `event()` / `emit()`: receives the reducer arguments, returns the event data. */
|
|
615
|
+
type EventPayloadFunction<TYPE extends string = string, STATE = any, DATA = any> =
|
|
616
|
+
(state: STATE, data: DATA, next: EventNextFunction, props: any) => EventPayload<TYPE>
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Remaining arguments of `event()` / `emit()` when the payload is a static value.
|
|
620
|
+
* The payload may be omitted when the registry is empty or the event's payload
|
|
621
|
+
* type accepts `undefined` (e.g. `void`).
|
|
622
|
+
*/
|
|
623
|
+
type StaticEventArgs<TYPE extends string> = keyof SygnalEvents extends never
|
|
624
|
+
? [payload?: any]
|
|
625
|
+
: undefined extends EventPayload<TYPE>
|
|
626
|
+
? [payload?: EventPayload<TYPE>]
|
|
627
|
+
: [payload: EventPayload<TYPE>]
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* The sink function returned by `event()`. Use it as the value of an `EVENTS` key in a
|
|
631
|
+
* model entry: `ACTION: { STATE: ..., EVENTS: event('TYPE', fn) }`.
|
|
632
|
+
*/
|
|
633
|
+
type EventSink<TYPE extends string = string, STATE = any, DATA = any> =
|
|
634
|
+
(state: STATE, data: DATA, next: any, props: any) => EmittedEvent<TYPE>
|
|
62
635
|
|
|
63
|
-
|
|
636
|
+
type NonStateSinkReturns = {
|
|
64
637
|
EVENTS?: unknown;
|
|
65
638
|
LOG?: unknown;
|
|
66
639
|
PARENT?: unknown;
|
|
67
640
|
}
|
|
68
641
|
|
|
69
642
|
type ResolvedNonStateSinkReturns<SINK_RETURNS extends NonStateSinkReturns = {}> = {
|
|
70
|
-
EVENTS: SINK_RETURNS extends { EVENTS: infer EVENTS_RETURN } ? EVENTS_RETURN :
|
|
643
|
+
EVENTS: SINK_RETURNS extends { EVENTS: infer EVENTS_RETURN } ? EVENTS_RETURN : RegisteredEvent;
|
|
71
644
|
LOG: SINK_RETURNS extends { LOG: infer LOG_RETURN } ? LOG_RETURN : any;
|
|
72
|
-
|
|
645
|
+
// `unknown` (any value may be sent) rather than `any`, so CHILD.select(Child) of a child
|
|
646
|
+
// annotated without `{ PARENT: T }` is a Stream<unknown>, not a silent Stream<any>
|
|
647
|
+
PARENT: SINK_RETURNS extends { PARENT: infer PARENT_RETURN } ? PARENT_RETURN : unknown;
|
|
73
648
|
}
|
|
74
649
|
|
|
75
650
|
/**
|
|
@@ -82,23 +657,55 @@ type SinkValue<STATE, PROPS, ACTIONS, DATA, RETURN, CALCULATED, CONTEXT = {}> =
|
|
|
82
657
|
| true
|
|
83
658
|
| Reducer<STATE & CALCULATED, PROPS, ACTIONS, DATA, RETURN, CONTEXT>
|
|
84
659
|
|
|
660
|
+
/**
|
|
661
|
+
* Valid values for a non-STATE sink (EVENTS, LOG, PARENT, custom drivers): a SinkValue, or
|
|
662
|
+
* a constant that is sent as-is every time the action fires (`LOG: 'saved'`). Not for
|
|
663
|
+
* STATE, whose values must be reducers. A constant can't be a function (that is a
|
|
664
|
+
* reducer) or `true` (that is pass-through).
|
|
665
|
+
*/
|
|
666
|
+
type NonStateSinkValue<STATE, PROPS, ACTIONS, DATA, RETURN, CALCULATED, CONTEXT = {}> =
|
|
667
|
+
| SinkValue<STATE, PROPS, ACTIONS, DATA, RETURN, CALCULATED, CONTEXT>
|
|
668
|
+
| SinkConstant<RETURN>
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* A constant for a sink whose value type is `RETURN`. When RETURN is `any` or `unknown`
|
|
672
|
+
* (untyped sinks), any non-function value: a bare `any`/`unknown` would also accept reducers
|
|
673
|
+
* with wrong parameter types and switch off checking of the whole model entry.
|
|
674
|
+
*/
|
|
675
|
+
type SinkConstant<RETURN> = unknown extends RETURN
|
|
676
|
+
? AnySinkConstant
|
|
677
|
+
: RETURN extends (...args: any[]) => any ? never : RETURN
|
|
678
|
+
|
|
679
|
+
type AnySinkConstant =
|
|
680
|
+
| string | number | bigint | boolean | null
|
|
681
|
+
| readonly unknown[]
|
|
682
|
+
| { [key: string]: unknown; apply?: never; call?: never; bind?: never }
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* An EFFECT handler. It may be async: a returned promise is expected (no SYG219) and its
|
|
686
|
+
* rejection is reported as SYG214. `next()` after the component is disposed does nothing.
|
|
687
|
+
* `props.signal` aborts on DISPOSE (undefined where AbortController is missing).
|
|
688
|
+
*/
|
|
85
689
|
type EffectReducer<STATE, PROPS, ACTIONS, DATA, CALCULATED, CONTEXT = {}> =
|
|
86
|
-
| ((state: STATE & CALCULATED, args: DATA, next: NextFunction<ACTIONS>, props: ReducerExtras<PROPS, CONTEXT>) => void)
|
|
690
|
+
| ((state: STATE & CALCULATED, args: DATA, next: NextFunction<ACTIONS>, props: ReducerExtras<PROPS, CONTEXT> & { signal?: AbortSignal }) => void)
|
|
87
691
|
|
|
88
692
|
type DefaultSinks<STATE, PROPS, ACTIONS, DATA, CALCULATED, SINK_RETURNS extends NonStateSinkReturns = {}, CONTEXT = {}> = {
|
|
89
693
|
STATE?: SinkValue<STATE, PROPS, ACTIONS, DATA, STATE, CALCULATED, CONTEXT>;
|
|
90
|
-
EVENTS?:
|
|
91
|
-
LOG?:
|
|
92
|
-
PARENT?:
|
|
694
|
+
EVENTS?: NonStateSinkValue<STATE, PROPS, ACTIONS, DATA, ResolvedNonStateSinkReturns<SINK_RETURNS>['EVENTS'], CALCULATED, CONTEXT>;
|
|
695
|
+
LOG?: NonStateSinkValue<STATE, PROPS, ACTIONS, DATA, ResolvedNonStateSinkReturns<SINK_RETURNS>['LOG'], CALCULATED, CONTEXT>;
|
|
696
|
+
PARENT?: NonStateSinkValue<STATE, PROPS, ACTIONS, DATA, ResolvedNonStateSinkReturns<SINK_RETURNS>['PARENT'], CALCULATED, CONTEXT>;
|
|
93
697
|
EFFECT?: EffectReducer<STATE, PROPS, ACTIONS, DATA, CALCULATED, CONTEXT>;
|
|
698
|
+
/** PLAN-4 GS-2: element commands (built in, no driver): `{ focus: Email }`, `[{ ... }, { ... }]` */
|
|
699
|
+
ELEMENT?: NonStateSinkValue<STATE, PROPS, ACTIONS, DATA, ElementCommands, CALCULATED, CONTEXT>;
|
|
94
700
|
}
|
|
95
701
|
|
|
702
|
+
/** Keys a type declares by name (index signatures left out). */
|
|
96
703
|
type CustomDriverSinks<STATE, PROPS, DRIVERS, ACTIONS, ACTION_ENTRY, CALCULATED, CONTEXT = {}> = keyof DRIVERS extends never
|
|
97
704
|
? {
|
|
98
|
-
[driver: string]:
|
|
705
|
+
[driver: string]: NonStateSinkValue<STATE, PROPS, ACTIONS, any, any, CALCULATED, CONTEXT>
|
|
99
706
|
}
|
|
100
707
|
: {
|
|
101
|
-
[DRIVER_KEY in keyof DRIVERS]:
|
|
708
|
+
[DRIVER_KEY in keyof DRIVERS]: NonStateSinkValue<
|
|
102
709
|
STATE,
|
|
103
710
|
PROPS,
|
|
104
711
|
ACTIONS,
|
|
@@ -119,7 +726,6 @@ type ModelEntry<STATE, PROPS, DRIVERS, ACTIONS, ACTION_ENTRY, CALCULATED, SINK_R
|
|
|
119
726
|
type WithDefaultActions<STATE, ACTIONS> = ACTIONS & {
|
|
120
727
|
BOOTSTRAP?: never;
|
|
121
728
|
INITIALIZE?: STATE;
|
|
122
|
-
HYDRATE?: any;
|
|
123
729
|
DISPOSE?: never;
|
|
124
730
|
}
|
|
125
731
|
|
|
@@ -149,214 +755,1562 @@ type ComponentModel<STATE, PROPS, DRIVERS, ACTIONS, CALCULATED, SINK_RETURNS ext
|
|
|
149
755
|
>
|
|
150
756
|
}
|
|
151
757
|
|
|
152
|
-
type
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
758
|
+
/** Value type produced by a PARENT sink value (a reducer's return, minus ABORT/undefined). */
|
|
759
|
+
type ParentSinkValueReturn<VALUE> =
|
|
760
|
+
// an expando model widens `PARENT: true` (pass-through) and `PARENT: false` to boolean:
|
|
761
|
+
// the payload can't be told apart there
|
|
762
|
+
boolean extends VALUE ? ParentConstantReturn<Exclude<VALUE, boolean>> : ParentConstantReturn<VALUE>
|
|
156
763
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
764
|
+
type ParentConstantReturn<VALUE> =
|
|
765
|
+
VALUE extends (...args: any[]) => infer RETURN ? Exclude<RETURN, ABORT | undefined | void>
|
|
766
|
+
// `true` is pass-through (payload unknown here)
|
|
767
|
+
: VALUE extends true ? never
|
|
768
|
+
// a constant (including `false`) is sent as-is
|
|
769
|
+
: VALUE
|
|
160
770
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
771
|
+
type ParentPayloadFromEntry<ENTRY> =
|
|
772
|
+
ENTRY extends (...args: any[]) => any ? never
|
|
773
|
+
: ENTRY extends object
|
|
774
|
+
? 'PARENT' extends keyof ENTRY ? ParentSinkValueReturn<NonNullable<ENTRY['PARENT']>> : never
|
|
775
|
+
: never
|
|
164
776
|
|
|
165
|
-
|
|
166
|
-
STATE: {
|
|
167
|
-
source: StateSource<STATE>;
|
|
168
|
-
sink: STATE;
|
|
169
|
-
};
|
|
170
|
-
DOM: {
|
|
171
|
-
source: SygnalDOMSource;
|
|
172
|
-
sink: never;
|
|
173
|
-
};
|
|
174
|
-
EVENTS: {
|
|
175
|
-
source: EventsSource<EVENTS>;
|
|
176
|
-
sink: EVENTS;
|
|
177
|
-
};
|
|
178
|
-
LOG: {
|
|
179
|
-
source: never;
|
|
180
|
-
sink: any;
|
|
181
|
-
};
|
|
182
|
-
CHILD: {
|
|
183
|
-
source: ChildSource;
|
|
184
|
-
sink: never;
|
|
185
|
-
};
|
|
186
|
-
}
|
|
777
|
+
type AnyIfNever<T> = [T] extends [never] ? any : T
|
|
187
778
|
|
|
188
|
-
|
|
189
|
-
|
|
779
|
+
/**
|
|
780
|
+
* The value type a component sends to its parent through the `PARENT` sink, inferred from the
|
|
781
|
+
* component's `model` (its `{ PARENT: fn }` entries),
|
|
782
|
+
* or for a `Component<...>` annotation, the `PARENT` entry of its `SINK_RETURNS`. A
|
|
783
|
+
* `Component<...>` annotation without one gives `unknown` (declare it: `{ PARENT: T }`); no model
|
|
784
|
+
* or `PARENT: true` pass-through entries only give `any`.
|
|
785
|
+
*/
|
|
786
|
+
type ParentPayloadOf<COMPONENT> =
|
|
787
|
+
COMPONENT extends { model?: infer MODEL }
|
|
788
|
+
// the union is written out here (not behind a helper alias) so hovers and errors print
|
|
789
|
+
// the payload (`Stream<{ taskId: number }>`), not `Stream<Helper<...the whole model...>>`
|
|
790
|
+
? 0 extends (1 & MODEL) ? any : AnyIfNever<
|
|
791
|
+
ParentPayloadFromEntry<NonNullable<NonNullable<MODEL>[keyof NonNullable<MODEL>]>>
|
|
792
|
+
>
|
|
793
|
+
: any
|
|
794
|
+
|
|
795
|
+
type ChildSource = {
|
|
796
|
+
/** Typed: the stream type is inferred from the child's PARENT sink (falls back to `any`). */
|
|
797
|
+
select<COMPONENT extends (...args: any[]) => any>(component: COMPONENT): Stream<ParentPayloadOf<COMPONENT>>;
|
|
798
|
+
select<T = any>(component: (...args: any[]) => any): Stream<T>;
|
|
190
799
|
}
|
|
191
800
|
|
|
192
801
|
/**
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
* (
|
|
196
|
-
* Action data types are enforced at the model layer via reducer signatures.
|
|
802
|
+
* `DOM.<event>(selector)` shorthands for the standard DOM events, typed like
|
|
803
|
+
* `DOM.select(selector).events('<event>')`: `DOM.keydown('.x')` is a stream of KeyboardEvent,
|
|
804
|
+
* `DOM.click('.x')` of MouseEvent (PointerEvent in newer DOM typings), and so on.
|
|
197
805
|
*/
|
|
198
|
-
type
|
|
199
|
-
|
|
200
|
-
|
|
806
|
+
type DOMEventShorthands = {
|
|
807
|
+
[EVENT in Exclude<keyof HTMLElementEventMap, keyof MainDOMSource>]: DOMEventShorthand<HTMLElementEventMap[EVENT]>
|
|
808
|
+
}
|
|
201
809
|
|
|
202
810
|
/**
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
* signatures, so a structural fallback check is needed).
|
|
811
|
+
* One `DOM.<event>` shorthand: a selector string gives a stream of the event; a control gives
|
|
812
|
+
* the event with `currentTarget` (and `ownerTarget`) typed as the control's element.
|
|
206
813
|
*/
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
? DRIVERS
|
|
212
|
-
: keyof DRIVERS extends never
|
|
213
|
-
? {}
|
|
214
|
-
: DRIVERS extends { [K in keyof DRIVERS]: { source: any; sink: any } }
|
|
215
|
-
? DRIVERS
|
|
216
|
-
: {}
|
|
217
|
-
|
|
218
|
-
type CombinedSources<STATE, DRIVERS> = Sources<DefaultDrivers<STATE> & DRIVERS> & { dispose$: Stream<boolean> }
|
|
219
|
-
|
|
220
|
-
interface ComponentIntent<STATE, DRIVERS, ACTIONS> {
|
|
221
|
-
(args: CombinedSources<STATE, DRIVERS>): Partial<IntentActions<ACTIONS>>
|
|
814
|
+
interface DOMEventShorthand<EVENT> {
|
|
815
|
+
<CONTROL extends AnyControl>(control: CONTROL): EnrichedEventStream<ControlEvent<EVENT, ControlElementOf<CONTROL>>>
|
|
816
|
+
// last, so ReturnType<SygnalDOMSource['click']> stays the selector form
|
|
817
|
+
(selector: string): EnrichedEventStream<EVENT>
|
|
222
818
|
}
|
|
223
819
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
820
|
+
/**
|
|
821
|
+
* `select()` overloads added to the DOM source: a control selects its element, typed;
|
|
822
|
+
* `'document'` / `'body'` give sources that also take a control; a selector string gives a
|
|
823
|
+
* source whose `select()` takes controls too.
|
|
824
|
+
*/
|
|
825
|
+
type ControlSelectOverlay = {
|
|
826
|
+
select<CONTROL extends AnyControl>(control: CONTROL): ControlDOMSource<ControlElementOf<CONTROL>>
|
|
827
|
+
select(selector: 'document'): SygnalDocumentDOMSource
|
|
828
|
+
select(selector: 'body'): SygnalBodyDOMSource
|
|
829
|
+
select(selector: string): SelectedDOMSource
|
|
830
|
+
}
|
|
227
831
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
: { [CALCULATED_KEY in keyof CALCULATED]: boolean | CalculatedFieldValue<STATE & CALCULATED, CALCULATED[CALCULATED_KEY]> }
|
|
832
|
+
/** What `DOM.select('<selector>')` returns: a MainDOMSource whose `select()` also takes controls. */
|
|
833
|
+
type SelectedDOMSource = ControlSelectOverlay & MainDOMSource
|
|
231
834
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
:
|
|
835
|
+
/** `DOM.select('document')`: document-level listeners, optionally filtered to a selector or control. */
|
|
836
|
+
type SygnalDocumentDOMSource = DocumentDOMSource & {
|
|
837
|
+
select(control: AnyControl): DocumentDOMSource
|
|
838
|
+
}
|
|
235
839
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
840
|
+
/** `DOM.select('body')`: body-level listeners, optionally filtered to a selector or control. */
|
|
841
|
+
type SygnalBodyDOMSource = BodyDOMSource & {
|
|
842
|
+
select(control: AnyControl): BodyDOMSource
|
|
239
843
|
}
|
|
240
844
|
|
|
241
|
-
|
|
845
|
+
/**
|
|
846
|
+
* `DOM.select(control)`: a DOM source scoped to a control's element. `events(name)` is typed
|
|
847
|
+
* like a selector's, with `currentTarget` typed as the control's element; `element()` and
|
|
848
|
+
* `elements()` give that element type.
|
|
849
|
+
*/
|
|
850
|
+
type ControlDOMSource<ELEMENT extends Element = Element> = ControlSelectOverlay & Omit<MainDOMSource, 'select' | 'events' | 'element' | 'elements'> & {
|
|
851
|
+
events<K extends keyof HTMLElementEventMap>(eventType: K, options?: EventsFnOptions, bubbles?: boolean): EnrichedEventStream<ControlEvent<HTMLElementEventMap[K], ELEMENT>>
|
|
852
|
+
events(eventType: string, options?: EventsFnOptions, bubbles?: boolean): EnrichedEventStream<ControlEvent<globalThis.Event, ELEMENT>>
|
|
853
|
+
element(): MemoryStream<ELEMENT>
|
|
854
|
+
elements(): MemoryStream<ELEMENT[]>
|
|
855
|
+
}
|
|
242
856
|
|
|
243
|
-
|
|
857
|
+
type SygnalDOMSource = ControlSelectOverlay & MainDOMSource & DOMEventShorthands & {
|
|
858
|
+
/** Any other event name (custom events): `DOM['my-event']('.x')`, `DOM['my-event'](Control)` */
|
|
859
|
+
[eventName: string]: DOMEventShorthand<globalThis.Event>
|
|
860
|
+
}
|
|
244
861
|
|
|
245
|
-
|
|
862
|
+
// ── Controls (PLAN-4 CT-1) ────────────────────────────────────────
|
|
246
863
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
864
|
+
/**
|
|
865
|
+
* The pragma's own createElement, passed to a spec's `vnode()` as `h` (D116):
|
|
866
|
+
* `h(tag, props, ...children)`. Spec authors build vnodes with it, never with an imported
|
|
867
|
+
* `createElement` (under the automatic JSX runtime that would bundle a second pragma).
|
|
868
|
+
*/
|
|
869
|
+
type ControlH = (tag: any, props?: Record<string, any> | null, ...children: unknown[]) => VNode
|
|
250
870
|
|
|
251
871
|
/**
|
|
252
|
-
*
|
|
872
|
+
* The spec-object form of a control spec (D101, amended D116): `controls({ DueDate: datePicker })`.
|
|
873
|
+
* `vnode(props, children, h)` returns the one element vnode the control renders (the pragma
|
|
874
|
+
* stamps `data-control` on it, keeping its key and hooks, and copies the props' `key` onto it
|
|
875
|
+
* when it has none). `commands` are looked up by element commands before native methods.
|
|
876
|
+
* `__props` is a phantom field (types only) that gives the control its props type.
|
|
253
877
|
*/
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
> = ComponentProps<STATE & CALCULATED, PROPS, CONTEXT> & {
|
|
263
|
-
label?: string;
|
|
264
|
-
DOMSourceName?: string;
|
|
265
|
-
stateSourceName?: string;
|
|
266
|
-
requestSourceName?: string;
|
|
267
|
-
model?: ComponentModel<STATE, PROPS, FixDrivers<DRIVERS>, ACTIONS, CALCULATED, SINK_RETURNS, CONTEXT>;
|
|
268
|
-
intent?: ComponentIntent<STATE & CALCULATED, FixDrivers<DRIVERS>, ACTIONS>;
|
|
269
|
-
initialState?: STATE;
|
|
270
|
-
calculated?: Calculated<STATE, CALCULATED>;
|
|
271
|
-
storeCalculatedInState?: boolean;
|
|
272
|
-
context?: Context<STATE & CALCULATED, CONTEXT>;
|
|
273
|
-
peers?: { [name: string]: Component };
|
|
274
|
-
components?: { [name: string]: Component };
|
|
275
|
-
onError?: (error: Error, info: { componentName: string }) => any;
|
|
276
|
-
debug?: boolean;
|
|
878
|
+
interface ControlSpecObject<P = any> {
|
|
879
|
+
/** Free-form kind ('widget' in PLAN-5), shown in inspect() and diagnostics */
|
|
880
|
+
kind: string;
|
|
881
|
+
/** Must return one element vnode (not a component, fragment or text), built with `h` */
|
|
882
|
+
vnode(props: P, children: unknown[], h: ControlH): VNode;
|
|
883
|
+
commands?: Record<string, (elm: Element, options: Record<string, unknown>) => void>;
|
|
884
|
+
/** Phantom, types only: the control's props */
|
|
885
|
+
__props?: P;
|
|
277
886
|
}
|
|
278
887
|
|
|
279
888
|
/**
|
|
280
|
-
*
|
|
889
|
+
* A control spec (frozen contract D101): an intrinsic tag name (`'button'`, `'input'`,
|
|
890
|
+
* `'wa-rating'`) or a spec object `{ kind, vnode(props, children, h), commands?, __props? }`.
|
|
281
891
|
*/
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
CONTEXT = {},
|
|
288
|
-
SINK_RETURNS extends NonStateSinkReturns = {}
|
|
289
|
-
> = Component<STATE, any, DRIVERS, ACTIONS, CALCULATED, CONTEXT, SINK_RETURNS>
|
|
892
|
+
type ControlSpec<P = any> =
|
|
893
|
+
| keyof JSX.IntrinsicElements
|
|
894
|
+
| ControlSpecObject<P>
|
|
895
|
+
|
|
896
|
+
declare const CONTROL: unique symbol
|
|
290
897
|
|
|
291
898
|
/**
|
|
292
|
-
* A
|
|
293
|
-
*
|
|
899
|
+
* A control (`controls({ Add: 'button' }).Add`): a JSX tag that renders its element with the
|
|
900
|
+
* props passed plus `data-control="<KEY>"`. Not a component (no state, intent or isolation).
|
|
901
|
+
* Anywhere a selector is accepted (`DOM.select`, `DOM.<event>`, `simulateEvent`, `query`,
|
|
902
|
+
* `queryAll`), a control selects its element; in a template string it is its selector:
|
|
903
|
+
* `` `li ${Add}` `` is `li [data-control="Add"]`.
|
|
904
|
+
*
|
|
905
|
+
* KEY is the control's name, ELEMENT the element type (`HTMLButtonElement` for 'button';
|
|
906
|
+
* `Element` for a spec object), PROPS its JSX props.
|
|
294
907
|
*/
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
908
|
+
interface Control<KEY extends string = string, ELEMENT extends Element = Element, PROPS = any> {
|
|
909
|
+
(props: PROPS): JSX.Element;
|
|
910
|
+
/** The control's selector: `[data-control="<KEY>"]` */
|
|
911
|
+
toString(): `[data-control="${KEY}"]`;
|
|
912
|
+
/** 'element' for a tag spec, else the spec object's `kind` */
|
|
913
|
+
readonly kind: string;
|
|
914
|
+
/** The spec the control was made from */
|
|
915
|
+
readonly spec: ControlSpec;
|
|
916
|
+
/** Phantom, types only */
|
|
917
|
+
readonly [CONTROL]: { key: KEY; element: ELEMENT; props: PROPS };
|
|
918
|
+
}
|
|
303
919
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
920
|
+
/** Any control, whatever its key, element and props. */
|
|
921
|
+
type AnyControl = Control<string, any, any>
|
|
922
|
+
|
|
923
|
+
/** The element type a control (or a control spec) renders. */
|
|
924
|
+
type ControlElementOf<C> =
|
|
925
|
+
C extends { readonly [CONTROL]: { element: infer ELEMENT } } ? ELEMENT
|
|
926
|
+
: C extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[C]
|
|
927
|
+
: C extends keyof SVGElementTagNameMap ? SVGElementTagNameMap[C]
|
|
928
|
+
: C extends string ? HTMLElement
|
|
929
|
+
: Element
|
|
930
|
+
|
|
931
|
+
/** An event from a control's listener: `currentTarget` / `ownerTarget` are the control's element. */
|
|
932
|
+
type ControlEvent<EVENT, ELEMENT extends Element = Element> = EVENT & {
|
|
933
|
+
readonly currentTarget: ELEMENT;
|
|
934
|
+
readonly ownerTarget: ELEMENT;
|
|
935
|
+
}
|
|
309
936
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
937
|
+
type IfEqual<X, Y, A, B> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? A : B
|
|
938
|
+
|
|
939
|
+
/** Keys of T that are not readonly. */
|
|
940
|
+
type WritableKeys<T> = {
|
|
941
|
+
[KEY in keyof T]-?: IfEqual<{ [Q in KEY]: T[KEY] }, { -readonly [Q in KEY]: T[KEY] }, KEY, never>
|
|
942
|
+
}[keyof T]
|
|
943
|
+
|
|
944
|
+
/** Writable, non-function, non-constant properties of an element: its settable DOM props. */
|
|
945
|
+
type SettableElementProps<ELEMENT> = {
|
|
946
|
+
[KEY in WritableKeys<ELEMENT> as KEY extends string
|
|
947
|
+
? KEY extends Uppercase<KEY> ? never
|
|
948
|
+
: NonNullable<ELEMENT[KEY]> extends (...args: any[]) => any ? never
|
|
949
|
+
: KEY
|
|
950
|
+
: never
|
|
951
|
+
]?: KEY extends 'value' ? ELEMENT[KEY] | number : ELEMENT[KEY]
|
|
313
952
|
}
|
|
314
953
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
appear?: boolean;
|
|
954
|
+
/** JSX props every control takes, besides its element's own props (or the spec's props). */
|
|
955
|
+
type ControlCommonProps<ELEMENT = Element> = {
|
|
956
|
+
key?: string | number;
|
|
319
957
|
children?: any;
|
|
958
|
+
ref?: Ref<any> | ((element: ELEMENT | null) => void);
|
|
320
959
|
}
|
|
321
960
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
961
|
+
/**
|
|
962
|
+
* JSX props of an intrinsic element as rendered by Sygnal: the element's settable DOM
|
|
963
|
+
* properties (no event handlers: listen in the intent), plus `class`, `style`, `attrs`,
|
|
964
|
+
* `props`, `hook`, `for`, `tabindex`, `autoFocus` / `autoSelect`. `data-*` / `aria-*` are
|
|
965
|
+
* accepted as hyphenated attributes. Custom elements (`'wa-rating'`) and SVG elements take any prop.
|
|
966
|
+
*/
|
|
967
|
+
type IntrinsicControlProps<TAG extends string> =
|
|
968
|
+
ControlCommonProps<ControlElementOf<TAG>> & {
|
|
969
|
+
class?: string | ReadonlyArray<unknown> | Record<string, boolean | null | undefined>;
|
|
970
|
+
style?: string | Record<string, any>;
|
|
971
|
+
attrs?: Record<string, any>;
|
|
972
|
+
props?: Record<string, any>;
|
|
973
|
+
hook?: Record<string, (...args: any[]) => any>;
|
|
974
|
+
for?: string;
|
|
975
|
+
tabindex?: number | string;
|
|
976
|
+
/** Focus the element when it enters the DOM */
|
|
977
|
+
autoFocus?: boolean;
|
|
978
|
+
/** Select the element's text after focusing (input/textarea) */
|
|
979
|
+
autoSelect?: boolean;
|
|
980
|
+
} & (TAG extends keyof HTMLElementTagNameMap
|
|
981
|
+
? Omit<SettableElementProps<HTMLElementTagNameMap[TAG]>, 'style'>
|
|
982
|
+
: { [prop: string]: any })
|
|
983
|
+
|
|
984
|
+
/** The JSX props of a control made from a spec: a tag's IntrinsicControlProps, or a spec object's P. */
|
|
985
|
+
type ControlPropsOf<SPEC> =
|
|
986
|
+
SPEC extends string ? IntrinsicControlProps<SPEC>
|
|
987
|
+
: SPEC extends { __props?: infer P }
|
|
988
|
+
? unknown extends P
|
|
989
|
+
? (SPEC extends { vnode(props: infer VP, ...rest: any[]): any } ? VP : any) & ControlCommonProps
|
|
990
|
+
: P & ControlCommonProps
|
|
991
|
+
: any
|
|
992
|
+
|
|
993
|
+
/** The controls `controls(spec)` returns: one per key, typed from its spec. */
|
|
994
|
+
type ControlsOf<SPECS> = {
|
|
995
|
+
[KEY in keyof SPECS & string]: Control<
|
|
996
|
+
KEY,
|
|
997
|
+
SPECS[KEY] extends string ? ControlElementOf<SPECS[KEY]> : Element,
|
|
998
|
+
ControlPropsOf<SPECS[KEY]>
|
|
999
|
+
>
|
|
325
1000
|
}
|
|
326
1001
|
|
|
327
|
-
|
|
1002
|
+
/**
|
|
1003
|
+
* Element tokens for linking a view to its intent by identifier (CT-1):
|
|
1004
|
+
*
|
|
1005
|
+
* const { Draft, Add } = controls({ Draft: 'input', Add: 'button' })
|
|
1006
|
+
* <Draft className="field" value={state.draft} /><Add>Add</Add>
|
|
1007
|
+
* AddTodo.intent = ({ DOM }) => ({ DRAFT: DOM.input(Draft).value(), ADD: DOM.click(Add) })
|
|
1008
|
+
*
|
|
1009
|
+
* Each key becomes a control that renders its spec (a tag name, or a spec object) with
|
|
1010
|
+
* `data-control="<Key>"`. The keys are the names, so keep them unique in a file.
|
|
1011
|
+
*/
|
|
1012
|
+
declare function controls<const SPECS extends Record<string, ControlSpec>>(spec: SPECS): ControlsOf<SPECS>
|
|
328
1013
|
|
|
329
|
-
|
|
330
|
-
mountPoint?: string;
|
|
331
|
-
fragments?: boolean;
|
|
332
|
-
useDefaultDrivers?: boolean;
|
|
333
|
-
}
|
|
1014
|
+
// ── Widgets (PLAN-5 W-1) ───────────────────────────────────────────
|
|
334
1015
|
|
|
335
|
-
|
|
336
|
-
|
|
1016
|
+
/**
|
|
1017
|
+
* Props a widget tag puts on its host element (they reach the widget's `mount`/`update` too,
|
|
1018
|
+
* except `key` and `ref`): id, className/class, style, title, name, placeholder, role, tabindex,
|
|
1019
|
+
* hidden, lang, dir, attrs, aria-*, data-* (and the definition's `hostProps`). `ref` gets the
|
|
1020
|
+
* host element (the widget's own API is reached through its `commands`).
|
|
1021
|
+
*/
|
|
1022
|
+
type WidgetHostProps = {
|
|
1023
|
+
key?: string | number;
|
|
1024
|
+
ref?: { current: Element | null } | ((el: Element | null) => void);
|
|
1025
|
+
id?: string;
|
|
1026
|
+
className?: string;
|
|
1027
|
+
class?: string | ReadonlyArray<unknown> | Record<string, boolean | null | undefined>;
|
|
1028
|
+
style?: string | Record<string, any>;
|
|
1029
|
+
title?: string;
|
|
1030
|
+
name?: string;
|
|
1031
|
+
placeholder?: string;
|
|
1032
|
+
role?: string;
|
|
1033
|
+
tabindex?: number | string;
|
|
1034
|
+
tabIndex?: number;
|
|
1035
|
+
hidden?: boolean;
|
|
1036
|
+
lang?: string;
|
|
1037
|
+
dir?: string;
|
|
1038
|
+
attrs?: Record<string, any>;
|
|
1039
|
+
[aria: `aria-${string}`]: any;
|
|
1040
|
+
[data: `data-${string}`]: any;
|
|
337
1041
|
}
|
|
338
1042
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
| Array<COMPONENT | { default: COMPONENT } | null | undefined>
|
|
343
|
-
| null
|
|
344
|
-
| undefined
|
|
1043
|
+
/** The element type of a widget's host tag (`'input'` → `HTMLInputElement`). */
|
|
1044
|
+
type WidgetElementOf<TAG extends string> =
|
|
1045
|
+
TAG extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[TAG] : HTMLElement
|
|
345
1046
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
hmr: (newComponent?: AnyComponentModule<RootComponent<STATE, DRIVERS>>, state?: STATE) => void;
|
|
351
|
-
}
|
|
1047
|
+
/** A widget's `dispatch(name, detail)` (mount's third parameter): dispatches a bubbling `CustomEvent` named `name` on the host. */
|
|
1048
|
+
type WidgetDispatch<EV extends string = string> = (name: EV, detail?: unknown) => void
|
|
1049
|
+
/** @deprecated the earlier name of `WidgetDispatch` */
|
|
1050
|
+
type WidgetEmit<EV extends string = string> = WidgetDispatch<EV>
|
|
352
1051
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
1052
|
+
/** The object passed to `defineWidget()`. */
|
|
1053
|
+
interface WidgetDefinition<P = {}, I = unknown, EV extends string = string, TAG extends string = 'div'> {
|
|
1054
|
+
/** A name for diagnostics and inspect() (`'DatePicker'`); the host tag stands in without one */
|
|
1055
|
+
name?: string;
|
|
1056
|
+
/** The host element (default `'div'`). It has no children of its own: the widget owns its content. */
|
|
1057
|
+
tag?: TAG;
|
|
1058
|
+
/**
|
|
1059
|
+
* Called once the host is in the document (or on the first client patch after SSR) with the
|
|
1060
|
+
* props the view passed. Returns the instance that `update`, `unmount` and `commands` get.
|
|
1061
|
+
* `error(e)` reports a later failure the widget catches itself (a library's own re-render): it
|
|
1062
|
+
* is handled like a throwing `update` (SYG661, the owner's `onError` fallback in its place).
|
|
1063
|
+
*/
|
|
1064
|
+
mount(el: WidgetElementOf<TAG>, props: P, dispatch: WidgetDispatch<EV>, error: (e: unknown) => void): I;
|
|
1065
|
+
/** Called with the newest props when they change (shallow compare; `style`/`attrs` objects by their entries). Without it, a change remounts. */
|
|
1066
|
+
update?(instance: I, props: P, el: WidgetElementOf<TAG>): void;
|
|
1067
|
+
/** Called when the host leaves the DOM. */
|
|
1068
|
+
unmount?(instance: I, el: WidgetElementOf<TAG>): void;
|
|
1069
|
+
/** The events `dispatch` sends (bubbling CustomEvents on the host; read them with `.detail()`) */
|
|
1070
|
+
events?: readonly EV[];
|
|
1071
|
+
/**
|
|
1072
|
+
* Element commands (`ELEMENT: { open: '.due' }` or `{ open: Due }`), called with the instance.
|
|
1073
|
+
* A command wins over a native method of the same name (`focus`, `close`, `togglePopover`: it
|
|
1074
|
+
* gets the options object). Register the names in `ElementCommandRegistry` to type the commands.
|
|
1075
|
+
*/
|
|
1076
|
+
commands?: Record<string, (instance: I, options: Record<string, any>, el: WidgetElementOf<TAG>) => unknown>;
|
|
1077
|
+
/** What SSR renders inside the host until the client mounts (not for void hosts like `input`) */
|
|
1078
|
+
fallback?: VNode | string | ((props: P, h: ControlH) => VNode | string);
|
|
1079
|
+
/** More prop names to put on the host element as well */
|
|
1080
|
+
hostProps?: readonly string[];
|
|
1081
|
+
/** Prop names that stay off the host (the widget applies them itself, e.g. `aria-label` on its own control) */
|
|
1082
|
+
ownProps?: readonly string[];
|
|
356
1083
|
}
|
|
357
1084
|
|
|
358
|
-
|
|
359
|
-
|
|
1085
|
+
/**
|
|
1086
|
+
* A widget (`defineWidget()`): a JSX tag that renders its host element, selected like any
|
|
1087
|
+
* element (`className`, `DOM.select('.due').events('pick').detail()`), and also a control spec
|
|
1088
|
+
* (`controls({ Due: DatePicker })`, the alternative form).
|
|
1089
|
+
*/
|
|
1090
|
+
interface Widget<P = {}, I = unknown, EV extends string = string, TAG extends string = string> extends ControlSpecObject<P & WidgetHostProps> {
|
|
1091
|
+
(props: P & WidgetHostProps): JSX.Element;
|
|
1092
|
+
readonly kind: 'widget';
|
|
1093
|
+
/** The declared events */
|
|
1094
|
+
readonly events: readonly EV[];
|
|
1095
|
+
/** The control-spec commands, `(hostElement, options)` (D102) */
|
|
1096
|
+
readonly commands: Record<string, (elm: Element, options: Record<string, unknown>) => void>;
|
|
1097
|
+
readonly def: WidgetDefinition<P, I, EV, any>;
|
|
1098
|
+
/** Phantom, types only */
|
|
1099
|
+
__props?: P & WidgetHostProps;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
/**
|
|
1103
|
+
* Wraps a framework-agnostic widget (a date picker, a chart, an editor) as a JSX tag:
|
|
1104
|
+
*
|
|
1105
|
+
* const DatePicker = defineWidget({
|
|
1106
|
+
* tag: 'input',
|
|
1107
|
+
* mount: (el, props: { value?: Date }, dispatch) =>
|
|
1108
|
+
* flatpickr(el, { defaultDate: props.value, onChange: ([d]) => dispatch('pick', d) }),
|
|
1109
|
+
* update: (fp, props) => fp.setDate(props.value ?? '', false),
|
|
1110
|
+
* unmount: (fp) => fp.destroy(),
|
|
1111
|
+
* events: ['pick'],
|
|
1112
|
+
* commands: { open: (fp) => fp.open() },
|
|
1113
|
+
* })
|
|
1114
|
+
* // view: <label>Due <DatePicker className="due" value={state.due} /></label>
|
|
1115
|
+
* // intent: DUE: DOM.select('.due').events('pick').detail()
|
|
1116
|
+
* // model: OPEN: { ELEMENT: { open: '.due' } }
|
|
1117
|
+
*
|
|
1118
|
+
* The host keeps its widget instance across renders and keyed moves; `update` gets the newest
|
|
1119
|
+
* props. A `mount`/`update` that throws is reported to `onError` with phase `'widget'` and the
|
|
1120
|
+
* owner's `onError` fallback renders in its place.
|
|
1121
|
+
*/
|
|
1122
|
+
declare function defineWidget<P = {}, I = unknown, const EV extends string = string, const TAG extends string = 'div'>(definition: WidgetDefinition<P, I, EV, TAG>): Widget<P, I, EV, TAG>
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* The target of an element command: a control, or a selector. It is looked up in the view of the
|
|
1126
|
+
* component instance that sends the command (a child's elements are isolated from its parent; a
|
|
1127
|
+
* Collection item reaches only its own).
|
|
1128
|
+
*/
|
|
1129
|
+
type ElementTarget = AnyControl | string
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* Element commands beyond the built-in ones, by method name → options, for a control spec's
|
|
1133
|
+
* `commands` (D102) or another method of the element. Augment it (the first key of a command is
|
|
1134
|
+
* the method, the rest are the options):
|
|
1135
|
+
*
|
|
1136
|
+
* declare module 'sygnal' { interface ElementCommandRegistry { open: { at?: number }; play: {} } }
|
|
1137
|
+
* // ELEMENT: { open: DueDate, at: 3 }
|
|
1138
|
+
*/
|
|
1139
|
+
interface ElementCommandRegistry {}
|
|
1140
|
+
|
|
1141
|
+
type RegisteredElementCommand = {
|
|
1142
|
+
[METHOD in keyof ElementCommandRegistry & string]: { [K in METHOD]: ElementTarget } & ElementCommandRegistry[METHOD]
|
|
1143
|
+
}[keyof ElementCommandRegistry & string]
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* One element command (the built-in `ELEMENT` sink, PLAN-4 GS-2): `{ <method>: target, ...options }`.
|
|
1147
|
+
* The FIRST key is the method, the others are its options; `close` passes `returnValue` as its
|
|
1148
|
+
* argument. A control whose spec declares `commands` is asked first (`{ open: DueDate }`), then
|
|
1149
|
+
* the element's own method runs, after the next render reaches the page. Register other method
|
|
1150
|
+
* names (a spec's commands, `play`, `reset`...) in `ElementCommandRegistry`.
|
|
1151
|
+
*/
|
|
1152
|
+
type ElementCommand =
|
|
1153
|
+
| { focus: ElementTarget | WithinTarget; preventScroll?: boolean; focusVisible?: boolean }
|
|
1154
|
+
| { blur: ElementTarget }
|
|
1155
|
+
| { select: ElementTarget }
|
|
1156
|
+
| { click: ElementTarget }
|
|
1157
|
+
| { scrollIntoView: ElementTarget; block?: ScrollLogicalPosition; inline?: ScrollLogicalPosition; behavior?: ScrollBehavior }
|
|
1158
|
+
| { showModal: ElementTarget }
|
|
1159
|
+
| { show: ElementTarget }
|
|
1160
|
+
| { close: ElementTarget; returnValue?: string }
|
|
1161
|
+
| { showPopover: ElementTarget }
|
|
1162
|
+
| { hidePopover: ElementTarget }
|
|
1163
|
+
| { togglePopover: ElementTarget; force?: boolean }
|
|
1164
|
+
/** A `<VirtualCollection>`'s container: scroll to the row at `index` (of the filtered, sorted rows) */
|
|
1165
|
+
| { scrollToIndex: ElementTarget; index: number; align?: ScrollToAlign; behavior?: 'auto' | 'smooth' }
|
|
1166
|
+
/** A `<VirtualCollection>`'s container: scroll to the row whose item has this `id` */
|
|
1167
|
+
| { scrollToId: ElementTarget; id: string | number; align?: ScrollToAlign; behavior?: 'auto' | 'smooth' }
|
|
1168
|
+
| RegisteredElementCommand
|
|
1169
|
+
|
|
1170
|
+
/** A `focusWithin(selector)` target (D194): `{ focus: focusWithin('.title') }`. */
|
|
1171
|
+
interface WithinTarget {
|
|
1172
|
+
/** The selector, searched under the sender's root element (children included). */
|
|
1173
|
+
readonly within: string
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/**
|
|
1177
|
+
* An ELEMENT target that reaches inside the sender's children (PLAN-5 D194): `{ focus:
|
|
1178
|
+
* focusWithin(selector), ...options }` focuses the first element under the sender's root element
|
|
1179
|
+
* that matches `selector`, a child component's or a Collection item's included (a plain `{ focus:
|
|
1180
|
+
* '.x' }` stays in the sender's own isolated scope). It runs after the next patch, so an item the
|
|
1181
|
+
* same action adds is there. No match: nothing happens.
|
|
1182
|
+
*
|
|
1183
|
+
* ADD: { STATE: addRow, ELEMENT: (s) => ({ focus: focusWithin(`[data-id="${s.next}"] .title`) }) }
|
|
1184
|
+
*/
|
|
1185
|
+
declare function focusWithin(selector: string): WithinTarget
|
|
1186
|
+
|
|
1187
|
+
/** What the `ELEMENT` sink takes: one command or several (run in order). */
|
|
1188
|
+
type ElementCommands = ElementCommand | readonly ElementCommand[]
|
|
1189
|
+
|
|
1190
|
+
// ── Behaviors (PLAN-4 GS-1) ────────────────────────────────────────
|
|
1191
|
+
|
|
1192
|
+
/**
|
|
1193
|
+
* What a behavior's intent receives: the host component's sources (DOM is the host's isolated
|
|
1194
|
+
* DOM source; CHILD, EVENTS, drivers as they are), with STATE lensed to the behavior's slice.
|
|
1195
|
+
*/
|
|
1196
|
+
type BehaviorSources<SLICE = any> = IntentSources<SLICE> & { [source: string]: any }
|
|
1197
|
+
|
|
1198
|
+
/** A behavior model handler's 4th argument: the host's props, context, uid and its whole state. */
|
|
1199
|
+
type BehaviorProps = ReducerExtras<Record<string, any>, any> & { state: any; signal?: AbortSignal }
|
|
1200
|
+
|
|
1201
|
+
/**
|
|
1202
|
+
* A behavior model handler: `(slice, data, next, props, options, key)`. `options` are the use's
|
|
1203
|
+
* options (`pager({ pageSize: 10 })`), `key` its key in `uses` (`'pager'`), e.g. to name a reply
|
|
1204
|
+
* action `key + '.LOADED'` (D197).
|
|
1205
|
+
*/
|
|
1206
|
+
type BehaviorHandler<SLICE = any, OPTIONS = any, RETURN = any> = (
|
|
1207
|
+
slice: SLICE,
|
|
1208
|
+
data: any,
|
|
1209
|
+
next: NextFunction<any>,
|
|
1210
|
+
props: BehaviorProps,
|
|
1211
|
+
options: OPTIONS,
|
|
1212
|
+
key: string
|
|
1213
|
+
) => RETURN | ABORT | undefined
|
|
1214
|
+
|
|
1215
|
+
/** One behavior model entry: a slice reducer, or an object of sinks (STATE, HOST, EFFECT, ELEMENT, EVENTS, drivers). */
|
|
1216
|
+
type BehaviorModelEntry<SLICE = any, OPTIONS = any> =
|
|
1217
|
+
| BehaviorHandler<SLICE, OPTIONS, SLICE>
|
|
1218
|
+
| ABORT
|
|
1219
|
+
| {
|
|
1220
|
+
/** A reducer on the slice: ABORT, or the slice itself, means no change. */
|
|
1221
|
+
STATE?: BehaviorHandler<SLICE, OPTIONS, SLICE> | ABORT;
|
|
1222
|
+
/**
|
|
1223
|
+
* D197: a reducer on the host's WHOLE state (for a behavior that edits a host field, e.g.
|
|
1224
|
+
* reorders `state[options.from]`); return the new host state. ABORT, or the same state,
|
|
1225
|
+
* means no change. Use it instead of STATE in an entry.
|
|
1226
|
+
*/
|
|
1227
|
+
HOST?: (state: any, data: any, next: NextFunction<any>, props: BehaviorProps, options: OPTIONS, key: string) => any;
|
|
1228
|
+
EFFECT?: (slice: SLICE, data: any, next: NextFunction<any>, props: BehaviorProps, options: OPTIONS, key: string) => void | Promise<void>;
|
|
1229
|
+
ELEMENT?: BehaviorHandler<SLICE, OPTIONS, ElementCommands> | ElementCommands;
|
|
1230
|
+
[sink: string]: BehaviorHandler<SLICE, OPTIONS, any> | string | number | boolean | object | null | undefined;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/** The object passed to `defineBehavior()`. Reducers and sinks get the slice, not the host state (HOST: the host state). */
|
|
1234
|
+
interface BehaviorDefinition<SLICE = any, ACTIONS = {}, CALCULATED = {}, OPTIONS = Record<string, any>> {
|
|
1235
|
+
/** The slice a host starts with (`state[key]`); options naming one of its keys override it. */
|
|
1236
|
+
initialState: SLICE;
|
|
1237
|
+
/** Actions named without the key (`NEXT`); the host sees them as `'<key>.NEXT'`. `key`: the use's key in `uses`. */
|
|
1238
|
+
intent?: (sources: BehaviorSources<SLICE>, options: OPTIONS, key: string) => { [ACTION in keyof ACTIONS]: Stream<ACTIONS[ACTION]> };
|
|
1239
|
+
/**
|
|
1240
|
+
* Model entries on the slice: ABORT, or the slice itself, means no change. `next('X')` names
|
|
1241
|
+
* the behavior's own actions. Handlers get `(slice, data, next, props, options, key)`; a
|
|
1242
|
+
* `HOST` entry gets the host's whole state. (The slice's calculated fields are there at
|
|
1243
|
+
* runtime, but typed only on the host's state: TypeScript can't infer them while typing these
|
|
1244
|
+
* functions.)
|
|
1245
|
+
*/
|
|
1246
|
+
model?: { [action: string]: BehaviorModelEntry<SLICE, OPTIONS> };
|
|
1247
|
+
/** Fields of the slice (`state.pager.offset`), stored on it and recomputed when it changes. */
|
|
1248
|
+
calculated?: { [FIELD in keyof CALCULATED]: (slice: SLICE) => CALCULATED[FIELD] };
|
|
1249
|
+
/**
|
|
1250
|
+
* D197: timers for the host (`makeTimerDriver()`), from the slice: `{ name: spec }`, as the
|
|
1251
|
+
* `timers` static. The host sees them as `'<key>.<name>'`; a spec's `action` (or `frame`)
|
|
1252
|
+
* naming one of the behavior's actions is namespaced (`'SHOW'` → `'<key>.SHOW'`), any other
|
|
1253
|
+
* name is sent to the host as written. They join the host's own `timers`.
|
|
1254
|
+
*/
|
|
1255
|
+
timers?: (slice: SLICE, options: OPTIONS, key: string) => Timers;
|
|
1256
|
+
/** `false`: the slice is UI state (sortable's drag): a root's `persist()` neither saves nor restores it (the root's own `uses` only: a sub-component's or Collection item's slice is saved with its data) */
|
|
1257
|
+
persist?: false;
|
|
1258
|
+
/**
|
|
1259
|
+
* The actions that complete one undoable step (sortable: `['DROPPED']`). With `undo()` on the
|
|
1260
|
+
* same host, the behavior's other actions are steps of a gesture: their changes to the undo
|
|
1261
|
+
* key aren't recorded; the completing action records the value from before the gesture as
|
|
1262
|
+
* one entry (none for a gesture that ends without it, or whose steps bring the value back: the
|
|
1263
|
+
* same value, or an array / plain object with the same entries by identity), whatever the
|
|
1264
|
+
* `uses` order. A recorded change, UNDO or REDO mid-gesture first records the value from
|
|
1265
|
+
* before it; `track` / `coalesce` naming any of the behavior's actions cover its steps (a
|
|
1266
|
+
* `track` naming none of them: the gesture is never recorded; drops join only when `coalesce`
|
|
1267
|
+
* names one).
|
|
1268
|
+
*/
|
|
1269
|
+
undoStep?: string[];
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/** One use of a behavior (a `defineBehavior()` factory's result), for a component's `uses`. */
|
|
1273
|
+
interface Behavior<SLICE = any, ACTIONS = any, CALCULATED = {}, OPTIONS = any> {
|
|
1274
|
+
readonly initialState: SLICE;
|
|
1275
|
+
readonly options: OPTIONS;
|
|
1276
|
+
/** The slice a host starts with: initialState, the options naming its keys, the calculated fields. */
|
|
1277
|
+
readonly state: SLICE & CALCULATED;
|
|
1278
|
+
/** Merges the behavior into a component instance under `key`; called by the core for each `uses` entry. */
|
|
1279
|
+
merge(component: any, key: string): void;
|
|
1280
|
+
/** Phantom, types only */
|
|
1281
|
+
readonly __behavior?: { actions: ACTIONS };
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/** What `defineBehavior()` returns: call it with the options of one use. */
|
|
1285
|
+
type BehaviorFactory<SLICE = any, ACTIONS = {}, CALCULATED = {}, OPTIONS = Record<string, any>> =
|
|
1286
|
+
(options?: OPTIONS & Partial<SLICE>) => Behavior<SLICE, ACTIONS, CALCULATED, OPTIONS>
|
|
1287
|
+
|
|
1288
|
+
/**
|
|
1289
|
+
* A reusable piece of state, intent and model (GS-1). A host uses it under a key:
|
|
1290
|
+
*
|
|
1291
|
+
* const pager = defineBehavior({
|
|
1292
|
+
* initialState: { page: 0, pageSize: 20 },
|
|
1293
|
+
* intent: ({ DOM }, { next, prev }) => ({ NEXT: DOM.click(next), PREV: DOM.click(prev) }),
|
|
1294
|
+
* model: { NEXT: (p) => ({ ...p, page: p.page + 1 }), PREV: (p) => (p.page === 0 ? ABORT : { ...p, page: p.page - 1 }) },
|
|
1295
|
+
* calculated: { offset: (p) => p.page * p.pageSize },
|
|
1296
|
+
* })
|
|
1297
|
+
* TaskList.uses = { pager: pager({ pageSize: 10, next: Newer, prev: Older }) } // state.pager, 'pager.NEXT'
|
|
1298
|
+
*
|
|
1299
|
+
* A host model entry for a behavior action ('pager.NEXT') runs after the behavior's: its STATE
|
|
1300
|
+
* reducer gets the full state with the behavior's update, its EFFECT runs too, and its value
|
|
1301
|
+
* sinks (EVENTS, PARENT, drivers) replace the behavior's. A host intent action of the same name
|
|
1302
|
+
* replaces the behavior's trigger.
|
|
1303
|
+
*/
|
|
1304
|
+
declare function defineBehavior<SLICE extends Record<string, any>, ACTIONS = {}, CALCULATED = {}, OPTIONS = Record<string, any>>(
|
|
1305
|
+
definition: BehaviorDefinition<SLICE, ACTIONS, CALCULATED, OPTIONS>
|
|
1306
|
+
): BehaviorFactory<SLICE, ACTIONS, CALCULATED, OPTIONS>
|
|
1307
|
+
|
|
1308
|
+
/** The slice a behavior gives its host (initialState & calculated fields). */
|
|
1309
|
+
type BehaviorState<BEHAVIOR> = BEHAVIOR extends Behavior<infer SLICE, any, infer CALCULATED, any> ? SLICE & CALCULATED : never
|
|
1310
|
+
|
|
1311
|
+
/** The state keys a `uses` object adds: `type State = { tasks: Task[] } & UsesState<typeof uses>`. */
|
|
1312
|
+
type UsesState<USES> = { [KEY in keyof USES]: BehaviorState<USES[KEY]> }
|
|
1313
|
+
|
|
1314
|
+
type UsesActionEntries<USES> = {
|
|
1315
|
+
[KEY in keyof USES & string]: USES[KEY] extends Behavior<any, infer ACTIONS, any, any>
|
|
1316
|
+
? { [ACTION in keyof ACTIONS & string]: [`${KEY}.${ACTION}`, ACTIONS[ACTION]] }[keyof ACTIONS & string]
|
|
1317
|
+
: never
|
|
1318
|
+
}[keyof USES & string]
|
|
1319
|
+
|
|
1320
|
+
/** The namespaced actions a `uses` object adds ('pager.NEXT'), for a typed ACTIONS map. */
|
|
1321
|
+
type UsesActions<USES> = { [ENTRY in UsesActionEntries<USES> as ENTRY[0]]: ENTRY[1] }
|
|
1322
|
+
|
|
1323
|
+
// ── First-party behaviors (PLAN-4 GS-1, GS-8) ──────────────────────
|
|
1324
|
+
|
|
1325
|
+
/** Where a behavior option takes something to listen to: a control or a CSS selector. */
|
|
1326
|
+
type BehaviorTarget = AnyControl | string
|
|
1327
|
+
|
|
1328
|
+
/** A `pager` slice: `state.pager`. */
|
|
1329
|
+
interface PagerState { page: number; pageSize: number; total: number | null }
|
|
1330
|
+
/** A `pager` slice's calculated fields. `pages` is null while `total` is unknown. */
|
|
1331
|
+
interface PagerCalculated { offset: number; pages: number | null; hasPrev: boolean; hasNext: boolean }
|
|
1332
|
+
interface PagerOptions {
|
|
1333
|
+
/** Items per page (default 20) */
|
|
1334
|
+
pageSize?: number;
|
|
1335
|
+
/** Starting page, from 0 (default 0) */
|
|
1336
|
+
page?: number;
|
|
1337
|
+
/** Item count; null (default) = unknown, NEXT has no upper bound */
|
|
1338
|
+
total?: number | null;
|
|
1339
|
+
/** Its clicks dispatch NEXT */
|
|
1340
|
+
next?: BehaviorTarget;
|
|
1341
|
+
/** Its clicks dispatch PREV */
|
|
1342
|
+
prev?: BehaviorTarget;
|
|
1343
|
+
}
|
|
1344
|
+
interface PagerActions { NEXT: any; PREV: any; GOTO: number; SET_TOTAL: number }
|
|
1345
|
+
|
|
1346
|
+
/**
|
|
1347
|
+
* A page cursor (GS-1): `List.uses = { pager: pager({ pageSize: 10, total, next: Newer, prev: Older }) }`
|
|
1348
|
+
* gives `state.pager = { page, pageSize, total, offset, pages, hasPrev, hasNext }` and the actions
|
|
1349
|
+
* 'pager.NEXT' / 'pager.PREV' (no change at the bounds), 'pager.GOTO' (a page number, clamped)
|
|
1350
|
+
* and 'pager.SET_TOTAL' (the item count).
|
|
1351
|
+
*/
|
|
1352
|
+
declare function pager(options?: PagerOptions): Behavior<PagerState, PagerActions, PagerCalculated, PagerOptions>
|
|
1353
|
+
|
|
1354
|
+
// ── Forms (PLAN-5 F-1, D193) ───────────────────────────────────────
|
|
1355
|
+
|
|
1356
|
+
/** Field errors by field name (`'email'`, `'addresses.7.city'`; `''` = form-level). */
|
|
1357
|
+
type FieldErrors = Record<string, string>
|
|
1358
|
+
|
|
1359
|
+
/** One field of a `form` slice: `state.form.fields.email`, `state.form.fields['addresses.7.city']`. */
|
|
1360
|
+
interface FormField {
|
|
1361
|
+
/** The field name, for `name={f.name}` (the path in `values`; rows of an array by id) */
|
|
1362
|
+
name: string;
|
|
1363
|
+
value: any;
|
|
1364
|
+
/** What to show: a server or check error at once, a schema error once the field is touched (per `show`) or after a submit; '' when none */
|
|
1365
|
+
error: string;
|
|
1366
|
+
/** `!!error`, for `aria-invalid={f.email.invalid}` */
|
|
1367
|
+
invalid: boolean;
|
|
1368
|
+
touched: boolean;
|
|
1369
|
+
/** The value differs from `initial` */
|
|
1370
|
+
dirty: boolean;
|
|
1371
|
+
/** Its async `check` is running */
|
|
1372
|
+
pending: boolean;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/** A `form` slice's stored fields: `state.form`. */
|
|
1376
|
+
interface FormState<V = any> {
|
|
1377
|
+
values: V;
|
|
1378
|
+
/** The values the form started with (or was last saved / reset with) */
|
|
1379
|
+
initial: V;
|
|
1380
|
+
/** Every current schema error by field name, shown or not */
|
|
1381
|
+
errors: FieldErrors;
|
|
1382
|
+
touched: Record<string, boolean>;
|
|
1383
|
+
/** Server errors (`form.ERRORS`) */
|
|
1384
|
+
server: FieldErrors;
|
|
1385
|
+
/** Async check results by field name ('' = passed) */
|
|
1386
|
+
remote: FieldErrors;
|
|
1387
|
+
/** Field name → the value its check is running for */
|
|
1388
|
+
pending: Record<string, any>;
|
|
1389
|
+
/** A valid submit sent its request (the submit entry has the `http` sink) and has no `form.DONE` / `form.ERRORS` yet; a submit that sends nothing is done at once */
|
|
1390
|
+
submitting: boolean;
|
|
1391
|
+
/** `form.DONE` arrived, or a submit that sends nothing was dispatched */
|
|
1392
|
+
submitted: boolean;
|
|
1393
|
+
submitCount: number;
|
|
1394
|
+
/** A submit waits for an async check or an async schema */
|
|
1395
|
+
queued: boolean;
|
|
1396
|
+
/** The schema hasn't answered for the current values yet (an async one is running; also at the start) */
|
|
1397
|
+
validating: boolean;
|
|
1398
|
+
/** The schema has answered at least once since the start (or the last `form.RESET`) */
|
|
1399
|
+
validated: boolean;
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
/** A `form` slice's calculated fields. */
|
|
1403
|
+
interface FormCalculated {
|
|
1404
|
+
/** Every field of `values` by name (leaves, arrays, and array rows' fields by row id) */
|
|
1405
|
+
fields: Record<string, FormField>;
|
|
1406
|
+
/** No schema error and no failed check, as of the schema's last answer (false until its first; an async re-validation keeps the previous value) */
|
|
1407
|
+
valid: boolean;
|
|
1408
|
+
dirty: boolean;
|
|
1409
|
+
/** The form-level message: a server error without a field, or a schema issue without a path after a submit */
|
|
1410
|
+
error: string;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
/** An async per-field check through a driver (a request with reply actions). */
|
|
1414
|
+
interface FormFieldCheck {
|
|
1415
|
+
/** The driver request for a value (without `ok` / `error` / `latest`: the form sets them, SYG235) */
|
|
1416
|
+
request: (value: any) => Record<string, any>;
|
|
1417
|
+
/** The message for a reply body, or a falsy value when the value is fine */
|
|
1418
|
+
error?: (body: any) => string | false | null | undefined;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
interface FormOptions<V = any> {
|
|
1422
|
+
/**
|
|
1423
|
+
* The start values; field names are paths in it (rows of an array of objects need an `id`, SYG236).
|
|
1424
|
+
* Or a function of the host component's state, for start values that come from state (a default
|
|
1425
|
+
* from the settings): called when the form starts, and each time it starts over (`resetOnShow`)
|
|
1426
|
+
*/
|
|
1427
|
+
values: V | ((state: any) => V);
|
|
1428
|
+
/** The host action a valid submit dispatches with the schema's output (SYG234 when it has no model entry). With an `http` sink in its entry the submit is pending until `form.DONE` / `form.ERRORS`; otherwise it is done at once */
|
|
1429
|
+
submit: string;
|
|
1430
|
+
/** Async checks by field name: run on blur and before a submit, which waits for them */
|
|
1431
|
+
check?: Record<string, FormFieldCheck>;
|
|
1432
|
+
/** When a schema error shows: after the field's blur (default), on input, or only after a submit */
|
|
1433
|
+
show?: 'blur' | 'input' | 'submit';
|
|
1434
|
+
/** The form element's selector (default 'form'): input, focusout and submit are heard on it */
|
|
1435
|
+
form?: string;
|
|
1436
|
+
/** The driver sink the checks' requests go to, and that makes a submit entry a request (default 'HTTP') */
|
|
1437
|
+
http?: string;
|
|
1438
|
+
/**
|
|
1439
|
+
* Start over each time the form is shown (D239): when its host component starts (mounted again,
|
|
1440
|
+
* a Switchable page made again) and each time the form element appears again (a Switchable page
|
|
1441
|
+
* shown again; hidden pages stay alive). Back to the start values, touched, errors and the submit
|
|
1442
|
+
* state cleared. Default false: the values live in state and survive a visit (a wizard step keeps
|
|
1443
|
+
* them when the user comes back)
|
|
1444
|
+
*/
|
|
1445
|
+
resetOnShow?: boolean;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
interface FormActions {
|
|
1449
|
+
/** A field changed (the form element's input events): `{ name, value }`; a checkbox gives `checked` as `value` and its own value as `item` (on an array field: added or removed) */
|
|
1450
|
+
CHANGE: { name: string; value: any; item?: any };
|
|
1451
|
+
/** A field lost focus (focusout): its name */
|
|
1452
|
+
BLUR: string;
|
|
1453
|
+
/** The form element's submit (default prevented) */
|
|
1454
|
+
SUBMIT: any;
|
|
1455
|
+
/** Adds a row to an array field (with the next id): `{ field: 'addresses', value: { street: '' } }` */
|
|
1456
|
+
ADD: { field: string; value?: Record<string, any> };
|
|
1457
|
+
/** Removes a row by id: `{ field: 'addresses', id }` */
|
|
1458
|
+
REMOVE: { field: string; id: any };
|
|
1459
|
+
/** Server errors: an error reply (`{ status, body: { errors } }`) or a map `{ name: message }`; focuses the first */
|
|
1460
|
+
ERRORS: any;
|
|
1461
|
+
/** The submit was saved: `initial` becomes `values` */
|
|
1462
|
+
DONE: any;
|
|
1463
|
+
/** Back to `initial` (or to the values given) */
|
|
1464
|
+
RESET: any;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
/**
|
|
1468
|
+
* A form with validation (PLAN-5 F-1), any Standard Schema validator (zod, valibot, arktype, a
|
|
1469
|
+
* hand-written object):
|
|
1470
|
+
*
|
|
1471
|
+
* Signup.uses = { form: form(signupSchema, { values: { email: '', password: '' }, submit: 'SIGN_UP' }) }
|
|
1472
|
+
* // view: const f = state.form.fields; <form><input name="email" value={f.email.value} aria-invalid={f.email.invalid} /></form>
|
|
1473
|
+
* // the host's SIGN_UP gets the schema's output
|
|
1474
|
+
*
|
|
1475
|
+
* Fields are matched by `name=` inside the form element (Collection items' fields too). A failed
|
|
1476
|
+
* submit focuses the first invalid field. A host that sends the submit as a request answers it with
|
|
1477
|
+
* `form.DONE` / `form.ERRORS` (reply actions `ok: 'form.DONE', error: 'form.ERRORS'`); a submit
|
|
1478
|
+
* that sends nothing (STATE, PARENT, EVENTS) is done at once. Throws SYG231 when
|
|
1479
|
+
* `schema` isn't a Standard Schema.
|
|
1480
|
+
*/
|
|
1481
|
+
declare function form<V = any>(schema: StandardSchemaLike, options: FormOptions<V>): Behavior<FormState<V>, FormActions, FormCalculated, FormOptions<V>>
|
|
1482
|
+
|
|
1483
|
+
/** A Standard Schema's result for `values`: errors by field name, and the output when there are none. A Promise for an async schema. */
|
|
1484
|
+
declare function checkForm(schema: StandardSchemaLike, values: any): { errors: FieldErrors; value: any } | Promise<{ errors: FieldErrors; value: any }>
|
|
1485
|
+
/** The errors only ({} when valid); a Promise of them for an async schema. */
|
|
1486
|
+
declare function formErrors(schema: StandardSchemaLike, values: any): FieldErrors | Promise<FieldErrors>
|
|
1487
|
+
/** `values` with the field at `name` set (immutable; rows of an array by id). */
|
|
1488
|
+
declare function setField<V>(values: V, name: string, value: any): V
|
|
1489
|
+
/** The value at a field name. */
|
|
1490
|
+
declare function getField(values: any, name: string): any
|
|
1491
|
+
/** Whether `name` is a field of `values` (its path exists, also when the value is undefined; rows by id). */
|
|
1492
|
+
declare function hasField(values: any, name: string): boolean
|
|
1493
|
+
/** The field name of a schema issue path (array indexes become row ids). */
|
|
1494
|
+
declare function fieldName(values: any, path?: ReadonlyArray<any>): string
|
|
1495
|
+
/** Every field name of `values` (leaves, arrays, rows' fields). */
|
|
1496
|
+
declare function fieldNames(values: any): string[]
|
|
1497
|
+
/** Server errors (an error reply, a map, or a list of issues) as field errors; '' for a form-level message. */
|
|
1498
|
+
declare function replyErrors(reply: any, values?: any): FieldErrors
|
|
1499
|
+
/** An ELEMENT command focusing the first field (DOM order) named in `names` (or with a message in an errors map), children included; with `within` (the form element's selector), only fields inside it; ABORT when none. */
|
|
1500
|
+
declare function focusInvalid(names: string[] | FieldErrors, within?: string): ElementCommand | ABORT
|
|
1501
|
+
|
|
1502
|
+
/** A `selection` slice: the selected ids, as strings, in selection order. */
|
|
1503
|
+
interface SelectionState { selected: string[] }
|
|
1504
|
+
interface SelectionCalculated { count: number }
|
|
1505
|
+
interface SelectionOptions {
|
|
1506
|
+
/** Several items at once: an item click toggles its id (default false: it replaces the selection) */
|
|
1507
|
+
multi?: boolean;
|
|
1508
|
+
/** The control on each item; its clicks dispatch SELECT with the item's `attr` */
|
|
1509
|
+
item?: BehaviorTarget;
|
|
1510
|
+
/** Select-all toggle: dispatches TOGGLE_ALL (needs `from`) */
|
|
1511
|
+
all?: BehaviorTarget;
|
|
1512
|
+
/** Its clicks dispatch CLEAR */
|
|
1513
|
+
clear?: BehaviorTarget;
|
|
1514
|
+
/** The item element's attribute that holds its id (default 'data-id') */
|
|
1515
|
+
attr?: string;
|
|
1516
|
+
/** The host state key of the item list, for SELECT_ALL / TOGGLE_ALL */
|
|
1517
|
+
from?: string;
|
|
1518
|
+
/** The id field of the `from` list's items (default 'id') */
|
|
1519
|
+
idField?: string;
|
|
1520
|
+
}
|
|
1521
|
+
interface SelectionActions { SELECT: string | number | Event$1; SELECT_ALL: Array<string | number> | undefined; TOGGLE_ALL: Array<string | number> | Event$1 | undefined; CLEAR: any }
|
|
1522
|
+
|
|
1523
|
+
/**
|
|
1524
|
+
* Single or multiple selection (GS-1): `uses = { sel: selection({ multi: true, item: Pick, all: All, from: 'mails' }) }`
|
|
1525
|
+
* gives `state.sel = { selected, count }` and 'sel.SELECT' (an id or an item click),
|
|
1526
|
+
* 'sel.SELECT_ALL' (ids, or every id of `state[from]`), 'sel.TOGGLE_ALL' and 'sel.CLEAR'.
|
|
1527
|
+
*/
|
|
1528
|
+
declare function selection(options?: SelectionOptions): Behavior<SelectionState, SelectionActions, SelectionCalculated, SelectionOptions>
|
|
1529
|
+
|
|
1530
|
+
/** Is `id` in a `selection` slice? Ids compare as strings. */
|
|
1531
|
+
declare function isSelected(slice: SelectionState | undefined | null, id: string | number): boolean
|
|
1532
|
+
|
|
1533
|
+
/** A `sortable` slice: the drag state the view styles from, and the live-region text. */
|
|
1534
|
+
interface SortableState {
|
|
1535
|
+
/** The id (as a string) of the item being moved, or null */
|
|
1536
|
+
dragging: string | null;
|
|
1537
|
+
/** The id the pointer is over (pointer drags), or null */
|
|
1538
|
+
over: string | null;
|
|
1539
|
+
/** true: the item lands after `over`; false: before it */
|
|
1540
|
+
after: boolean;
|
|
1541
|
+
/** The `from` key the item would land in (pointer drags; two lists) */
|
|
1542
|
+
list: string | null;
|
|
1543
|
+
mode: 'pointer' | 'keyboard' | null;
|
|
1544
|
+
/** The text for an ARIA live region: lift, move, drop and cancel announcements */
|
|
1545
|
+
message: string;
|
|
1546
|
+
/** A `uid()` id for the instructions element the handles' `aria-describedby` names, unique per host (null until the first focus, press or key inside the host) */
|
|
1547
|
+
helpId: string | null;
|
|
1548
|
+
/** Internal: the pointer press before the threshold (`n`: the instance that started it) */
|
|
1549
|
+
press: { id: string; x: number; y: number; n: number } | null;
|
|
1550
|
+
/** Internal: where the item started (`n`: a keyboard drag's instance) */
|
|
1551
|
+
origin: { list: string; index: number; n?: number } | null;
|
|
1552
|
+
}
|
|
1553
|
+
interface SortableOptions {
|
|
1554
|
+
/** The host state key of the list; an array of keys allows moves between lists (each container marked `data-list="<key>"`) */
|
|
1555
|
+
from: string | string[];
|
|
1556
|
+
/** Selector of each item's element, which carries its id in `attr` (default `[data-id]`) */
|
|
1557
|
+
item?: string;
|
|
1558
|
+
/** Selector of the part that starts a drag and takes keyboard focus (default: the item) */
|
|
1559
|
+
handle?: string;
|
|
1560
|
+
/** 'y' (default): ArrowUp / ArrowDown move, ArrowLeft / ArrowRight change lists; 'x': the other way round */
|
|
1561
|
+
axis?: 'x' | 'y';
|
|
1562
|
+
/** Pixels a pointer moves before a drag starts (default 4) */
|
|
1563
|
+
threshold?: number;
|
|
1564
|
+
/** The item element's attribute that holds its id (default 'data-id') */
|
|
1565
|
+
attr?: string;
|
|
1566
|
+
/** The id field of the list's items (default 'id') */
|
|
1567
|
+
idField?: string;
|
|
1568
|
+
/** An item's name in announcements (default: its title, name, label or id) */
|
|
1569
|
+
label?: (item: any) => string;
|
|
1570
|
+
/**
|
|
1571
|
+
* Announcement texts. `extra`: lift, true for a keyboard lift; move / drop / stay, the list key when the item changed lists.
|
|
1572
|
+
* `cancel`: Escape put the item back; `stay`: Escape after something else changed the list, so the item stays where it is
|
|
1573
|
+
*/
|
|
1574
|
+
messages?: Partial<Record<'lift' | 'move' | 'drop' | 'cancel' | 'stay', (label: string, position: number, count: number, extra?: any) => string>>;
|
|
1575
|
+
}
|
|
1576
|
+
/** 'sort.DROPPED': one completed move (pointer drop, or keyboard drop away from where it started) */
|
|
1577
|
+
interface SortableDropped { id: string; list: string; index: number; fromList: string; fromIndex: number }
|
|
1578
|
+
interface SortableActions {
|
|
1579
|
+
INIT: any; HELP: any; END: any; PRESS: any; MOVE: any; UP: any; CANCEL: any;
|
|
1580
|
+
KEY: { key: string; id?: string };
|
|
1581
|
+
DROPPED: SortableDropped;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
/**
|
|
1585
|
+
* Drag-and-drop reordering of `state[from]` by pointer (mouse, touch, pen) and keyboard (PLAN-5
|
|
1586
|
+
* B-1): `uses = { sort: sortable({ from: 'tasks', item: '.task', handle: '.grip' }) }`. Works on
|
|
1587
|
+
* Collection items (it listens on the host's root). 'sort.DROPPED' fires once per completed move;
|
|
1588
|
+
* `state.sort.message` is the live-region text.
|
|
1589
|
+
*/
|
|
1590
|
+
declare function sortable(options: SortableOptions): Behavior<SortableState, SortableActions, {}, SortableOptions>
|
|
1591
|
+
|
|
1592
|
+
/** `state.history` of `undoable()` / `undo()`: snapshots of `state[key]` (`key: ['a', 'b']`: of `{ a, b }`), newest last in `past`. */
|
|
1593
|
+
interface UndoHistory<T = any> {
|
|
1594
|
+
past: T[]; future: T[];
|
|
1595
|
+
/** Internal (`undo()` with a gesture behavior, `undoStep`): [the value before the gesture in progress, the value after its last step, the behavior's key in `uses`] */
|
|
1596
|
+
base?: [T, T, string?];
|
|
1597
|
+
}
|
|
1598
|
+
interface UndoOptions {
|
|
1599
|
+
/** The state key whose value is snapshotted; several keys (`['todo', 'done']`: a sortable's lists) are snapshotted together as `{ todo, done }` */
|
|
1600
|
+
key: string | string[];
|
|
1601
|
+
/** The most snapshots kept in `past` (default 100) */
|
|
1602
|
+
limit?: number;
|
|
1603
|
+
/** Only these actions are recorded (default: every action with a STATE reducer) */
|
|
1604
|
+
track?: string[];
|
|
1605
|
+
/**
|
|
1606
|
+
* Changes by one action within this many ms join one undo step (default 0: off; 500 when
|
|
1607
|
+
* `coalesce` is given). Without `coalesce`, every action's quick repeats join (not a gesture's
|
|
1608
|
+
* drops: those join only when `coalesce` names a gesture action)
|
|
1609
|
+
*/
|
|
1610
|
+
coalesceMs?: number;
|
|
1611
|
+
/**
|
|
1612
|
+
* Only these actions' quick repeats join one step (typing); every other action is always its
|
|
1613
|
+
* own step: `undo({ key: 'poster', coalesce: ['HEADLINE'], coalesceMs: 1000 })`
|
|
1614
|
+
*/
|
|
1615
|
+
coalesce?: string[];
|
|
1616
|
+
/** These actions clear the history (a load) and are not recorded */
|
|
1617
|
+
resetOn?: string[];
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
/**
|
|
1621
|
+
* Undo / redo for `state[key]` (GS-8): wraps the model's STATE reducers so each change pushes
|
|
1622
|
+
* the old value onto `state.history.past`, and adds UNDO and REDO (no change when there is
|
|
1623
|
+
* nothing to undo or redo). `Editor.model = undoable({ TYPE: ... }, { key: 'doc', coalesceMs: 500 })`.
|
|
1624
|
+
*/
|
|
1625
|
+
declare function undoable<MODEL extends Record<string, any>>(model: MODEL, options: UndoOptions): MODEL & { UNDO: any; REDO: any }
|
|
1626
|
+
|
|
1627
|
+
/** `undo()` options: undoable()'s, plus the controls that trigger UNDO / REDO. */
|
|
1628
|
+
interface UndoBehaviorOptions extends UndoOptions { undo?: BehaviorTarget; redo?: BehaviorTarget }
|
|
1629
|
+
|
|
1630
|
+
/**
|
|
1631
|
+
* undoable() as a behavior (GS-8): `uses = { history: undo({ key: 'doc', undo: UndoButton, redo: RedoButton }) }`
|
|
1632
|
+
* gives `state.history = { past, future, canUndo, canRedo }` and 'history.UNDO' / 'history.REDO'.
|
|
1633
|
+
*/
|
|
1634
|
+
declare function undo(options: UndoBehaviorOptions): Behavior<UndoHistory, { UNDO: any; REDO: any }, { canUndo: boolean; canRedo: boolean }, UndoBehaviorOptions>
|
|
1635
|
+
|
|
1636
|
+
/** The top-level state keys of STATE (any string while STATE is unknown) */
|
|
1637
|
+
type PersistKey<STATE> = 0 extends (1 & STATE) ? string : keyof STATE & string
|
|
1638
|
+
|
|
1639
|
+
/**
|
|
1640
|
+
* A synchronous storage for `persist({ storage })` (async storages are not supported).
|
|
1641
|
+
* `subscribe` (optional) is what `sync: true` listens to instead of the window `storage` event:
|
|
1642
|
+
* call `fn(key, newValue)` when another writer changes a key; return the unsubscribe function.
|
|
1643
|
+
*/
|
|
1644
|
+
interface PersistStorage {
|
|
1645
|
+
getItem(key: string): string | null
|
|
1646
|
+
setItem(key: string, value: string): void
|
|
1647
|
+
removeItem(key: string): void
|
|
1648
|
+
subscribe?(fn: (key: string, newValue: string | null) => void): () => void
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
/** PLAN-4 GS-5: `persist()` options */
|
|
1652
|
+
interface PersistOptions<STATE = any> {
|
|
1653
|
+
/** The storage key */
|
|
1654
|
+
key: string
|
|
1655
|
+
/** The top-level state keys to save (default: all but `omit`) */
|
|
1656
|
+
pick?: ReadonlyArray<PersistKey<STATE>>
|
|
1657
|
+
/** The top-level state keys not to save (calculated fields are never saved) */
|
|
1658
|
+
omit?: ReadonlyArray<PersistKey<STATE>>
|
|
1659
|
+
/** The version saved with the state (default 1). A stored entry with another version goes through `migrate` */
|
|
1660
|
+
version?: number
|
|
1661
|
+
/**
|
|
1662
|
+
* Turns a stored entry of another version into this version's picked keys; nothing (undefined
|
|
1663
|
+
* or null) discards it. Without migrate such an entry is ignored. If it throws: SYG642.
|
|
1664
|
+
*/
|
|
1665
|
+
migrate?: (old: any, fromVersion: number) => Partial<STATE> | null | undefined | void
|
|
1666
|
+
/** 'local' (localStorage, the default), 'session' (sessionStorage) or a synchronous adapter */
|
|
1667
|
+
storage?: 'local' | 'session' | PersistStorage
|
|
1668
|
+
/** Apply other tabs' writes to this key (a RESTORE action) */
|
|
1669
|
+
sync?: boolean
|
|
1670
|
+
/**
|
|
1671
|
+
* Restore in a RESTORE action after the first render (so the first render matches the server's
|
|
1672
|
+
* HTML) instead of before INITIALIZE. Detected when omitted: run()'s mount point holds
|
|
1673
|
+
* renderToString markup (its root element has `data-sygnal-ssr`), a server-rendered Astro
|
|
1674
|
+
* island, a Vike hydration. true / false override it
|
|
1675
|
+
*/
|
|
1676
|
+
hydrate?: boolean
|
|
1677
|
+
/** Writes wait for this many ms without a state change (default 100); flushed on pagehide and dispose */
|
|
1678
|
+
debounceMs?: number
|
|
1679
|
+
/**
|
|
1680
|
+
* 'versioned' (the default) stores `{ version, state }`; 'plain' stores the picked keys
|
|
1681
|
+
* themselves (`{ title, body }`), with no `version` / `migrate`
|
|
1682
|
+
*/
|
|
1683
|
+
format?: 'versioned' | 'plain'
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
/**
|
|
1687
|
+
* The options `persist()` takes: `format: 'plain'` rules out `version` and `migrate` (a plain
|
|
1688
|
+
* entry has no version to migrate from)
|
|
1689
|
+
*/
|
|
1690
|
+
type PersistOptionsFor<STATE = any> =
|
|
1691
|
+
| (PersistOptions<STATE> & { format?: 'versioned' })
|
|
1692
|
+
| (Omit<PersistOptions<STATE>, 'version' | 'migrate' | 'format'> & { format: 'plain'; version?: never; migrate?: never })
|
|
1693
|
+
|
|
1694
|
+
/** The value `persist()` returns: set it as the root component's `persist` static */
|
|
1695
|
+
interface Persist<STATE = any> {
|
|
1696
|
+
readonly options: PersistOptions<STATE>
|
|
1697
|
+
/** Called by the core on the root component (internal) */
|
|
1698
|
+
setup(component: any): void
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
/**
|
|
1702
|
+
* PLAN-4 GS-5: save the root component's state and restore it at startup:
|
|
1703
|
+
* `TodoApp.persist = persist({ key: 'todo-app', pick: ['todos', 'filter'], version: 2, migrate })`.
|
|
1704
|
+
* Stored as JSON `{ version, state }` (`format: 'plain'`: the picked keys themselves). The restore is merged into initialState (part of
|
|
1705
|
+
* INITIALIZE); writes are debounced (`debounceMs`) and flushed on pagehide and dispose.
|
|
1706
|
+
* `PERSIST: { clear: true }` in a model entry removes the stored copy. Root component only
|
|
1707
|
+
* (SYG224); failures are SYG642 (warn) and the app continues on initialState.
|
|
1708
|
+
*/
|
|
1709
|
+
declare function persist<STATE = any>(options: PersistOptionsFor<[STATE] extends [infer S] ? S : never>): Persist<STATE>
|
|
1710
|
+
|
|
1711
|
+
type EventsSelect = keyof SygnalEvents extends never
|
|
1712
|
+
? { select<T = any>(type: string): Stream<T>; }
|
|
1713
|
+
: { select<TYPE extends keyof SygnalEvents & string>(type: TYPE): Stream<SygnalEvents[TYPE]>; }
|
|
1714
|
+
|
|
1715
|
+
type EventsSource<EVENTS = any> = Stream<Event$1<EVENTS>> & EventsSelect
|
|
1716
|
+
|
|
1717
|
+
type DefaultDrivers<STATE, EVENTS = any> = {
|
|
1718
|
+
STATE: {
|
|
1719
|
+
source: StateSource<STATE>;
|
|
1720
|
+
sink: STATE;
|
|
1721
|
+
};
|
|
1722
|
+
DOM: {
|
|
1723
|
+
source: SygnalDOMSource;
|
|
1724
|
+
sink: never;
|
|
1725
|
+
};
|
|
1726
|
+
EVENTS: {
|
|
1727
|
+
source: EventsSource<EVENTS>;
|
|
1728
|
+
sink: EVENTS;
|
|
1729
|
+
};
|
|
1730
|
+
LOG: {
|
|
1731
|
+
source: never;
|
|
1732
|
+
sink: any;
|
|
1733
|
+
};
|
|
1734
|
+
CHILD: {
|
|
1735
|
+
source: ChildSource;
|
|
1736
|
+
sink: never;
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
type Sources<DRIVERS> = {
|
|
1741
|
+
[DRIVER_KEY in keyof DRIVERS]: DRIVERS[DRIVER_KEY] extends { source: infer SOURCE } ? SOURCE : never
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
/**
|
|
1745
|
+
* Maps action types to streams for intent return type.
|
|
1746
|
+
* Uses Stream<any> for values to avoid invariance issues with xstream's Stream<T>
|
|
1747
|
+
* (e.g., Stream<PointerEvent> from DOM.events('click') not assignable to Stream<Event>).
|
|
1748
|
+
* Action data types are enforced at the model layer via reducer signatures.
|
|
1749
|
+
*/
|
|
1750
|
+
type IntentActions<ACTIONS> = keyof ACTIONS extends never
|
|
1751
|
+
? { [action: string]: Stream<any> }
|
|
1752
|
+
: { [ACTION_KEY in keyof ACTIONS]: Stream<any> }
|
|
1753
|
+
|
|
1754
|
+
/**
|
|
1755
|
+
* Normalizes driver types. Passes through valid driver specs, returns {} for `any` or invalid types.
|
|
1756
|
+
* Supports both `type` aliases and `interface` declarations (interfaces lack implicit index
|
|
1757
|
+
* signatures, so a structural fallback check is needed).
|
|
1758
|
+
*/
|
|
1759
|
+
type FixDrivers<DRIVERS> =
|
|
1760
|
+
0 extends (1 & DRIVERS)
|
|
1761
|
+
? {}
|
|
1762
|
+
: DRIVERS extends DriverSpecs
|
|
1763
|
+
? DRIVERS
|
|
1764
|
+
: keyof DRIVERS extends never
|
|
1765
|
+
? {}
|
|
1766
|
+
: DRIVERS extends { [K in keyof DRIVERS]: { source: any; sink: any } }
|
|
1767
|
+
? DRIVERS
|
|
1768
|
+
: {}
|
|
1769
|
+
|
|
1770
|
+
type CombinedSources<STATE, DRIVERS> = Sources<DefaultDrivers<STATE> & DRIVERS> & { dispose$: Stream<boolean> }
|
|
1771
|
+
|
|
1772
|
+
/**
|
|
1773
|
+
* The sources object an intent function receives (DOM, STATE, EVENTS, CHILD, dispose$, plus
|
|
1774
|
+
* any custom drivers). Use it to annotate an intent declared before its component:
|
|
1775
|
+
*
|
|
1776
|
+
* const intent = ({ DOM }: IntentSources<State>) => ({ INC: DOM.click('.inc') })
|
|
1777
|
+
*/
|
|
1778
|
+
type IntentSources<STATE = any, DRIVERS = {}> = CombinedSources<STATE, FixDrivers<DRIVERS>>
|
|
1779
|
+
|
|
1780
|
+
type StreamPayload<STREAM> = STREAM extends Stream<infer T> ? T : any
|
|
1781
|
+
|
|
1782
|
+
type IntentReturnToActions<RETURN> = {
|
|
1783
|
+
[ACTION_KEY in keyof RETURN & string]-?: StreamPayload<Exclude<RETURN[ACTION_KEY], undefined>>
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
/**
|
|
1787
|
+
* Derives the ACTIONS map (action name → payload type) from an intent function's type
|
|
1788
|
+
* (or from its return object type): each key's `Stream<T>` becomes `T`.
|
|
1789
|
+
*
|
|
1790
|
+
* const intent = ({ DOM }: IntentSources<State>) => ({
|
|
1791
|
+
* INC: DOM.click('.inc').mapTo(1), // Stream<number>
|
|
1792
|
+
* NAME: DOM.input('.name').value(), // Stream<string>
|
|
1793
|
+
* })
|
|
1794
|
+
* const Counter: Component<State, {}, {}, ActionsOf<typeof intent>> = ...
|
|
1795
|
+
* Counter.intent = intent
|
|
1796
|
+
* Counter.model = { INC: (state, n) => ..., NAME: (state, name) => ... } // n: number, name: string
|
|
1797
|
+
*
|
|
1798
|
+
* With it, model keys not returned by the intent are type errors (the built-ins BOOTSTRAP,
|
|
1799
|
+
* INITIALIZE and DISPOSE stay allowed). Actions reached only through `next()` or as reply actions
|
|
1800
|
+
* of a request (`ok: 'LOADED'`, `error: 'FAILED'`) are added explicitly, typed by their data:
|
|
1801
|
+
*
|
|
1802
|
+
* type Actions = ActionsOf<typeof intent> & { SAVED: { id: string }; LOADED: Quote; FAILED: FetchFailure }
|
|
1803
|
+
*/
|
|
1804
|
+
type ActionsOf<INTENT> = INTENT extends (...args: any[]) => infer RETURN
|
|
1805
|
+
? IntentReturnToActions<RETURN>
|
|
1806
|
+
: IntentReturnToActions<INTENT>
|
|
1807
|
+
|
|
1808
|
+
interface ComponentIntent<STATE, DRIVERS, ACTIONS, CALCULATED = {}> {
|
|
1809
|
+
(args: IntentArgs<STATE, DRIVERS, CALCULATED>): Partial<IntentActions<ACTIONS>>
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
/**
|
|
1813
|
+
* What a component's intent receives. With calculated fields, STATE also matches
|
|
1814
|
+
* `StateSource<STATE>` (a stream of STATE & CALCULATED is also one of STATE), so an intent
|
|
1815
|
+
* annotated with the documented `IntentSources<State>` is accepted as well as
|
|
1816
|
+
* `IntentSources<State & Calculated>`; an inline intent sees the calculated fields.
|
|
1817
|
+
*/
|
|
1818
|
+
type IntentArgs<STATE, DRIVERS, CALCULATED> = keyof CALCULATED extends never
|
|
1819
|
+
? CombinedSources<STATE, DRIVERS>
|
|
1820
|
+
: CombinedSources<STATE & CALCULATED, DRIVERS> & { STATE: StateSource<STATE> }
|
|
1821
|
+
|
|
1822
|
+
type CalculatedFieldValue<FULL_STATE, RETURN> =
|
|
1823
|
+
| StateOnlyReducer<FULL_STATE, RETURN>
|
|
1824
|
+
| [ReadonlyArray<string & keyof FULL_STATE>, StateOnlyReducer<FULL_STATE, RETURN>]
|
|
1825
|
+
|
|
1826
|
+
type Calculated<STATE, CALCULATED> = keyof CALCULATED extends never
|
|
1827
|
+
? { [field: string]: CalculatedFieldValue<STATE, any> }
|
|
1828
|
+
: { [CALCULATED_KEY in keyof CALCULATED]: CalculatedFieldValue<STATE & CALCULATED, CALCULATED[CALCULATED_KEY]> }
|
|
1829
|
+
|
|
1830
|
+
type Context<STATE, CONTEXT> = keyof CONTEXT extends never
|
|
1831
|
+
? { [field: string]: StateOnlyReducer<STATE, any> }
|
|
1832
|
+
: { [CONTEXT_KEY in keyof CONTEXT]: StateOnlyReducer<STATE, CONTEXT[CONTEXT_KEY]> }
|
|
1833
|
+
|
|
1834
|
+
type Lense<PARENT_STATE = any, CHILD_STATE = any> = {
|
|
1835
|
+
get: (state: PARENT_STATE) => CHILD_STATE;
|
|
1836
|
+
set: (state: PARENT_STATE, childState: CHILD_STATE) => PARENT_STATE;
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
type Lens<PARENT_STATE = any, CHILD_STATE = any> = Lense<PARENT_STATE, CHILD_STATE>
|
|
1840
|
+
|
|
1841
|
+
type Filter<ITEM = any> = (item: ITEM) => boolean
|
|
1842
|
+
|
|
1843
|
+
type SortFunction<ITEM = any> = (a: ITEM, b: ITEM) => number
|
|
1844
|
+
|
|
1845
|
+
/** Sort by one field: `{ name: 'asc' }`, `{ priority: -1 }` (one key; 1 = ascending). */
|
|
1846
|
+
type SortObject<ITEM = any> = {
|
|
1847
|
+
[field: string]: 'asc' | 'desc' | 1 | -1
|
|
1848
|
+
}
|
|
1849
|
+
|
|
1850
|
+
/**
|
|
1851
|
+
* A Collection `sort`: 'asc'/'desc' (whole items), a field name (ascending), a comparator,
|
|
1852
|
+
* a SortObject, or an array of field names, SortObjects and comparators applied in order.
|
|
1853
|
+
*/
|
|
1854
|
+
type SortSpec<ITEM = any> =
|
|
1855
|
+
| string
|
|
1856
|
+
| SortFunction<ITEM>
|
|
1857
|
+
| SortObject<ITEM>
|
|
1858
|
+
| ReadonlyArray<string | SortFunction<ITEM> | SortObject<ITEM>>
|
|
1859
|
+
|
|
1860
|
+
/**
|
|
1861
|
+
* Sygnal Component
|
|
1862
|
+
*/
|
|
1863
|
+
type Component<
|
|
1864
|
+
STATE = any,
|
|
1865
|
+
PROPS = { [prop: string]: any },
|
|
1866
|
+
DRIVERS = {},
|
|
1867
|
+
ACTIONS = {},
|
|
1868
|
+
CALCULATED = {},
|
|
1869
|
+
CONTEXT = {},
|
|
1870
|
+
SINK_RETURNS extends NonStateSinkReturns = {},
|
|
1871
|
+
PROVIDED_CONTEXT = CONTEXT
|
|
1872
|
+
> = ComponentProps<STATE & CALCULATED, PROPS, CONTEXT> & {
|
|
1873
|
+
/** The component's name (diagnostics, devtools, uid()); defaults to the function's name. */
|
|
1874
|
+
componentName?: string;
|
|
1875
|
+
model?: ComponentModel<STATE, PROPS, FixDrivers<DRIVERS>, ACTIONS, CALCULATED, SINK_RETURNS, CONTEXT>;
|
|
1876
|
+
intent?: ComponentIntent<STATE, FixDrivers<DRIVERS>, ACTIONS, CALCULATED>;
|
|
1877
|
+
initialState?: STATE;
|
|
1878
|
+
/**
|
|
1879
|
+
* Give a sub-component its own state instead of the slice its parent passes in.
|
|
1880
|
+
* Required to use `initialState` on a sub-component (otherwise SYG405). Without a
|
|
1881
|
+
* `state` prop the state is local to the instance and never written to the parent.
|
|
1882
|
+
*/
|
|
1883
|
+
isolatedState?: boolean;
|
|
1884
|
+
calculated?: Calculated<STATE, CALCULATED>;
|
|
1885
|
+
/**
|
|
1886
|
+
* Context this component provides to itself and its descendants. Typed by PROVIDED_CONTEXT,
|
|
1887
|
+
* which defaults to CONTEXT (the context the view and reducers see).
|
|
1888
|
+
*/
|
|
1889
|
+
context?: Context<STATE & CALCULATED, PROVIDED_CONTEXT>;
|
|
1890
|
+
onError?: (error: Error, info: { componentName: string }) => any;
|
|
1891
|
+
debug?: boolean;
|
|
1892
|
+
/**
|
|
1893
|
+
* WebSocket / server-sent events connections derived from state (`makeSocketDriver()`):
|
|
1894
|
+
* `Chat.connections = (state) => ({ room: state.room && { socket: '/ws/rooms/' + state.room, message: 'RECEIVED' } })`.
|
|
1895
|
+
* Recomputed from the current state (after the action's reducer) and sent to the socket
|
|
1896
|
+
* driver's sink whenever the result changes structurally (once at startup too): a new name
|
|
1897
|
+
* opens, a removed or falsy one closes, a changed URL reconnects. Events arrive as the named
|
|
1898
|
+
* actions on this instance; send with `{ to: 'room', json }` from a model entry (a connection
|
|
1899
|
+
* the same action opens or changes is declared first). Dispose closes them.
|
|
1900
|
+
*/
|
|
1901
|
+
connections?: (state: STATE & CALCULATED) => Connections;
|
|
1902
|
+
/**
|
|
1903
|
+
* PLAN-3: declarative reads (`makeFetchDriver()`). Each entry derives a
|
|
1904
|
+
* request from state; falsy means idle:
|
|
1905
|
+
* `Quote.resources = { quote: (state) => state.id && '/api/quotes/' + state.id }`.
|
|
1906
|
+
* `state.quote` is a `Resource`: `{ status: 'idle' | 'loading' | 'success' | 'error', data,
|
|
1907
|
+
* error, refreshing }`, written by the built-in RESOURCE action (idle until the first request).
|
|
1908
|
+
* A changed request is fetched with latest semantics (the stale one aborted, its reply never
|
|
1909
|
+
* shown): 'loading' with no data (unless `keepPrevious: true`). A refetch of the same request
|
|
1910
|
+
* (`{ refresh: 'quote' }` on the HTTP sink, `{ invalidate }`, focus, `refetchEvery`) keeps
|
|
1911
|
+
* `data`, `error` and `status` and sets `refreshing: true` (D78). `ok` / `error` on the
|
|
1912
|
+
* request also dispatch those actions after the write.
|
|
1913
|
+
*/
|
|
1914
|
+
resources?: { [name: string]: (state: STATE & CALCULATED) => ResourceRequest | false | null | undefined | '' | 0 };
|
|
1915
|
+
/**
|
|
1916
|
+
* The router's reply action (`makeRouter()`): `App.route = 'ROUTE'`. The driver sends this
|
|
1917
|
+
* instance ROUTE with the `Route` (`{ name, params, query, hash, path }`) once declared and on
|
|
1918
|
+
* every change; the reducer stores it (`ROUTE: (state, route) => ({ ...state, route })`).
|
|
1919
|
+
* The first (outermost) declarer gets each route first and may redirect from that entry
|
|
1920
|
+
* (`ROUTER: { to: 'login', replace: true }`); the others get it right after its ROUTE ran, only
|
|
1921
|
+
* if no redirect happened. All in one flush: every declarer has its first route before the
|
|
1922
|
+
* first render is patched, and a navigation is one patch (G-555, G-565); past 32 navigations in
|
|
1923
|
+
* a row (a redirect loop) each next one waits a task (G-571). A component that
|
|
1924
|
+
* declares later gets the current route in the flush that declared it. A function of state may
|
|
1925
|
+
* return a falsy value to stop listening. Works
|
|
1926
|
+
* with or without a model; a root needs `initialState` (SYG132), seeded with `router.current()`.
|
|
1927
|
+
*/
|
|
1928
|
+
route?: string | ((state: STATE & CALCULATED) => string | false | null | undefined);
|
|
1929
|
+
/**
|
|
1930
|
+
* Document head values for `makeHeadDriver()`, derived from state: `App.head = (state) =>
|
|
1931
|
+
* ({ title: state.task?.title })`. Recomputed when the result changes; removed on dispose.
|
|
1932
|
+
* A later-mounted component's `title` wins; `meta` keys and `link`s merge. Also collected by
|
|
1933
|
+
* `renderToString(App, { head: list })` for SSR (`renderHead(list)`). Like every declaration
|
|
1934
|
+
* static, it is sent with or without a model; a root needs `initialState` (SYG132).
|
|
1935
|
+
*/
|
|
1936
|
+
head?: (state: STATE & CALCULATED) => HeadValue | false | null | undefined;
|
|
1937
|
+
/**
|
|
1938
|
+
* PLAN-4 GS-1: behaviors this component uses, each under its state key:
|
|
1939
|
+
* `TaskList.uses = { pager: pager({ pageSize: 10, next: Newer, prev: Older }) }` gives
|
|
1940
|
+
* `state.pager` and the actions 'pager.NEXT', 'pager.PREV'. See `defineBehavior`.
|
|
1941
|
+
*/
|
|
1942
|
+
uses?: { [key: string]: Behavior<any, any, any, any> };
|
|
1943
|
+
/**
|
|
1944
|
+
* PLAN-4 GS-7: timers derived from state, run by `makeTimerDriver()` (registered:
|
|
1945
|
+
* `run(App, { TIMER: makeTimerDriver() })`; renderComponent provides it) and delivered as this
|
|
1946
|
+
* instance's own actions:
|
|
1947
|
+
* `Stopwatch.timers = (state) => ({ tick: state.running && { every: 100, action: 'TICK' } })`.
|
|
1948
|
+
* Diffed by name whenever the result changes structurally: a new name starts, a falsy or
|
|
1949
|
+
* removed one stops, a changed spec restarts. `every` is drift-free (data `{ n, t }`), `after`
|
|
1950
|
+
* fires once (`{ t }`), `frame` runs every animation frame (`{ t, dt }`). A hidden Switchable
|
|
1951
|
+
* page's timers stop unless `background: true`; dispose stops them; nothing runs during SSR.
|
|
1952
|
+
*/
|
|
1953
|
+
timers?: (state: STATE & CALCULATED) => Timers<ActionNameOf<ACTIONS>>;
|
|
1954
|
+
/**
|
|
1955
|
+
* PLAN-5 B-3: browser sources derived from state, run by `makeBrowserDriver()` (registered:
|
|
1956
|
+
* `run(App, { BROWSER: makeBrowserDriver() })`; renderComponent provides a fake, `t.browser`)
|
|
1957
|
+
* and delivered as this instance's own actions:
|
|
1958
|
+
* `Card.browser = (state) => ({ seen: !state.seen && { intersection: '.cover', action: 'SEEN' } })`.
|
|
1959
|
+
* Diffed by name as `timers`: a new name starts, a falsy or removed one stops, a changed spec
|
|
1960
|
+
* restarts. A hidden Switchable page's sources stop unless `background: true`; dispose stops
|
|
1961
|
+
* them; nothing runs during SSR.
|
|
1962
|
+
*/
|
|
1963
|
+
browser?: (state: STATE & CALCULATED) => BrowserSources<ActionNameOf<ACTIONS>>;
|
|
1964
|
+
/**
|
|
1965
|
+
* PLAN-4 GS-5: save this root component's state and restore it at startup:
|
|
1966
|
+
* `TodoApp.persist = persist({ key: 'todo-app', pick: ['todos', 'filter'] })`. `pick` / `omit`
|
|
1967
|
+
* are typed against STATE's keys. Root component only (SYG224 elsewhere).
|
|
1968
|
+
*/
|
|
1969
|
+
persist?: Persist<STATE>;
|
|
1970
|
+
/**
|
|
1971
|
+
* PLAN-4 GS-12: actions whose state change animates as a View Transition:
|
|
1972
|
+
* `Board.viewTransitions = ['MOVE']`, `App.viewTransitions = ['ROUTE']` (the router's reply
|
|
1973
|
+
* action) for route changes. The DOM patch that the action's STATE reducer causes runs inside
|
|
1974
|
+
* `document.startViewTransition()`, which needs the app's DOM driver from
|
|
1975
|
+
* `makeViewTransitionDOMDriver()` (SYG645 in dev otherwise). Patched at once, without a
|
|
1976
|
+
* transition, under `prefers-reduced-motion: reduce` and where the browser has no API.
|
|
1977
|
+
*/
|
|
1978
|
+
viewTransitions?: Array<ActionNameOf<ACTIONS>>;
|
|
1979
|
+
}
|
|
1980
|
+
|
|
1981
|
+
/** The action names of an ACTIONS map (any string when it names none) */
|
|
1982
|
+
type ActionNameOf<ACTIONS> = keyof ACTIONS extends never ? string : keyof ACTIONS & string;
|
|
1983
|
+
|
|
1984
|
+
/**
|
|
1985
|
+
* Sygnal Root Component (the one passed to `run()`): a Component without props, so the
|
|
1986
|
+
* view's `state` is typed by STATE (& CALCULATED).
|
|
1987
|
+
*/
|
|
1988
|
+
type RootComponent<
|
|
1989
|
+
STATE = any,
|
|
1990
|
+
DRIVERS = {},
|
|
1991
|
+
ACTIONS = {},
|
|
1992
|
+
CALCULATED = {},
|
|
1993
|
+
CONTEXT = {},
|
|
1994
|
+
SINK_RETURNS extends NonStateSinkReturns = {},
|
|
1995
|
+
PROVIDED_CONTEXT = CONTEXT
|
|
1996
|
+
> = Component<STATE, {}, DRIVERS, ACTIONS, CALCULATED, CONTEXT, SINK_RETURNS, PROVIDED_CONTEXT>
|
|
1997
|
+
|
|
1998
|
+
/**
|
|
1999
|
+
* A component function that can be used in Collection/Switchable.
|
|
2000
|
+
* Uses a permissive type to avoid contravariance issues with typed custom drivers.
|
|
2001
|
+
*/
|
|
2002
|
+
type AnyComponent = ((...args: any[]) => any) & Record<string, any>
|
|
2003
|
+
|
|
2004
|
+
/** Keys of STATE whose value is an array (optional/nullable arrays included). */
|
|
2005
|
+
type ArrayKeysOf<STATE> = {
|
|
2006
|
+
[KEY in keyof STATE]-?: NonNullable<STATE[KEY]> extends ReadonlyArray<any> ? KEY : never
|
|
2007
|
+
}[keyof STATE] & string
|
|
2008
|
+
|
|
2009
|
+
/**
|
|
2010
|
+
* Valid `from` values for a Collection: any string or Lense while the parent STATE is unknown
|
|
2011
|
+
* (`any`); otherwise an array-valued key of STATE or a Lense over STATE.
|
|
2012
|
+
*/
|
|
2013
|
+
type CollectionFrom<STATE = any> = 0 extends (1 & STATE)
|
|
2014
|
+
? string | Lense
|
|
2015
|
+
: ArrayKeysOf<STATE> | Lense<STATE, any>
|
|
2016
|
+
|
|
2017
|
+
/**
|
|
2018
|
+
* Collection props. Pass the parent component's state type as STATE to type-check `from`:
|
|
2019
|
+
*
|
|
2020
|
+
* const TaskCollection = Collection<{}, LaneState> // instantiation expression
|
|
2021
|
+
* <TaskCollection of={TaskCard} from="tasks" /> // 'tasks' must be an array key of LaneState
|
|
2022
|
+
*/
|
|
2023
|
+
type CollectionProps<PROPS = any, STATE = any> = {
|
|
2024
|
+
of: AnyComponent;
|
|
2025
|
+
from: CollectionFrom<STATE>;
|
|
2026
|
+
filter?: Filter;
|
|
2027
|
+
sort?: SortSpec;
|
|
2028
|
+
/**
|
|
2029
|
+
* PLAN-5 A-1: a CSS identifier such as `'card'`. Each item with an `id` gets
|
|
2030
|
+
* `view-transition-name: card-<id>` and `view-transition-class: card` on its root element (its
|
|
2031
|
+
* own style wins), so an action in a `viewTransitions` static animates the items between their
|
|
2032
|
+
* places, and between Collections with the same prefix. Items without an `id` get none.
|
|
2033
|
+
*/
|
|
2034
|
+
viewTransitionName?: string;
|
|
2035
|
+
/**
|
|
2036
|
+
* Removed in 6.0 (D229, SYG612): a Collection has no element of its own; its items render
|
|
2037
|
+
* directly into the parent. Put the class on your own element: `<ul className="x"><Collection … /></ul>`
|
|
2038
|
+
*/
|
|
2039
|
+
className?: never;
|
|
2040
|
+
} & Omit<PROPS, 'of' | 'from' | 'filter' | 'sort' | 'viewTransitionName' | 'className'>
|
|
2041
|
+
|
|
2042
|
+
/**
|
|
2043
|
+
* VirtualCollection props (PLAN-5 V-1): Collection's, plus the scroll container's. Pass the parent
|
|
2044
|
+
* component's state type as STATE to type-check `from`, as for Collection.
|
|
2045
|
+
*/
|
|
2046
|
+
type VirtualCollectionProps<PROPS = any, STATE = any> = {
|
|
2047
|
+
of: AnyComponent;
|
|
2048
|
+
from: CollectionFrom<STATE>;
|
|
2049
|
+
filter?: Filter;
|
|
2050
|
+
sort?: SortSpec;
|
|
2051
|
+
/** The scroll container's class; give it a bounded height (`height`, `max-height`, or a flex item with `min-height: 0`) */
|
|
2052
|
+
className?: string;
|
|
2053
|
+
/** A row's height in px before it is measured: a number (default 32) or `(item, index) => px` */
|
|
2054
|
+
estimateSize?: number | ((item: any, index: number) => number);
|
|
2055
|
+
/** Rows rendered beyond each edge of the view (default 5) */
|
|
2056
|
+
overscan?: number;
|
|
2057
|
+
/** The container's role (default `'list'`; its rows get `role="listitem"` unless they have a role). `null`: none */
|
|
2058
|
+
role?: string | null;
|
|
2059
|
+
/** The container's tabIndex (default 0: the keyboard scrolls it) */
|
|
2060
|
+
tabIndex?: number;
|
|
2061
|
+
/** The container's inline style, after `overflow-y: auto; overflow-anchor: none` */
|
|
2062
|
+
style?: Record<string, string | number>;
|
|
2063
|
+
id?: string;
|
|
2064
|
+
'aria-label'?: string;
|
|
2065
|
+
'aria-labelledby'?: string;
|
|
2066
|
+
'aria-describedby'?: string;
|
|
2067
|
+
/** As Collection's (G-417): each row with an `id` gets `view-transition-name: <prefix>-<id>` and `view-transition-class: <prefix>` */
|
|
2068
|
+
viewTransitionName?: string;
|
|
2069
|
+
} & Omit<PROPS, 'of' | 'from' | 'filter' | 'sort' | 'className' | 'estimateSize' | 'overscan' | 'role' | 'tabIndex' | 'style' | 'id' | 'viewTransitionName'>
|
|
2070
|
+
|
|
2071
|
+
/** Where `scrollToIndex` / `scrollToId` put the row: 'auto' (default) scrolls only when it isn't in view */
|
|
2072
|
+
type ScrollToAlign = 'auto' | 'start' | 'center' | 'end'
|
|
2073
|
+
|
|
2074
|
+
type SwitchableProps<PROPS = any> = {
|
|
2075
|
+
of: Record<string, AnyComponent>;
|
|
2076
|
+
current: string;
|
|
2077
|
+
/**
|
|
2078
|
+
* The current page's instance key. When it changes, the current page is disposed and created
|
|
2079
|
+
* again (fresh state); a hidden page shown with another key than it last had is re-created on
|
|
2080
|
+
* show. Switching `current` alone keeps pages alive. Router recipe: `instance={state.route.path}`
|
|
2081
|
+
*/
|
|
2082
|
+
instance?: string | number;
|
|
2083
|
+
state?: string | Lense;
|
|
2084
|
+
} & Omit<PROPS, 'of' | 'state' | 'current' | 'instance'>
|
|
2085
|
+
|
|
2086
|
+
type PortalProps = {
|
|
2087
|
+
target: string;
|
|
2088
|
+
children?: any;
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
type TransitionProps = {
|
|
2092
|
+
name?: string;
|
|
2093
|
+
duration?: number;
|
|
2094
|
+
appear?: boolean;
|
|
2095
|
+
children?: any;
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
type SuspenseProps = {
|
|
2099
|
+
/**
|
|
2100
|
+
* Shown (wrapped in `<div data-sygnal-suspense="pending">`) while any child is not ready:
|
|
2101
|
+
* a `lazy()` component still loading, or a component with an explicit READY model entry
|
|
2102
|
+
* that hasn't emitted true. A string renders as text. Without it the children render as-is.
|
|
2103
|
+
*/
|
|
2104
|
+
fallback?: JSX.Element | string;
|
|
2105
|
+
children?: any;
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
type SlotProps = {
|
|
2109
|
+
name?: string;
|
|
2110
|
+
children?: any;
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
type ClassesType = (string | string[] | { [className: string]: boolean | undefined })[]
|
|
2114
|
+
|
|
2115
|
+
/**
|
|
2116
|
+
* Diagnostics mode.
|
|
2117
|
+
* - 'off' — no checks, no collection (default in production)
|
|
2118
|
+
* - 'collect' — collect diagnostics silently (read with getDiagnostics())
|
|
2119
|
+
* - 'warn' — collect and print warn/error diagnostics to the console (default in Vite dev)
|
|
2120
|
+
* - 'error' — collect and throw on warn/error diagnostics
|
|
2121
|
+
*/
|
|
2122
|
+
type DiagnosticsMode = 'off' | 'collect' | 'warn' | 'error'
|
|
2123
|
+
|
|
2124
|
+
/** Stable diagnostic code, e.g. 'SYG101'. See https://sygnal.js.org/reference/errors */
|
|
2125
|
+
type DiagnosticCode = `SYG${number}`
|
|
2126
|
+
|
|
2127
|
+
type DiagnosticSeverity = 'error' | 'warn' | 'info'
|
|
2128
|
+
|
|
2129
|
+
type Diagnostic = {
|
|
2130
|
+
code: DiagnosticCode;
|
|
2131
|
+
severity: DiagnosticSeverity;
|
|
2132
|
+
/** Name of the component the diagnostic is about */
|
|
2133
|
+
component?: string;
|
|
2134
|
+
/** What is wrong */
|
|
2135
|
+
message: string;
|
|
2136
|
+
/** How to fix it */
|
|
2137
|
+
fix?: string;
|
|
2138
|
+
/** Structured payload (check-specific) */
|
|
2139
|
+
data?: any;
|
|
2140
|
+
/** Link to the docs entry for this code */
|
|
2141
|
+
docsUrl: string;
|
|
2142
|
+
/** Fully formatted message: `[Sygnal SYG123] Component: message. fix docsUrl` */
|
|
2143
|
+
text: string;
|
|
2144
|
+
timestamp: number;
|
|
2145
|
+
}
|
|
2146
|
+
|
|
2147
|
+
type DiagnosticsOptions = {
|
|
2148
|
+
mode?: DiagnosticsMode;
|
|
2149
|
+
/** Codes to ignore entirely */
|
|
2150
|
+
ignore?: DiagnosticCode[];
|
|
2151
|
+
/**
|
|
2152
|
+
* Strict (canonical-form, SYG5xx) runtime checks. Needs the 'sygnal/diagnostics' dev entry
|
|
2153
|
+
* (otherwise SYG608 is printed once). Without a `mode`, `strict: true` also turns diagnostics
|
|
2154
|
+
* on ('warn'). Omitted: an earlier `configureStrict()` setting is kept.
|
|
2155
|
+
*/
|
|
2156
|
+
strict?: boolean;
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
/**
|
|
2160
|
+
* Where an error reported to the app-level `onError` hook happened (PLAN-4 GS-11). `'intent'`: an
|
|
2161
|
+
* intent stream errored (it stops emitting; `action` is its action name). `'context'`: a
|
|
2162
|
+
* `.context` entry threw (it keeps its last value). `'widget'`: a `defineWidget` widget's `mount`,
|
|
2163
|
+
* `update` or `unmount` threw (`componentName` is the component that renders it). `'patch'`: the
|
|
2164
|
+
* DOM driver's patch threw (a vnode hook, a DOM module, flattening the tree); the app's DOM stops
|
|
2165
|
+
* updating (reported once) and the mount point is marked `data-sygnal-error="patch"` (already when
|
|
2166
|
+
* `onError` runs) until the app is disposed.
|
|
2167
|
+
*/
|
|
2168
|
+
type AppErrorPhase = 'view' | 'reducer' | 'effect' | 'intent' | 'context' | 'declaration' | 'driver' | 'instantiate' | 'dispose' | 'widget' | 'patch'
|
|
2169
|
+
|
|
2170
|
+
/** What the app-level `onError` hook gets with the error */
|
|
2171
|
+
interface AppErrorInfo {
|
|
2172
|
+
/** The component whose view, reducer, EFFECT or sub-component threw (not for 'driver') */
|
|
2173
|
+
componentName?: string
|
|
2174
|
+
/** The action whose reducer or EFFECT threw ('reducer', 'effect'), or whose intent stream errored ('intent') */
|
|
2175
|
+
action?: string
|
|
2176
|
+
phase: AppErrorPhase
|
|
2177
|
+
/** The driver (sink) name, for 'driver' */
|
|
2178
|
+
driver?: string
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
/**
|
|
2182
|
+
* App-level error hook: reporting only (e.g. to an error tracker). It is called after the
|
|
2183
|
+
* component's own `onError` boundary chose the fallback, once per error, in every diagnostics
|
|
2184
|
+
* mode. An exception thrown by the hook is logged with console.error and swallowed.
|
|
2185
|
+
*/
|
|
2186
|
+
type AppErrorHook = (error: any, info: AppErrorInfo) => void
|
|
2187
|
+
|
|
2188
|
+
type RunOptions = {
|
|
2189
|
+
/** Where the app renders: a CSS selector (default '#root') or an element */
|
|
2190
|
+
mountPoint?: string | Element;
|
|
2191
|
+
/** @deprecated No effect since 6.0: fragments always work (the DOM driver splices them into their parent) */
|
|
2192
|
+
fragments?: boolean;
|
|
2193
|
+
useDefaultDrivers?: boolean;
|
|
2194
|
+
/**
|
|
2195
|
+
* Runtime diagnostics. Takes precedence over `globalThis.__SYGNAL_DEV__`
|
|
2196
|
+
* (set by the Sygnal Vite plugin in dev), which enables 'warn'. Default: 'off'.
|
|
2197
|
+
*/
|
|
2198
|
+
diagnostics?: DiagnosticsMode | DiagnosticsOptions;
|
|
2199
|
+
/** App-level error hook for this app (each run() has its own); see AppErrorHook */
|
|
2200
|
+
onError?: AppErrorHook;
|
|
2201
|
+
/**
|
|
2202
|
+
* The root of this app's `uid()` strings (default 'u'). Give each app on one page its own
|
|
2203
|
+
* (`run(Signup, {}, { mountPoint: '#signup', uid: 'signup' })`), and pass the same value to
|
|
2204
|
+
* renderToString's `uid` when hydrating server markup.
|
|
2205
|
+
*/
|
|
2206
|
+
uid?: string;
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
/** All diagnostics collected so far (most recent last). */
|
|
2210
|
+
declare function getDiagnostics(): Diagnostic[]
|
|
2211
|
+
|
|
2212
|
+
/** Clear the collected diagnostics. */
|
|
2213
|
+
declare function clearDiagnostics(): void
|
|
2214
|
+
|
|
2215
|
+
/** Subscribe to diagnostics as they are reported. Returns an unsubscribe function. */
|
|
2216
|
+
declare function onDiagnostic(callback: (diagnostic: Diagnostic) => void): () => void
|
|
2217
|
+
|
|
2218
|
+
|
|
2219
|
+
/**
|
|
2220
|
+
* The Sygnal DevTools bridge (also `window.__SYGNAL_DEVTOOLS__`), installed in a
|
|
2221
|
+
* browser by the dev-only 'sygnal/devtools' entry, which sygnal/vite injects in dev.
|
|
2222
|
+
* Only the stable, documented members are typed.
|
|
2223
|
+
*/
|
|
2224
|
+
interface SygnalDevTools {
|
|
2225
|
+
/** true while the browser extension is connected */
|
|
2226
|
+
readonly connected: boolean
|
|
2227
|
+
/** Diagnostics collected so far (same as getDiagnostics()) */
|
|
2228
|
+
getDiagnostics(): Diagnostic[]
|
|
2229
|
+
/**
|
|
2230
|
+
* The machine-readable app graph of the live components. Present only when the
|
|
2231
|
+
* 'sygnal/diagnostics' dev entry is loaded (it attaches this method); needs diagnostics on.
|
|
2232
|
+
*/
|
|
2233
|
+
inspect?(): InspectGraph
|
|
2234
|
+
/** PLAN-4 3-E (G-226): defaults for "Copy as test" from the extension panel (componentImport, drivers, ...) */
|
|
2235
|
+
configureCopyAsTest(options: DevToolsCopyAsTestOptions): void
|
|
2236
|
+
/**
|
|
2237
|
+
* PLAN-4 3-E (G-226): one instance's recorded session: undefined (the newest root), an
|
|
2238
|
+
* instance id, a component, or run()'s result
|
|
2239
|
+
*/
|
|
2240
|
+
getSession(target?: string | number | ((...args: any[]) => any) | { sources: any }): DevToolsSessionRecording
|
|
2241
|
+
}
|
|
2242
|
+
|
|
2243
|
+
/** "Copy as test" options (the same as CopyAsTestOptions in 'sygnal/devtools') */
|
|
2244
|
+
interface DevToolsCopyAsTestOptions {
|
|
2245
|
+
/** The import line(s) for the component (default: `import <Name> from './<Name>.js'`) */
|
|
2246
|
+
componentImport?: string
|
|
2247
|
+
/** The component's identifier in the test (default: its recorded name) */
|
|
2248
|
+
componentName?: string
|
|
2249
|
+
/** More import lines (drivers, helpers) */
|
|
2250
|
+
imports?: string[]
|
|
2251
|
+
/** Drivers for renderComponent, as source code by sink name: { DND: 'mockDragDriver().driver' } */
|
|
2252
|
+
drivers?: Record<string, string>
|
|
2253
|
+
/** More renderComponent options, as source code: 'strict: true' */
|
|
2254
|
+
renderOptions?: string
|
|
2255
|
+
/** The test's name */
|
|
2256
|
+
testName?: string
|
|
2257
|
+
/** Adds a `// @vitest-environment <env>` first line */
|
|
2258
|
+
environment?: string
|
|
2259
|
+
}
|
|
2260
|
+
|
|
2261
|
+
type DevToolsActionCause = 'intent' | 'next' | 'reply' | 'built-in' | 'simulateAction' | 'behavior'
|
|
2262
|
+
|
|
2263
|
+
/** One instance's recorded session (the same as SessionRecording in 'sygnal/devtools') */
|
|
2264
|
+
interface DevToolsSessionRecording {
|
|
2265
|
+
version: 1
|
|
2266
|
+
component: string
|
|
2267
|
+
instance: string
|
|
2268
|
+
/** The state when the session started for this instance */
|
|
2269
|
+
initialState?: any
|
|
2270
|
+
/** The component's own initialState (renderComponent's default) */
|
|
2271
|
+
definitionInitialState?: any
|
|
2272
|
+
finalState: any
|
|
2273
|
+
/** The action names the component can be sent (model keys, behavior actions) */
|
|
2274
|
+
actionNames?: string[]
|
|
2275
|
+
/** Source names beyond DOM / EVENTS / STATE / LOG / CHILD / PARENT / READY */
|
|
2276
|
+
drivers: string[]
|
|
2277
|
+
/** Those of `drivers` renderComponent fakes (makeFetchDriver sources) */
|
|
2278
|
+
fakeable: string[]
|
|
2279
|
+
/** The instance's own actions, in order */
|
|
2280
|
+
actions: Array<{ type: string; data: any; cause: DevToolsActionCause; sinks: string[]; at: number; replySink?: string; replyKind?: 'fetch' | 'other'; echo?: true }>
|
|
2281
|
+
/** State changes in descendant instances a replay at this instance can't reproduce */
|
|
2282
|
+
foreign: Array<{ type: string; component: string; instance: string; cause: DevToolsActionCause }>
|
|
2283
|
+
truncated?: boolean
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
/** The installed DevTools bridge; undefined unless 'sygnal/devtools' was loaded (always in production builds). */
|
|
2287
|
+
declare function getDevTools(): SygnalDevTools | undefined
|
|
2288
|
+
|
|
2289
|
+
type SygnalSinks<STATE = any, DRIVERS = {}> = {
|
|
2290
|
+
[SINK_NAME in keyof (DefaultDrivers<STATE> & FixDrivers<DRIVERS>) | string]?: Stream<any>
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
type AnyComponentModule<COMPONENT = any> =
|
|
2294
|
+
| COMPONENT
|
|
2295
|
+
| { default: COMPONENT }
|
|
2296
|
+
| Array<COMPONENT | { default: COMPONENT } | null | undefined>
|
|
2297
|
+
| null
|
|
2298
|
+
| undefined
|
|
2299
|
+
|
|
2300
|
+
type SygnalApp<STATE = any, DRIVERS = {}> = {
|
|
2301
|
+
sources: CombinedSources<STATE, FixDrivers<DRIVERS>>;
|
|
2302
|
+
sinks: SygnalSinks<STATE, DRIVERS>;
|
|
2303
|
+
dispose: () => void;
|
|
2304
|
+
hmr: (newComponent?: AnyComponentModule<RootComponent<STATE, DRIVERS>>, state?: STATE) => void;
|
|
2305
|
+
}
|
|
2306
|
+
|
|
2307
|
+
type HotModuleAPI = {
|
|
2308
|
+
accept: (...args: any[]) => any;
|
|
2309
|
+
dispose?: (callback: () => void) => void;
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
declare function run<
|
|
2313
|
+
STATE = any,
|
|
360
2314
|
DRIVERS = {},
|
|
361
2315
|
ACTIONS = {},
|
|
362
2316
|
CALCULATED = {},
|
|
@@ -367,21 +2321,21 @@ export function run<
|
|
|
367
2321
|
options?: RunOptions
|
|
368
2322
|
): SygnalApp<STATE & CALCULATED, FixDrivers<DRIVERS>>
|
|
369
2323
|
|
|
370
|
-
|
|
2324
|
+
declare function run(
|
|
371
2325
|
component: any,
|
|
372
2326
|
drivers?: Record<string, CycleDriver<any, any>>,
|
|
373
2327
|
options?: RunOptions
|
|
374
2328
|
): SygnalApp<any, {}>
|
|
375
2329
|
|
|
376
|
-
|
|
2330
|
+
declare function enableHMR<STATE = any, DRIVERS = {}>(
|
|
377
2331
|
app: SygnalApp<STATE, DRIVERS>,
|
|
378
2332
|
hot: HotModuleAPI,
|
|
379
2333
|
loadComponent?: () => Promise<AnyComponentModule<RootComponent<STATE, DRIVERS>>> | AnyComponentModule<RootComponent<STATE, DRIVERS>>,
|
|
380
2334
|
acceptDependencies?: string | string[]
|
|
381
2335
|
): SygnalApp<STATE, DRIVERS>
|
|
382
2336
|
|
|
383
|
-
|
|
384
|
-
|
|
2337
|
+
declare function classes(...classes: ClassesType): string
|
|
2338
|
+
declare function exactState<STATE>(): <ACTUAL extends STATE>(state: ExactShape<STATE, ACTUAL>) => STATE
|
|
385
2339
|
|
|
386
2340
|
// ── Reducer helpers ────────────────────────────────────────────────
|
|
387
2341
|
|
|
@@ -394,253 +2348,1380 @@ export function exactState<STATE>(): <ACTUAL extends STATE>(state: ExactShape<ST
|
|
|
394
2348
|
* Dynamic form — function receives (state, data, next, props) and
|
|
395
2349
|
* returns the partial update to merge:
|
|
396
2350
|
* `set((state, title) => ({ title }))`
|
|
2351
|
+
*
|
|
2352
|
+
* Not a field name: `set('title')` is a type error (and SYG221 in the dev checks).
|
|
397
2353
|
*/
|
|
398
|
-
|
|
399
|
-
partial: Partial<S> | ((state: S, data: any, next: Function, props: any) => Partial<S>)
|
|
2354
|
+
declare function set<S = any>(
|
|
2355
|
+
partial: (Partial<S> & object) | ((state: S, data: any, next: Function, props: any) => Partial<S>)
|
|
400
2356
|
): (state: S, data: any, next: Function, props: any) => S
|
|
401
2357
|
|
|
402
2358
|
/**
|
|
403
|
-
* Create a reducer that toggles a boolean field on state.
|
|
404
|
-
*
|
|
405
|
-
* `toggle('showModal')`
|
|
2359
|
+
* Create a reducer that toggles a boolean field on state.
|
|
2360
|
+
*
|
|
2361
|
+
* `toggle('showModal')`
|
|
2362
|
+
*/
|
|
2363
|
+
declare function toggle<S = any>(field: keyof S & string): (state: S) => S
|
|
2364
|
+
|
|
2365
|
+
type EmitEntry<TYPE extends string> = keyof SygnalEvents extends never
|
|
2366
|
+
? { EVENTS: (state: any, actionData: any, next: Function, props: any) => { type: string; data: any } }
|
|
2367
|
+
: { EVENTS: (state: any, actionData: any, next: Function, props: any) => EmittedEvent<TYPE> }
|
|
2368
|
+
|
|
2369
|
+
/**
|
|
2370
|
+
* Create a model entry that emits an EVENTS bus event.
|
|
2371
|
+
* Prefer `event()` inside the object form: `ACTION: { EVENTS: event('TYPE', fn) }`.
|
|
2372
|
+
*
|
|
2373
|
+
* `emit('DELETE_LANE', (state) => ({ laneId: state.id }))`
|
|
2374
|
+
* `emit('REFRESH')`
|
|
2375
|
+
*
|
|
2376
|
+
* With a `SygnalEvents` registry, `type` and the payload are checked against it.
|
|
2377
|
+
*/
|
|
2378
|
+
declare function emit<TYPE extends EventName>(
|
|
2379
|
+
type: TYPE,
|
|
2380
|
+
data: (state: any, actionData: any, next: Function, props: any) => EventPayload<TYPE>
|
|
2381
|
+
): EmitEntry<TYPE>
|
|
2382
|
+
declare function emit<TYPE extends EventName>(
|
|
2383
|
+
type: TYPE,
|
|
2384
|
+
...data: StaticEventArgs<TYPE>
|
|
2385
|
+
): EmitEntry<TYPE>
|
|
2386
|
+
|
|
2387
|
+
/**
|
|
2388
|
+
* Create an EVENTS sink function that puts `{ type, data }` on the EVENTS bus.
|
|
2389
|
+
* Use it as the `EVENTS` value inside an object-form model entry:
|
|
2390
|
+
*
|
|
2391
|
+
* DELETE: {
|
|
2392
|
+
* STATE: (state) => ({ ...state, deleting: true }),
|
|
2393
|
+
* EVENTS: event('DELETE_LANE', (state) => ({ laneId: state.id })),
|
|
2394
|
+
* }
|
|
2395
|
+
*
|
|
2396
|
+
* The payload is either a function `(state, data, next, props) => payload` or a static value:
|
|
2397
|
+
* `event('RESET')`, `event('SET_MODE', 'dark')`.
|
|
2398
|
+
*
|
|
2399
|
+
* With a `SygnalEvents` registry, `type` must be a registered name and the payload must match
|
|
2400
|
+
* its type. When used inside a model, the payload function's `state` and `data` parameters are
|
|
2401
|
+
* typed from the component.
|
|
2402
|
+
*/
|
|
2403
|
+
declare function event<TYPE extends EventName, STATE = any, DATA = any>(
|
|
2404
|
+
type: TYPE,
|
|
2405
|
+
...payload: EventArgs<TYPE, STATE, DATA>
|
|
2406
|
+
): EventSink<TYPE, STATE, DATA>
|
|
2407
|
+
|
|
2408
|
+
/**
|
|
2409
|
+
* The payload argument of `event()`: a payload function or a static value (one signature, not
|
|
2410
|
+
* overloads, so an unregistered name is reported as one "not assignable to parameter" error).
|
|
2411
|
+
*/
|
|
2412
|
+
type EventArgs<TYPE extends string, STATE, DATA> = keyof SygnalEvents extends never
|
|
2413
|
+
? [payload?: EventPayloadFunction<TYPE, STATE, DATA> | AnySinkConstant]
|
|
2414
|
+
: undefined extends EventPayload<TYPE>
|
|
2415
|
+
? [payload?: EventPayloadFunction<TYPE, STATE, DATA> | EventPayload<TYPE>]
|
|
2416
|
+
: [payload: EventPayloadFunction<TYPE, STATE, DATA> | EventPayload<TYPE>]
|
|
2417
|
+
|
|
2418
|
+
/**
|
|
2419
|
+
* Any object with an events() method (e.g., DOM.select('form')).
|
|
2420
|
+
* Uses permissive signature to be compatible with MainDOMSource's overloaded events().
|
|
2421
|
+
*/
|
|
2422
|
+
type FormSource = {
|
|
2423
|
+
events(eventName: string, ...args: any[]): Stream<any>
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
type FormData<FIELDS extends Record<string, string> = Record<string, string>> = FIELDS & {
|
|
2427
|
+
event: globalThis.Event;
|
|
2428
|
+
eventType: string;
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
type ProcessFormOptions = {
|
|
2432
|
+
events?: string | string[];
|
|
2433
|
+
preventDefault?: boolean;
|
|
2434
|
+
}
|
|
2435
|
+
|
|
2436
|
+
/**
|
|
2437
|
+
* Extracts form field values from a DOM source's form events.
|
|
2438
|
+
*
|
|
2439
|
+
* @example
|
|
2440
|
+
* // Untyped — all fields are `string`
|
|
2441
|
+
* const form$ = processForm(DOM.select('form'))
|
|
2442
|
+
* // form$ is Stream<FormData> → { event, eventType, [field]: string }
|
|
2443
|
+
*
|
|
2444
|
+
* @example
|
|
2445
|
+
* // Typed — specify expected field names
|
|
2446
|
+
* const form$ = processForm<{ username: string; email: string }>(DOM.select('form'))
|
|
2447
|
+
* // form$ is Stream<FormData<{ username: string; email: string }>>
|
|
2448
|
+
* // form$.username is typed as string ✓
|
|
2449
|
+
*/
|
|
2450
|
+
declare function processForm<FIELDS extends Record<string, string> = Record<string, string>>(
|
|
2451
|
+
target: FormSource,
|
|
2452
|
+
options?: ProcessFormOptions
|
|
2453
|
+
): Stream<FormData<FIELDS>>
|
|
2454
|
+
|
|
2455
|
+
/**
|
|
2456
|
+
* Any object with an events() method (e.g., DOM.select('.draggable')).
|
|
2457
|
+
* Uses permissive signature to be compatible with MainDOMSource's overloaded events().
|
|
2458
|
+
*/
|
|
2459
|
+
type DragSource = {
|
|
2460
|
+
events(eventName: string, ...args: any[]): Stream<any>
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
declare function processDrag(
|
|
2464
|
+
sources?: { draggable?: DragSource; dropZone?: DragSource },
|
|
2465
|
+
options?: { effectAllowed?: string }
|
|
2466
|
+
): {
|
|
2467
|
+
dragStart$: Stream<DragEvent>
|
|
2468
|
+
dragEnd$: Stream<null>
|
|
2469
|
+
dragOver$: Stream<null>
|
|
2470
|
+
drop$: Stream<DragEvent>
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
type DragDriverRegistration = {
|
|
2474
|
+
category: string;
|
|
2475
|
+
draggable?: string;
|
|
2476
|
+
dropZone?: string;
|
|
2477
|
+
/** Restricts which dragging category this drop zone will accept. Omit to accept any. */
|
|
2478
|
+
accepts?: string;
|
|
2479
|
+
/** CSS selector for a drag preview element. Resolved as the nearest ancestor of the draggable element. */
|
|
2480
|
+
dragImage?: string;
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
type DragStartPayload = {
|
|
2484
|
+
element: HTMLElement;
|
|
2485
|
+
dataset: Record<string, string>;
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
type DropPayload = {
|
|
2489
|
+
dropZone: HTMLElement;
|
|
2490
|
+
insertBefore: HTMLElement | null;
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
type DragDriverCategory = {
|
|
2494
|
+
events(eventType: 'dragstart'): Stream<DragStartPayload>;
|
|
2495
|
+
events(eventType: 'dragend'): Stream<null>;
|
|
2496
|
+
events(eventType: 'drop'): Stream<DropPayload>;
|
|
2497
|
+
events(eventType: string): Stream<any>;
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
type DragDriverSource = {
|
|
2501
|
+
select(category: string): DragDriverCategory;
|
|
2502
|
+
dragstart(category: string): Stream<DragStartPayload>;
|
|
2503
|
+
dragend(category: string): Stream<null>;
|
|
2504
|
+
drop(category: string): Stream<DropPayload>;
|
|
2505
|
+
dragover(category: string): Stream<any>;
|
|
2506
|
+
dispose(): void;
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
declare function makeDragDriver(): (sink$: Stream<DragDriverRegistration | DragDriverRegistration[]>) => DragDriverSource
|
|
2510
|
+
|
|
2511
|
+
/**
|
|
2512
|
+
* The options `defineComponent()` takes: the view plus the statics a function component carries
|
|
2513
|
+
* (`model`, `intent`, `initialState`, ...), and `name` (its `componentName`; defaults to the view's
|
|
2514
|
+
* name, or 'Component' for an anonymous inline view). Statics already on the view are copied; the
|
|
2515
|
+
* options override them.
|
|
2516
|
+
*/
|
|
2517
|
+
type DefineComponentOptions<
|
|
2518
|
+
STATE = any,
|
|
2519
|
+
PROPS = { [prop: string]: any },
|
|
2520
|
+
DRIVERS = {},
|
|
2521
|
+
ACTIONS = {},
|
|
2522
|
+
CALCULATED = {},
|
|
2523
|
+
CONTEXT = {},
|
|
2524
|
+
SINK_RETURNS extends NonStateSinkReturns = {}
|
|
2525
|
+
> = {
|
|
2526
|
+
/** The view: `({ state, context, ...props }) => vnode` */
|
|
2527
|
+
view: ComponentProps<STATE & CALCULATED, PROPS, CONTEXT>;
|
|
2528
|
+
/** The component's name (its `componentName`); defaults to the view function's name */
|
|
2529
|
+
name?: string;
|
|
2530
|
+
} & Omit<Component<STATE, PROPS, DRIVERS, ACTIONS, CALCULATED, CONTEXT, SINK_RETURNS>, 'componentName' | keyof Function>
|
|
2531
|
+
|
|
2532
|
+
/**
|
|
2533
|
+
* Build a component from an options object (for code that makes components from data). Returns
|
|
2534
|
+
* an ordinary function component that calls `view`, with the other options as its statics:
|
|
2535
|
+
*
|
|
2536
|
+
* const Counter = defineComponent({ name: 'Counter', view, model, initialState: { count: 0 } })
|
|
2537
|
+
*
|
|
2538
|
+
* Writing the function and its statics directly is the usual form.
|
|
2539
|
+
*/
|
|
2540
|
+
declare function defineComponent<
|
|
2541
|
+
STATE = any,
|
|
2542
|
+
PROPS = { [prop: string]: any },
|
|
2543
|
+
DRIVERS = {},
|
|
2544
|
+
ACTIONS = {},
|
|
2545
|
+
CALCULATED = {},
|
|
2546
|
+
CONTEXT = {},
|
|
2547
|
+
SINK_RETURNS extends NonStateSinkReturns = {}
|
|
2548
|
+
>(
|
|
2549
|
+
options: DefineComponentOptions<STATE, PROPS, DRIVERS, ACTIONS, CALCULATED, CONTEXT, SINK_RETURNS>
|
|
2550
|
+
): Component<STATE, PROPS, DRIVERS, ACTIONS, CALCULATED, CONTEXT, SINK_RETURNS>
|
|
2551
|
+
|
|
2552
|
+
declare function portal(...args: any[]): any
|
|
2553
|
+
|
|
2554
|
+
declare function Collection<PROPS extends { [prop: string]: any }, STATE = any>(props: CollectionProps<PROPS, STATE>): JSX.Element
|
|
2555
|
+
/**
|
|
2556
|
+
* PLAN-5 V-1: a Collection that renders only the rows in view (+ `overscan`). Same `of` / `from` /
|
|
2557
|
+
* `filter` / `sort`; the element is its own scroll container (give `className` a bounded height).
|
|
2558
|
+
* Rows scrolled out are disposed and made again when they come back: keep row state in the array.
|
|
2559
|
+
* Jump with element commands: `{ scrollToIndex: '.rows', index }`, `{ scrollToId: '.rows', id }`.
|
|
2560
|
+
*
|
|
2561
|
+
* <VirtualCollection of={Row} from="rows" className="rows" estimateSize={32} />
|
|
2562
|
+
*/
|
|
2563
|
+
declare function VirtualCollection<PROPS extends { [prop: string]: any }, STATE = any>(props: VirtualCollectionProps<PROPS, STATE>): JSX.Element
|
|
2564
|
+
declare function Switchable<PROPS extends { [prop: string]: any }>(props: SwitchableProps<PROPS>): JSX.Element
|
|
2565
|
+
declare function Portal(props: PortalProps): JSX.Element
|
|
2566
|
+
declare function Transition(props: TransitionProps): JSX.Element
|
|
2567
|
+
declare function Suspense(props: SuspenseProps): JSX.Element
|
|
2568
|
+
declare function Slot(props: SlotProps): JSX.Element
|
|
2569
|
+
|
|
2570
|
+
/**
|
|
2571
|
+
* What `lazy()` returns: a sub-component used in JSX with its own props only
|
|
2572
|
+
* (`<Chart title="Sales" />`; `state` is optional, as for any sub-component), that
|
|
2573
|
+
* still carries the component statics (`model`, `intent`, …) once loaded.
|
|
2574
|
+
*/
|
|
2575
|
+
type LazyComponent<PROPS = any> = ((
|
|
2576
|
+
props: PROPS & { state?: any; children?: JSX.Element | JSX.Element[] }
|
|
2577
|
+
) => JSX.Element) & Omit<Component<any, PROPS>, never> & {
|
|
2578
|
+
/** start the import now (a deferred `when` one too: preload on hover, or in a test); resolves once it has loaded or failed */
|
|
2579
|
+
load(): Promise<void>
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
/**
|
|
2583
|
+
* PLAN-5 B-4: `when` defers the import until a placeholder is visible ('visible',
|
|
2584
|
+
* IntersectionObserver; `rootMargin` to start earlier) or the browser is idle after it is on the
|
|
2585
|
+
* page ('idle', requestIdleCallback with a 2 s timeout). The placeholder stays in its place: a
|
|
2586
|
+
* Suspense boundary waits for it (its fallback) only once the import has started. SSR renders the
|
|
2587
|
+
* placeholder and never loads.
|
|
2588
|
+
*/
|
|
2589
|
+
interface LazyOptions {
|
|
2590
|
+
when?: 'visible' | 'idle'
|
|
2591
|
+
rootMargin?: string
|
|
2592
|
+
/** the placeholder's min-height (a number: px; or a CSS length), e.g. the component's expected height */
|
|
2593
|
+
placeholderHeight?: number | string
|
|
2594
|
+
}
|
|
2595
|
+
|
|
2596
|
+
declare function lazy<PROPS = any>(
|
|
2597
|
+
loadFn: () => Promise<{ default: Component<any, PROPS> } | Component<any, PROPS>>,
|
|
2598
|
+
options?: LazyOptions
|
|
2599
|
+
): LazyComponent<PROPS>
|
|
2600
|
+
|
|
2601
|
+
/**
|
|
2602
|
+
* The reply-action keys of a request to makeFetchDriver or driverFromAsync: the
|
|
2603
|
+
* outcome becomes an action on exactly the component instance that sent the request, instead
|
|
2604
|
+
* of reaching `select()` / `errors()`.
|
|
2605
|
+
*
|
|
2606
|
+
* LOAD: { HTTP: (state) => ({ url: `/api/q/${state.id}`, ok: 'LOADED', error: 'FAILED' }) },
|
|
2607
|
+
* LOADED: (state, quote) => ({ ...state, quote }), // data: the parsed body
|
|
2608
|
+
* FAILED: (state, { status }) => ({ ...state, status }), // data: { error, status?, body?, request }
|
|
2609
|
+
*
|
|
2610
|
+
* The action's data type is not inferred from the request: type it in the component's ACTIONS
|
|
2611
|
+
* (`{ LOADED: Quote; FAILED: FetchFailure }`). `ok` / `error` are plain strings in the types (D70);
|
|
2612
|
+
* a name with no model entry is SYG112 (sygnal-check and the dev entry).
|
|
2613
|
+
*/
|
|
2614
|
+
type ReplyRequest = {
|
|
2615
|
+
/** Action that receives the success value (fetch: the parsed body; driverFromAsync: the resolved value) */
|
|
2616
|
+
ok?: string;
|
|
2617
|
+
/** Action that receives the failure (`{ error, request }`, plus `status` / `body` for fetch) */
|
|
2618
|
+
error?: string;
|
|
2619
|
+
/** Not allowed: a `then` key makes the request a thenable (SYG610, not sent). Use `ok` */
|
|
2620
|
+
then?: never;
|
|
2621
|
+
/** Not allowed (SYG610, not sent). Use `error` */
|
|
2622
|
+
catch?: never;
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
/**
|
|
2626
|
+
* A request to a driverFromAsync() sink: your own fields (`value`, the args, ...) plus the
|
|
2627
|
+
* reply-action keys `ok` / `error`. Type the driver's sink with it: `{ QUOTE: { source:
|
|
2628
|
+
* AsyncDriverFromFunction; sink: AsyncRequest<{ value: number }> } }`.
|
|
2629
|
+
*/
|
|
2630
|
+
type AsyncRequest<FIELDS = { [field: string]: any }> = FIELDS & ReplyRequest
|
|
2631
|
+
|
|
2632
|
+
/** Payload on `errors()` of a driverFromAsync source when a request fails */
|
|
2633
|
+
type AsyncDriverError<INCOMING = any> = {
|
|
2634
|
+
/** The rejection reason (or what `post` threw) */
|
|
2635
|
+
error: any;
|
|
2636
|
+
/** The request that failed */
|
|
2637
|
+
request: INCOMING;
|
|
2638
|
+
/** The request's selector property (default 'category') is copied here (not for an `error` reply action) */
|
|
2639
|
+
[selectorProperty: string]: any;
|
|
2640
|
+
}
|
|
2641
|
+
|
|
2642
|
+
type AsyncDriverFromFunction<INCOMING = any, OUTGOING = any> = {
|
|
2643
|
+
select: (selector?: string | ((value: OUTGOING) => boolean)) => Stream<OUTGOING>
|
|
2644
|
+
/**
|
|
2645
|
+
* Failed requests (rejected promise, rejected/throwing `post`). Filters like
|
|
2646
|
+
* `select()`. Failures are only console.error'd while nothing listens here.
|
|
2647
|
+
*/
|
|
2648
|
+
errors: (selector?: string | ((error: AsyncDriverError<INCOMING>) => boolean)) => Stream<AsyncDriverError<INCOMING>>
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
type DriverFromAsyncOptions<INCOMING = any, OUTGOING = any, RETURN = any> = {
|
|
2652
|
+
selector?: string;
|
|
2653
|
+
args?: string | string[] | ((incoming: INCOMING) => any | any[]);
|
|
2654
|
+
return?: string | undefined;
|
|
2655
|
+
pre?: (incoming: INCOMING) => INCOMING;
|
|
2656
|
+
post?: (value: RETURN, incoming: INCOMING) => OUTGOING | Promise<OUTGOING>;
|
|
2657
|
+
}
|
|
2658
|
+
|
|
2659
|
+
declare function driverFromAsync<INCOMING = any, RETURN = any, OUTGOING = any>(
|
|
2660
|
+
promiseReturningFunction: (...args: any[]) => Promise<RETURN>,
|
|
2661
|
+
options?: DriverFromAsyncOptions<INCOMING, OUTGOING, RETURN>
|
|
2662
|
+
): (fromApp$: Stream<INCOMING>) => AsyncDriverFromFunction<INCOMING, OUTGOING>
|
|
2663
|
+
|
|
2664
|
+
/** fetch() options a request (or the driver) may set under `init`; the driver owns `signal` */
|
|
2665
|
+
type FetchInit = {
|
|
2666
|
+
method?: string;
|
|
2667
|
+
headers?: Record<string, string> | Headers;
|
|
2668
|
+
body?: any;
|
|
2669
|
+
mode?: string;
|
|
2670
|
+
credentials?: 'omit' | 'same-origin' | 'include';
|
|
2671
|
+
cache?: string;
|
|
2672
|
+
redirect?: string;
|
|
2673
|
+
referrer?: string;
|
|
2674
|
+
referrerPolicy?: string;
|
|
2675
|
+
integrity?: string;
|
|
2676
|
+
keepalive?: boolean;
|
|
2677
|
+
priority?: 'high' | 'low' | 'auto';
|
|
2678
|
+
window?: null;
|
|
2679
|
+
duplex?: 'half';
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
/**
|
|
2683
|
+
* A request sent to a makeFetchDriver() sink. A plain string is a GET of that URL.
|
|
2684
|
+
* Other fetch() options go under `init` (`init: { credentials: 'include' }`). Any other key is
|
|
2685
|
+
* the app's own: not sent, but returned on the reply's `request`.
|
|
2686
|
+
*
|
|
2687
|
+
* Reply actions (canonical): `{ url, ok: 'LOADED', error: 'FAILED' }` delivers the parsed body as
|
|
2688
|
+
* LOADED, a failure as FAILED (`FetchFailure`), to exactly the sending instance (see
|
|
2689
|
+
* ReplyRequest). Without `ok` / `error` the reply goes to `select()` / `errors()`.
|
|
2690
|
+
*
|
|
2691
|
+
* Isolation: the replies, `latest` and `abort` of a component instance are its own (and its
|
|
2692
|
+
* descendants'): two instances, or Collection items, using the same category never see or
|
|
2693
|
+
* cancel each other's requests. The root component sees every reply.
|
|
2694
|
+
*/
|
|
2695
|
+
type FetchRequest = string | {
|
|
2696
|
+
/** Request URL (prefixed with the driver's `baseUrl`) */
|
|
2697
|
+
url: string;
|
|
2698
|
+
/** Reply action that receives the parsed body of a 2xx response (the Response with `parse: 'response'`) */
|
|
2699
|
+
ok?: string;
|
|
2700
|
+
/** Reply action that receives a failure, `{ error, status?, body?, request }` (FetchFailure) */
|
|
2701
|
+
error?: string;
|
|
2702
|
+
/**
|
|
2703
|
+
* Reply actions: the `latest` / `abort` group (default: the `ok` action, else `error`). Requests with
|
|
2704
|
+
* the same key from the same instance supersede each other under `latest: true`
|
|
2705
|
+
*/
|
|
2706
|
+
key?: string;
|
|
2707
|
+
/** Without reply actions: tag read back with `select(category)` / `errors(category)`; also the `latest` / `abort` group */
|
|
2708
|
+
category?: string;
|
|
2709
|
+
/** Default: 'POST' when `json` or `body` is set, else 'GET' */
|
|
2710
|
+
method?: string;
|
|
2711
|
+
/** Merged over the driver's `headers`, case-insensitively (names are sent lowercased) */
|
|
2712
|
+
headers?: Record<string, string> | Headers;
|
|
2713
|
+
/**
|
|
2714
|
+
* Appended as a query string (`{ q: 'dune' }` → `?q=dune`), before any `#fragment`; null/undefined
|
|
2715
|
+
* values are skipped; an array repeats the key (`{ tag: ['a', 'b'] }` → `?tag=a&tag=b`)
|
|
2716
|
+
*/
|
|
2717
|
+
query?: Record<string, string | number | boolean | null | undefined | Array<string | number | boolean | null | undefined>>;
|
|
2718
|
+
/** Sent as JSON.stringify(json), with `Content-Type: application/json` unless set */
|
|
2719
|
+
json?: any;
|
|
2720
|
+
/** Raw body (string, FormData, Blob, ...) */
|
|
2721
|
+
body?: any;
|
|
2722
|
+
/**
|
|
2723
|
+
* Latest only: sending this request aborts this component's requests still in flight in the
|
|
2724
|
+
* same category; their responses and errors are never delivered. Default: the driver's `latest`
|
|
2725
|
+
* option.
|
|
2726
|
+
*/
|
|
2727
|
+
latest?: boolean;
|
|
2728
|
+
/** Fail with a TimeoutError (on `errors()`) after this many ms. Default: the driver's `timeoutMs` */
|
|
2729
|
+
timeoutMs?: number;
|
|
2730
|
+
/**
|
|
2731
|
+
* How the 2xx body becomes `value`: 'auto' (default; JSON when the content-type says json,
|
|
2732
|
+
* else text; 204 → null), 'json', 'text', 'response' (the Response), or a function.
|
|
2733
|
+
*/
|
|
2734
|
+
parse?: 'auto' | 'json' | 'text' | 'response' | ((response: Response) => any);
|
|
2735
|
+
/** Other fetch() options (merged over the driver's `init`) */
|
|
2736
|
+
init?: FetchInit;
|
|
2737
|
+
/** Not allowed: a `then` key makes the request a thenable (SYG610, not sent). Use `ok` */
|
|
2738
|
+
then?: never;
|
|
2739
|
+
/** Not allowed (SYG610, not sent). Use `error` */
|
|
2740
|
+
catch?: never;
|
|
2741
|
+
/**
|
|
2742
|
+
* PLAN-3 5-3 (D79): cache this request's reply in the driver's `queryCache()` (any method; a
|
|
2743
|
+
* POST is SYG630; without a queryCache nothing is cached, SYG635). `false`: never cached (a
|
|
2744
|
+
* resource under `makeFetchDriver({ cache: queryCache() })` too). A request with reply actions
|
|
2745
|
+
* is otherwise one-send-one-request
|
|
2746
|
+
*/
|
|
2747
|
+
cache?: boolean;
|
|
2748
|
+
/** How long a cached reply stays fresh, in ms (served without a fetch). Implies `cache` */
|
|
2749
|
+
staleTime?: number;
|
|
2750
|
+
/** Invalidation tags of this request (and its cache entry): `{ invalidate: 'quotes' }` matches `tags: ['quotes']` */
|
|
2751
|
+
tags?: string[];
|
|
2752
|
+
/**
|
|
2753
|
+
* After a 2xx reply: invalidate these tags / URL prefixes / the predicate's matches (like
|
|
2754
|
+
* `{ invalidate }`). A read of them already in flight is aborted, so an older reply never lands
|
|
2755
|
+
*/
|
|
2756
|
+
invalidates?: FetchInvalidate;
|
|
2757
|
+
/**
|
|
2758
|
+
* PLAN-3 6-A (G-184; React Query's setQueryData): after a 2xx reply, write it into this
|
|
2759
|
+
* component's resources with these names (and their `queryCache()` entries) before the `ok`
|
|
2760
|
+
* action and before `invalidates`: `updates: 'item'` (the reply becomes `state.item.data`), or
|
|
2761
|
+
* `{ items: (list, reply) => newList }` to derive it. A read of them in flight is aborted;
|
|
2762
|
+
* other mounted resources on the same cache entry show it too. Resources without a request
|
|
2763
|
+
* (idle) are skipped. The `ok` action still gets the reply
|
|
2764
|
+
*/
|
|
2765
|
+
updates?: string | string[] | Record<string, true | ((data: any, reply: any) => any)>;
|
|
2766
|
+
/**
|
|
2767
|
+
* Retries (default 0): a count, or a count with the backoff of makeSocketDriver's reconnect
|
|
2768
|
+
* (`{ count: 3, delayMs: 500, maxDelayMs: 10000, jitter: 0.2 }`; count defaults to 3).
|
|
2769
|
+
* Network errors, 408, 429 (a Retry-After in seconds wins) and 5xx are retried, never other
|
|
2770
|
+
* 4xx; the failure arrives once, after the last attempt, with `attempts`
|
|
2771
|
+
*/
|
|
2772
|
+
retry?: number | FetchRetry;
|
|
2773
|
+
/**
|
|
2774
|
+
* A Standard Schema (zod, valibot, arktype, ...) the parsed 2xx body must pass; the reply is the
|
|
2775
|
+
* schema's (possibly transformed) value. A failure is an error with `issues`
|
|
2776
|
+
*/
|
|
2777
|
+
validate?: StandardSchemaLike;
|
|
2778
|
+
/** Your own fields (an id, ...): not sent, returned on the reply's `request` */
|
|
2779
|
+
[appData: string]: any;
|
|
2780
|
+
} | {
|
|
2781
|
+
/**
|
|
2782
|
+
* Cancel. Reply actions: `{ abort: 'LOADED' }` aborts this instance's requests in flight whose key
|
|
2783
|
+
* (`key`, else `ok`, else `error`) is 'LOADED'; `{ abort: true, key: 'search' }` does the same
|
|
2784
|
+
* by key. Without reply actions: `{ category: 'search', abort: true }` aborts this component's requests in
|
|
2785
|
+
* that category (all of them, reply-action ones included, without a category or key). Nothing is
|
|
2786
|
+
* delivered for a cancelled request.
|
|
2787
|
+
*/
|
|
2788
|
+
abort: true | string;
|
|
2789
|
+
key?: string;
|
|
2790
|
+
category?: string;
|
|
2791
|
+
} | {
|
|
2792
|
+
/** PLAN-3 3-A: refetch this instance's resource(s) by name, keeping data (nothing while idle) */
|
|
2793
|
+
refresh: string | string[];
|
|
2794
|
+
} | {
|
|
2795
|
+
/**
|
|
2796
|
+
* PLAN-3 5-3 (D80), from any component: matching cache entries go stale and matching mounted
|
|
2797
|
+
* resources refetch, keeping data. With or without the cache
|
|
2798
|
+
*/
|
|
2799
|
+
invalidate: FetchInvalidate;
|
|
2800
|
+
} | {
|
|
2801
|
+
/**
|
|
2802
|
+
* PLAN-3 5-5 (H-7): fetch this request into the driver's `queryCache()` without a reply (a
|
|
2803
|
+
* fresh entry or the same fetch in flight: nothing new), so a resource that reads it later
|
|
2804
|
+
* renders 'success' at once. Without a queryCache it does nothing (SYG635)
|
|
2805
|
+
*/
|
|
2806
|
+
prefetch: string | { url: string; [field: string]: any };
|
|
2807
|
+
}
|
|
2808
|
+
|
|
2809
|
+
/**
|
|
2810
|
+
* What `{ invalidate }` / `invalidates` match: a tag (`tags: ['quotes']` on the request), a URL
|
|
2811
|
+
* prefix of the request's `url` (a string starting with '/'), several of them, or a predicate
|
|
2812
|
+
*/
|
|
2813
|
+
type FetchInvalidate = string | string[] | ((request: any) => boolean);
|
|
2814
|
+
|
|
2815
|
+
/** Retry policy of a makeFetchDriver() request: the reconnect backoff of makeSocketDriver plus a count */
|
|
2816
|
+
type FetchRetry = SocketReconnect & {
|
|
2817
|
+
/** Retries after the first attempt. Default 3 in this object form */
|
|
2818
|
+
count?: number;
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
/** Any Standard Schema (https://standardschema.dev): an object with `~standard.validate` */
|
|
2822
|
+
type StandardSchemaLike = {
|
|
2823
|
+
readonly '~standard': {
|
|
2824
|
+
validate: (value: unknown) => {value?: any; issues?: ReadonlyArray<{message: string; path?: ReadonlyArray<any>}>} | Promise<{value?: any; issues?: ReadonlyArray<{message: string; path?: ReadonlyArray<any>}>}>;
|
|
2825
|
+
[key: string]: any;
|
|
2826
|
+
};
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2829
|
+
/** PLAN-3 5-3 (D79), D88: `queryCache(options)` */
|
|
2830
|
+
type FetchCacheOptions = {
|
|
2831
|
+
/** How long a reply stays fresh, in ms: a fresh entry is served without a fetch. Default 0 (always refetch, showing the cached data meanwhile) */
|
|
2832
|
+
staleTime?: number;
|
|
2833
|
+
/** How long an entry no resource uses is kept, in ms. Default 300000 (5 min); Infinity keeps it */
|
|
2834
|
+
gcTime?: number;
|
|
2835
|
+
/** Refetch stale mounted resources when the window regains focus / the page becomes visible. Default true */
|
|
2836
|
+
refetchOnFocus?: boolean;
|
|
2837
|
+
/** Refetch stale mounted resources when the browser comes back online. Default true */
|
|
2838
|
+
refetchOnReconnect?: boolean;
|
|
2839
|
+
/** PLAN-3 5-5 (H-7): entries to start with, from `dehydrate()` (SSR seeding) */
|
|
2840
|
+
initial?: QueryCacheSnapshot;
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
/** PLAN-3 5-5 (H-7): one entry of a `dehydrate()` snapshot (JSON-safe when `data` is) */
|
|
2844
|
+
type QueryCacheSnapshotEntry = {
|
|
2845
|
+
/** method, URL as written (query sorted), body and parse: `'GET /api/quotes/1'` */
|
|
2846
|
+
key: string;
|
|
2847
|
+
/** the parsed (and validated) body */
|
|
2848
|
+
data: any;
|
|
2849
|
+
/** when it was fetched (ms since the epoch): it is fresh until `updatedAt + staleTime` */
|
|
2850
|
+
updatedAt: number;
|
|
2851
|
+
/** the request's invalidation tags */
|
|
2852
|
+
tags?: string[];
|
|
2853
|
+
}
|
|
2854
|
+
type QueryCacheSnapshot = QueryCacheSnapshotEntry[];
|
|
2855
|
+
|
|
2856
|
+
/** PLAN-3 D88: the query cache of a makeFetchDriver (`makeFetchDriver({ cache: queryCache() })`) */
|
|
2857
|
+
interface QueryCache {
|
|
2858
|
+
/** the entries with data, as a JSON-safe snapshot (serialise it into the page) */
|
|
2859
|
+
dehydrate(): QueryCacheSnapshot;
|
|
2860
|
+
/** writes a snapshot's entries (an entry newer than the snapshot's is kept) */
|
|
2861
|
+
hydrate(snapshot: QueryCacheSnapshot | null | undefined): void;
|
|
2862
|
+
/** writes one entry, keyed like the request (a loader on the server: `cache.set('/api/quotes/1', quote)`) */
|
|
2863
|
+
set(request: ResourceRequest, data: any): void;
|
|
2864
|
+
/** the driver given this cache fetches the request into it, without a reply (like the `{ prefetch }` command) */
|
|
2865
|
+
prefetch(request: ResourceRequest): void;
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
/**
|
|
2869
|
+
* PLAN-3 D88: the opt-in query cache, passed to `makeFetchDriver({ cache: queryCache({ staleTime: 30000 }) })`.
|
|
2870
|
+
* Resources' GET/HEAD replies are cached (stale-while-revalidate: cached data shows at once,
|
|
2871
|
+
* `refreshing` while it refetches; an entry younger than `staleTime` is served without a fetch),
|
|
2872
|
+
* identical cacheable requests in flight share one fetch, focus / reconnect refetch stale
|
|
2873
|
+
* mounted resources, and unused entries are dropped after `gcTime`. SSR seeding:
|
|
2874
|
+
* `renderToString(App, { cache })` renders cached resources as 'success', `dehydrate()` /
|
|
2875
|
+
* `queryCache({ initial })` carry the entries to the client. One cache per driver
|
|
2876
|
+
*/
|
|
2877
|
+
declare function queryCache(options?: FetchCacheOptions): QueryCache
|
|
2878
|
+
|
|
2879
|
+
/** PLAN-3: a request a `resources` entry derives (a URL, or a request without `abort`) */
|
|
2880
|
+
type ResourceRequest = string | (Exclude<FetchRequest, string | { abort: true | string } | { refresh: string | string[] }> & {
|
|
2881
|
+
/**
|
|
2882
|
+
* Keep this resource live while its component is in a hidden Switchable page (default: a
|
|
2883
|
+
* hidden page's resources are paused, keeping their last result, and refetched when it is shown)
|
|
2884
|
+
*/
|
|
2885
|
+
background?: boolean;
|
|
2886
|
+
/** D78: on a new request (key change), keep the previous `data` (status unchanged, `refreshing: true`) instead of 'loading' (pagination) */
|
|
2887
|
+
keepPrevious?: boolean;
|
|
2888
|
+
/** Refetch every this many ms after each result (skipped while the document is hidden) */
|
|
2889
|
+
refetchEvery?: number;
|
|
2890
|
+
})
|
|
2891
|
+
|
|
2892
|
+
/**
|
|
2893
|
+
* PLAN-3: the state slot of a resource (`state.quote`), written by the built-in RESOURCE action.
|
|
2894
|
+
* `data` is the parsed (validated) body; `error` is the Error (`error.status` / `error.body` for
|
|
2895
|
+
* a non-2xx response, `error.issues` for a validation failure). D78: a refetch of the same
|
|
2896
|
+
* request keeps `data` and `error` with `refreshing: true`; a failed refetch keeps `data`.
|
|
2897
|
+
*/
|
|
2898
|
+
type Resource<DATA = any, ERROR = any> =
|
|
2899
|
+
| { status: 'idle' | 'loading'; data?: undefined; error?: undefined; refreshing?: undefined }
|
|
2900
|
+
| { status: 'success'; data: DATA; error?: undefined; refreshing?: boolean }
|
|
2901
|
+
| { status: 'error'; data?: DATA; error: ERROR; refreshing?: boolean }
|
|
2902
|
+
|
|
2903
|
+
/** The data of the `error` reply action of a makeFetchDriver() request (`error: 'FAILED'`) */
|
|
2904
|
+
type FetchFailure<REQUEST = any> = {
|
|
2905
|
+
/**
|
|
2906
|
+
* 'HTTP 404 ...' for a non-2xx status (with `.status` and `.body`), the network error
|
|
2907
|
+
* (TypeError), the body parse error, a TimeoutError (`.name === 'TimeoutError'`), or
|
|
2908
|
+
* "fetch is not available"
|
|
2909
|
+
*/
|
|
2910
|
+
error: any;
|
|
2911
|
+
/** HTTP status, for a non-2xx response (undefined for a network error or timeout) */
|
|
2912
|
+
status?: number;
|
|
2913
|
+
/** The non-2xx response's body, parsed like 'auto' */
|
|
2914
|
+
body?: any;
|
|
2915
|
+
/** The request as the app sent it */
|
|
2916
|
+
request: REQUEST;
|
|
2917
|
+
/** With `retry`: how many attempts were made */
|
|
2918
|
+
attempts?: number;
|
|
2919
|
+
/** With `validate`: the schema's issues (the body didn't validate) */
|
|
2920
|
+
issues?: ReadonlyArray<{message: string; path?: ReadonlyArray<any>}>;
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
/** A successful (2xx) response on `select()` of a makeFetchDriver() source */
|
|
2924
|
+
type FetchResponse<VALUE = any, REQUEST = any> = {
|
|
2925
|
+
/** The request's category */
|
|
2926
|
+
category: string | undefined;
|
|
2927
|
+
/** The parsed body (see `parse`) */
|
|
2928
|
+
value: VALUE;
|
|
2929
|
+
/** HTTP status */
|
|
2930
|
+
status: number;
|
|
2931
|
+
/** The request as the app sent it (any extra fields you put on it come back here) */
|
|
2932
|
+
request: REQUEST;
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
/** A failure on `errors()` of a makeFetchDriver() source */
|
|
2936
|
+
type FetchError<REQUEST = any> = {
|
|
2937
|
+
/**
|
|
2938
|
+
* 'HTTP 404 ...' for a non-2xx status (with `.status` and `.body`), the network error
|
|
2939
|
+
* (TypeError), the body parse error, a TimeoutError (`.name === 'TimeoutError'`), or
|
|
2940
|
+
* "fetch is not available"
|
|
2941
|
+
*/
|
|
2942
|
+
error: any;
|
|
2943
|
+
category: string | undefined;
|
|
2944
|
+
request: REQUEST;
|
|
2945
|
+
/** HTTP status, for a non-2xx response (undefined for a network error or timeout) */
|
|
2946
|
+
status?: number;
|
|
2947
|
+
/** The non-2xx response's body, parsed like 'auto' */
|
|
2948
|
+
body?: any;
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
type FetchSource<VALUE = any> = {
|
|
2952
|
+
/** 2xx responses: all of them, one category, or those a predicate accepts */
|
|
2953
|
+
select: (category?: string | ((response: FetchResponse<VALUE>) => boolean)) => Stream<FetchResponse<VALUE>>
|
|
2954
|
+
/** Failures. Filters like select(). While nothing listens, failures are console.error'd */
|
|
2955
|
+
errors: (category?: string | ((failure: FetchError) => boolean)) => Stream<FetchError>
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
type FetchDriverOptions = {
|
|
2959
|
+
/** Prefix for every request URL, e.g. '/api' or 'https://api.example.com' */
|
|
2960
|
+
baseUrl?: string;
|
|
2961
|
+
/** Headers for every request (a request's own `headers` win, case-insensitively) */
|
|
2962
|
+
headers?: Record<string, string> | Headers;
|
|
2963
|
+
/** fetch() options for every request, e.g. `{ credentials: 'include' }` (a request's `init` wins) */
|
|
2964
|
+
init?: FetchInit;
|
|
2965
|
+
/**
|
|
2966
|
+
* Latest only for every request (a request's own `latest` wins). Default false. renderComponent's
|
|
2967
|
+
* fake can't see this option: write `latest: true` on the request (the canonical form)
|
|
2968
|
+
*/
|
|
2969
|
+
latest?: boolean;
|
|
2970
|
+
/** Timeout for every request, in ms. Default: none */
|
|
2971
|
+
timeoutMs?: number;
|
|
2972
|
+
/** Default `parse` for every request. Default 'auto' */
|
|
2973
|
+
parse?: 'auto' | 'json' | 'text' | 'response' | ((response: Response) => any);
|
|
2974
|
+
/** The fetch implementation. Default: `globalThis.fetch`, read at each request (so test stubs apply) */
|
|
2975
|
+
fetch?: (input: string, init?: any) => Promise<any>;
|
|
2976
|
+
/**
|
|
2977
|
+
* PLAN-3 D88: the query cache, off by default: `cache: queryCache({ staleTime })`. On:
|
|
2978
|
+
* resources' GET/HEAD replies are cached (stale-while-revalidate: cached data shows at once,
|
|
2979
|
+
* `refreshing` while it refetches), identical cacheable requests in flight share one fetch,
|
|
2980
|
+
* and focus / reconnect refetch stale mounted resources
|
|
2981
|
+
*/
|
|
2982
|
+
cache?: QueryCache;
|
|
2983
|
+
/** Default `retry` for GET/HEAD requests (a request's own `retry` applies to any method). Default 0 */
|
|
2984
|
+
retry?: number | FetchRetry;
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
/**
|
|
2988
|
+
* An HTTP driver over `fetch`: `run(App, { HTTP: makeFetchDriver() })`. Canonical: a request with reply actions
|
|
2989
|
+
* (`HTTP: (state) => ({ url: '/api/quote', ok: 'LOADED', error: 'FAILED' })`) whose
|
|
2990
|
+
* outcome arrives as the LOADED (parsed body) or FAILED (FetchFailure) action of the sending
|
|
2991
|
+
* instance. Without reply actions: the model sends a
|
|
2992
|
+
* request (`HTTP: (state) => ({ category: 'quote', url: '/api/quote' })`); the intent reads
|
|
2993
|
+
* `HTTP.select('quote')` (`{ category, value, status, request }`) and `HTTP.errors('quote')`
|
|
2994
|
+
* (`{ error, category, request, status?, body? }`). Non-2xx statuses, network errors and
|
|
2995
|
+
* timeouts go to errors(), never select(). `latest: true` drops superseded requests;
|
|
2996
|
+
* `{ category, abort: true }` cancels; disposing the app aborts everything in flight.
|
|
2997
|
+
* During SSR no requests are made (server rendering runs views only). In renderComponent
|
|
2998
|
+
* tests, pass no driver and answer with `t.respond('HTTP', value)` / `t.fail('HTTP', 404)`.
|
|
406
2999
|
*/
|
|
407
|
-
|
|
3000
|
+
declare function makeFetchDriver(options?: FetchDriverOptions): ((request$: Stream<any>) => FetchSource) & { cache?: QueryCache }
|
|
408
3001
|
|
|
409
3002
|
/**
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
* `emit('REFRESH')`
|
|
3003
|
+
* Reconnect policy of a makeSocketDriver() connection. Delay of retry n (from 0):
|
|
3004
|
+
* `min(maxDelayMs, delayMs * 2^n)`, varied by ±`jitter` (a fraction). A fixed 1 s retry:
|
|
3005
|
+
* `{ delayMs: 1000, maxDelayMs: 1000, jitter: false }`. Defaults: 500 ms, 10 s, 0.2.
|
|
414
3006
|
*/
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
3007
|
+
type SocketReconnect = {
|
|
3008
|
+
delayMs?: number;
|
|
3009
|
+
maxDelayMs?: number;
|
|
3010
|
+
/** false (or 0): no jitter; true: 0.2; a number: that fraction (0.2 = ±20%) */
|
|
3011
|
+
jitter?: boolean | number;
|
|
3012
|
+
}
|
|
419
3013
|
|
|
420
|
-
/**
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
3014
|
+
/** The reply actions of a connection. Each is optional; an event without one goes to `select()` */
|
|
3015
|
+
type SocketActions = {
|
|
3016
|
+
/** Action for each incoming message: the JSON-parsed frame when it parses, else the raw data (binary as is) */
|
|
3017
|
+
message?: string;
|
|
3018
|
+
/** Action when the connection opens, every time: `SocketOpen` (`{ reconnected }`) */
|
|
3019
|
+
open?: string;
|
|
3020
|
+
/**
|
|
3021
|
+
* Action when the connection closes or fails to open without the app closing it (never for a
|
|
3022
|
+
* connection the app removed or replaced, or on dispose): `SocketClose`
|
|
3023
|
+
*/
|
|
3024
|
+
close?: string;
|
|
3025
|
+
/** Action on an error event: `{ error }` (`SocketError`) */
|
|
3026
|
+
error?: string;
|
|
3027
|
+
/**
|
|
3028
|
+
* Reconnect after a drop (default on, with jittered backoff; the driver's `reconnect` option
|
|
3029
|
+
* is the default). `false`: a drop closes the connection for good. SSE: EventSource retries
|
|
3030
|
+
* transient drops itself; this applies when it gives up
|
|
3031
|
+
*/
|
|
3032
|
+
reconnect?: false | SocketReconnect;
|
|
3033
|
+
/** Default true: connections to the same URL (and protocols) share one socket. false: a socket of its own */
|
|
3034
|
+
share?: boolean;
|
|
3035
|
+
/**
|
|
3036
|
+
* Keep this connection open while its component is in a hidden Switchable page (default: a
|
|
3037
|
+
* hidden page's connections close, and open again as new ones when it is shown)
|
|
3038
|
+
*/
|
|
3039
|
+
background?: boolean;
|
|
3040
|
+
/** Not allowed: a `then` key makes the value a thenable (SYG610). Use `message` / `open` */
|
|
3041
|
+
then?: never;
|
|
3042
|
+
catch?: never;
|
|
426
3043
|
}
|
|
427
3044
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
3045
|
+
/** A WebSocket connection of `{ connections }` */
|
|
3046
|
+
type SocketConnection = SocketActions & {
|
|
3047
|
+
/** URL or path (`/ws/rooms/general`: resolved against the page, ws: for http:, wss: for https:), after `baseUrl` */
|
|
3048
|
+
socket: string;
|
|
3049
|
+
/** WebSocket subprotocols (part of the connection's identity: a change reconnects) */
|
|
3050
|
+
protocols?: string | string[];
|
|
431
3051
|
}
|
|
432
3052
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
3053
|
+
/** A server-sent events (EventSource) connection of `{ connections }`: read-only */
|
|
3054
|
+
type SseConnection = SocketActions & {
|
|
3055
|
+
/** URL (after `baseUrl`) */
|
|
3056
|
+
sse: string;
|
|
3057
|
+
withCredentials?: boolean;
|
|
3058
|
+
/** Named events → actions: `{ 'price-update': 'PRICE' }` (data JSON-parsed when it parses) */
|
|
3059
|
+
events?: Record<string, string>;
|
|
436
3060
|
}
|
|
437
3061
|
|
|
438
3062
|
/**
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
* @example
|
|
442
|
-
* // Untyped — all fields are `string`
|
|
443
|
-
* const form$ = processForm(DOM.select('form'))
|
|
444
|
-
* // form$ is Stream<FormData> → { event, eventType, [field]: string }
|
|
445
|
-
*
|
|
446
|
-
* @example
|
|
447
|
-
* // Typed — specify expected field names
|
|
448
|
-
* const form$ = processForm<{ username: string; email: string }>(DOM.select('form'))
|
|
449
|
-
* // form$ is Stream<FormData<{ username: string; email: string }>>
|
|
450
|
-
* // form$.username is typed as string ✓
|
|
3063
|
+
* A value sent to a makeSocketDriver() sink: the sender's whole set of connections (a falsy
|
|
3064
|
+
* entry or a missing name closes that connection), or a message to send on one of them.
|
|
451
3065
|
*/
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
3066
|
+
/** A component's whole set of connections: a falsy entry (`state.room && { ... }`) means closed */
|
|
3067
|
+
type Connections = Record<string, SocketConnection | SseConnection | false | null | undefined | '' | 0>
|
|
3068
|
+
|
|
3069
|
+
type SocketRequest =
|
|
3070
|
+
| { connections: Connections }
|
|
3071
|
+
| {
|
|
3072
|
+
/** The name of one of this instance's own WebSocket connections (else SYG611, not sent) */
|
|
3073
|
+
to: string;
|
|
3074
|
+
/** Sent as JSON.stringify(json) */
|
|
3075
|
+
json?: any;
|
|
3076
|
+
/** Sent as is */
|
|
3077
|
+
text?: string;
|
|
3078
|
+
/** Sent as is (ArrayBuffer, Blob, typed array) */
|
|
3079
|
+
binary?: any;
|
|
3080
|
+
}
|
|
456
3081
|
|
|
457
|
-
/**
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
3082
|
+
/** Data of a connection's `open` action */
|
|
3083
|
+
type SocketOpen = { reconnected: boolean }
|
|
3084
|
+
/** Data of a connection's `close` action (SSE: no code / reason) */
|
|
3085
|
+
type SocketClose = { code?: number; reason?: string; willReconnect: boolean }
|
|
3086
|
+
/** Data of a connection's `error` action: the error Event (or the constructor's exception) */
|
|
3087
|
+
type SocketError = { error: any }
|
|
3088
|
+
|
|
3089
|
+
/** An event without a reply action, on `select(name?)` */
|
|
3090
|
+
type SocketEvent<DATA = any> =
|
|
3091
|
+
| { name: string; type: 'message'; data: DATA }
|
|
3092
|
+
| { name: string; type: 'open'; data: SocketOpen }
|
|
3093
|
+
| { name: string; type: 'close'; data: SocketClose }
|
|
3094
|
+
| { name: string; type: 'error'; data: SocketError }
|
|
3095
|
+
|
|
3096
|
+
type SocketSource = {
|
|
3097
|
+
/** Events of connections without an action name for that event type, for one connection name or all */
|
|
3098
|
+
select: (name?: string) => Stream<SocketEvent>
|
|
463
3099
|
}
|
|
464
3100
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
)
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
3101
|
+
type SocketDriverOptions = {
|
|
3102
|
+
/** Prefix for relative URLs, e.g. '/api' or 'https://api.example.com' */
|
|
3103
|
+
baseUrl?: string;
|
|
3104
|
+
/** Default reconnect policy (a connection's `reconnect` wins, merged over it). Default on */
|
|
3105
|
+
reconnect?: false | SocketReconnect;
|
|
3106
|
+
/** Messages kept per connection while it (re)connects; the oldest are dropped. Default 100 */
|
|
3107
|
+
queueLimit?: number;
|
|
3108
|
+
/** The WebSocket class. Default: `globalThis.WebSocket`, read at connect time (so test stubs apply) */
|
|
3109
|
+
WebSocket?: any;
|
|
3110
|
+
/** The EventSource class. Default: `globalThis.EventSource`, read at connect time */
|
|
3111
|
+
EventSource?: any;
|
|
473
3112
|
}
|
|
474
3113
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
3114
|
+
/**
|
|
3115
|
+
* WebSocket and server-sent events: `run(App, { WS: makeSocketDriver() })`. A component sends
|
|
3116
|
+
* `{ connections: { room: { socket: '/ws/rooms/general', message: 'RECEIVED', open: 'ONLINE',
|
|
3117
|
+
* close: 'DROPPED' } } }` to declare its connections (diffed by name: new ones open, removed or
|
|
3118
|
+
* falsy ones close, a changed URL reconnects) and `{ to: 'room', json: { text } }` to send.
|
|
3119
|
+
* Events arrive as the named actions on exactly that instance. Drops reconnect with backoff;
|
|
3120
|
+
* connections to the same URL are shared; a disposed instance's connections close. No
|
|
3121
|
+
* connections during SSR.
|
|
3122
|
+
*/
|
|
3123
|
+
declare function makeSocketDriver(options?: SocketDriverOptions): (sink$: Stream<any>) => SocketSource
|
|
3124
|
+
|
|
3125
|
+
/** The names of the `:params` in a route pattern: ParamNames<'/tasks/:id'> = 'id' */
|
|
3126
|
+
type ParamNames<P extends string> =
|
|
3127
|
+
P extends `${string}:${infer K}/${infer Rest}` ? K | ParamNames<`/${Rest}`> :
|
|
3128
|
+
P extends `${string}:${infer K}` ? K : never;
|
|
3129
|
+
|
|
3130
|
+
/** Route names of a route table, without the `'*'` not-found route (`string` when not literal) */
|
|
3131
|
+
type RouteName<R extends Record<string, string>> =
|
|
3132
|
+
string extends keyof R ? string : { [K in keyof R & string]: R[K] extends '*' ? never : K }[keyof R & string];
|
|
3133
|
+
|
|
3134
|
+
/** href()'s params for one pattern: required when it has `:params`, none otherwise */
|
|
3135
|
+
type RouteParamsArg<P extends string> =
|
|
3136
|
+
string extends P ? [params?: Record<string, string | number>] :
|
|
3137
|
+
[ParamNames<P>] extends [never] ? [params?: Record<string, never>] :
|
|
3138
|
+
[params: { [K in ParamNames<P>]: string | number }];
|
|
3139
|
+
|
|
3140
|
+
type RouteQuery = Record<string, string | number | boolean | null | undefined>;
|
|
3141
|
+
|
|
3142
|
+
/** The route value the `route` reply action carries */
|
|
3143
|
+
interface Route<NAME extends string = string> {
|
|
3144
|
+
/** the matched route's name (the `'*'` route's name when nothing matched; null without one) */
|
|
3145
|
+
name: NAME | null;
|
|
3146
|
+
/** decoded `:params` */
|
|
3147
|
+
params: Record<string, string>;
|
|
3148
|
+
/** the query string as an object (the last value of a repeated key) */
|
|
3149
|
+
query: Record<string, string>;
|
|
3150
|
+
/** the decoded fragment without '#' ('' when none) */
|
|
3151
|
+
hash: string;
|
|
3152
|
+
/** the path without `base`, normalised: no trailing slash, no empty segments */
|
|
3153
|
+
path: string;
|
|
483
3154
|
}
|
|
484
3155
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}
|
|
3156
|
+
/** A value for the router's sink (`ROUTER`); `{ route }` is reserved for the `route` static */
|
|
3157
|
+
type RouterCommand<R extends Record<string, string> = Record<string, string>> =
|
|
3158
|
+
| { to: RouteName<R>; params?: Record<string, string | number>; query?: RouteQuery; hash?: string; replace?: boolean; scroll?: boolean; force?: boolean; block?: string | false | null }
|
|
3159
|
+
| { url: string; replace?: boolean; scroll?: boolean; force?: boolean; block?: string | false | null }
|
|
3160
|
+
| { back: true; force?: boolean; block?: string | false | null } | { forward: true; force?: boolean; block?: string | false | null } | { go: number; force?: boolean; block?: string | false | null }
|
|
3161
|
+
| { block: string | false | null }
|
|
3162
|
+
| { prefetch: RouteName<R> | string; params?: Record<string, string | number>; query?: RouteQuery };
|
|
489
3163
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
3164
|
+
/**
|
|
3165
|
+
* The data of a block action (`{ block: 'CONFIRM_LEAVE' }`): the navigation that was not made.
|
|
3166
|
+
* Send `proceed` to the router's sink to make it anyway.
|
|
3167
|
+
*/
|
|
3168
|
+
interface RouterBlocked {
|
|
3169
|
+
/** the URL the navigation would go to */
|
|
3170
|
+
to: string;
|
|
3171
|
+
route: Route;
|
|
3172
|
+
proceed: RouterCommand;
|
|
493
3173
|
}
|
|
494
3174
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
3175
|
+
interface RouterOptions<R extends Record<string, string> = Record<string, string>> {
|
|
3176
|
+
/** `{ name: '/path/:param' | '*' }`; first match wins; `'*'` is the not-found route */
|
|
3177
|
+
routes: R;
|
|
3178
|
+
/** path prefix the app lives under ('/app'); in hash mode, the page's path */
|
|
3179
|
+
base?: string;
|
|
3180
|
+
/** 'history' (default) or 'hash' (`/#/tasks/1`) */
|
|
3181
|
+
mode?: 'history' | 'hash';
|
|
3182
|
+
/** scroll to top on a push, restore on back/forward. Default true (false with `navigate`) */
|
|
3183
|
+
scroll?: boolean;
|
|
3184
|
+
/**
|
|
3185
|
+
* After a push or back/forward, once the DOM is quiet, focus the first match of these
|
|
3186
|
+
* comma-separated selectors, tried in order. Default '[data-router-focus],main h1,h1'; false disables
|
|
3187
|
+
*/
|
|
3188
|
+
focus?: string | false;
|
|
3189
|
+
/** quiet time (ms) before scroll restore and focus. Default 30 */
|
|
3190
|
+
settleMs?: number;
|
|
3191
|
+
/**
|
|
3192
|
+
* called for `{ prefetch }` commands, e.g. to warm a route's data in the fetch driver's cache:
|
|
3193
|
+
* `prefetch: (route) => routeData[route.name]?.(route).forEach(cache.prefetch)`
|
|
3194
|
+
*/
|
|
3195
|
+
prefetch?: (route: Route, url: string) => void;
|
|
3196
|
+
/** Vike: its `navigate()` (from 'vike/client/router'); the router then leaves links and history to Vike */
|
|
3197
|
+
navigate?: (url: string, options: { overwriteLastHistoryEntry: boolean }) => any;
|
|
3198
|
+
/** test seams: default the global window and its history, location and document */
|
|
3199
|
+
window?: any;
|
|
3200
|
+
history?: any;
|
|
3201
|
+
location?: any;
|
|
3202
|
+
document?: any;
|
|
500
3203
|
}
|
|
501
3204
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
drop(category: string): Stream<DropPayload>;
|
|
507
|
-
dragover(category: string): Stream<any>;
|
|
3205
|
+
/** The ROUTER source: reply actions plus the latest route */
|
|
3206
|
+
interface RouterSource {
|
|
3207
|
+
current(): Route | null;
|
|
3208
|
+
href: (name: string, params?: Record<string, string | number>, query?: RouteQuery, hash?: string) => string;
|
|
508
3209
|
dispose(): void;
|
|
509
3210
|
}
|
|
510
3211
|
|
|
511
|
-
|
|
3212
|
+
interface Router<R extends Record<string, string> = Record<string, string>> {
|
|
3213
|
+
routes: R;
|
|
3214
|
+
/** a link to a named route (pure, SSR-safe): href('task', { id: 2 }, { tab: 'notes' }) */
|
|
3215
|
+
href<N extends RouteName<R>>(name: N, ...args: [...RouteParamsArg<R[N]>, query?: RouteQuery, hash?: string]): string;
|
|
3216
|
+
/** the Route of a URL (a path or an absolute URL); pure */
|
|
3217
|
+
match(url: string): Route<RouteName<R>>;
|
|
3218
|
+
/** the Route of the current location, or of `url` (SSR: the request URL). The `initialState.route` seed */
|
|
3219
|
+
current(url?: string): Route<RouteName<R>>;
|
|
3220
|
+
/** the driver: `run(App, { ROUTER: router.driver })` */
|
|
3221
|
+
driver: (sink$: Stream<any>) => RouterSource;
|
|
3222
|
+
options: RouterOptions<R>;
|
|
3223
|
+
}
|
|
512
3224
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
calculated?: Component<STATE, PROPS, DRIVERS, ACTIONS, CALCULATED, CONTEXT, SINK_RETURNS>['calculated'];
|
|
532
|
-
storeCalculatedInState?: boolean;
|
|
533
|
-
DOMSourceName?: string;
|
|
534
|
-
stateSourceName?: string;
|
|
535
|
-
requestSourceName?: string;
|
|
536
|
-
debug?: boolean;
|
|
3225
|
+
/**
|
|
3226
|
+
* The SPA router: `export const router = makeRouter({ routes: { home: '/', task: '/tasks/:id',
|
|
3227
|
+
* notFound: '*' } })`, then `run(App, { ROUTER: router.driver })`. Components declare
|
|
3228
|
+
* `App.route = 'ROUTE'` and store the route; views link with `<a href={router.href('task', { id })}>`
|
|
3229
|
+
* (clicks are intercepted at the document); models navigate with `ROUTER: { to: 'task', params }`.
|
|
3230
|
+
*/
|
|
3231
|
+
declare function makeRouter<const R extends Record<string, string>>(options: RouterOptions<R>): Router<R>
|
|
3232
|
+
|
|
3233
|
+
/** `makeRouter(options).driver` */
|
|
3234
|
+
declare function makeRouterDriver<const R extends Record<string, string>>(options: RouterOptions<R>): (sink$: Stream<any>) => RouterSource
|
|
3235
|
+
|
|
3236
|
+
/** A `head` static value or HEAD sink value */
|
|
3237
|
+
interface HeadValue {
|
|
3238
|
+
title?: string;
|
|
3239
|
+
/** `{ description: '...', 'og:title': '...' }`: og:/article:/... keys use `property`, others `name`; null removes */
|
|
3240
|
+
meta?: Record<string, string | null | undefined>;
|
|
3241
|
+
/** link tags; merged by `key`, else by `rel` for canonical, else by `rel` + `href` */
|
|
3242
|
+
link?: Array<{ rel: string; href: string; key?: string; [attr: string]: any }>;
|
|
537
3243
|
}
|
|
538
3244
|
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
)
|
|
3245
|
+
/**
|
|
3246
|
+
* Document title, meta and link tags: `run(App, { HEAD: makeHeadDriver() })` with a `head`
|
|
3247
|
+
* static (`App.head = (state) => ({ title })`) or HEAD sink values from a model entry.
|
|
3248
|
+
* `titleTemplate: '%s · Tasks'` formats every title.
|
|
3249
|
+
*/
|
|
3250
|
+
/**
|
|
3251
|
+
* PLAN-4 GS-12: a DOM driver that runs the patches asked for by a component's
|
|
3252
|
+
* `viewTransitions` static inside `document.startViewTransition()`:
|
|
3253
|
+
* `run(App, { DOM: makeViewTransitionDOMDriver('#root') })`. It is `makeDOMDriver(mountPoint,
|
|
3254
|
+
* options)` plus the hook: one action's patches (a
|
|
3255
|
+
* Collection move is several) are folded into one transition (20 ms quiet window, capped at
|
|
3256
|
+
* 200 ms); `prefers-reduced-motion: reduce` and browsers without the API patch at once.
|
|
3257
|
+
*/
|
|
3258
|
+
declare function makeViewTransitionDOMDriver(mountPoint?: string | Element | DocumentFragment, options?: DOMDriverOptions): (vnode$: Stream<any>, name?: string) => MainDOMSource
|
|
550
3259
|
|
|
551
|
-
|
|
552
|
-
export function switchable(...args: any[]): any
|
|
553
|
-
export function portal(...args: any[]): any
|
|
3260
|
+
declare function makeHeadDriver(options?: { titleTemplate?: string; document?: any }): (sink$: Stream<any>) => { dispose(): void }
|
|
554
3261
|
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
3262
|
+
/**
|
|
3263
|
+
* PLAN-4 GS-7: one timer of a `timers` static. `every`: a positive interval in ms, drift-free
|
|
3264
|
+
* (tick n is due at start + n * every; a late tick coalesces the missed ones and n jumps);
|
|
3265
|
+
* `after`: once, after ms (0 or more); `frame`: every animation frame (requestAnimationFrame,
|
|
3266
|
+
* else every 16 ms). `background: true` keeps it running while its Switchable page is hidden.
|
|
3267
|
+
* An invalid spec is not started (SYG422 in dev).
|
|
3268
|
+
*/
|
|
3269
|
+
type TimerSpec<ACTION extends string = string> =
|
|
3270
|
+
| { every: number; action: ACTION; background?: boolean; after?: never; frame?: never }
|
|
3271
|
+
| { after: number; action: ACTION; background?: boolean; every?: never; frame?: never }
|
|
3272
|
+
| { frame: ACTION; background?: boolean; every?: never; after?: never; action?: never }
|
|
560
3273
|
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
): Component<any, PROPS>
|
|
3274
|
+
/** A component's whole set of timers, by name: a falsy entry (`state.running && { ... }`) is stopped */
|
|
3275
|
+
type Timers<ACTION extends string = string> = { [name: string]: TimerSpec<ACTION> | false | null | undefined | 0 | '' }
|
|
564
3276
|
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
3277
|
+
/** The data of an `every` timer's action: the tick number from 1 (it jumps over coalesced ticks) and Date.now() */
|
|
3278
|
+
interface TimerTick { n: number; t: number }
|
|
3279
|
+
/** The data of an `after` timer's action: Date.now() */
|
|
3280
|
+
interface TimerAfter { t: number }
|
|
3281
|
+
/** The data of a `frame` timer's action: Date.now() and the ms since the previous frame (0 on the first) */
|
|
3282
|
+
interface TimerFrame { t: number; dt: number }
|
|
568
3283
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
3284
|
+
/**
|
|
3285
|
+
* PLAN-4 GS-7: runs the components' `timers` statics: `run(App, { TIMER: makeTimerDriver() })`.
|
|
3286
|
+
* The key is free (the core finds the driver by the static it takes); `TIMER` by convention.
|
|
3287
|
+
* Each timer's action goes to the instance that declared it. A disposed instance's timers stop;
|
|
3288
|
+
* app dispose stops them all. A component declaring `timers` with no timer driver gets SYG643 in dev.
|
|
3289
|
+
*/
|
|
3290
|
+
declare function makeTimerDriver(): (sink$: Stream<any>) => { dispose(): void }
|
|
3291
|
+
|
|
3292
|
+
/** PLAN-5 B-3: fields every browser-source spec takes */
|
|
3293
|
+
interface BrowserSpecBase<ACTION extends string> {
|
|
3294
|
+
/** the action each event (or the current value) is delivered as */
|
|
3295
|
+
action: ACTION;
|
|
3296
|
+
/** keep it running while its Switchable page is hidden */
|
|
3297
|
+
background?: boolean;
|
|
575
3298
|
}
|
|
3299
|
+
/**
|
|
3300
|
+
* PLAN-5 B-3: one entry of a `browser` static; its one source key is its kind:
|
|
3301
|
+
* - `intersection`: the elements a selector matches in this component (`true`: its root element)
|
|
3302
|
+
* entering or leaving the viewport (IntersectionObserver), data `BrowserIntersection`;
|
|
3303
|
+
* - `resize`: their content-box size (ResizeObserver), data `BrowserResize`;
|
|
3304
|
+
* - `media`: a media query, data `{ matches, media }` (the current value first);
|
|
3305
|
+
* - `storage`: a localStorage key (`area: 'session'`: sessionStorage; `json: true`: parsed), read
|
|
3306
|
+
* and observed (other tabs' writes, and BROWSER `setItem` / `removeItem`), data `{ key, value }`;
|
|
3307
|
+
* for state that should survive a reload use `persist()`;
|
|
3308
|
+
* - `visibility: true`: the document's visibility, data `{ visible }`;
|
|
3309
|
+
* - `online: true`: the network, data `{ online }`;
|
|
3310
|
+
* - `geolocation`: watchPosition (permission-gated; `true` or PositionOptions), data
|
|
3311
|
+
* `BrowserPosition`, failures `{ code, message }` to `error`.
|
|
3312
|
+
* An invalid spec is not started (SYG663 in dev).
|
|
3313
|
+
*/
|
|
3314
|
+
type BrowserSpec<ACTION extends string = string> =
|
|
3315
|
+
| (BrowserSpecBase<ACTION> & { intersection: string | true; threshold?: number | number[]; rootMargin?: string })
|
|
3316
|
+
| (BrowserSpecBase<ACTION> & { resize: string | true })
|
|
3317
|
+
| (BrowserSpecBase<ACTION> & { media: string })
|
|
3318
|
+
| (BrowserSpecBase<ACTION> & { storage: string; area?: 'local' | 'session'; json?: boolean; error?: ACTION })
|
|
3319
|
+
| (BrowserSpecBase<ACTION> & { visibility: true })
|
|
3320
|
+
| (BrowserSpecBase<ACTION> & { online: true })
|
|
3321
|
+
| (BrowserSpecBase<ACTION> & { geolocation: true | { enableHighAccuracy?: boolean; maximumAge?: number; timeout?: number }; error?: ACTION })
|
|
3322
|
+
|
|
3323
|
+
/** A component's browser sources, by name: a falsy entry (`!state.seen && { ... }`) is stopped */
|
|
3324
|
+
type BrowserSources<ACTION extends string = string> = { [name: string]: BrowserSpec<ACTION> | false | null | undefined | 0 | '' }
|
|
3325
|
+
|
|
3326
|
+
/** The data of an `intersection` action: visible (isIntersecting), the ratio, which matched element (its index and dataset) */
|
|
3327
|
+
interface BrowserIntersection { visible: boolean; ratio: number; index: number; dataset: Record<string, string> }
|
|
3328
|
+
/** The data of a `resize` action: the content box, which matched element */
|
|
3329
|
+
interface BrowserResize { width: number; height: number; index: number; dataset: Record<string, string> }
|
|
3330
|
+
/** The data of a `geolocation` action */
|
|
3331
|
+
interface BrowserPosition { latitude: number; longitude: number; accuracy: number; altitude: number | null; altitudeAccuracy: number | null; heading: number | null; speed: number | null; timestamp: number }
|
|
576
3332
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
3333
|
+
/**
|
|
3334
|
+
* PLAN-5 B-3: a command for the browser driver's sink, from a model entry
|
|
3335
|
+
* (`COPY: { BROWSER: (state) => ({ copy: state.link, ok: 'COPIED' }) }`); the method is its
|
|
3336
|
+
* `copy`, `paste`, `setItem` or `removeItem` key, in any order. `ok` / `error` name reply actions (copy/paste: `{ text }`; a failure `{ name, message }`).
|
|
3337
|
+
*/
|
|
3338
|
+
type BrowserCommand =
|
|
3339
|
+
| { copy: string; ok?: string; error?: string }
|
|
3340
|
+
| { paste: true; ok: string; error?: string }
|
|
3341
|
+
| { setItem: string; value: any; area?: 'local' | 'session'; json?: boolean; ok?: string; error?: string }
|
|
3342
|
+
| { removeItem: string; area?: 'local' | 'session'; ok?: string; error?: string }
|
|
3343
|
+
|
|
3344
|
+
/** PLAN-5 B-3: a browser source for makeBrowserDriverWith (its declaration kinds and commands) */
|
|
3345
|
+
interface BrowserSource { d?: Record<string, Function>; c?: Record<string, Function> }
|
|
3346
|
+
/** `intersection` declarations */
|
|
3347
|
+
declare const intersectionSource: BrowserSource
|
|
3348
|
+
/** `resize` declarations */
|
|
3349
|
+
declare const resizeSource: BrowserSource
|
|
3350
|
+
/** `media` declarations */
|
|
3351
|
+
declare const mediaSource: BrowserSource
|
|
3352
|
+
/** `storage` declarations and the `setItem` / `removeItem` commands */
|
|
3353
|
+
declare const storageSource: BrowserSource
|
|
3354
|
+
/** `visibility` declarations */
|
|
3355
|
+
declare const visibilitySource: BrowserSource
|
|
3356
|
+
/** `online` declarations */
|
|
3357
|
+
declare const onlineSource: BrowserSource
|
|
3358
|
+
/** `geolocation` declarations */
|
|
3359
|
+
declare const geolocationSource: BrowserSource
|
|
3360
|
+
/** the `copy` / `paste` commands */
|
|
3361
|
+
declare const clipboardSource: BrowserSource
|
|
3362
|
+
|
|
3363
|
+
/**
|
|
3364
|
+
* PLAN-5 B-3: runs the components' `browser` statics and the BROWSER sink commands, with every
|
|
3365
|
+
* source: `run(App, { BROWSER: makeBrowserDriver() })`. The key is free (the core finds the driver
|
|
3366
|
+
* by the static it takes); `BROWSER` by convention. A component declaring `browser` with no
|
|
3367
|
+
* browser driver gets SYG643 in dev.
|
|
3368
|
+
*/
|
|
3369
|
+
declare function makeBrowserDriver(): (sink$: Stream<any>, name?: string) => { dispose(): void }
|
|
3370
|
+
/** PLAN-5 B-3: makeBrowserDriver() with only the given sources (the others add no bytes): `makeBrowserDriverWith(intersectionSource, mediaSource)` */
|
|
3371
|
+
declare function makeBrowserDriverWith(...sources: BrowserSource[]): (sink$: Stream<any>, name?: string) => { dispose(): void }
|
|
581
3372
|
|
|
582
|
-
|
|
3373
|
+
/** The tags for the head values `renderToString(App, { head: list })` collected (SSR) */
|
|
3374
|
+
declare function renderHead(list: Array<HeadValue | null | undefined | false>, options?: { titleTemplate?: string }): string
|
|
3375
|
+
|
|
3376
|
+
interface Ref<T = HTMLElement> {
|
|
583
3377
|
current: T | null;
|
|
584
3378
|
}
|
|
585
3379
|
|
|
586
|
-
|
|
3380
|
+
interface Ref$<T = HTMLElement> extends Ref<T> {
|
|
587
3381
|
stream: MemoryStream<T | null>;
|
|
588
3382
|
}
|
|
589
3383
|
|
|
590
|
-
|
|
591
|
-
|
|
3384
|
+
declare function createRef<T = HTMLElement>(): Ref<T>
|
|
3385
|
+
declare function createRef$<T = HTMLElement>(): Ref$<T>
|
|
592
3386
|
|
|
593
|
-
|
|
3387
|
+
interface Command {
|
|
594
3388
|
send(type: string, data?: any): void;
|
|
595
3389
|
}
|
|
596
3390
|
|
|
597
|
-
|
|
3391
|
+
interface CommandSource {
|
|
598
3392
|
select(type: string): Stream<any>;
|
|
599
3393
|
}
|
|
600
3394
|
|
|
601
|
-
|
|
3395
|
+
declare function createCommand(): Command
|
|
602
3396
|
|
|
603
3397
|
// ── PWA Helpers ──────────────────────────────────────────────
|
|
604
3398
|
|
|
605
|
-
|
|
3399
|
+
interface ServiceWorkerSource {
|
|
606
3400
|
select(type?: 'installed' | 'activated' | 'waiting' | 'controlling' | 'error' | 'message' | string): Stream<any>;
|
|
607
3401
|
}
|
|
608
3402
|
|
|
609
|
-
|
|
3403
|
+
interface ServiceWorkerCommand {
|
|
610
3404
|
action: 'skipWaiting' | 'postMessage' | 'unregister';
|
|
611
3405
|
data?: any;
|
|
612
3406
|
}
|
|
613
3407
|
|
|
614
|
-
|
|
3408
|
+
interface ServiceWorkerOptions {
|
|
615
3409
|
scope?: string;
|
|
616
3410
|
}
|
|
617
3411
|
|
|
618
|
-
|
|
3412
|
+
declare function makeServiceWorkerDriver(
|
|
619
3413
|
scriptUrl: string,
|
|
620
3414
|
options?: ServiceWorkerOptions
|
|
621
3415
|
): (sink$: Stream<ServiceWorkerCommand>) => ServiceWorkerSource
|
|
622
3416
|
|
|
623
|
-
|
|
3417
|
+
declare const onlineStatus$: Stream<boolean>
|
|
624
3418
|
|
|
625
|
-
|
|
3419
|
+
interface InstallPrompt {
|
|
626
3420
|
select(type: 'beforeinstallprompt' | 'appinstalled'): Stream<any>;
|
|
627
3421
|
prompt(): Promise<any> | undefined;
|
|
628
3422
|
}
|
|
629
3423
|
|
|
630
|
-
|
|
3424
|
+
declare function createInstallPrompt(): InstallPrompt
|
|
3425
|
+
|
|
3426
|
+
interface SimulatedEventInit {
|
|
3427
|
+
/** Merged into `event.target` (value, checked, dataset, ...) */
|
|
3428
|
+
target?: Record<string, any>;
|
|
3429
|
+
/** Shorthand for target.value */
|
|
3430
|
+
value?: any;
|
|
3431
|
+
/** Shorthand for target.checked */
|
|
3432
|
+
checked?: boolean;
|
|
3433
|
+
/** Merged into target.dataset (values become strings, like the DOM) */
|
|
3434
|
+
dataset?: Record<string, any>;
|
|
3435
|
+
/** Alias for dataset */
|
|
3436
|
+
data?: Record<string, any>;
|
|
3437
|
+
/** Keyboard key (e.key) */
|
|
3438
|
+
key?: string;
|
|
3439
|
+
/**
|
|
3440
|
+
* Don't fail when the selector matches no rendered element: wait up to 300ms for it, then
|
|
3441
|
+
* drop the event with SYG103 (info). Not copied onto the event.
|
|
3442
|
+
*/
|
|
3443
|
+
allowMissing?: boolean;
|
|
3444
|
+
/**
|
|
3445
|
+
* Target the matching element inside the first element matching this selector or control
|
|
3446
|
+
* (e.g. one Collection item): `t.simulateEvent(Done, 'click', { within: '[data-id="2"]' })`.
|
|
3447
|
+
* Not copied onto the event.
|
|
3448
|
+
*/
|
|
3449
|
+
within?: string | AnyControl;
|
|
3450
|
+
/** Any other event properties are copied onto the event */
|
|
3451
|
+
[prop: string]: any;
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3454
|
+
/**
|
|
3455
|
+
* Which request a t.respond()/t.fail() answers, as options. An object with only these keys is
|
|
3456
|
+
* options; any other object is a request pattern (see `respond`).
|
|
3457
|
+
*/
|
|
3458
|
+
interface FakeReplyOptions {
|
|
3459
|
+
/** Only requests with this category */
|
|
3460
|
+
category?: string;
|
|
3461
|
+
/**
|
|
3462
|
+
* The request to answer, compared by value: a request object (e.g. an element of
|
|
3463
|
+
* t.requests(name), or the constant the model returns; among equal pending requests that very
|
|
3464
|
+
* object, else the newest), a partial request (`{ url: '/a' }`), a URL string, or a predicate
|
|
3465
|
+
* `(request) => boolean`. `null`: push the value without a request (for a source that emits
|
|
3466
|
+
* on its own); `category` then sets its category.
|
|
3467
|
+
*/
|
|
3468
|
+
request?: any;
|
|
3469
|
+
/** respond(): the status (default 200). fail(): an HTTP error response with this status */
|
|
3470
|
+
status?: number;
|
|
3471
|
+
/** fail(): the parsed error body */
|
|
3472
|
+
body?: any;
|
|
3473
|
+
/**
|
|
3474
|
+
* That very request by its position in t.requests(name) (counting only those matching
|
|
3475
|
+
* `request`/`category`): 0 the first, -1 the newest. Answers the older of two identical
|
|
3476
|
+
* requests; throws at the call when it is no longer pending (answered, aborted, superseded).
|
|
3477
|
+
*/
|
|
3478
|
+
nth?: number;
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3481
|
+
/** A connection on a driverless socket sink, as `t.connections(name)` lists it: the spec as declared plus these */
|
|
3482
|
+
interface FakeConnection {
|
|
3483
|
+
/** Its name in `{ connections: { [name]: spec } }` */
|
|
3484
|
+
name: string;
|
|
3485
|
+
/** The URL as declared (WebSocket) */
|
|
3486
|
+
socket?: string;
|
|
3487
|
+
/** The URL as declared (server-sent events) */
|
|
3488
|
+
sse?: string;
|
|
3489
|
+
/** The URL it opened (a socket path resolves to ws:/wss: on the page's host) */
|
|
3490
|
+
url: string;
|
|
3491
|
+
/** 'closed': dropped (t.drop), waiting for a retry, or gone (reconnect: false) */
|
|
3492
|
+
state: 'connecting' | 'open' | 'closed';
|
|
3493
|
+
/** The name of the component that declared it */
|
|
3494
|
+
sender: string;
|
|
3495
|
+
[key: string]: any;
|
|
3496
|
+
}
|
|
3497
|
+
/**
|
|
3498
|
+
* Which connections t.open / t.push / t.drop act on: a connection name or URL (as declared or
|
|
3499
|
+
* opened), a partial FakeConnection compared by value (`{ socket: '/ws/a' }`), or a predicate.
|
|
3500
|
+
* Nothing: the newest connection that can take the call.
|
|
3501
|
+
*/
|
|
3502
|
+
type FakeConnectionTarget = string | Record<string, any> | ((connection: FakeConnection) => boolean);
|
|
3503
|
+
|
|
3504
|
+
/** PLAN-3 5-3: a cache entry of an HTTP fake (t.cache) */
|
|
3505
|
+
type FakeCacheEntry = {
|
|
3506
|
+
/** method, URL (query sorted), body and parse */
|
|
3507
|
+
key: string;
|
|
3508
|
+
/** ms since the reply was cached (undefined before data arrives) */
|
|
3509
|
+
age?: number;
|
|
3510
|
+
/** older than staleTime, or invalidated */
|
|
3511
|
+
stale: boolean;
|
|
3512
|
+
/** mounted resources using it */
|
|
3513
|
+
subscribers: number;
|
|
3514
|
+
/** the parsed body */
|
|
3515
|
+
data: any;
|
|
3516
|
+
}
|
|
631
3517
|
|
|
632
|
-
|
|
3518
|
+
interface RenderOptions {
|
|
633
3519
|
/** Override initial state (defaults to component's .initialState) */
|
|
634
3520
|
initialState?: any;
|
|
3521
|
+
/**
|
|
3522
|
+
* D214: context from the ancestors the rendered component would have, for testing a child
|
|
3523
|
+
* alone: `renderComponent(Greeting, { context: { lang: 'fr' } })`. The view, reducers and
|
|
3524
|
+
* descendants read it as `context.lang`; the component's own `.context` entries win over a key
|
|
3525
|
+
* of the same name. Fixed values for the test's lifetime.
|
|
3526
|
+
*/
|
|
3527
|
+
context?: Record<string, any>;
|
|
635
3528
|
/** Mock DOM configuration — maps selectors to event streams */
|
|
636
3529
|
mockConfig?: Record<string, any>;
|
|
637
|
-
/**
|
|
3530
|
+
/** The app-level error hook, as run()'s `onError` (PLAN-4 GS-11) */
|
|
3531
|
+
onError?: AppErrorHook;
|
|
3532
|
+
/**
|
|
3533
|
+
* Additional drivers beyond DOM, EVENTS, STATE, and LOG. A custom sink without a driver, in
|
|
3534
|
+
* the component or any child, gets a recording one (read it with sinkValues / requests), and
|
|
3535
|
+
* a source without a driver a scriptable fake (answer with respond / fail).
|
|
3536
|
+
*/
|
|
638
3537
|
drivers?: Record<string, any>;
|
|
3538
|
+
/**
|
|
3539
|
+
* Diagnostics mode while rendered. Default: 'collect' (error-severity messages still print),
|
|
3540
|
+
* or the current mode when diagnostics are already on. Restored when the last instance is
|
|
3541
|
+
* disposed. Dev checks require `import 'sygnal/diagnostics'` (the Vite plugin adds it under Vitest).
|
|
3542
|
+
*/
|
|
3543
|
+
diagnostics?: DiagnosticsMode;
|
|
3544
|
+
/** Enable strict (canonical-form) runtime checks while rendered (requires 'sygnal/diagnostics') */
|
|
3545
|
+
strict?: boolean;
|
|
3546
|
+
/**
|
|
3547
|
+
* settle()'s quiet window in ms (default 20). A model `next('X', data, ms)` with a longer
|
|
3548
|
+
* delay fires after settle() resolved: raise this, or wait with `t.next(pred)`.
|
|
3549
|
+
*/
|
|
3550
|
+
settleMs?: number;
|
|
3551
|
+
/** How long simulateEvent waits for a matching element / its listeners, in ms (default 300) */
|
|
3552
|
+
eventWaitMs?: number;
|
|
3553
|
+
/** Default timeout of next(), waitForState() and settle(), in ms (default 2000) */
|
|
3554
|
+
timeoutMs?: number;
|
|
3555
|
+
/**
|
|
3556
|
+
* 'mock' (default): the mock DOM. 'real': mount into a real container element (needs a DOM:
|
|
3557
|
+
* `// @vitest-environment jsdom`, or `environment: 'jsdom'` / 'happy-dom'), so `checked`,
|
|
3558
|
+
* `value`, `disabled`, focus, refs and Portals are real. simulateEvent then dispatches a real
|
|
3559
|
+
* event on the first element matching any CSS selector (a value/checked init is set on the
|
|
3560
|
+
* element first; 'click' runs the default action and skips disabled controls; 'focus'/'blur'
|
|
3561
|
+
* move focus). Read elements with `t.query(sel)` / `t.queryAll(sel)` / `t.container`. While
|
|
3562
|
+
* it runs, what jsdom lacks is added (and removed after): the `<dialog>` and popover methods,
|
|
3563
|
+
* scrollIntoView, and for Zag's machines ResizeObserver, CSS.escape and Element#scrollTo.
|
|
3564
|
+
*/
|
|
3565
|
+
dom?: 'mock' | 'real';
|
|
3566
|
+
/**
|
|
3567
|
+
* Fake socket connections open by themselves (default true; reconnects too; a t.push / t.drop
|
|
3568
|
+
* right after the declaration opens it first). false: they stay 'connecting' until t.open(),
|
|
3569
|
+
* for "Connecting…" assertions and failures to open (t.drop on a connecting one).
|
|
3570
|
+
*/
|
|
3571
|
+
autoConnect?: boolean;
|
|
3572
|
+
/**
|
|
3573
|
+
* PLAN-3 G-160: the driverless sink that receives the components' `connections` static
|
|
3574
|
+
* (default 'WS'). It is created even when no model entry names it. Pass a driver for it in
|
|
3575
|
+
* `drivers` to use a real one.
|
|
3576
|
+
*/
|
|
3577
|
+
socketSink?: string;
|
|
3578
|
+
/**
|
|
3579
|
+
* PLAN-3: the driverless sink that receives the components' `resources`
|
|
3580
|
+
* static (default 'HTTP'); each resource fetch is pending until t.respond / t.fail, and is
|
|
3581
|
+
* listed in t.requests as `{ url, ...request, resource: name }`.
|
|
3582
|
+
*/
|
|
3583
|
+
resourceSink?: string;
|
|
3584
|
+
/**
|
|
3585
|
+
* PLAN-3 5-3: options for the HTTP fakes' makeFetchDriver (all but `fetch`), e.g.
|
|
3586
|
+
* `{ cache: queryCache() }`. Focus / reconnect refetches come only from t.focus() / t.online()
|
|
3587
|
+
*/
|
|
3588
|
+
http?: FetchDriverOptions;
|
|
3589
|
+
/**
|
|
3590
|
+
* The app's router (the object `makeRouter()` returns). With no driver for `routerSink` in
|
|
3591
|
+
* `drivers`, renderComponent runs its real driver over an in-memory window (location, history,
|
|
3592
|
+
* popstate a task later, document listeners): read with `t.location`, drive with
|
|
3593
|
+
* `t.navigate` / `t.back` / `t.forward`; the commands the app sent are `t.sent('ROUTER')`.
|
|
3594
|
+
* Required when the component declares `route` (renderComponent throws naming it otherwise).
|
|
3595
|
+
* The real `window.location` is never changed.
|
|
3596
|
+
*/
|
|
3597
|
+
router?: Router<any>;
|
|
3598
|
+
/** The router fake's start URL (default '/'), e.g. '/tasks/2?tab=notes' */
|
|
3599
|
+
url?: string;
|
|
3600
|
+
/** The sink the router fake serves (default 'ROUTER') */
|
|
3601
|
+
routerSink?: string;
|
|
3602
|
+
/** Run the router's scroll handling in the fake (default false; positions are kept in memory) */
|
|
3603
|
+
routerScroll?: boolean;
|
|
3604
|
+
/** Run the router's focus handling (default false; true: the router's own `focus` selectors; a string: these selectors). Needs `dom: 'real'` */
|
|
3605
|
+
routerFocus?: boolean | string;
|
|
3606
|
+
/** The sink the HEAD fake serves (default 'HEAD'); with no driver for it, `t.head()` reads what the components declared */
|
|
3607
|
+
headSink?: string;
|
|
3608
|
+
/** The HEAD fake's `titleTemplate` (`'%s · App'`), as passed to makeHeadDriver */
|
|
3609
|
+
titleTemplate?: string;
|
|
3610
|
+
/** PLAN-4 GS-7: the sink the timer fake serves (default 'TIMER'); with no driver for it, the real makeTimerDriver() runs on the test's timers and `t.timers()` lists them */
|
|
3611
|
+
timerSink?: string;
|
|
3612
|
+
/** PLAN-5 B-3: the sink the browser fake serves (default 'BROWSER'); with no driver for it, the browser driver runs over fake sources `t.browser` drives */
|
|
3613
|
+
browserSink?: string;
|
|
3614
|
+
/** PLAN-5 B-3: the browser fake's environment at start (default: no media query matches, empty storage, visible, online, empty clipboard, no position, nothing denied) */
|
|
3615
|
+
browser?: BrowserFakeOptions;
|
|
3616
|
+
/**
|
|
3617
|
+
* PLAN-4 GS-5: the fake storage behind the root's `persist()` ('local' and 'session' alike), as
|
|
3618
|
+
* key -> stored entry (`{ version, state }`; with `format: 'plain'` the stored keys themselves;
|
|
3619
|
+
* or a raw string). Used as is, not copied: writes
|
|
3620
|
+
* land in it, and renderComponent calls given the same object share one storage (`sync: true`
|
|
3621
|
+
* applies one's writes in the other). Default: a new empty object.
|
|
3622
|
+
*/
|
|
3623
|
+
storage?: Record<string, PersistedEntry | Record<string, any> | string>;
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3626
|
+
/** PLAN-4 GS-5: a stored persist() entry (as `t.storage(key)` returns it) */
|
|
3627
|
+
interface PersistedEntry<STATE = any> {
|
|
3628
|
+
version: number
|
|
3629
|
+
state: Partial<STATE>
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3632
|
+
/** PLAN-5 B-3: renderComponent's `browser` option: the fake environment at start */
|
|
3633
|
+
interface BrowserFakeOptions {
|
|
3634
|
+
/** media query -> matches */
|
|
3635
|
+
media?: Record<string, boolean>;
|
|
3636
|
+
/** localStorage, key -> stored string */
|
|
3637
|
+
storage?: Record<string, string>;
|
|
3638
|
+
/** sessionStorage, key -> stored string */
|
|
3639
|
+
sessionStorage?: Record<string, string>;
|
|
3640
|
+
visible?: boolean;
|
|
3641
|
+
online?: boolean;
|
|
3642
|
+
clipboard?: string;
|
|
3643
|
+
/** the position a geolocation declaration starts with (the coords; the rest default) */
|
|
3644
|
+
position?: Partial<BrowserPosition>;
|
|
3645
|
+
/** permissions denied from the start */
|
|
3646
|
+
deny?: Array<'geolocation' | 'clipboard'>;
|
|
3647
|
+
}
|
|
3648
|
+
|
|
3649
|
+
/** PLAN-5 B-3: `t.browser` */
|
|
3650
|
+
interface BrowserFake {
|
|
3651
|
+
/** the declarations of `intersection: target` hear `{ visible, ratio: 1 | 0, index: 0, dataset: {}, ...data }`; `at`: only the at-th of them (start order). Throws when nothing declares it. (Each declaration also hears `{ visible: false, ratio: 0, index: 0, dataset: {} }` when it starts, and a resize one `{ width: 0, height: 0, index: 0, dataset: {} }`, as the observers report) */
|
|
3652
|
+
intersect(target: string | true, visible?: boolean, data?: Partial<BrowserIntersection> & { at?: number }): Promise<void>;
|
|
3653
|
+
/** the declarations of `resize: target` hear `{ width, height, index: 0, dataset: {}, ...size }`. Throws when nothing declares it */
|
|
3654
|
+
resize(target: string | true, size: Partial<BrowserResize> & { at?: number }): Promise<void>;
|
|
3655
|
+
/** a position (accuracy 0, the rest null, timestamp now unless given) or an error `{ code, message }` for the geolocation declarations */
|
|
3656
|
+
geolocation(position: Partial<BrowserPosition> | { code: number; message?: string }): Promise<void>;
|
|
3657
|
+
/** a media query now matches (or not) */
|
|
3658
|
+
media(query: string, matches: boolean): Promise<void>;
|
|
3659
|
+
visibility(visible: boolean): Promise<void>;
|
|
3660
|
+
online(online: boolean): Promise<void>;
|
|
3661
|
+
/** the stored string (null when absent) */
|
|
3662
|
+
storage(key: string): string | null;
|
|
3663
|
+
/** another tab writes the key (a non-string is stored as JSON; null removes it) */
|
|
3664
|
+
storage(key: string, value: any, area?: 'local' | 'session'): Promise<void>;
|
|
3665
|
+
/** the clipboard's text */
|
|
3666
|
+
clipboard(): string;
|
|
3667
|
+
/** the clipboard's text is now `text` */
|
|
3668
|
+
clipboard(text: string): Promise<void>;
|
|
3669
|
+
/** deny permissions: copy/paste fail with NotAllowedError, geolocation with code 1 (running ones too) */
|
|
3670
|
+
deny(...kinds: Array<'geolocation' | 'clipboard'>): void;
|
|
3671
|
+
/** the running declarations, in start order: `{ name, ...spec, component }` */
|
|
3672
|
+
active(): Array<Record<string, any>>;
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3675
|
+
/** PLAN-4 GS-7: an active timer, as `t.timers()` lists it: the spec as declared plus these */
|
|
3676
|
+
type ActiveTimer = TimerSpec & {
|
|
3677
|
+
/** Its name in the `timers` static */
|
|
3678
|
+
name: string;
|
|
3679
|
+
/** The action it sends (a `frame` timer's is its `frame`) */
|
|
3680
|
+
action: string;
|
|
3681
|
+
/** The declaring component's name */
|
|
3682
|
+
component: string;
|
|
3683
|
+
}
|
|
3684
|
+
|
|
3685
|
+
/**
|
|
3686
|
+
* What renderComponent() returns. STATE is the component's state type (calculated fields
|
|
3687
|
+
* included), inferred by renderComponent; name it for a handle declared before it is assigned:
|
|
3688
|
+
* `let t: RenderResult<State>`. Defaults to `any` (untyped tests compile as before).
|
|
3689
|
+
*/
|
|
3690
|
+
/** PLAN-4 GS-10: where an action in `t.actions` came from */
|
|
3691
|
+
type ActionCause = 'intent' | 'next' | 'reply' | 'built-in' | 'simulateAction' | 'behavior'
|
|
3692
|
+
|
|
3693
|
+
/**
|
|
3694
|
+
* PLAN-4 GS-10: one action in renderComponent's `t.actions`. `sinks` fills in as the action's
|
|
3695
|
+
* reducers run (STATE a microtask later), so an entry is live.
|
|
3696
|
+
*/
|
|
3697
|
+
interface TestAction {
|
|
3698
|
+
/** The action name (a behavior's actions are namespaced: `pager.NEXT`) */
|
|
3699
|
+
type: string
|
|
3700
|
+
/** Its data (the DOM event for a DOM intent stream) */
|
|
3701
|
+
data: any
|
|
3702
|
+
/** The name of the component that ran it */
|
|
3703
|
+
component: string
|
|
3704
|
+
/** That component instance's id (stable for its life; inspect()'s component id) */
|
|
3705
|
+
instance: string
|
|
3706
|
+
/** The sinks that produced a value: not ABORT; STATE not the unchanged state; EFFECT when it ran */
|
|
3707
|
+
sinks: string[]
|
|
3708
|
+
/** 'intent' | 'next' (a reducer's or EFFECT's next()) | 'reply' (reply actions) | 'built-in' (INITIALIZE, BOOTSTRAP, DISPOSE, RESOURCE) | 'simulateAction' | 'behavior' */
|
|
3709
|
+
cause: ActionCause
|
|
3710
|
+
/** ms since renderComponent() was called (the fake clock under fake timers) */
|
|
3711
|
+
at: number
|
|
639
3712
|
}
|
|
640
3713
|
|
|
641
|
-
|
|
3714
|
+
/** PLAN-4 GS-10: what `t.explain()` returns */
|
|
3715
|
+
interface ExplainedAction<STATE = any> extends TestAction {
|
|
3716
|
+
/** The root state the action produced (the first recorded after its STATE reducer ran) */
|
|
3717
|
+
state: STATE
|
|
3718
|
+
/** Its STATE reducer: the model's function and its source text (JS exposes no source location) */
|
|
3719
|
+
reducer?: { action: string; sink: string; fn: Function; source: string }
|
|
3720
|
+
}
|
|
3721
|
+
|
|
3722
|
+
interface RenderResult<STATE = any> {
|
|
642
3723
|
/** Stream of state values */
|
|
643
|
-
state$: Stream<
|
|
3724
|
+
state$: Stream<STATE>;
|
|
644
3725
|
/** Stream of rendered VNode trees */
|
|
645
3726
|
dom$: Stream<any>;
|
|
646
3727
|
/** Event bus source — call .select(type) to filter */
|
|
@@ -649,19 +3730,286 @@ export interface RenderResult {
|
|
|
649
3730
|
sinks: Record<string, any>;
|
|
650
3731
|
/** All source objects by driver name */
|
|
651
3732
|
sources: Record<string, any>;
|
|
652
|
-
/** Push an action
|
|
3733
|
+
/** Push an action into the intent→model pipeline under its real name (all sinks of the entry run) */
|
|
653
3734
|
simulateAction: (actionName: string, data?: any) => void;
|
|
654
|
-
/**
|
|
655
|
-
|
|
3735
|
+
/**
|
|
3736
|
+
* Dispatch a synthetic DOM event through the mock DOM source so the component's real intent
|
|
3737
|
+
* streams fire (DOM.click('.x'), DOM.select('.x').events('click'), .value(), .data()).
|
|
3738
|
+
* Targets the first rendered element matching `selector` and bubbles within its isolation scope.
|
|
3739
|
+
* Selectors match the rendered tree like the real DOM: tag, .class, #id, [attr], [attr="v"]
|
|
3740
|
+
* (^= $= *= ~=), :first-child, :last-child, :only-child, :nth-child(an+b|odd|even),
|
|
3741
|
+
* :nth-last-child(), :first/last/only/nth-of-type, :not(), descendant ' ' and child '>'
|
|
3742
|
+
* combinators, ',' lists. Other syntax (:has(), '+', '~', pseudo-elements...) throws.
|
|
3743
|
+
* If nothing matches yet, the event waits (up to 300ms, re-checked on every render) for a
|
|
3744
|
+
* matching element, then targets the first one. If none renders, the test fails with an
|
|
3745
|
+
* error naming the selector: it rejects the pending next()/waitForState()/settle(), or is
|
|
3746
|
+
* thrown by the next t.* call or dispose() (or by simulateEvent itself when nothing is
|
|
3747
|
+
* pending and the tree is quiet). Pass `{ allowMissing: true }` to drop the event with SYG103
|
|
3748
|
+
* instead. It is never sent to every listener with that selector string. It also waits until
|
|
3749
|
+
* the listeners it reaches are subscribed (a just-mounted child subscribes a few ms late).
|
|
3750
|
+
* `'document'` / `'body'` go to the DOM.select('document' | 'body') listeners.
|
|
3751
|
+
* simulateAction/simulateEvent calls are delivered in call order; a waiting event holds the
|
|
3752
|
+
* calls after it. Reports SYG104 (selector only matches inside a child component).
|
|
3753
|
+
*/
|
|
3754
|
+
simulateEvent: (selector: string | AnyControl, eventType: string, eventInit?: SimulatedEventInit) => void;
|
|
3755
|
+
/**
|
|
3756
|
+
* Resolves once the component is subscribed (earlier simulate* calls are buffered and replayed).
|
|
3757
|
+
* Also a cursor: the first next() after `await t.ready()` also matches the states the replayed
|
|
3758
|
+
* calls produced, unless another t.* call came in between.
|
|
3759
|
+
*/
|
|
3760
|
+
ready: () => Promise<void>;
|
|
3761
|
+
/**
|
|
3762
|
+
* Wait for a state that satisfies the predicate. Matches the recorded HISTORY too: a state
|
|
3763
|
+
* from before the call resolves it (e.g. `count === 0` right after a reset resolves at once
|
|
3764
|
+
* with the initial state). Resolves with the matching state once the whole tree (children
|
|
3765
|
+
* included) has rendered it. Use `next()` to wait for a new state.
|
|
3766
|
+
*/
|
|
3767
|
+
waitForState: (predicate: (state: STATE) => boolean, timeoutMs?: number) => Promise<STATE>;
|
|
3768
|
+
/**
|
|
3769
|
+
* Wait for the next state emitted AFTER this call (right after `await t.ready()`: after the
|
|
3770
|
+
* component became ready) that satisfies the predicate (default: any state). Resolves with it
|
|
3771
|
+
* once the whole tree (children included) has rendered it; rejects after timeoutMs (default:
|
|
3772
|
+
* the timeoutMs option, 2000). The error names a model next() still scheduled, and a recorded
|
|
3773
|
+
* state that already matched. With `{ dom: 'real' }`, a next() right after another wait (no
|
|
3774
|
+
* input in between) starts after the state that wait resolved with, so
|
|
3775
|
+
* `await t.next(a); await t.next(b)` sees a `b` that arrived while the DOM showed `a`.
|
|
3776
|
+
*/
|
|
3777
|
+
next: (predicate?: (state: STATE) => boolean, timeoutMs?: number) => Promise<STATE>;
|
|
3778
|
+
/**
|
|
3779
|
+
* Resolves once nothing is pending: the component is ready, no simulated input is waiting,
|
|
3780
|
+
* and nothing in the tree has rendered, reduced or changed state for settleMs (default 20,
|
|
3781
|
+
* longer than a model next()'s default delay). Rejects after timeoutMs if it never calms down.
|
|
3782
|
+
*/
|
|
3783
|
+
settle: (timeoutMs?: number) => Promise<void>;
|
|
656
3784
|
/** Collected state values — grows as new states are emitted */
|
|
657
|
-
states:
|
|
658
|
-
/**
|
|
3785
|
+
states: STATE[];
|
|
3786
|
+
/**
|
|
3787
|
+
* PLAN-4 GS-10: every action the rendered tree ran (children and Collection items included),
|
|
3788
|
+
* in order, as `{ type, data, component, instance, sinks, cause, at }`. Live array.
|
|
3789
|
+
*/
|
|
3790
|
+
actions: TestAction[];
|
|
3791
|
+
/**
|
|
3792
|
+
* PLAN-4 GS-10: the first action whose resulting root state matches the predicate, with that
|
|
3793
|
+
* state and its STATE reducer; undefined when none did.
|
|
3794
|
+
*/
|
|
3795
|
+
explain: (predicate: (state: STATE) => boolean) => ExplainedAction<STATE> | undefined;
|
|
3796
|
+
/** The latest recorded state (`t.states.at(-1)`; undefined before the first one), calculated fields current. Read-only */
|
|
3797
|
+
readonly state: STATE;
|
|
3798
|
+
/** Live array of values emitted on a sink (EVENTS as {type, data}, PARENT unwrapped, custom sinks of any component in the tree) */
|
|
3799
|
+
sinkValues: (sinkName: string) => any[];
|
|
3800
|
+
/**
|
|
3801
|
+
* Live array of the requests a sink was sent, as objects: a string request is `{ url }`, a
|
|
3802
|
+
* resource fetch `{ url, ...request, resource: name }`. Never the `{ abort }` commands,
|
|
3803
|
+
* `{ resources }` declarations or `{ refresh }` commands (sinkValues has every value, as sent)
|
|
3804
|
+
*/
|
|
3805
|
+
requests: (sinkName: string) => any[];
|
|
3806
|
+
/**
|
|
3807
|
+
* Answer a pending request on a driverless sink/source (e.g. `HTTP` with no
|
|
3808
|
+
* `drivers: { HTTP }`, which runs makeFetchDriver over an in-memory fetch) with a response whose
|
|
3809
|
+
* body is `value` (JSON; text for a string): a request with reply actions (`ok: 'LOADED'`) gets
|
|
3810
|
+
* the parsed body as its `LOADED` action, on exactly the component that sent it; a plain one gets
|
|
3811
|
+
* `{ category, value, status: 200, request }` on `HTTP.select(category)`.
|
|
3812
|
+
* Which request (the newest pending one that matches): `target` is an `ok`/`error` action
|
|
3813
|
+
* name, key, category, resource name or URL (`'LOADED'`); a partial request compared by value
|
|
3814
|
+
* with its t.requests form (`{ url: '/a' }`,
|
|
3815
|
+
* the constant the model returns); a predicate `(request) => boolean`; or FakeReplyOptions.
|
|
3816
|
+
* Nothing: the newest pending request. Requests answered, superseded by `latest: true`,
|
|
3817
|
+
* aborted, or whose component is gone aren't pending.
|
|
3818
|
+
* Throws at the call when nothing matching is pending, unless simulateEvent/simulateAction/
|
|
3819
|
+
* respond/fail calls are still queued before it or the component isn't ready yet: then it is
|
|
3820
|
+
* delivered after them, waiting up to 1s (half of timeoutMs if lower) for the request.
|
|
3821
|
+
* Resolves once the reply has been reduced and the tree rendered; rejects (and, if not
|
|
3822
|
+
* awaited, fails the next wait) when no request comes or nothing receives a plain reply.
|
|
3823
|
+
*/
|
|
3824
|
+
respond: (sinkName: string, value: any, target?: string | FakeReplyOptions | Record<string, any> | ((request: any) => boolean)) => Promise<void>;
|
|
3825
|
+
/**
|
|
3826
|
+
* Fail a pending request (chosen as in respond): one with reply actions (`error: 'FAILED'`) gets
|
|
3827
|
+
* `{ error, request, status?, body? }` as its `FAILED` action, on its sender; a plain one
|
|
3828
|
+
* `{ error, category, request, status, body }` on `HTTP.errors(category)`. `error`: an HTTP
|
|
3829
|
+
* status (an error response: 404 → the driver's Error 'HTTP 404: url', status 404), or an
|
|
3830
|
+
* Error / message (a network failure: no status).
|
|
3831
|
+
*/
|
|
3832
|
+
fail: (sinkName: string, error: any, target?: string | FakeReplyOptions | Record<string, any> | ((request: any) => boolean)) => Promise<void>;
|
|
3833
|
+
/**
|
|
3834
|
+
* A driverless sink that gets `{ connections }` / `{ to }` values (e.g. `WS` with no
|
|
3835
|
+
* `drivers: { WS }`) is a fake makeSocketDriver: diffed per component and connection name,
|
|
3836
|
+
* `open`/`message`/`close`/`error` as reply actions of the sender (no `close` for closes the
|
|
3837
|
+
* app makes), other events on `WS.select(name)`, shared by URL, reconnect per the spec on
|
|
3838
|
+
* the test's timers (fake timers included). The connections declared now, in order.
|
|
3839
|
+
* t.open / t.push / t.drop throw at the call when no connection matches (unless input is still
|
|
3840
|
+
* queued before them, as for respond) and resolve once the result has been reduced and rendered.
|
|
3841
|
+
*/
|
|
3842
|
+
connections: (sinkName: string) => FakeConnection[];
|
|
3843
|
+
/** PLAN-3 5-3: the cache entries of an HTTP fake (`renderComponent(C, { http: { cache: queryCache() } })`) */
|
|
3844
|
+
cache: (sinkName: string) => FakeCacheEntry[];
|
|
3845
|
+
/** PLAN-3 5-3: the window regains focus (queued like simulate*): stale mounted resources refetch (cache on) */
|
|
3846
|
+
focus: () => void;
|
|
3847
|
+
/** PLAN-3 5-3: the browser comes back online (queued like simulate*): stale mounted resources refetch (cache on) */
|
|
3848
|
+
online: () => void;
|
|
3849
|
+
/** Complete the open of connecting connection(s) (`autoConnect: false`, or a pending retry): `open` fires with `{ reconnected }` */
|
|
3850
|
+
open: (sinkName: string, target?: FakeConnectionTarget) => Promise<void>;
|
|
3851
|
+
/**
|
|
3852
|
+
* The server sends `data` (objects as JSON text) on open connection(s): `message` fires with
|
|
3853
|
+
* the data, JSON-parsed when it parses. `{ event, connection? }` sends an SSE named event.
|
|
3854
|
+
*/
|
|
3855
|
+
push: (sinkName: string, data: any, target?: FakeConnectionTarget | { event?: string; connection?: FakeConnectionTarget }) => Promise<void>;
|
|
3856
|
+
/**
|
|
3857
|
+
* Connection(s) close without the app closing them: `close` fires with `{ code, reason,
|
|
3858
|
+
* willReconnect }` (default `{ code: 1006, reason: '' }`; a connecting one fails to open: `error`
|
|
3859
|
+
* first), then the fake reconnects per the spec's `reconnect`.
|
|
3860
|
+
*/
|
|
3861
|
+
drop: (sinkName: string, close?: { code?: number; reason?: string } | FakeConnectionTarget, target?: FakeConnectionTarget) => Promise<void>;
|
|
3862
|
+
/**
|
|
3863
|
+
* Live array of the `{ to, json | text | binary }` values sent (`to`: only those to that connection).
|
|
3864
|
+
* For the router fake's sink (`t.sent('ROUTER')`): the commands the components sent
|
|
3865
|
+
* (`{ to, params }`, `{ back: true }`, `{ block }`...), not the `route` declarations
|
|
3866
|
+
*/
|
|
3867
|
+
sent: (sinkName: string, to?: string) => any[];
|
|
3868
|
+
/**
|
|
3869
|
+
* Router fake (`renderComponent(App, { router })`): navigate as a click on a link with this
|
|
3870
|
+
* href (`'/tasks/2'`), or as the command `{ to: 'task', params: { id: 2 }, query?, hash?,
|
|
3871
|
+
* replace? }`. Goes through `{ block }` like the real thing. Throws at the call for an unknown
|
|
3872
|
+
* route name, a missing param or another origin; resolves once the route has been reduced
|
|
3873
|
+
* and the tree rendered.
|
|
3874
|
+
*/
|
|
3875
|
+
navigate: (target: string | { to: string; params?: Record<string, string | number>; query?: Record<string, any>; hash?: string; replace?: boolean }) => Promise<void>;
|
|
3876
|
+
/** Router fake: the browser's back button (popstate a task later; a block undoes it). Throws with no entry to go back to */
|
|
3877
|
+
back: () => Promise<void>;
|
|
3878
|
+
/** Router fake: the browser's forward button. Throws with no entry to go forward to */
|
|
3879
|
+
forward: () => Promise<void>;
|
|
3880
|
+
/** Router fake: the in-memory location: `path` (pathname), `search` ('?tab=x' or ''), `hash` ('#c' or ''), `href` */
|
|
3881
|
+
readonly location: { path: string; search: string; hash: string; href: string };
|
|
3882
|
+
/**
|
|
3883
|
+
* HEAD fake (no HEAD driver passed): the head the components declare now (`head` statics and
|
|
3884
|
+
* HEAD sink values), merged as makeHeadDriver merges them: `{ title, meta: { name: content },
|
|
3885
|
+
* link: [{ rel, href }] }`, `titleTemplate` applied
|
|
3886
|
+
*/
|
|
3887
|
+
head: () => { title: string | undefined; meta: Record<string, string>; link: Array<Record<string, any>> };
|
|
3888
|
+
/**
|
|
3889
|
+
* PLAN-4 GS-7: the timer fake's active timers, in start order (`{ name, every | after | frame,
|
|
3890
|
+
* action, background?, component }`): a fired `after`, a stopped one or an invalid spec is not
|
|
3891
|
+
* listed. The fake is the real makeTimerDriver(), so fake timers drive it
|
|
3892
|
+
* (`vi.useFakeTimers()`, then `await vi.advanceTimersByTimeAsync(ms)`). Throws when a driver
|
|
3893
|
+
* was passed for the timer sink.
|
|
3894
|
+
*/
|
|
3895
|
+
timers: () => ActiveTimer[];
|
|
3896
|
+
/**
|
|
3897
|
+
* PLAN-5 B-3: the browser fake's controls (no browser driver passed): `await
|
|
3898
|
+
* t.browser.intersect('.cover', true)`, `await t.browser.media('(prefers-color-scheme: dark)',
|
|
3899
|
+
* true)`, `t.browser.active()`. Each input resolves once its actions are reduced and the tree
|
|
3900
|
+
* rendered. Throws when a driver was passed for the browser sink.
|
|
3901
|
+
*/
|
|
3902
|
+
browser: BrowserFake;
|
|
3903
|
+
/**
|
|
3904
|
+
* PLAN-4 GS-5: the fake storage's entry for `key` (`{ version, state }`), undefined when none
|
|
3905
|
+
* (a raw string when what is stored isn't JSON). Pending persist() writes are flushed by t.settle().
|
|
3906
|
+
* A `format: 'plain'` entry is the stored keys: `t.storage<{ title: string }>('note-draft')`
|
|
3907
|
+
*/
|
|
3908
|
+
storage: <ENTRY = PersistedEntry<STATE>>(key: string) => ENTRY | undefined;
|
|
3909
|
+
/** Live array of EVENTS sink emissions ({type, data}) */
|
|
3910
|
+
emitted: Array<{ type: string; data: any }>;
|
|
3911
|
+
/** Live array of diagnostics reported while rendered */
|
|
3912
|
+
diagnostics: Diagnostic[];
|
|
3913
|
+
/**
|
|
3914
|
+
* PLAN-4 GS-2: the element commands the tree's instances sent on `ELEMENT`, one entry per command
|
|
3915
|
+
* (arrays flattened), as sent: `expect(t.commands('ELEMENT')).toEqual([{ focus: Email }])`; a
|
|
3916
|
+
* `sygnal/ui` dialog's or popover's command with its selector: `{ showModal: '.profile' }`. The
|
|
3917
|
+
* mock DOM records them (and reports SYG640/SYG641); `dom: 'real'` also runs them (jsdom gets
|
|
3918
|
+
* `<dialog>` show/showModal/close, the popover methods and a no-op scrollIntoView). Another sink
|
|
3919
|
+
* name gives its sinkValues.
|
|
3920
|
+
*/
|
|
3921
|
+
commands: {
|
|
3922
|
+
(sinkName?: 'ELEMENT'): ElementCommand[];
|
|
3923
|
+
(sinkName: string): any[];
|
|
3924
|
+
};
|
|
3925
|
+
/** Throws (with the formatted texts) if any warn/error diagnostics were collected */
|
|
3926
|
+
expectNoDiagnostics: () => void;
|
|
3927
|
+
/**
|
|
3928
|
+
* Latest rendered VNode serialized to HTML like innerHTML (text escapes only & < >, so
|
|
3929
|
+
* `Couldn't`, not `'`). Throws if called before the first render: wait
|
|
3930
|
+
* with `await t.ready()` (or `t.next(...)`) first. '' for a component that renders nothing.
|
|
3931
|
+
*/
|
|
3932
|
+
html: () => string;
|
|
3933
|
+
/** Tear down the component, clean up listeners and restore the diagnostics mode */
|
|
659
3934
|
dispose: () => void;
|
|
3935
|
+
/**
|
|
3936
|
+
* The app graph of the rendered tree (components, actions, selectors with the mock DOM's
|
|
3937
|
+
* match / isolation results, EVENTS, diagnostics). Throws unless 'sygnal/diagnostics' is loaded.
|
|
3938
|
+
* `{ actions: true }` (or a number: the last n) adds `recentActions` (GS-10).
|
|
3939
|
+
*/
|
|
3940
|
+
inspect: (options?: Pick<InspectOptions, 'actions'>) => InspectGraph;
|
|
3941
|
+
/** `{ dom: 'real' }`: the element the tree is mounted in (removed by dispose()); otherwise null */
|
|
3942
|
+
container: Element | null;
|
|
3943
|
+
/**
|
|
3944
|
+
* The first element matching a selector in the rendered tree (Portal content included), or
|
|
3945
|
+
* null: `expect(t.query('input[name="plan"]:checked').value).toBe('team')`. With `dom: 'real'`
|
|
3946
|
+
* a real element (any CSS selector); with the mock DOM a read-only snapshot of what the view
|
|
3947
|
+
* rendered (simulateEvent's selectors plus `:checked`/`:disabled`/`:enabled`; textContent,
|
|
3948
|
+
* value, checked, disabled, getAttribute, classList, dataset, querySelector, closest...; no
|
|
3949
|
+
* focus()). Throws before the first render (`await t.ready()` first). Right after
|
|
3950
|
+
* `await t.next(pred)` / `waitForState` / `settle()` / `ready()` it shows the state the wait resolved with.
|
|
3951
|
+
*/
|
|
3952
|
+
query: {
|
|
3953
|
+
(selector: string): Element | null;
|
|
3954
|
+
/** A control's element (`t.query(Draft)` is an HTMLInputElement for an 'input' control), or null */
|
|
3955
|
+
<CONTROL extends AnyControl>(control: CONTROL): ControlElementOf<CONTROL> | null;
|
|
3956
|
+
};
|
|
3957
|
+
/** Every element matching a selector (or a control) in the rendered tree (Portals included), as query() */
|
|
3958
|
+
queryAll: {
|
|
3959
|
+
(selector: string): Element[];
|
|
3960
|
+
<CONTROL extends AnyControl>(control: CONTROL): Array<ControlElementOf<CONTROL>>;
|
|
3961
|
+
};
|
|
3962
|
+
/**
|
|
3963
|
+
* A widget's host (PLAN-5 W-1), by selector (`'.due'`) or control. `props`: what the view
|
|
3964
|
+
* passed the widget (mock DOM: the rendered host's; `dom: 'real'`: the mounted widget's).
|
|
3965
|
+
* `instance` (`dom: 'real'`): what `mount` returned. `emit(name, detail)`: the CustomEvent the
|
|
3966
|
+
* widget's dispatch() sends, sent like `simulateEvent`. Reading `props`/`instance` throws
|
|
3967
|
+
* when no widget host matches.
|
|
3968
|
+
*/
|
|
3969
|
+
widget: {
|
|
3970
|
+
<P = any, I = any>(selector: string): WidgetHandle<P, I>;
|
|
3971
|
+
<CONTROL extends AnyControl>(control: CONTROL): WidgetHandle<CONTROL extends { readonly [CONTROL]: { props: infer P } } ? P : any>;
|
|
3972
|
+
};
|
|
3973
|
+
}
|
|
3974
|
+
|
|
3975
|
+
/** `t.widget(target)` (PLAN-5 W-1) */
|
|
3976
|
+
interface WidgetHandle<P = any, I = any> {
|
|
3977
|
+
readonly props: P;
|
|
3978
|
+
readonly instance: I | undefined;
|
|
3979
|
+
emit(name: string, detail?: unknown): void;
|
|
660
3980
|
}
|
|
661
3981
|
|
|
662
|
-
|
|
3982
|
+
/**
|
|
3983
|
+
* A component renderComponent() accepts. STATE is inferred from its view's `state` (a
|
|
3984
|
+
* `Component<State, ...>` annotation, or a typed `({ state }: { state: State })` parameter;
|
|
3985
|
+
* calculated fields included), else from its `initialState` (INITIAL).
|
|
3986
|
+
*/
|
|
3987
|
+
type RenderableComponent<STATE = any, INITIAL = STATE> =
|
|
3988
|
+
// a method signature: parameters are compared bivariantly, so views with their own required
|
|
3989
|
+
// props are accepted
|
|
3990
|
+
& { view(props: { state: STATE }, state: STATE, ...rest: any[]): any }['view']
|
|
3991
|
+
& { initialState?: INITIAL }
|
|
3992
|
+
|
|
3993
|
+
/**
|
|
3994
|
+
* STATE, or INITIAL when STATE is unknown (an untyped view with a typed `initialState`). `never`
|
|
3995
|
+
* (a generic call inlined as the argument, `renderComponent(defineComponent({...}))`) becomes `any`.
|
|
3996
|
+
*/
|
|
3997
|
+
type RenderedState<STATE, INITIAL> =
|
|
3998
|
+
[STATE] extends [never] ? any
|
|
3999
|
+
: 0 extends (1 & STATE) ? AnyIfNever<INITIAL>
|
|
4000
|
+
: STATE
|
|
4001
|
+
|
|
4002
|
+
/**
|
|
4003
|
+
* Render a component in tests (mock DOM, fake drivers). The handle's state is typed from the
|
|
4004
|
+
* component, so `await t.next(s => s.count > 0)` needs no annotation. Pass the state type
|
|
4005
|
+
* explicitly for an untyped component: `renderComponent<State>(Counter)`.
|
|
4006
|
+
*/
|
|
4007
|
+
declare function renderComponent<STATE = any, INITIAL = STATE>(
|
|
4008
|
+
componentDef: RenderableComponent<STATE, INITIAL>,
|
|
4009
|
+
options?: RenderOptions
|
|
4010
|
+
): RenderResult<RenderedState<STATE, INITIAL>>
|
|
663
4011
|
|
|
664
|
-
|
|
4012
|
+
interface RenderToStringOptions {
|
|
665
4013
|
/** Initial state for the root component */
|
|
666
4014
|
state?: any
|
|
667
4015
|
/** Props to pass to the root component */
|
|
@@ -674,23 +4022,36 @@ export interface RenderToStringOptions {
|
|
|
674
4022
|
* When a string, uses that as the variable name.
|
|
675
4023
|
*/
|
|
676
4024
|
hydrateState?: boolean | string
|
|
4025
|
+
/** An array that receives each rendered component's `head` static value; pass it to `renderHead()` */
|
|
4026
|
+
head?: any[]
|
|
4027
|
+
/**
|
|
4028
|
+
* PLAN-3 5-5 (H-7): a seeded `queryCache()` the components' `resources` render from: a cached
|
|
4029
|
+
* entry as `{ status: 'success', data }`, any other request as `{ status: 'loading' }` (nothing
|
|
4030
|
+
* is fetched during SSR)
|
|
4031
|
+
*/
|
|
4032
|
+
cache?: QueryCache
|
|
4033
|
+
/** The app-level error hook, as run()'s `onError`: phase 'view' only (PLAN-4 GS-11) */
|
|
4034
|
+
onError?: AppErrorHook
|
|
4035
|
+
/** The root of the `uid()` strings (default 'u'), as run()'s `uid`: the same value on both sides */
|
|
4036
|
+
uid?: string
|
|
677
4037
|
}
|
|
678
4038
|
|
|
679
4039
|
/**
|
|
680
4040
|
* Render a Sygnal component to an HTML string on the server.
|
|
4041
|
+
*
|
|
4042
|
+
* ```ts
|
|
4043
|
+
* renderToString(App, { state: { count: 0 } })
|
|
4044
|
+
* // → '<div data-sygnal-ssr=""><h1>Count: 0</h1></div>'
|
|
4045
|
+
* ```
|
|
4046
|
+
*
|
|
4047
|
+
* The root element carries an empty `data-sygnal-ssr` attribute, which marks the markup as
|
|
4048
|
+
* Sygnal's server HTML (persist() under plain run() then restores after the first render); the
|
|
4049
|
+
* first client render removes it.
|
|
681
4050
|
*/
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
export const xs: typeof xsDefault
|
|
4051
|
+
declare function renderToString(componentDef: any, options?: RenderToStringOptions): string
|
|
685
4052
|
|
|
686
|
-
|
|
687
|
-
export { default as throttle } from 'xstream/extra/throttle.js'
|
|
688
|
-
export { default as delay } from 'xstream/extra/delay.js'
|
|
689
|
-
export { default as dropRepeats } from 'xstream/extra/dropRepeats.js'
|
|
690
|
-
export { default as sampleCombine } from 'xstream/extra/sampleCombine.js'
|
|
4053
|
+
declare const xs: typeof xsDefault
|
|
691
4054
|
|
|
692
|
-
export * from './cycle/dom/index'
|
|
693
|
-
export type { MemoryStream, Stream }
|
|
694
4055
|
|
|
695
4056
|
/**
|
|
696
4057
|
* JSX Types
|
|
@@ -715,5 +4076,13 @@ declare global {
|
|
|
715
4076
|
interface ElementChildrenAttribute {
|
|
716
4077
|
children: {};
|
|
717
4078
|
}
|
|
4079
|
+
|
|
4080
|
+
/**
|
|
4081
|
+
* A component's view receives `state: STATE` and `context`, but in JSX the parent passes
|
|
4082
|
+
* an optional `state` slice name or lens (`<Editor state="editor" />`, `<Panel />`).
|
|
4083
|
+
*/
|
|
4084
|
+
type LibraryManagedAttributes<C, P> = ElementProps<P>
|
|
718
4085
|
}
|
|
719
4086
|
}
|
|
4087
|
+
|
|
4088
|
+
export { ABORT, type ActionCause, type ActionsOf, type ActiveTimer, type AnyComponentModule, type AnyControl, type AppErrorHook, type AppErrorInfo, type AppErrorPhase, type ArrayKeysOf, type AsyncDriverError, type AsyncDriverFromFunction, type AsyncRequest, type Behavior, type BehaviorDefinition, type BehaviorFactory, type BehaviorHandler, type BehaviorModelEntry, type BehaviorProps, type BehaviorSources, type BehaviorState, type BehaviorTarget, type BrowserCommand, type BrowserFake, type BrowserFakeOptions, type BrowserIntersection, type BrowserPosition, type BrowserResize, type BrowserSource, type BrowserSources, type BrowserSpec, type ClassesType, Collection, type CollectionFrom, type CollectionProps, type Command, type CommandSource, type Component, type Connections, type Control, type ControlCommonProps, type ControlDOMSource, type ControlElementOf, type ControlEvent, type ControlH, type ControlPropsOf, type ControlSpec, type ControlSpecObject, type ControlsOf, type CycleDOMEvent, type CycleDriver, type DOMDriverOptions, type DOMEventShorthand, type DOMEventShorthands, type DOMSource, type DefaultDrivers, type DefineComponentOptions, type DevToolsActionCause, type DevToolsCopyAsTestOptions, type DevToolsSessionRecording, type Diagnostic, type DiagnosticCode, type DiagnosticSeverity, type DiagnosticsMode, type DiagnosticsOptions, type DragDriverCategory, type DragDriverRegistration, type DragDriverSource, type DragSource, type DragStartPayload, type DriverFactories, type DriverFromAsyncOptions, type DriverSpec, type DriverSpecs, type DropPayload, type ElementCommand, type ElementCommandRegistry, type ElementCommands, type ElementProps, type ElementTarget, type EmittedEvent, type EnrichedEventStream, type Event$1 as Event, type EventName, type EventPayload, type EventPayloadFunction, type EventSink, type EventsFnOptions, type EventsSource, type ExactShape, type ExplainedAction, type FakeCacheEntry, type FakeConnection, type FakeConnectionTarget, type FakeReplyOptions, type FetchCacheOptions, type FetchDriverOptions, type FetchError, type FetchFailure, type FetchInit, type FetchInvalidate, type FetchRequest, type FetchResponse, type FetchRetry, type FetchSource, type FieldErrors, type Filter, type FixDrivers, type FormActions, type FormCalculated, type FormData, type FormField, type FormFieldCheck, type FormOptions, type FormSource, type FormState, type HeadValue, type HotModuleAPI, type InspectAction, type InspectActionTrigger, type InspectChild, type InspectCommand, type InspectComponent, type InspectDiagnostic, type InspectGraph, type InspectRecentAction, type InspectSelector, type InspectTimer, type InstallPrompt, type IntentSources, type IntrinsicControlProps, type LazyComponent, type LazyOptions, type Lens, type Lense, MainDOMSource, type MockConfig, MockedDOMSource, type NonStateSinkReturns, type PagerActions, type PagerCalculated, type PagerOptions, type PagerState, type ParamNames, type ParentPayloadOf, type Persist, type PersistOptions, type PersistOptionsFor, type PersistStorage, type PersistedEntry, Portal, type PortalProps, type ProcessFormOptions, type QueryCache, type QueryCacheSnapshot, type QueryCacheSnapshotEntry, type Ref, type Ref$, type RegisteredEvent, type RenderOptions, type RenderResult, type RenderToStringOptions, type RenderableComponent, type ReplyRequest, type Resource, type ResourceRequest, type RootComponent, type Route, type RouteName, type RouteParamsArg, type RouteQuery, type Router, type RouterBlocked, type RouterCommand, type RouterOptions, type RouterSource, type RunOptions, type ScrollToAlign, type SelectedDOMSource, type SelectionActions, type SelectionCalculated, type SelectionOptions, type SelectionState, type ServiceWorkerCommand, type ServiceWorkerOptions, type ServiceWorkerSource, type SimulatedEventInit, Slot, type SlotProps, type SocketActions, type SocketClose, type SocketConnection, type SocketDriverOptions, type SocketError, type SocketEvent, type SocketOpen, type SocketReconnect, type SocketRequest, type SocketSource, type SortFunction, type SortObject, type SortSpec, type SortableActions, type SortableDropped, type SortableOptions, type SortableState, type SseConnection, type StandardSchemaLike, type StateProp, Suspense, type SuspenseProps, Switchable, type SwitchableProps, type SygnalApp, type SygnalBodyDOMSource, type SygnalDOMSource, type SygnalDevTools, type SygnalDocumentDOMSource, type SygnalEvents, type SygnalSinks, type TestAction, type Thunk, type ThunkData, type TimerAfter, type TimerFrame, type TimerSpec, type TimerTick, type Timers, Transition, type TransitionProps, type UidFunction, type UndoBehaviorOptions, type UndoHistory, type UndoOptions, type UsesActions, type UsesState, type ViewProps, VirtualCollection, type VirtualCollectionProps, type Widget, type WidgetDefinition, type WidgetDispatch, type WidgetElementOf, type WidgetEmit, type WidgetHandle, type WidgetHostProps, type WithinTarget, checkForm, classes, clearDiagnostics, clipboardSource, controls, createCommand, createInstallPrompt, createRef, createRef$, defineBehavior, defineComponent, defineWidget, driverFromAsync, emit, enableHMR, event, exactState, fieldName, fieldNames, focusInvalid, focusWithin, form, formErrors, geolocationSource, getDevTools, getDiagnostics, getField, hasField, intersectionSource, isSelected, lazy, makeBrowserDriver, makeBrowserDriverWith, makeDOMDriver, makeDragDriver, makeFetchDriver, makeHeadDriver, makeRouter, makeRouterDriver, makeServiceWorkerDriver, makeSocketDriver, makeTimerDriver, makeViewTransitionDOMDriver, mediaSource, mockDOMSource, onDiagnostic, onlineSource, onlineStatus$, pager, persist, portal, processDrag, processForm, queryCache, renderComponent, renderHead, renderToString, replyErrors, resizeSource, run, selection, set, setField, sortable, storageSource, thunk, toggle, undo, undoable, visibilitySource, xs };
|