@panphora/clayjs 1.13.1 → 1.14.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.
Files changed (136) hide show
  1. package/THIRD-PARTY-NOTICES.md +19 -0
  2. package/dist/clay.standalone.js +69667 -41708
  3. package/dist/server/checkpoint.cjs +70690 -0
  4. package/dist/server/checkpoint.mjs +8 -0
  5. package/dist/server/rooms.cjs +72039 -0
  6. package/dist/server/rooms.mjs +66 -0
  7. package/docs/guide.md +94 -93
  8. package/entries/clay.js +22 -0
  9. package/package.json +16 -2
  10. package/packed-contract.json +17 -3
  11. package/src/core/conflict-download.js +11 -2
  12. package/src/core/host-meta.js +75 -0
  13. package/src/core/persist.js +11 -0
  14. package/src/core/save-core.js +266 -31
  15. package/src/core/save.js +62 -32
  16. package/src/core/snapshot.js +39 -13
  17. package/src/core/style-comparisons.js +37 -0
  18. package/src/dom/insert-style-tag.js +11 -12
  19. package/src/lib/effect.js +130 -0
  20. package/src/lib/style-adoption.js +9 -0
  21. package/src/lib/style.js +44 -0
  22. package/src/live/fast-lane.js +347 -60
  23. package/src/live/local-lane.js +32 -6
  24. package/src/live/ops.js +77 -0
  25. package/src/live/provenance.js +129 -0
  26. package/src/live/save-history.js +188 -0
  27. package/src/live/server-lane.js +9 -2
  28. package/src/live/store.js +194 -16
  29. package/src/loader-logic.js +7 -3
  30. package/src/loader.js +6 -0
  31. package/src/page/address.js +20 -0
  32. package/src/page/adopt-boot.js +189 -0
  33. package/src/page/adopt.js +240 -0
  34. package/src/page/ai-tool.js +188 -0
  35. package/src/page/announce.js +21 -0
  36. package/src/page/blocks.js +1121 -0
  37. package/src/page/blocksel.js +269 -0
  38. package/src/page/board-drag.js +226 -0
  39. package/src/page/board-panel.js +353 -0
  40. package/src/page/board-style.js +146 -0
  41. package/src/page/board-view.js +447 -0
  42. package/src/page/board.js +41 -0
  43. package/src/page/chart.js +220 -0
  44. package/src/page/checklist.js +143 -0
  45. package/src/page/chrome.js +587 -0
  46. package/src/page/csv-import.js +169 -0
  47. package/src/page/drag.js +202 -0
  48. package/src/page/drop.js +112 -0
  49. package/src/page/editor.js +384 -0
  50. package/src/page/highlight.js +268 -0
  51. package/src/page/inline.js +60 -1
  52. package/src/page/items.js +159 -0
  53. package/src/page/link.js +247 -0
  54. package/src/page/markdown.js +71 -0
  55. package/src/page/media.js +570 -0
  56. package/src/page/merge.js +130 -0
  57. package/src/page/model.js +71 -9
  58. package/src/page/placeholder.js +23 -0
  59. package/src/page/projection.js +647 -0
  60. package/src/page/recover.js +193 -0
  61. package/src/page/rich/cell.js +16 -0
  62. package/src/page/rich/chart.js +104 -0
  63. package/src/page/rich/csv.js +118 -0
  64. package/src/page/rich/page-tool.js +86 -0
  65. package/src/page/rich/tool-runtime.js +195 -0
  66. package/src/page/save-projection.js +53 -0
  67. package/src/page/save-states.js +85 -0
  68. package/src/page/schema.js +12 -5
  69. package/src/page/table-kinds.js +20 -0
  70. package/src/page/table-paste.js +57 -0
  71. package/src/page/table-select.js +244 -0
  72. package/src/page/table-style.js +25 -0
  73. package/src/page/table.js +650 -0
  74. package/src/page/themes/content.css +46 -0
  75. package/src/page/themes/light-1.css +269 -0
  76. package/src/page/themes/light-2.css +211 -0
  77. package/src/page/themes/light-3.css +130 -0
  78. package/src/page/themes/light-4.css +299 -0
  79. package/src/page/tool-data.js +25 -0
  80. package/src/page/tool-view.js +68 -0
  81. package/src/page/tools.js +133 -0
  82. package/src/page/view.js +136 -0
  83. package/src/page/ybind/author.js +25 -0
  84. package/src/page/ybind/binding.js +92 -0
  85. package/src/page/ybind/board-commands.js +170 -0
  86. package/src/page/ybind/board.js +89 -0
  87. package/src/page/ybind/bridge.js +382 -0
  88. package/src/page/ybind/conversion.js +26 -0
  89. package/src/page/ybind/core.js +7 -0
  90. package/src/page/ybind/decode.js +208 -0
  91. package/src/page/ybind/doc.js +58 -0
  92. package/src/page/ybind/external.js +190 -0
  93. package/src/page/ybind/group.js +124 -0
  94. package/src/page/ybind/index.js +12 -0
  95. package/src/page/ybind/layout.js +27 -0
  96. package/src/page/ybind/marks.js +18 -0
  97. package/src/page/ybind/migrate.js +152 -0
  98. package/src/page/ybind/moves.js +288 -0
  99. package/src/page/ybind/origins.js +257 -0
  100. package/src/page/ybind/parts.js +44 -0
  101. package/src/page/ybind/projection-map.js +92 -0
  102. package/src/page/ybind/provider.js +285 -0
  103. package/src/page/ybind/recovery.js +411 -0
  104. package/src/page/ybind/seed.js +27 -0
  105. package/src/page/ybind/table.js +166 -0
  106. package/src/page/ybind/tokens.js +93 -0
  107. package/src/page/ybind/walk.js +333 -0
  108. package/src/page/ybind/writer.js +307 -0
  109. package/src/plugins/indicator.js +16 -6
  110. package/src/plugins/live.js +742 -65
  111. package/src/plugins/page.js +547 -0
  112. package/src/plugins/undo.js +60 -3
  113. package/src/server/checkpoint/index.js +218 -0
  114. package/src/server/rooms/index.js +59 -0
  115. package/src/server/rooms/live-data-ops.js +369 -0
  116. package/src/server/rooms/live-data-rooms.js +965 -0
  117. package/src/server/rooms/live-data-tags.js +49 -0
  118. package/src/server/rooms/live-page-history.js +41 -0
  119. package/src/server/rooms/live-provenance.js +142 -0
  120. package/src/server/rooms/page-admission.js +79 -0
  121. package/src/server/rooms/page-conversion.js +62 -0
  122. package/src/server/rooms/page-rebase.js +108 -0
  123. package/src/server/rooms/page-state.js +91 -0
  124. package/src/server/rooms/page-write-admission.js +63 -0
  125. package/src/server/rooms/page-write-guard.js +23 -0
  126. package/src/server/rooms/recent.js +18 -0
  127. package/src/server/rooms/spec-wire.js +17 -0
  128. package/src/standalone.js +22 -0
  129. package/src/sync/live-sync.js +13 -2
  130. package/src/sync/stream.js +62 -16
  131. package/src/ui/index.js +4 -0
  132. package/src/ui/toast.js +21 -5
  133. package/src/vendor/pierre/pierre.vendor.js +3 -0
  134. package/src/vendor/yjs.vendor.js +7 -0
  135. package/src/page/binding.js +0 -647
  136. package/src/page/replay.js +0 -578
