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
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* PLAN-5 F-1 (D193): the `form` behavior, the canonical form recipe.
|
|
3
|
+
*
|
|
4
|
+
* Signup.uses = { form: form(signupSchema, {
|
|
5
|
+
* values: { email: '', password: '', addresses: [{ id: 1, street: '', city: '' }] },
|
|
6
|
+
* submit: 'SIGN_UP', // the host action a valid submit dispatches (data: the schema's output)
|
|
7
|
+
* check: { email: { request: (email) => ({ url: '/api/email-free', query: { email } }), error: (body) => !body.free && 'Taken' } },
|
|
8
|
+
* }) }
|
|
9
|
+
*
|
|
10
|
+
* Fields are addressed by their `name=` attribute (the path in `values`; rows of an array by
|
|
11
|
+
* id: `addresses.7.street`), heard on the form element (option `form`, default 'form'): input
|
|
12
|
+
* and focusout bubble to it, also from fields rendered by Collection items. Nothing is wired per
|
|
13
|
+
* field.
|
|
14
|
+
*
|
|
15
|
+
* Options: `values` (the start values; D239: or a function of the host's state, called when the
|
|
16
|
+
* form starts), `submit` (host action), `check` (async per-field checks through a driver: a
|
|
17
|
+
* request with a reply action, `latest`), `show` ('blur' default: a schema error shows once its
|
|
18
|
+
* field was blurred; 'input'; 'submit'), `form` (selector), `http` (the driver sink of the
|
|
19
|
+
* checks, 'HTTP'), `resetOnShow` (D239).
|
|
20
|
+
*
|
|
21
|
+
* D239 (G-578): `resetOnShow: true` starts the form over (the start values, as a new form: touched,
|
|
22
|
+
* errors, server errors, checks and the submit state cleared) each time it is shown: when its
|
|
23
|
+
* host starts (VALIDATE: a component mounted again, a Switchable page made again for another
|
|
24
|
+
* `instance`, a slice kept in the parent's state) and each time the form element appears again
|
|
25
|
+
* in the host's DOM after being absent (a Switchable page shown again: hidden pages stay alive;
|
|
26
|
+
* a form rendered conditionally). Re-renders while it is shown keep what the user typed. Off by
|
|
27
|
+
* default: a wizard step keeps its values when the user comes back. Without a real DOM (the mock
|
|
28
|
+
* DOM, SSR) only the host start counts. A `values` function is called with the host's state at
|
|
29
|
+
* each start over (without resetOnShow: once, when the host starts with the slice untouched);
|
|
30
|
+
* before that (a child's first render) the fields come from calling it with a blank state.
|
|
31
|
+
*
|
|
32
|
+
* Slice (state.form): values, initial, errors (all current schema errors by name), touched,
|
|
33
|
+
* server (server errors), remote (async check results: '' passed), pending (name -> value being
|
|
34
|
+
* checked), submitting, submitted, submitCount, queued (a submit waits for a check or an async
|
|
35
|
+
* schema), validating (an async schema runs), validated (the schema has answered since the start
|
|
36
|
+
* or the last reset); calculated: fields (per name: { name, value, error, invalid, touched,
|
|
37
|
+
* dirty, pending }; `error` is what to show), valid (G-382: the last answer's, kept while an async
|
|
38
|
+
* schema re-validates), dirty, error (form-level message).
|
|
39
|
+
*
|
|
40
|
+
* Actions: form.CHANGE ({ name, value, item? }: G-376: a checkbox gives `checked`, and `item`, its
|
|
41
|
+
* value, which toggles membership when the field is an array; <select multiple> an array of the
|
|
42
|
+
* selected values; type=file is ignored), form.BLUR (name), form.SUBMIT, form.ADD ({ field,
|
|
43
|
+
* value }), form.REMOVE ({ field, id }), form.ERRORS (an error reply or a map: server errors),
|
|
44
|
+
* form.DONE (saved), form.RESET (values?); internal: form.RESULT (an async schema's result),
|
|
45
|
+
* form.CHECKED_<name> (a check's reply; a failed check passes: the server checks on submit),
|
|
46
|
+
* form.VALIDATE (the first validation, when the host starts: G-375; `valid` is false until the
|
|
47
|
+
* schema answered; with resetOnShow also each time the form element appears, D239).
|
|
48
|
+
*
|
|
49
|
+
* G-575 (D236): a valid submit is `submitting` (until form.DONE / form.ERRORS, a second submit
|
|
50
|
+
* dropped) only when the host's submit entry has the `http` sink; any other submit (STATE, PARENT,
|
|
51
|
+
* EVENTS, EFFECT, ...) sends no request and is done at once (submitted, as form.DONE would). A
|
|
52
|
+
* slice kept in parent state from a host that unmounted mid-submit starts unstuck: VALIDATE (each
|
|
53
|
+
* host start) clears submitting, queued and pending.
|
|
54
|
+
*
|
|
55
|
+
* Each action is one step function, `(slice, data, key) => outcome | null` (null: no change),
|
|
56
|
+
* the outcome `{ s: new slice, focus: ELEMENT command, req: a check request, send: 1 (dispatch
|
|
57
|
+
* submit), wait: { values, submit? } (an async schema to await) }`. Its sinks (STATE, EFFECT,
|
|
58
|
+
* ELEMENT, and the checks' driver) read that outcome, computed once per (slice, data). D197:
|
|
59
|
+
* the handlers get the `uses` key, which names the reply actions.
|
|
60
|
+
*
|
|
61
|
+
* Dev diagnostics (checks/forms.ts, through the core bridge's `form` hook; nothing in
|
|
62
|
+
* production): SYG230 a field name not in values, SYG231 not a Standard Schema (throws), SYG232
|
|
63
|
+
* a submit dropped, SYG233 a values key missing from the schema's output, SYG234 a submit that
|
|
64
|
+
* dispatched one of the form's own actions (G-577), SYG235 a check request that sets reply
|
|
65
|
+
* fields. The host checks (SYG234 submit, SYG235 check names, SYG236 row ids)
|
|
66
|
+
* run when the host is created.
|
|
67
|
+
*/
|
|
68
|
+
import xs from './xstreamCompat'
|
|
69
|
+
import {defineBehavior} from './behaviors'
|
|
70
|
+
import {ABORT} from '../shared'
|
|
71
|
+
import {isStandardSchema} from './standardSchema'
|
|
72
|
+
import {checkForm, fieldNames, getField, hasField, setField, replyErrors, focusInvalid} from './formHelpers'
|
|
73
|
+
|
|
74
|
+
const dev = (...a: any[]): any => (globalThis as any).__SYGNAL_DIAGNOSTICS__?.form?.(...a)
|
|
75
|
+
const keys = (o: any) => Object.keys(o).filter(k => o[k])
|
|
76
|
+
const drop = (o: any, p: string) => {
|
|
77
|
+
const r: any = {}
|
|
78
|
+
for (const k in o) if (k != p && !k.startsWith(p + '.')) r[k] = o[k]
|
|
79
|
+
return r
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const form = (schema: any, o: any = {}): any => {
|
|
83
|
+
isStandardSchema(schema) || dev(231, schema)
|
|
84
|
+
const {values: vo = {}, submit, check: checks = {}, show = 'blur', http = 'HTTP', form: sel = 'form', resetOnShow: again} = o
|
|
85
|
+
// D239: `values` can be a function of the host's state, called when the form starts (and each
|
|
86
|
+
// show, resetOnShow). Before that (the host's first render, before its first action) the
|
|
87
|
+
// fields come from calling it with a blank state: every read gives '' in the result
|
|
88
|
+
const fn = typeof vo == 'function', B: any = new Proxy(() => B, {get: (_, k) => k == Symbol.toPrimitive ? () => '' : B})
|
|
89
|
+
const clean = (x: any): any => x === B ? '' : Array.isArray(x) ? x.map(clean) : x && typeof x == 'object' ? Object.fromEntries(Object.entries(x).map(([k, y]) => [k, clean(y)])) : x
|
|
90
|
+
let values: any = vo
|
|
91
|
+
if (fn) try { values = clean(vo(B)) } catch (_) { values = {} }
|
|
92
|
+
const cache = new WeakMap(), checked = Object.keys(checks)
|
|
93
|
+
// G-575: a submit is pending (until form.DONE / form.ERRORS) only when the host's submit entry
|
|
94
|
+
// sends a request on the `http` sink; any other submit is done once its entry has run. Set when
|
|
95
|
+
// the form merges into its host
|
|
96
|
+
let req = true
|
|
97
|
+
// the schema's result, once per values object; an async one replaces its Promise when it settles
|
|
98
|
+
const v = (vals: any): any => {
|
|
99
|
+
let r = cache.get(vals)
|
|
100
|
+
if (!r) {
|
|
101
|
+
cache.set(vals, r = checkForm(schema, vals))
|
|
102
|
+
r.then?.((x: any) => cache.set(vals, x))
|
|
103
|
+
}
|
|
104
|
+
return r
|
|
105
|
+
}
|
|
106
|
+
// G-379: a field is a path of values (an undefined value too); `quiet`: no SYG230 (a focusout
|
|
107
|
+
// from a named button)
|
|
108
|
+
const known = (s: any, n: any, quiet?: any) => {
|
|
109
|
+
const k = n && hasField(s.values, n)
|
|
110
|
+
n && !k && !quiet && dev(230, n, s.values)
|
|
111
|
+
return k
|
|
112
|
+
}
|
|
113
|
+
// new values: errors now (sync schema), or after the schema's Promise (RESULT)
|
|
114
|
+
const edit = (s: any, vals: any, x?: any) => {
|
|
115
|
+
const r = v(vals), a = !!r.then
|
|
116
|
+
return {s: {...s, ...x, values: vals, validating: a, ...(!a && {errors: r.errors, validated: true})}, wait: a && {values: vals}}
|
|
117
|
+
}
|
|
118
|
+
// the first check due: no schema error, a value, and not checked or running for it
|
|
119
|
+
const due = (s: any, only?: string) => checked.find(f => {
|
|
120
|
+
const x = getField(s.values, f)
|
|
121
|
+
return (!only || f == only) && !s.errors[f] && x !== '' && x != null && !(f in s.pending) && !(f in s.remote)
|
|
122
|
+
})
|
|
123
|
+
const ask = (s: any, f: string, k: string, queued: boolean) => {
|
|
124
|
+
const x = getField(s.values, f), q = checks[f].request(x), ok = `${k}.CHECKED_${f}`
|
|
125
|
+
dev(235, f, q)
|
|
126
|
+
return {s: {...s, queued, pending: {...s.pending, [f]: x}}, req: {...q, ok, error: ok, latest: true}}
|
|
127
|
+
}
|
|
128
|
+
// a submit with s's errors: blocked (focus the first), a check to run or to wait for, or sent
|
|
129
|
+
const attempt = (s: any, k: string) => {
|
|
130
|
+
const bad = [...keys(s.errors), ...keys(s.remote)], f = due(s)
|
|
131
|
+
return bad.length ? {s: {...s, queued: false}, focus: focusInvalid(bad, sel)}
|
|
132
|
+
: f ? ask(s, f, k, true)
|
|
133
|
+
: Object.keys(s.pending).length ? {s: {...s, queued: true}}
|
|
134
|
+
: {s: req ? {...s, queued: false, submitting: true} : done({...s, queued: false}), send: 1}
|
|
135
|
+
}
|
|
136
|
+
const base = (vals: any) => ({initial: vals, values: vals, touched: {}, server: {}, remote: {}, pending: {},
|
|
137
|
+
submitting: false, submitted: false, submitCount: 0, queued: false, errors: {}, validated: false})
|
|
138
|
+
const fresh = (vals: any) => edit(base(vals), vals)
|
|
139
|
+
const done = (s: any) => ({...s, submitting: false, submitted: true, initial: s.values, server: {}, touched: {}})
|
|
140
|
+
const group = (cur: any, d: any) => {
|
|
141
|
+
if (!Array.isArray(cur) || !('item' in d)) return d.value
|
|
142
|
+
const r = cur.filter(x => x !== d.item)
|
|
143
|
+
return d.value ? [...r, d.item] : r
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const steps: Record<string, (s: any, d: any, k: string, h?: any) => any> = {
|
|
147
|
+
// G-376: a checkbox (`item`: its value) on an array field is one of a group: checked adds
|
|
148
|
+
// its value, unchecked removes it
|
|
149
|
+
CHANGE: (s, d) => known(s, d?.name) ? edit(s, setField(s.values, d.name, group(getField(s.values, d.name), d)), {
|
|
150
|
+
server: drop(drop(s.server, d.name), ''), remote: drop(s.remote, d.name), pending: drop(s.pending, d.name), queued: false,
|
|
151
|
+
touched: show == 'input' ? {...s.touched, [d.name]: true} : s.touched,
|
|
152
|
+
}) : null,
|
|
153
|
+
BLUR: (s, n, k) => {
|
|
154
|
+
if (!known(s, n, 1)) return null
|
|
155
|
+
const t = show == 'submit' || s.touched[n] ? s.touched : {...s.touched, [n]: true}, x = {...s, touched: t}
|
|
156
|
+
return due(s, n) ? ask(x, n, k, s.queued) : t != s.touched ? {s: x} : null
|
|
157
|
+
},
|
|
158
|
+
SUBMIT: (s, _, k) => {
|
|
159
|
+
if (s.submitting || s.queued) return void dev(232, s)
|
|
160
|
+
const n = {...s, submitCount: s.submitCount + 1}, r = v(s.values)
|
|
161
|
+
// G-371: a submit waiting for an async schema is queued (a second one is dropped)
|
|
162
|
+
return r.then ? {s: {...n, validating: true, queued: true}, wait: {values: s.values, submit: 1}} : attempt({...n, errors: r.errors}, k)
|
|
163
|
+
},
|
|
164
|
+
RESULT: (s, d, k) => {
|
|
165
|
+
if (d.values !== s.values) return
|
|
166
|
+
const n = {...s, errors: v(d.values).errors, validating: false, validated: true}
|
|
167
|
+
// the submit it was queued for (G-575: a done local submit isn't `submitting`, so not that)
|
|
168
|
+
return d.submit && s.queued ? attempt(n, k) : {s: n}
|
|
169
|
+
},
|
|
170
|
+
ADD: (s, {field, value}) => {
|
|
171
|
+
const rows = getField(s.values, field) || []
|
|
172
|
+
return edit(s, setField(s.values, field, [...rows, {id: rows.reduce((m: number, r: any) => Math.max(m, +r.id || 0), 0) + 1, ...value}]))
|
|
173
|
+
},
|
|
174
|
+
REMOVE: (s, {field, id}) => {
|
|
175
|
+
const p = field + '.' + id
|
|
176
|
+
return edit(s, setField(s.values, field, getField(s.values, field).filter((r: any) => r.id + '' != id)),
|
|
177
|
+
{touched: drop(s.touched, p), server: drop(s.server, p)})
|
|
178
|
+
},
|
|
179
|
+
ERRORS: (s, d) => {
|
|
180
|
+
const e = replyErrors(d, s.values)
|
|
181
|
+
return {s: {...s, server: e, submitting: false, queued: false}, focus: focusInvalid(e, sel)}
|
|
182
|
+
},
|
|
183
|
+
DONE: (s) => ({s: done(s)}),
|
|
184
|
+
RESET: (s, d) => fresh(d && typeof d == 'object' ? d : s.initial),
|
|
185
|
+
// G-375: the first validation, when the host starts (form() doesn't validate at module load).
|
|
186
|
+
// G-575: a slice kept in parent state from a host that unmounted mid-submit starts unstuck.
|
|
187
|
+
// D239: with resetOnShow (also each time the form element appears again), or a `values`
|
|
188
|
+
// function not called yet, the slice starts over from the start values (h: the host state)
|
|
189
|
+
VALIDATE: (s, _, _k, h) => again || fn && s.values === values ? fresh(fn ? vo(h) : vo)
|
|
190
|
+
: edit({...s, submitting: false, queued: false, pending: {}}, s.values),
|
|
191
|
+
}
|
|
192
|
+
// a check's reply (ok and error: an error reply carries an Error and passes)
|
|
193
|
+
for (const f of checked) steps['CHECKED_' + f] = (s, d, k) => {
|
|
194
|
+
if (!(f in s.pending)) return
|
|
195
|
+
const n = {...s, pending: drop(s.pending, f), remote: {...s.remote, [f]: d?.error instanceof Error ? '' : checks[f].error?.(d) || ''}}
|
|
196
|
+
return s.queued ? attempt(n, k) : {s: n}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const model: any = {}
|
|
200
|
+
for (const a in steps) {
|
|
201
|
+
let last: any[] = []
|
|
202
|
+
// (HOST, with the host state h, always computes: one slice object starts every app's host)
|
|
203
|
+
const out = (s: any, d: any, k: string, h?: any) => !h && last[0] === s && last[1] === d ? last[2] : (last = [s, d, steps[a](s, d, k, h)])[2]
|
|
204
|
+
model[a] = {
|
|
205
|
+
// VALIDATE: on the host's state (HOST), for a `values` function; the slice's own steps on it
|
|
206
|
+
...a == 'VALIDATE' ? {HOST: (h: any, d: any, _n: any, _p: any, _o: any, k: string) => { const x = out(h[k], d, k, h); return x ? {...h, [k]: x.s} : h }}
|
|
207
|
+
: {STATE: (s: any, d: any, _n: any, _p: any, _o: any, k: string) => out(s, d, k)?.s || ABORT},
|
|
208
|
+
EFFECT: (s: any, d: any, next: any, _p: any, _o: any, k: string) => {
|
|
209
|
+
const x = out(s, d, k), w = x?.wait
|
|
210
|
+
if (x?.send) {
|
|
211
|
+
const r = v(x.s.values).value
|
|
212
|
+
dev(233, s.values, r)
|
|
213
|
+
// G-577: a submit named after one of the form's own actions goes to that one
|
|
214
|
+
submit in steps && dev(234, submit, k, b)
|
|
215
|
+
next(submit, r, 0)
|
|
216
|
+
}
|
|
217
|
+
w && Promise.resolve(v(w.values)).then(() => next('RESULT', w, 0))
|
|
218
|
+
},
|
|
219
|
+
ELEMENT: (s: any, d: any, _n: any, _p: any, _o: any, k: string) => out(s, d, k)?.focus || ABORT,
|
|
220
|
+
...(checked.length && {[http]: (s: any, d: any, _n: any, _p: any, _o: any, k: string) => out(s, d, k)?.req || ABORT}),
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const b = defineBehavior({
|
|
225
|
+
form: schema,
|
|
226
|
+
// validating until VALIDATE (sync) or its RESULT (async): not valid yet (G-375, not validated)
|
|
227
|
+
initialState: {...base(values), validating: true},
|
|
228
|
+
intent: ({DOM}: any) => {
|
|
229
|
+
const f = DOM.select(sel)
|
|
230
|
+
let on = 0
|
|
231
|
+
return {
|
|
232
|
+
// D239: resetOnShow also starts over each time the form element appears again (a
|
|
233
|
+
// Switchable page shown again: hidden pages stay alive; a form rendered again)
|
|
234
|
+
VALIDATE: again ? xs.merge(xs.of(0), f.elements().filter((e: any) => { const r = e.length && !on; on = e.length; return r })) : xs.of(0),
|
|
235
|
+
// G-376: a checkbox (native, or a hyphenated tag with a boolean `checked`: a
|
|
236
|
+
// form-associated custom checkbox / switch) gives `checked` and its value as `item`; a
|
|
237
|
+
// <select multiple> the selected values; type=file is left alone; others give `value`
|
|
238
|
+
CHANGE: f.events('input').filter(({target: t}: any) => t.type != 'file').map(({target: t}: any) =>
|
|
239
|
+
t.type == 'checkbox' || /-/.test(t.tagName) && typeof t.checked == 'boolean' ? {name: t.name, value: t.checked, item: t.value}
|
|
240
|
+
: {name: t.name, value: t.type == 'select-multiple' ? [...t.selectedOptions].map((o: any) => o.value) : t.value}),
|
|
241
|
+
BLUR: f.events('focusout').map((e: any) => e.target.name),
|
|
242
|
+
SUBMIT: f.events('submit', {preventDefault: true}),
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
model,
|
|
246
|
+
calculated: {
|
|
247
|
+
fields: (s: any) => {
|
|
248
|
+
const out: any = {}
|
|
249
|
+
for (const n of fieldNames(s.values)) {
|
|
250
|
+
const value = getField(s.values, n)
|
|
251
|
+
const error = s.server[n] || s.remote[n] || ((s.submitCount || show != 'submit' && s.touched[n]) && s.errors[n]) || ''
|
|
252
|
+
out[n] = {name: n, value, error, invalid: !!error, touched: !!s.touched[n], dirty: value !== getField(s.initial, n), pending: n in s.pending}
|
|
253
|
+
}
|
|
254
|
+
return out
|
|
255
|
+
},
|
|
256
|
+
// G-382: an async schema re-validating keeps the last answer's validity (no flicker)
|
|
257
|
+
valid: (s: any) => s.validated && !keys(s.errors).length && !keys(s.remote).length,
|
|
258
|
+
dirty: (s: any) => JSON.stringify(s.values) != JSON.stringify(s.initial),
|
|
259
|
+
error: (s: any) => s.server[''] || (s.submitCount && s.errors['']) || '',
|
|
260
|
+
},
|
|
261
|
+
})({...o, values}), merge = b.merge
|
|
262
|
+
b.merge = (c: any, k: string) => {
|
|
263
|
+
const e = c.model?.[submit]
|
|
264
|
+
req = !!e && typeof e == 'object' && http in e
|
|
265
|
+
merge(c, k)
|
|
266
|
+
}
|
|
267
|
+
return b
|
|
268
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* PLAN-5 F-1: form helpers. The `form` behavior (./form.ts) is built on them, and they are
|
|
3
|
+
* exported as the escape hatch for a form written with plain model actions (D193, shape B).
|
|
4
|
+
* Pure functions over a plain `values` object, for model reducers; 0 B core.
|
|
5
|
+
*
|
|
6
|
+
* Field names: a field's `name=` attribute is its path in `values`, dot-separated. A segment
|
|
7
|
+
* inside an array of objects is the row's `id` (not its index), so touched/errors follow a row
|
|
8
|
+
* when another row is removed: `addresses.7.street` is `values.addresses.find(r => r.id == 7).street`.
|
|
9
|
+
* An array of non-objects (or rows without an id) uses the index; a row with an id is named by
|
|
10
|
+
* its id only (G-380: `addresses.0.city` is no field when the rows have ids).
|
|
11
|
+
*
|
|
12
|
+
* Validation: any Standard Schema (`schema['~standard'].validate`: zod, valibot, arktype, a
|
|
13
|
+
* hand-written object), no dependency. A sync schema validates inside reducers; an async one
|
|
14
|
+
* (validate returns a Promise) returns a Promise here, for an EFFECT.
|
|
15
|
+
*/
|
|
16
|
+
import type {SchemaIssue} from './standardSchema'
|
|
17
|
+
import {focusWithin} from './focusWithin'
|
|
18
|
+
import {ABORT} from '../shared'
|
|
19
|
+
|
|
20
|
+
export type FieldErrors = Record<string, string>
|
|
21
|
+
export type FormCheck = {errors: FieldErrors; value: any}
|
|
22
|
+
|
|
23
|
+
const isRow = (r: any) => r?.id != null
|
|
24
|
+
// the key of segment `s` in `v`: an array's row with that id, else the index unless the element
|
|
25
|
+
// there is a row (G-380: a row is named by its id only; -1, no element), an object's key
|
|
26
|
+
const key = (v: any, s: string): any => {
|
|
27
|
+
if (!Array.isArray(v)) return s
|
|
28
|
+
const i = v.findIndex(r => isRow(r) && r.id + '' == s)
|
|
29
|
+
return i < 0 && !isRow(v[+s]) ? +s : i
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** the field name (id-based path) of a schema issue path (index-based) */
|
|
33
|
+
export const fieldName = (values: any, path: ReadonlyArray<any> = []): string => {
|
|
34
|
+
let v = values
|
|
35
|
+
return path.map(p => {
|
|
36
|
+
p = p?.key ?? p
|
|
37
|
+
const r = Array.isArray(v) && isRow(v[p]) ? v[p].id : p
|
|
38
|
+
v = v?.[p]
|
|
39
|
+
return r
|
|
40
|
+
}).join('.')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** the value at a field name (undefined when there is none) */
|
|
44
|
+
export const getField = (values: any, name: string): any =>
|
|
45
|
+
name.split('.').reduce((v, s) => v?.[key(v, s)], values)
|
|
46
|
+
|
|
47
|
+
/** whether `name` is a field of `values`: its path exists (G-379: also when the value is undefined) */
|
|
48
|
+
export const hasField = (values: any, name: string): boolean => {
|
|
49
|
+
let v = values
|
|
50
|
+
for (const s of name.split('.')) {
|
|
51
|
+
const k = v && typeof v == 'object' ? key(v, s) : -1
|
|
52
|
+
if (!(k in Object(v))) return false
|
|
53
|
+
v = v[k]
|
|
54
|
+
}
|
|
55
|
+
return true
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** `values` with the field at `name` set to `value` (immutable; an unknown row is left alone) */
|
|
59
|
+
export const setField = (values: any, name: string, value: any): any => {
|
|
60
|
+
const [s, ...rest] = name.split('.'), k = key(values, s), a = Array.isArray(values)
|
|
61
|
+
const x = rest.length ? setField(values?.[k] ?? {}, rest.join('.'), value) : value
|
|
62
|
+
if (x === values?.[k] || a && !(k in values)) return values
|
|
63
|
+
const c = a ? values.slice() : {...values}
|
|
64
|
+
c[k] = x
|
|
65
|
+
return c
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* every field name of `values`, in object order: each leaf, and each array itself (for an
|
|
70
|
+
* array-level error such as "at least one address"); a row's `id` is not a field
|
|
71
|
+
*/
|
|
72
|
+
export const fieldNames = (v: any, at = '', row?: boolean): string[] => {
|
|
73
|
+
if (!v || typeof v != 'object') return at ? [at] : []
|
|
74
|
+
const a = Array.isArray(v), out = a ? [at] : []
|
|
75
|
+
for (const k in v) {
|
|
76
|
+
const x = (v as any)[k]
|
|
77
|
+
if (!row || k != 'id') out.push(...fieldNames(x, (at && at + '.') + (a && isRow(x) ? x.id : k), a))
|
|
78
|
+
}
|
|
79
|
+
return out
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const errorsOf = (values: any, issues: ReadonlyArray<SchemaIssue> = []): FieldErrors => {
|
|
83
|
+
const e: FieldErrors = {}
|
|
84
|
+
for (const i of issues) {
|
|
85
|
+
const n = fieldName(values, i.path)
|
|
86
|
+
n in e || (e[n] = i.message)
|
|
87
|
+
}
|
|
88
|
+
return e
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Validates `values` with a Standard Schema: `{ errors, value }` (errors by field name, '' for
|
|
93
|
+
* a form-level issue, the first message per field; `value` is the schema's output, transformed,
|
|
94
|
+
* when there are none). A Promise of it when the schema is async. `schema` must be a Standard
|
|
95
|
+
* Schema object (the `form` behavior reports SYG231 in dev).
|
|
96
|
+
*/
|
|
97
|
+
export const checkForm = (schema: any, values: any): FormCheck | Promise<FormCheck> => {
|
|
98
|
+
const r = schema['~standard'].validate(values)
|
|
99
|
+
const done = (r: any): FormCheck => ({errors: errorsOf(values, r.issues), value: r.issues ? undefined : r.value})
|
|
100
|
+
return r.then ? r.then(done) : done(r)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** the errors only, by field name ({} when valid); a Promise of them for an async schema */
|
|
104
|
+
export const formErrors = (schema: any, values: any): any => {
|
|
105
|
+
const r: any = checkForm(schema, values)
|
|
106
|
+
return r.then ? r.then((x: FormCheck) => x.errors) : r.errors
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Server errors as field errors: an error reply (a driver's `{ error, status?, body?, request }`
|
|
111
|
+
* or `{ status, body }`; the body `{ errors }` or the map itself), a map `{ name: message |
|
|
112
|
+
* messages }`, or a list of Standard-Schema-like issues `[{ path, message }]` (index paths
|
|
113
|
+
* become field names with `values`). With `values`, a message for a name that isn't one of its
|
|
114
|
+
* fields (`message`, `error`) is the form-level message (''); a reply without any becomes
|
|
115
|
+
* 'Request failed (status)' under ''.
|
|
116
|
+
*/
|
|
117
|
+
export const replyErrors = (reply: any, values?: any): FieldErrors => {
|
|
118
|
+
let e = reply && ('request' in reply || 'body' in reply && 'status' in reply) ? reply.body : reply
|
|
119
|
+
e = e?.errors ?? e
|
|
120
|
+
const out: FieldErrors = Array.isArray(e) ? errorsOf(values, e) : {}
|
|
121
|
+
if (e && typeof e == 'object' && !Array.isArray(e)) for (const k in e) {
|
|
122
|
+
const m = [].concat(e[k])[0] + ''
|
|
123
|
+
k && values && !hasField(values, k) ? out[''] ||= m : out[k] = m
|
|
124
|
+
}
|
|
125
|
+
return Object.keys(out).length ? out : {'': 'Request failed' + (reply?.status ? ` (${reply.status})` : '')}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* An ELEMENT command that focuses the first field, in DOM order, whose `name` is one of `names`
|
|
130
|
+
* (a list, or an errors map: its names with a message; '' is skipped), anywhere in the sender's view, fields
|
|
131
|
+
* of child components and Collection items included (`focusWithin`, D194). With `within` (a
|
|
132
|
+
* selector: the form element's), only fields inside a matching element (G-373: a second form,
|
|
133
|
+
* or a child component's field of the same name outside the form, isn't focused). ABORT when
|
|
134
|
+
* there is nothing to focus: `ELEMENT: (state) => focusInvalid(state.errors, '.signup')`.
|
|
135
|
+
*/
|
|
136
|
+
export const focusInvalid = (names: string[] | FieldErrors, within?: string): any => {
|
|
137
|
+
const list = (Array.isArray(names) ? names : Object.keys(names).filter(n => names[n])).filter(n => n)
|
|
138
|
+
return list.length ? {focus: focusWithin(list.map(n => `${within ? within + ' ' : ''}[name=${JSON.stringify(n)}]`).join())} : ABORT
|
|
139
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import {Stream} from 'xstream';
|
|
2
|
+
import {senderOf, makeReplies} from './replies';
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* makeHeadDriver({ titleTemplate?, document? }) (PLAN-3 §1.7, X-1). Docs: src/index.d.ts and
|
|
6
|
+
* docs guide/head.md. Summary:
|
|
7
|
+
*
|
|
8
|
+
* - Values `{ title?, meta?: { [name | property]: content | null }, link?: [{ rel, href, key?, ... }] }`
|
|
9
|
+
* come from a component's `head` static (`App.head = state => ({ title })`, sent as
|
|
10
|
+
* `{ head }`, `__sygnalStatic: 'head'`) or from a model entry (`HEAD: (s, d) => ({ title })`).
|
|
11
|
+
* Each component instance has one entry, replaced by its next value; a falsy value or the
|
|
12
|
+
* instance's dispose removes it.
|
|
13
|
+
* - Merge, in mount order (first declaration; a removed entry that comes back goes last): a
|
|
14
|
+
* later `title` wins, `meta` keys merge (null removes a key), `link`s merge by `key`, else by
|
|
15
|
+
* `rel` for canonical, else by `rel` + `href`. Meta keys with an `og:`/`article:`/... prefix
|
|
16
|
+
* use `property`, the others `name`.
|
|
17
|
+
* - DOM: `document.title` (the page's original title when no entry sets one); meta and link
|
|
18
|
+
* tags it creates carry `data-sygnal-head="<key>"`. An existing unmarked `<meta>` with the same
|
|
19
|
+
* name is updated and restored when no entry sets it. Marked tags from SSR (`renderHead()`)
|
|
20
|
+
* are adopted, or removed once no entry sets them.
|
|
21
|
+
* - SSR: `renderToString(App, { head: list })` pushes each rendered component's `head` static
|
|
22
|
+
* into `list`; `renderHead(list, { titleTemplate })` gives the tags. No document: no-op.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const PROPERTY = /^(og|fb|article|book|profile|music|video|al):/;
|
|
26
|
+
const esc = (s: any) => String(s).replace(/[&<>"]/g, c => `&#${c.charCodeAt(0)};`);
|
|
27
|
+
const attrOf = (k: string) => PROPERTY.test(k) ? 'property' : 'name';
|
|
28
|
+
|
|
29
|
+
/** merged `{ title, meta: [[key, content]], link: [[key, attrs]] }` of head values, in order */
|
|
30
|
+
export function mergeHead(list: any[], titleTemplate?: string) {
|
|
31
|
+
let title: any;
|
|
32
|
+
const meta: Record<string, any> = {}, link: Record<string, any> = {};
|
|
33
|
+
for (const h of list) {
|
|
34
|
+
if (!h || typeof h != 'object') continue;
|
|
35
|
+
if (h.title != null) title = h.title;
|
|
36
|
+
Object.assign(meta, h.meta);
|
|
37
|
+
for (const l of h.link || []) if (l) link['l:' + (l.key ?? (l.rel == 'canonical' ? l.rel : l.rel + ' ' + l.href))] = l;
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
title: title != null && titleTemplate ? titleTemplate.replace('%s', title) : title,
|
|
41
|
+
meta: Object.keys(meta).filter(k => meta[k] != null).map(k => ['m:' + k, k, meta[k]]),
|
|
42
|
+
link: Object.entries(link),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** the `<title>`, `<meta>` and `<link>` tags of head values (SSR) */
|
|
47
|
+
export function renderHead(list: any[], options: {titleTemplate?: string} = {}): string {
|
|
48
|
+
const m = mergeHead(list, options.titleTemplate);
|
|
49
|
+
const attrs = (o: any) => Object.keys(o).filter(k => k != 'key' && o[k] != null && o[k] !== false).map(k => ` ${k}="${esc(o[k])}"`).join('');
|
|
50
|
+
return (m.title != null ? `<title>${esc(m.title)}</title>` : '') +
|
|
51
|
+
m.meta.map(([key, k, c]) => `<meta${attrs({[attrOf(k)]: k, content: c})} data-sygnal-head="${esc(key)}">`).join('') +
|
|
52
|
+
m.link.map(([key, l]) => `<link${attrs(l)} data-sygnal-head="${esc(key)}">`).join('');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function makeHeadDriver(options: {titleTemplate?: string, document?: any} = {}) {
|
|
56
|
+
return (sink$: Stream<any>) => {
|
|
57
|
+
const d: any = options.document || (typeof document != 'undefined' ? document : null);
|
|
58
|
+
const entries = new Map<any, any>();
|
|
59
|
+
// key → element this driver manages; adopted: element → its original content
|
|
60
|
+
const made = new Map<string, any>(), adopted = new Map<any, any>();
|
|
61
|
+
const title0 = d?.title;
|
|
62
|
+
d?.querySelectorAll('[data-sygnal-head]').forEach((el: any) => made.set(el.getAttribute('data-sygnal-head'), el));
|
|
63
|
+
|
|
64
|
+
const set = (el: any, o: any) => { for (const k in o) if (k != 'key') o[k] == null || o[k] === false ? el.removeAttribute(k) : el.setAttribute(k, o[k]); };
|
|
65
|
+
const apply = () => {
|
|
66
|
+
if (!d) return;
|
|
67
|
+
const m = mergeHead([...entries.values()], options.titleTemplate), want = new Set<string>();
|
|
68
|
+
d.title = m.title ?? title0;
|
|
69
|
+
const put = (key: string, tag: string, o: any, find?: string) => {
|
|
70
|
+
want.add(key);
|
|
71
|
+
let el = made.get(key);
|
|
72
|
+
if (!el) {
|
|
73
|
+
el = find && d.head.querySelector(find);
|
|
74
|
+
if (el) adopted.set(el, el.getAttribute('content'));
|
|
75
|
+
else (el = d.createElement(tag)).setAttribute('data-sygnal-head', key), d.head.appendChild(el);
|
|
76
|
+
made.set(key, el);
|
|
77
|
+
}
|
|
78
|
+
set(el, o);
|
|
79
|
+
};
|
|
80
|
+
for (const [key, k, c] of m.meta) put(key, 'meta', {[attrOf(k)]: k, content: c}, `meta[${attrOf(k)}="${k}"]`);
|
|
81
|
+
for (const [key, l] of m.link) put(key, 'link', l);
|
|
82
|
+
made.forEach((el, key) => {
|
|
83
|
+
if (want.has(key)) return;
|
|
84
|
+
made.delete(key);
|
|
85
|
+
if (adopted.has(el)) set(el, {content: adopted.get(el)}), adopted.delete(el);
|
|
86
|
+
else el.remove();
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const {replies} = makeReplies(s => { if (entries.delete(s)) apply(); });
|
|
91
|
+
|
|
92
|
+
sink$.addListener({
|
|
93
|
+
next: (v: any) => {
|
|
94
|
+
if (!v || typeof v != 'object') return;
|
|
95
|
+
const s = senderOf(v), h = 'head' in v ? v.head : v;
|
|
96
|
+
if (h && typeof h == 'object') entries.set(s, h);
|
|
97
|
+
else entries.delete(s);
|
|
98
|
+
apply();
|
|
99
|
+
},
|
|
100
|
+
error: (e: any) => console.error('[Sygnal] head', e),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const dispose = () => { entries.clear(); apply(); };
|
|
104
|
+
return {...replies, __sygnalStatic: 'head', dispose};
|
|
105
|
+
};
|
|
106
|
+
}
|
package/src/extra/hmr.ts
CHANGED
|
@@ -10,7 +10,6 @@ interface HotModule {
|
|
|
10
10
|
interface SygnalApp {
|
|
11
11
|
hmr: (next: any, state?: any) => void;
|
|
12
12
|
dispose?: () => void;
|
|
13
|
-
sources?: {STATE?: {stream?: {_v?: any}}};
|
|
14
13
|
}
|
|
15
14
|
|
|
16
15
|
function normalizeModule(maybeModule: any): ModuleWithDefault | null {
|
|
@@ -44,8 +43,8 @@ export default function enableHMR(
|
|
|
44
43
|
next = normalizeModule(loaded);
|
|
45
44
|
}
|
|
46
45
|
if (next) {
|
|
47
|
-
|
|
48
|
-
app.hmr(next
|
|
46
|
+
// (hmr() keeps the app's current state, read through the runtime API)
|
|
47
|
+
app.hmr(next);
|
|
49
48
|
}
|
|
50
49
|
} finally {
|
|
51
50
|
applying = false;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PLAN-4.5 G-275: marks an initial state that a wrapper built from its caller's values (an
|
|
3
|
+
* element's host props, Vike's pageContext.data or hydrated state, renderComponent's
|
|
4
|
+
* initialState option). The dev statics freeze (D152, checks/statics.ts) leaves a marked object
|
|
5
|
+
* and everything in it alone: the caller may still change those values. A non-enumerable symbol
|
|
6
|
+
* key: spreads, JSON and Object.keys don't see it.
|
|
7
|
+
*/
|
|
8
|
+
export const OWNED = Symbol.for('sygnal.owned')
|
|
9
|
+
|
|
10
|
+
export const owned = <T>(v: T): T => {
|
|
11
|
+
// G-289: an object that refuses the key (a Proxy) is returned as is
|
|
12
|
+
try { if (v && typeof v == 'object' && Object.isExtensible(v)) Object.defineProperty(v, OWNED, {value: 1}) } catch {}
|
|
13
|
+
return v
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* G-289: owned() of a shallow copy of a plain object or array (the caller's own object stays
|
|
18
|
+
* unmarked: it may be a component's static, which the freeze must still cover, or sealed, which
|
|
19
|
+
* can't be marked); anything else as owned()
|
|
20
|
+
*/
|
|
21
|
+
export const ownedCopy = <T>(v: T): T => {
|
|
22
|
+
const p = v && typeof v == 'object' ? Object.getPrototypeOf(v) : 0
|
|
23
|
+
return owned(Array.isArray(v) ? [...v] as any : p == Object.prototype || p === null ? {...v} : v)
|
|
24
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* PLAN-4 GS-1: the first-party `pager` behavior.
|
|
3
|
+
*
|
|
4
|
+
* const { Older, Newer } = controls({ Older: 'button', Newer: 'button' })
|
|
5
|
+
* List.uses = { pager: pager({ pageSize: 10, next: Newer, prev: Older }) }
|
|
6
|
+
*
|
|
7
|
+
* Slice: { page, pageSize, total } plus the calculated offset, pages, hasPrev, hasNext.
|
|
8
|
+
* Actions: pager.NEXT, pager.PREV (ABORT at the bounds), pager.GOTO (page number, clamped),
|
|
9
|
+
* pager.SET_TOTAL (item count; keeps the page in range). `total` is optional: without it NEXT
|
|
10
|
+
* has no upper bound and `pages` is null. A host reducer may also write `state.pager.total`;
|
|
11
|
+
* the calculated fields follow.
|
|
12
|
+
*/
|
|
13
|
+
import { defineBehavior } from './behaviors'
|
|
14
|
+
import { ABORT } from '../shared'
|
|
15
|
+
|
|
16
|
+
const pagesOf = (p: any) => p.total == null ? null : Math.max(1, Math.ceil(p.total / p.pageSize))
|
|
17
|
+
const to = (p: any, page: number) => {
|
|
18
|
+
const n = pagesOf(p)
|
|
19
|
+
page = Math.max(0, Math.min(page | 0, n == null ? page : n - 1))
|
|
20
|
+
return page === p.page ? ABORT : {...p, page}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A page cursor over a list (GS-1 behavior). Options: `pageSize` (20), `page` (0), `total`
|
|
25
|
+
* (null: unknown), and the controls `next` / `prev` whose clicks move the page. Use it as
|
|
26
|
+
* `uses = { pager: pager({ pageSize: 10, next: Newer, prev: Older }) }`, then render
|
|
27
|
+
* `items.slice(state.pager.offset, state.pager.offset + state.pager.pageSize)`.
|
|
28
|
+
*/
|
|
29
|
+
export const pager = /*#__PURE__*/ defineBehavior({
|
|
30
|
+
initialState: {page: 0, pageSize: 20, total: null},
|
|
31
|
+
intent: ({DOM}: any, {next, prev}: any) => ({
|
|
32
|
+
...(next && {NEXT: DOM.click(next)}),
|
|
33
|
+
...(prev && {PREV: DOM.click(prev)}),
|
|
34
|
+
}),
|
|
35
|
+
model: {
|
|
36
|
+
NEXT: (p: any) => to(p, p.page + 1),
|
|
37
|
+
PREV: (p: any) => to(p, p.page - 1),
|
|
38
|
+
GOTO: (p: any, page: number) => to(p, page),
|
|
39
|
+
SET_TOTAL: (p: any, total: number) => {
|
|
40
|
+
const q = {...p, total}, r = to(q, p.page)
|
|
41
|
+
return r === ABORT ? q : r
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
calculated: {
|
|
45
|
+
offset: (p: any) => p.page * p.pageSize,
|
|
46
|
+
pages: pagesOf,
|
|
47
|
+
hasPrev: (p: any) => p.page > 0,
|
|
48
|
+
hasNext: (p: any) => p.total == null || p.page + 1 < pagesOf(p)!,
|
|
49
|
+
},
|
|
50
|
+
})
|