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