@@ -0,0 +1,66 @@
1
+ import rooms from './rooms.cjs';
2
+ export default rooms;
3
+ export const {
4
+ LIVE_LIMITS,
5
+ PageConversionError,
6
+ PageFormatError,
7
+ PersistentReservationError,
8
+ RANGE_LIMIT,
9
+ RECEIPT_LIMIT,
10
+ REFUSED_MS,
11
+ applyOp,
12
+ applyOps,
13
+ applyTracked,
14
+ assignDeep,
15
+ checkOp,
16
+ checkPageConversion,
17
+ checkPageInitial,
18
+ checkPageWriteAdmission,
19
+ checkPageWriteFormat,
20
+ cloneJson,
21
+ closestBase,
22
+ coalesce,
23
+ composePage,
24
+ contains,
25
+ createRooms,
26
+ descendants,
27
+ diffOps,
28
+ documentEtag,
29
+ formatBy,
30
+ frameSet,
31
+ hasNumber,
32
+ identitySet,
33
+ ifMatchSatisfied,
34
+ inspectPage,
35
+ isHybridPageValue,
36
+ isYPageValue,
37
+ liveTags,
38
+ makePageTransition,
39
+ overlaps,
40
+ pageBaseline,
41
+ pageLineageLost,
42
+ pageRev,
43
+ pageTransitionState,
44
+ parseBy,
45
+ parsePageConversion,
46
+ parsePath,
47
+ parseStamp,
48
+ postReceipt,
49
+ rangeCount,
50
+ rebasePage,
51
+ receiptBytes,
52
+ receiptHas,
53
+ replaceReceipt,
54
+ retireReceipt,
55
+ roomIsDirty,
56
+ saveReceipt,
57
+ serializeLive,
58
+ sizeChange,
59
+ splitPage,
60
+ stampOf,
61
+ stampPageRev,
62
+ textHash,
63
+ undoTracked,
64
+ unionRanges,
65
+ validateOp,
66
+ } = rooms;
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.13), because an app whose product is editing should not route editing through a form sidebar.
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
 
