@panphora/sapjs 0.2.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/LICENSE +21 -0
- package/README.md +350 -0
- package/dist/sap.js +2490 -0
- package/dist/sap.min.js +6 -0
- package/package.json +52 -0
- package/src/actions.js +310 -0
- package/src/carrier.js +128 -0
- package/src/compile.js +91 -0
- package/src/control-serialize.js +85 -0
- package/src/debug.js +0 -0
- package/src/dom.js +182 -0
- package/src/errors.js +190 -0
- package/src/helpers.js +119 -0
- package/src/lint.js +229 -0
- package/src/mount.js +84 -0
- package/src/mutation-bridge.js +195 -0
- package/src/pass.js +472 -0
- package/src/platform.js +78 -0
- package/src/sap.js +178 -0
- package/src/scheduler.js +65 -0
- package/src/scope.js +265 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 panphora
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
# sap
|
|
2
|
+
|
|
3
|
+
**Reactive HTML in attributes. No build step, no virtual DOM, no state object.**
|
|
4
|
+
|
|
5
|
+
Sap is a tiny reactive layer for hand-written HTML files. You declare state, formulas, and paints as plain attributes; Sap rebuilds everything from the live DOM on every change. The DOM is the only store, so the file you save *is* the app: open it in any browser, it runs.
|
|
6
|
+
|
|
7
|
+
```html
|
|
8
|
+
<script src="https://cdn.jsdelivr.net/npm/sapjs/dist/sap.min.js"></script>
|
|
9
|
+
|
|
10
|
+
<main sap>
|
|
11
|
+
<input type="number" bind="qty" value="3">
|
|
12
|
+
<input type="number" bind="price" value="10">
|
|
13
|
+
<output calc:total="state.qty * state.price" text:usd="state.total"></output>
|
|
14
|
+
</main>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Type in either box and the total repaints. That is the whole program. There is no JavaScript to write.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Why
|
|
22
|
+
|
|
23
|
+
- **The DOM is the state.** No store to sync, no hydration mismatch. Edit a field by hand in devtools, paste from the console, receive a live-sync morph: the next pass reads it and repaints. Same code path as a keystroke.
|
|
24
|
+
- **The saved file is correct before JS runs.** Every paint targets a real attribute (`hidden`, `value`, `textContent`, `disabled`), so view-source is legible and pre-JS renders right.
|
|
25
|
+
- **One write path.** Every change is a property write plus a synthetic `input`/`change` event. Persistence, undo, and recompute all hang off the same event, so they never diverge.
|
|
26
|
+
- **Failures are loud and attributed.** A typo'd field name, a foreign-dialect attribute, a calc cycle: each prints a stable error code, the element's CSS path, the authored source, and a copy-pasteable fix. Agents can author Sap files and read back exactly what is wrong.
|
|
27
|
+
- **It runs anywhere.** Standalone in any `.html` file, or wired into [Hyperclay](https://hyperclay.com) for autosave, versioning, and real-time collaboration with zero extra code.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## The mental model
|
|
32
|
+
|
|
33
|
+
Three ideas, mapped to attributes:
|
|
34
|
+
|
|
35
|
+
| | What | Attributes |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| **structure** | declared state | `state=`, `bind`, `items`, `scope` |
|
|
38
|
+
| **compute** | derived values | `calc:` |
|
|
39
|
+
| **paint** | output + side effects | `text`, `show`, `attr:`, `class:`, `css:`, `effect=` |
|
|
40
|
+
|
|
41
|
+
Reads are expressions over `state` (the nearest scope) and `item` (the nearest row). Writes go through `Sap(el)` in your `onclick` handlers, or the action attributes (`set:`, `trigger-*`, `move:`, `sort:`).
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
Drop-in (auto-mounts every `[sap]` on load):
|
|
48
|
+
|
|
49
|
+
```html
|
|
50
|
+
<script src="https://cdn.jsdelivr.net/npm/sapjs/dist/sap.min.js"></script>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or as a module:
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
import Sap from "sapjs";
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npm install sapjs
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Importing the module also auto-mounts every `[sap]` on `DOMContentLoaded`. Call `Sap.mount(rootOrSelector)` to mount a root added later, or `Sap.mount()` to rescan; `Sap.config({ formats })` registers custom formats as a config-object alternative to `Sap.formats.x`.
|
|
64
|
+
|
|
65
|
+
## Internal explainer
|
|
66
|
+
|
|
67
|
+
For the source-grounded walkthrough with live demos, open `docs/sapjs-explained.html` directly in a browser. It is a self-contained file with the real `dist/sap.js` engine inlined, so it works from `file://` with no network or build step.
|
|
68
|
+
|
|
69
|
+
Edit `docs/sapjs-explained.template.html`, then run:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npm run build:docs
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
That regenerates `docs/sapjs-explained.html` and refreshes the inlined engine from `dist/sap.js`.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Quickstart
|
|
80
|
+
|
|
81
|
+
A complete todo app, with no JavaScript at all. Copy it into a `.html` file and open it.
|
|
82
|
+
|
|
83
|
+
```html
|
|
84
|
+
<script src="https://cdn.jsdelivr.net/npm/sapjs/dist/sap.min.js"></script>
|
|
85
|
+
|
|
86
|
+
<main sap>
|
|
87
|
+
<form trigger-add="todos">
|
|
88
|
+
<input bind="title" placeholder="What needs doing?" required autofocus>
|
|
89
|
+
</form>
|
|
90
|
+
|
|
91
|
+
<ul items="todos">
|
|
92
|
+
<li item template>
|
|
93
|
+
<input type="checkbox" bind="done">
|
|
94
|
+
<span bind="title" contenteditable="plaintext-only"></span>
|
|
95
|
+
<button trigger-remove>✕</button>
|
|
96
|
+
</li>
|
|
97
|
+
</ul>
|
|
98
|
+
|
|
99
|
+
<output text="plural(count(state.todos, t => !t.done), 'task', 'tasks') + ' left'"></output>
|
|
100
|
+
</main>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The form's input is named `title`, the same as the row's field. On Enter, `trigger-add` clones the template, copies the form's matching fields into the new row, and clears the box. `required` lets the browser block empty submits natively. `count(...)` sees every row live. Nothing is stored anywhere but the DOM.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## How it works
|
|
108
|
+
|
|
109
|
+
**Every change triggers one pass.** A pass throws away all in-memory state and rebuilds it by reading the DOM top to bottom: every `state=`, `bind`, and `items=` is read into a fresh object, `calc:` fields compute, then paints write only what changed. The objects are then discarded. There is no retained JS state to drift from the DOM, which is why a hand-edit in devtools, a console paste, or a live-sync morph all just work: each is simply the next pass's input.
|
|
110
|
+
|
|
111
|
+
**Passes are batched.** A burst of writes coalesces into one pass on the next microtask, so reading painted output on the line right after a write sees the old DOM. Call `Sap.refresh()` for an immediate synchronous pass when you must read painted output now.
|
|
112
|
+
|
|
113
|
+
**One write path, three steps.** A write (1) sets the control's live value, (2) mirrors it into a serializable attribute so view-source and save reflect it, then (3) fires synthetic `input` + `change` events. Because a programmatic write looks exactly like typing, autosave, undo, and recompute all observe it the same way. Two consequences follow: paints write silently (no event, to avoid feedback loops), and undo/redo replay needs the Hyperclay bridge, since replaying an attribute fires no event.
|
|
114
|
+
|
|
115
|
+
**Scopes and rows.** `sap`, `scope=`, `items=`/`item`, and `detail=` are the boundaries that form the state tree. A field belongs to its nearest enclosing scope, and to the **row** object when it sits inside an `[item]`, so the same `bind="title"` lands in a different owner depending on nesting. `scope="cart"` reads as `state.cart.field`; `root` always points at the app scope.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Vocabulary
|
|
120
|
+
|
|
121
|
+
### Declaring state
|
|
122
|
+
|
|
123
|
+
```html
|
|
124
|
+
<main sap state="filter=all step:num=1 done:bool">
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`state=` declares attribute-carried fields on a scope. Bare = string, `:num` = number, `:bool` = boolean, `name=value` = default. `bind` declares a field carried by a control. `items` declares a field that is an array of rows. `scope="name"` nests a child state object readable as `state.name`.
|
|
128
|
+
|
|
129
|
+
### `bind` — two-way, by control type
|
|
130
|
+
|
|
131
|
+
The control is the contract. No modifiers, ever.
|
|
132
|
+
|
|
133
|
+
| Control | Value |
|
|
134
|
+
|---|---|
|
|
135
|
+
| `input[type=text/email/url]`, `textarea` | string |
|
|
136
|
+
| `input[type=number/range]` | number |
|
|
137
|
+
| `input[type=checkbox]` | boolean |
|
|
138
|
+
| `input[type=radio]` (group by `name`) | the checked value |
|
|
139
|
+
| `input[type=date/time]` | ISO string |
|
|
140
|
+
| `select` | selected value |
|
|
141
|
+
| `select[multiple]` | array of values |
|
|
142
|
+
| `[contenteditable]`, text leaf | textContent |
|
|
143
|
+
|
|
144
|
+
Binding a `type=file` is always a mount error (`E32`); files never serialize into an HTML file. Binding a `type=password` halts unless you add `transient` (`E31`). `transient` (the bare attribute, or a `state="field:transient"` suffix) keeps a value in the live DOM only: Sap drives the app from it but strips it from the saved file, so passwords and search boxes never persist.
|
|
145
|
+
|
|
146
|
+
Note: `<select>` and `<textarea>` now mirror their live value to attributes the way checkbox/radio/number/text do — a `<select>` writes `selected` on the chosen option, a `<textarea>` writes a cursor-safe `data-value` that finalizes to `textContent` on save — so a programmatic change survives a save. `[contenteditable]`/text-leaf bindings carry no attribute: they persist because their `textContent` is saved with the DOM as-is.
|
|
147
|
+
|
|
148
|
+
#### `step` on a bound text input
|
|
149
|
+
|
|
150
|
+
A `step` attribute on a bound text-kind input (`type=text/tel/search`) makes ArrowUp/ArrowDown adjust the value, the way a native `type=number` already does, but without the spinner and with centered styling intact. ArrowUp adds `step`, ArrowDown subtracts it, Shift multiplies the step by 10, and optional `min`/`max` attributes clamp the result. The write goes through the one write path, so calcs and paints recompute on the next pass. Opt-in only: no `step`, no stepping. Native `type=number` inputs are left alone.
|
|
151
|
+
|
|
152
|
+
### `calc:` — computed fields
|
|
153
|
+
|
|
154
|
+
```html
|
|
155
|
+
<dd calc:subtotal="sum(state.lines, 'linetotal')" text:usd2="state.subtotal"></dd>
|
|
156
|
+
<dd calc:total="state.subtotal + state.tax" text:usd2="state.total"></dd>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Place the formula beside the cell that shows it. Order is by dependency, not document position; `total` waits for `subtotal` automatically. A cycle logs `E07` naming the chain, then falls back to source-order evaluation: the values may be wrong, but the app keeps running.
|
|
160
|
+
|
|
161
|
+
### Paints
|
|
162
|
+
|
|
163
|
+
| Attribute | Paints |
|
|
164
|
+
|---|---|
|
|
165
|
+
| `text` / `text:FMT` | `textContent`, optionally formatted |
|
|
166
|
+
| `show="expr"` | toggles native `hidden` |
|
|
167
|
+
| `attr:NAME="expr"` | an attribute (native booleans like `disabled` toggle by presence) |
|
|
168
|
+
| `class:NAME="expr"` | a class on/off |
|
|
169
|
+
| `css:NAME="expr"` | a `--NAME` custom property |
|
|
170
|
+
| `effect="stmt"` | a statement run after paint (charts, `document.title`, third-party sync) |
|
|
171
|
+
| `invalid="expr"` | `setCustomValidity(msg)` for native form gating |
|
|
172
|
+
|
|
173
|
+
A throwing paint writes nothing: the DOM keeps its last-good value, the element gets a `sap-error` beacon, and the console logs one attributed error.
|
|
174
|
+
|
|
175
|
+
`effect=` is for side effects only: touch `el`, set `document.title`, call a chart library. Assigning to `state.*` inside an effect does nothing, since it mutates a per-pass snapshot that is then discarded; and writing `value=` or `checked=` on a bound control inside an effect halts the app at mount (`E30`). Write state through `onclick` + `Sap(this)` instead.
|
|
176
|
+
|
|
177
|
+
`invalid=` is the one expression that fails quiet: if it throws, the field is treated as **valid** and nothing is logged, unlike `text`/`calc`/`effect`. Guard `invalid` expressions against undefined, or a bug there silently disables the gate.
|
|
178
|
+
|
|
179
|
+
### Visibility: `show` vs `show-when`
|
|
180
|
+
|
|
181
|
+
Two ways to toggle visibility. The shape tells you which engine runs it:
|
|
182
|
+
|
|
183
|
+
| Attribute | Resolves | Without sapjs |
|
|
184
|
+
|---|---|---|
|
|
185
|
+
| `show="expr"` | a JS expression, e.g. `show="state.tab === 'overview'"` | no |
|
|
186
|
+
| `show-when:FIELD="a\|b"` | literal equality: visible when the nearest ancestor `FIELD` attribute equals `a` or `b` | yes — compiles to CSS via hyperclay's `optionVisibility` |
|
|
187
|
+
| `hide-when:FIELD="a\|b"` | the inverse: hidden when `FIELD` equals `a` or `b` (shown otherwise, including when absent) | yes |
|
|
188
|
+
| `option:FIELD="a"` | alias of `show-when:` | yes |
|
|
189
|
+
| `option-not:FIELD="a"` | visible when `FIELD` is present but not equal to `a` | yes |
|
|
190
|
+
|
|
191
|
+
`show-when:` and its family compare against a **literal** value, not an expression, so `show-when:tab="overview"` matches the bare `tab="overview"` an ancestor carries. Writing an expression there (`show-when:tab="state.tab"`) is a mistake sapjs warns about (`W04`) — reach for `show=` for anything beyond literal equality. When hyperclay's CSS floor is present, sapjs defers visibility to it, so the two never fight and the markup works with either library loaded alone.
|
|
192
|
+
|
|
193
|
+
**Edit-mode-aware UI (with hyperclay):** hyperclay stamps `editmode="true|false"` and `pageowner="true|false"` on `<html>` on load, and resets them to `false` before a save. So `show-when:editmode="true"` is the canonical way to show edit-only UI: it is visible while editing and always hidden in the saved file, with no sapjs `state=` needed (the verb reads the `<html>` attribute through its nearest-ancestor walk). Standalone, nothing stamps `editmode`, so the UI stays hidden until you set the attribute yourself.
|
|
194
|
+
|
|
195
|
+
### Actions (attributes)
|
|
196
|
+
|
|
197
|
+
| Attribute | Does |
|
|
198
|
+
|---|---|
|
|
199
|
+
| `set:field="expr"` | write a field on click |
|
|
200
|
+
| `trigger-add="list"` | add a row. On a `<form>` it fires on Enter, fills the new row from the form's matching-named fields, then clears them |
|
|
201
|
+
| `trigger-remove` | remove the nearest row |
|
|
202
|
+
| `trigger-reset` | reset the scope to its defaults |
|
|
203
|
+
| `move:up` / `move:down` | reorder a row |
|
|
204
|
+
| `move:to="list"` | move a row to another list |
|
|
205
|
+
| `sort:FIELD` | stable column sort, direction toggles statelessly |
|
|
206
|
+
| `confirm="msg"` | gate any action behind a confirm dialog: the platform's themed dialog when Hyperclay is loaded, native `window.confirm` otherwise |
|
|
207
|
+
| `detail="LIST by KEYEXPR"` | project the selected row into a panel |
|
|
208
|
+
|
|
209
|
+
### Actions (JavaScript)
|
|
210
|
+
|
|
211
|
+
`Sap(el)` returns a live, write-through proxy onto the scope or row that owns `el`. Reads come from the DOM; writes go through the one write path.
|
|
212
|
+
|
|
213
|
+
```html
|
|
214
|
+
<button onclick="['Buy milk', 'Walk dog'].forEach(t => Sap(this).$add('todos').title = t)">Add starter tasks</button>
|
|
215
|
+
<button onclick="Sap(this).inbox.filter(m => m.picked).forEach(m => m.$remove())">Delete selected</button>
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Reach for the verbs when an attribute can't express the work: bulk adds, filtered removes, transforms. For a single "add one row from a form", prefer `<form trigger-add>` (above). `$add(list)` returns the new row's proxy. Row proxies carry `$key`, `$index`, `$el`, and `$add(list)`, `$reset()`, `$remove()`, `$move(listOrPath)`.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Expressions
|
|
223
|
+
|
|
224
|
+
The compile signature is frozen forever:
|
|
225
|
+
|
|
226
|
+
```js
|
|
227
|
+
(state, item, el, root, fmt, num, sum, count, avg, min, max, plural, days)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
- `state` — the nearest scope object; `item` — the nearest row; `root` — the app scope; `el` — the host element.
|
|
231
|
+
- Helpers: `num(v)`, `sum(rows, key|fn)`, `count(rows, pred?)`, `avg`, `min`, `max`, `plural(n, one, many)`, `days(a, b)`.
|
|
232
|
+
- Row metadata: `item.$key`, `item.$index`, `item.$el`.
|
|
233
|
+
|
|
234
|
+
Expressions are **read-only**. Writing happens through `Sap(el)` or the action attributes.
|
|
235
|
+
|
|
236
|
+
`num(v)` coerces any non-number (`"12px"`, `"abc"`, blank) to `0`, never `NaN`, and `sum`/`avg`/`min`/`max` inherit that, so a typo'd field name silently sums to 0 rather than erroring. It strips `$`, `,`, `%`, and whitespace first, so pasted values like `"$1,200"` and `"12%"` parse to `1200` and `12`. (A numeric *format* on a non-finite value still throws `E22`; see Formats below.)
|
|
237
|
+
|
|
238
|
+
### Formats
|
|
239
|
+
|
|
240
|
+
`text:usd`, `usd2`, `pct`, `pct1`, `int`, `num`, `num2`, `compact`, `date`, `clock`. Register your own:
|
|
241
|
+
|
|
242
|
+
```js
|
|
243
|
+
Sap.formats.eur = (n) => "€" + n.toFixed(2);
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
A numeric format applied to `NaN`/`Infinity` throws a loud `E22` instead of silently painting "NaN".
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Collections
|
|
251
|
+
|
|
252
|
+
```html
|
|
253
|
+
<ul items="contacts">
|
|
254
|
+
<li item template set:selected="item.$key" class:active="state.selected === item.$key">
|
|
255
|
+
<span bind="name"></span>
|
|
256
|
+
</li>
|
|
257
|
+
</ul>
|
|
258
|
+
|
|
259
|
+
<form detail="contacts by state.selected" state="selected">
|
|
260
|
+
<label>Name <input bind="name"></label>
|
|
261
|
+
<label>Email <input type="email" bind="email"></label>
|
|
262
|
+
<button onclick="Sap(this).$remove()">Delete</button>
|
|
263
|
+
</form>
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Click a row to select it; the detail panel projects that row. Edits in the panel route back to the source row through the proxy. `Sap(this)` inside the panel resolves the selected row, so `$remove()` and `$move()` work. No match hides the panel. The panel also hides silently if the `by` key expression throws or the spec is malformed: no error, no beacon. If a panel never appears, check the `by` expression and that the key resolves to the selected row's `$key`.
|
|
267
|
+
|
|
268
|
+
**Filtering:** hide, never remove, so aggregates still see every row.
|
|
269
|
+
|
|
270
|
+
```html
|
|
271
|
+
<li item calc:match="state.q === '' || item.name.toLowerCase().includes(state.q)" show="item.match">
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**Nested lists** (kanban). Nesting works past one level: a row that declares `items="cards"` exposes them as `item.cards`, and `Sap(this).$add('cards')` adds a card inside that row. Cloning the row keeps its inner template.
|
|
275
|
+
|
|
276
|
+
```html
|
|
277
|
+
<main sap items="columns">
|
|
278
|
+
<section item template scope="board">
|
|
279
|
+
<h2 bind="title"></h2>
|
|
280
|
+
<ul items="cards">
|
|
281
|
+
<li item template bind="name"></li>
|
|
282
|
+
</ul>
|
|
283
|
+
<button onclick="Sap(this).$add('cards')">+ card</button>
|
|
284
|
+
</section>
|
|
285
|
+
</main>
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
The one nesting limit is an `items=` list placed inside a `detail=` panel, which is a mount error (`E17`).
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
292
|
+
## The console contract
|
|
293
|
+
|
|
294
|
+
Every app prints one machine-readable line on mount:
|
|
295
|
+
|
|
296
|
+
```
|
|
297
|
+
sap ✓ main[sap] · fields 12 · calcs 4 · paints 18 · actions 6 · lists 2 · rows 7 · warnings 0 · mount writes 0 · 1.8ms
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
`mount writes 0` on a previously-saved file is the zero-byte-mount guarantee: Sap touched nothing.
|
|
301
|
+
|
|
302
|
+
| Call | Returns |
|
|
303
|
+
|---|---|
|
|
304
|
+
| `Sap.status()` | JSON twin of the green line |
|
|
305
|
+
| `Sap.report()` | JSON list of every error `{code, el, expr, problem, fix}` |
|
|
306
|
+
| `Sap.why(el)` | how a field resolves: declaring element, carrier, value |
|
|
307
|
+
| `Sap.debug(true)` | per-pass paint diffs |
|
|
308
|
+
| `Sap.doctor()` | full-page audit (dead state, duplicate ids, drift, …) |
|
|
309
|
+
| `Sap.refresh()` | one synchronous pass (set state, refresh, read on the next line) |
|
|
310
|
+
| `Sap.batch(label, fn)` | group bulk mutations into one undo entry |
|
|
311
|
+
|
|
312
|
+
### Error codes (selection)
|
|
313
|
+
|
|
314
|
+
| Code | Meaning |
|
|
315
|
+
|---|---|
|
|
316
|
+
| `E01` | foreign-dialect attribute (`x-text`, `v-if`, `@click`) — teaches the Sap spelling |
|
|
317
|
+
| `E07` | `calc:` cycle, names the chain |
|
|
318
|
+
| `E12` | unknown state key, with a did-you-mean |
|
|
319
|
+
| `E22` | a numeric format hit `NaN`/`Infinity` |
|
|
320
|
+
| `E24` | an expression threw (preserve-on-error + beacon) |
|
|
321
|
+
| `E26` | recompute circuit breaker tripped |
|
|
322
|
+
| `E31` | `bind` on a password without `transient` |
|
|
323
|
+
|
|
324
|
+
Errors that contradict the file's structure halt the app at mount (no listeners arm, the file stays byte-frozen). Everything else degrades per element.
|
|
325
|
+
|
|
326
|
+
---
|
|
327
|
+
|
|
328
|
+
## Hyperclay integration
|
|
329
|
+
|
|
330
|
+
Sap runs standalone in any HTML file. When `window.hyperclay` is present it rides the platform for free:
|
|
331
|
+
|
|
332
|
+
- live-sync morphs trigger a synchronous re-derive (`hyperclay:livesync-applied`),
|
|
333
|
+
- a `sortable` drag reorder triggers a re-derive (`clay:sorted`),
|
|
334
|
+
- undo/redo replays heal derived paints,
|
|
335
|
+
- `Sap.batch` labels grouped edits in the undo history,
|
|
336
|
+
- a morph that replaces the `[sap]` element re-mounts itself.
|
|
337
|
+
|
|
338
|
+
No configuration. The same file works offline and online.
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## What's in v1
|
|
343
|
+
|
|
344
|
+
The full vocabulary above. Deferred to v1.1: `check:` in-file assertions, `$invalid` (covered by native `:invalid` + CSS), and a first-class drag surface.
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## License
|
|
349
|
+
|
|
350
|
+
MIT
|