@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,220 @@
1
+ import { state } from "../vendor/prosemirror.vendor.js";
2
+ import { schema } from "./schema.js";
3
+ import { blockJson, keyBetween, newId, orderedIds } from "./model.js";
4
+ import { chartModel, renderChartSvg } from "./rich/chart.js";
5
+ import { openPageMenu } from "./chrome.js";
6
+ import { project } from "./projection.js";
7
+ import { columnKinds } from "./table-kinds.js";
8
+ import { cellText } from "./rich/cell.js";
9
+
10
+ const views = new WeakMap();
11
+
12
+ function sourceTable(doc, id) {
13
+ let table = null;
14
+ doc.forEach((node) => { if (node.type === schema.nodes.table && node.attrs.id === id) table = node; });
15
+ return table;
16
+ }
17
+
18
+ function tableData(node) {
19
+ if (!node) return null;
20
+ return project({ [node.attrs.id]: blockJson(node) })[0];
21
+ }
22
+
23
+ function chartAttrs(table, pos, parent) {
24
+ const colIds = orderedIds(table.attrs.cols);
25
+ if (!colIds.length) return null;
26
+ const kinds = columnKinds(table);
27
+ const series = colIds.filter((id) => id !== colIds[0] && kinds[id] === "number");
28
+ return {
29
+ id: newId(),
30
+ pos,
31
+ parent,
32
+ source: table.attrs.id,
33
+ kind: "bar",
34
+ x: colIds[0],
35
+ series: series.length ? series : colIds[1] ? [colIds[1]] : [],
36
+ title: "",
37
+ highlight: null,
38
+ };
39
+ }
40
+
41
+ export function insertChartAfterTable(view, tablePos) {
42
+ const doc = view.state.doc;
43
+ const table = doc.nodeAt(tablePos);
44
+ if (table?.type !== schema.nodes.table) return false;
45
+ const after = tablePos + table.nodeSize;
46
+ const next = doc.maybeChild(doc.resolve(after).index(0));
47
+ const attrs = chartAttrs(table, keyBetween(table.attrs.pos, next?.attrs.pos ?? null), table.attrs.parent);
48
+ if (!attrs) return false;
49
+ view.dispatch(view.state.tr.insert(after, schema.nodes.chart.create(attrs)));
50
+ return true;
51
+ }
52
+
53
+ export function chartTables(doc) {
54
+ const tables = [];
55
+ doc.forEach((node) => { if (node.type === schema.nodes.table && orderedIds(node.attrs.cols).length) tables.push(node); });
56
+ return tables;
57
+ }
58
+
59
+ // /chart: an empty line becomes the chart, otherwise it goes below the line.
60
+ // One table is charted at once; several open a picker named by their columns.
61
+ function placeChart(view, index, table, remove) {
62
+ let tr = view.state.tr;
63
+ if (remove) tr.delete(remove.from, remove.to);
64
+ if (!sourceTable(tr.doc, table.attrs.id) || index >= tr.doc.childCount) return false;
65
+ let at = 0;
66
+ for (let i = 0; i < index; i++) at += tr.doc.child(i).nodeSize;
67
+ const node = tr.doc.child(index);
68
+ const replace = node.isTextblock && node.content.size === 0;
69
+ const next = tr.doc.maybeChild(index + 1);
70
+ const pos = replace ? node.attrs.pos : keyBetween(node.attrs.pos, next?.attrs.pos ?? null);
71
+ const attrs = chartAttrs(table, pos, node.attrs.parent ?? null);
72
+ if (!attrs) return false;
73
+ const chart = schema.nodes.chart.create(attrs);
74
+ tr = replace ? tr.replaceWith(at, at + node.nodeSize, chart) : tr.insert(at + node.nodeSize, chart);
75
+ view.dispatch(tr.scrollIntoView());
76
+ return true;
77
+ }
78
+
79
+ export const tableLabel = (table) => orderedIds(table.attrs.cols).map((id) => table.attrs.cols[id].name || "Untitled").join(", ");
80
+
81
+ export function chartFromSlash(view, index, remove = null) {
82
+ const tables = chartTables(view.state.doc);
83
+ if (!tables.length) return false;
84
+ if (tables.length === 1) return placeChart(view, index, tables[0], remove);
85
+ const anchor = view.nodeDOM(view.state.doc.resolve(view.state.selection.head).before(1)) ?? view.dom;
86
+ return openPageMenu(view, anchor, tables.map((table, i) => ({
87
+ label: `Table ${i + 1}`,
88
+ description: tableLabel(table),
89
+ icon: "▦",
90
+ run: () => placeChart(view, index, table, remove),
91
+ })));
92
+ }
93
+
94
+ export class ChartView {
95
+ constructor(node, view, getPos) {
96
+ this.node = node;
97
+ this.view = view;
98
+ this.getPos = getPos;
99
+ this.source = undefined;
100
+ this.frame = null;
101
+ const doc = view.dom.ownerDocument;
102
+ this.dom = doc.createElement("figure");
103
+ this.dom.className = "page-chart";
104
+ this.dom.dataset.id = node.attrs.id ?? "";
105
+ this.dom.dataset.type = "chart";
106
+ this.plot = doc.createElement("div");
107
+ this.plot.className = "page-chart-plot";
108
+ this.caption = doc.createElement("figcaption");
109
+ this.settings = doc.createElement("button");
110
+ this.settings.type = "button";
111
+ this.settings.className = "page-chart-settings";
112
+ this.settings.textContent = "Chart settings";
113
+ this.settings.setAttribute("aria-label", "Chart settings");
114
+ this.settings.contentEditable = "false";
115
+ this.settings.addEventListener("click", () => this.openSettings());
116
+ this.dom.append(this.plot, this.caption, this.settings);
117
+ if (!views.has(view)) views.set(view, new Set());
118
+ views.get(view).add(this);
119
+ this.refresh();
120
+ }
121
+
122
+ refresh() {
123
+ const source = sourceTable(this.view.state.doc, this.node.attrs.source);
124
+ if (source === this.source) return;
125
+ this.source = source;
126
+ this.schedule();
127
+ }
128
+
129
+ schedule() {
130
+ if (this.frame !== null) return;
131
+ const win = this.dom.ownerDocument.defaultView;
132
+ const raf = win.requestAnimationFrame?.bind(win) ?? ((fn) => win.setTimeout(fn, 0));
133
+ this.frame = raf(() => { this.frame = null; this.draw(); });
134
+ }
135
+
136
+ draw() {
137
+ const attrs = this.node.attrs;
138
+ const chart = chartModel({ id: attrs.id, type: attrs.kind, x: attrs.x, series: attrs.series, title: attrs.title, highlight: attrs.highlight }, tableData(this.source));
139
+ this.plot.innerHTML = renderChartSvg(chart, { width: 640, height: 320, narrow: this.dom.getBoundingClientRect().width < 480 });
140
+ this.caption.textContent = attrs.title;
141
+ }
142
+
143
+ setAttrs(changes) {
144
+ const pos = this.getPos();
145
+ if (pos == null) return;
146
+ const node = this.view.state.doc.nodeAt(pos);
147
+ if (node?.type !== schema.nodes.chart) return;
148
+ this.view.dispatch(this.view.state.tr.setNodeMarkup(pos, null, { ...node.attrs, ...changes }));
149
+ }
150
+
151
+ submenu(items) {
152
+ openPageMenu(this.view, this.settings, items);
153
+ }
154
+
155
+ openSettings() {
156
+ const attrs = this.node.attrs;
157
+ const table = tableData(sourceTable(this.view.state.doc, attrs.source));
158
+ const cols = table?.cols ?? [];
159
+ const rows = table?.rows ?? [];
160
+ this.submenu([
161
+ { label: "Line", icon: attrs.kind === "line" ? "✓" : "", run: () => this.setAttrs({ kind: "line" }) },
162
+ { label: "Bar", icon: attrs.kind === "bar" ? "✓" : "", run: () => this.setAttrs({ kind: "bar" }) },
163
+ { label: "X column ▸", focus: false, run: () => this.submenu(cols.map((col) => ({ label: col.name || col.id, icon: attrs.x === col.id ? "✓" : "", run: () => this.setAttrs({ x: col.id, series: attrs.series.filter((id) => id !== col.id) }) }))) },
164
+ { label: "Series ▸", focus: false, run: () => this.submenu(cols.filter((col) => col.id !== attrs.x).map((col) => ({ label: col.name || col.id, icon: attrs.series.includes(col.id) ? "✓" : "", checked: attrs.series.includes(col.id), run: () => this.setAttrs({ series: attrs.series.includes(col.id) ? attrs.series.filter((id) => id !== col.id) : [...attrs.series, col.id] }) }))) },
165
+ { label: "Highlight row ▸", focus: false, run: () => this.submenu([{ label: "None", icon: !attrs.highlight ? "✓" : "", run: () => this.setAttrs({ highlight: null }) }, ...rows.map((row) => ({ label: cellText(row.cells[attrs.x]) || row.id, icon: attrs.highlight === row.id ? "✓" : "", run: () => this.setAttrs({ highlight: row.id }) }))]) },
166
+ { label: "Edit title", focus: false, run: () => this.editTitle() },
167
+ ]);
168
+ }
169
+
170
+ editTitle() {
171
+ const input = this.dom.ownerDocument.createElement("input");
172
+ input.type = "text";
173
+ input.className = "page-chart-title-input";
174
+ input.setAttribute("aria-label", "Chart title");
175
+ input.value = this.node.attrs.title ?? "";
176
+ const finish = (save) => {
177
+ input.remove();
178
+ this.caption.hidden = false;
179
+ if (save) this.setAttrs({ title: input.value });
180
+ else this.settings.focus();
181
+ };
182
+ input.addEventListener("keydown", (event) => {
183
+ if (event.key === "Enter" || event.key === "Escape") {
184
+ event.preventDefault();
185
+ event.stopPropagation();
186
+ finish(event.key === "Enter");
187
+ }
188
+ });
189
+ this.caption.hidden = true;
190
+ this.caption.after(input);
191
+ input.focus();
192
+ input.select();
193
+ }
194
+
195
+ update(node) {
196
+ if (node.type !== schema.nodes.chart) return false;
197
+ const changed = !node.sameMarkup(this.node);
198
+ this.node = node;
199
+ this.dom.dataset.id = node.attrs.id ?? "";
200
+ if (changed) { this.source = sourceTable(this.view.state.doc, node.attrs.source); this.schedule(); }
201
+ return true;
202
+ }
203
+
204
+ stopEvent(event) { return !!event.target.closest?.(".page-chart-settings, .page-chart-title-input"); }
205
+
206
+ destroy() {
207
+ views.get(this.view)?.delete(this);
208
+ if (this.frame !== null) {
209
+ const win = this.dom.ownerDocument.defaultView;
210
+ if (win.cancelAnimationFrame) win.cancelAnimationFrame(this.frame);
211
+ else win.clearTimeout(this.frame);
212
+ }
213
+ }
214
+ }
215
+
216
+ export function chartPlugin() {
217
+ return new state.Plugin({
218
+ view: () => ({ update(view) { for (const chart of views.get(view) ?? []) chart.refresh(); } }),
219
+ });
220
+ }
@@ -0,0 +1,143 @@
1
+ import { state, view as pmView } from "../vendor/prosemirror.vendor.js";
2
+ import { schema } from "./schema.js";
3
+ import { asStep } from "./blocksel.js";
4
+ import { el, cardFace } from "./board-view.js";
5
+
6
+ const { Plugin } = state;
7
+ const { Decoration, DecorationSet } = pmView;
8
+
9
+ function itemAt(view, id) {
10
+ let found = null;
11
+ view.state.doc.forEach((node, pos) => { if (node.attrs.id === id && node.type === schema.nodes.list_item && node.attrs.kind === "check") found = { node, pos }; });
12
+ return found;
13
+ }
14
+
15
+ function setItem(view, id, attr, value) {
16
+ const found = itemAt(view, id);
17
+ if (!found) return;
18
+ view.dispatch(asStep(view.state.tr.setNodeMarkup(found.pos, null, { ...found.node.attrs, [attr]: value || null })));
19
+ }
20
+
21
+ function chip(doc, label, aria, attr) {
22
+ const button = doc.createElement("button");
23
+ button.type = "button";
24
+ button.className = "page-check-chip";
25
+ button.dataset.check = attr;
26
+ button.textContent = label;
27
+ button.setAttribute("aria-label", aria);
28
+ return button;
29
+ }
30
+
31
+ // The same owner field a board card's panel shows: people as chips, "No owner",
32
+ // then a name box for anyone not in the list.
33
+ function ownerPicker(view, id, anchor) {
34
+ const doc = anchor.ownerDocument;
35
+ const current = itemAt(view, id)?.node.attrs.owner || "";
36
+ const popup = doc.createElement("span");
37
+ popup.className = "page-check-popup page-check-owner";
38
+ popup.contentEditable = "false";
39
+ const box = el(doc, "span", "page-board-owners");
40
+ const people = el(doc, "span", "page-board-people", { role: "group", "aria-label": "Owner" });
41
+ const input = el(doc, "input", "page-board-owner-input", { type: "text", "aria-label": "Owner name", placeholder: "Or type a name…" });
42
+ input.value = current;
43
+ const choose = (name) => setItem(view, id, "owner", name);
44
+ const person = (name, label, attr, pressed) => {
45
+ const button = el(doc, "button", `page-board-person${name ? "" : " is-none"}`, { type: "button", "aria-pressed": String(pressed), "data-check": attr });
46
+ if (name) button.append(cardFace(doc, name), el(doc, "span", null, {}, name));
47
+ else button.textContent = label;
48
+ button.addEventListener("click", () => choose(name));
49
+ return button;
50
+ };
51
+ const draw = (available) => {
52
+ const typed = input.value.trim().toLowerCase();
53
+ const shown = (available || []).filter((p) => !typed || typed === current.toLowerCase() || p.name.toLowerCase().includes(typed));
54
+ people.replaceChildren(...shown.map((p) => person(p.name, p.name, "owner-option", p.name === current)), person(null, "No owner", "clear-owner", !current));
55
+ };
56
+ input.addEventListener("keydown", (event) => {
57
+ if (event.key === "Enter") { event.preventDefault(); choose(input.value.trim()); }
58
+ if (event.key === "Escape") { event.preventDefault(); popup.remove(); anchor.focus(); }
59
+ event.stopPropagation();
60
+ });
61
+ box.append(people, input);
62
+ popup.append(box);
63
+ anchor.after(popup);
64
+ input.focus();
65
+ let available = [];
66
+ draw(available);
67
+ input.addEventListener("input", () => draw(available));
68
+ const source = globalThis.clay?.people?.available;
69
+ if (source) Promise.resolve().then(() => source()).then((list) => { if (popup.isConnected) { available = list || []; draw(available); } }).catch(() => {});
70
+ }
71
+
72
+ function datePicker(view, id, anchor) {
73
+ const doc = anchor.ownerDocument;
74
+ const popup = doc.createElement("span");
75
+ popup.className = "page-check-popup";
76
+ popup.contentEditable = "false";
77
+ const input = doc.createElement("input");
78
+ input.type = "date";
79
+ input.setAttribute("aria-label", "Due date");
80
+ input.value = itemAt(view, id)?.node.attrs.due || "";
81
+ input.addEventListener("change", () => setItem(view, id, "due", input.value));
82
+ input.addEventListener("keydown", (event) => event.stopPropagation());
83
+ const clear = chip(doc, "Clear", "Clear due date", "clear-date");
84
+ clear.addEventListener("click", () => setItem(view, id, "due", null));
85
+ popup.append(input, clear);
86
+ anchor.after(popup);
87
+ input.focus();
88
+ }
89
+
90
+ function controls(view, node) {
91
+ const doc = view.dom.ownerDocument;
92
+ const box = doc.createElement("span");
93
+ box.className = "page-check-controls";
94
+ box.contentEditable = "false";
95
+ box.dataset.empty = String(!node.attrs.owner && !node.attrs.due);
96
+ const owner = chip(doc, node.attrs.owner || "Assign", node.attrs.owner ? `Owner: ${node.attrs.owner}` : "Assign owner", "owner");
97
+ const dueLabel = node.attrs.due ? new Date(`${node.attrs.due}T00:00:00Z`).toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" }) : "Date";
98
+ const date = chip(doc, dueLabel, node.attrs.due ? `Due ${dueLabel}` : "Set due date", "date");
99
+ owner.addEventListener("click", () => ownerPicker(view, node.attrs.id, owner));
100
+ date.addEventListener("click", () => datePicker(view, node.attrs.id, date));
101
+ box.append(owner, date);
102
+ return box;
103
+ }
104
+
105
+ function focusControl(view, name) {
106
+ const $pos = view.state.selection.$head;
107
+ if ($pos.depth !== 1 || $pos.parent.type !== schema.nodes.list_item || $pos.parent.attrs.kind !== "check") return false;
108
+ const id = $pos.parent.attrs.id;
109
+ const widget = [...view.dom.querySelectorAll(".page-check-controls")].find((el) => el.dataset.item === id);
110
+ const button = widget?.querySelector(`[data-check="${name}"]`);
111
+ if (!button) return false;
112
+ button.focus();
113
+ return true;
114
+ }
115
+
116
+ export const checklistKeys = new Plugin({
117
+ props: {
118
+ handleKeyDown(view, event) {
119
+ if (!(event.metaKey || event.ctrlKey) || !event.shiftKey || event.altKey) return false;
120
+ const name = event.key.toLowerCase() === "o" ? "owner" : event.key.toLowerCase() === "d" ? "date" : null;
121
+ return name ? focusControl(view, name) : false;
122
+ },
123
+ },
124
+ });
125
+
126
+ export function checklistPlugin() {
127
+ return new Plugin({
128
+ props: {
129
+ decorations(st) {
130
+ const widgets = [];
131
+ st.doc.forEach((node, pos) => {
132
+ if (node.type !== schema.nodes.list_item || node.attrs.kind !== "check") return;
133
+ widgets.push(Decoration.widget(pos + node.nodeSize - 1, (view) => {
134
+ const box = controls(view, node);
135
+ box.dataset.item = node.attrs.id;
136
+ return box;
137
+ }, { key: `${node.attrs.id}:${node.attrs.owner}:${node.attrs.due}`, side: 1, stopEvent: () => true, ignoreSelection: true }));
138
+ });
139
+ return DecorationSet.create(st.doc, widgets);
140
+ },
141
+ },
142
+ });
143
+ }