@@ -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.ready.then(() => {
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
- new MutationObserver(render).observe(items, { subtree: true, childList: true, attributes: true, characterData: true });
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
- - **The boot guard** `window.__appStarted` stops a second run if a live-sync merge re-inserts the script (9.10).
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
- - **`syncCheckboxes`** puts each checkbox's displayed state back in step with its `checked` attribute after undo, redo or a merge, which change the attribute but not the property (8.3).
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,7 +219,7 @@ 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.14) |
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) |
@@ -259,7 +247,7 @@ Satellites are separate classic script tags. Each publishes a promise on `clay.l
259
247
  | `clay-utils.js` | `clay.loaded.utils` | `clay.utils.debounce`, `throttle`, `cookie.get/remove`, `slugify`, `copyToClipboard` | |
260
248
  | `clay-internals.js` | `clay.loaded.internals` | `clay.internals`: capture, transforms, raw save | |
261
249
  | `clay-data.js` | `clay.loaded.data` | `clay.extractData`, `clay.applyData` | 9.9 |
262
- | `sap.js` | `clay.loaded.sap` | `Sap`, the reactive attribute layer | 8.12 |
250
+ | `sap.js` | `clay.loaded.sap` | `Sap`, the reactive attribute layer | 8.11 |
263
251
 
264
252
  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
253
 
@@ -267,13 +255,13 @@ Satellites do not wait for `clay.ready` and work without `clay.js` at all, inclu
267
255
 
268
256
  ### 3.4 `clay.ready`
269
257
 
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.ready.then(...)`.
258
+ `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
259
 
272
260
  You can set configuration before the tag, and the loader merges into it: `window.clay = { saveToast: true }`.
273
261
 
274
262
  ### 3.5 Theming ClayJS's own UI
275
263
 
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, Bevel.
264
+ 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
265
 
278
266
  To match them to your design, set `window.clayTheme` before the loader tag:
279
267
 
@@ -289,8 +277,8 @@ window.clayTheme = {
289
277
 
290
278
  - **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
279
  - **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 Bevel.
293
- - `window.clayTheme = false` keeps the exact Bevel look. After loading, `clay.theme(options)` replaces the theme, `clay.theme(false)` and `clay.theme(null)` restore Bevel or the default, and `clay.theme()` returns the current one. A change affects the next dialog, not one already open.
280
+ - A value wins in this order: `tokens`, then `--clay-ui-*` properties, then the page's sampled font and colours, then the built-in look.
281
+ - `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
282
 
295
283
  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
284
 
@@ -346,7 +334,7 @@ The family:
346
334
  | `option-not:NAME="a"` | some ancestor has `NAME` with a value other than `a`; hidden when no ancestor has `NAME` |
