sygnal 7.0.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 +632 -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 +73 -45
- package/dist/astro/server.cjs.js.map +1 -1
- package/dist/astro/server.mjs +73 -45
- 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 +224 -20
- package/dist/diagnostics.cjs.js.map +1 -1
- package/dist/diagnostics.esm.js +224 -20
- package/dist/diagnostics.esm.js.map +1 -1
- package/dist/guide/accessibility.md +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/resources.md +2 -0
- package/dist/guide/sandboxed-components.md +223 -0
- package/dist/guide/timers.md +32 -5
- 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 +3948 -586
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +584 -44
- package/dist/index.esm.js +3918 -586
- 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 +37 -35
- 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/hosts/switchable.ts +26 -8
- package/src/core/instance.ts +98 -45
- package/src/core/markers/portal.ts +4 -0
- package/src/core/registry.ts +4 -0
- package/src/core/runtime.ts +34 -25
- package/src/core/statics.ts +5 -5
- 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/ai/transports/shared.ts +3 -1
- package/src/extra/api/contract.ts +485 -0
- package/src/extra/api/index.ts +26 -0
- package/src/extra/behaviors.ts +3 -3
- 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 +78 -4
- package/src/extra/diagnostics/checks/props.ts +6 -1
- 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 +51 -1
- package/src/extra/elementCommands.ts +20 -4
- package/src/extra/fetchDriver.ts +4 -1
- package/src/extra/fetchEventSource.ts +6 -2
- 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/socketDriver.ts +51 -6
- package/src/extra/ssr.ts +128 -42
- package/src/extra/testing.ts +295 -39
- package/src/extra/timers.ts +96 -7
- 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 +302 -38
- package/src/index.ts +15 -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/llms/ai.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Sygnal: AI (`sygnal/ai`): chat, one-off requests, agent tools, decisions, composed UI, chat cards
|
|
2
|
+
|
|
3
|
+
> A topic file of Sygnal's agent context. It assumes the core (`llms.txt`, or the sygnal-dev skill's `SKILL.md`: mental model, component anatomy, canonical forms, wiring rules, testing): read that first. Canonical forms only.
|
|
4
|
+
|
|
5
|
+
## In short
|
|
6
|
+
- **AI** (`sygnal/ai`): a chat conversation is the `chat` behavior, never hand-rolled: `uses = { chat: chat({ agent: false, form: '.ask', prompt: '.prompt', stop: '.stop', regenerate: '.retry' }) }` → `state.chat` (`messages`, `prompt`, `draft`, `status`, `error`; Stop keeps the partial reply, Retry doesn't repeat the question); a one-off request (summary, structured `output`) is an `LLM` sink request `{ messages: [{ role: 'user', content: text }], delta: 'DELTA', ok: 'DONE', error: 'FAILED' }`; both need `makeChatDriver({ transport })` (tests: `t.stream('LLM', ['Hi'])`); what agents may run is the `agent` static `{ name, description, read, actions, queries }` (only declared actions; `read` is all they see; `queries: { find: { description, input, run: (state, input) => result } }` are read-only lookups, pure and synchronous; an action that starts a fetch answers once it lands with `until: 'resourceName'`; tests: `t.callTool(name, args)`); typed answers: `decide()` + `choice` / `noul` / `score` as a resource. No provider API key in the browser: `uiMessageStream('/api/chat')` to your server.
|
|
7
|
+
|
|
8
|
+
## Details
|
|
9
|
+
- **A chat conversation** (send, streaming draft, Stop keeping the partial reply, Retry) is the `chat` behavior with no tools; don't hand-roll it: `Chat.uses = { chat: chat({ agent: false, form: '.ask', prompt: '.prompt', stop: '.stop', regenerate: '.retry' }) }` → `state.chat` (`messages`, `prompt` (bind `<input className="prompt" value={state.chat.prompt} />`), `draft`, `status`: `'ready' | 'submitted' | 'streaming' | 'error'`, `error`), the markup in the host's own view (SYG104), `next('chat.SEND', text)` to send from code, a host entry `'chat.DONE'` after a reply; options and slice: agent.md "Options and state". **One-off requests** (a summary, structured `output`, or your own state shape) are a driver request, like HTTP: `SEND: { STATE: (s) => ({ ...s, messages: [...s.messages, { role: 'user', content: s.prompt }], prompt: '' }), LLM: (s) => ({ messages: s.messages, key: 'reply', delta: 'DELTA', ok: 'DONE', error: 'FAILED' }) }` (STATE runs first: the LLM sink sends the list it stored), `DELTA: (s, { text }) => ({ ...s, draft: text })` (all the text so far), `DONE: (s, { message }) => ({ ...s, messages: [...s.messages, message], draft: '' })` (also `text`, `toolCalls`, and `value` for `output: schema`), `FAILED` gets `{ error }`; `{ abort: 'reply' }` stops it (nothing more arrives; `latest` is on: a new request with the same key aborts the old); render with `messageText(m)`. main.js, a local Ollama: `run(App, { LLM: makeChatDriver({ transport: openResponses({ baseURL: 'http://localhost:11434/v1', model: 'llama3.2' }) }) })`; production: `transport: uiMessageStream('/api/chat')` (an AI SDK route on your server; the JSON it POSTs: ai-chat.md, "What uiMessageStream sends"). Tests need no driver: `await t.stream('LLM', ['Hel', 'lo'])` (then `ok`), `t.respond('LLM', text)`, `t.fail('LLM', 429)`, `t.requests('LLM')`.
|
|
10
|
+
- **`agent` static**: what an LLM may do in the app: `TodoApp.agent = { name: 'todos', description: 'The todo list', read: (state) => ({ todos: state.todos.map(({ id, text, done }) => ({ id, text, done })) }), actions: { ADD: { description: 'Add a todo', input: z.string().min(1) }, CLEAR_DONE: { description: 'Delete the done todos', consequential: true } } }` → tools `todos_add`, `todos_clear_done`, `todos_read`. **Only declared actions are tools, and `read` is all agents see** (a projection: keep ids, leave out secrets; `untrusted: true` when it holds text users typed). Per action: `input` (a Standard Schema with JSON Schema: zod 4.2+, or `jsonSchema({ type: 'string' })`; the reducer's data is the parsed input, so `z.object({ packed: z.boolean() })` gives `(state, { packed })`, never `(state, packed)`), `consequential` (the user confirms), `when: (state) => bool`, `idempotent`, `until: 'search'` (the call answers once that resource has loaded, with the `read`, not `loading`; or `until: (state) => bool` for a reply action; `timeout` → `{ ok: true, pending: true }`); an entry counts as a trigger (SYG102); a Collection item's `agent` (+ `label: (s) => s.text`) gives one tool per action with an `id` parameter. Data too large for `read` (a long list, one record): `queries: { find: { description, input, run: (state, input) => state.todos.filter(…) } }` → read-only tool `todos_find`, `{ ok: true, result }`, nothing dispatched; `run` is pure and synchronous (async is SYG246: load data with an action + `until`). A reducer refuses with `abort('reason')` (from `'sygnal'`). Tests: `t.tools()`, `await t.callTool('todos_add', { value: 'milk' })` → `{ ok: true, state }` (a consequential tool needs `{ confirm: true }`), `t.agentContext()`.
|
|
11
|
+
- **Decisions** (typed answers from a decision model; a fetch-driver request, `HTTP: makeFetchDriver()`): `const questions = { topic: choice('What is this ticket about?', { billing: 'Payments, invoices', bug: 'Something is broken' }), urgent: noul('Does it need handling today?'), mood: score('How upset is the customer?', ['calm', 'annoyed', 'angry']) }`, as a resource: `Ticket.resources = { triage: (state) => state.text && decide({ model: 'jev-latest', state: state.text, questions }) }` → `state.triage.data.answers.topic.choice` (`.confidence`: below ~0.6, ask a chat model), `answers.urgent.noul` (0–1), `answers.mood.score`; a click-triggered one is an `HTTP` sink `decide({ …, ok, error })`. Tests: `await t.respond('HTTP', answers(questions, { topic: 'bug', urgent: true }), 'triage')`.
|
|
12
|
+
- **A page a model composes from your parts** (a dashboard the user describes in words; the model only picks parts, params and places, never markup): `const catalog = defineCatalog({ parts: { revenue: { component: Kpi, props: { metric: 'revenue' }, description: 'Revenue (KPI card: the total this month)' }, orders: { component: OrdersTable, description: 'Orders (table of recent orders)', params: { status: { default: 'any', options: { any: 'orders of every status', pending: 'pending orders (not paid yet)' } } } }, tasks: { component: Checklist, state: 'tasks', description: "Tasks (the user's checklist)" } } })` (`description` is all a model sees: "Noun (what it shows)", no overlap between parts; params arrive as props; `state` is a key, a lens, or for a record param (`{ from: (state) => records }`) `(node) => byId('customers', node.params.id)` with `byId` from `'sygnal'`; `containers: { tabs: { slots, render: ({ slots, control }) => … } }` are view functions, never components (SYG161)), `Dashboard.uses = { page: compose(catalog, { backend: compose.decisions({ model: 'jev-latest' }), form: '.ask', prompt: '.prompt' }) }` (or `compose.llm()` on the `LLM` sink: same app, same tests) → `state.page` (`status`: `'composing' | 'preview' | 'done' | 'unavailable' | 'error'`, `layout`, `decisions`, `error`), rendered in the host's view as `{catalog.render(state.page, state)}` or `<Composed catalog={catalog} from="page" />`: the parts are the host's children (lenses, `CHILD.select`) keyed by identity, so a follow-up `next('page.COMPOSE', 'Move the orders to the top')` edits the page and keeps the untouched parts' instances (`{ prompt, fresh: true }` starts over; `persist` the slice to keep the page); for an agent: `actions: { ...compose.action('page') }`, `read: (s) => ({ page: compose.summary(s.page) })`. Tests (either backend): `t.simulateAction('page.COMPOSE', 'Pending orders')`, then `await composeWith(t, catalog, [{ part: 'orders', params: { status: 'pending' } }])`.
|
|
13
|
+
- **Cards in a chat** (the assistant asks with your parts; the user clicks the answer): `uses = { assistant: chat({ form: '.ask', prompt: '.prompt', ask: { catalog } }) }` gives the model the tool `ask_user({ need })`; the card composes from the catalog (`compose.llm()` on the chat's sink by default, or `ask: { catalog, backend: compose.decisions({ model: 'jev-latest' }) }`) and lives on the tool part as `part.ui` (`status`: `'composing' | 'open' | 'answered' | 'failed' | 'cancelled'`; persisted with the chat): render it where the view renders a `tool-ask_user` message part, `askCard(catalog, state, part) ?? <p>Composing…</p>`. A part with `result: true` answers it: `PICK: { PARENT: (state, id, next, props) => compose.answer(props, id) }` → the call's output is `{ part, answer }` and the chat goes on (no result part in the catalog: SYG686). Tools the app answers itself: `clientTools: { confirm: { description, inputSchema } }`, listed by `openToolCalls(state.assistant)`, answered with `next('assistant.TOOL_OUTPUT', { toolCallId, output })`. `chat({ model: (state) => state.chatModel })` picks the model per request. Tests: `await t.stream('LLM', [{ toolCall: { id: 'c1', name: 'ask_user', input: { need: 'Pick a plan' } } }])`, then `await composeWith(t, catalog, [{ part: 'plans' }])`.
|
|
14
|
+
- **Never put a provider API key in browser code**: the browser calls your server (`uiMessageStream('/api/chat')`, the default `/api/decide` of `decide()` and `compose.decisions()`), which adds the key, fixes the model and instructions, and rate-limits; an auth header to another origin is refused (SYG670). Strict provider schemas: `openResponses({ baseURL, model, strict: strictSchemas })` (an import; `strict: true` is SYG672). Every request sends the whole conversation (models are stateless); never `fetch` a provider from a component.
|
|
15
|
+
- Guides: `node_modules/sygnal/dist/guide/ai-chat.md` (transports, structured `output`, tools, shipping), `ai-decisions.md` (escalation, `decide.openai()`), `ai-compose.md` (catalogs, containers, edits, `byId`), `ai-cards.md` (`chat({ ask })`, client tools), `agent.md` (the in-app assistant `uses = { assistant: chat({ form: '.ask', prompt: '.prompt', instructions }) }`, `commandBar`, security), `webmcp.md` (`experimentalExposeWebMcp(app)`, `form(schema, { tool: formTool({ name, description }) })`: experimental, outside semver), `mcp-apps.md` (`makeMcpAppDriver()`: the app inside Claude or ChatGPT); online https://sygnal.js.org/guide/ai-chat/ (and `/guide/ai-decisions/`, `/guide/ai-compose/`, `/guide/ai-cards/`, `/guide/agent/`, `/guide/webmcp/`, `/guide/mcp-apps/`)
|
|
16
|
+
|
|
17
|
+
## A composed page, end to end
|
|
18
|
+
A model builds a page (or a card in a chat) from parts the app prepared: it only picks parts, params and places, never markup. Guides: `ai-compose.md`, `ai-cards.md`.
|
|
19
|
+
```jsx
|
|
20
|
+
import { defineCatalog, compose, Composed } from 'sygnal/ai'
|
|
21
|
+
import { OrdersTable, Checklist } from './parts.jsx'
|
|
22
|
+
|
|
23
|
+
export const catalog = defineCatalog({
|
|
24
|
+
parts: {
|
|
25
|
+
orders: {
|
|
26
|
+
component: OrdersTable,
|
|
27
|
+
description: 'Orders (table of recent orders)', // all a model sees: "Noun (what it shows)"
|
|
28
|
+
params: { status: { default: 'any', options: { any: 'orders of every status', pending: 'pending orders (not paid yet)' } } },
|
|
29
|
+
},
|
|
30
|
+
tasks: { component: Checklist, state: 'tasks', description: "Tasks (the user's checklist)" }, // a lens into app state
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
export function Dashboard({ state }) {
|
|
35
|
+
return (
|
|
36
|
+
<main>
|
|
37
|
+
<form className="ask">
|
|
38
|
+
<input className="prompt" aria-label="The page you want" value={state.page.prompt} />
|
|
39
|
+
<button type="submit">Compose</button>
|
|
40
|
+
</form>
|
|
41
|
+
<Composed catalog={catalog} from="page" />
|
|
42
|
+
</main>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
Dashboard.initialState = { tasks: [] }
|
|
46
|
+
Dashboard.uses = { page: compose(catalog, { backend: compose.decisions({ model: 'jev-latest' }), form: '.ask', prompt: '.prompt' }) }
|
|
47
|
+
```
|
|
48
|
+
```js
|
|
49
|
+
import { it, expect } from 'vitest'
|
|
50
|
+
import { renderComponent } from 'sygnal'
|
|
51
|
+
import { composeWith } from 'sygnal/ai'
|
|
52
|
+
import { catalog, Dashboard } from './Dashboard.jsx'
|
|
53
|
+
|
|
54
|
+
it('composes the pending orders', async () => {
|
|
55
|
+
const t = renderComponent(Dashboard)
|
|
56
|
+
t.simulateAction('page.COMPOSE', 'Pending orders')
|
|
57
|
+
await composeWith(t, catalog, [{ part: 'orders', params: { status: 'pending' } }]) // answers either backend
|
|
58
|
+
await t.waitForState((s) => s.page.status === 'done')
|
|
59
|
+
expect(t.state.page.layout.nodes.orders.params).toEqual({ status: 'pending' })
|
|
60
|
+
t.dispose()
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
- `backend: compose.llm()` (the `LLM` sink, a streamed preview) takes the same catalog, view and tests. `compose.decisions()` posts to your `/api/decide` route, which holds the API key. `state.page`: `status` (`'composing' | 'preview' | 'done' | 'unavailable' | 'error'`), `layout` (plain JSON), `decisions`, `error`.
|
|
64
|
+
- Parts are the host's children, keyed by their node key: params arrive as props, `CHILD.select(Part)` hears their `PARENT`, a `state` lens edits app state. A follow-up `next('page.COMPOSE', 'Move the orders to the top')` edits the page and keeps the untouched parts' instances; `{ prompt, fresh: true }` starts over. One part per record: a param `{ from: (state) => records }` + `state: (node) => byId('customers', node.params.id)` (`byId` from `'sygnal'`). Containers (`containers: { tabs: { slots, render: (view) => … } }`) are view functions, never components (SYG161). `{catalog.render(state.page, state)}` is the view-call form of `<Composed>`. An agent composes with `actions: { ...compose.action('page') }` and reads `compose.summary(state.page)`.
|
|
65
|
+
- **Chat cards**: `chat({ form: '.ask', prompt: '.prompt', ask: { catalog } })` under `assistant` gives the model `ask_user({ need })`; render each `tool-ask_user` message part with `askCard(catalog, state, part)` (null while composing; the card is `part.ui`, `status` `'composing' | 'open' | 'answered' | 'failed' | 'cancelled'`). A part with `result: true` answers it: `PICK: { PARENT: (state, id, next, props) => compose.answer(props, id) }` → the call's output `{ part, answer }` (no result part: SYG686). Tools the app answers itself: `clientTools: { confirm: { description, inputSchema } }`, `openToolCalls(state.assistant)`, `next('assistant.TOOL_OUTPUT', { toolCallId, output })`. Tests: `await t.stream('LLM', [{ toolCall: { id: 'c1', name: 'ask_user', input: { need: 'Pick a plan' } } }])`, then `await composeWith(t, catalog, [{ part: 'plans' }])`.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Sygnal: Collaborative state (`sygnal/yjs`, experimental)
|
|
2
|
+
|
|
3
|
+
> A topic file of Sygnal's agent context. It assumes the core (`llms.txt`, or the sygnal-dev skill's `SKILL.md`: mental model, component anatomy, canonical forms, wiring rules, testing): read that first. Canonical forms only.
|
|
4
|
+
|
|
5
|
+
- **Collaborative state** (experimental, `sygnal/yjs`; `npm install yjs y-protocols`, optional peers): several users edit one key of the state at once, merged by a Yjs document. `Board.uses = { board: shared({ presence: 'peers', history: 'history', shape: { title: 'text', items: collection({ text: 'text' }) } }) }` (import `shared`, `collection`, `makeYjsDriver` from `'sygnal/yjs'`); keep `board` in `Board.initialState` (`shared()` adds no slice of its own: `ownsState: false`, so no SYG127). Reducers stay ordinary: each new `state.board` is diffed into the document (`board.LOCAL`, once per flush) and other users' changes arrive as `board.REMOTE` (applied only while there's no unsynced local edit; both hidden in DevTools as built-in). main.js: `run(Board, { SHARED: makeYjsDriver({ doc, provider }) })` with the app's own `Y.Doc` and provider (y-websocket, Hocuspocus, y-webrtc, y-indexeddb); the first value seeds an empty document after the provider's first sync (`seed: 'whenSynced'`, default; `'ifEmpty'` for a local-only doc), else the document wins. Arrays of objects with `id` are id-keyed collections (concurrent add/edit/move/remove merge); declare `'text'` fields for character-level merging and declare empty arrays (`'list'` / `collection()`; else SYG261). Undo per user: `UNDO: { SHARED: { undo: 'board' } }`, `REDO: { SHARED: { redo: 'board' } }`, never `undo({ key: 'board' })` (SYG260: it reverts others' changes); presence `FOCUS: { SHARED: (state, id) => ({ presence: { editing: id } }) }` → `state.peers`; offline `{ SHARED: { connect: false } }`. Tests: `const net = createNetwork(), peer = net.join()` (from `'sygnal/yjs/testing'`), `renderComponent(Board, { drivers: { SHARED: makeYjsDriver({ doc: peer.doc, provider: peer }) } })`, `peer.offline()` / `peer.online()`. Guide: `node_modules/sygnal/dist/guide/collaborative-state.md`
|
package/llms/graphics.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Sygnal: Islands, three.js scenes (`sygnal/three`), 2D canvas editors (`sygnal/canvas`)
|
|
2
|
+
|
|
3
|
+
> A topic file of Sygnal's agent context. It assumes the core (`llms.txt`, or the sygnal-dev skill's `SKILL.md`: mental model, component anatomy, canonical forms, wiring rules, testing): read that first. Canonical forms only.
|
|
4
|
+
|
|
5
|
+
## In short
|
|
6
|
+
- **Imperative island** (three.js scene, canvas, map with markers as JSX children): `defineWidget({ children: true, mount, sync })`; the children are elements the widget reads in `sync` and draws; it dispatches DOM events on them (raycast / hit test → `click` on `v.elm`), so each child component's intent uses `DOM.click('.cube')`. Per-frame motion is computed in the widget from props, not in state (`islands.md`). A three.js scene: `<Scene>` from `sygnal/three` (`npm install three`): meshes, geometries, materials, lights and cameras as lowercase tags, raycast hits as `click` / `pointerover` on the mesh's element with `event.point`, `animate={{ rotation: (t) => [0, t, 0] }}` for motion, `<Model src="/tree.glb" />` / `<Texture src="/wood.jpg" />` for loaded assets (with `<Suspense>`; a failed load renders `onError`), `capture="xz"` to drag a mesh along the floor (`pointermove` with `event.captured` and `event.delta`), `draggable` for `processDrag`, tests with `sceneOf(el).click(meshEl)` / `.drag(meshEl, [x, y])` under `dom: 'real'` (`renderers/three.md`).
|
|
7
|
+
|
|
8
|
+
## Details
|
|
9
|
+
- Islands (a three.js scene, a canvas, a map: parts as JSX children): `defineWidget({ children: true, mount, sync(instance, childrenVnode, el), focus(instance, child) })`. The tag's children render (by Sygnal, as elements: components, `<Collection>`, intents work) into a visually hidden, accessible container; `sync` reads them (`v.sel`, `v.data.props` / `v.data.attrs`, `v.elm`) and draws them; the widget turns raycasts / hit tests into DOM events on `v.elm` (`el.dispatchEvent(new MouseEvent('click', { bubbles: true }))`), so `DOM.click('.cube')` works in the child's intent. Enter / Space on a focused child (`role`, a name, `tabIndex={0}`) is its click. `tags: ['marker']` stops the pragma treating the widget's tags as SVG. Per-frame values: compute them in the widget's own loop from props, never through state; a `timers` `{ frame: 'ACTION' }` only for state that changes every frame. Guide: `islands.md`. three.js ready-made: `import { Scene } from 'sygnal/three'` (`npm install three`, an optional peer): `<Scene className="viewport" width={640} height={400}><ambientLight /><Collection of={Cube} from="cubes" /></Scene>`, every three.js class lowercased as a tag (`<mesh position={[x, 0, 0]}><boxGeometry args={[1, 1, 1]} /><meshStandardMaterial color="orange" /></mesh>`), raycasts become DOM events with `event.point` (`DOM.click('.cube')`), per-frame motion `animate={{ rotation: (t) => [0, t, 0] }}` (never state), tests in jsdom `sceneOf(el).click(meshEl)`. Loaded assets are tags: `<Model src="/tree.glb" position={[0, 0, 0]} />` (GLTF by default; `loader={OBJLoader}` or a configured loader instance for others; one clone per element) and `<Texture src="/wood.jpg" />` inside a material (attaches as `map`; `attach="normalMap"`), with `<Suspense fallback={…}>` around them (inside the Scene: a 3D fallback; around it: the scene is created once loaded); cached per URL, freed with the last element using it, `Model.preload(src)`; a failed load renders the component's `onError`. Dragging: `capture="xz"` on a mesh captures the pointer from the press to the release (`gotpointercapture`, then every `pointermove` with `event.captured`, `event.point` on the drag plane and `event.delta` from the press, then `lostpointercapture`; a drag clicks nothing; camera controls sit it out); `draggable` sends the HTML drag events, so `processDrag({ draggable: DOM.select('.box'), dropZone: DOM.select('.bin') })` works; tests `sceneOf(el).drag(meshEl, [x, y])`; guide `renderers/three.md`. 2D editors, whiteboards, diagrams (experimental): `import { Canvas, pointer } from 'sygnal/canvas'`; shapes are data in state, one box-based vocabulary (`{ id, type: 'rect' | 'ellipse' | 'text' | 'frame' | 'image' | 'line' | 'path', x, y, width, height, rotation, fill, stroke, radius, parent }`, an ellipse is its box, not SVG's cx/cy/rx/ry): `<Canvas className="board" camera={state.camera}><shapes className="doc" items={state.shapes} active={draggedIds} />{handles(selectionBox(state.shapes, state.selection), state.camera)}</Canvas>`; one element paints, culls and hit-tests all shapes, a hit is a DOM event with `e.hit` = the id: `DOWN: DOM.select('.board').events('pointerdown').map(pointer)` (plain `{ x, y, hit, part, shift, mod }`, world coordinates); every press is captured (`e.captured`, `e.delta` from the press); pan/zoom built in, the camera a controlled value (`CAMERA: DOM.select('.board').events('camerachange').detail()`); drags go through actions (`gesture.start/move/delta`, `snap`, `resize`, `rotation`, `turn`; `history('shapes')`: `commit(s, next, gesture.before)` = one undo step per gesture, `UNDO: h.undo`; with `sygnal/yjs` `shared()` the same entries become per-user undo). Patch only changed shapes (`shapes.map(s => s.id === id ? { ...s, x } : s)`; rebuilding all is SYG262: every frame repaints; per move, work on the changed shapes only: `positionsOf(shapes, prev)` is an id → index map shared across a drag's arrays, a derived list returns its previous array when its rows didn't change); `follow={{ ids, from }}` only for huge selections; `exportSVG(shapes, ids)`; tests `canvasOf(el).pointer('down', x, y)`; guide `renderers/canvas.md`. Text wraps to its `width` (words, `\n`; `wrap: 'char' | 'none'`, `maxLines` ends in '…', `align`, `lineHeight`; no width: one line, box measured; `layoutText(text, { width, fontSize })` → `{ lines, width, height }`). Images: `{ type: 'image', src: '/a.png', x, y, width, height, fit: 'contain', alt }` (crop `sx sy sw sh`, `flipX`, `smoothing: false` for pixel art), loaded once per URL, painted when loaded (`imageload` / `imageerror` events on the canvas); `<Image src x y />` inside `<Canvas>` makes `<Suspense>` wait (a failed load: `onError`); sprite sheets `...frameOf({ src, frameWidth, frameHeight, columns }, frameAt([1, 2], s.tick, 8))`.
|
|
10
|
+
- 2D games on `sygnal/canvas` (`examples/platformer`): the world is state, plain functions over plain data; a fixed-step frame timer steps it with the sampled keys: `Game.timers = (s) => ({ loop: s.mode == 'playing' && { frame: 'STEP', step: 1000 / 60, keys: { left: ['ArrowLeft', 'KeyA'], right: 'ArrowRight', jump: 'Space' } } })`, `STEP: (s, { held, pressed }) => step(s, held, pressed)` (`pressed`: since the last frame, so a tap shorter than a frame counts; keys default-prevented, released on window blur, ignored in text fields); menu keys (Enter, P) through `keymap` (they work while the loop is off). `<Canvas navigate={false} a11y={0}>` (Space is the jump; no projection), `<shapes items={hills} parallax={0.5} />` (scrolls at half the camera, own coordinates), cached layers for tiles / scenery (keep node identity), `<shapes items={actors} live />` for what moves every frame (no ids); layers paint in their order. The camera is state: `camera: follow(s.camera, player, { width, height, deadzone: { width: 120, height }, offset: { x: -60 }, lerp: 0.2, bounds, round: true })` in the step (pure; the same object while still); a scrolling camera moves the cached layers' canvases (CSS transform), painting only what comes into view. The canvas paints in the frame the step ran. Never a variable `dt` for physics. Tests: fake timers, real `KeyboardEvent`s on `document`, `await t.frames(60)`; `settle()` ignores the loop; a `startRecording()` replays exactly (each STEP carries its keys). Guide: `renderers/canvas.md` (Games), `guide/timers.md`.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Sygnal: Other renderer targets: terminal, LLM agent, React Native, primitives, Web Worker
|
|
2
|
+
|
|
3
|
+
> A topic file of Sygnal's agent context. It assumes the core (`llms.txt`, or the sygnal-dev skill's `SKILL.md`: mental model, component anatomy, canonical forms, wiring rules, testing): read that first. Canonical forms only.
|
|
4
|
+
|
|
5
|
+
- Other renderer targets, the same components (intents must select controls to be portable; every target makes the DOM's events): a terminal `runTerminal(App, { styles })` / tests `createTerminal()` + `t.press('Tab')` + `t.frame()` (`sygnal/terminal`), an LLM agent `const { ui } = runAgentUI(App); serveMcp(ui, { tools: agentTools(app) })` (accessibility snapshot + `ui_click` / `ui_type` / … tools, `sygnal/agent-ui`), React Native `<SygnalView app={App} />` (`sygnal/react-native`), one file for web + native + terminal with the primitives `View` / `Text` / `Pressable` / `TextInput` / `Switch` / `Image` / `ScrollView` / `List` (`sygnal/primitives`, RN style objects, `controls({ Add: Pressable })`), your own on `sygnal/host` https://sygnal.js.org/renderers/overview/
|
|
6
|
+
- Terminal apps (`sygnal/terminal`): `runTerminal(App, { styles, drivers, mouse: true })`; the views stay HTML and the terminal stylesheet is `[selector, style]` rules. A full-screen layout is boxes in that sheet: `['main', { display: 'flex', flexDirection: 'column', height: '100%' }]`, `['.body', { flex: 1, display: 'flex' }]` (a flex row whose items have `width: '40%'` / `flex: 1` / `border` lays them out side by side), `['.list', { width: '40%', border: 'round', overflow: 'auto' }]` (scrolls with a scrollbar, keeps the focus in view; the title is its `aria-label`), plus `padding`, `gap`, `color`, `background`, `bold`/`dim`/`inverse`, and `:focus` / `:focus-within` / `:checked` selectors. Native `<select>`, `<dialog>` (`ELEMENT: { showModal: Confirm }`: the focus moves in and stays in, Escape closes it, `DOM.close(Confirm)` hears it), `[popover]`, `<details>`, `<textarea>` and radio groups behave as in a browser; the footer shows what the keys do on the focused element. A list is one Tab stop (roving `tabIndex`, ↑ / ↓ inside it, prevent their default so the focus doesn't move on), so Tab reaches the next pane. Quit from the model with `QUIT: { TERMINAL: () => 'exit' }` (also `{ title }`, `'bell'`). A Collection item's element is the item's: a row click is the item's intent + `PARENT`, focus it from the parent with `focusWithin(...)`. Every `sygnal/ui` native part works in a terminal (dialog, popover, tooltip, tabs, accordion, disclosure, `<Toaster />` with `drivers: { TIMER: makeTimerDriver() }`); a terminal box's title is its `aria-label`, so a `dialogAttrs` dialog keeps `aria-label="Confirm"`. Tests: `const t = createTerminal({ styles, width: 80, height: 24 }); const app = t.run(App, { HTTP })` (adds DOM and TERMINAL), `await t.settle()` (quiet: flushed, element commands, short delays; drives fake timers), then `t.press('Enter')`, `t.type()`, `t.paste()`, `t.click(line, col)`, `t.wheel(line, col, rows)`, `t.keyHint()`, `t.commands`, and compare `t.frame()` (plain text) https://sygnal.js.org/renderers/terminal/
|
|
7
|
+
- A whole app in a Web Worker (experimental, the app unchanged: worker `const target = createWorkerTarget(self); run(App, { DOM: target.driver, STORE: target.remote('STORE', ['get']) })` from `sygnal/worker`, page `startBridge(new Worker(url, { type: 'module' }), root, { drivers: { STORE } })` from `sygnal/worker/page`; default actions are decided on the page from declarations, so use `events('keydown', { preventDefault: { key: 'Tab' } })` (true or a pattern, not a function); not yet: `sygnal/ui` dialogs/popovers, widgets, `fromReact`/`fromZag`, `ELEMENT` commands, layout reads, drag and drop, devtools, HMR) https://sygnal.js.org/renderers/off-main-thread/
|
package/llms/replay.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Sygnal: Record and replay (`sygnal/replay`, dev preview)
|
|
2
|
+
|
|
3
|
+
> A topic file of Sygnal's agent context. It assumes the core (`llms.txt`, or the sygnal-dev skill's `SKILL.md`: mental model, component anatomy, canonical forms, wiring rules, testing): read that first. Canonical forms only.
|
|
4
|
+
|
|
5
|
+
- Record and replay (dev preview, `sygnal/replay`): `const rec = startRecording()` before `run(App)` records the session's input actions (redaction of typed text and server data on by default; `{ redact: { keep: ['SET_FILTER'] } }` / `false`); `rec.stop()` gives JSON. In a test, `const r = await replay(App, log)` (also from `'sygnal'`) replays it in renderComponent: `r.divergence` (first action whose state differs, with `diff`), `r.view` (view-only change), `r.effects`, `r.unhandled`, `r.warnings`, `r.final`, `r.t.dispose()`. `replayTest(log)` writes the test file, `bugReport(log, r)` Markdown; DevTools' Copy as test has `{ mode: 'replay' }`. Guide: https://sygnal.js.org/guide/record-replay/
|
package/llms/sandbox.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Sygnal: Sandboxed components (`sygnal/sandbox`, dev preview)
|
|
2
|
+
|
|
3
|
+
> A topic file of Sygnal's agent context. It assumes the core (`llms.txt`, or the sygnal-dev skill's `SKILL.md`: mental model, component anatomy, canonical forms, wiring rules, testing): read that first. Canonical forms only.
|
|
4
|
+
|
|
5
|
+
- Untrusted or model-written components (dev preview): `sygnal/sandbox` (`npm install quickjs-emscripten-core @jitl/quickjs-wasmfile-release-sync`) runs one in QuickJS-in-WASM, rendered into the real DOM through an allowlist: `import { Sandboxed } from 'sygnal/sandbox'` + `import kernel from 'sygnal/sandbox/kernel?raw'` (sygnal/vite builds it from the installed packages; else `buildSandboxKernel()` from `sygnal/sandbox/build`), `<Sandboxed className="card" kernel={kernel} source={cjs} emits={{ ANSWER: AnswerSchema }} input={{ bill: state.bill }} />` (`source`: CommonJS, JSX compiled to `__h`/`__frag`; `emits` required: only declared types with schema-valid data arrive, SYG692 otherwise) + `ANSWER: DOM.select('.card').events('emit').detail().filter(e => e.type === 'ANSWER').map(e => e.data)`, `'sandboxerror'` (`{ reason, message }`); `isolation: 'worker'` (default: hostile code, a watchdog) or `'thread'` (model-written code, sync events); the guest has `DOM`, `STATE`, `EVENTS`, `PROPS`, and `HTTP` only via `capabilities.fetch: { allow: [urlPrefix] }`; guide `advanced/sandboxed-components`. Model-written parts in a composed page (experimental, outside semver): `const gen = compose.generated(generatedRuntime({ kernel, compile: sucraseCompiler(() => import('sucrase')) }), { data: { orders: { description, shape, from: (s) => s.orders } }, emits: { OPEN_CUSTOMER: { description, schema: z.object({ id: z.number() }) } } })` (`generatedRuntime`, `sucraseCompiler` from `sygnal/sandbox`; sucrase is the app's dependency), then `parts: { custom: gen.part }` (opt in per catalog; `compose.llm()` only), `compose.llm({ instructions: gen.instructions })`, `uses = { fix: gen.repair() }`, `OPEN_CUSTOMER: gen.events(CHILD, 'OPEN_CUSTOMER')` (`{ node, ...data }`, schema-validated); defaults worker, `maxUses: 3`, `maxCode: 4000`, `maxAttempts: 2`, check before mount on, no capabilities; `gen.card('TYPE')` is a `chat({ ask: { catalog, direct: true } })` card answered by that event; guide `ai-compose.md` "Generated parts".
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Sygnal: Typed API calls (`sygnal/api`, dev preview)
|
|
2
|
+
|
|
3
|
+
> A topic file of Sygnal's agent context. It assumes the core (`llms.txt`, or the sygnal-dev skill's `SKILL.md`: mental model, component anatomy, canonical forms, wiring rules, testing): read that first. Canonical forms only.
|
|
4
|
+
|
|
5
|
+
- **Typed API calls** (dev preview, `sygnal/api`): requests built from the server's contract, no URLs or reply types written: `const api = orpcClient(contract, { prefix: '/api' })` (an oRPC contract, served by oRPC's OpenAPIHandler; or `openApiClient<paths>()`, `honoClient<ExtractSchema<typeof app>>()`), then `api.quotes.list({ tag })` is a request (a resource), `api.quotes.add(input, { ok: 'ADDED', error: 'FAILED', invalidates: [api.quotes.list] })` a sink value, a stream endpoint a `connections` entry; actions typed `Output<typeof api.quotes.add>` / `ApiFailure<typeof api.quotes.add>`; tests `t.respond('HTTP', fixture(api.quotes.list, rows), api.quotes.list)`. Function-first (`sygnal({ api: { functions: true, handler: 'server/api.ts' } })`): `src/quotes.server.ts` exports `query(z.object({ tag: z.string() }), async ({ tag }) => …)` / `mutation` / `stream` (input always validated), which components import and call like endpoints (the client build gets stubs only); the server mounts `serverFunctions(import.meta.glob('/src/**/*.server.ts', { eager: true }), { os })` with oRPC. Guide: `node_modules/sygnal/dist/guide/typed-apis.md`
|
package/llms/ui.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Sygnal: UI parts (`sygnal/ui`), Menu / Select / Combobox, sortable lists, long lists, browser sources, library recipes
|
|
2
|
+
|
|
3
|
+
> A topic file of Sygnal's agent context. It assumes the core (`llms.txt`, or the sygnal-dev skill's `SKILL.md`: mental model, component anatomy, canonical forms, wiring rules, testing): read that first. Canonical forms only.
|
|
4
|
+
|
|
5
|
+
- Browser sources: `Card.browser = (state) => ({ seen: !state.seen && { intersection: '.cover', action: 'SEEN' }, dark: { media: '(prefers-color-scheme: dark)', action: 'DARK' } })` + `run(App, { BROWSER: makeBrowserDriver() })` (also `resize`, `storage`, `visibility`, `online`, `geolocation`; clipboard: `{ BROWSER: (s) => ({ copy: s.url, ok: 'COPIED', error: 'COPY_FAILED' }) }`; tests need no driver: `t.browser.intersect('.cover', true)`, `t.browser.media(query, true)`); `lazy(() => import('./Chart'), { when: 'visible' })` (or `'idle'`) defers the import. Guide: `node_modules/sygnal/dist/guide/browser-sources.md`
|
|
6
|
+
- Long lists (from ~1,000 rows): `<VirtualCollection of={Row} from="rows" className="rows" estimateSize={32} aria-label="Rows" />` (Collection's props; it is itself the scroll container, a `div` with `role="list"`: give `.rows` a bounded height in CSS and put `className`, `role`, `aria-label` on it, never wrap it in your own scrolling `<ul>`; SYG430 means fix that height; `estimateSize` is the rows' real height in px (too low: blank space at the end of a fast scroll, SYG436); only rows in view have components, so keep row state in the array); jump with `ELEMENT: (s, index) => ({ scrollToIndex: '.rows', index })` or `{ scrollToId: '.rows', id }`. Guide: `node_modules/sygnal/dist/guide/virtual-collections.md`
|
|
7
|
+
- Reorderable list: `List.uses = { sort: sortable({ from: 'tasks', item: '.task', handle: '.grip' }) }` (mouse, touch, keyboard); items `<li className="task" data-id={state.id}><button className="grip" aria-label=… aria-describedby={context.sort.helpId}>` (context `sort: (s) => s.sort`; style from `context.sort.dragging` / `over` / `after`), host `<p id={state.sort.helpId} hidden>` instructions and `<p role="status" aria-live="assertive">{state.sort.message}</p>` (English defaults: "Dropped X at position 2 of 5.", "Reorder cancelled. X is back at position…"; a spec's exact wording: `messages: { lift, move, drop, cancel, stay }`, each `(label, position, count) => text`), save in `'sort.DROPPED': (state, { id, index, fromIndex }) => …` (the list is already reordered); two lists `from: ['todo', 'done']` + `data-list="todo"` on each container. HTML5 / file drops: `makeDragDriver`. Guide: `node_modules/sygnal/dist/guide/drag-and-drop.md`
|
|
8
|
+
- UI parts (`import { dialog, dialogAttrs, popover, popoverAttrs, tooltip, tooltips, tabs, tabsAttrs, accordion, accordionAttrs, disclosure, disclosureAttrs, Toaster } from 'sygnal/ui'`; native HTML, unstyled): behaviors over your own markup in `uses` (`Profile.uses = { profile: dialog({ dialog: '.profile', trigger: '.edit', close: '.cancel' }) }` → `state.profile.open`, `'profile.CLOSED'`; spread `dialogAttrs(state.profile, uid).trigger` / `.dialog` / `.title` / `.description` (ARIA, ids; `role="alertdialog"` on the dialog for an alert); for tabs/accordion/disclosure spread `tabsAttrs(state.tabs, uid).tab('general')` etc. for roles, ids and ARIA), every slice is the source of truth (Radix-style controlled): a reducer may write `{ ...state.faq, expanded: [id] }` or `{ ...state.profile, open: true }` and the element follows; start values through options (`dialog({ …, open: true })`), never the same key in `initialState`; refuse a close with a host entry returning `previousState` (`'profile.CANCEL': (state, e, next, { previousState }) => (state.dirty ? previousState : state)`; also `'profile.CLOSE'` for the close button / `method="dialog"` form); pass `open` (even `open: false`) to make a dialog / popover controlled: the browser never dismisses it, the behavior does (Escape, a press or focus outside → `'profile.CANCEL'` / `'filters.DISMISS'`, one per interaction, top layer first; an open Menu / Select / Combobox inside is the top layer), so a refused dismiss never flickers; a popover's own button: `popover({ popover: '.share', trigger: '.share-button' })` + spread `popoverAttrs(state.share, uid).trigger` / `.popover` (ARIA, ids; clicks → `'share.TOGGLE'`); add another opener by listing it in the option (`trigger: '.edit, .avatar'`), never with an intent action named `'profile.OPEN'` (it replaces the trigger, SYG118); `tooltips({ trigger: '.term', tip: '.term-tip' })` serves many triggers with one slice (each trigger's `aria-describedby` names its tip), and `<Toaster />` once + `EVENTS: event('TOAST', { text, kind, timeoutMs })` from anywhere (needs `makeTimerDriver()`). Guides: `node_modules/sygnal/dist/guide/ui/overview.md` (and `ui/dialog.md`, `popover`, `tooltip`, `tabs`, `accordion`, `disclosure`, `toaster`)
|
|
9
|
+
- Menu / Select / Combobox: `import { Select } from 'sygnal/ui/select'` (`sygnal/ui/menu`, `sygnal/ui/combobox`; each needs `@zag-js/vanilla` + its machine, e.g. `@zag-js/select@~1.45.0`): widget tags, `<Select className="size" label="Size" items={['S', 'M']} value={state.size} />` + `SIZE: DOM.select('.size').events('value-change').detail()` (Menu: `'select'`; Combobox also `'input-change'`), commands `{ open: '.size' }`; never `events('change')` (a native select's habit: no detail; SYG141); with `name` in a form they are form fields (the `form` behavior reads value and blur: `value={state.form.values.size}`); in a `<Portal>` listen through `DOM.select('document').select('.size')`. Groups: `items={[{ group: 'Fruit', items: ['Apple'] }]}`. Combobox: `multiple` renders chips (a grid, one tab stop: Left at the input's start, Left / Right / Home / End, Delete / Backspace on a chip, Backspace in the empty input or `{ remove: '.tags', value }` remove one; removals announced, `removedMessage={(l) => …}`), `empty="No results"`, `allowCustomValue` commits the typed text on blur / Enter as the value (`loading` while async items load, so the value isn't shown as raw text first). Menu items: `{ type: 'checkbox' | 'radio', value, label, checked }` (event `checked-change` `{ value, checked, type }`; keep `checked` in state), `{ href, value, label }`, a submenu `{ value, label, items }` (ArrowRight / ArrowLeft). Lists are `position: fixed` by default (`positioning={{ strategy: 'absolute' }}` inside a transformed ancestor). Escape hatch for one React/Preact component or a Zag machine: `fromReact(Comp, { events: { rate: 'onChange' } })` (`sygnal/react`) / `fromZag(menu, (api, props) => …, { events })` (`sygnal/zag`, `import * as menu from '@zag-js/menu'`) return widget tags; test them with `dom: 'real'`. Guides: `node_modules/sygnal/dist/guide/ui/menu.md` (`ui/select.md`, `ui/combobox.md`), `node_modules/sygnal/dist/guide/adapters.md`
|
|
10
|
+
- Third-party libraries (tested recipes): charts (Chart.js, ECharts), rich text (Tiptap), code editor (CodeMirror), carousel (Embla), data grid (AG Grid) are `defineWidget` tags; TanStack Table v9 runs in the view as a pure function of state (`constructTable` per render, settings in state); icons: `lucide` data → SVG vnodes with a helper called in the view (`{icon(Plus)}`, not a component); i18n: i18next `getFixedT(locale)` once per locale in `App.context = { t }`, the locale in state saved with `persist()` (never `changeLanguage()`). Recipes: `node_modules/sygnal/dist/guide/recipes/overview.md` (and `recipes/charts.md`, `code-editor`, `rich-text`, `carousel`, `data-table`, `data-grid`, `icons`, `i18n`)
|