@panphora/clayjs 1.13.0 → 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 (137) hide show
  1. package/THIRD-PARTY-NOTICES.md +19 -0
  2. package/dist/clay.standalone.js +69739 -41784
  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 +95 -94
  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/plugins/visitor-drafts/prompt.js +4 -6
  114. package/src/server/checkpoint/index.js +218 -0
  115. package/src/server/rooms/index.js +59 -0
  116. package/src/server/rooms/live-data-ops.js +369 -0
  117. package/src/server/rooms/live-data-rooms.js +965 -0
  118. package/src/server/rooms/live-data-tags.js +49 -0
  119. package/src/server/rooms/live-page-history.js +41 -0
  120. package/src/server/rooms/live-provenance.js +142 -0
  121. package/src/server/rooms/page-admission.js +79 -0
  122. package/src/server/rooms/page-conversion.js +62 -0
  123. package/src/server/rooms/page-rebase.js +108 -0
  124. package/src/server/rooms/page-state.js +91 -0
  125. package/src/server/rooms/page-write-admission.js +63 -0
  126. package/src/server/rooms/page-write-guard.js +23 -0
  127. package/src/server/rooms/recent.js +18 -0
  128. package/src/server/rooms/spec-wire.js +17 -0
  129. package/src/standalone.js +22 -0
  130. package/src/sync/live-sync.js +13 -2
  131. package/src/sync/stream.js +62 -16
  132. package/src/ui/index.js +4 -0
  133. package/src/ui/toast.js +21 -5
  134. package/src/vendor/pierre/pierre.vendor.js +3 -0
  135. package/src/vendor/yjs.vendor.js +7 -0
  136. package/src/page/binding.js +0 -647
  137. package/src/page/replay.js +0 -578