347
335
 
348
336
  - 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.12).
337
+ - It compares literal strings only. Expressions belong to sap's `show=` (8.11).
350
338
  - `show-when:savestatus="error|offline"` shows a "not saved" warning. `show-when:pageowner="true"` shows owner links.
351
339
  - 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
340
  - 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 +453,7 @@ A control's typed value is a property, not an attribute, and serialization only
465
453
  - A persist control's value change counts as an edit and starts autosave.
466
454
  - Do not persist a filter or search box. Those are tab-local: put them in chrome (9.3).
467
455
 
468
- **After undo, redo or a merge, the screen can disagree with the file.** Undo and sync restore the attribute, but the browser's checkbox and input display follow the property once the user has touched the control. Re-sync the property from the attribute on all three events, as the skeleton and kanban do for checkboxes:
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
- ```
456
+ **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
457
 
495
458
  ### 5.4 Where to keep data that is not visible text
496
459
 
@@ -744,9 +707,9 @@ More engine rules:
744
707
 
745
708
  - 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
709
  - 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 the boot guard (9.10).
710
+ - A new or changed inline `<script>` in the frame runs once after the merge. Hence `clay.start` (9.10).
748
711
  - 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. Your own `MutationObserver` still sees them, which is how the examples re-render.
712
+ - 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
713
 
751
714
  ### 7.3 Regions and JSON under sync
752
715
 
@@ -777,16 +740,33 @@ document.addEventListener("clay:sync-applied", () => {
777
740
 
778
741
  ### 7.5 Reconciling in app code
779
742
 
780
- After `clay:sync-applied`, rebuild everything derived from the record and every piece of chrome that points into it:
743
+ 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.
744
+
745
+ ```js
746
+ clay.effect(() => {
747
+ const rows = items.querySelectorAll(":scope > li");
748
+ const done = items.querySelectorAll("input[checked]").length;
749
+ setText(summary, `${done} of ${rows.length} read`);
750
+ });
751
+ ```
752
+
753
+ Rules for `fn`:
754
+
755
+ - **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.
756
+ - **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.
757
+ - **One effect does not see another's writes**, because each runs paused. Draw values that depend on each other in one effect.
758
+ - **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.
759
+
760
+ 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.
761
+
762
+ Work that belongs to a merge alone goes in a `clay:sync-applied` listener:
781
763
 
782
764
  ```js
783
765
  document.addEventListener("clay:sync-applied", (event) => {
784
- syncCheckboxes();
785
- render();
766
+ if (openId && !cardById(openId)) closeCard();
786
767
  });
787
768
  ```
788
769
 
789
- - Re-render derived values and projections.
790
770
  - 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
771
  - Re-parse any JSON you parsed earlier.
792
772
  - Re-attach any per-element setup that a re-created element lost (the writer re-injects block gutters on every render).
@@ -813,7 +793,7 @@ An agent edits a malleable file the way a person in another tab does.
813
793
 
814
794
  - **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
795
  - **`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 (HTML Clay, Hyperclay Local).
796
+ - **`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
797
  - **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
798
 
819
799
  ### 7.9 Live data: `clay="live"`
@@ -848,7 +828,7 @@ function render() {
848
828
  }
849
829
  ```
850
830
 
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). Leave it 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.
831
+ **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
832
 
853
833
  **The API.** `clay.live(name)` returns the same handle every time.
854
834
 
@@ -875,11 +855,13 @@ Two rules for the proxy:
875
855
 
876
856
  `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
857
 
858
+ **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.
859
+
878
860
  **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
861
 
880
862
  **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
863
 
882
- **Limits on hyperclay.com.** A frame carries at most 128 ops and 16 KB. A store holds at most 128 KB of JSON, a document at most 512 KB across at most 16 stores. 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 console warning.
864
+ **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
865
 
884
866
  **A two-player example.** The board above plus a never-saved lobby for pointers:
885
867
 
@@ -1012,7 +994,6 @@ Traps:
1012
994
 
1013
995
  Traps:
1014
996
 
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
997
  - **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
998
  - History is per tab, capped at 100, and not saved.
1018
999
  - 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 +1075,11 @@ For fixed content, such as code samples in a docs page, mark the block `clay="fr
