@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,299 @@
1
+ @import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;450;500;600&family=JetBrains+Mono:wght@400;500&display=swap");
2
+
3
+ /* Light 4: warm paper, compact Inter with small 14px section heads, amber selection and frosted faint chrome, from the Gemini Argon bake-off seat. */
4
+
5
+ @import url("./content.css");
6
+
7
+ :where(article[data-page], .page-shell) {
8
+ --chart-1: #8A6A2E;
9
+ --chart-2: #6d4e0b;
10
+ --chart-3: #6E6A62;
11
+ --chart-4: #2B2926;
12
+ --chart-5: #9B968C;
13
+ --chart-6: #3B3833;
14
+ --chart-text: #2B2926;
15
+ --page-bg: #FAF8F4;
16
+ --page-ink: #2B2926;
17
+ --page-ink-strong: #1E1D1A;
18
+ --page-ink-soft: #6E6A62;
19
+ --page-ink-faint: #9B968C;
20
+ --page-rule: rgba(31, 30, 27, 0.08);
21
+ --page-accent: #8A6A2E;
22
+ --page-link: #6d4e0b;
23
+ --page-caret: #2B2926;
24
+ --page-selection: rgba(196, 152, 74, 0.22);
25
+
26
+ --page-font: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
27
+ --page-font-display: var(--page-font);
28
+ --page-font-heading: var(--page-font);
29
+ --page-font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
30
+ --page-font-size: 16px;
31
+ --page-line-height: 1.68;
32
+ --page-measure: 586px;
33
+ --page-pad-top: 84px;
34
+ --page-h1-size: 27px;
35
+ --page-h1-weight: 600;
36
+ --page-h1-tracking: -0.022em;
37
+ --page-h1-line: 1.24;
38
+ --page-h2-size: 14px;
39
+ --page-h2-weight: 600;
40
+ --page-h3-size: 14px;
41
+ --page-h3-weight: 500;
42
+ --page-block-gap: 0.32em;
43
+ --page-indent: 1.375em;
44
+
45
+ --page-marker: #9B968C;
46
+ --page-check-size: 16px;
47
+ --page-check-radius: 4px;
48
+ --page-check-border: rgba(31, 30, 27, 0.28);
49
+ --page-check-fill: #3B3833;
50
+ --page-check-ink: #FAF8F4;
51
+ --page-done-ink: #6E6A62;
52
+ --page-toggle-ink: #6E6A62;
53
+ --page-callout-bg: rgba(31, 30, 27, 0.026);
54
+ --page-callout-line: rgba(31, 30, 27, 0.07);
55
+ --page-code-bg: rgba(31, 30, 27, 0.055);
56
+ --page-code-ink: #35322D;
57
+
58
+ --page-chrome-ink-rest: rgba(31, 30, 27, 0.24);
59
+ --page-chrome-ink-moved: rgba(31, 30, 27, 0.45);
60
+ --page-chrome-bg-rest: rgba(250, 248, 244, 0.78);
61
+ --page-chrome-bg-moved: rgba(250, 248, 244, 0.92);
62
+ --page-chrome-line-rest: rgba(31, 30, 27, 0.065);
63
+ --page-chrome-line-moved: rgba(31, 30, 27, 0.13);
64
+ --page-chrome-shadow-rest: 0 1px 2px rgba(31, 30, 27, 0.025);
65
+ --page-chrome-shadow-moved: 0 2px 6px rgba(31, 30, 27, 0.05);
66
+ --page-chrome-radius: 8px;
67
+ --page-plus-size: 24px;
68
+ --page-fade: 180ms;
69
+
70
+ --page-pop-bg: #F6F3EC;
71
+ --page-pop-ink: #262421;
72
+ --page-pop-muted: #78736A;
73
+ --page-pop-line: rgba(31, 30, 27, 0.12);
74
+ --page-pop-shadow: 0 10px 28px rgba(31, 30, 27, 0.09), 0 2px 6px rgba(31, 30, 27, 0.05);
75
+ --page-pop-hover: rgba(31, 30, 27, 0.065);
76
+ --page-pop-radius: 10px;
77
+ --page-bar-bg: #F6F3EC;
78
+ --page-bar-ink: #262421;
79
+ --page-scrim: rgba(24, 23, 20, 0.14);
80
+
81
+ -webkit-font-smoothing: antialiased;
82
+ -moz-osx-font-smoothing: grayscale;
83
+ }
84
+
85
+ article[data-page] figure[data-type="chart"] { --chart-1: #8A6A2E; --chart-2: #6d4e0b; --chart-3: #6E6A62; --chart-4: #2B2926; --chart-5: #9B968C; --chart-6: #3B3833; --chart-text: #2B2926; max-width: 100%; }
86
+ article[data-page] figure[data-type="chart"] > svg { display: block; width: 100%; height: auto; color: var(--chart-text); font: 12px Inter, sans-serif; }
87
+
88
+ :is(article[data-page], .page-shell .page-editor) {
89
+ letter-spacing: -0.006em;
90
+ }
91
+
92
+ :is(article[data-page], .page-shell .page-editor) > p {
93
+ margin-block-end: 0.62em;
94
+ }
95
+
96
+ :is(article[data-page], .page-shell .page-editor) > h2,
97
+ :is(article[data-page], .page-shell .page-editor) > h3 {
98
+ margin-block: 1.55em 0.4em;
99
+ line-height: 1.4;
100
+ letter-spacing: -0.008em;
101
+ }
102
+
103
+ :is(article[data-page], .page-shell .page-editor) > h1:first-child {
104
+ margin-block-end: 0.67em;
105
+ }
106
+
107
+ :is(article[data-page], .page-shell .page-editor) :not(pre) > code {
108
+ font-size: 0.86em;
109
+ padding: 1.5px 5px;
110
+ border-radius: 4px;
111
+ }
112
+
113
+ :is(article[data-page], .page-shell .page-editor) .page-li[data-kind="check"][data-checked="true"] {
114
+ text-decoration-color: var(--page-ink-faint);
115
+ }
116
+
117
+ :is(article[data-page], .page-shell .page-editor) .page-li[data-kind="check"]::before {
118
+ border-width: 1.25px;
119
+ }
120
+
121
+ :is(article[data-page], .page-shell .page-editor) .page-toggle {
122
+ padding-block: 7px;
123
+ padding-inline-end: 10px;
124
+ border: 1px solid rgba(31, 30, 27, 0.07);
125
+ border-radius: 7px;
126
+ background: rgba(31, 30, 27, 0.026);
127
+ font-size: 15px;
128
+ line-height: 1.45;
129
+ }
130
+
131
+ :is(article[data-page], .page-shell .page-editor) .page-toggle-button::before {
132
+ content: "";
133
+ display: inline-block;
134
+ width: 14px;
135
+ height: 14px;
136
+ background: var(--page-toggle-ink);
137
+ clip-path: polygon(30% 20%, 80% 50%, 30% 80%);
138
+ transform-origin: 50% 50%;
139
+ vertical-align: -0.05em;
140
+ }
141
+
142
+ :is(article[data-page], .page-shell .page-editor) .page-toggle[aria-expanded="true"] > .page-toggle-button::before {
143
+ transform: rotate(90deg);
144
+ }
145
+
146
+ :is(article[data-page], .page-shell .page-editor) aside.page-callout {
147
+ border-radius: 7px;
148
+ }
149
+
150
+ .page-shell .page-dock {
151
+ gap: 2px;
152
+ padding: 3px 6px;
153
+ }
154
+
155
+ .page-shell .page-dock .page-tool {
156
+ height: 24px;
157
+ padding: 0 6px;
158
+ border-radius: 5px;
159
+ font-size: 12px;
160
+ font-weight: 450;
161
+ letter-spacing: -0.005em;
162
+ }
163
+
164
+ .page-shell .page-tool svg {
165
+ width: 14px;
166
+ height: 14px;
167
+ }
168
+
169
+ .page-shell .page-tool-sep {
170
+ width: 1px;
171
+ height: 13px;
172
+ margin: 0 2px;
173
+ background: rgba(31, 30, 27, 0.055);
174
+ }
175
+
176
+ .page-shell.is-moved .page-tool-sep {
177
+ background: rgba(31, 30, 27, 0.10);
178
+ }
179
+
180
+ .page-shell .page-plus {
181
+ border-radius: 6px;
182
+ background: rgba(31, 30, 27, 0.022);
183
+ }
184
+
185
+ .page-shell.is-moved .page-plus {
186
+ background: rgba(31, 30, 27, 0.048);
187
+ }
188
+
189
+ .page-shell .page-plus svg {
190
+ width: 13px;
191
+ height: 13px;
192
+ }
193
+
194
+ .page-shell .page-bubble {
195
+ gap: 2px;
196
+ padding: 4px 5px;
197
+ border-radius: 8px;
198
+ }
199
+
200
+ .page-shell .page-bubble .page-tool {
201
+ height: 26px;
202
+ min-width: 26px;
203
+ padding: 0 6px;
204
+ border-radius: 5px;
205
+ font-size: 12.5px;
206
+ font-weight: 500;
207
+ }
208
+
209
+ .page-shell .page-menu {
210
+ width: 264px;
211
+ padding: 6px;
212
+ }
213
+
214
+ .page-shell .page-menu-title {
215
+ padding: 4px 8px 5px;
216
+ font-size: 11px;
217
+ font-weight: 500;
218
+ letter-spacing: 0.03em;
219
+ text-transform: uppercase;
220
+ }
221
+
222
+ .page-shell .page-menu-item {
223
+ gap: 10px;
224
+ min-height: 33px;
225
+ padding: 4px 8px;
226
+ border-radius: 6px;
227
+ font-size: 13.5px;
228
+ }
229
+
230
+ .page-shell .page-menu-icon {
231
+ width: 22px;
232
+ height: 22px;
233
+ border-radius: 5px;
234
+ background: rgba(31, 30, 27, 0.045);
235
+ border: 1px solid rgba(31, 30, 27, 0.075);
236
+ }
237
+
238
+ .page-shell .page-menu-label {
239
+ font-weight: 450;
240
+ line-height: 1.25;
241
+ }
242
+
243
+ .page-shell .page-menu-hint {
244
+ font-family: var(--page-font-mono);
245
+ font-size: 11px;
246
+ }
247
+
248
+ @media (max-width: 560px) {
249
+ :where(article[data-page], .page-shell) {
250
+ --page-font-size: 15.5px;
251
+ --page-line-height: 1.62;
252
+ --page-h1-size: 23px;
253
+ --page-pad-top: 68px;
254
+ }
255
+
256
+ .page-shell .page-bubble {
257
+ padding: 0 16px;
258
+ border-radius: 0;
259
+ box-shadow: 0 -4px 16px rgba(31, 30, 27, 0.06);
260
+ }
261
+
262
+ .page-shell .page-bubble .page-tool {
263
+ height: 34px;
264
+ min-width: 36px;
265
+ font-size: 13px;
266
+ }
267
+
268
+ .page-shell .page-menu {
269
+ width: auto;
270
+ padding: 8px 12px 12px;
271
+ border-radius: 16px 16px 0 0;
272
+ }
273
+
274
+ .page-shell .page-menu::before {
275
+ content: "";
276
+ display: block;
277
+ width: 36px;
278
+ height: 4px;
279
+ margin: 2px auto 8px;
280
+ border-radius: 2px;
281
+ background: var(--page-pop-line);
282
+ }
283
+
284
+ .page-shell .page-menu-item {
285
+ min-height: 45px;
286
+ padding: 6px 10px;
287
+ font-size: 14.5px;
288
+ }
289
+
290
+ .page-shell .page-menu-icon {
291
+ width: 26px;
292
+ height: 26px;
293
+ }
294
+
295
+ .page-shell .page-plus {
296
+ width: 28px;
297
+ height: 28px;
298
+ }
299
+ }
@@ -0,0 +1,25 @@
1
+ import { schema } from "./schema.js";
2
+ import { blockJson } from "./model.js";
3
+ import { project } from "./projection.js";
4
+ import { cellText } from "./rich/cell.js";
5
+
6
+ export function tablesFromDoc(doc) {
7
+ const tables = [];
8
+ let name = "";
9
+ doc.forEach((node) => {
10
+ if (node.type === schema.nodes.heading) name = node.textContent;
11
+ if (node.type !== schema.nodes.table) return;
12
+ const table = project({ [node.attrs.id]: blockJson(node) })[0];
13
+ const rows = table.rows.map((row) => ({ ...row, cells: Object.fromEntries(Object.entries(row.cells).map(([col, cell]) => [col, cellText(cell)])) }));
14
+ tables.push({ id: table.id, name: name || `Table ${tables.length + 1}`, cols: table.cols, rows });
15
+ });
16
+ return tables;
17
+ }
18
+
19
+ export function toolContext(doc, title) {
20
+ const outline = [];
21
+ doc.forEach((node) => {
22
+ if (node.type === schema.nodes.heading) outline.push({ level: node.attrs.level, text: node.textContent });
23
+ });
24
+ return { title, outline, tables: tablesFromDoc(doc) };
25
+ }
@@ -0,0 +1,68 @@
1
+ import { schema } from "./schema.js";
2
+ import { openPageMenu } from "./chrome.js";
3
+
4
+ export class ToolView {
5
+ constructor(node, view, getPos, { host, revise = null }) {
6
+ this.node = node;
7
+ this.view = view;
8
+ this.getPos = getPos;
9
+ this.host = host;
10
+ this.revise = revise;
11
+ this.showingFallback = false;
12
+ const doc = view.dom.ownerDocument;
13
+ this.dom = doc.createElement("section");
14
+ this.dom.className = "page-tool-block";
15
+ this.dom.dataset.type = "tool";
16
+ this.dom.dataset.id = node.attrs.id ?? "";
17
+ this.dom.contentEditable = "false";
18
+ this.content = doc.createElement("div");
19
+ this.content.className = "page-tool-content";
20
+ this.menu = doc.createElement("button");
21
+ this.menu.type = "button";
22
+ this.menu.className = "page-tool-settings";
23
+ this.menu.textContent = "Tool options";
24
+ this.menu.setAttribute("aria-label", "Tool options");
25
+ this.menu.addEventListener("click", () => this.openMenu());
26
+ this.dom.append(this.content, this.menu);
27
+ this.mount();
28
+ }
29
+
30
+ mount() {
31
+ this.instance?.dispose();
32
+ this.instance = null;
33
+ if (this.showingFallback) this.content.innerHTML = this.node.attrs.artifact?.fallbackHtml ?? "";
34
+ else this.instance = this.host.mount(this.content, {
35
+ id: this.node.attrs.id,
36
+ artifact: this.node.attrs.artifact,
37
+ rev: this.node.attrs.rev,
38
+ seed: this.node.attrs.state,
39
+ });
40
+ }
41
+
42
+ openMenu() {
43
+ const items = [];
44
+ if (this.revise) items.push({ label: "Revise…", run: () => this.revise(this.node.attrs.id, this.node.attrs.rev) });
45
+ items.push({ label: this.showingFallback ? "Show tool" : "Show fallback", run: () => {
46
+ this.showingFallback = !this.showingFallback;
47
+ this.mount();
48
+ } });
49
+ items.push({ label: "Delete", run: () => {
50
+ const pos = this.getPos();
51
+ if (pos == null || this.view.state.doc.nodeAt(pos)?.type !== schema.nodes.tool) return;
52
+ this.view.dispatch(this.view.state.tr.delete(pos, pos + this.node.nodeSize));
53
+ } });
54
+ openPageMenu(this.view, this.menu, items);
55
+ }
56
+
57
+ update(node) {
58
+ if (node.type !== schema.nodes.tool || node.attrs.id !== this.node.attrs.id) return false;
59
+ const changed = node.attrs.rev !== this.node.attrs.rev || JSON.stringify(node.attrs.artifact) !== JSON.stringify(this.node.attrs.artifact);
60
+ this.node = node;
61
+ if (changed) this.mount();
62
+ return true;
63
+ }
64
+
65
+ stopEvent(event) { return this.dom.contains(event.target); }
66
+ ignoreMutation() { return true; }
67
+ destroy() { this.instance?.dispose(); this.instance = null; }
68
+ }
@@ -0,0 +1,133 @@
1
+ import { mountTool, readerStateHandle, scopedStateHandle, validateArtifact } from "./rich/tool-runtime.js";
2
+
3
+ export function createToolHost({ store = null, mode, tables = () => [] }) {
4
+ const mounted = new Set();
5
+ const warned = new Set();
6
+ let disposed = false;
7
+ let frame = null;
8
+ let frameWindow = null;
9
+
10
+ const warn = (id, rev, error) => {
11
+ const key = `${id}:${rev}`;
12
+ if (warned.has(key)) return;
13
+ warned.add(key);
14
+ console.warn(`clayjs: tool ${id} revision ${rev} failed`, error);
15
+ };
16
+
17
+ const fail = (item, error) => {
18
+ warn(item.id, item.rev, error);
19
+ item.readerOff?.();
20
+ item.readerOff = null;
21
+ try { item.runtime?.dispose(); } catch (cleanupError) { warn(item.id, item.rev, cleanupError); }
22
+ item.runtime = null;
23
+ item.root.innerHTML = item.artifact?.fallbackHtml ?? "";
24
+ const note = item.root.ownerDocument.createElement("p");
25
+ note.setAttribute("data-tool-error", "");
26
+ note.textContent = "Tool could not start";
27
+ item.root.append(note);
28
+ };
29
+
30
+ const onChange = () => {
31
+ for (const item of mounted) {
32
+ if (item.disposed || mode === "view") continue;
33
+ const next = store?.data?.[item.id]?.[item.rev];
34
+ const signature = JSON.stringify(next);
35
+ if (signature === item.signature) continue;
36
+ item.signature = signature;
37
+ try {
38
+ if (!item.runtime) item.render();
39
+ else if (next !== undefined) item.runtime.notifyState();
40
+ else item.render();
41
+ } catch (error) { fail(item, error); }
42
+ }
43
+ };
44
+ const unsubscribe = store?.on?.("change", onChange) ?? (() => {});
45
+
46
+ const host = {
47
+ mount(root, { id, artifact, rev, seed = {} }) {
48
+ if (disposed) throw new Error("tool host is disposed");
49
+ const item = { root, id, artifact, rev, seed, runtime: null, readerOff: null, disposed: false, signature: undefined };
50
+ const note = (message) => {
51
+ const line = root.ownerDocument.createElement("p");
52
+ line.setAttribute("data-tool-note", "");
53
+ line.textContent = message;
54
+ root.append(line);
55
+ };
56
+ item.render = () => {
57
+ item.readerOff?.();
58
+ item.readerOff = null;
59
+ try { item.runtime?.dispose(); } catch (error) { warn(item.id, item.rev, error); }
60
+ item.runtime = null;
61
+ root.removeAttribute("data-tool-scope");
62
+ if (validateArtifact(item.artifact).ok === false) {
63
+ root.textContent = "Tool unavailable";
64
+ warn(item.id, item.rev, new Error("invalid artifact"));
65
+ return;
66
+ }
67
+ const current = store?.data?.[item.id]?.[item.rev];
68
+ item.signature = JSON.stringify(current);
69
+ if (mode === "edit" && store && current === undefined) {
70
+ root.innerHTML = item.artifact.fallbackHtml;
71
+ note("Waiting for this tool's state…");
72
+ return;
73
+ }
74
+ const state = mode === "edit" && store
75
+ ? scopedStateHandle(store, item.id, item.rev)
76
+ : readerStateHandle(current ?? item.seed ?? {});
77
+ item.runtime = mountTool({ root, artifact: item.artifact, mode, state, tables, onError: (error) => warn(item.id, item.rev, error) });
78
+ item.readerOff = state.on?.("change", () => {
79
+ try { item.runtime?.notifyState(); } catch (error) { fail(item, error); }
80
+ }) ?? null;
81
+ if (mode === "edit" && !store) note("This document can't save tool state");
82
+ };
83
+ try { item.render(); } catch (error) { fail(item, error); }
84
+ mounted.add(item);
85
+ return {
86
+ update({ artifact: nextArtifact, rev: nextRev }) {
87
+ if (item.disposed) return;
88
+ if (nextRev === item.rev && nextArtifact === item.artifact) return;
89
+ item.artifact = nextArtifact;
90
+ item.rev = nextRev;
91
+ try { item.render(); } catch (error) { fail(item, error); }
92
+ },
93
+ dispose() {
94
+ if (item.disposed) return;
95
+ item.disposed = true;
96
+ mounted.delete(item);
97
+ item.readerOff?.();
98
+ try { item.runtime?.dispose(); } catch (error) { warn(item.id, item.rev, error); }
99
+ item.runtime = null;
100
+ },
101
+ };
102
+ },
103
+ tablesChanged() {
104
+ if (disposed || frame !== null || !mounted.size) return;
105
+ const win = [...mounted][0]?.root.ownerDocument.defaultView ?? globalThis;
106
+ frameWindow = win;
107
+ const raf = win.requestAnimationFrame?.bind(win) ?? ((fn) => win.setTimeout(fn, 0));
108
+ frame = raf(() => {
109
+ frame = null;
110
+ for (const item of mounted) {
111
+ try { item.runtime?.notifyTables(); } catch (error) { fail(item, error); }
112
+ }
113
+ });
114
+ },
115
+ dispose() {
116
+ if (disposed) return;
117
+ disposed = true;
118
+ unsubscribe();
119
+ if (frame !== null) {
120
+ if (frameWindow.cancelAnimationFrame) frameWindow.cancelAnimationFrame(frame);
121
+ else frameWindow.clearTimeout(frame);
122
+ frame = null;
123
+ }
124
+ for (const item of [...mounted]) {
125
+ item.disposed = true;
126
+ item.readerOff?.();
127
+ try { item.runtime?.dispose(); } catch (error) { warn(item.id, item.rev, error); }
128
+ }
129
+ mounted.clear();
130
+ },
131
+ };
132
+ return host;
133
+ }
@@ -0,0 +1,136 @@
1
+ // View mode: readers see the saved article, and each newer file the host sends
2
+ // on the saved lane (every checkpoint is a save) is morphed into it. Live sync
3
+ // never touches a frozen element's contents, so this is the only writer here.
4
+ // No ProseMirror, no projection code: the writer's save already rendered it.
5
+ // A toggle the reader opened or closed keeps the reader's state across updates.
6
+ import { morph } from "../vendor/hyper-morph.vendor.js";
7
+ import { createToolHost } from "./tools.js";
8
+
9
+ const ARTICLE = "article[data-page]";
10
+ const TOOL = 'section[data-type="tool"][data-id]';
11
+
12
+ export function applySavedArticle(html, doc = document, readerOpen = new Map(), savedOpen = new Map(), preserveTools = new Set()) {
13
+ const live = doc.querySelector(ARTICLE);
14
+ if (!live || typeof html !== "string") return false;
15
+ const incoming = new doc.defaultView.DOMParser().parseFromString(html, "text/html").querySelector(ARTICLE);
16
+ if (!incoming) return false;
17
+ for (const details of incoming.querySelectorAll("details[data-id]")) {
18
+ const id = details.getAttribute("data-id");
19
+ savedOpen.set(id, details.hasAttribute("open"));
20
+ if (readerOpen.has(id)) details.toggleAttribute("open", readerOpen.get(id));
21
+ }
22
+ const rev = incoming.getAttribute("data-rev");
23
+ if (rev && rev === live.getAttribute("data-rev") && incoming.innerHTML === live.innerHTML) return false;
24
+ const kept = new Map();
25
+ for (const root of live.querySelectorAll(TOOL)) {
26
+ const id = root.getAttribute("data-id");
27
+ if (!preserveTools.has(id)) continue;
28
+ kept.set(id, root);
29
+ root.replaceWith(root.cloneNode(false));
30
+ }
31
+ // raw: the sync policy ignores everything under [clay~=freeze], which is all of it.
32
+ // Scripts never run: a tool's fallback is markup, and tools mount from the store.
33
+ morph(live, incoming.innerHTML, {
34
+ policy: "raw", morphStyle: "innerHTML", scripts: { handle: false },
35
+ key: (el) => el.getAttribute("data-id") || null,
36
+ });
37
+ for (const root of live.querySelectorAll(TOOL)) {
38
+ const original = kept.get(root.getAttribute("data-id"));
39
+ if (original) root.replaceWith(original);
40
+ }
41
+ if (rev) live.setAttribute("data-rev", rev);
42
+ return true;
43
+ }
44
+
45
+ // A toggle counts as the reader's own when its state differs from what the
46
+ // last saved article said: clicks, find-in-page and keyboard all fire `toggle`,
47
+ // and a toggle the morph caused matches the saved state.
48
+ export function startPageView(doc = document) {
49
+ const readerOpen = new Map();
50
+ const savedOpen = new Map();
51
+ const mounted = new Map();
52
+ const toolStore = { data: {} };
53
+ const host = createToolHost({ store: toolStore, mode: "view" });
54
+ let stopped = false;
55
+ let sequence = 0;
56
+ for (const details of doc.querySelectorAll(`${ARTICLE} details[data-id]`)) savedOpen.set(details.getAttribute("data-id"), details.hasAttribute("open"));
57
+ const onToggle = (event) => {
58
+ const details = event.target;
59
+ if (details.tagName !== "DETAILS" || !details.closest(ARTICLE)) return;
60
+ const id = details.getAttribute("data-id");
61
+ if (!id) return;
62
+ if (details.open === savedOpen.get(id)) readerOpen.delete(id);
63
+ else readerOpen.set(id, details.open);
64
+ };
65
+ const toolsIn = async (source) => {
66
+ if (!source.querySelector(`${ARTICLE} ${TOOL}`)) return null;
67
+ const blocks = {};
68
+ for (const root of source.querySelectorAll(`${ARTICLE} ${TOOL}`)) {
69
+ const data = root.querySelector(':scope > script[type="application/json"][data-tool]');
70
+ let tool = {};
71
+ try { tool = data ? JSON.parse(data.textContent) : {}; } catch {}
72
+ blocks[root.getAttribute("data-id")] = { t: "tool", artifact: tool.artifact ?? null, rev: tool.rev ?? null, state: tool.state ?? {} };
73
+ }
74
+ const stateTag = source.querySelector('script[type="application/json"][merge="page.tools"]');
75
+ const state = stateTag ? JSON.parse(stateTag.textContent) : {};
76
+ return { blocks, state };
77
+ };
78
+ const syncTools = (decoded) => {
79
+ toolStore.data = decoded?.state ?? {};
80
+ const seen = new Set();
81
+ for (const root of doc.querySelectorAll(`${ARTICLE} ${TOOL}`)) {
82
+ const id = root.getAttribute("data-id");
83
+ seen.add(id);
84
+ const block = decoded?.blocks?.[id];
85
+ const old = mounted.get(id);
86
+ if (!block || block.t !== "tool" || !block.artifact || !block.rev) {
87
+ old?.instance.dispose();
88
+ mounted.delete(id);
89
+ continue;
90
+ }
91
+ if (old && old.root === root && old.rev === block.rev && JSON.stringify(old.artifact) === JSON.stringify(block.artifact)) continue;
92
+ old?.instance.dispose();
93
+ mounted.set(id, { root, rev: block.rev, artifact: block.artifact, instance: host.mount(root, {
94
+ id, artifact: block.artifact, rev: block.rev, seed: block.state,
95
+ }) });
96
+ }
97
+ for (const [id, old] of mounted) if (!seen.has(id)) { old.instance.dispose(); mounted.delete(id); }
98
+ };
99
+ const onFrame = (event) => {
100
+ const html = event.detail?.html;
101
+ if (typeof html !== "string") return;
102
+ const source = new doc.defaultView.DOMParser().parseFromString(html, "text/html");
103
+ const turn = ++sequence;
104
+ const incomingIds = new Set([...source.querySelectorAll(`${ARTICLE} ${TOOL}`)].map((el) => el.getAttribute("data-id")));
105
+ const preserve = new Set([...mounted.keys()].filter((id) => incomingIds.has(id)));
106
+ for (const [id, old] of mounted) if (!incomingIds.has(id)) { old.instance.dispose(); mounted.delete(id); }
107
+ applySavedArticle(html, doc, readerOpen, savedOpen, preserve);
108
+ if (!incomingIds.size) {
109
+ syncTools(null);
110
+ return;
111
+ }
112
+ toolsIn(source).then((decoded) => {
113
+ if (stopped || turn !== sequence) return;
114
+ syncTools(decoded);
115
+ }).catch((error) => {
116
+ if (stopped || turn !== sequence) return;
117
+ console.warn("clayjs: could not decode saved tools", error);
118
+ applySavedArticle(html, doc, readerOpen, savedOpen);
119
+ syncTools(null);
120
+ });
121
+ };
122
+ doc.addEventListener("toggle", onToggle, true);
123
+ doc.addEventListener("clay:sync-applied", onFrame);
124
+ if (doc.querySelector(`${ARTICLE} ${TOOL}`)) {
125
+ const turn = ++sequence;
126
+ toolsIn(doc).then((decoded) => { if (!stopped && turn === sequence) syncTools(decoded); })
127
+ .catch((error) => { if (!stopped) console.warn("clayjs: could not decode saved tools", error); });
128
+ }
129
+ return () => {
130
+ stopped = true;
131
+ sequence++;
132
+ host.dispose();
133
+ doc.removeEventListener("toggle", onToggle, true);
134
+ doc.removeEventListener("clay:sync-applied", onFrame);
135
+ };
136
+ }
@@ -0,0 +1,25 @@
1
+ const POINTER = "clay.page.author";
2
+ const OWNED = "clay.page.author.owned.";
3
+
4
+ const mint = () => globalThis.crypto?.randomUUID?.() ?? `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
5
+
6
+ export function authorIdentity(storage = globalThis.localStorage) {
7
+ try {
8
+ const owned = [];
9
+ for (let i = 0; i < storage.length; i++) {
10
+ const key = storage.key(i);
11
+ if (key && key.startsWith(OWNED)) owned.push(key.slice(OWNED.length));
12
+ }
13
+ let token = storage.getItem(POINTER);
14
+ if (!token || !owned.includes(token)) {
15
+ token = mint();
16
+ storage.setItem(OWNED + token, "1");
17
+ storage.setItem(POINTER, token);
18
+ owned.push(token);
19
+ }
20
+ return { token, owned: owned.sort() };
21
+ } catch {
22
+ const token = mint();
23
+ return { token, owned: [token] };
24
+ }
25
+ }