@panphora/clayjs 1.13.1 → 1.15.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/THIRD-PARTY-NOTICES.md +19 -0
- package/dist/clay.standalone.js +69385 -40992
- package/dist/server/checkpoint.cjs +1 -0
- package/dist/server/checkpoint.mjs +8 -0
- package/dist/server/core.cjs +72210 -0
- package/dist/server/rooms.cjs +1 -0
- package/dist/server/rooms.mjs +66 -0
- package/docs/guide.md +127 -99
- package/entries/clay.js +22 -0
- package/entries/sap.js +5 -5
- package/package.json +16 -2
- package/packed-contract.json +18 -3
- package/src/core/boot-phase.js +6 -0
- package/src/core/conflict-download.js +12 -3
- package/src/core/host-meta.js +75 -0
- package/src/core/persist.js +11 -0
- package/src/core/save-core.js +266 -31
- package/src/core/save.js +154 -36
- package/src/core/snapshot.js +51 -13
- package/src/core/style-comparisons.js +37 -0
- package/src/dom/insert-style-tag.js +11 -12
- package/src/lib/derived.js +116 -0
- package/src/lib/effect.js +130 -0
- package/src/lib/mutation.js +3 -1
- package/src/lib/style-adoption.js +9 -0
- package/src/lib/style.js +47 -0
- package/src/live/fast-lane.js +347 -60
- package/src/live/local-lane.js +32 -6
- package/src/live/ops.js +77 -0
- package/src/live/provenance.js +129 -0
- package/src/live/save-history.js +188 -0
- package/src/live/server-lane.js +9 -2
- package/src/live/store.js +194 -16
- package/src/loader-logic.js +16 -4
- package/src/loader.js +37 -1
- package/src/page/address.js +20 -0
- package/src/page/adopt-boot.js +189 -0
- package/src/page/adopt.js +240 -0
- package/src/page/ai-tool.js +188 -0
- package/src/page/announce.js +21 -0
- package/src/page/blocks.js +1170 -0
- package/src/page/blocksel.js +269 -0
- package/src/page/board-drag.js +226 -0
- package/src/page/board-panel.js +353 -0
- package/src/page/board-style.js +146 -0
- package/src/page/board-view.js +447 -0
- package/src/page/board.js +41 -0
- package/src/page/chart.js +220 -0
- package/src/page/checklist.js +143 -0
- package/src/page/chrome.js +587 -0
- package/src/page/csv-import.js +169 -0
- package/src/page/drag.js +202 -0
- package/src/page/drop.js +112 -0
- package/src/page/editor.js +386 -0
- package/src/page/highlight.js +268 -0
- package/src/page/inline.js +60 -1
- package/src/page/items.js +159 -0
- package/src/page/link.js +247 -0
- package/src/page/markdown.js +71 -0
- package/src/page/media.js +570 -0
- package/src/page/merge.js +130 -0
- package/src/page/model.js +71 -9
- package/src/page/placeholder.js +23 -0
- package/src/page/projection.js +647 -0
- package/src/page/recover.js +193 -0
- package/src/page/rich/cell.js +16 -0
- package/src/page/rich/chart.js +104 -0
- package/src/page/rich/csv.js +118 -0
- package/src/page/rich/page-tool.js +86 -0
- package/src/page/rich/tool-runtime.js +195 -0
- package/src/page/save-projection.js +53 -0
- package/src/page/save-states.js +85 -0
- package/src/page/schema.js +12 -5
- package/src/page/table-kinds.js +20 -0
- package/src/page/table-paste.js +57 -0
- package/src/page/table-select.js +244 -0
- package/src/page/table-style.js +25 -0
- package/src/page/table.js +650 -0
- package/src/page/themes/content.css +46 -0
- package/src/page/themes/light-1.css +269 -0
- package/src/page/themes/light-2.css +211 -0
- package/src/page/themes/light-3.css +130 -0
- package/src/page/themes/light-4.css +299 -0
- package/src/page/tool-data.js +25 -0
- package/src/page/tool-view.js +68 -0
- package/src/page/tools.js +133 -0
- package/src/page/view.js +136 -0
- package/src/page/ybind/author.js +25 -0
- package/src/page/ybind/binding.js +92 -0
- package/src/page/ybind/board-commands.js +170 -0
- package/src/page/ybind/board.js +89 -0
- package/src/page/ybind/bridge.js +382 -0
- package/src/page/ybind/conversion.js +26 -0
- package/src/page/ybind/core.js +7 -0
- package/src/page/ybind/decode.js +208 -0
- package/src/page/ybind/doc.js +58 -0
- package/src/page/ybind/external.js +190 -0
- package/src/page/ybind/group.js +124 -0
- package/src/page/ybind/index.js +12 -0
- package/src/page/ybind/layout.js +27 -0
- package/src/page/ybind/marks.js +18 -0
- package/src/page/ybind/migrate.js +152 -0
- package/src/page/ybind/moves.js +288 -0
- package/src/page/ybind/origins.js +257 -0
- package/src/page/ybind/parts.js +44 -0
- package/src/page/ybind/projection-map.js +92 -0
- package/src/page/ybind/provider.js +285 -0
- package/src/page/ybind/recovery.js +411 -0
- package/src/page/ybind/seed.js +27 -0
- package/src/page/ybind/table.js +166 -0
- package/src/page/ybind/tokens.js +93 -0
- package/src/page/ybind/walk.js +333 -0
- package/src/page/ybind/writer.js +307 -0
- package/src/plugins/derive.js +12 -0
- package/src/plugins/indicator.js +16 -6
- package/src/plugins/live.js +742 -65
- package/src/plugins/page.js +547 -0
- package/src/plugins/undo.js +62 -4
- package/src/server/checkpoint/index.js +218 -0
- package/src/server/rooms/index.js +59 -0
- package/src/server/rooms/live-data-ops.js +369 -0
- package/src/server/rooms/live-data-rooms.js +965 -0
- package/src/server/rooms/live-data-tags.js +49 -0
- package/src/server/rooms/live-page-history.js +41 -0
- package/src/server/rooms/live-provenance.js +142 -0
- package/src/server/rooms/page-admission.js +79 -0
- package/src/server/rooms/page-conversion.js +62 -0
- package/src/server/rooms/page-rebase.js +108 -0
- package/src/server/rooms/page-state.js +91 -0
- package/src/server/rooms/page-write-admission.js +63 -0
- package/src/server/rooms/page-write-guard.js +23 -0
- package/src/server/rooms/recent.js +18 -0
- package/src/server/rooms/spec-wire.js +17 -0
- package/src/standalone.js +22 -0
- package/src/sync/conflict-revert.js +2 -0
- package/src/sync/conflicts.js +2 -2
- package/src/sync/live-sync.js +36 -8
- package/src/sync/stream.js +62 -16
- package/src/ui/index.js +4 -0
- package/src/ui/toast.js +21 -5
- package/src/vendor/pierre/pierre.vendor.js +3 -0
- package/src/vendor/sap-core.vendor.js +9 -0
- package/src/vendor/sap-ownership.vendor.js +111 -0
- package/src/vendor/yjs.vendor.js +7 -0
- package/src/page/binding.js +0 -647
- package/src/page/replay.js +0 -578
package/docs/guide.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
This is the complete reference for building malleable documents with ClayJS: one HTML file that is the whole app, edits itself in place, saves itself back to its host, and merges edits from other tabs, other people and agents. Read it top to bottom once. After that, sections 2 (the skeleton), 9 (architecture) and 13 (checklist and trap index) are the parts to return to.
|
|
4
4
|
|
|
5
|
-
It is written for a developer or an LLM building full apps with inline editing and inline controls: kanban boards with card detail, block writers, finance trackers, CRMs, flashcards, trackers. It covers the CMS plugin only in passing (section 8.
|
|
5
|
+
It is written for a developer or an LLM building full apps with inline editing and inline controls: kanban boards with card detail, block writers, finance trackers, CRMs, flashcards, trackers. It covers the CMS plugin only in passing (section 8.12), because an app whose product is editing should not route editing through a form sidebar.
|
|
6
6
|
|
|
7
7
|
Every behavior stated here was read from ClayJS 1.8.2 source or executed. The four example files below are complete, tested apps that this guide quotes. A [test suite](https://github.com/panphora/clayjs/tree/main/tests/unit/guide-examples) boots each one in jsdom with the real loader and checks what it saves, how it merges a peer's edit, and what a viewer receives. Open any of them and view source, or download one and open it in HTML Clay to edit it:
|
|
8
8
|
|
|
@@ -36,7 +36,7 @@ Every element on an open page belongs to one of three kinds. Decide which before
|
|
|
36
36
|
|---|---|---|---|---|---|
|
|
37
37
|
| **Record** | The work: card titles, checklist items, amounts, block text, order | Ordinary elements, attributes, text and `persist` control values | yes | yes | yes |
|
|
38
38
|
| **Chrome** | UI for editing: buttons, grips, toolbars, menus, filter boxes, status text | Authored in the file, hidden from viewers with `show-when:editmode="true"`. UI a script fills or changes goes in `clay="editor-ui"` (9.2) | hidden: yes, `editor-ui`: no | hidden: yes, `editor-ui`: no | no |
|
|
39
|
-
| **Derived** | Values computed from the record: counts, progress, totals, outlines | `clay="editor-ui"` (tab only) or `clay="no-dirty"` (saved, remote wins) |
|
|
39
|
+
| **Derived** | Values computed from the record: counts, progress, totals, outlines, a status pill's colour | sapjs paints (`attr:`, `class:`, `css:`, `text`, `show`, 8.11), `clay="editor-ui"` (tab only) or `clay="no-dirty"` (saved, remote wins) | paints and `no-dirty`: yes | `no-dirty` only; paints are repainted, never merged | no |
|
|
40
40
|
|
|
41
41
|
Nearly every bug in a malleable file is one of these three leaking into another: chrome a script changes saved into the file, a derived count merged as if someone typed it, a record edited only in a copy that never reaches the DOM.
|
|
42
42
|
|
|
@@ -44,7 +44,7 @@ Nearly every bug in a malleable file is one of these three leaking into another:
|
|
|
44
44
|
|
|
45
45
|
1. **The DOM is the record.** Everything durable is an element, an attribute, text, child order, or the value of a control marked `persist`. Never keep the authoritative copy in a JavaScript object.
|
|
46
46
|
2. **Chrome is authored in the file and hidden from viewers with `show-when:editmode="true"`.** This is the default: it needs no script and is in place before ClayJS loads. Use `clay="editor-ui"` only for UI a script fills or changes while the page is open, or that viewers must not receive (9.2).
|
|
47
|
-
3. **Derived values are recomputed, never stored as edits.**
|
|
47
|
+
3. **Derived values are recomputed, never stored as edits.** Store the value once, as the record (`bind`), and declare every other place it shows as a sapjs paint: `attr:data-state="item.state"`, `class:done="item.done"`, `text`, `show` (8.11). ClayJS loads the sapjs core for any page with a `[sap]` root and treats painted values as derived. Without sapjs, write derived values into `editor-ui` elements, or into `clay="no-dirty"` elements when the saved file must show them. Never copy a record into a second attribute with a script: the copy goes stale in the file, and a merge brings the stale copy back.
|
|
48
48
|
4. **Every repeated item has a unique `data-id` from birth.** Mint a fresh one on every create and every duplicate. Sync pairs elements by it.
|
|
49
49
|
5. **Per-tab state stays out of the record.** Which card is open, the filter, the current view: URL hash, `localStorage` or memory, painted through `ephemeral` attributes (5.7), a runtime `<style>` or `editor-ui` elements.
|
|
50
50
|
6. **A command writes the record once, in one place,** wrapped in an undo commit. The mutation is what autosave, undo and sync see.
|
|
@@ -104,10 +104,7 @@ Start every app from [`examples/skeleton.html`](https://clayjs.com/examples/skel
|
|
|
104
104
|
<script src="https://clayjs.com/v1/clay-options.js"></script>
|
|
105
105
|
<script src="https://clayjs.com/v1/clay-ui.js"></script>
|
|
106
106
|
<script>
|
|
107
|
-
clay.
|
|
108
|
-
if (window.__appStarted) return;
|
|
109
|
-
window.__appStarted = true;
|
|
110
|
-
|
|
107
|
+
clay.start(() => {
|
|
111
108
|
const items = document.getElementById("items");
|
|
112
109
|
const newId = (prefix) => prefix + "-" + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
113
110
|
const command = (label, fn) => (clay.undo ? clay.undo.commit(label, fn) : fn());
|
|
@@ -142,10 +139,6 @@ clay.ready.then(() => {
|
|
|
142
139
|
setText(chrome.querySelector("#view-state"), hidden.map((li) => `#items > [data-id="${CSS.escape(li.dataset.id)}"]{display:none}`).join("\n"));
|
|
143
140
|
}
|
|
144
141
|
|
|
145
|
-
function syncCheckboxes() {
|
|
146
|
-
for (const box of items.querySelectorAll("input[type=checkbox]")) box.checked = box.hasAttribute("checked");
|
|
147
|
-
}
|
|
148
|
-
|
|
149
142
|
document.addEventListener("click", (event) => {
|
|
150
143
|
const button = event.target.closest("[data-action]");
|
|
151
144
|
if (!button || !clay.isEditMode) return;
|
|
@@ -168,12 +161,8 @@ clay.ready.then(() => {
|
|
|
168
161
|
}
|
|
169
162
|
});
|
|
170
163
|
|
|
171
|
-
document.addEventListener("clay:sync-applied", () => { syncCheckboxes(); render(); });
|
|
172
|
-
clay.undo?.on("undo", syncCheckboxes);
|
|
173
|
-
clay.undo?.on("redo", syncCheckboxes);
|
|
174
164
|
window.addEventListener("hashchange", () => { filter.value = new URLSearchParams(location.hash.slice(1)).get("q") || ""; render(); });
|
|
175
|
-
|
|
176
|
-
render();
|
|
165
|
+
clay.effect(render);
|
|
177
166
|
});
|
|
178
167
|
</script>
|
|
179
168
|
</body>
|
|
@@ -188,13 +177,12 @@ What each part does, and the section that explains it:
|
|
|
188
177
|
- **The templates** are blueprints. `item-tpl` holds a new row in its **active form** (`contenteditable="plaintext-only"`, no `disabled`), because a clone is inserted while the owner is editing and the mode transform never touches template content (4.4).
|
|
189
178
|
- **`chrome-tpl`** is cloned once into a `clay="editor-ui"` wrapper. The summary, the filter and the `#view-state` style live there, so none of them is saved, synced or undone (5.2, 9.3).
|
|
190
179
|
- **The script tags**: core with `sync,sortable,undo,indicator`, `exclude=richclay` because this app has no rich text, then the `clay-options` and `clay-ui` satellites (3).
|
|
191
|
-
-
|
|
180
|
+
- **`clay.start`** runs the app code once, after boot. A live-sync merge can re-insert the script, and the repeat call does nothing (9.10).
|
|
192
181
|
- **`newId`** makes ids from a timestamp and random suffix. `crypto.randomUUID` exists only in secure contexts, so it fails on a file served over plain http from another machine (5.5).
|
|
193
182
|
- **`command`** wraps every record write in an undo step when the undo plugin is loaded (8.3).
|
|
194
183
|
- **The `onSnapshot` hook** strips Sortable's drag residue (`draggable="false"`, empty `style`, drag classes) from every snapshot, so a drag never writes it into the file (6.6).
|
|
195
184
|
- **`render`** derives the summary and the filter style from the record. It writes only when text changes, because writing the same text is still a mutation (9.4).
|
|
196
|
-
- **`
|
|
197
|
-
- **The `MutationObserver`** re-renders on any record change: local typing, drag, merge or undo. It observes only the record, so the chrome's own writes do not feed back.
|
|
185
|
+
- **`clay.effect(render)`** runs `render` now and again after any change to the document: local typing, a drag, a merge, undo or redo. `render` writes only into the chrome, and an effect's own writes never re-trigger it (7.5).
|
|
198
186
|
|
|
199
187
|
The test suite checks this file opens without becoming dirty, saves the record and no chrome, keeps the filter out of the file, keeps undo and the screen in step, mints unique ids, merges a peer's edit beside an unsaved local edit, and gives a viewer inert text and disabled checkboxes.
|
|
200
188
|
|
|
@@ -231,16 +219,18 @@ The test suite checks this file opens without becoming dirty, saves the record a
|
|
|
231
219
|
| `quickcrop` | off | yes | `clay.quickcrop` |
|
|
232
220
|
| `cms` | off | yes | `clay.cms`; implies `quickcrop` and `upload` |
|
|
233
221
|
| `wire` | off | yes | `clay.wire`, the channel to a local agent process |
|
|
234
|
-
| `message` | off | yes | `message-form`: a visitor's contact form to the owner, `clay.message` (8.
|
|
222
|
+
| `message` | off | yes | `message-form`: a visitor's contact form to the owner, `clay.message` (8.13) |
|
|
235
223
|
| `ai-edit` | off | no | select text, ask an agent to rewrite it; implies `wire` |
|
|
236
224
|
| `demo` | off | yes | fakes saving into `localStorage` for public demos |
|
|
237
225
|
| `visitor-drafts` | off | yes | the draft store, `clay.visitorDrafts`, the download prompt, the intro area and the owner chip on a page that opted in (14) |
|
|
226
|
+
| `derive` | on for any page with a `[sap]` root | yes | the sapjs core: records and paints, treated as derived (8.11) |
|
|
238
227
|
|
|
239
228
|
- Edit-only plugins never load in view mode, even when named. Their members are absent there: test `clay.isEditMode` or `'save' in clay`, never the presence of `clay.RichClay` alone.
|
|
240
229
|
- `exclude` wins over implications. `exclude=wire` also drops `ai-edit`.
|
|
241
230
|
- Load order is fixed regardless of how you list them.
|
|
242
231
|
- `exclude=richclay` when the app has no rich text. `exclude=source` saves one request and about 48 KB, at the cost of saves reprinting the whole document instead of keeping hand formatting.
|
|
243
232
|
- Nothing implies `undo`, on purpose: it intercepts Cmd+Z in every text field on the page (8.3).
|
|
233
|
+
- `derive` loads by itself when the page has a `[sap]` root at boot, and stays idle when full sapjs is on the page. `exclude=derive` opts out; `plugins=derive` loads it for a page that adds its `[sap]` root later.
|
|
244
234
|
- `visitor-drafts` is inert outside draft mode: named on a page with no `visitor-drafts` slot, no `api` rules tag, or a rules tag this version cannot read (not `data-rules-version="1"`, or not JSON), it loads, warns in the console and draws nothing. Section 14 covers the opt in.
|
|
245
235
|
|
|
246
236
|
**A good default for apps:** `?plugins=sync,sortable,undo,indicator`, plus `upload` for images or attachments, plus `exclude=richclay` when there is no rich text.
|
|
@@ -259,7 +249,7 @@ Satellites are separate classic script tags. Each publishes a promise on `clay.l
|
|
|
259
249
|
| `clay-utils.js` | `clay.loaded.utils` | `clay.utils.debounce`, `throttle`, `cookie.get/remove`, `slugify`, `copyToClipboard` | |
|
|
260
250
|
| `clay-internals.js` | `clay.loaded.internals` | `clay.internals`: capture, transforms, raw save | |
|
|
261
251
|
| `clay-data.js` | `clay.loaded.data` | `clay.extractData`, `clay.applyData` | 9.9 |
|
|
262
|
-
| `sap.js` | `clay.loaded.sap` | `Sap`, the reactive attribute layer | 8.
|
|
252
|
+
| `sap.js` | `clay.loaded.sap` | `Sap`, the reactive attribute layer | 8.11 |
|
|
263
253
|
|
|
264
254
|
Satellites do not wait for `clay.ready` and work without `clay.js` at all, including in view mode and on a static host. Await the promise before using what it adds: `await clay.loaded.ui` before `clay.toast` in code that runs early. Code inside a `clay.ready` callback can usually use `clay.toast?.(...)` safely, since the satellites finish loading at about the same time, but the optional call costs nothing.
|
|
265
255
|
|
|
@@ -267,13 +257,13 @@ Satellites do not wait for `clay.ready` and work without `clay.js` at all, inclu
|
|
|
267
257
|
|
|
268
258
|
### 3.4 `clay.ready`
|
|
269
259
|
|
|
270
|
-
`clay.ready` is a promise that resolves with `window.clay` once boot is done and every plugin has loaded. `clay:ready` fires on `document` at the same moment. Before it resolves, members like `clay.save` and `clay.undo` do not exist. Start app code with `clay.
|
|
260
|
+
`clay.ready` is a promise that resolves with `window.clay` once boot is done and every plugin has loaded. `clay:ready` fires on `document` at the same moment. Before it resolves, members like `clay.save` and `clay.undo` do not exist. Start app code with `clay.start(...)`: it waits for `clay.ready` and runs once per page (9.10).
|
|
271
261
|
|
|
272
262
|
You can set configuration before the tag, and the loader merges into it: `window.clay = { saveToast: true }`.
|
|
273
263
|
|
|
274
264
|
### 3.5 Theming ClayJS's own UI
|
|
275
265
|
|
|
276
|
-
ClayJS draws a few things on your page: dialogs (`clay.ask`, `clay.confirm`, `clay.tell`, `clay.snippet`, `clay.modal`) and the people plugin's name prompt. By default they take your page's font, and on a page with a plain solid background, its background and text colours. Everything else comes from ClayJS's own look
|
|
266
|
+
ClayJS draws a few things on your page: dialogs (`clay.ask`, `clay.confirm`, `clay.tell`, `clay.snippet`, `clay.modal`) and the people plugin's name prompt. By default they take your page's font, and on a page with a plain solid background, its background and text colours. Everything else comes from ClayJS's own built-in look.
|
|
277
267
|
|
|
278
268
|
To match them to your design, set `window.clayTheme` before the loader tag:
|
|
279
269
|
|
|
@@ -289,8 +279,8 @@ window.clayTheme = {
|
|
|
289
279
|
|
|
290
280
|
- **Tokens** set the basics: `font`, `headingFont`, `text`, `mutedText`, `background`, `surface`, `border`, `accent`, `accentText`, `danger`, `dangerText`, `success`, `warning`, `radius`, `shadow`, `overlay`, `spacing`, `buttonHeight`. Each is a CSS value, and `var(--your-variable, fallback)` works. A page can also set a token in CSS as `--clay-ui-<name>` (`--clay-ui-accent-text` for `accentText`). `spacing` is one length, such as `20px`.
|
|
291
281
|
- **Parts** set anything else. Every element ClayJS draws has a stable name, such as `dialog.panel`, `dialog.title`, `button.primary` or `people.choice`, and each takes `base`, `hover`, `active`, `focus` and `disabled` declarations. A part names its surface, so `people.title` overrides `dialog.title` in the name prompt only. The names are in `data-clay-part` on each element, and the full list is in `docs/injected-ui.md`.
|
|
292
|
-
- A value wins in this order: `tokens`, then `--clay-ui-*` properties, then the page's sampled font and colours, then
|
|
293
|
-
- `window.clayTheme = false` keeps the exact
|
|
282
|
+
- A value wins in this order: `tokens`, then `--clay-ui-*` properties, then the page's sampled font and colours, then the built-in look.
|
|
283
|
+
- `window.clayTheme = false` keeps the exact built-in look. After loading, `clay.theme(options)` replaces the theme, `clay.theme(false)` and `clay.theme(null)` restore the built-in look or the default, and `clay.theme()` returns the current one. A change affects the next dialog, not one already open.
|
|
294
284
|
|
|
295
285
|
Your stylesheet still cannot reach these elements: every declaration is inline and `!important`, so a stray `button { ... }` rule never breaks a prompt. On a phone, dialog buttons stay at least 44px tall whatever the theme asks for.
|
|
296
286
|
|
|
@@ -346,7 +336,7 @@ The family:
|
|
|
346
336
|
| `option-not:NAME="a"` | some ancestor has `NAME` with a value other than `a`; hidden when no ancestor has `NAME` |
|
|
347
337
|
|
|
348
338
|
- It matches **any** ancestor, not the nearest. Nested views need different attribute names (`view` on the app, `cardview` on a card).
|
|
349
|
-
- It compares literal strings only. Expressions belong to sap's `show=` (8.
|
|
339
|
+
- It compares literal strings only. Expressions belong to sap's `show=` (8.11).
|
|
350
340
|
- `show-when:savestatus="error|offline"` shows a "not saved" warning. `show-when:pageowner="true"` shows owner links.
|
|
351
341
|
- For content only viewers should see, use `hide-when:editmode="true"`: it is visible when `editmode` is absent. `show-when:editmode="false"` is hidden until boot and then appears.
|
|
352
342
|
- The attribute you switch views with is ordinary saved content. Setting `view="list"` on a saved element saves it and syncs it to every collaborator. Section 9.3 covers tab-local views.
|
|
@@ -465,32 +455,7 @@ A control's typed value is a property, not an attribute, and serialization only
|
|
|
465
455
|
- A persist control's value change counts as an edit and starts autosave.
|
|
466
456
|
- Do not persist a filter or search box. Those are tab-local: put them in chrome (9.3).
|
|
467
457
|
|
|
468
|
-
**After undo, redo or a merge,
|
|
469
|
-
|
|
470
|
-
```js
|
|
471
|
-
function syncCheckboxes() {
|
|
472
|
-
for (const box of root.querySelectorAll("input[type=checkbox][persist]")) box.checked = box.hasAttribute("checked");
|
|
473
|
-
}
|
|
474
|
-
document.addEventListener("clay:sync-applied", syncCheckboxes);
|
|
475
|
-
clay.undo?.on("undo", syncCheckboxes);
|
|
476
|
-
clay.undo?.on("redo", syncCheckboxes);
|
|
477
|
-
```
|
|
478
|
-
|
|
479
|
-
Text and number inputs need the same for `value`, and it matters more there: a save copies the live value into the file, so after an undo that left the old number on screen, the next save writes the undone value back. The finance tracker re-syncs every persisted input, skipping the focused one after a merge (7.4), then refreshes sap:
|
|
480
|
-
|
|
481
|
-
```js
|
|
482
|
-
function syncControls({ keepFocused = false } = {}) {
|
|
483
|
-
for (const input of app.querySelectorAll("input[persist]")) {
|
|
484
|
-
if (keepFocused && input === document.activeElement) continue;
|
|
485
|
-
const value = input.getAttribute("value") ?? "";
|
|
486
|
-
if (input.value !== value) input.value = value;
|
|
487
|
-
}
|
|
488
|
-
Sap.refresh();
|
|
489
|
-
}
|
|
490
|
-
clay.undo?.on("undo", () => syncControls());
|
|
491
|
-
clay.undo?.on("redo", () => syncControls());
|
|
492
|
-
document.addEventListener("clay:sync-applied", () => syncControls({ keepFocused: true }));
|
|
493
|
-
```
|
|
458
|
+
**After undo, redo or a merge, ClayJS puts each control's display back in step with its attributes.** A control the user has touched shows its live property, not its attribute, so a merge and the undo plugin copy `checked`, `value` and `selected` back onto the property (undo does this for `persist` controls outside `no-undo` regions). A merge leaves the focused text field alone; undo, which the user asked for, restores it too.
|
|
494
459
|
|
|
495
460
|
### 5.4 Where to keep data that is not visible text
|
|
496
461
|
|
|
@@ -744,9 +709,9 @@ More engine rules:
|
|
|
744
709
|
|
|
745
710
|
- Text merges word by word inside a block, and formatting merges per character. Inline `<style>` text merges word by word. Scripts, textarea text and comments merge as whole values.
|
|
746
711
|
- Matched live nodes are moved, not recreated, so listeners, focus, caret and iframes survive. An element that matches nothing (re-keyed, or changed beyond recognition among similar siblings) is removed and inserted fresh, and listeners attached directly to it are gone. **Use delegated listeners** on `document` or a stable root.
|
|
747
|
-
- A new or changed inline `<script>` in the frame runs once after the merge. Hence
|
|
712
|
+
- A new or changed inline `<script>` in the frame runs once after the merge. Hence `clay.start` (9.10).
|
|
748
713
|
- Window scroll is restored. Inner scroll containers are not, except the focused element's own.
|
|
749
|
-
- The merge runs with the mutation hub, the dirty gate and undo paused: incoming changes never autosave and never enter undo history.
|
|
714
|
+
- The merge runs with the mutation hub, the dirty gate and undo paused: incoming changes never autosave and never enter undo history. `clay.effect` re-runs after it through `clay:sync-applied`, which is how the examples re-render (7.5).
|
|
750
715
|
|
|
751
716
|
### 7.3 Regions and JSON under sync
|
|
752
717
|
|
|
@@ -777,16 +742,33 @@ document.addEventListener("clay:sync-applied", () => {
|
|
|
777
742
|
|
|
778
743
|
### 7.5 Reconciling in app code
|
|
779
744
|
|
|
780
|
-
|
|
745
|
+
Draw everything derived from the record inside `clay.effect(fn)`. It runs `fn` on the next microtask, then again after every change the mutation hub observes (local edits, drags, undo and redo) and after every merge, batched to one run per microtask. Changes inside `editor-ui` or `no-watch` regions, ephemeral attributes and live properties with no attribute change do not re-run it. `fn` runs with the hub paused, so its own writes never re-trigger it, start an autosave or enter undo history. It returns a function that stops it.
|
|
746
|
+
|
|
747
|
+
```js
|
|
748
|
+
clay.effect(() => {
|
|
749
|
+
const rows = items.querySelectorAll(":scope > li");
|
|
750
|
+
const done = items.querySelectorAll("input[checked]").length;
|
|
751
|
+
setText(summary, `${done} of ${rows.length} read`);
|
|
752
|
+
});
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
Rules for `fn`:
|
|
756
|
+
|
|
757
|
+
- **Keep it synchronous.** Writes after an `await` land outside the pause and would re-trigger it forever, so an effect whose `fn` returns a promise is stopped with a console error.
|
|
758
|
+
- **Do not call `clay.undo.commit` inside it.** A commit drains the hub's records, so the effect's writes reach autosave and the effect again.
|
|
759
|
+
- **One effect does not see another's writes**, because each runs paused. Draw values that depend on each other in one effect.
|
|
760
|
+
- **A derived value saved in the record** (a count painted in markup, 9.4) is written with the hub paused, so a repaint after a merge leaves the page dirty with no save scheduled. Put saved derived values in `clay="no-dirty"`, or in chrome, or declare them as sapjs paints, which ClayJS leaves out of every comparison (8.11).
|
|
761
|
+
|
|
762
|
+
In edit mode each effect warns once in the console when it breaks one of these rules: a write to saved content outside `no-dirty` and `editor-ui`, a `clay.undo.commit` call, an effect created inside it, or more than 50 runs in a second (usually a timer or async callback it started that keeps changing the document). The effect keeps running; the warning names the element or command.
|
|
763
|
+
|
|
764
|
+
Work that belongs to a merge alone goes in a `clay:sync-applied` listener:
|
|
781
765
|
|
|
782
766
|
```js
|
|
783
767
|
document.addEventListener("clay:sync-applied", (event) => {
|
|
784
|
-
|
|
785
|
-
render();
|
|
768
|
+
if (openId && !cardById(openId)) closeCard();
|
|
786
769
|
});
|
|
787
770
|
```
|
|
788
771
|
|
|
789
|
-
- Re-render derived values and projections.
|
|
790
772
|
- Close or repoint any view whose record a peer deleted. The kanban closes the open card when its id is gone: `if (openId && !cardById(openId)) return writeHash({ card: "" }, { replace: true });`
|
|
791
773
|
- Re-parse any JSON you parsed earlier.
|
|
792
774
|
- Re-attach any per-element setup that a re-created element lost (the writer re-injects block gutters on every render).
|
|
@@ -813,7 +795,7 @@ An agent edits a malleable file the way a person in another tab does.
|
|
|
813
795
|
|
|
814
796
|
- **Editing the file on disk** (HTML Clay, Hyperclay Local): the host notices the change and pushes a `disk` frame. The open tab merges it three ways, so the person's unsaved typing in other paragraphs survives, and an overlap goes to the ledger. Tell agents to address elements by `data-id`, never by position, and to leave chrome alone (it is not in the file anyway).
|
|
815
797
|
- **`clay.wire`** (`?plugins=wire`, HTML Clay and Hyperclay Local): `clay.wire.send(payload)` sends a request to a local process started with `htmlclay wire serve file.htmlclay -- my-agent`. The page saves first, so the agent reads what the person sees, and autosave pauses until the agent's edit lands as a disk frame. `run.done` resolves then.
|
|
816
|
-
- **`ai-edit`** (`?plugins=ai-edit`): select text, press Cmd+J, describe the change. The rewrite is previewed in place; Keep saves it, Revert discards it. Available where the host offers the `ai-edit` helper
|
|
798
|
+
- **`ai-edit`** (`?plugins=ai-edit`): select text, press Cmd+J, describe the change. The rewrite is previewed in place; Keep saves it, Revert discards it. Available where the host offers the `ai-edit` helper: HTML Clay, Hyperclay Local, and hyperclay.com (for the owner and team editors on a paid plan, billed to the document's AI credits).
|
|
817
799
|
- **The data API**: a host that announces `data-read`/`data-write` serves `/_/api/<file>`, a JSON read and write face over the file defined by a rules tag (9.9). `clay.readData()` and `clay.writeData()` call it from the page; `writeData` is for agents and other processes, and refuses while the page has unsaved edits.
|
|
818
800
|
|
|
819
801
|
### 7.9 Live data: `clay="live"`
|
|
@@ -848,7 +830,7 @@ function render() {
|
|
|
848
830
|
}
|
|
849
831
|
```
|
|
850
832
|
|
|
851
|
-
**The tag.** A `<script type="application/json">` with `merge="name"` and `clay="live"`, holding a JSON object or list. The name is 1 to 64 letters, digits, `_`, `.`, `:` or `-`, and two live tags with the same name are both ignored. Write the tag into the file before people share it: a tag added later is not in the rooms of tabs that loaded before. Checkpoints add a `live-at` attribute to the tag (the room position the value was saved at, and a hash of the text).
|
|
833
|
+
**The tag.** A `<script type="application/json">` with `merge="name"` and `clay="live"`, holding a JSON object or list. The name is 1 to 64 letters, digits, `_`, `.`, `:` or `-`, and two live tags with the same name are both ignored. Write the tag into the file before people share it: a tag added later is not in the rooms of tabs that loaded before. Checkpoints add a `live-at` attribute to the tag (the room position the value was saved at, and a hash of the text). A save made while the tab is out of the room also adds `live-by` (that tab and the last update it sent), so the room does not apply those updates a second time. The room trusts `live-by` only while the value still matches the hash in `live-at`, and never on a restored version. Leave both in place when you or an agent edit the tag's JSON by hand: the room then merges your edit with the play since that checkpoint. Without it, the edit is merged over what the room last saved. A save hook (`onbeforesave`) may reformat a live tag's JSON, but must not change its value. A tag is written in the layout it was loaded with: one line, or indented when the authored JSON had line breaks. `live-format="lines"` writes each entry of each top-level object on its own line instead, so the file's version history shows only the entries that changed, which suits a large store keyed by id (the page editor's blocks).
|
|
852
834
|
|
|
853
835
|
**The API.** `clay.live(name)` returns the same handle every time.
|
|
854
836
|
|
|
@@ -875,11 +857,13 @@ Two rules for the proxy:
|
|
|
875
857
|
|
|
876
858
|
`game.status.mode` is `fast` on the first two lanes and `file` on the third. In view mode a store reads normally, but writes stay in the tab and are never sent or saved.
|
|
877
859
|
|
|
860
|
+
**The page editor's own store.** A Document's blocks are a live store named `page`, and its merge state is not in the file: hyperclay.com and Hyperclay Local keep it beside the file and hand it back to an editor only while it still belongs to the article (the article's `data-rev` matches), and HTML Clay keeps none, so open tabs share it and a fresh open rebuilds it from the article. Saved files and downloads never carry it. A page changed outside the editor (the code editor, an agent, a version restore, another device, a text editor) merges into the page tabs that are open, the way a peer's edit does: each block is merged three ways against the article the change started from, so edits on both sides survive. Where both sides changed the same words, the outside change wins, and the tab offers a download of what it had. An edit beats a delete. Only a change with no article this host can trace it back to shows a reload notice, and the tab stops saving so the old article is not written back over the new one.
|
|
861
|
+
|
|
878
862
|
**When a room is lost.** A room lives in memory: a server restart, or two minutes with nobody connected, ends it. The next room starts from the last checkpoint, so up to 8 seconds of play can be gone. A change of yours that the old room may already have taken is not sent to the new one, even when its answer was lost: if the old room's last checkpoint lands afterwards, it brings that change back, and sending it again would count it twice. A change that never left the tab is sent as usual. Closing the tab that runs a local room is not a loss: the next tab takes over the room with every move.
|
|
879
863
|
|
|
880
864
|
**Load the plugin wherever a live tag exists.** A page without `?plugins=live` still leaves live tags out of the merge, so an incoming save does not update them and the next save writes the old text back.
|
|
881
865
|
|
|
882
|
-
**Limits on hyperclay.com.** A frame carries at most 128 ops and
|
|
866
|
+
**Limits on hyperclay.com.** A frame carries at most 128 ops and 64 KB, except that one value larger than that is sent in a request of its own of up to 5 MB, so a single change can write a value up to the store's 4 MB; the page editor splits its changes into 12 KB pieces, so a Document's size is not limited by the frame. A store holds at most 4 MB of JSON, a document at most 6 MB across at most 16 stores, which gives a Document roughly 900 KB of text that keeps compacting. One person sends at most 25 frames a second per document (bursts to 40), and ClayJS paces itself to stay under that. A change the room refuses is undone in the tab, with a warning. When the server holds as much live data as it can, a document without a room, or a change that would grow one, moves at save speed and tries the room again a minute later.
|
|
883
867
|
|
|
884
868
|
**A two-player example.** The board above plus a never-saved lobby for pointers:
|
|
885
869
|
|
|
@@ -1012,7 +996,6 @@ Traps:
|
|
|
1012
996
|
|
|
1013
997
|
Traps:
|
|
1014
998
|
|
|
1015
|
-
- **Undo of a `persist` control restores the attribute, not what is shown** (5.3). Re-sync `.checked` and `.value` on `undo` and `redo`.
|
|
1016
999
|
- **It takes Cmd+Z in every text field**, including inputs it does not record (a non-persist input), so their native undo is lost.
|
|
1017
1000
|
- History is per tab, capped at 100, and not saved.
|
|
1018
1001
|
- Undoing a delete re-inserts the original node even if a peer re-created an equivalent; dedupe by `data-id` on `clay:sync-applied` if that matters.
|
|
@@ -1094,15 +1077,11 @@ For fixed content, such as code samples in a docs page, mark the block `clay="fr
|
|
|
1094
1077
|
|
|
1095
1078
|
For a notes or journal app where markdown is the format, OverType (`https://cdn.jsdelivr.net/npm/overtype@2.6.1/dist/overtype.min.js`) is a textarea under a styled preview. With `new OverType(el, { persist: true })` the saved file holds only a `<textarea persist>` and its wrapper; everything else is `editor-ui`. Keep one live instance at a time, and normalize OverType's layout state in an `onbeforesave` on the container. A journal built this way keeps entries as `<article data-id>` records, clones its chrome from a `<template>`, and paints the open entry with a runtime stylesheet.
|
|
1096
1079
|
|
|
1097
|
-
### 8.10
|
|
1098
|
-
|
|
1099
|
-
Bevel (`@panphora/bevel`) is a separate component library with a kanban board, an editable data grid, inline edit, upload dropzone and modal, all with state in markup. ClayJS does not load it. Load `bevel.css` and `bevel.js` yourself and register `Bevel.prepareForSave` with `clay.onSnapshot`. It has its own drag and events and is not wired to ClayJS undo or sortable. Use it when a ready-made grid or board is acceptable; build from this guide when the app needs its own shape.
|
|
1080
|
+
### 8.10 Attributes from older Hyperclay files
|
|
1100
1081
|
|
|
1101
|
-
|
|
1082
|
+
Older Hyperclay files can carry attributes ClayJS ignores: `movable`, `movable-handle`, `prevent-enter`, `autosize`, `ajax-form`, `ajax-button`, `send-message`, `upgrade`. Replace them with a few lines of code (`prevent-enter`: a keydown handler; `autosize`: CSS `field-sizing: content`; `movable`: pointer events writing `data-x`/`data-y`; `send-message`: the `message` plugin, 8.13). Old saved `<html editmode="false">` and hand-written `option-visibility-styles` style tags are dead weight; ClayJS strips the root attributes on the next save.
|
|
1102
1083
|
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
### 8.12 sap.js: records, views and totals in markup
|
|
1084
|
+
### 8.11 sap.js: records, views and totals in markup
|
|
1106
1085
|
|
|
1107
1086
|
`sap.js` (a satellite, also published standalone as `@panphora/sapjs`) is a reactive attribute layer whose only store is the DOM. Every change runs one pass: it reads the app out of the live page, computes `calc:` fields in dependency order, and writes only the paints that changed. It is the way to build the reactive parts of a malleable document app: totals, filtered and sorted views, a form that adds records, an editor for the selected record.
|
|
1108
1087
|
|
|
@@ -1123,23 +1102,45 @@ Older Hyperclay files can carry attributes ClayJS ignores: `movable`, `movable-h
|
|
|
1123
1102
|
|
|
1124
1103
|
The vocabulary in one breath: `sap` marks an app root; `items="list"` holds `[item]` records and `[item][template]` (or a shared `<template items="list">`) is the blueprint; `bind` two-way binds a control or a text leaf; `state="data-due data-done:presence"` declares fields stored as attributes; `calc:`, `text:fmt`, `show`, `class:`, `css:`, `attr:` and `effect` paint; `each="expr"` renders a view of any array whose rows edit the records they show; `detail="LIST by KEY"` projects one record into a panel or a `<dialog>`; actions are `trigger-add`, `trigger-remove`, `move:`, `sort:`, `set:` and `confirm=`; `Sap(el)` is a write-through proxy and `Sap.batch(label, fn)` makes one undo step. Done means no `sap ✗` console line and `Sap.status().ok === true`.
|
|
1125
1104
|
|
|
1126
|
-
How it fits ClayJS:
|
|
1105
|
+
How it fits ClayJS:
|
|
1106
|
+
|
|
1107
|
+
- **The core loads by itself.** On any page with a `[sap]` root, ClayJS loads the sapjs core (the `derive` plugin, 3.2). The core covers records (`bind`, `state=`, `items` and `item`) and paints (`text`, `text:fmt`, `show`, `attr:`, `class:`, `css:`, the `show-when:` family). Markup that needs full sapjs (`calc:`, `each`, `detail`, `effect`, `invalid`, the actions, `Sap(el)`) halts with `E50`, which names it. Load `https://clayjs.com/v1/sap.js` for full sapjs; the markup stays the same, and full sapjs takes the page over from the core.
|
|
1108
|
+
- **Painted values are derived.** Comparisons, the dirty check, autosave, undo and every merge input leave out what a paint owns: the attribute an `attr:` names, the one class token a `class:` names, the `--` property a `css:` names, the children of a `text` element, and `hidden` under `show`. A paint never makes the page unsaved, and two tabs that change different records while repainting the same total end with the right total and no conflict.
|
|
1109
|
+
- **Ownership is read from the markup.** It comes from the element's own attributes, never from evaluating an expression, so it holds the same in the live page, a save clone and a frame from disk. `attr:` never owns `id`, `data-id`, `class`, `style`, `value`, `checked`, `selected`, or a name the same element declares in `state=`: those stay record.
|
|
1110
|
+
- **Repainted before anything records a baseline.** The engine mounts in a boot phase, after frozen regions record their markup and visitor drafts restore, and the boot baseline settles on the painted page. After every merge it repaints right away, before the saved, peer and disk baselines and before `clay:sync-applied`.
|
|
1111
|
+
- **Painted values are still saved,** so the file shows them without JavaScript. When a repaint leaves the file's painted values different from the page's (an agent wrote a record and left the old paint), one clean tab with autosave on saves the painted page after a short random delay, and other tabs that repainted the same frame see that save and skip theirs.
|
|
1112
|
+
- **Agents and `api` rules read and write records, never paints.** Point a rule at the `bind` element or a `state=` attribute (`"state": ".state"`), never at a painted attribute: a painted value on disk can lag the record until a tab repaints and saves it.
|
|
1113
|
+
- Rows of an `each=` view carry `clay="editor-ui"`, so views never save or sync.
|
|
1114
|
+
|
|
1115
|
+
A status that sets text, colour and a label, with the state stored once:
|
|
1116
|
+
|
|
1117
|
+
```html
|
|
1118
|
+
<main sap>
|
|
1119
|
+
<table><tbody items="steps">
|
|
1120
|
+
<tr item class="step" data-id="s1" attr:data-state="item.state">
|
|
1121
|
+
<td><span class="state" bind="state" attr:aria-label="'Status: ' + item.state">accepted</span></td>
|
|
1122
|
+
</tr>
|
|
1123
|
+
</tbody></table>
|
|
1124
|
+
</main>
|
|
1125
|
+
```
|
|
1126
|
+
|
|
1127
|
+
`tr[data-state="accepted"] .state { background: var(--ok) }` colours it, and `htmlfile data --write` or the JSON API writes only `.state`.
|
|
1127
1128
|
|
|
1128
1129
|
Rules for sapjs in a malleable file:
|
|
1129
1130
|
|
|
1130
|
-
- **Store each record once, as an element; derive everything else.** Counts, totals and card faces computed only from records are ordinary saved content: paint them into the record markup and ship the file pre-painted, so the first pass writes nothing (`mount writes 0`). A
|
|
1131
|
+
- **Store each record once, as an element; derive everything else.** Counts, totals and card faces computed only from records are ordinary saved content: paint them into the record markup and ship the file pre-painted, so the first pass writes nothing (`mount writes 0`). A stale painted value repaints at mount; under ClayJS the paint heal saves it, and on other hosts sapjs logs `W30` and the value goes out with the next real save.
|
|
1131
1132
|
- **Per-tab values go in `clay="freeze"`**: anything from today's date, the locale, the URL hash, the search box or runtime data. Freeze saves an element's contents as they were at load, so put the per-tab values inside the frozen element, or fill it from an `effect`, which re-runs every pass.
|
|
1132
1133
|
- **Per-tab state is `transient`** (`state="q:transient open:transient"`, and `transient` on the bound search input), or the URL hash through `clay-router.js`.
|
|
1133
|
-
- **Filter with an `each=` view, or a frozen stylesheet**, never with `show=` on saved records: `show=`
|
|
1134
|
+
- **Filter with an `each=` view, or a frozen stylesheet**, never with `show=` on saved records: `show=` paints `hidden`, which ClayJS treats as derived, so it never syncs, but a save still writes the saving tab's filter into the file.
|
|
1134
1135
|
- **Every `detail=` panel and `<dialog>` sits inside a `clay="freeze"` wrapper.** Its controls are painted with the open record's values; outside freeze they would save a second copy. Not `editor-ui`, which removes the element from the file. sapjs warns `W31` for a dialog without the wrapper.
|
|
1135
1136
|
- **Give records a `data-id`**, and `data-id=""` on blueprints: `$add` and `trigger-add` mint fresh ids. Live sync pairs elements by it.
|
|
1136
1137
|
- **`persist` on bound inputs inside records**, so typed values reach the attributes a save captures.
|
|
1137
|
-
- **
|
|
1138
|
+
- **Undo and redo restore `persist` controls' live values** from their attributes, and a sync merge does the same, so sapjs reads what the file says. Controls without `persist` are not restored; give them `persist` or re-sync them yourself.
|
|
1138
1139
|
- **`viewmode:` attributes log `W03`** (unknown attribute) in sapjs, for example 4.4's `viewmode:readonly`. The warning is harmless, but it makes a "no warnings" check fail.
|
|
1139
1140
|
- **Hide blueprints in your own CSS** (`[item][template] { display: none }`); sapjs's own rule arrives only with the script. Sapjs also adds `[hidden] { display: none !important }` page-wide.
|
|
1140
1141
|
- A pass is batched: after a write, call `Sap.refresh()` before reading painted output. Expressions compile with `new Function`, so a Content Security Policy without `unsafe-eval` breaks them.
|
|
1141
1142
|
|
|
1142
|
-
### 8.
|
|
1143
|
+
### 8.12 The CMS, in passing
|
|
1143
1144
|
|
|
1144
1145
|
`?plugins=cms` turns a page into a content-managed page: a JSON rules tag maps field names to selectors, and an "Edit content" button opens a sidebar form (or edits text in place) that writes straight into the DOM.
|
|
1145
1146
|
|
|
@@ -1153,7 +1154,7 @@ It fits content sites: landing pages, menus, pricing, FAQs, team pages, where th
|
|
|
1153
1154
|
|
|
1154
1155
|
---
|
|
1155
1156
|
|
|
1156
|
-
### 8.
|
|
1157
|
+
### 8.13 message: contact forms
|
|
1157
1158
|
|
|
1158
1159
|
`?plugins=message` sends a visitor's form to the document's owner. On hyperclay.com the owner gets it by email and in their messages.
|
|
1159
1160
|
|
|
@@ -1245,7 +1246,7 @@ Never put per-viewer state on `<html>` or any record element without that: it au
|
|
|
1245
1246
|
|
|
1246
1247
|
Pick by whether the saved file must show the value:
|
|
1247
1248
|
|
|
1248
|
-
- **Computed only from the records** (counts, progress badges, totals, a card's face): ordinary saved content. Write it into the record markup, ship the file pre-painted, and recompute on every change
|
|
1249
|
+
- **Computed only from the records** (counts, progress badges, totals, a card's face): ordinary saved content. Write it into the record markup, ship the file pre-painted, and recompute on every change. With sap this is just a paint (8.11), and ClayJS treats it as derived: merges leave it out and the engine repaints it from the merged records, so two tabs that changed different records end with the right value and no conflict. Hand-written derived text in saved content does not get this; give it `no-dirty`. The starter Kanban paints its list counts and card faces this way.
|
|
1249
1250
|
- **Different per tab** (search results, which entry is open, "overdue" against today's date, OS shortcut labels, names loaded from the people plugin): write it inside a `clay="freeze"` scratchpad (5.2). The element saves and syncs; its contents never do.
|
|
1250
1251
|
- **Pure UI** (buttons, menus, handles): `editor-ui`, created from a `<template>` (9.2).
|
|
1251
1252
|
- **Optionally, `no-dirty`** for a computed value the saved file shows: it saves with the next real edit, never counts as an edit itself, and remote wins under sync. Apply the 7.3 workaround. The finance tracker does this.
|
|
@@ -1257,7 +1258,7 @@ Always write derived text through a guard, because writing the same text is stil
|
|
|
1257
1258
|
const setText = (el, text) => { if (el.textContent !== text) el.textContent = text; };
|
|
1258
1259
|
```
|
|
1259
1260
|
|
|
1260
|
-
Recompute
|
|
1261
|
+
Recompute inside `clay.effect(render)`, which covers record mutations, merges, undo and redo (7.5), and call `render()` from a `hashchange` listener when the hash affects the view.
|
|
1261
1262
|
|
|
1262
1263
|
With sapjs, every derived value here is a paint. sapjs recomputes on record mutations and `clay:sync-applied` by itself, and on hash changes only through `clay-router.js` (`clay:route`); an app with its own hash format keeps a `hashchange` listener that calls `Sap.refresh()`. See [sapjs guide section 6](https://saphtml.com/guide.html).
|
|
1263
1264
|
|
|
@@ -1333,7 +1334,7 @@ document.addEventListener("click", (event) => {
|
|
|
1333
1334
|
|
|
1334
1335
|
### 9.7 Filters and view switching
|
|
1335
1336
|
|
|
1336
|
-
- **Filter by hiding** records with a runtime stylesheet (skeleton, kanban), or with sapjs through an `each=` view or a frozen stylesheet (8.
|
|
1337
|
+
- **Filter by hiding** records with a runtime stylesheet (skeleton, kanban), or with sapjs through an `each=` view or a frozen stylesheet (8.11). The finance example still uses sap `show=` plus snapshot normalization, an older pattern: `show=` writes `hidden` into saved records. Never remove records to filter.
|
|
1337
1338
|
- **Switch layouts with CSS** over the same records where possible: a list view can be the board with `display: contents` on columns.
|
|
1338
1339
|
- **Projection views** (a table of all cards, a calendar, a due-today list) render into an `editor-ui` container and rebuild on record mutation, `clay:sync-applied` and `hashchange`. Edits made in a projection write to the record by id.
|
|
1339
1340
|
|
|
@@ -1341,7 +1342,7 @@ With sapjs, a projection view is an `each=` attribute, and its rows edit the rec
|
|
|
1341
1342
|
|
|
1342
1343
|
### 9.8 Computed totals
|
|
1343
1344
|
|
|
1344
|
-
Plain JavaScript into `editor-ui` (9.4) for a count or two. Sapjs for anything with arithmetic across rows, categories and formats (8.
|
|
1345
|
+
Plain JavaScript into `editor-ui` (9.4) for a count or two. Sapjs for anything with arithmetic across rows, categories and formats (8.11, and the expense tracker in sapjs guide section 9). Store money as integers or as plain numbers in `value` attributes, never as formatted strings.
|
|
1345
1346
|
|
|
1346
1347
|
### 9.9 Import and export
|
|
1347
1348
|
|
|
@@ -1380,18 +1381,18 @@ Sap.batch("Import CSV", () => {
|
|
|
1380
1381
|
|
|
1381
1382
|
A rules tag with `data-rules-name="api"` gives agents and the host's data API the same shape your export uses.
|
|
1382
1383
|
|
|
1383
|
-
### 9.10
|
|
1384
|
+
### 9.10 Starting the app once
|
|
1384
1385
|
|
|
1385
|
-
A live-sync merge can re-insert an inline script, which runs it again: every listener doubles and every chrome element appears twice.
|
|
1386
|
+
A live-sync merge can re-insert an inline script, which runs it again: every listener doubles and every chrome element appears twice. Start the app with `clay.start`. It runs its function once per page, after `clay.ready`, and a repeat call does nothing:
|
|
1386
1387
|
|
|
1387
1388
|
```js
|
|
1388
|
-
clay.
|
|
1389
|
-
if (window.__boardStarted) return;
|
|
1390
|
-
window.__boardStarted = true;
|
|
1389
|
+
clay.start(() => {
|
|
1391
1390
|
…
|
|
1392
1391
|
});
|
|
1393
1392
|
```
|
|
1394
1393
|
|
|
1394
|
+
A page with more than one app script names each one so they do not share a slot: `clay.start("board", fn)`.
|
|
1395
|
+
|
|
1395
1396
|
### 9.11 Size
|
|
1396
1397
|
|
|
1397
1398
|
Every save captures and serializes the whole document, every change check captures it again, and every peer merges the whole document per accepted save. Measured in jsdom and scaled to a browser (estimates):
|
|
@@ -1405,6 +1406,28 @@ Every save captures and serializes the whole document, every change check captur
|
|
|
1405
1406
|
|
|
1406
1407
|
Up to about 1,000 records feels instant. From 1,000 to 3,000 there is a visible hitch at each save. Past about 3,000 records or 1.5 MB, autosave stalls typing and peers lag. Hosts cap a save at 5 MB (hyperclay.com), 20 MB (Hyperclay Local) and 50 MB (HTML Clay). Design for archiving: one file per year, a hidden archive section that can move to another file, or one file per project.
|
|
1407
1408
|
|
|
1409
|
+
### 9.12 Styles a script writes
|
|
1410
|
+
|
|
1411
|
+
Use `clay.style` when a script owns CSS that should stay in the saved document. The saved style paints the next load before the script runs. When the script runs, it refreshes that same tag with its current CSS:
|
|
1412
|
+
|
|
1413
|
+
```js
|
|
1414
|
+
await clay.ready;
|
|
1415
|
+
clay.style("app-colors", ".status { color: blue; }");
|
|
1416
|
+
```
|
|
1417
|
+
|
|
1418
|
+
The name identifies a `<style data-name="app-colors" clay="no-dirty">` in the document's head. A repeated call updates the existing tag in place. Refreshing it does not add an undo step, start an autosave, or raise the close warning. The next real save includes the current CSS. On the first load, before any save has carried the tag, the script creates it.
|
|
1419
|
+
|
|
1420
|
+
The one-argument form keeps an external stylesheet link current:
|
|
1421
|
+
|
|
1422
|
+
```js
|
|
1423
|
+
await clay.ready;
|
|
1424
|
+
const link = clay.style("/app.css?v=2");
|
|
1425
|
+
```
|
|
1426
|
+
|
|
1427
|
+
If the saved file has `/app.css?v=1`, this updates that link's `href` to `/app.css?v=2`. Matching uses the URL's origin and path, ignoring its query and fragment. The link is also `no-dirty` and travels with the next real save. Both forms return the element synchronously; the link form does not wait for the stylesheet to load. Both work in edit and view mode.
|
|
1428
|
+
|
|
1429
|
+
Use `clay="editor-ui"` instead for CSS that viewers must not receive or that represents per-tab state, such as a temporary filter. Those styles are stripped from saved HTML. `clay.style` is for CSS that belongs in the file; changing its name on every load creates another saved tag. Adding or rewriting a plain style tag without `no-dirty` can raise the leave warning even though the person edited nothing.
|
|
1430
|
+
|
|
1408
1431
|
---
|
|
1409
1432
|
|
|
1410
1433
|
## 10. Recipes
|
|
@@ -1455,7 +1478,7 @@ Missing pieces to add on the same model: a table block (`<table>` with `editmode
|
|
|
1455
1478
|
- `main#app[sap]` with a summary of `no-dirty` outputs (budgeted, spent, left), a categories table (`items="categories"`, each row a name, a persisted budget input, computed spent and left, a progress bar via `css:used`) and a transactions table (`items="txns"`, each row date, memo, category with a datalist, amount).
|
|
1456
1479
|
- Category spent: `calc:spent="sum(root.txns.filter(t => t.cat === item.name), 'amount')"`.
|
|
1457
1480
|
- Search: a `transient` bound input, `calc:match` and `show="item.match"` per row, `hidden` stripped from snapshots.
|
|
1458
|
-
- Rows get stable ids from `claimRowIds`, a workaround from before sapjs minted unique ids. A new app writes `data-id=""` on the blueprint and lets `trigger-add` mint ids (8.
|
|
1481
|
+
- Rows get stable ids from `claimRowIds`, a workaround from before sapjs minted unique ids. A new app writes `data-id=""` on the blueprint and lets `trigger-add` mint ids (8.11).
|
|
1459
1482
|
- CSV import (edit only) and export (both modes).
|
|
1460
1483
|
- The 7.3 acknowledge workaround for `no-dirty` totals.
|
|
1461
1484
|
|
|
@@ -1479,7 +1502,7 @@ One `<section class="slide" data-id data-layout="title|bullets|image|quote">` pe
|
|
|
1479
1502
|
|
|
1480
1503
|
### 10.8 Content site
|
|
1481
1504
|
|
|
1482
|
-
Without the CMS: `editable` regions for copy, upload and quickcrop for images, `sortable` lists for features. With the CMS: a rules tag and `?plugins=cms` (8.
|
|
1505
|
+
Without the CMS: `editable` regions for copy, upload and quickcrop for images, `sortable` lists for features. With the CMS: a rules tag and `?plugins=cms` (8.12).
|
|
1483
1506
|
|
|
1484
1507
|
---
|
|
1485
1508
|
|
|
@@ -1519,7 +1542,8 @@ Cross-cutting:
|
|
|
1519
1542
|
| Presence | none | none | avatar stack, no API |
|
|
1520
1543
|
| `by` on frames, editing marker | no | no | yes |
|
|
1521
1544
|
| Live stores (7.9) | tabs of one browser at full speed | tabs of one browser at full speed; other devices at save speed | every editor at full speed |
|
|
1522
|
-
|
|
|
1545
|
+
| Page editor merge state (7.9) | none in the file: open tabs share it, a fresh open rebuilds it from the article | kept beside the file | kept beside the file |
|
|
1546
|
+
| Wire and ai-edit | yes | yes | ai-edit only, for owners and team editors on a paid plan |
|
|
1523
1547
|
| Data API write | yes | yes | read only |
|
|
1524
1548
|
| Uploads | 25 MB, in `~/htmlclay/uploads/` | 25 MB, in the root's `uploads/` | 10, 20 or 100 MB by plan, in the account's `uploads/` |
|
|
1525
1549
|
| Version history | yes | 60 days, newest 20 | a version per save; a restore does not reach open editors' tabs, so reload after restoring |
|
|
@@ -1559,10 +1583,10 @@ The saved file is the data. Everyone who can download it can read everything in
|
|
|
1559
1583
|
7. Every control whose value matters has `persist`; no filter or search box does.
|
|
1560
1584
|
8. All chrome is cloned from a `<template>` into `clay="editor-ui"` after `clay.ready`; nothing in the file is chrome.
|
|
1561
1585
|
9. Tab-local state is in the hash, `localStorage` or memory, painted through a runtime style; opening, filtering and searching leave `clay.hasUnsavedChanges()` false.
|
|
1562
|
-
10. Derived values are in `editor-ui`, or `no-dirty` when the file must show them; every derived write is guarded.
|
|
1563
|
-
11. Commands are wrapped in `clay.undo.commit`;
|
|
1564
|
-
12.
|
|
1565
|
-
13. The app script
|
|
1586
|
+
10. Derived values are sapjs paints, in `editor-ui`, or `no-dirty` when the file must show them; every hand-written derived write is guarded, and no script copies a record into a second attribute.
|
|
1587
|
+
11. Commands are wrapped in `clay.undo.commit`; derived views are drawn in `clay.effect`, with no effect warnings in the console.
|
|
1588
|
+
12. Views whose record a peer deleted are closed, in the effect or a `clay:sync-applied` handler.
|
|
1589
|
+
13. The app script starts with `clay.start`; all listeners are delegated.
|
|
1566
1590
|
14. Nothing writes the record at boot.
|
|
1567
1591
|
15. Edit-only calls are guarded for view mode, and edit actions check `clay.isEditMode`.
|
|
1568
1592
|
16. `clay.confirm` calls have a rejection handler.
|
|
@@ -1615,6 +1639,9 @@ The saved file is the data. Everyone who can download it can read everything in
|
|
|
1615
1639
|
| Same-key JSON edits under `merge`: incoming wins, yours goes to the notice | 7.3 |
|
|
1616
1640
|
| Rewriting the record at boot makes incoming frames hold | 7.4 |
|
|
1617
1641
|
| Parsed JSON goes stale after a merge | 7.5 |
|
|
1642
|
+
| A script that copies a record into a styling attribute goes stale in the file, and a merge brings the stale copy back | 1.3, 8.11 |
|
|
1643
|
+
| An `api` rule that reads a painted value can read a stale one until a tab repaints and saves | 8.11 |
|
|
1644
|
+
| Full sapjs markup under the sapjs core halts with `E50` | 8.11 |
|
|
1618
1645
|
| There is no presence API | 7.7 |
|
|
1619
1646
|
| `data-richclay` card mode deletes images on load | 8.1 |
|
|
1620
1647
|
| The richclay toolbar is unusable inside `showModal()` and popovers | 8.1 |
|
|
@@ -1628,7 +1655,7 @@ The saved file is the data. Everyone who can download it can read everything in
|
|
|
1628
1655
|
| The undo plugin takes Cmd+Z in every text field | 8.3 |
|
|
1629
1656
|
| Undoing an add removes the record even after a peer edited it | 8.3 |
|
|
1630
1657
|
| Pausing undo across an `await` loses the earlier mutations | 8.3 |
|
|
1631
|
-
| Sap reads live control values: undo without re-syncing inputs saves the undone number | 5.3, 8.
|
|
1658
|
+
| Sap reads live control values: undo without re-syncing inputs saves the undone number | 5.3, 8.11 |
|
|
1632
1659
|
| `clay.upload` is not a picker; embed only on `unsupported` | 8.5 |
|
|
1633
1660
|
| `clay.confirm` rejects with `undefined` on cancel | 8.6 |
|
|
1634
1661
|
| A live element passed to `clay.modal.html` is deleted | 8.6 |
|
|
@@ -1636,15 +1663,16 @@ The saved file is the data. Everyone who can download it can read everything in
|
|
|
1636
1663
|
| `onclone` does not fire for template fragments | 8.7 |
|
|
1637
1664
|
| `el.nearest` reads a neighbour record's field | 8.7 |
|
|
1638
1665
|
| `hide()`, `show()`, `css()` write saved inline styles | 8.7 |
|
|
1639
|
-
| A file whose painted values do not match sap's state writes at load | 8.
|
|
1666
|
+
| A file whose painted values do not match sap's state writes at load | 8.11 |
|
|
1640
1667
|
| Dialog prompts are HTML: record text in `clay.confirm` runs a collaborator's markup | 8.6 |
|
|
1641
1668
|
| `editmode:resource type="inert/javascript"` activates as `javascript` and never runs | 4.4 |
|
|
1642
|
-
| Sap's template row shows before sap loads | 8.
|
|
1643
|
-
| CMS `cms-template` with `hidden` makes every new item invisible | 8.
|
|
1669
|
+
| Sap's template row shows before sap loads | 8.11 |
|
|
1670
|
+
| CMS `cms-template` with `hidden` makes every new item invisible | 8.12 |
|
|
1644
1671
|
| Records stored twice drift | 9.1 |
|
|
1645
1672
|
| `applyData` pairs rows by content and order, not by id | 9.9 |
|
|
1646
1673
|
| Activity logged from a mutation observer also logs peers and undo | 9.6 |
|
|
1647
1674
|
| Saves slow down past about 1,000 records and stall past 3,000 | 9.11 |
|
|
1675
|
+
| Updating a runtime style tag without `no-dirty` can raise the leave warning | 9.12 |
|
|
1648
1676
|
| A version restore on hyperclay.com does not reach open editors | 12.1 |
|
|
1649
1677
|
| Hidden is not private | 12.3 |
|
|
1650
1678
|
| Typed values vanish from a visitor's draft: the control has no `persist` | 14.2 |
|
|
@@ -1664,11 +1692,11 @@ The owner is a visitor here too. Signed in, they get the same private draft unle
|
|
|
1664
1692
|
|
|
1665
1693
|
A page that wants draft mode needs all of this. The example in 14.8 carries every item, and its test proves the round trip:
|
|
1666
1694
|
|
|
1667
|
-
1. An `api` rules tag naming every saved field, with a `cms-template` seed row for every list the rules describe. Hide the seed in your own CSS (`[cms-template] { display: none !important }`), never with `hidden`, which the CMS reads as data (8.
|
|
1695
|
+
1. An `api` rules tag naming every saved field, with a `cms-template` seed row for every list the rules describe. Hide the seed in your own CSS (`[cms-template] { display: none !important }`), never with `hidden`, which the CMS reads as data (8.12).
|
|
1668
1696
|
2. `<html visitor-drafts="<site-stem>-v1" autosave>`. The value is the slot: letters, digits, dot, underscore and hyphen, up to 100 characters. It names this app's corner of `localStorage`, so two apps on one origin never mix.
|
|
1669
1697
|
3. `plugins=visitor-drafts` on the loader tag, plus whatever else the app needs.
|
|
1670
1698
|
4. `persist` on every control the rules name. A typed value without `persist` is lost at save (5.3), and in draft mode a save is the visitor's own copy.
|
|
1671
|
-
5. `data-id` on every record and `data-id=""` on every blueprint. New rows mint their own (`trigger-add` does it, 8.
|
|
1699
|
+
5. `data-id` on every record and `data-id=""` on every blueprint. New rows mint their own (`trigger-add` does it, 8.11).
|
|
1672
1700
|
6. `<meta name="application-name" content="...">`. The prompt, the intro and the download name the app from it, falling back to the title before the first `:` or `|`.
|
|
1673
1701
|
7. A feedback link the visitor can use: a `mailto:` address in the page, not in the plugin.
|
|
1674
1702
|
8. Optional: a `<template visitor-intro>` (14.6). No template, no intro; the prompt still shows.
|
|
@@ -1725,7 +1753,7 @@ The rung is where a free document lives, so the details matter:
|
|
|
1725
1753
|
|
|
1726
1754
|
### 14.5 Restoring and redrawing
|
|
1727
1755
|
|
|
1728
|
-
The restore runs while ClayJS boots, before `clay.ready` resolves, and applies the stored JSON to the live page with `applyData`. sapjs and any code that waits for `clay.ready` therefore see the restored values, not the authored ones, and sapjs re-derives on the mutations the restore makes, so a count or a total painted in markup is right on the first paint after it. Load `clay.js` before `sap.js` for that: sapjs mounts when `clay.ready` resolves, and a page that mounts it first paints the authored values with no later mutation to correct them (8.
|
|
1756
|
+
The restore runs while ClayJS boots, before `clay.ready` resolves, and applies the stored JSON to the live page with `applyData`. sapjs and any code that waits for `clay.ready` therefore see the restored values, not the authored ones, and sapjs re-derives on the mutations the restore makes, so a count or a total painted in markup is right on the first paint after it. Load `clay.js` before `sap.js` for that: sapjs mounts when `clay.ready` resolves, and a page that mounts it first paints the authored values with no later mutation to correct them (8.11).
|
|
1729
1757
|
|
|
1730
1758
|
Code that drew before `clay.ready` has two ways to hear about the restore: listen for `clay:drafts-restored` on `document`, or call `clay.visitorDrafts.onRestore(fn)`, which runs `fn` at once when the restore already happened. Either way the detail is `{ restored, hadDraft, parked }`.
|
|
1731
1759
|
|
|
@@ -1765,7 +1793,7 @@ An element inside the template with `[visitor-intro-start]` scrolls to the app,
|
|
|
1765
1793
|
|
|
1766
1794
|
### 14.8 A complete example
|
|
1767
1795
|
|
|
1768
|
-
[`examples/free-reading-list.html`](https://clayjs.com/examples/free-reading-list.html). Plugins `visitor-drafts,indicator&exclude=richclay`, satellites `clay-options` and `sap.js`. A reading list: a title, an author and a status select per book, one scalar goal for the year, and sapjs counting the books that are read. It is the shortest file that carries every item of the contract, and `tests/unit/guide-examples/free-document.test.js` runs it end to end: edit a title, add a book, check the slot, then load the file again with that storage and check the books and the counts come back. The record is authored in the saved form, so sapjs logs its harmless W03 warning for each `viewmode:` attribute (8.
|
|
1796
|
+
[`examples/free-reading-list.html`](https://clayjs.com/examples/free-reading-list.html). Plugins `visitor-drafts,indicator&exclude=richclay`, satellites `clay-options` and `sap.js`. A reading list: a title, an author and a status select per book, one scalar goal for the year, and sapjs counting the books that are read. It is the shortest file that carries every item of the contract, and `tests/unit/guide-examples/free-document.test.js` runs it end to end: edit a title, add a book, check the slot, then load the file again with that storage and check the books and the counts come back. The record is authored in the saved form, so sapjs logs its harmless W03 warning for each `viewmode:` attribute (8.11). The `reading` option of a status select carries no `selected`: it is the first option, which is the default, and the row that is `read` marks it.
|
|
1769
1797
|
|
|
1770
1798
|
```html
|
|
1771
1799
|
<!DOCTYPE html>
|
package/entries/clay.js
CHANGED
|
@@ -23,6 +23,28 @@
|
|
|
23
23
|
|
|
24
24
|
if (!clay.ready) mintReady();
|
|
25
25
|
|
|
26
|
+
// clay.start runs app code once per page, after boot. A live-sync merge can
|
|
27
|
+
// re-insert an inline script and run it again; the repeat call is a no-op.
|
|
28
|
+
// It lives in the bootstrap so an inline script can call it before boot ends.
|
|
29
|
+
if (!clay.start) {
|
|
30
|
+
var started = Object.create(null);
|
|
31
|
+
clay.start = function (name, fn) {
|
|
32
|
+
if (typeof name === "function") { fn = name; name = ""; }
|
|
33
|
+
if (started[name]) return;
|
|
34
|
+
started[name] = true;
|
|
35
|
+
// A failed boot rejects the old promise after minting a new one, so a call
|
|
36
|
+
// made before the failure waits for the corrected tag's boot instead.
|
|
37
|
+
var wait = function () {
|
|
38
|
+
var ready = clay.ready;
|
|
39
|
+
return ready.then(fn, function (err) {
|
|
40
|
+
if (clay.ready === ready) throw err;
|
|
41
|
+
return wait();
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
return wait();
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
26
48
|
var script = document.currentScript;
|
|
27
49
|
if (!script || !script.src) {
|
|
28
50
|
console.error("clayjs: could not determine my own URL (load me with a classic <script src=...>, not type=\"module\")");
|