1094
1075
 
1095
1076
  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
1077
 
1097
- ### 8.10 Bevel components
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.
1078
+ ### 8.10 Attributes from older Hyperclay files
1100
1079
 
1101
- ### 8.11 Attributes from older Hyperclay files
1080
+ 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
1081
 
1103
- 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.14). 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.
1104
-
1105
- ### 8.12 sap.js: records, views and totals in markup
1082
+ ### 8.11 sap.js: records, views and totals in markup
1106
1083
 
1107
1084
  `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
1085
 
@@ -1134,12 +1111,12 @@ Rules for sapjs in a malleable file:
1134
1111
  - **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
1112
  - **Give records a `data-id`**, and `data-id=""` on blueprints: `$add` and `trigger-add` mint fresh ids. Live sync pairs elements by it.
1136
1113
  - **`persist` on bound inputs inside records**, so typed values reach the attributes a save captures.
1137
- - **Re-sync controls after undo and redo**: they restore the attribute, not the live value sapjs reads (a sync merge moves live values itself). Use the `syncControls` script in [sapjs guide section 8](https://saphtml.com/guide.html#8-work-with-clayjs), which restores inputs, checkboxes, textareas and selects.
1114
+ - **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
1115
  - **`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
1116
  - **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
1117
  - 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
1118
 
1142
- ### 8.13 The CMS, in passing
1119
+ ### 8.12 The CMS, in passing
1143
1120
 
1144
1121
  `?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
1122
 
@@ -1153,7 +1130,7 @@ It fits content sites: landing pages, menus, pricing, FAQs, team pages, where th
1153
1130
 
1154
1131
  ---
1155
1132
 
1156
- ### 8.14 message: contact forms
1133
+ ### 8.13 message: contact forms
1157
1134
 
1158
1135
  `?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
1136
 
@@ -1245,7 +1222,7 @@ Never put per-viewer state on `<html>` or any record element without that: it au
1245
1222
 
1246
1223
  Pick by whether the saved file must show the value:
1247
1224
 
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; every tab computes the same value, so a merge never fights over it. With sap this is just a paint (8.12). The starter Kanban paints its list counts and card faces this way.
1225
+ - **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; every tab computes the same value, so a merge never fights over it. With sap this is just a paint (8.11). The starter Kanban paints its list counts and card faces this way.
1249
1226
  - **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
1227
  - **Pure UI** (buttons, menus, handles): `editor-ui`, created from a `<template>` (9.2).
1251
1228
  - **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 +1234,7 @@ Always write derived text through a guard, because writing the same text is stil
1257
1234
  const setText = (el, text) => { if (el.textContent !== text) el.textContent = text; };
1258
1235
  ```
1259
1236
 
1260
- Recompute on three triggers: record mutations (a `MutationObserver` on the record container, coalesced with a microtask), `clay:sync-applied`, and `hashchange` when the hash affects the view. Observe the record, not the chrome, so derived writes do not trigger themselves.
1237
+ 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
1238
 
1262
1239
  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
1240
 
@@ -1333,7 +1310,7 @@ document.addEventListener("click", (event) => {
1333
1310
 
1334
1311
  ### 9.7 Filters and view switching
1335
1312
 
1336
- - **Filter by hiding** records with a runtime stylesheet (skeleton, kanban), or with sapjs through an `each=` view or a frozen stylesheet (8.12). The finance example still uses sap `show=` plus snapshot normalization, an older pattern: `show=` writes `hidden` into saved records. Never remove records to filter.
1313
+ - **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
1314
  - **Switch layouts with CSS** over the same records where possible: a list view can be the board with `display: contents` on columns.
1338
1315
  - **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
1316
 
@@ -1341,7 +1318,7 @@ With sapjs, a projection view is an `each=` attribute, and its rows edit the rec
1341
1318
 
1342
1319
  ### 9.8 Computed totals
1343
1320
 
1344
- Plain JavaScript into `editor-ui` (9.4) for a count or two. Sapjs for anything with arithmetic across rows, categories and formats (8.12, and the expense tracker in sapjs guide section 9). Store money as integers or as plain numbers in `value` attributes, never as formatted strings.
1321
+ 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
1322
 
1346
1323
  ### 9.9 Import and export
1347
1324
 
@@ -1380,18 +1357,18 @@ Sap.batch("Import CSV", () => {
1380
1357
 
1381
1358
  A rules tag with `data-rules-name="api"` gives agents and the host's data API the same shape your export uses.
1382
1359
 
1383
- ### 9.10 The boot guard
1360
+ ### 9.10 Starting the app once
1384
1361
 
1385
- A live-sync merge can re-insert an inline script, which runs it again: every listener doubles and every chrome element appears twice. Guard the app script:
1362
+ 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
1363
 
1387
1364
  ```js
