@tldraw/commenting 0.0.0-bootstrap → 5.3.0-internal.c5c7f1d817d0
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/commenting.css +131 -131
- package/dist-cjs/canvas/comment-body.js +1 -1
- package/dist-cjs/canvas/comment-body.js.map +2 -2
- package/dist-cjs/canvas/comments-filter-menu.js +1 -1
- package/dist-cjs/canvas/comments-filter-menu.js.map +1 -1
- package/dist-cjs/canvas/comments-overflow-menu.js +3 -3
- package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
- package/dist-cjs/canvas/comments-overlay.js +65 -50
- package/dist-cjs/canvas/comments-overlay.js.map +2 -2
- package/dist-cjs/canvas/comments-sidebar.js +2 -2
- package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
- package/dist-cjs/clustering/runtime.js +119 -7
- package/dist-cjs/clustering/runtime.js.map +2 -2
- package/dist-cjs/index.d.ts +28 -8
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/ui/avatar.js +10 -2
- package/dist-cjs/ui/avatar.js.map +2 -2
- package/dist-cjs/ui/byline.js +4 -4
- package/dist-cjs/ui/byline.js.map +2 -2
- package/dist-cjs/ui/comment-card.js +3 -3
- package/dist-cjs/ui/comment-card.js.map +2 -2
- package/dist-cjs/ui/comment-composer.js +30 -24
- package/dist-cjs/ui/comment-composer.js.map +2 -2
- package/dist-cjs/ui/comment-mention.js +1 -1
- package/dist-cjs/ui/comment-mention.js.map +2 -2
- package/dist-cjs/ui/comment-pin.js +5 -1
- package/dist-cjs/ui/comment-pin.js.map +2 -2
- package/dist-cjs/ui/comment-text.js +1 -1
- package/dist-cjs/ui/comment-text.js.map +2 -2
- package/dist-cjs/ui/comment-thread.js +6 -6
- package/dist-cjs/ui/comment-thread.js.map +2 -2
- package/dist-cjs/ui/comments-list.js +11 -11
- package/dist-cjs/ui/comments-list.js.map +2 -2
- package/dist-cjs/ui/count-badge.js +1 -1
- package/dist-cjs/ui/count-badge.js.map +2 -2
- package/dist-cjs/ui/empty-state.js +3 -3
- package/dist-cjs/ui/empty-state.js.map +2 -2
- package/dist-cjs/ui/mention-list.js +7 -7
- package/dist-cjs/ui/mention-list.js.map +2 -2
- package/dist-cjs/ui/mention-suggestion.js +3 -3
- package/dist-cjs/ui/mention-suggestion.js.map +2 -2
- package/dist-cjs/ui/mention.js +1 -1
- package/dist-cjs/ui/mention.js.map +2 -2
- package/dist-cjs/ui/reaction.js +11 -4
- package/dist-cjs/ui/reaction.js.map +2 -2
- package/dist-cjs/ui/reactions.js +10 -2
- package/dist-cjs/ui/reactions.js.map +2 -2
- package/dist-cjs/ui/send-button.js +1 -1
- package/dist-cjs/ui/send-button.js.map +2 -2
- package/dist-esm/canvas/comment-body.mjs +1 -1
- package/dist-esm/canvas/comment-body.mjs.map +2 -2
- package/dist-esm/canvas/comments-filter-menu.mjs +1 -1
- package/dist-esm/canvas/comments-filter-menu.mjs.map +1 -1
- package/dist-esm/canvas/comments-overflow-menu.mjs +3 -3
- package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
- package/dist-esm/canvas/comments-overlay.mjs +65 -50
- package/dist-esm/canvas/comments-overlay.mjs.map +2 -2
- package/dist-esm/canvas/comments-sidebar.mjs +2 -2
- package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
- package/dist-esm/clustering/runtime.mjs +119 -7
- package/dist-esm/clustering/runtime.mjs.map +2 -2
- package/dist-esm/index.d.mts +28 -8
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/ui/avatar.mjs +10 -2
- package/dist-esm/ui/avatar.mjs.map +2 -2
- package/dist-esm/ui/byline.mjs +4 -4
- package/dist-esm/ui/byline.mjs.map +2 -2
- package/dist-esm/ui/comment-card.mjs +3 -3
- package/dist-esm/ui/comment-card.mjs.map +2 -2
- package/dist-esm/ui/comment-composer.mjs +30 -24
- package/dist-esm/ui/comment-composer.mjs.map +2 -2
- package/dist-esm/ui/comment-mention.mjs +1 -1
- package/dist-esm/ui/comment-mention.mjs.map +2 -2
- package/dist-esm/ui/comment-pin.mjs +5 -1
- package/dist-esm/ui/comment-pin.mjs.map +2 -2
- package/dist-esm/ui/comment-text.mjs +1 -1
- package/dist-esm/ui/comment-text.mjs.map +2 -2
- package/dist-esm/ui/comment-thread.mjs +6 -6
- package/dist-esm/ui/comment-thread.mjs.map +2 -2
- package/dist-esm/ui/comments-list.mjs +11 -11
- package/dist-esm/ui/comments-list.mjs.map +2 -2
- package/dist-esm/ui/count-badge.mjs +1 -1
- package/dist-esm/ui/count-badge.mjs.map +2 -2
- package/dist-esm/ui/empty-state.mjs +3 -3
- package/dist-esm/ui/empty-state.mjs.map +2 -2
- package/dist-esm/ui/mention-list.mjs +7 -7
- package/dist-esm/ui/mention-list.mjs.map +2 -2
- package/dist-esm/ui/mention-suggestion.mjs +3 -3
- package/dist-esm/ui/mention-suggestion.mjs.map +2 -2
- package/dist-esm/ui/mention.mjs +1 -1
- package/dist-esm/ui/mention.mjs.map +2 -2
- package/dist-esm/ui/reaction.mjs +11 -4
- package/dist-esm/ui/reaction.mjs.map +2 -2
- package/dist-esm/ui/reactions.mjs +10 -2
- package/dist-esm/ui/reactions.mjs.map +2 -2
- package/dist-esm/ui/send-button.mjs +1 -1
- package/dist-esm/ui/send-button.mjs.map +2 -2
- package/package.json +3 -3
- package/src/canvas/canvas.css +25 -25
- package/src/canvas/comment-body.tsx +1 -1
- package/src/canvas/comment-render.test.ts +1 -1
- package/src/canvas/comments-filter-menu.tsx +1 -1
- package/src/canvas/comments-overflow-menu.tsx +3 -3
- package/src/canvas/comments-overlay.tsx +109 -76
- package/src/canvas/comments-sidebar.tsx +2 -2
- package/src/clustering/runtime.test.ts +183 -7
- package/src/clustering/runtime.ts +164 -15
- package/src/ui/avatar.tsx +9 -2
- package/src/ui/byline.tsx +4 -4
- package/src/ui/comment-card.tsx +3 -3
- package/src/ui/comment-composer.tsx +49 -30
- package/src/ui/comment-mention.ts +2 -2
- package/src/ui/comment-pin.tsx +5 -1
- package/src/ui/comment-text.tsx +1 -1
- package/src/ui/comment-thread.tsx +8 -6
- package/src/ui/comments-list.tsx +16 -12
- package/src/ui/comments.css +106 -106
- package/src/ui/count-badge.tsx +1 -1
- package/src/ui/empty-state.tsx +3 -3
- package/src/ui/mention-list.tsx +8 -8
- package/src/ui/mention-suggestion.tsx +3 -3
- package/src/ui/mention.tsx +1 -1
- package/src/ui/reaction.tsx +6 -3
- package/src/ui/reactions.tsx +6 -2
- package/src/ui/send-button.tsx +1 -1
|
@@ -9,16 +9,42 @@ class ClusterRuntimeImpl {
|
|
|
9
9
|
table;
|
|
10
10
|
// mutable internally; readonly through the ClusterRuntime interface
|
|
11
11
|
k = 0;
|
|
12
|
+
version = 0;
|
|
13
|
+
// Structural partition: exactly leaves + applied events, never patched. The cursor invariants
|
|
14
|
+
// live here, untouched by detaches — resolution to the displayed partition happens at read
|
|
15
|
+
// time in getVisible().
|
|
12
16
|
visible = /* @__PURE__ */ new Map();
|
|
13
17
|
seeded = false;
|
|
18
|
+
// Indices (< k) of band events held unmerged by seedFrom carryover. The single cursor can
|
|
19
|
+
// only express "merged up to here", but a carried-over partition can be "merged except these"
|
|
20
|
+
// — the exceptions live here. Self-draining: an entry leaves via onCamera when the zoom
|
|
21
|
+
// crosses its own zMerge (merges) or its zSplit (the split walk retreats past it).
|
|
22
|
+
suppressed = /* @__PURE__ */ new Set();
|
|
23
|
+
// Leaves removed from the displayed partition (deleted / popped out / opened). Patches map
|
|
24
|
+
// each structural node containing a detached leaf to its displayed replacement (or null to
|
|
25
|
+
// drop it). Rebuilt from the detached set; cleared by seed/seedFrom.
|
|
26
|
+
detached = /* @__PURE__ */ new Set();
|
|
27
|
+
patched = /* @__PURE__ */ new Map();
|
|
28
|
+
leafById = null;
|
|
29
|
+
resolvedCache = null;
|
|
30
|
+
getSuppressedCount() {
|
|
31
|
+
return this.suppressed.size;
|
|
32
|
+
}
|
|
33
|
+
getDetachedCount() {
|
|
34
|
+
return this.detached.size;
|
|
35
|
+
}
|
|
14
36
|
seed(zoom) {
|
|
15
37
|
validateZoom(zoom);
|
|
16
38
|
this.k = seedCount(this.table.events, zoom);
|
|
39
|
+
this.suppressed.clear();
|
|
40
|
+
this.detached.clear();
|
|
41
|
+
this.patched.clear();
|
|
17
42
|
this.resetVisible();
|
|
18
43
|
for (let i = 0; i < this.k; i++) {
|
|
19
44
|
applyEvent(this.visible, this.table.events[i]);
|
|
20
45
|
}
|
|
21
46
|
this.seeded = true;
|
|
47
|
+
this.version++;
|
|
22
48
|
}
|
|
23
49
|
seedFrom(zoom, previous) {
|
|
24
50
|
validateZoom(zoom);
|
|
@@ -30,35 +56,121 @@ class ClusterRuntimeImpl {
|
|
|
30
56
|
}
|
|
31
57
|
const events = this.table.events;
|
|
32
58
|
let k = 0;
|
|
33
|
-
while (k < events.length) {
|
|
34
|
-
const event = events[k];
|
|
35
|
-
if (zoom >= event.zSplit) break;
|
|
36
|
-
if (zoom > event.zMerge && !wasMergedTogether(event.result.members, ownerByMember)) break;
|
|
59
|
+
while (k < events.length && zoom < events[k].zSplit) {
|
|
37
60
|
k++;
|
|
38
61
|
}
|
|
39
62
|
this.k = k;
|
|
63
|
+
this.suppressed.clear();
|
|
64
|
+
this.detached.clear();
|
|
65
|
+
this.patched.clear();
|
|
40
66
|
this.resetVisible();
|
|
41
67
|
for (let i = 0; i < k; i++) {
|
|
42
|
-
|
|
68
|
+
const event = events[i];
|
|
69
|
+
if (zoom > event.zMerge && !wasMergedTogether(event.result.members, ownerByMember)) {
|
|
70
|
+
this.suppressed.add(i);
|
|
71
|
+
} else {
|
|
72
|
+
applyEvent(this.visible, event);
|
|
73
|
+
}
|
|
43
74
|
}
|
|
44
75
|
this.seeded = true;
|
|
76
|
+
this.version++;
|
|
45
77
|
}
|
|
46
78
|
onCamera(zoom) {
|
|
47
79
|
validateZoom(zoom);
|
|
48
80
|
if (!this.seeded) {
|
|
49
81
|
throw new Error("Cluster runtime must be seeded before onCamera");
|
|
50
82
|
}
|
|
83
|
+
let changed = false;
|
|
51
84
|
while (this.k < this.table.events.length && zoom <= this.table.events[this.k].zMerge) {
|
|
52
85
|
applyEvent(this.visible, this.table.events[this.k]);
|
|
53
86
|
this.k++;
|
|
87
|
+
changed = true;
|
|
54
88
|
}
|
|
55
89
|
while (this.k > 0 && zoom >= this.table.events[this.k - 1].zSplit) {
|
|
56
90
|
this.k--;
|
|
57
|
-
|
|
91
|
+
if (!this.suppressed.delete(this.k)) {
|
|
92
|
+
unapplyEvent(this.visible, this.table.events[this.k]);
|
|
93
|
+
changed = true;
|
|
94
|
+
}
|
|
58
95
|
}
|
|
96
|
+
if (this.suppressed.size > 0) {
|
|
97
|
+
for (const i of [...this.suppressed]) {
|
|
98
|
+
if (zoom <= this.table.events[i].zMerge) {
|
|
99
|
+
this.suppressed.delete(i);
|
|
100
|
+
applyEvent(this.visible, this.table.events[i]);
|
|
101
|
+
changed = true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (changed) this.version++;
|
|
106
|
+
}
|
|
107
|
+
detachLeaf(leafId) {
|
|
108
|
+
if (this.detached.has(leafId)) return;
|
|
109
|
+
if (!this.getLeafById().has(leafId)) return;
|
|
110
|
+
this.detached.add(leafId);
|
|
111
|
+
this.rebuildPatches();
|
|
112
|
+
this.version++;
|
|
59
113
|
}
|
|
60
114
|
getVisible() {
|
|
61
|
-
|
|
115
|
+
if (this.resolvedCache && this.resolvedCache.version === this.version) {
|
|
116
|
+
return this.resolvedCache.map;
|
|
117
|
+
}
|
|
118
|
+
let map;
|
|
119
|
+
if (this.patched.size === 0) {
|
|
120
|
+
map = this.visible;
|
|
121
|
+
} else {
|
|
122
|
+
map = /* @__PURE__ */ new Map();
|
|
123
|
+
for (const node of this.visible.values()) {
|
|
124
|
+
const resolved = this.patched.has(node.id) ? this.patched.get(node.id) : node;
|
|
125
|
+
if (resolved) map.set(resolved.id, resolved);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
this.resolvedCache = { version: this.version, map };
|
|
129
|
+
return map;
|
|
130
|
+
}
|
|
131
|
+
getLeafById() {
|
|
132
|
+
if (!this.leafById) {
|
|
133
|
+
this.leafById = new Map(this.table.leaves.map((leaf) => [leaf.id, leaf]));
|
|
134
|
+
}
|
|
135
|
+
return this.leafById;
|
|
136
|
+
}
|
|
137
|
+
/** Recompute the patch map from the detached set. A node is patched iff it contains a
|
|
138
|
+
* detached member; the patch drops those members and recomputes count/centroid, collapsing
|
|
139
|
+
* to the surviving leaf node at count 1 and to nothing at count 0. Patched nodes keep their
|
|
140
|
+
* structural id, so cursor events keep addressing them. */
|
|
141
|
+
rebuildPatches() {
|
|
142
|
+
this.patched.clear();
|
|
143
|
+
const leafById = this.getLeafById();
|
|
144
|
+
for (const id of this.detached) {
|
|
145
|
+
this.patched.set(id, null);
|
|
146
|
+
}
|
|
147
|
+
const patch = (node) => {
|
|
148
|
+
if (node.members.length === 1 || !node.members.some((m) => this.detached.has(m))) return;
|
|
149
|
+
const members = node.members.filter((m) => !this.detached.has(m));
|
|
150
|
+
if (members.length === 0) {
|
|
151
|
+
this.patched.set(node.id, null);
|
|
152
|
+
} else if (members.length === 1) {
|
|
153
|
+
this.patched.set(node.id, leafById.get(members[0]));
|
|
154
|
+
} else {
|
|
155
|
+
let x = 0;
|
|
156
|
+
let y = 0;
|
|
157
|
+
for (const m of members) {
|
|
158
|
+
const leaf = leafById.get(m);
|
|
159
|
+
x += leaf.centroid.x;
|
|
160
|
+
y += leaf.centroid.y;
|
|
161
|
+
}
|
|
162
|
+
this.patched.set(node.id, {
|
|
163
|
+
id: node.id,
|
|
164
|
+
centroid: { x: x / members.length, y: y / members.length },
|
|
165
|
+
count: members.length,
|
|
166
|
+
members
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
for (const event of this.table.events) {
|
|
171
|
+
patch(event.result);
|
|
172
|
+
for (const child of event.children) patch(child);
|
|
173
|
+
}
|
|
62
174
|
}
|
|
63
175
|
resetVisible() {
|
|
64
176
|
this.visible = /* @__PURE__ */ new Map();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/clustering/runtime.ts"],
|
|
4
|
-
"sourcesContent": ["import type { ClusterNode, ClusterTable, MergeEvent } from './types'\n\n/** @public */\nexport interface ClusterRuntime {\n\t/** Events[0..k) are active. Exposed for tests and debugging. */\n\treadonly k: number\n\t/** Reset state from scratch for the given zoom (cold start / after rebuild). */\n\tseed(zoom: number): void\n\t/**\n\t * Reset state for the given zoom, carrying hysteresis state over from a previous partition\n\t * (the visible map of the model being replaced). Threshold-forced events ignore history:\n\t * zoom \\<= zMerge is always active, zoom \\>= zSplit always inactive. An event inside its band\n\t * is active iff its members were merged together in `previous`; a band event with no history\n\t * (e.g. introduced by the rebuild) stays inactive until the next zoom-out crosses its zMerge.\n\t * Active events must form a prefix of the table, so classification stops at the first\n\t * inactive event; later band-active events conservatively resolve to split.\n\t */\n\tseedFrom(zoom: number, previous: ReadonlyMap<string, ClusterNode>): void\n\t/** Advance/retreat the cursor for a camera change. No-op if zoom sits inside all bands. */\n\tonCamera(zoom: number): void\n\t/** The current partition: cluster id \u2192 node. Do not mutate. */\n\tgetVisible(): ReadonlyMap<string, ClusterNode>\n}\n\n/** @public */\nexport function createClusterRuntime(table: ClusterTable): ClusterRuntime {\n\treturn new ClusterRuntimeImpl(table)\n}\n\nclass ClusterRuntimeImpl implements ClusterRuntime {\n\t// mutable internally; readonly through the ClusterRuntime interface\n\tk = 0\n\tprivate visible = new Map<string, ClusterNode>()\n\tprivate seeded = false\n\n\tconstructor(private readonly table: ClusterTable) {\n\t\tthis.resetVisible()\n\t}\n\n\tseed(zoom: number): void {\n\t\tvalidateZoom(zoom)\n\t\tthis.k = seedCount(this.table.events, zoom)\n\t\tthis.resetVisible()\n\t\tfor (let i = 0; i < this.k; i++) {\n\t\t\tapplyEvent(this.visible, this.table.events[i])\n\t\t}\n\t\tthis.seeded = true\n\t}\n\n\tseedFrom(zoom: number, previous: ReadonlyMap<string, ClusterNode>): void {\n\t\tvalidateZoom(zoom)\n\t\tconst ownerByMember = new Map<string, string>()\n\t\tfor (const node of previous.values()) {\n\t\t\tfor (const member of node.members) {\n\t\t\t\townerByMember.set(member, node.id)\n\t\t\t}\n\t\t}\n\n\t\tconst events = this.table.events\n\t\tlet k = 0\n\t\twhile (k < events.length) {\n\t\t\tconst event = events[k]\n\t\t\tif (zoom >= event.zSplit) break\n\t\t\tif (zoom > event.zMerge && !wasMergedTogether(event.result.members, ownerByMember)) break\n\t\t\tk++\n\t\t}\n\n\t\tthis.k = k\n\t\tthis.resetVisible()\n\t\tfor (let i = 0; i < k; i++) {\n\t\t\tapplyEvent(this.visible, events[i])\n\t\t}\n\t\tthis.seeded = true\n\t}\n\n\tonCamera(zoom: number): void {\n\t\tvalidateZoom(zoom)\n\t\tif (!this.seeded) {\n\t\t\tthrow new Error('Cluster runtime must be seeded before onCamera')\n\t\t}\n\n\t\twhile (this.k < this.table.events.length && zoom <= this.table.events[this.k].zMerge) {\n\t\t\tapplyEvent(this.visible, this.table.events[this.k])\n\t\t\tthis.k++\n\t\t}\n\t\twhile (this.k > 0 && zoom >= this.table.events[this.k - 1].zSplit) {\n\t\t\tthis.k--\n\t\t\tunapplyEvent(this.visible, this.table.events[this.k])\n\t\t}\n\t}\n\n\tgetVisible(): ReadonlyMap<string, ClusterNode> {\n\t\treturn this.visible\n\t}\n\n\tprivate resetVisible() {\n\t\tthis.visible = new Map()\n\t\tfor (const leaf of this.table.leaves) {\n\t\t\tthis.visible.set(leaf.id, leaf)\n\t\t}\n\t}\n}\n\n/** True iff every member id belonged to the same node of the previous partition. */\nfunction wasMergedTogether(\n\tmembers: readonly string[],\n\townerByMember: ReadonlyMap<string, string>\n): boolean {\n\tconst owner = ownerByMember.get(members[0])\n\tif (owner === undefined) return false\n\tfor (let i = 1; i < members.length; i++) {\n\t\tif (ownerByMember.get(members[i]) !== owner) return false\n\t}\n\treturn true\n}\n\nfunction seedCount(events: readonly MergeEvent[], zoom: number): number {\n\tlet lo = 0\n\tlet hi = events.length\n\twhile (lo < hi) {\n\t\tconst mid = (lo + hi) >> 1\n\t\tif (zoom <= seedThreshold(events[mid])) {\n\t\t\tlo = mid + 1\n\t\t} else {\n\t\t\thi = mid\n\t\t}\n\t}\n\treturn lo\n}\n\nfunction seedThreshold(event: MergeEvent): number {\n\treturn Math.sqrt(event.zMerge * event.zSplit)\n}\n\nfunction applyEvent(visible: Map<string, ClusterNode>, event: MergeEvent) {\n\tfor (const child of event.children) {\n\t\tvisible.delete(child.id)\n\t}\n\tvisible.set(event.result.id, event.result)\n}\n\nfunction unapplyEvent(visible: Map<string, ClusterNode>, event: MergeEvent) {\n\tvisible.delete(event.result.id)\n\tfor (const child of event.children) {\n\t\tvisible.set(child.id, child)\n\t}\n}\n\nfunction validateZoom(zoom: number) {\n\tif (!Number.isFinite(zoom) || zoom <= 0) {\n\t\tthrow new Error('zoom must be finite and greater than 0')\n\t}\n}\n"],
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type { ClusterNode, ClusterTable, MergeEvent } from './types'\n\n/** @public */\nexport interface ClusterRuntime {\n\t/** Events[0..k) are in the cursor (applied unless suppressed). Exposed for tests/debugging. */\n\treadonly k: number\n\t/**\n\t * Increments whenever the displayed partition changes. Subscribe to this, not `k`: detaches\n\t * and suppressed-event healing change the partition without moving the cursor.\n\t */\n\treadonly version: number\n\t/** Number of suppressed band events currently inside the cursor. Exposed for tests. */\n\tgetSuppressedCount(): number\n\t/** Number of leaves currently detached from the displayed partition. Exposed for tests. */\n\tgetDetachedCount(): number\n\t/** Reset state from scratch for the given zoom (cold start / after rebuild). Clears detaches. */\n\tseed(zoom: number): void\n\t/**\n\t * Reset state for the given zoom, carrying hysteresis state over from a previous partition\n\t * (the visible map of the model being replaced). Threshold-forced events ignore history:\n\t * zoom \\<= zMerge is always merged, zoom \\>= zSplit always split. An event inside its band\n\t * keeps its previous state: merged iff its members were merged together in `previous`; a band\n\t * event that was unmerged (or has no history, e.g. introduced by the rebuild) stays unmerged \u2014\n\t * inside the cursor but suppressed \u2014 until a zoom-out crosses its own zMerge, exactly like any\n\t * other pending merge. Carryover is exact: no group changes state because of the swap alone.\n\t * Clears detaches.\n\t */\n\tseedFrom(zoom: number, previous: ReadonlyMap<string, ClusterNode>): void\n\t/** Advance/retreat the cursor for a camera change. No-op if zoom sits inside all bands. */\n\tonCamera(zoom: number): void\n\t/**\n\t * Remove one leaf from the displayed partition without touching the event table. Local by\n\t * construction: only the nodes containing the leaf change \u2014 a badge shrinks in place (count\n\t * and centroid recomputed from its remaining members), a pair collapses to its surviving\n\t * leaf, and the leaf on its own disappears. Everything else is untouched, so a deletion,\n\t * pop-out, or thread-open never re-flows the rest of the document. The table's thresholds\n\t * around the detached leaf go stale; the caller is expected to hold a corrected rebuild and\n\t * adopt it (with seedFrom) at the next zoom-out. Unknown or already-detached ids are no-ops.\n\t */\n\tdetachLeaf(leafId: string): void\n\t/** The displayed partition: cluster id \u2192 node, with detaches applied. Do not mutate. */\n\tgetVisible(): ReadonlyMap<string, ClusterNode>\n}\n\n/** @public */\nexport function createClusterRuntime(table: ClusterTable): ClusterRuntime {\n\treturn new ClusterRuntimeImpl(table)\n}\n\nclass ClusterRuntimeImpl implements ClusterRuntime {\n\t// mutable internally; readonly through the ClusterRuntime interface\n\tk = 0\n\tversion = 0\n\t// Structural partition: exactly leaves + applied events, never patched. The cursor invariants\n\t// live here, untouched by detaches \u2014 resolution to the displayed partition happens at read\n\t// time in getVisible().\n\tprivate visible = new Map<string, ClusterNode>()\n\tprivate seeded = false\n\t// Indices (< k) of band events held unmerged by seedFrom carryover. The single cursor can\n\t// only express \"merged up to here\", but a carried-over partition can be \"merged except these\"\n\t// \u2014 the exceptions live here. Self-draining: an entry leaves via onCamera when the zoom\n\t// crosses its own zMerge (merges) or its zSplit (the split walk retreats past it).\n\tprivate suppressed = new Set<number>()\n\t// Leaves removed from the displayed partition (deleted / popped out / opened). Patches map\n\t// each structural node containing a detached leaf to its displayed replacement (or null to\n\t// drop it). Rebuilt from the detached set; cleared by seed/seedFrom.\n\tprivate detached = new Set<string>()\n\tprivate patched = new Map<string, ClusterNode | null>()\n\tprivate leafById: Map<string, ClusterNode> | null = null\n\tprivate resolvedCache: { version: number; map: Map<string, ClusterNode> } | null = null\n\n\tconstructor(private readonly table: ClusterTable) {\n\t\tthis.resetVisible()\n\t}\n\n\tgetSuppressedCount(): number {\n\t\treturn this.suppressed.size\n\t}\n\n\tgetDetachedCount(): number {\n\t\treturn this.detached.size\n\t}\n\n\tseed(zoom: number): void {\n\t\tvalidateZoom(zoom)\n\t\tthis.k = seedCount(this.table.events, zoom)\n\t\tthis.suppressed.clear()\n\t\tthis.detached.clear()\n\t\tthis.patched.clear()\n\t\tthis.resetVisible()\n\t\tfor (let i = 0; i < this.k; i++) {\n\t\t\tapplyEvent(this.visible, this.table.events[i])\n\t\t}\n\t\tthis.seeded = true\n\t\tthis.version++\n\t}\n\n\tseedFrom(zoom: number, previous: ReadonlyMap<string, ClusterNode>): void {\n\t\tvalidateZoom(zoom)\n\t\tconst ownerByMember = new Map<string, string>()\n\t\tfor (const node of previous.values()) {\n\t\t\tfor (const member of node.members) {\n\t\t\t\townerByMember.set(member, node.id)\n\t\t\t}\n\t\t}\n\n\t\t// Cut the cursor purely on split thresholds. zSplit is non-increasing down the table, so\n\t\t// everything past the first mandatory split (zoom >= zSplit) is also mandatorily split \u2014\n\t\t// this cut can never conflict with an event that should be merged.\n\t\tconst events = this.table.events\n\t\tlet k = 0\n\t\twhile (k < events.length && zoom < events[k].zSplit) {\n\t\t\tk++\n\t\t}\n\n\t\tthis.k = k\n\t\tthis.suppressed.clear()\n\t\tthis.detached.clear()\n\t\tthis.patched.clear()\n\t\tthis.resetVisible()\n\t\tfor (let i = 0; i < k; i++) {\n\t\t\tconst event = events[i]\n\t\t\tif (zoom > event.zMerge && !wasMergedTogether(event.result.members, ownerByMember)) {\n\t\t\t\t// In its band and previously unmerged: keep it unmerged, as an exception inside\n\t\t\t\t// the cursor. Dependency-safe: an applied event can never consume a suppressed\n\t\t\t\t// result \u2014 merged members imply merged (subset) children, and threshold-forced\n\t\t\t\t// events force their children too (zMerge is non-increasing down the table).\n\t\t\t\tthis.suppressed.add(i)\n\t\t\t} else {\n\t\t\t\tapplyEvent(this.visible, event)\n\t\t\t}\n\t\t}\n\t\tthis.seeded = true\n\t\tthis.version++\n\t}\n\n\tonCamera(zoom: number): void {\n\t\tvalidateZoom(zoom)\n\t\tif (!this.seeded) {\n\t\t\tthrow new Error('Cluster runtime must be seeded before onCamera')\n\t\t}\n\n\t\tlet changed = false\n\t\twhile (this.k < this.table.events.length && zoom <= this.table.events[this.k].zMerge) {\n\t\t\tapplyEvent(this.visible, this.table.events[this.k])\n\t\t\tthis.k++\n\t\t\tchanged = true\n\t\t}\n\t\twhile (this.k > 0 && zoom >= this.table.events[this.k - 1].zSplit) {\n\t\t\tthis.k--\n\t\t\t// A suppressed event was never applied \u2014 retreating past it is bookkeeping only.\n\t\t\tif (!this.suppressed.delete(this.k)) {\n\t\t\t\tunapplyEvent(this.visible, this.table.events[this.k])\n\t\t\t\tchanged = true\n\t\t\t}\n\t\t}\n\t\t// Heal suppressed events at their own merge threshold: a zoom-out past zMerge merges a\n\t\t// held-out band event exactly as if it had still been ahead of the cursor. Set iteration\n\t\t// is insertion order (ascending index), so a healed event's suppressed children (larger\n\t\t// zMerge, smaller index) always heal before it.\n\t\tif (this.suppressed.size > 0) {\n\t\t\tfor (const i of [...this.suppressed]) {\n\t\t\t\tif (zoom <= this.table.events[i].zMerge) {\n\t\t\t\t\tthis.suppressed.delete(i)\n\t\t\t\t\tapplyEvent(this.visible, this.table.events[i])\n\t\t\t\t\tchanged = true\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif (changed) this.version++\n\t}\n\n\tdetachLeaf(leafId: string): void {\n\t\tif (this.detached.has(leafId)) return\n\t\tif (!this.getLeafById().has(leafId)) return\n\t\tthis.detached.add(leafId)\n\t\tthis.rebuildPatches()\n\t\tthis.version++\n\t}\n\n\tgetVisible(): ReadonlyMap<string, ClusterNode> {\n\t\tif (this.resolvedCache && this.resolvedCache.version === this.version) {\n\t\t\treturn this.resolvedCache.map\n\t\t}\n\t\tlet map: Map<string, ClusterNode>\n\t\tif (this.patched.size === 0) {\n\t\t\tmap = this.visible\n\t\t} else {\n\t\t\tmap = new Map()\n\t\t\tfor (const node of this.visible.values()) {\n\t\t\t\tconst resolved = this.patched.has(node.id) ? this.patched.get(node.id)! : node\n\t\t\t\tif (resolved) map.set(resolved.id, resolved)\n\t\t\t}\n\t\t}\n\t\tthis.resolvedCache = { version: this.version, map }\n\t\treturn map\n\t}\n\n\tprivate getLeafById(): Map<string, ClusterNode> {\n\t\tif (!this.leafById) {\n\t\t\tthis.leafById = new Map(this.table.leaves.map((leaf) => [leaf.id, leaf]))\n\t\t}\n\t\treturn this.leafById\n\t}\n\n\t/** Recompute the patch map from the detached set. A node is patched iff it contains a\n\t * detached member; the patch drops those members and recomputes count/centroid, collapsing\n\t * to the surviving leaf node at count 1 and to nothing at count 0. Patched nodes keep their\n\t * structural id, so cursor events keep addressing them. */\n\tprivate rebuildPatches() {\n\t\tthis.patched.clear()\n\t\tconst leafById = this.getLeafById()\n\t\tfor (const id of this.detached) {\n\t\t\tthis.patched.set(id, null)\n\t\t}\n\t\tconst patch = (node: ClusterNode) => {\n\t\t\tif (node.members.length === 1 || !node.members.some((m) => this.detached.has(m))) return\n\t\t\tconst members = node.members.filter((m) => !this.detached.has(m))\n\t\t\tif (members.length === 0) {\n\t\t\t\tthis.patched.set(node.id, null)\n\t\t\t} else if (members.length === 1) {\n\t\t\t\tthis.patched.set(node.id, leafById.get(members[0])!)\n\t\t\t} else {\n\t\t\t\tlet x = 0\n\t\t\t\tlet y = 0\n\t\t\t\tfor (const m of members) {\n\t\t\t\t\tconst leaf = leafById.get(m)!\n\t\t\t\t\tx += leaf.centroid.x\n\t\t\t\t\ty += leaf.centroid.y\n\t\t\t\t}\n\t\t\t\tthis.patched.set(node.id, {\n\t\t\t\t\tid: node.id,\n\t\t\t\t\tcentroid: { x: x / members.length, y: y / members.length },\n\t\t\t\t\tcount: members.length,\n\t\t\t\t\tmembers,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\tfor (const event of this.table.events) {\n\t\t\tpatch(event.result)\n\t\t\tfor (const child of event.children) patch(child)\n\t\t}\n\t}\n\n\tprivate resetVisible() {\n\t\tthis.visible = new Map()\n\t\tfor (const leaf of this.table.leaves) {\n\t\t\tthis.visible.set(leaf.id, leaf)\n\t\t}\n\t}\n}\n\n/** True iff every member id belonged to the same node of the previous partition. */\nfunction wasMergedTogether(\n\tmembers: readonly string[],\n\townerByMember: ReadonlyMap<string, string>\n): boolean {\n\tconst owner = ownerByMember.get(members[0])\n\tif (owner === undefined) return false\n\tfor (let i = 1; i < members.length; i++) {\n\t\tif (ownerByMember.get(members[i]) !== owner) return false\n\t}\n\treturn true\n}\n\nfunction seedCount(events: readonly MergeEvent[], zoom: number): number {\n\tlet lo = 0\n\tlet hi = events.length\n\twhile (lo < hi) {\n\t\tconst mid = (lo + hi) >> 1\n\t\tif (zoom <= seedThreshold(events[mid])) {\n\t\t\tlo = mid + 1\n\t\t} else {\n\t\t\thi = mid\n\t\t}\n\t}\n\treturn lo\n}\n\nfunction seedThreshold(event: MergeEvent): number {\n\treturn Math.sqrt(event.zMerge * event.zSplit)\n}\n\nfunction applyEvent(visible: Map<string, ClusterNode>, event: MergeEvent) {\n\tfor (const child of event.children) {\n\t\tvisible.delete(child.id)\n\t}\n\tvisible.set(event.result.id, event.result)\n}\n\nfunction unapplyEvent(visible: Map<string, ClusterNode>, event: MergeEvent) {\n\tvisible.delete(event.result.id)\n\tfor (const child of event.children) {\n\t\tvisible.set(child.id, child)\n\t}\n}\n\nfunction validateZoom(zoom: number) {\n\tif (!Number.isFinite(zoom) || zoom <= 0) {\n\t\tthrow new Error('zoom must be finite and greater than 0')\n\t}\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist-esm/index.d.mts
CHANGED
|
@@ -170,23 +170,43 @@ export declare interface ClusterOptions {
|
|
|
170
170
|
|
|
171
171
|
/** @public */
|
|
172
172
|
export declare interface ClusterRuntime {
|
|
173
|
-
/** Events[0..k) are
|
|
173
|
+
/** Events[0..k) are in the cursor (applied unless suppressed). Exposed for tests/debugging. */
|
|
174
174
|
readonly k: number;
|
|
175
|
-
/**
|
|
175
|
+
/**
|
|
176
|
+
* Increments whenever the displayed partition changes. Subscribe to this, not `k`: detaches
|
|
177
|
+
* and suppressed-event healing change the partition without moving the cursor.
|
|
178
|
+
*/
|
|
179
|
+
readonly version: number;
|
|
180
|
+
/** Number of suppressed band events currently inside the cursor. Exposed for tests. */
|
|
181
|
+
getSuppressedCount(): number;
|
|
182
|
+
/** Number of leaves currently detached from the displayed partition. Exposed for tests. */
|
|
183
|
+
getDetachedCount(): number;
|
|
184
|
+
/** Reset state from scratch for the given zoom (cold start / after rebuild). Clears detaches. */
|
|
176
185
|
seed(zoom: number): void;
|
|
177
186
|
/**
|
|
178
187
|
* Reset state for the given zoom, carrying hysteresis state over from a previous partition
|
|
179
188
|
* (the visible map of the model being replaced). Threshold-forced events ignore history:
|
|
180
|
-
* zoom \<= zMerge is always
|
|
181
|
-
*
|
|
182
|
-
* (e.g. introduced by the rebuild) stays
|
|
183
|
-
*
|
|
184
|
-
*
|
|
189
|
+
* zoom \<= zMerge is always merged, zoom \>= zSplit always split. An event inside its band
|
|
190
|
+
* keeps its previous state: merged iff its members were merged together in `previous`; a band
|
|
191
|
+
* event that was unmerged (or has no history, e.g. introduced by the rebuild) stays unmerged —
|
|
192
|
+
* inside the cursor but suppressed — until a zoom-out crosses its own zMerge, exactly like any
|
|
193
|
+
* other pending merge. Carryover is exact: no group changes state because of the swap alone.
|
|
194
|
+
* Clears detaches.
|
|
185
195
|
*/
|
|
186
196
|
seedFrom(zoom: number, previous: ReadonlyMap<string, ClusterNode>): void;
|
|
187
197
|
/** Advance/retreat the cursor for a camera change. No-op if zoom sits inside all bands. */
|
|
188
198
|
onCamera(zoom: number): void;
|
|
189
|
-
/**
|
|
199
|
+
/**
|
|
200
|
+
* Remove one leaf from the displayed partition without touching the event table. Local by
|
|
201
|
+
* construction: only the nodes containing the leaf change — a badge shrinks in place (count
|
|
202
|
+
* and centroid recomputed from its remaining members), a pair collapses to its surviving
|
|
203
|
+
* leaf, and the leaf on its own disappears. Everything else is untouched, so a deletion,
|
|
204
|
+
* pop-out, or thread-open never re-flows the rest of the document. The table's thresholds
|
|
205
|
+
* around the detached leaf go stale; the caller is expected to hold a corrected rebuild and
|
|
206
|
+
* adopt it (with seedFrom) at the next zoom-out. Unknown or already-detached ids are no-ops.
|
|
207
|
+
*/
|
|
208
|
+
detachLeaf(leafId: string): void;
|
|
209
|
+
/** The displayed partition: cluster id → node, with detaches applied. Do not mutate. */
|
|
190
210
|
getVisible(): ReadonlyMap<string, ClusterNode>;
|
|
191
211
|
}
|
|
192
212
|
|
package/dist-esm/index.mjs
CHANGED
package/dist-esm/ui/avatar.mjs
CHANGED
|
@@ -5,12 +5,20 @@ function initial(name) {
|
|
|
5
5
|
}
|
|
6
6
|
function Avatar({ name, color, image }) {
|
|
7
7
|
if (image) {
|
|
8
|
-
return /* @__PURE__ */ jsx(
|
|
8
|
+
return /* @__PURE__ */ jsx(
|
|
9
|
+
"img",
|
|
10
|
+
{
|
|
11
|
+
className: "tlui-cmt-avatar tlui-cmt-avatar--image",
|
|
12
|
+
src: image,
|
|
13
|
+
alt: "",
|
|
14
|
+
"aria-hidden": "true"
|
|
15
|
+
}
|
|
16
|
+
);
|
|
9
17
|
}
|
|
10
18
|
return /* @__PURE__ */ jsx(
|
|
11
19
|
"div",
|
|
12
20
|
{
|
|
13
|
-
className: "cmt-avatar",
|
|
21
|
+
className: "tlui-cmt-avatar",
|
|
14
22
|
"aria-hidden": "true",
|
|
15
23
|
style: color ? { backgroundColor: color } : void 0,
|
|
16
24
|
children: initial(name)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/ui/avatar.tsx"],
|
|
4
|
-
"sourcesContent": ["import { getFirstCharacter } from 'tldraw'\n\n/** @public */\nexport interface AvatarProps {\n\tname: string\n\t/** Background colour, used when there's no `image`. Falls back to the default avatar tint. */\n\tcolor?: string\n\t/** Avatar image URL. When set, shows the image instead of the coloured initial. */\n\timage?: string\n}\n\nfunction initial(name: string) {\n\treturn (getFirstCharacter(name.trim()) || '?').toUpperCase()\n}\n\n/** A commenter's avatar \u2014 their image if provided, otherwise a single-initial coloured circle.\n * @public @react */\nexport function Avatar({ name, color, image }: AvatarProps) {\n\tif (image) {\n\t\treturn <img
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import { getFirstCharacter } from 'tldraw'\n\n/** @public */\nexport interface AvatarProps {\n\tname: string\n\t/** Background colour, used when there's no `image`. Falls back to the default avatar tint. */\n\tcolor?: string\n\t/** Avatar image URL. When set, shows the image instead of the coloured initial. */\n\timage?: string\n}\n\nfunction initial(name: string) {\n\treturn (getFirstCharacter(name.trim()) || '?').toUpperCase()\n}\n\n/** A commenter's avatar \u2014 their image if provided, otherwise a single-initial coloured circle.\n * @public @react */\nexport function Avatar({ name, color, image }: AvatarProps) {\n\tif (image) {\n\t\treturn (\n\t\t\t<img\n\t\t\t\tclassName=\"tlui-cmt-avatar tlui-cmt-avatar--image\"\n\t\t\t\tsrc={image}\n\t\t\t\talt=\"\"\n\t\t\t\taria-hidden=\"true\"\n\t\t\t/>\n\t\t)\n\t}\n\treturn (\n\t\t<div\n\t\t\tclassName=\"tlui-cmt-avatar\"\n\t\t\taria-hidden=\"true\"\n\t\t\tstyle={color ? { backgroundColor: color } : undefined}\n\t\t>\n\t\t\t{initial(name)}\n\t\t</div>\n\t)\n}\n"],
|
|
5
|
+
"mappings": "AAoBG;AApBH,SAAS,yBAAyB;AAWlC,SAAS,QAAQ,MAAc;AAC9B,UAAQ,kBAAkB,KAAK,KAAK,CAAC,KAAK,KAAK,YAAY;AAC5D;AAIO,SAAS,OAAO,EAAE,MAAM,OAAO,MAAM,GAAgB;AAC3D,MAAI,OAAO;AACV,WACC;AAAA,MAAC;AAAA;AAAA,QACA,WAAU;AAAA,QACV,KAAK;AAAA,QACL,KAAI;AAAA,QACJ,eAAY;AAAA;AAAA,IACb;AAAA,EAEF;AACA,SACC;AAAA,IAAC;AAAA;AAAA,MACA,WAAU;AAAA,MACV,eAAY;AAAA,MACZ,OAAO,QAAQ,EAAE,iBAAiB,MAAM,IAAI;AAAA,MAE3C,kBAAQ,IAAI;AAAA;AAAA,EACd;AAEF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist-esm/ui/byline.mjs
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { formatRelativeTime } from "./format-time.mjs";
|
|
3
3
|
function Byline({ author, date, edited }) {
|
|
4
|
-
return /* @__PURE__ */ jsxs("div", { className: "cmt-head", children: [
|
|
5
|
-
/* @__PURE__ */ jsx("span", { className: "cmt-author", children: author }),
|
|
6
|
-
/* @__PURE__ */ jsxs("span", { className: "cmt-time", children: [
|
|
4
|
+
return /* @__PURE__ */ jsxs("div", { className: "tlui-cmt-head", children: [
|
|
5
|
+
/* @__PURE__ */ jsx("span", { className: "tlui-cmt-author", children: author }),
|
|
6
|
+
/* @__PURE__ */ jsxs("span", { className: "tlui-cmt-time", children: [
|
|
7
7
|
formatRelativeTime(date),
|
|
8
|
-
edited && /* @__PURE__ */ jsx("span", { className: "cmt-edited", children: " \xB7 edited" })
|
|
8
|
+
edited && /* @__PURE__ */ jsx("span", { className: "tlui-cmt-edited", children: " \xB7 edited" })
|
|
9
9
|
] })
|
|
10
10
|
] });
|
|
11
11
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/ui/byline.tsx"],
|
|
4
|
-
"sourcesContent": ["import { formatRelativeTime } from './format-time'\n\n/** @public */\nexport interface BylineProps {\n\tauthor: string\n\t/** ISO datetime; formatted to relative time by the component. */\n\tdate: string\n\t/** Shows an \"edited\" marker when the comment has been edited. */\n\tedited?: boolean\n}\n\n/** A comment's metadata line: author name, relative time, and an edited marker. @public @react */\nexport function Byline({ author, date, edited }: BylineProps) {\n\treturn (\n\t\t<div className=\"cmt-head\">\n\t\t\t<span className=\"cmt-author\">{author}</span>\n\t\t\t<span className=\"cmt-time\">\n\t\t\t\t{formatRelativeTime(date)}\n\t\t\t\t{edited && <span className=\"cmt-edited\"> \u00B7 edited</span>}\n\t\t\t</span>\n\t\t</div>\n\t)\n}\n"],
|
|
5
|
-
"mappings": "AAeG,cACA,YADA;AAfH,SAAS,0BAA0B;AAY5B,SAAS,OAAO,EAAE,QAAQ,MAAM,OAAO,GAAgB;AAC7D,SACC,qBAAC,SAAI,WAAU,
|
|
4
|
+
"sourcesContent": ["import { formatRelativeTime } from './format-time'\n\n/** @public */\nexport interface BylineProps {\n\tauthor: string\n\t/** ISO datetime; formatted to relative time by the component. */\n\tdate: string\n\t/** Shows an \"edited\" marker when the comment has been edited. */\n\tedited?: boolean\n}\n\n/** A comment's metadata line: author name, relative time, and an edited marker. @public @react */\nexport function Byline({ author, date, edited }: BylineProps) {\n\treturn (\n\t\t<div className=\"tlui-cmt-head\">\n\t\t\t<span className=\"tlui-cmt-author\">{author}</span>\n\t\t\t<span className=\"tlui-cmt-time\">\n\t\t\t\t{formatRelativeTime(date)}\n\t\t\t\t{edited && <span className=\"tlui-cmt-edited\"> \u00B7 edited</span>}\n\t\t\t</span>\n\t\t</div>\n\t)\n}\n"],
|
|
5
|
+
"mappings": "AAeG,cACA,YADA;AAfH,SAAS,0BAA0B;AAY5B,SAAS,OAAO,EAAE,QAAQ,MAAM,OAAO,GAAgB;AAC7D,SACC,qBAAC,SAAI,WAAU,iBACd;AAAA,wBAAC,UAAK,WAAU,mBAAmB,kBAAO;AAAA,IAC1C,qBAAC,UAAK,WAAU,iBACd;AAAA,yBAAmB,IAAI;AAAA,MACvB,UAAU,oBAAC,UAAK,WAAU,mBAAkB,0BAAS;AAAA,OACvD;AAAA,KACD;AAEF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,13 +2,13 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Avatar } from "./avatar.mjs";
|
|
3
3
|
import { Byline } from "./byline.mjs";
|
|
4
4
|
function CommentCard({ author, body, date, you, edited, actions }) {
|
|
5
|
-
return /* @__PURE__ */ jsxs("div", { className: you ? "cmt-card cmt-card--you" : "cmt-card", children: [
|
|
5
|
+
return /* @__PURE__ */ jsxs("div", { className: you ? "tlui-cmt-card tlui-cmt-card--you" : "tlui-cmt-card", children: [
|
|
6
6
|
/* @__PURE__ */ jsx(Avatar, { name: author }),
|
|
7
|
-
/* @__PURE__ */ jsxs("div", { className: "cmt-body", children: [
|
|
7
|
+
/* @__PURE__ */ jsxs("div", { className: "tlui-cmt-body", children: [
|
|
8
8
|
/* @__PURE__ */ jsx(Byline, { author, date, edited }),
|
|
9
9
|
body
|
|
10
10
|
] }),
|
|
11
|
-
actions !== void 0 && /* @__PURE__ */ jsx("div", { className: "cmt-card__actions", children: actions })
|
|
11
|
+
actions !== void 0 && /* @__PURE__ */ jsx("div", { className: "tlui-cmt-card__actions", children: actions })
|
|
12
12
|
] });
|
|
13
13
|
}
|
|
14
14
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/ui/comment-card.tsx"],
|
|
4
|
-
"sourcesContent": ["import { ReactNode } from 'react'\nimport { Avatar } from './avatar'\nimport { Byline } from './byline'\n\n/** @public */\nexport interface CommentCardProps {\n\tauthor: string\n\t/** The rendered comment body. The card doesn't dictate a format \u2014 pass a `<CommentText>`\n\t * for markdown, a rich-text render, or any node. */\n\tbody: ReactNode\n\t/** ISO datetime; formatted to relative time by the component. */\n\tdate: string\n\tyou: boolean\n\t/** Whether the comment has been edited (shows an \"edited\" marker). */\n\tedited?: boolean\n\t/** Hover-revealed controls at the card's top-right (e.g. an edit affordance). */\n\tactions?: ReactNode\n}\n\n/** A single comment: Avatar, Byline, and a body slot the consumer renders. @public @react */\nexport function CommentCard({ author, body, date, you, edited, actions }: CommentCardProps) {\n\treturn (\n\t\t<div className={you ? 'cmt-card cmt-card--you' : 'cmt-card'}>\n\t\t\t<Avatar name={author} />\n\t\t\t<div className=\"cmt-body\">\n\t\t\t\t<Byline author={author} date={date} edited={edited} />\n\t\t\t\t{body}\n\t\t\t</div>\n\t\t\t{actions !== undefined && <div className=\"cmt-card__actions\">{actions}</div>}\n\t\t</div>\n\t)\n}\n"],
|
|
5
|
-
"mappings": "AAuBG,cACA,YADA;AAtBH,SAAS,cAAc;AACvB,SAAS,cAAc;AAkBhB,SAAS,YAAY,EAAE,QAAQ,MAAM,MAAM,KAAK,QAAQ,QAAQ,GAAqB;AAC3F,SACC,qBAAC,SAAI,WAAW,MAAM,
|
|
4
|
+
"sourcesContent": ["import { ReactNode } from 'react'\nimport { Avatar } from './avatar'\nimport { Byline } from './byline'\n\n/** @public */\nexport interface CommentCardProps {\n\tauthor: string\n\t/** The rendered comment body. The card doesn't dictate a format \u2014 pass a `<CommentText>`\n\t * for markdown, a rich-text render, or any node. */\n\tbody: ReactNode\n\t/** ISO datetime; formatted to relative time by the component. */\n\tdate: string\n\tyou: boolean\n\t/** Whether the comment has been edited (shows an \"edited\" marker). */\n\tedited?: boolean\n\t/** Hover-revealed controls at the card's top-right (e.g. an edit affordance). */\n\tactions?: ReactNode\n}\n\n/** A single comment: Avatar, Byline, and a body slot the consumer renders. @public @react */\nexport function CommentCard({ author, body, date, you, edited, actions }: CommentCardProps) {\n\treturn (\n\t\t<div className={you ? 'tlui-cmt-card tlui-cmt-card--you' : 'tlui-cmt-card'}>\n\t\t\t<Avatar name={author} />\n\t\t\t<div className=\"tlui-cmt-body\">\n\t\t\t\t<Byline author={author} date={date} edited={edited} />\n\t\t\t\t{body}\n\t\t\t</div>\n\t\t\t{actions !== undefined && <div className=\"tlui-cmt-card__actions\">{actions}</div>}\n\t\t</div>\n\t)\n}\n"],
|
|
5
|
+
"mappings": "AAuBG,cACA,YADA;AAtBH,SAAS,cAAc;AACvB,SAAS,cAAc;AAkBhB,SAAS,YAAY,EAAE,QAAQ,MAAM,MAAM,KAAK,QAAQ,QAAQ,GAAqB;AAC3F,SACC,qBAAC,SAAI,WAAW,MAAM,qCAAqC,iBAC1D;AAAA,wBAAC,UAAO,MAAM,QAAQ;AAAA,IACtB,qBAAC,SAAI,WAAU,iBACd;AAAA,0BAAC,UAAO,QAAgB,MAAY,QAAgB;AAAA,MACnD;AAAA,OACF;AAAA,IACC,YAAY,UAAa,oBAAC,SAAI,WAAU,0BAA0B,mBAAQ;AAAA,KAC5E;AAEF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Extension } from "@tiptap/core";
|
|
3
2
|
import { EditorContent, useEditor } from "@tiptap/react";
|
|
4
3
|
import { useEffect, useMemo, useRef, useState } from "react";
|
|
5
4
|
import { isEqual, useMaybeEditor } from "tldraw";
|
|
@@ -34,26 +33,12 @@ function CommentComposer({
|
|
|
34
33
|
const renderMentionSuggestionRef = useRef(renderMentionSuggestion);
|
|
35
34
|
renderMentionSuggestionRef.current = renderMentionSuggestion;
|
|
36
35
|
const [isEmpty, setIsEmpty] = useState(() => !value || isCommentEmpty(value));
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
name: "commentComposerSubmit",
|
|
40
|
-
priority: 1e3,
|
|
41
|
-
addKeyboardShortcuts() {
|
|
42
|
-
return {
|
|
43
|
-
Enter: () => {
|
|
44
|
-
if (isMentionPickerOpen()) return false;
|
|
45
|
-
if (!disabledRef.current) onSubmitRef.current?.();
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
}),
|
|
51
|
-
[]
|
|
52
|
-
);
|
|
36
|
+
const editorRef = useRef(null);
|
|
37
|
+
const replayingEnter = useRef(false);
|
|
53
38
|
const mentionsEnabled = !!getMentionSuggestions;
|
|
54
39
|
const hasCustomRow = !!renderMentionSuggestion;
|
|
55
40
|
const extensions = useMemo(() => {
|
|
56
|
-
const list = [...commentTipTapExtensions
|
|
41
|
+
const list = [...commentTipTapExtensions];
|
|
57
42
|
if (mentionsEnabled) {
|
|
58
43
|
const resolveSuggestions = (query) => getMentionSuggestionsRef.current?.(query) ?? [];
|
|
59
44
|
const renderRow = hasCustomRow ? (member) => renderMentionSuggestionRef.current?.(member) : void 0;
|
|
@@ -69,7 +54,7 @@ function CommentComposer({
|
|
|
69
54
|
list.push(commentMention({ suggestion: { char: "@", allow: () => false } }));
|
|
70
55
|
}
|
|
71
56
|
return list;
|
|
72
|
-
}, [
|
|
57
|
+
}, [mentionsEnabled, hasCustomRow, tlEditor]);
|
|
73
58
|
const editor = useEditor(
|
|
74
59
|
{
|
|
75
60
|
extensions,
|
|
@@ -80,7 +65,27 @@ function CommentComposer({
|
|
|
80
65
|
// mirrors RichTextArea's setup.
|
|
81
66
|
enableCoreExtensions: { textDirection: false },
|
|
82
67
|
textDirection: "auto",
|
|
83
|
-
editorProps: {
|
|
68
|
+
editorProps: {
|
|
69
|
+
attributes: { class: "tlui-cmt-input" },
|
|
70
|
+
// Runs before every keymap plugin, so it can distinguish Shift+Enter from Enter — an
|
|
71
|
+
// `Enter` keymap binding also fires on Shift+Enter and would otherwise swallow it.
|
|
72
|
+
handleKeyDown: (_view, event) => {
|
|
73
|
+
if (replayingEnter.current) return false;
|
|
74
|
+
if (event.key !== "Enter" || event.isComposing) return false;
|
|
75
|
+
if (isMentionPickerOpen()) return false;
|
|
76
|
+
if (event.shiftKey && !event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
77
|
+
replayingEnter.current = true;
|
|
78
|
+
try {
|
|
79
|
+
editorRef.current?.commands.enter();
|
|
80
|
+
} finally {
|
|
81
|
+
replayingEnter.current = false;
|
|
82
|
+
}
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
if (!disabledRef.current) onSubmitRef.current?.();
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
},
|
|
84
89
|
onUpdate: ({ editor: editor2 }) => {
|
|
85
90
|
setIsEmpty(editor2.isEmpty);
|
|
86
91
|
onChangeRef.current?.(editor2.getJSON());
|
|
@@ -88,6 +93,7 @@ function CommentComposer({
|
|
|
88
93
|
},
|
|
89
94
|
[interactive]
|
|
90
95
|
);
|
|
96
|
+
editorRef.current = editor;
|
|
91
97
|
useEffect(() => {
|
|
92
98
|
if (!editor || value === void 0) return;
|
|
93
99
|
if (isEqual(editor.getJSON(), value)) return;
|
|
@@ -99,12 +105,12 @@ function CommentComposer({
|
|
|
99
105
|
const raf = requestAnimationFrame(() => editor.commands.focus("end"));
|
|
100
106
|
return () => cancelAnimationFrame(raf);
|
|
101
107
|
}, [autoFocus, editor]);
|
|
102
|
-
return /* @__PURE__ */ jsxs("div", { className: "cmt-composer", children: [
|
|
108
|
+
return /* @__PURE__ */ jsxs("div", { className: "tlui-cmt-composer", children: [
|
|
103
109
|
leading ?? /* @__PURE__ */ jsx(Avatar, { name: author }),
|
|
104
|
-
/* @__PURE__ */ jsxs("div", { className: "cmt-composer__field", children: [
|
|
105
|
-
/* @__PURE__ */ jsxs("div", { className: "cmt-composer__input-wrap", children: [
|
|
110
|
+
/* @__PURE__ */ jsxs("div", { className: "tlui-cmt-composer__field", children: [
|
|
111
|
+
/* @__PURE__ */ jsxs("div", { className: "tlui-cmt-composer__input-wrap", children: [
|
|
106
112
|
/* @__PURE__ */ jsx(EditorContent, { editor }),
|
|
107
|
-
isEmpty && /* @__PURE__ */ jsx("div", { className: "cmt-input__placeholder", "aria-hidden": "true", children: placeholder })
|
|
113
|
+
isEmpty && /* @__PURE__ */ jsx("div", { className: "tlui-cmt-input__placeholder", "aria-hidden": "true", children: placeholder })
|
|
108
114
|
] }),
|
|
109
115
|
interactive && /* @__PURE__ */ jsx(SendButton, { label: sendLabel, onClick: onSubmit, disabled })
|
|
110
116
|
] })
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/ui/comment-composer.tsx"],
|
|
4
|
-
"sourcesContent": ["import {
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import { JSONContent } from '@tiptap/core'\nimport { EditorContent, useEditor } from '@tiptap/react'\nimport { ReactNode, useEffect, useMemo, useRef, useState } from 'react'\nimport { isEqual, TLRichText, useMaybeEditor } from 'tldraw'\nimport { Avatar } from './avatar'\nimport { commentTipTapExtensions, EMPTY_COMMENT, isCommentEmpty } from './comment-extensions'\nimport { commentMention } from './comment-mention'\nimport { MentionMember } from './mention-list'\nimport { createMentionSuggestion, isMentionPickerOpen } from './mention-suggestion'\nimport { SendButton } from './send-button'\n\n/** @public */\nexport interface CommentComposerProps {\n\tauthor: string\n\tplaceholder: string\n\t/** Controlled rich-text value. Omit for the presentational (display-only) composer. */\n\tvalue?: TLRichText\n\tonChange?(value: TLRichText): void\n\t/** Called on Send click or Enter. When set, the composer is interactive. */\n\tonSubmit?(): void\n\tsendLabel?: string\n\tdisabled?: boolean\n\tautoFocus?: boolean\n\t/** The leading element before the field. Defaults to the author's avatar. */\n\tleading?: ReactNode\n\t/** Resolve the members matching an `@`-query (sync or async). Provide to enable mentions. */\n\tgetMentionSuggestions?(query: string): MentionMember[] | Promise<MentionMember[]>\n\t/** Override a picker row's content. Defaults to avatar + name (+ secondary). */\n\trenderMentionSuggestion?(member: MentionMember): ReactNode\n}\n\n/**\n * The input for writing a comment: a TipTap rich-text editor restricted to the comment extension\n * set (bold, italic, lists, links, code, highlight \u2014 no headings), with a Send button. Formatting\n * is applied through markdown and keyboard shortcuts (e.g. `**bold**`, `- `, Cmd+B); there's no\n * floating toolbar. Presentational by default; pass value/onChange/onSubmit to drive it as a form.\n * @public @react\n */\nexport function CommentComposer({\n\tauthor,\n\tplaceholder,\n\tvalue,\n\tonChange,\n\tonSubmit,\n\tsendLabel = 'Send',\n\tdisabled,\n\tautoFocus,\n\tleading,\n\tgetMentionSuggestions,\n\trenderMentionSuggestion,\n}: CommentComposerProps) {\n\tconst interactive = !!onChange || !!onSubmit\n\t// The canvas editor the composer lives in, if any \u2014 lets the mention popup track the camera. Null\n\t// when the composer is used outside a tldraw editor (e.g. an isolated demo).\n\tconst tlEditor = useMaybeEditor()\n\n\t// Callbacks are read through refs so the editor instance doesn't need to be recreated when they\n\t// change identity between renders.\n\tconst onChangeRef = useRef(onChange)\n\tonChangeRef.current = onChange\n\tconst onSubmitRef = useRef(onSubmit)\n\tonSubmitRef.current = onSubmit\n\tconst disabledRef = useRef(disabled)\n\tdisabledRef.current = disabled\n\tconst getMentionSuggestionsRef = useRef(getMentionSuggestions)\n\tgetMentionSuggestionsRef.current = getMentionSuggestions\n\tconst renderMentionSuggestionRef = useRef(renderMentionSuggestion)\n\trenderMentionSuggestionRef.current = renderMentionSuggestion\n\n\tconst [isEmpty, setIsEmpty] = useState(() => !value || isCommentEmpty(value))\n\n\t// The editor instance, reachable from `handleKeyDown` (which is created before `useEditor`\n\t// returns). Updated on every render so the ref never points at a stale editor.\n\tconst editorRef = useRef<ReturnType<typeof useEditor>>(null)\n\n\t// Set while we replay Enter through the keymaps for a Shift+Enter newline: `commands.enter()`\n\t// re-dispatches Enter through `handleKeyDown`, and without this guard our own handler would catch\n\t// that synthetic Enter and submit instead.\n\tconst replayingEnter = useRef(false)\n\n\t// Enter and Cmd/Ctrl+Enter submit the comment; Shift+Enter inserts a new line for the occasional\n\t// multi-line comment. This is handled through `editorProps.handleKeyDown` (below) rather than a\n\t// keyboard-shortcut extension: ProseMirror runs `handleKeyDown` before every keymap plugin, so it\n\t// can tell Shift+Enter apart from Enter \u2014 an `Enter` keymap binding also fires on Shift/Cmd+Enter\n\t// and would otherwise swallow the newline.\n\n\t// The suggestion plugin is built once (the editor is recreated only on `interactive`) and runs\n\t// outside React, so it must read the mention callbacks through refs \u2014 like onChange/onSubmit \u2014\n\t// or it queries the roster present at mount forever, never seeing a member who loads or joins\n\t// later. Whether mentions (and a custom picker row) are wired at all is fixed at mount; only the\n\t// callbacks themselves are live.\n\tconst mentionsEnabled = !!getMentionSuggestions\n\tconst hasCustomRow = !!renderMentionSuggestion\n\tconst extensions = useMemo(() => {\n\t\tconst list = [...commentTipTapExtensions]\n\t\t// Always register the mention node so an existing body that contains a mention keeps it on\n\t\t// edit (an unregistered node would be stripped by ProseMirror when the content loads). The `@`\n\t\t// picker itself only turns on when the host provides a resolver; otherwise the node is present\n\t\t// but its trigger is disabled.\n\t\tif (mentionsEnabled) {\n\t\t\tconst resolveSuggestions = (query: string) => getMentionSuggestionsRef.current?.(query) ?? []\n\t\t\tconst renderRow = hasCustomRow\n\t\t\t\t? (member: MentionMember) => renderMentionSuggestionRef.current?.(member)\n\t\t\t\t: undefined\n\t\t\tlist.push(\n\t\t\t\tcommentMention({\n\t\t\t\t\tsuggestion: createMentionSuggestion(resolveSuggestions, {\n\t\t\t\t\t\trenderMember: renderRow,\n\t\t\t\t\t\teditor: tlEditor,\n\t\t\t\t\t}),\n\t\t\t\t})\n\t\t\t)\n\t\t} else {\n\t\t\tlist.push(commentMention({ suggestion: { char: '@', allow: () => false } }))\n\t\t}\n\t\treturn list\n\t}, [mentionsEnabled, hasCustomRow, tlEditor])\n\n\tconst editor = useEditor(\n\t\t{\n\t\t\textensions,\n\t\t\tcontent: (value ?? EMPTY_COMMENT) as JSONContent,\n\t\t\teditable: interactive,\n\t\t\t// tldraw's default extensions add their own TextDirection extension (so it can be\n\t\t\t// overridden), so disable TipTap's core one to avoid a duplicate-extension warning \u2014\n\t\t\t// mirrors RichTextArea's setup.\n\t\t\tenableCoreExtensions: { textDirection: false },\n\t\t\ttextDirection: 'auto',\n\t\t\teditorProps: {\n\t\t\t\tattributes: { class: 'tlui-cmt-input' },\n\t\t\t\t// Runs before every keymap plugin, so it can distinguish Shift+Enter from Enter \u2014 an\n\t\t\t\t// `Enter` keymap binding also fires on Shift+Enter and would otherwise swallow it.\n\t\t\t\thandleKeyDown: (_view, event) => {\n\t\t\t\t\t// Let the keymaps handle the synthetic Enter we replay for a Shift+Enter newline.\n\t\t\t\t\tif (replayingEnter.current) return false\n\t\t\t\t\tif (event.key !== 'Enter' || event.isComposing) return false\n\t\t\t\t\t// While the @-mention picker is open, Enter selects the highlighted member \u2014 defer.\n\t\t\t\t\tif (isMentionPickerOpen()) return false\n\t\t\t\t\t// Shift+Enter inserts a new line. Replay a plain Enter through the keymaps (guarded so\n\t\t\t\t\t// we don't re-enter and submit) to reuse the editor's list-aware Enter handling \u2014 a new\n\t\t\t\t\t// list item in a list, a new paragraph otherwise. tldraw doesn't do soft breaks.\n\t\t\t\t\tif (event.shiftKey && !event.metaKey && !event.ctrlKey && !event.altKey) {\n\t\t\t\t\t\treplayingEnter.current = true\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\teditorRef.current?.commands.enter()\n\t\t\t\t\t\t} finally {\n\t\t\t\t\t\t\treplayingEnter.current = false\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn true\n\t\t\t\t\t}\n\t\t\t\t\t// Enter, Cmd+Enter, and Ctrl+Enter submit.\n\t\t\t\t\tif (!disabledRef.current) onSubmitRef.current?.()\n\t\t\t\t\treturn true\n\t\t\t\t},\n\t\t\t},\n\t\t\tonUpdate: ({ editor }) => {\n\t\t\t\tsetIsEmpty(editor.isEmpty)\n\t\t\t\tonChangeRef.current?.(editor.getJSON() as TLRichText)\n\t\t\t},\n\t\t},\n\t\t[interactive]\n\t)\n\teditorRef.current = editor\n\n\t// Sync controlled resets (e.g. the parent clearing to EMPTY_COMMENT after posting) into the\n\t// editor without echoing back the value the editor itself just emitted.\n\tuseEffect(() => {\n\t\tif (!editor || value === undefined) return\n\t\tif (isEqual(editor.getJSON(), value)) return\n\t\teditor.commands.setContent(value as JSONContent)\n\t\tsetIsEmpty(editor.isEmpty)\n\t}, [editor, value])\n\n\t// Focus on the next frame rather than via TipTap's autofocus: the composer often mounts from a\n\t// canvas pointer event whose default focus handling would otherwise steal it back.\n\tuseEffect(() => {\n\t\tif (!autoFocus || !editor) return\n\t\tconst raf = requestAnimationFrame(() => editor.commands.focus('end'))\n\t\treturn () => cancelAnimationFrame(raf)\n\t}, [autoFocus, editor])\n\n\treturn (\n\t\t<div className=\"tlui-cmt-composer\">\n\t\t\t{leading ?? <Avatar name={author} />}\n\t\t\t<div className=\"tlui-cmt-composer__field\">\n\t\t\t\t<div className=\"tlui-cmt-composer__input-wrap\">\n\t\t\t\t\t<EditorContent editor={editor} />\n\t\t\t\t\t{isEmpty && (\n\t\t\t\t\t\t<div className=\"tlui-cmt-input__placeholder\" aria-hidden=\"true\">\n\t\t\t\t\t\t\t{placeholder}\n\t\t\t\t\t\t</div>\n\t\t\t\t\t)}\n\t\t\t\t</div>\n\t\t\t\t{interactive && <SendButton label={sendLabel} onClick={onSubmit} disabled={disabled} />}\n\t\t\t</div>\n\t\t</div>\n\t)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["editor"]
|
|
7
7
|
}
|
|
@@ -6,7 +6,7 @@ function mentionName(attrs, resolveName) {
|
|
|
6
6
|
}
|
|
7
7
|
function commentMention({ resolveName, suggestion } = {}) {
|
|
8
8
|
return Mention.configure({
|
|
9
|
-
HTMLAttributes: { class: "cmt-mention" },
|
|
9
|
+
HTMLAttributes: { class: "tlui-cmt-mention" },
|
|
10
10
|
renderText: ({ node }) => `@${mentionName(node.attrs, resolveName)}`,
|
|
11
11
|
renderHTML: ({ node, options }) => [
|
|
12
12
|
"span",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/ui/comment-mention.ts"],
|
|
4
|
-
"sourcesContent": ["import { mergeAttributes } from '@tiptap/core'\nimport { Mention, type MentionNodeAttrs, type MentionOptions } from '@tiptap/extension-mention'\n\n/**\n * A mention node's display name: the member's current name, resolved live from its id when a\n * resolver is given (so a rename shows through), falling back to the label captured at insert time\n * when no source can name the id \u2014 e.g. a deleted account. The stored id is always the source of\n * truth; live resolution only freshens the displayed name over that captured label.\n */\nfunction mentionName(\n\tattrs: MentionNodeAttrs,\n\tresolveName?: (id: string) => string | undefined\n): string {\n\tconst resolved = attrs.id && resolveName ? resolveName(attrs.id) : undefined\n\treturn resolved ?? attrs.label ?? attrs.id ?? ''\n}\n\nexport interface CommentMentionOptions {\n\t/**\n\t * Resolve a member id to its current display name \u2014 for the read-only render paths. Returns\n\t * `undefined` when the id can't be resolved, so the render falls back to the mention's stored label.\n\t */\n\tresolveName?(id: string): string | undefined\n\t/** The `@`-picker suggestion config \u2014 for the composer (omit on the render paths). */\n\tsuggestion?: MentionOptions['suggestion']\n}\n\n/**\n * The comment @-mention node \u2014 TipTap's `Mention` configured to render as a `.cmt-mention` pill.\n *\n * A factory rather than a shared constant because it's configured differently per context: the\n * read-only render paths pass `resolveName` (so the stored `{ id }` node always shows the member's\n * current name, not a copy frozen at insert time), while the composer passes a `suggestion` (the\n * `@` picker). The node schema is identical either way \u2014 only these two levers differ.\n */\nexport function commentMention({ resolveName, suggestion }: CommentMentionOptions = {}) {\n\treturn Mention.configure({\n\t\tHTMLAttributes: { class: 'cmt-mention' },\n\t\trenderText: ({ node }) => `@${mentionName(node.attrs as MentionNodeAttrs, resolveName)}`,\n\t\trenderHTML: ({ node, options }) => [\n\t\t\t'span',\n\t\t\tmergeAttributes(options.HTMLAttributes, { 'data-mention-id': node.attrs.id }),\n\t\t\t`@${mentionName(node.attrs as MentionNodeAttrs, resolveName)}`,\n\t\t],\n\t\t...(suggestion ? { suggestion } : {}),\n\t})\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,uBAAuB;AAChC,SAAS,eAA2D;AAQpE,SAAS,YACR,OACA,aACS;AACT,QAAM,WAAW,MAAM,MAAM,cAAc,YAAY,MAAM,EAAE,IAAI;AACnE,SAAO,YAAY,MAAM,SAAS,MAAM,MAAM;AAC/C;AAoBO,SAAS,eAAe,EAAE,aAAa,WAAW,IAA2B,CAAC,GAAG;AACvF,SAAO,QAAQ,UAAU;AAAA,IACxB,gBAAgB,EAAE,OAAO,
|
|
4
|
+
"sourcesContent": ["import { mergeAttributes } from '@tiptap/core'\nimport { Mention, type MentionNodeAttrs, type MentionOptions } from '@tiptap/extension-mention'\n\n/**\n * A mention node's display name: the member's current name, resolved live from its id when a\n * resolver is given (so a rename shows through), falling back to the label captured at insert time\n * when no source can name the id \u2014 e.g. a deleted account. The stored id is always the source of\n * truth; live resolution only freshens the displayed name over that captured label.\n */\nfunction mentionName(\n\tattrs: MentionNodeAttrs,\n\tresolveName?: (id: string) => string | undefined\n): string {\n\tconst resolved = attrs.id && resolveName ? resolveName(attrs.id) : undefined\n\treturn resolved ?? attrs.label ?? attrs.id ?? ''\n}\n\nexport interface CommentMentionOptions {\n\t/**\n\t * Resolve a member id to its current display name \u2014 for the read-only render paths. Returns\n\t * `undefined` when the id can't be resolved, so the render falls back to the mention's stored label.\n\t */\n\tresolveName?(id: string): string | undefined\n\t/** The `@`-picker suggestion config \u2014 for the composer (omit on the render paths). */\n\tsuggestion?: MentionOptions['suggestion']\n}\n\n/**\n * The comment @-mention node \u2014 TipTap's `Mention` configured to render as a `.tlui-cmt-mention` pill.\n *\n * A factory rather than a shared constant because it's configured differently per context: the\n * read-only render paths pass `resolveName` (so the stored `{ id }` node always shows the member's\n * current name, not a copy frozen at insert time), while the composer passes a `suggestion` (the\n * `@` picker). The node schema is identical either way \u2014 only these two levers differ.\n */\nexport function commentMention({ resolveName, suggestion }: CommentMentionOptions = {}) {\n\treturn Mention.configure({\n\t\tHTMLAttributes: { class: 'tlui-cmt-mention' },\n\t\trenderText: ({ node }) => `@${mentionName(node.attrs as MentionNodeAttrs, resolveName)}`,\n\t\trenderHTML: ({ node, options }) => [\n\t\t\t'span',\n\t\t\tmergeAttributes(options.HTMLAttributes, { 'data-mention-id': node.attrs.id }),\n\t\t\t`@${mentionName(node.attrs as MentionNodeAttrs, resolveName)}`,\n\t\t],\n\t\t...(suggestion ? { suggestion } : {}),\n\t})\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,uBAAuB;AAChC,SAAS,eAA2D;AAQpE,SAAS,YACR,OACA,aACS;AACT,QAAM,WAAW,MAAM,MAAM,cAAc,YAAY,MAAM,EAAE,IAAI;AACnE,SAAO,YAAY,MAAM,SAAS,MAAM,MAAM;AAC/C;AAoBO,SAAS,eAAe,EAAE,aAAa,WAAW,IAA2B,CAAC,GAAG;AACvF,SAAO,QAAQ,UAAU;AAAA,IACxB,gBAAgB,EAAE,OAAO,mBAAmB;AAAA,IAC5C,YAAY,CAAC,EAAE,KAAK,MAAM,IAAI,YAAY,KAAK,OAA2B,WAAW,CAAC;AAAA,IACtF,YAAY,CAAC,EAAE,MAAM,QAAQ,MAAM;AAAA,MAClC;AAAA,MACA,gBAAgB,QAAQ,gBAAgB,EAAE,mBAAmB,KAAK,MAAM,GAAG,CAAC;AAAA,MAC5E,IAAI,YAAY,KAAK,OAA2B,WAAW,CAAC;AAAA,IAC7D;AAAA,IACA,GAAI,aAAa,EAAE,WAAW,IAAI,CAAC;AAAA,EACpC,CAAC;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { jsx } from "react/jsx-runtime";
|
|
2
2
|
function CommentPin({ children, resolved, open }) {
|
|
3
|
-
const className = [
|
|
3
|
+
const className = [
|
|
4
|
+
"tlui-cmt-pin",
|
|
5
|
+
resolved && "tlui-cmt-pin--resolved",
|
|
6
|
+
open && "tlui-cmt-pin--open"
|
|
7
|
+
].filter(Boolean).join(" ");
|
|
4
8
|
return /* @__PURE__ */ jsx("div", { className, children: resolved ? "\u2713" : children });
|
|
5
9
|
}
|
|
6
10
|
export {
|