@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.
- package/THIRD-PARTY-NOTICES.md +19 -0
- package/dist/clay.standalone.js +69667 -41708
- package/dist/server/checkpoint.cjs +70690 -0
- package/dist/server/checkpoint.mjs +8 -0
- package/dist/server/rooms.cjs +72039 -0
- package/dist/server/rooms.mjs +66 -0
- package/docs/guide.md +94 -93
- package/entries/clay.js +22 -0
- package/package.json +16 -2
- package/packed-contract.json +17 -3
- package/src/core/conflict-download.js +11 -2
- package/src/core/host-meta.js +75 -0
- package/src/core/persist.js +11 -0
- package/src/core/save-core.js +266 -31
- package/src/core/save.js +62 -32
- package/src/core/snapshot.js +39 -13
- package/src/core/style-comparisons.js +37 -0
- package/src/dom/insert-style-tag.js +11 -12
- package/src/lib/effect.js +130 -0
- package/src/lib/style-adoption.js +9 -0
- package/src/lib/style.js +44 -0
- package/src/live/fast-lane.js +347 -60
- package/src/live/local-lane.js +32 -6
- package/src/live/ops.js +77 -0
- package/src/live/provenance.js +129 -0
- package/src/live/save-history.js +188 -0
- package/src/live/server-lane.js +9 -2
- package/src/live/store.js +194 -16
- package/src/loader-logic.js +7 -3
- package/src/loader.js +6 -0
- package/src/page/address.js +20 -0
- package/src/page/adopt-boot.js +189 -0
- package/src/page/adopt.js +240 -0
- package/src/page/ai-tool.js +188 -0
- package/src/page/announce.js +21 -0
- package/src/page/blocks.js +1121 -0
- package/src/page/blocksel.js +269 -0
- package/src/page/board-drag.js +226 -0
- package/src/page/board-panel.js +353 -0
- package/src/page/board-style.js +146 -0
- package/src/page/board-view.js +447 -0
- package/src/page/board.js +41 -0
- package/src/page/chart.js +220 -0
- package/src/page/checklist.js +143 -0
- package/src/page/chrome.js +587 -0
- package/src/page/csv-import.js +169 -0
- package/src/page/drag.js +202 -0
- package/src/page/drop.js +112 -0
- package/src/page/editor.js +384 -0
- package/src/page/highlight.js +268 -0
- package/src/page/inline.js +60 -1
- package/src/page/items.js +159 -0
- package/src/page/link.js +247 -0
- package/src/page/markdown.js +71 -0
- package/src/page/media.js +570 -0
- package/src/page/merge.js +130 -0
- package/src/page/model.js +71 -9
- package/src/page/placeholder.js +23 -0
- package/src/page/projection.js +647 -0
- package/src/page/recover.js +193 -0
- package/src/page/rich/cell.js +16 -0
- package/src/page/rich/chart.js +104 -0
- package/src/page/rich/csv.js +118 -0
- package/src/page/rich/page-tool.js +86 -0
- package/src/page/rich/tool-runtime.js +195 -0
- package/src/page/save-projection.js +53 -0
- package/src/page/save-states.js +85 -0
- package/src/page/schema.js +12 -5
- package/src/page/table-kinds.js +20 -0
- package/src/page/table-paste.js +57 -0
- package/src/page/table-select.js +244 -0
- package/src/page/table-style.js +25 -0
- package/src/page/table.js +650 -0
- package/src/page/themes/content.css +46 -0
- package/src/page/themes/light-1.css +269 -0
- package/src/page/themes/light-2.css +211 -0
- package/src/page/themes/light-3.css +130 -0
- package/src/page/themes/light-4.css +299 -0
- package/src/page/tool-data.js +25 -0
- package/src/page/tool-view.js +68 -0
- package/src/page/tools.js +133 -0
- package/src/page/view.js +136 -0
- package/src/page/ybind/author.js +25 -0
- package/src/page/ybind/binding.js +92 -0
- package/src/page/ybind/board-commands.js +170 -0
- package/src/page/ybind/board.js +89 -0
- package/src/page/ybind/bridge.js +382 -0
- package/src/page/ybind/conversion.js +26 -0
- package/src/page/ybind/core.js +7 -0
- package/src/page/ybind/decode.js +208 -0
- package/src/page/ybind/doc.js +58 -0
- package/src/page/ybind/external.js +190 -0
- package/src/page/ybind/group.js +124 -0
- package/src/page/ybind/index.js +12 -0
- package/src/page/ybind/layout.js +27 -0
- package/src/page/ybind/marks.js +18 -0
- package/src/page/ybind/migrate.js +152 -0
- package/src/page/ybind/moves.js +288 -0
- package/src/page/ybind/origins.js +257 -0
- package/src/page/ybind/parts.js +44 -0
- package/src/page/ybind/projection-map.js +92 -0
- package/src/page/ybind/provider.js +285 -0
- package/src/page/ybind/recovery.js +411 -0
- package/src/page/ybind/seed.js +27 -0
- package/src/page/ybind/table.js +166 -0
- package/src/page/ybind/tokens.js +93 -0
- package/src/page/ybind/walk.js +333 -0
- package/src/page/ybind/writer.js +307 -0
- package/src/plugins/indicator.js +16 -6
- package/src/plugins/live.js +742 -65
- package/src/plugins/page.js +547 -0
- package/src/plugins/undo.js +60 -3
- package/src/server/checkpoint/index.js +218 -0
- package/src/server/rooms/index.js +59 -0
- package/src/server/rooms/live-data-ops.js +369 -0
- package/src/server/rooms/live-data-rooms.js +965 -0
- package/src/server/rooms/live-data-tags.js +49 -0
- package/src/server/rooms/live-page-history.js +41 -0
- package/src/server/rooms/live-provenance.js +142 -0
- package/src/server/rooms/page-admission.js +79 -0
- package/src/server/rooms/page-conversion.js +62 -0
- package/src/server/rooms/page-rebase.js +108 -0
- package/src/server/rooms/page-state.js +91 -0
- package/src/server/rooms/page-write-admission.js +63 -0
- package/src/server/rooms/page-write-guard.js +23 -0
- package/src/server/rooms/recent.js +18 -0
- package/src/server/rooms/spec-wire.js +17 -0
- package/src/standalone.js +22 -0
- package/src/sync/live-sync.js +13 -2
- package/src/sync/stream.js +62 -16
- package/src/ui/index.js +4 -0
- package/src/ui/toast.js +21 -5
- package/src/vendor/pierre/pierre.vendor.js +3 -0
- package/src/vendor/yjs.vendor.js +7 -0
- package/src/page/binding.js +0 -647
- 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
|
+
}
|
package/src/page/view.js
ADDED
|
@@ -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
|
+
}
|