1388
- clay.ready.then(() => {
1389
- if (window.__boardStarted) return;
1390
- window.__boardStarted = true;
1365
+ clay.start(() => {
1391
1366
  …
1392
1367
  });
1393
1368
  ```
1394
1369
 
1370
+ A page with more than one app script names each one so they do not share a slot: `clay.start("board", fn)`.
1371
+
1395
1372
  ### 9.11 Size
1396
1373
 
1397
1374
  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 +1382,28 @@ Every save captures and serializes the whole document, every change check captur
1405
1382
 
1406
1383
  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
1384
 
1385
+ ### 9.12 Styles a script writes
1386
+
1387
+ 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:
1388
+
1389
+ ```js
1390
+ await clay.ready;
1391
+ clay.style("app-colors", ".status { color: blue; }");
1392
+ ```
1393
+
1394
+ 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.
1395
+
1396
+ The one-argument form keeps an external stylesheet link current:
1397
+
1398
+ ```js
1399
+ await clay.ready;
1400
+ const link = clay.style("/app.css?v=2");
1401
+ ```
1402
+
1403
+ 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.
1404
+
1405
+ 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.
1406
+
1408
1407
  ---
1409
1408
 
1410
1409
  ## 10. Recipes
@@ -1455,7 +1454,7 @@ Missing pieces to add on the same model: a table block (`<table>` with `editmode
1455
1454
  - `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
1455
  - Category spent: `calc:spent="sum(root.txns.filter(t => t.cat === item.name), 'amount')"`.
1457
1456
  - 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.12).
1457
+ - 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
1458
  - CSV import (edit only) and export (both modes).
1460
1459
  - The 7.3 acknowledge workaround for `no-dirty` totals.
1461
1460
 
@@ -1479,7 +1478,7 @@ One `<section class="slide" data-id data-layout="title|bullets|image|quote">` pe
1479
1478
 
1480
1479
  ### 10.8 Content site
1481
1480
 
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.13).
1481
+ 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
1482
 
1484
1483
  ---
1485
1484
 
@@ -1519,7 +1518,8 @@ Cross-cutting:
1519
1518
  | Presence | none | none | avatar stack, no API |
1520
1519
  | `by` on frames, editing marker | no | no | yes |
1521
1520
  | 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
- | Wire and ai-edit | yes | yes | no |
1521
+ | 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 |
1522
+ | Wire and ai-edit | yes | yes | ai-edit only, for owners and team editors on a paid plan |
1523
1523
  | Data API write | yes | yes | read only |
1524
1524
  | 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
1525
  | Version history | yes | 60 days, newest 20 | a version per save; a restore does not reach open editors' tabs, so reload after restoring |
@@ -1560,9 +1560,9 @@ The saved file is the data. Everyone who can download it can read everything in
1560
1560
  8. All chrome is cloned from a `<template>` into `clay="editor-ui"` after `clay.ready`; nothing in the file is chrome.
1561
1561
  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
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`; checkboxes and inputs are re-synced from attributes on undo, redo and `clay:sync-applied`.
1564
- 12. A `clay:sync-applied` handler re-renders and closes views whose record vanished.
1565
- 13. The app script has a boot guard; all listeners are delegated.
1563
+ 11. Commands are wrapped in `clay.undo.commit`; derived views are drawn in `clay.effect`, with no effect warnings in the console.
1564
+ 12. Views whose record a peer deleted are closed, in the effect or a `clay:sync-applied` handler.
1565
+ 13. The app script starts with `clay.start`; all listeners are delegated.
1566
1566
  14. Nothing writes the record at boot.
1567
1567
  15. Edit-only calls are guarded for view mode, and edit actions check `clay.isEditMode`.
1568
1568
  16. `clay.confirm` calls have a rejection handler.
@@ -1628,7 +1628,7 @@ The saved file is the data. Everyone who can download it can read everything in
1628
1628
  | The undo plugin takes Cmd+Z in every text field | 8.3 |
1629
1629
  | Undoing an add removes the record even after a peer edited it | 8.3 |
1630
1630
  | 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.12 |
1631
+ | Sap reads live control values: undo without re-syncing inputs saves the undone number | 5.3, 8.11 |
1632
1632
  | `clay.upload` is not a picker; embed only on `unsupported` | 8.5 |
1633
1633
  | `clay.confirm` rejects with `undefined` on cancel | 8.6 |
1634
1634
  | A live element passed to `clay.modal.html` is deleted | 8.6 |
@@ -1636,15 +1636,16 @@ The saved file is the data. Everyone who can download it can read everything in
1636
1636
  | `onclone` does not fire for template fragments | 8.7 |
1637
1637
  | `el.nearest` reads a neighbour record's field | 8.7 |
1638
1638
  | `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.12 |
1639
+ | A file whose painted values do not match sap's state writes at load | 8.11 |
1640
1640
  | Dialog prompts are HTML: record text in `clay.confirm` runs a collaborator's markup | 8.6 |
1641
1641
  | `editmode:resource type="inert/javascript"` activates as `javascript` and never runs | 4.4 |
1642
- | Sap's template row shows before sap loads | 8.12 |
1643
- | CMS `cms-template` with `hidden` makes every new item invisible | 8.13 |
1642
+ | Sap's template row shows before sap loads | 8.11 |
1643
+ | CMS `cms-template` with `hidden` makes every new item invisible | 8.12 |
1644
1644
  | Records stored twice drift | 9.1 |
1645
1645
  | `applyData` pairs rows by content and order, not by id | 9.9 |
1646
1646
  | Activity logged from a mutation observer also logs peers and undo | 9.6 |
1647
1647
  | Saves slow down past about 1,000 records and stall past 3,000 | 9.11 |
1648
+ | Updating a runtime style tag without `no-dirty` can raise the leave warning | 9.12 |
1648
1649
  | A version restore on hyperclay.com does not reach open editors | 12.1 |
1649
1650
  | Hidden is not private | 12.3 |
1650
1651
  | Typed values vanish from a visitor's draft: the control has no `persist` | 14.2 |
@@ -1664,11 +1665,11 @@ The owner is a visitor here too. Signed in, they get the same private draft unle
1664
1665
 
1665
1666
  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
1667
 
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.13).
1668
+ 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
1669
  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
1670
  3. `plugins=visitor-drafts` on the loader tag, plus whatever else the app needs.
1670
1671
  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.12).
1672
+ 5. `data-id` on every record and `data-id=""` on every blueprint. New rows mint their own (`trigger-add` does it, 8.11).
1672
1673
  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
1674
  7. A feedback link the visitor can use: a `mailto:` address in the page, not in the plugin.
1674
1675
  8. Optional: a `<template visitor-intro>` (14.6). No template, no intro; the prompt still shows.
@@ -1725,7 +1726,7 @@ The rung is where a free document lives, so the details matter:
1725
1726
 
1726
1727
  ### 14.5 Restoring and redrawing
1727
1728
 
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.12).
1729
+ 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
1730
 
1730
1731
  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
1732
 
@@ -1765,7 +1766,7 @@ An element inside the template with `[visitor-intro-start]` scrolls to the app,
1765
1766
 
1766
1767
  ### 14.8 A complete example
1767
1768
 
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.12). 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
+ [`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
1770
 
1770
1771
  ```html
1771
1772
  <!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\")");