@@ -0,0 +1,269 @@
1
+ // Block selection: Esc, the grip and Shift+click select whole blocks, and every
2
+ // block command (delete, duplicate, move, indent, copy) acts on "the current
3
+ // span", the block selection or the blocks a text selection touches, lifted to
4
+ // whole sibling subtrees. BlockSelection itself lives in blocks.js because
5
+ // moveSpanTr needs it.
6
+ import { state, history, view as pmView, keymap } from "../vendor/prosemirror.vendor.js";
7
+ import { schema } from "./schema.js";
8
+ import { outline, siblingSpan, touchedBlocks, BlockSelection, STEP } from "./blocks.js";
9
+ import { keysBetween, newId, orderedIds, REMOTE } from "./model.js";
10
+ import { announce } from "./announce.js";
11
+
12
+ const { Selection, TextSelection, NodeSelection, Plugin, PluginKey } = state;
13
+ const N = schema.nodes;
14
+
15
+ export const asStep = (tr) => history.closeHistory(tr).setMeta(STEP, true);
16
+
17
+ export function selectBlocks(doc, a, b = a, ents = outline(doc)) {
18
+ const { lo, hi } = siblingSpan(ents, Math.min(a, b), Math.max(a, b));
19
+ const from = ents[lo].at, to = ents[hi].end;
20
+ return a <= b ? new BlockSelection(doc.resolve(from), doc.resolve(to)) : new BlockSelection(doc.resolve(to), doc.resolve(from));
21
+ }
22
+
23
+ export function currentSpan(st, ents = outline(st.doc)) {
24
+ const [a, b] = touchedBlocks(st.doc, st.selection, ents);
25
+ return siblingSpan(ents, a, b);
26
+ }
27
+
28
+ const visibleIdx = (ents, i, dir) => {
29
+ for (let j = i; j >= 0 && j < ents.length; j += dir) if (!ents[j].hidden) return j;
30
+ return -1;
31
+ };
32
+
33
+ export function selectCurrentBlock(st, dispatch) {
34
+ if (st.selection instanceof BlockSelection) return false;
35
+ const i = Math.min(st.selection.$head.index(0), st.doc.childCount - 1);
36
+ if (dispatch) dispatch(st.tr.setSelection(selectBlocks(st.doc, i)));
37
+ return true;
38
+ }
39
+
40
+ export const stepBlock = (dir) => (st, dispatch) => {
41
+ const sel = st.selection;
42
+ if (!(sel instanceof BlockSelection)) return false;
43
+ const ents = outline(st.doc);
44
+ const { lo, hi } = sel.span(st.doc);
45
+ const i = dir < 0 ? visibleIdx(ents, lo - 1, -1) : visibleIdx(ents, hi + 1, 1);
46
+ if (i < 0) return true;
47
+ if (dispatch) dispatch(st.tr.setSelection(selectBlocks(st.doc, i)).scrollIntoView());
48
+ return true;
49
+ };
50
+
51
+ function rootAt(ents, lo, hi, side) {
52
+ if (side < 0) return lo;
53
+ let r = lo;
54
+ while (ents[r].subEnd < hi) r = ents[r].subEnd + 1;
55
+ return r;
56
+ }
57
+
58
+ function prevRoot(ents, i) {
59
+ for (let j = i - 1; j >= 0; j--) if (ents[j].up === ents[i].up) return j;
60
+ return i;
61
+ }
62
+
63
+ export const extendBlock = (dir) => (st, dispatch) => {
64
+ const sel = st.selection;
65
+ if (!(sel instanceof BlockSelection)) return false;
66
+ const ents = outline(st.doc);
67
+ const { lo, hi } = sel.span(st.doc);
68
+ const fwd = sel.anchor <= sel.head;
69
+ const anchorIdx = fwd ? lo : rootAt(ents, lo, hi, 1);
70
+ let headIdx = fwd ? rootAt(ents, lo, hi, 1) : lo;
71
+ if (dir > 0) {
72
+ if (fwd || lo === hi || headIdx === anchorIdx) headIdx = visibleIdx(ents, (fwd ? hi : headIdx) + 1, 1);
73
+ else headIdx = visibleIdx(ents, ents[headIdx].subEnd + 1, 1);
74
+ } else {
75
+ if (!fwd || headIdx === anchorIdx) headIdx = visibleIdx(ents, (fwd ? headIdx : lo) - 1, -1);
76
+ else headIdx = prevRoot(ents, headIdx);
77
+ }
78
+ if (headIdx < 0) return true;
79
+ if (dispatch) dispatch(st.tr.setSelection(selectBlocks(st.doc, anchorIdx, headIdx, ents)).scrollIntoView());
80
+ return true;
81
+ };
82
+
83
+ export function deleteBlocks(st, dispatch) {
84
+ const sel = st.selection;
85
+ if (!(sel instanceof BlockSelection)) return false;
86
+ if (dispatch) {
87
+ const tr = st.tr.delete(sel.from, sel.to);
88
+ if (tr.doc.content.size === 0) tr.insert(0, N.paragraph.create());
89
+ tr.setSelection(Selection.near(tr.doc.resolve(Math.min(sel.from, tr.doc.content.size)), -1));
90
+ dispatch(asStep(tr).scrollIntoView());
91
+ }
92
+ return true;
93
+ }
94
+
95
+ function freshBoard({ columns = {}, cards = {} }) {
96
+ const order = orderedIds(columns);
97
+ const ids = new Map(order.map((id) => [id, newId("c")]));
98
+ const columnKeys = keysBetween(null, null, order.length);
99
+ const nextCards = {};
100
+ for (const col of new Set([...order, ...Object.values(cards).map((card) => card.col)])) {
101
+ const inColumn = orderedIds(Object.fromEntries(Object.entries(cards).filter(([, card]) => card.col === col)));
102
+ const keys = keysBetween(null, null, inColumn.length);
103
+ inColumn.forEach((id, i) => { nextCards[newId("k")] = { ...cards[id], col: ids.get(col) ?? col, pos: keys[i] }; });
104
+ }
105
+ return { columns: Object.fromEntries(order.map((id, i) => [ids.get(id), { ...columns[id], pos: columnKeys[i] }])), cards: nextCards };
106
+ }
107
+
108
+ export function duplicateBlocks(st, dispatch) {
109
+ const ents = outline(st.doc);
110
+ if (!ents.length) return false;
111
+ const { lo, hi } = currentSpan(st, ents);
112
+ if (dispatch) {
113
+ const ids = new Map();
114
+ for (let k = lo; k <= hi; k++) if (ents[k].node.type === N.toggle && ents[k].node.attrs.id) ids.set(ents[k].node.attrs.id, newId());
115
+ const nodes = [];
116
+ for (let k = lo; k <= hi; k++) {
117
+ const n = ents[k].node, p = n.attrs.parent ?? null;
118
+ nodes.push(n.type.create({ ...n.attrs, ...(n.type === N.board ? freshBoard(n.attrs) : {}), id: ids.get(n.attrs.id) ?? null, pos: null, parent: ids.get(p) ?? p }, n.content, n.marks));
119
+ }
120
+ const at = ents[hi].end, size = at - ents[lo].at;
121
+ const tr = st.tr.insert(at, nodes);
122
+ const s = st.selection;
123
+ tr.setSelection(s instanceof BlockSelection
124
+ ? new BlockSelection(tr.doc.resolve(at), tr.doc.resolve(at + size))
125
+ : TextSelection.create(tr.doc, s.anchor + size, s.head + size));
126
+ dispatch(asStep(tr).scrollIntoView());
127
+ }
128
+ return true;
129
+ }
130
+
131
+ const endOfFirstText = (doc, ents, lo, hi) => {
132
+ for (let k = lo; k <= hi; k++) if (ents[k].node.isTextblock) return ents[k].end - 1;
133
+ return -1;
134
+ };
135
+
136
+ export function exitToText(st, dispatch) {
137
+ const sel = st.selection;
138
+ if (!(sel instanceof BlockSelection)) return false;
139
+ const ents = outline(st.doc);
140
+ const { lo, hi } = sel.span(st.doc);
141
+ if (dispatch) {
142
+ const at = endOfFirstText(st.doc, ents, lo, hi);
143
+ dispatch(st.tr.setSelection(at >= 0 ? TextSelection.create(st.doc, at) : NodeSelection.create(st.doc, ents[lo].at)));
144
+ }
145
+ return true;
146
+ }
147
+
148
+ export function selectAllBlocks(st, dispatch) {
149
+ const { $from, $to } = st.selection;
150
+ const one = $from.depth === 1 && $from.sameParent($to);
151
+ const whole = one && $from.parentOffset === 0 && $to.parentOffset === $to.parent.content.size;
152
+ if (one && !whole && $from.parent.content.size) {
153
+ if (dispatch) dispatch(st.tr.setSelection(TextSelection.create(st.doc, $from.start(), $from.end())));
154
+ return true;
155
+ }
156
+ if (dispatch) dispatch(st.tr.setSelection(new BlockSelection(st.doc.resolve(0), st.doc.resolve(st.doc.content.size))));
157
+ return true;
158
+ }
159
+
160
+ export const typeIntoBlocks = (view, text) => {
161
+ const st = view.state, sel = st.selection;
162
+ if (!(sel instanceof BlockSelection)) return false;
163
+ const ents = outline(st.doc);
164
+ const { lo, hi } = sel.span(st.doc);
165
+ const at = endOfFirstText(st.doc, ents, lo, hi);
166
+ const tr = st.tr;
167
+ if (at < 0) {
168
+ const to = ents[hi].end;
169
+ tr.insert(to, N.paragraph.create({ parent: ents[lo].node.attrs.parent ?? null }));
170
+ tr.setSelection(TextSelection.create(tr.doc, to + 1));
171
+ asStep(tr);
172
+ } else tr.setSelection(TextSelection.create(tr.doc, at));
173
+ tr.insertText(text);
174
+ view.dispatch(tr.scrollIntoView());
175
+ return true;
176
+ };
177
+
178
+ const leaveSummaryDown = (st, dispatch, view) => {
179
+ const sel = st.selection;
180
+ if (!(sel instanceof TextSelection) || !view?.endOfTextblock("down")) return false;
181
+ const { $anchor: $a, $head: $h } = sel;
182
+ if ($a.depth !== 1 || $a.parent.type !== N.toggle || $a.parent.attrs.open || $a.index(0) !== $h.index(0)) return false;
183
+ const ents = outline(st.doc);
184
+ const i = $a.index(0);
185
+ const j = visibleIdx(ents, ents[i].subEnd + 1, 1);
186
+ if (j < 0) return false;
187
+ if (dispatch) dispatch(st.tr.setSelection(selectBlocks(st.doc, i, j, ents)).scrollIntoView());
188
+ return true;
189
+ };
190
+
191
+ const only = (cmd) => (st, d, v) => st.selection instanceof BlockSelection && cmd(st, d, v);
192
+
193
+ export const blockSelKeys = {
194
+ Escape: (st, d) => exitToText(st, d) || selectCurrentBlock(st, d),
195
+ Enter: only(exitToText),
196
+ ArrowUp: stepBlock(-1),
197
+ ArrowDown: stepBlock(1),
198
+ "Shift-ArrowUp": extendBlock(-1),
199
+ "Shift-ArrowDown": (st, d, v) => extendBlock(1)(st, d) || leaveSummaryDown(st, d, v),
200
+ Backspace: deleteBlocks,
201
+ "Alt-Backspace": deleteBlocks,
202
+ "Ctrl-h": deleteBlocks,
203
+ Delete: deleteBlocks,
204
+ "Mod-d": duplicateBlocks,
205
+ "Mod-a": selectAllBlocks,
206
+ };
207
+
208
+ export const blockSelKey = new PluginKey("page-blocksel");
209
+
210
+ const countOf = (st) => {
211
+ const sel = st.selection;
212
+ const { lo, hi } = sel.span(st.doc);
213
+ return outline(st.doc).filter((e) => e.i >= lo && e.i <= hi && !e.hidden).length;
214
+ };
215
+
216
+ export function blockSelectionPlugin() {
217
+ const keys = keymap.keymap(blockSelKeys);
218
+ return new Plugin({
219
+ key: blockSelKey,
220
+ state: {
221
+ init: () => 0,
222
+ apply: (tr, notice, prev, next) => (tr.getMeta(REMOTE) && prev.selection instanceof BlockSelection && !(next.selection instanceof BlockSelection) ? notice + 1 : notice),
223
+ },
224
+ props: {
225
+ handleKeyDown: (view, event) => keys.props.handleKeyDown(view, event),
226
+ handleTextInput: (view, from, to, text) => typeIntoBlocks(view, text),
227
+ decorations(st) {
228
+ const sel = st.selection;
229
+ if (!(sel instanceof BlockSelection)) return null;
230
+ const decos = [];
231
+ st.doc.nodesBetween(sel.from, sel.to, (n, pos) => {
232
+ decos.push(pmView.Decoration.node(pos, pos + n.nodeSize, { class: "page-block-selected" }));
233
+ return false;
234
+ });
235
+ return pmView.DecorationSet.create(st.doc, decos);
236
+ },
237
+ createSelectionBetween(view, $anchor, $head) {
238
+ const sel = view.state.selection;
239
+ return sel instanceof BlockSelection && $anchor.pos === sel.anchor && $head.pos === sel.head ? sel : null;
240
+ },
241
+ handleDOMEvents: {
242
+ mousedown(view, event) {
243
+ const sel = view.state.selection;
244
+ if (!event.shiftKey || !(sel instanceof BlockSelection) || event.button !== 0) return false;
245
+ const hit = view.posAtCoords({ left: event.clientX, top: event.clientY });
246
+ if (!hit) return false;
247
+ const ents = outline(view.state.doc);
248
+ const j = Math.min(view.state.doc.resolve(hit.inside >= 0 ? hit.inside : hit.pos).index(0), ents.length - 1);
249
+ const { lo, hi } = sel.span(view.state.doc);
250
+ const anchorIdx = sel.anchor <= sel.head ? lo : rootAt(ents, lo, hi, 1);
251
+ event.preventDefault();
252
+ view.dispatch(view.state.tr.setSelection(selectBlocks(view.state.doc, anchorIdx, j, ents)));
253
+ return true;
254
+ },
255
+ },
256
+ },
257
+ view: () => ({
258
+ update(view, prev) {
259
+ const a = view.state.selection, b = prev.selection;
260
+ if (a instanceof BlockSelection && !a.eq(b)) {
261
+ const n = countOf(view.state);
262
+ announce(view, `${n} ${n === 1 ? "block" : "blocks"} selected`);
263
+ } else if (b instanceof BlockSelection && !(a instanceof BlockSelection)) {
264
+ announce(view, blockSelKey.getState(view.state) !== blockSelKey.getState(prev) ? "Block selection cleared because the document changed" : "Selection cleared");
265
+ }
266
+ },
267
+ }),
268
+ });
269
+ }
@@ -0,0 +1,226 @@
1
+ import { announce } from "./announce.js";
2
+ import { scrollParent } from "./drag.js";
3
+
4
+ const SLOP = 4;
5
+ const EDGE = 56;
6
+ const MAX_SPEED = 18;
7
+ const STRIP = ["id", "data-focus", "data-action", "data-card", "data-col"];
8
+
9
+ const middle = (node, axis) => {
10
+ const r = node.getBoundingClientRect();
11
+ return axis === "x" ? r.left + r.width / 2 : r.top + r.height / 2;
12
+ };
13
+ const speed = (at, lo, hi) => (at < lo + EDGE ? -MAX_SPEED * Math.min(1, (lo + EDGE - at) / EDGE) : at > hi - EDGE ? MAX_SPEED * Math.min(1, (at - (hi - EDGE)) / EDGE) : 0);
14
+ const same = (a, b) => a?.col === b?.col && a?.afterId === b?.afterId && !a === !b;
15
+
16
+ export class BoardDrag {
17
+ constructor(host) {
18
+ this.host = host;
19
+ this.state = null;
20
+ this.line = null;
21
+ this.suppress = false;
22
+ this.onMove = (e) => this.move(e);
23
+ this.onUp = (e) => this.end(e);
24
+ this.onCancel = (e) => { if (e.pointerId === this.state?.pointer) this.cancel(); };
25
+ this.onKey = (e) => {
26
+ if (e.key === "Escape" && this.state?.active) {
27
+ e.preventDefault();
28
+ e.stopPropagation();
29
+ this.cancel();
30
+ }
31
+ };
32
+ host.dom.addEventListener("pointerdown", (e) => this.start(e));
33
+ host.dom.addEventListener("click", (e) => {
34
+ if (!this.suppress) return;
35
+ this.suppress = false;
36
+ e.preventDefault();
37
+ e.stopImmediatePropagation();
38
+ }, true);
39
+ }
40
+
41
+ start(event) {
42
+ const host = this.host;
43
+ if (event.button !== 0 || event.pointerType === "touch" || this.state || !host.commands || !host.view.editable) return;
44
+ const card = event.target.closest?.(".page-board-card[data-card]");
45
+ const head = card ? null : event.target.closest?.(".page-board-head[data-col]");
46
+ if (!card && (!head || event.target.closest("button, input"))) return;
47
+ this.state = {
48
+ kind: card ? "card" : "column", id: card ? card.dataset.card : head.dataset.col, pointer: event.pointerId,
49
+ x0: event.clientX, y0: event.clientY, x: event.clientX, y: event.clientY, active: false, slot: null, raf: 0, ghost: null,
50
+ };
51
+ const doc = host.dom.ownerDocument;
52
+ doc.addEventListener("pointermove", this.onMove);
53
+ doc.addEventListener("pointerup", this.onUp);
54
+ doc.addEventListener("pointercancel", this.onCancel);
55
+ }
56
+
57
+ move(event) {
58
+ const s = this.state;
59
+ if (!s || event.pointerId !== s.pointer) return;
60
+ s.x = event.clientX;
61
+ s.y = event.clientY;
62
+ if (!s.active) {
63
+ if (Math.hypot(s.x - s.x0, s.y - s.y0) <= SLOP) return;
64
+ this.begin();
65
+ }
66
+ event.preventDefault();
67
+ this.track();
68
+ }
69
+
70
+ source() {
71
+ const { kind, id } = this.state;
72
+ const nodes = kind === "card" ? this.host.dom.querySelectorAll(".page-board-card[data-card]") : this.host.lanes.querySelectorAll(".page-board-lane[data-col]");
73
+ return [...nodes].find((node) => (kind === "card" ? node.dataset.card : node.dataset.col) === id) ?? null;
74
+ }
75
+
76
+ exists() {
77
+ const { kind, id } = this.state, board = this.host.board;
78
+ return kind === "card" ? Object.hasOwn(board.cards, id) : board.columnIds.includes(id);
79
+ }
80
+
81
+ begin() {
82
+ const s = this.state, host = this.host;
83
+ const source = this.source();
84
+ if (!source || !this.exists()) return this.stop();
85
+ if (host.menu !== null) host.closeMenu(false);
86
+ s.active = true;
87
+ const r = source.getBoundingClientRect();
88
+ s.dx = s.x0 - r.left;
89
+ s.dy = s.y0 - r.top;
90
+ const ghost = source.cloneNode(true);
91
+ for (const node of [ghost, ...ghost.querySelectorAll("*")]) for (const name of STRIP) node.removeAttribute(name);
92
+ ghost.classList.add("page-board-ghost");
93
+ ghost.setAttribute("aria-hidden", "true");
94
+ ghost.setAttribute("inert", "");
95
+ ghost.style.width = `${r.width}px`;
96
+ s.ghost = ghost;
97
+ host.dom.append(ghost);
98
+ host.dom.classList.add("is-dragging");
99
+ this.line = host.dom.ownerDocument.createElement(s.kind === "card" ? "li" : "div");
100
+ this.line.className = s.kind === "card" ? "page-board-drop" : "page-board-drop is-column";
101
+ this.line.setAttribute("aria-hidden", "true");
102
+ host.dom.setPointerCapture?.(s.pointer);
103
+ host.dom.addEventListener("lostpointercapture", this.onCancel);
104
+ window.addEventListener("keydown", this.onKey, true);
105
+ source.classList.add("is-drag-source");
106
+ this.loop();
107
+ }
108
+
109
+ track(force = false) {
110
+ const s = this.state;
111
+ if (!s?.active) return;
112
+ s.ghost.style.transform = `translate(${s.x - s.dx}px, ${s.y - s.dy}px)`;
113
+ if (!this.exists()) return this.cancel();
114
+ const slot = this.inBounds() ? (s.kind === "card" ? this.cardSlot() : this.columnSlot()) : null;
115
+ if (!force && same(slot, s.slot)) return;
116
+ s.slot = slot;
117
+ this.paint();
118
+ }
119
+
120
+ inBounds() {
121
+ const s = this.state, r = this.host.scroll.getBoundingClientRect();
122
+ return s.x >= r.left - EDGE && s.x <= r.right + EDGE && s.y >= r.top - EDGE && s.y <= r.bottom + EDGE;
123
+ }
124
+
125
+ cardSlot() {
126
+ const s = this.state;
127
+ let lane = null, best = Infinity;
128
+ for (const node of this.host.lanes.querySelectorAll(".page-board-lane[data-col]")) {
129
+ const r = node.getBoundingClientRect();
130
+ const distance = s.x < r.left ? r.left - s.x : s.x > r.right ? s.x - r.right : 0;
131
+ if (distance < best) { best = distance; lane = node; }
132
+ }
133
+ if (!lane) return null;
134
+ const cards = [...lane.querySelectorAll(".page-board-card[data-card]")].filter((node) => node.dataset.card !== s.id);
135
+ const index = cards.filter((node) => middle(node, "y") < s.y).length;
136
+ return { col: lane.dataset.col, afterId: index ? cards[index - 1].dataset.card : null };
137
+ }
138
+
139
+ columnSlot() {
140
+ const s = this.state;
141
+ const rtl = getComputedStyle(this.host.lanes).direction === "rtl";
142
+ const lanes = [...this.host.lanes.querySelectorAll(".page-board-lane[data-col]")].filter((node) => node.dataset.col !== s.id);
143
+ const index = lanes.filter((node) => (rtl ? middle(node, "x") > s.x : middle(node, "x") < s.x)).length;
144
+ return { col: null, afterId: index ? lanes[index - 1].dataset.col : null };
145
+ }
146
+
147
+ paint() {
148
+ const s = this.state, line = this.line;
149
+ line.remove();
150
+ if (!s.slot) return;
151
+ const lanes = this.host.lanes;
152
+ if (s.kind === "card") {
153
+ const list = [...lanes.querySelectorAll(".page-board-lane[data-col]")].find((node) => node.dataset.col === s.slot.col)?.querySelector(".page-board-cards");
154
+ const after = s.slot.afterId && [...(list?.querySelectorAll(".page-board-card[data-card]") ?? [])].find((node) => node.dataset.card === s.slot.afterId)?.closest("li");
155
+ list?.insertBefore(line, after ? after.nextSibling : list.firstChild);
156
+ } else {
157
+ const after = s.slot.afterId && [...lanes.querySelectorAll(".page-board-lane[data-col]")].find((node) => node.dataset.col === s.slot.afterId);
158
+ lanes.insertBefore(line, after ? after.nextSibling : lanes.firstChild);
159
+ }
160
+ }
161
+
162
+ repaint() {
163
+ if (!this.state?.active) return;
164
+ this.source()?.classList.add("is-drag-source");
165
+ this.track(true);
166
+ }
167
+
168
+ loop() {
169
+ const step = () => {
170
+ const s = this.state;
171
+ if (!s?.active) return;
172
+ const box = this.host.scroll.getBoundingClientRect();
173
+ const page = scrollParent(this.host.dom);
174
+ const pageBox = page === page.ownerDocument.scrollingElement ? { top: 0, bottom: window.innerHeight } : page.getBoundingClientRect();
175
+ const vx = speed(s.x, box.left, box.right), vy = speed(s.y, pageBox.top, pageBox.bottom);
176
+ if (vx) this.host.scroll.scrollLeft += vx;
177
+ if (vy) page.scrollTop += vy;
178
+ if (vx || vy) this.track();
179
+ if (this.state === s) s.raf = requestAnimationFrame(step);
180
+ };
181
+ this.state.raf = requestAnimationFrame(step);
182
+ }
183
+
184
+ end(event) {
185
+ const s = this.state;
186
+ if (!s || event.pointerId !== s.pointer) return;
187
+ if (!s.active) return this.stop();
188
+ event.preventDefault();
189
+ this.suppress = true;
190
+ setTimeout(() => { this.suppress = false; }, 0);
191
+ const { kind, id, slot } = s;
192
+ this.stop();
193
+ const host = this.host;
194
+ if (!slot) return announce(host.view, "Move cancelled");
195
+ const result = kind === "card" ? host.run("moveCard", id, { columnId: slot.col, afterId: slot.afterId }) : host.run("moveColumn", id, { afterId: slot.afterId });
196
+ if (result.status !== "moved") return;
197
+ if (kind === "card") {
198
+ announce(host.view, `Moved card to ${host.board.columns[slot.col]?.name || "Untitled"}`);
199
+ host.focusKey(`card:${id}`);
200
+ } else announce(host.view, "Moved column");
201
+ }
202
+
203
+ cancel() {
204
+ this.stop();
205
+ announce(this.host.view, "Move cancelled");
206
+ }
207
+
208
+ stop() {
209
+ const s = this.state, host = this.host;
210
+ if (!s) return;
211
+ this.state = null;
212
+ cancelAnimationFrame(s.raf);
213
+ const doc = host.dom.ownerDocument;
214
+ doc.removeEventListener("pointermove", this.onMove);
215
+ doc.removeEventListener("pointerup", this.onUp);
216
+ doc.removeEventListener("pointercancel", this.onCancel);
217
+ host.dom.removeEventListener("lostpointercapture", this.onCancel);
218
+ window.removeEventListener("keydown", this.onKey, true);
219
+ if (host.dom.hasPointerCapture?.(s.pointer)) host.dom.releasePointerCapture(s.pointer);
220
+ s.ghost?.remove();
221
+ this.line?.remove();
222
+ this.line = null;
223
+ host.dom.classList.remove("is-dragging");
224
+ for (const node of host.dom.querySelectorAll(".is-drag-source")) node.classList.remove("is-drag-source");
225
+ }
226
+ }