@tldraw/commenting 0.0.0-bootstrap → 5.3.0-internal.1640468db8fd
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 +284 -153
- package/dist-cjs/canvas/comment-body.js +1 -1
- package/dist-cjs/canvas/comment-body.js.map +2 -2
- package/dist-cjs/canvas/comment-drafts.js +54 -0
- package/dist-cjs/canvas/comment-drafts.js.map +7 -0
- package/dist-cjs/canvas/comment-render.js +2 -0
- package/dist-cjs/canvas/comment-render.js.map +2 -2
- package/dist-cjs/canvas/comment-tool.js +65 -12
- package/dist-cjs/canvas/comment-tool.js.map +2 -2
- package/dist-cjs/canvas/comments-filter-menu.js +54 -63
- package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
- package/dist-cjs/canvas/comments-overflow-menu.js +21 -27
- package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
- package/dist-cjs/canvas/comments-overlay.js +304 -112
- package/dist-cjs/canvas/comments-overlay.js.map +2 -2
- package/dist-cjs/canvas/comments-sidebar.js +12 -18
- package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
- package/dist-cjs/canvas/comments-visibility-toggle.js +66 -0
- package/dist-cjs/canvas/comments-visibility-toggle.js.map +7 -0
- package/dist-cjs/canvas/options.js +1 -0
- package/dist-cjs/canvas/options.js.map +2 -2
- package/dist-cjs/canvas/sidebar-filters.js +2 -2
- package/dist-cjs/canvas/sidebar-filters.js.map +2 -2
- package/dist-cjs/canvas/state.js +11 -0
- package/dist-cjs/canvas/state.js.map +2 -2
- package/dist-cjs/canvas/thread-state.js +8 -1
- package/dist-cjs/canvas/thread-state.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 +126 -41
- package/dist-cjs/index.js +7 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/ui/avatar.js +14 -6
- 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-author.js +17 -0
- package/dist-cjs/ui/comment-author.js.map +7 -0
- package/dist-cjs/ui/comment-card.js +4 -4
- package/dist-cjs/ui/comment-card.js.map +2 -2
- package/dist-cjs/ui/comment-composer.js +92 -29
- 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 +23 -3
- 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 +12 -12
- 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/format-time.js +3 -0
- package/dist-cjs/ui/format-time.js.map +2 -2
- package/dist-cjs/ui/mention-list.js +8 -8
- 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-cjs/ui/tooltip-button.js +32 -0
- package/dist-cjs/ui/tooltip-button.js.map +7 -0
- package/dist-esm/canvas/comment-body.mjs +1 -1
- package/dist-esm/canvas/comment-body.mjs.map +2 -2
- package/dist-esm/canvas/comment-drafts.mjs +34 -0
- package/dist-esm/canvas/comment-drafts.mjs.map +7 -0
- package/dist-esm/canvas/comment-render.mjs +2 -0
- package/dist-esm/canvas/comment-render.mjs.map +2 -2
- package/dist-esm/canvas/comment-tool.mjs +67 -14
- package/dist-esm/canvas/comment-tool.mjs.map +2 -2
- package/dist-esm/canvas/comments-filter-menu.mjs +54 -63
- package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
- package/dist-esm/canvas/comments-overflow-menu.mjs +22 -27
- package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
- package/dist-esm/canvas/comments-overlay.mjs +322 -114
- package/dist-esm/canvas/comments-overlay.mjs.map +2 -2
- package/dist-esm/canvas/comments-sidebar.mjs +15 -21
- package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
- package/dist-esm/canvas/comments-visibility-toggle.mjs +46 -0
- package/dist-esm/canvas/comments-visibility-toggle.mjs.map +7 -0
- package/dist-esm/canvas/options.mjs +1 -0
- package/dist-esm/canvas/options.mjs.map +2 -2
- package/dist-esm/canvas/sidebar-filters.mjs +2 -2
- package/dist-esm/canvas/sidebar-filters.mjs.map +2 -2
- package/dist-esm/canvas/state.mjs +11 -0
- package/dist-esm/canvas/state.mjs.map +2 -2
- package/dist-esm/canvas/thread-state.mjs +8 -1
- package/dist-esm/canvas/thread-state.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 +126 -41
- package/dist-esm/index.mjs +11 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/ui/avatar.mjs +14 -6
- 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-author.mjs +1 -0
- package/dist-esm/ui/comment-author.mjs.map +7 -0
- package/dist-esm/ui/comment-card.mjs +4 -4
- package/dist-esm/ui/comment-card.mjs.map +2 -2
- package/dist-esm/ui/comment-composer.mjs +93 -30
- 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 +23 -3
- 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 +12 -12
- 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/format-time.mjs +3 -0
- package/dist-esm/ui/format-time.mjs.map +2 -2
- package/dist-esm/ui/mention-list.mjs +8 -8
- 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/dist-esm/ui/tooltip-button.mjs +12 -0
- package/dist-esm/ui/tooltip-button.mjs.map +7 -0
- package/package.json +3 -3
- package/src/canvas/canvas.css +79 -36
- package/src/canvas/comment-body.tsx +1 -1
- package/src/canvas/comment-drafts.test.ts +55 -0
- package/src/canvas/comment-drafts.ts +46 -0
- package/src/canvas/comment-render.test.ts +1 -1
- package/src/canvas/comment-render.ts +7 -2
- package/src/canvas/comment-tool.tsx +98 -17
- package/src/canvas/comments-filter-menu.tsx +27 -33
- package/src/canvas/comments-overflow-menu.tsx +13 -21
- package/src/canvas/comments-overlay.tsx +441 -145
- package/src/canvas/comments-sidebar.tsx +27 -28
- package/src/canvas/comments-visibility-toggle.tsx +50 -0
- package/src/canvas/options.ts +25 -0
- package/src/canvas/sidebar-filters.ts +4 -3
- package/src/canvas/state.ts +22 -0
- package/src/canvas/thread-state.ts +16 -3
- package/src/clustering/runtime.test.ts +183 -7
- package/src/clustering/runtime.ts +164 -15
- package/src/index.ts +7 -0
- package/src/ui/avatar.tsx +15 -11
- package/src/ui/byline.tsx +6 -5
- package/src/ui/comment-author.ts +15 -0
- package/src/ui/comment-card.tsx +6 -5
- package/src/ui/comment-composer.tsx +122 -33
- package/src/ui/comment-mention.ts +2 -2
- package/src/ui/comment-pin.tsx +35 -4
- package/src/ui/comment-text.tsx +1 -1
- package/src/ui/comment-thread.tsx +8 -6
- package/src/ui/comments-list.tsx +19 -14
- package/src/ui/comments.css +205 -117
- package/src/ui/count-badge.tsx +1 -1
- package/src/ui/empty-state.tsx +3 -3
- package/src/ui/format-time.ts +5 -0
- package/src/ui/mention-list.tsx +13 -16
- 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
- package/src/ui/tooltip-button.tsx +20 -0
|
@@ -2,23 +2,43 @@ import type { ClusterNode, ClusterTable, MergeEvent } from './types'
|
|
|
2
2
|
|
|
3
3
|
/** @public */
|
|
4
4
|
export interface ClusterRuntime {
|
|
5
|
-
/** Events[0..k) are
|
|
5
|
+
/** Events[0..k) are in the cursor (applied unless suppressed). Exposed for tests/debugging. */
|
|
6
6
|
readonly k: number
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Increments whenever the displayed partition changes. Subscribe to this, not `k`: detaches
|
|
9
|
+
* and suppressed-event healing change the partition without moving the cursor.
|
|
10
|
+
*/
|
|
11
|
+
readonly version: number
|
|
12
|
+
/** Number of suppressed band events currently inside the cursor. Exposed for tests. */
|
|
13
|
+
getSuppressedCount(): number
|
|
14
|
+
/** Number of leaves currently detached from the displayed partition. Exposed for tests. */
|
|
15
|
+
getDetachedCount(): number
|
|
16
|
+
/** Reset state from scratch for the given zoom (cold start / after rebuild). Clears detaches. */
|
|
8
17
|
seed(zoom: number): void
|
|
9
18
|
/**
|
|
10
19
|
* Reset state for the given zoom, carrying hysteresis state over from a previous partition
|
|
11
20
|
* (the visible map of the model being replaced). Threshold-forced events ignore history:
|
|
12
|
-
* zoom \<= zMerge is always
|
|
13
|
-
*
|
|
14
|
-
* (e.g. introduced by the rebuild) stays
|
|
15
|
-
*
|
|
16
|
-
*
|
|
21
|
+
* zoom \<= zMerge is always merged, zoom \>= zSplit always split. An event inside its band
|
|
22
|
+
* keeps its previous state: merged iff its members were merged together in `previous`; a band
|
|
23
|
+
* event that was unmerged (or has no history, e.g. introduced by the rebuild) stays unmerged —
|
|
24
|
+
* inside the cursor but suppressed — until a zoom-out crosses its own zMerge, exactly like any
|
|
25
|
+
* other pending merge. Carryover is exact: no group changes state because of the swap alone.
|
|
26
|
+
* Clears detaches.
|
|
17
27
|
*/
|
|
18
28
|
seedFrom(zoom: number, previous: ReadonlyMap<string, ClusterNode>): void
|
|
19
29
|
/** Advance/retreat the cursor for a camera change. No-op if zoom sits inside all bands. */
|
|
20
30
|
onCamera(zoom: number): void
|
|
21
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Remove one leaf from the displayed partition without touching the event table. Local by
|
|
33
|
+
* construction: only the nodes containing the leaf change — a badge shrinks in place (count
|
|
34
|
+
* and centroid recomputed from its remaining members), a pair collapses to its surviving
|
|
35
|
+
* leaf, and the leaf on its own disappears. Everything else is untouched, so a deletion,
|
|
36
|
+
* pop-out, or thread-open never re-flows the rest of the document. The table's thresholds
|
|
37
|
+
* around the detached leaf go stale; the caller is expected to hold a corrected rebuild and
|
|
38
|
+
* adopt it (with seedFrom) at the next zoom-out. Unknown or already-detached ids are no-ops.
|
|
39
|
+
*/
|
|
40
|
+
detachLeaf(leafId: string): void
|
|
41
|
+
/** The displayed partition: cluster id → node, with detaches applied. Do not mutate. */
|
|
22
42
|
getVisible(): ReadonlyMap<string, ClusterNode>
|
|
23
43
|
}
|
|
24
44
|
|
|
@@ -30,21 +50,49 @@ export function createClusterRuntime(table: ClusterTable): ClusterRuntime {
|
|
|
30
50
|
class ClusterRuntimeImpl implements ClusterRuntime {
|
|
31
51
|
// mutable internally; readonly through the ClusterRuntime interface
|
|
32
52
|
k = 0
|
|
53
|
+
version = 0
|
|
54
|
+
// Structural partition: exactly leaves + applied events, never patched. The cursor invariants
|
|
55
|
+
// live here, untouched by detaches — resolution to the displayed partition happens at read
|
|
56
|
+
// time in getVisible().
|
|
33
57
|
private visible = new Map<string, ClusterNode>()
|
|
34
58
|
private seeded = false
|
|
59
|
+
// Indices (< k) of band events held unmerged by seedFrom carryover. The single cursor can
|
|
60
|
+
// only express "merged up to here", but a carried-over partition can be "merged except these"
|
|
61
|
+
// — the exceptions live here. Self-draining: an entry leaves via onCamera when the zoom
|
|
62
|
+
// crosses its own zMerge (merges) or its zSplit (the split walk retreats past it).
|
|
63
|
+
private suppressed = new Set<number>()
|
|
64
|
+
// Leaves removed from the displayed partition (deleted / popped out / opened). Patches map
|
|
65
|
+
// each structural node containing a detached leaf to its displayed replacement (or null to
|
|
66
|
+
// drop it). Rebuilt from the detached set; cleared by seed/seedFrom.
|
|
67
|
+
private detached = new Set<string>()
|
|
68
|
+
private patched = new Map<string, ClusterNode | null>()
|
|
69
|
+
private leafById: Map<string, ClusterNode> | null = null
|
|
70
|
+
private resolvedCache: { version: number; map: Map<string, ClusterNode> } | null = null
|
|
35
71
|
|
|
36
72
|
constructor(private readonly table: ClusterTable) {
|
|
37
73
|
this.resetVisible()
|
|
38
74
|
}
|
|
39
75
|
|
|
76
|
+
getSuppressedCount(): number {
|
|
77
|
+
return this.suppressed.size
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
getDetachedCount(): number {
|
|
81
|
+
return this.detached.size
|
|
82
|
+
}
|
|
83
|
+
|
|
40
84
|
seed(zoom: number): void {
|
|
41
85
|
validateZoom(zoom)
|
|
42
86
|
this.k = seedCount(this.table.events, zoom)
|
|
87
|
+
this.suppressed.clear()
|
|
88
|
+
this.detached.clear()
|
|
89
|
+
this.patched.clear()
|
|
43
90
|
this.resetVisible()
|
|
44
91
|
for (let i = 0; i < this.k; i++) {
|
|
45
92
|
applyEvent(this.visible, this.table.events[i])
|
|
46
93
|
}
|
|
47
94
|
this.seeded = true
|
|
95
|
+
this.version++
|
|
48
96
|
}
|
|
49
97
|
|
|
50
98
|
seedFrom(zoom: number, previous: ReadonlyMap<string, ClusterNode>): void {
|
|
@@ -56,21 +104,34 @@ class ClusterRuntimeImpl implements ClusterRuntime {
|
|
|
56
104
|
}
|
|
57
105
|
}
|
|
58
106
|
|
|
107
|
+
// Cut the cursor purely on split thresholds. zSplit is non-increasing down the table, so
|
|
108
|
+
// everything past the first mandatory split (zoom >= zSplit) is also mandatorily split —
|
|
109
|
+
// this cut can never conflict with an event that should be merged.
|
|
59
110
|
const events = this.table.events
|
|
60
111
|
let k = 0
|
|
61
|
-
while (k < events.length) {
|
|
62
|
-
const event = events[k]
|
|
63
|
-
if (zoom >= event.zSplit) break
|
|
64
|
-
if (zoom > event.zMerge && !wasMergedTogether(event.result.members, ownerByMember)) break
|
|
112
|
+
while (k < events.length && zoom < events[k].zSplit) {
|
|
65
113
|
k++
|
|
66
114
|
}
|
|
67
115
|
|
|
68
116
|
this.k = k
|
|
117
|
+
this.suppressed.clear()
|
|
118
|
+
this.detached.clear()
|
|
119
|
+
this.patched.clear()
|
|
69
120
|
this.resetVisible()
|
|
70
121
|
for (let i = 0; i < k; i++) {
|
|
71
|
-
|
|
122
|
+
const event = events[i]
|
|
123
|
+
if (zoom > event.zMerge && !wasMergedTogether(event.result.members, ownerByMember)) {
|
|
124
|
+
// In its band and previously unmerged: keep it unmerged, as an exception inside
|
|
125
|
+
// the cursor. Dependency-safe: an applied event can never consume a suppressed
|
|
126
|
+
// result — merged members imply merged (subset) children, and threshold-forced
|
|
127
|
+
// events force their children too (zMerge is non-increasing down the table).
|
|
128
|
+
this.suppressed.add(i)
|
|
129
|
+
} else {
|
|
130
|
+
applyEvent(this.visible, event)
|
|
131
|
+
}
|
|
72
132
|
}
|
|
73
133
|
this.seeded = true
|
|
134
|
+
this.version++
|
|
74
135
|
}
|
|
75
136
|
|
|
76
137
|
onCamera(zoom: number): void {
|
|
@@ -79,18 +140,106 @@ class ClusterRuntimeImpl implements ClusterRuntime {
|
|
|
79
140
|
throw new Error('Cluster runtime must be seeded before onCamera')
|
|
80
141
|
}
|
|
81
142
|
|
|
143
|
+
let changed = false
|
|
82
144
|
while (this.k < this.table.events.length && zoom <= this.table.events[this.k].zMerge) {
|
|
83
145
|
applyEvent(this.visible, this.table.events[this.k])
|
|
84
146
|
this.k++
|
|
147
|
+
changed = true
|
|
85
148
|
}
|
|
86
149
|
while (this.k > 0 && zoom >= this.table.events[this.k - 1].zSplit) {
|
|
87
150
|
this.k--
|
|
88
|
-
|
|
151
|
+
// A suppressed event was never applied — retreating past it is bookkeeping only.
|
|
152
|
+
if (!this.suppressed.delete(this.k)) {
|
|
153
|
+
unapplyEvent(this.visible, this.table.events[this.k])
|
|
154
|
+
changed = true
|
|
155
|
+
}
|
|
89
156
|
}
|
|
157
|
+
// Heal suppressed events at their own merge threshold: a zoom-out past zMerge merges a
|
|
158
|
+
// held-out band event exactly as if it had still been ahead of the cursor. Set iteration
|
|
159
|
+
// is insertion order (ascending index), so a healed event's suppressed children (larger
|
|
160
|
+
// zMerge, smaller index) always heal before it.
|
|
161
|
+
if (this.suppressed.size > 0) {
|
|
162
|
+
for (const i of [...this.suppressed]) {
|
|
163
|
+
if (zoom <= this.table.events[i].zMerge) {
|
|
164
|
+
this.suppressed.delete(i)
|
|
165
|
+
applyEvent(this.visible, this.table.events[i])
|
|
166
|
+
changed = true
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (changed) this.version++
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
detachLeaf(leafId: string): void {
|
|
174
|
+
if (this.detached.has(leafId)) return
|
|
175
|
+
if (!this.getLeafById().has(leafId)) return
|
|
176
|
+
this.detached.add(leafId)
|
|
177
|
+
this.rebuildPatches()
|
|
178
|
+
this.version++
|
|
90
179
|
}
|
|
91
180
|
|
|
92
181
|
getVisible(): ReadonlyMap<string, ClusterNode> {
|
|
93
|
-
|
|
182
|
+
if (this.resolvedCache && this.resolvedCache.version === this.version) {
|
|
183
|
+
return this.resolvedCache.map
|
|
184
|
+
}
|
|
185
|
+
let map: Map<string, ClusterNode>
|
|
186
|
+
if (this.patched.size === 0) {
|
|
187
|
+
map = this.visible
|
|
188
|
+
} else {
|
|
189
|
+
map = new Map()
|
|
190
|
+
for (const node of this.visible.values()) {
|
|
191
|
+
const resolved = this.patched.has(node.id) ? this.patched.get(node.id)! : node
|
|
192
|
+
if (resolved) map.set(resolved.id, resolved)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
this.resolvedCache = { version: this.version, map }
|
|
196
|
+
return map
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private getLeafById(): Map<string, ClusterNode> {
|
|
200
|
+
if (!this.leafById) {
|
|
201
|
+
this.leafById = new Map(this.table.leaves.map((leaf) => [leaf.id, leaf]))
|
|
202
|
+
}
|
|
203
|
+
return this.leafById
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Recompute the patch map from the detached set. A node is patched iff it contains a
|
|
207
|
+
* detached member; the patch drops those members and recomputes count/centroid, collapsing
|
|
208
|
+
* to the surviving leaf node at count 1 and to nothing at count 0. Patched nodes keep their
|
|
209
|
+
* structural id, so cursor events keep addressing them. */
|
|
210
|
+
private rebuildPatches() {
|
|
211
|
+
this.patched.clear()
|
|
212
|
+
const leafById = this.getLeafById()
|
|
213
|
+
for (const id of this.detached) {
|
|
214
|
+
this.patched.set(id, null)
|
|
215
|
+
}
|
|
216
|
+
const patch = (node: ClusterNode) => {
|
|
217
|
+
if (node.members.length === 1 || !node.members.some((m) => this.detached.has(m))) return
|
|
218
|
+
const members = node.members.filter((m) => !this.detached.has(m))
|
|
219
|
+
if (members.length === 0) {
|
|
220
|
+
this.patched.set(node.id, null)
|
|
221
|
+
} else if (members.length === 1) {
|
|
222
|
+
this.patched.set(node.id, leafById.get(members[0])!)
|
|
223
|
+
} else {
|
|
224
|
+
let x = 0
|
|
225
|
+
let y = 0
|
|
226
|
+
for (const m of members) {
|
|
227
|
+
const leaf = leafById.get(m)!
|
|
228
|
+
x += leaf.centroid.x
|
|
229
|
+
y += leaf.centroid.y
|
|
230
|
+
}
|
|
231
|
+
this.patched.set(node.id, {
|
|
232
|
+
id: node.id,
|
|
233
|
+
centroid: { x: x / members.length, y: y / members.length },
|
|
234
|
+
count: members.length,
|
|
235
|
+
members,
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
for (const event of this.table.events) {
|
|
240
|
+
patch(event.result)
|
|
241
|
+
for (const child of event.children) patch(child)
|
|
242
|
+
}
|
|
94
243
|
}
|
|
95
244
|
|
|
96
245
|
private resetVisible() {
|
package/src/index.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { registerTldrawLibraryVersion } from '@tldraw/utils'
|
|
|
4
4
|
// custom commenting UI.
|
|
5
5
|
export { Avatar, type AvatarProps } from './ui/avatar'
|
|
6
6
|
export { Byline, type BylineProps } from './ui/byline'
|
|
7
|
+
export { type CommentAuthor } from './ui/comment-author'
|
|
7
8
|
export { CommentCard, type CommentCardProps } from './ui/comment-card'
|
|
8
9
|
export { CommentComposer, type CommentComposerProps } from './ui/comment-composer'
|
|
9
10
|
export { CountBadge, type CountBadgeProps } from './ui/count-badge'
|
|
@@ -61,9 +62,11 @@ export {
|
|
|
61
62
|
type CommentingOptions,
|
|
62
63
|
defaultCommentingOptions,
|
|
63
64
|
getCommentingOptions,
|
|
65
|
+
type ShapeCommentPrecisionContext,
|
|
64
66
|
useCommentingOptions,
|
|
65
67
|
} from './canvas/options'
|
|
66
68
|
export { CommentsOverflowMenu } from './canvas/comments-overflow-menu'
|
|
69
|
+
export { CommentsVisibilityToggle } from './canvas/comments-visibility-toggle'
|
|
67
70
|
export { CanvasCommentsSidebar, type CanvasCommentsSidebarProps } from './canvas/comments-sidebar'
|
|
68
71
|
export { useComments, useCommentThreads, useThreadComments } from './canvas/hooks'
|
|
69
72
|
export { useCommentingEnabled } from './canvas/license'
|
|
@@ -72,12 +75,15 @@ export { richTextToPlaintext } from './canvas/rich-text'
|
|
|
72
75
|
export { DEFAULT_SIDEBAR_FILTERS, type SidebarFilters } from './canvas/sidebar-filters'
|
|
73
76
|
export {
|
|
74
77
|
commentsHidden,
|
|
78
|
+
commentsSidebarOpen,
|
|
75
79
|
commitCommentMutation,
|
|
76
80
|
openThreadId,
|
|
77
81
|
pendingComment,
|
|
78
82
|
sidebarFilters,
|
|
79
83
|
toggleCommentsHidden,
|
|
84
|
+
toggleCommentsSidebar,
|
|
80
85
|
useCommentsHidden,
|
|
86
|
+
useCommentsSidebarOpen,
|
|
81
87
|
useOpenThreadId,
|
|
82
88
|
usePendingComment,
|
|
83
89
|
useSidebarFilters,
|
|
@@ -86,6 +92,7 @@ export {
|
|
|
86
92
|
anchorPagePoint,
|
|
87
93
|
DEFAULT_IMPRECISE_SHAPE_ANCHOR,
|
|
88
94
|
focusThread,
|
|
95
|
+
regionAnchorPinCorner,
|
|
89
96
|
shapeAnchorAt,
|
|
90
97
|
} from './canvas/thread-state'
|
|
91
98
|
|
package/src/ui/avatar.tsx
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
import { getFirstCharacter } from 'tldraw'
|
|
2
|
+
import { CommentAuthor } from './comment-author'
|
|
2
3
|
|
|
3
4
|
/** @public */
|
|
4
5
|
export interface AvatarProps {
|
|
5
|
-
|
|
6
|
-
/** Background colour, used when there's no `image`. Falls back to the default avatar tint. */
|
|
7
|
-
color?: string
|
|
8
|
-
/** Avatar image URL. When set, shows the image instead of the coloured initial. */
|
|
9
|
-
image?: string
|
|
6
|
+
author: CommentAuthor
|
|
10
7
|
}
|
|
11
8
|
|
|
12
9
|
function initial(name: string) {
|
|
@@ -15,17 +12,24 @@ function initial(name: string) {
|
|
|
15
12
|
|
|
16
13
|
/** A commenter's avatar — their image if provided, otherwise a single-initial coloured circle.
|
|
17
14
|
* @public @react */
|
|
18
|
-
export function Avatar({
|
|
19
|
-
if (image) {
|
|
20
|
-
return
|
|
15
|
+
export function Avatar({ author }: AvatarProps) {
|
|
16
|
+
if (author.image) {
|
|
17
|
+
return (
|
|
18
|
+
<img
|
|
19
|
+
className="tlui-cmt-avatar tlui-cmt-avatar--image"
|
|
20
|
+
src={author.image}
|
|
21
|
+
alt=""
|
|
22
|
+
aria-hidden="true"
|
|
23
|
+
/>
|
|
24
|
+
)
|
|
21
25
|
}
|
|
22
26
|
return (
|
|
23
27
|
<div
|
|
24
|
-
className="cmt-avatar"
|
|
28
|
+
className="tlui-cmt-avatar"
|
|
25
29
|
aria-hidden="true"
|
|
26
|
-
style={color ? { backgroundColor: color } : undefined}
|
|
30
|
+
style={author.color ? { backgroundColor: author.color } : undefined}
|
|
27
31
|
>
|
|
28
|
-
{initial(name)}
|
|
32
|
+
{initial(author.name)}
|
|
29
33
|
</div>
|
|
30
34
|
)
|
|
31
35
|
}
|
package/src/ui/byline.tsx
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { CommentAuthor } from './comment-author'
|
|
1
2
|
import { formatRelativeTime } from './format-time'
|
|
2
3
|
|
|
3
4
|
/** @public */
|
|
4
5
|
export interface BylineProps {
|
|
5
|
-
author:
|
|
6
|
+
author: CommentAuthor
|
|
6
7
|
/** ISO datetime; formatted to relative time by the component. */
|
|
7
8
|
date: string
|
|
8
9
|
/** Shows an "edited" marker when the comment has been edited. */
|
|
@@ -12,11 +13,11 @@ export interface BylineProps {
|
|
|
12
13
|
/** A comment's metadata line: author name, relative time, and an edited marker. @public @react */
|
|
13
14
|
export function Byline({ author, date, edited }: BylineProps) {
|
|
14
15
|
return (
|
|
15
|
-
<div className="cmt-head">
|
|
16
|
-
<span className="cmt-author">{author}</span>
|
|
17
|
-
<span className="cmt-time">
|
|
16
|
+
<div className="tlui-cmt-head">
|
|
17
|
+
<span className="tlui-cmt-author">{author.name}</span>
|
|
18
|
+
<span className="tlui-cmt-time">
|
|
18
19
|
{formatRelativeTime(date)}
|
|
19
|
-
{edited && <span className="cmt-edited"> · edited</span>}
|
|
20
|
+
{edited && <span className="tlui-cmt-edited"> · edited</span>}
|
|
20
21
|
</span>
|
|
21
22
|
</div>
|
|
22
23
|
)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { JsonObject } from 'tldraw'
|
|
2
|
+
|
|
3
|
+
/** A comment author's display identity, resolved from an author id.
|
|
4
|
+
* @public */
|
|
5
|
+
export interface CommentAuthor {
|
|
6
|
+
name: string
|
|
7
|
+
/** The author's color (any CSS color) — colors their avatar and pins.
|
|
8
|
+
* Omit for the default tint. */
|
|
9
|
+
color?: string
|
|
10
|
+
/** Avatar image URL. When set, avatars show the image instead of the colored initial;
|
|
11
|
+
* the canvas pin keeps the initial-on-color. */
|
|
12
|
+
image?: string
|
|
13
|
+
/** User-defined metadata for the author, never read by the toolkit (like a shape's `meta`). */
|
|
14
|
+
meta?: JsonObject
|
|
15
|
+
}
|
package/src/ui/comment-card.tsx
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { ReactNode } from 'react'
|
|
2
2
|
import { Avatar } from './avatar'
|
|
3
3
|
import { Byline } from './byline'
|
|
4
|
+
import { CommentAuthor } from './comment-author'
|
|
4
5
|
|
|
5
6
|
/** @public */
|
|
6
7
|
export interface CommentCardProps {
|
|
7
|
-
author:
|
|
8
|
+
author: CommentAuthor
|
|
8
9
|
/** The rendered comment body. The card doesn't dictate a format — pass a `<CommentText>`
|
|
9
10
|
* for markdown, a rich-text render, or any node. */
|
|
10
11
|
body: ReactNode
|
|
@@ -20,13 +21,13 @@ export interface CommentCardProps {
|
|
|
20
21
|
/** A single comment: Avatar, Byline, and a body slot the consumer renders. @public @react */
|
|
21
22
|
export function CommentCard({ author, body, date, you, edited, actions }: CommentCardProps) {
|
|
22
23
|
return (
|
|
23
|
-
<div className={you ? 'cmt-card cmt-card--you' : 'cmt-card'}>
|
|
24
|
-
<Avatar
|
|
25
|
-
<div className="cmt-body">
|
|
24
|
+
<div className={you ? 'tlui-cmt-card tlui-cmt-card--you' : 'tlui-cmt-card'}>
|
|
25
|
+
<Avatar author={author} />
|
|
26
|
+
<div className="tlui-cmt-body">
|
|
26
27
|
<Byline author={author} date={date} edited={edited} />
|
|
27
28
|
{body}
|
|
28
29
|
</div>
|
|
29
|
-
{actions !== undefined && <div className="cmt-card__actions">{actions}</div>}
|
|
30
|
+
{actions !== undefined && <div className="tlui-cmt-card__actions">{actions}</div>}
|
|
30
31
|
</div>
|
|
31
32
|
)
|
|
32
33
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { JSONContent } from '@tiptap/core'
|
|
2
2
|
import { EditorContent, useEditor } from '@tiptap/react'
|
|
3
|
-
import { ReactNode, useEffect, useMemo, useRef, useState } from 'react'
|
|
3
|
+
import { ReactNode, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
|
4
4
|
import { isEqual, TLRichText, useMaybeEditor } from 'tldraw'
|
|
5
5
|
import { Avatar } from './avatar'
|
|
6
|
+
import { CommentAuthor } from './comment-author'
|
|
6
7
|
import { commentTipTapExtensions, EMPTY_COMMENT, isCommentEmpty } from './comment-extensions'
|
|
7
8
|
import { commentMention } from './comment-mention'
|
|
8
9
|
import { MentionMember } from './mention-list'
|
|
@@ -11,7 +12,7 @@ import { SendButton } from './send-button'
|
|
|
11
12
|
|
|
12
13
|
/** @public */
|
|
13
14
|
export interface CommentComposerProps {
|
|
14
|
-
author:
|
|
15
|
+
author: CommentAuthor
|
|
15
16
|
placeholder: string
|
|
16
17
|
/** Controlled rich-text value. Omit for the presentational (display-only) composer. */
|
|
17
18
|
value?: TLRichText
|
|
@@ -68,29 +69,69 @@ export function CommentComposer({
|
|
|
68
69
|
renderMentionSuggestionRef.current = renderMentionSuggestion
|
|
69
70
|
|
|
70
71
|
const [isEmpty, setIsEmpty] = useState(() => !value || isCommentEmpty(value))
|
|
72
|
+
// Whether the field has grown to two rows: input across the full width, send button below.
|
|
73
|
+
// Flips when the (single-line) content width reaches the space left beside the send button.
|
|
74
|
+
const [expanded, setExpanded] = useState(false)
|
|
75
|
+
const expandedRef = useRef(expanded)
|
|
76
|
+
expandedRef.current = expanded
|
|
77
|
+
const inputWrapRef = useRef<HTMLDivElement>(null)
|
|
78
|
+
const mirrorRef = useRef<HTMLDivElement>(null)
|
|
71
79
|
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
80
|
+
// Measure whether the content still fits on one line beside the send button. The mirror is a
|
|
81
|
+
// hidden, nowrap clone of the editor's rendered content, so marks and mention chips measure at
|
|
82
|
+
// their true width. A small dead zone between the expand and collapse thresholds keeps the
|
|
83
|
+
// layout from flapping while typing at the boundary.
|
|
84
|
+
const remeasure = () => {
|
|
85
|
+
const wrap = inputWrapRef.current
|
|
86
|
+
const mirror = mirrorRef.current
|
|
87
|
+
const editor = editorRef.current
|
|
88
|
+
if (!wrap || !mirror || !editor) return
|
|
89
|
+
// Empty always fits — and measuring before TipTap's DOM has laid out reads zero widths,
|
|
90
|
+
// which would flash the expanded layout for a frame on mount.
|
|
91
|
+
if (editor.isEmpty) {
|
|
92
|
+
if (expandedRef.current) setExpanded(false)
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
if (wrap.clientWidth === 0) return
|
|
96
|
+
const doc = editor.state.doc
|
|
97
|
+
const multiBlock =
|
|
98
|
+
doc.childCount > 1 || (doc.firstChild !== null && doc.firstChild.type.name !== 'paragraph')
|
|
99
|
+
if (multiBlock) {
|
|
100
|
+
if (!expandedRef.current) setExpanded(true)
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
const field = wrap.parentElement
|
|
104
|
+
const send = field ? field.querySelector<HTMLElement>('.tlui-cmt-send') : null
|
|
105
|
+
const input = wrap.querySelector('.tlui-cmt-input')
|
|
106
|
+
if (!send || !input) return
|
|
107
|
+
mirror.innerHTML = input.innerHTML
|
|
108
|
+
const textWidth = mirror.offsetWidth
|
|
109
|
+
// The single-line space the input has beside the send button: when already expanded the
|
|
110
|
+
// wrap spans the full field, so subtract the button (plus the field's 6px gap) back out.
|
|
111
|
+
const collapsedAvailable = wrap.clientWidth - (expandedRef.current ? send.offsetWidth + 6 : 0)
|
|
112
|
+
if (!expandedRef.current && textWidth > collapsedAvailable - 8) {
|
|
113
|
+
setExpanded(true)
|
|
114
|
+
} else if (expandedRef.current && textWidth < collapsedAvailable - 24) {
|
|
115
|
+
setExpanded(false)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const remeasureRef = useRef(remeasure)
|
|
119
|
+
remeasureRef.current = remeasure
|
|
120
|
+
|
|
121
|
+
// The editor instance, reachable from `handleKeyDown` (which is created before `useEditor`
|
|
122
|
+
// returns). Updated on every render so the ref never points at a stale editor.
|
|
123
|
+
const editorRef = useRef<ReturnType<typeof useEditor>>(null)
|
|
124
|
+
|
|
125
|
+
// Set while we replay Enter through the keymaps for a Shift+Enter newline: `commands.enter()`
|
|
126
|
+
// re-dispatches Enter through `handleKeyDown`, and without this guard our own handler would catch
|
|
127
|
+
// that synthetic Enter and submit instead.
|
|
128
|
+
const replayingEnter = useRef(false)
|
|
129
|
+
|
|
130
|
+
// Enter and Cmd/Ctrl+Enter submit the comment; Shift+Enter inserts a new line for the occasional
|
|
131
|
+
// multi-line comment. This is handled through `editorProps.handleKeyDown` (below) rather than a
|
|
132
|
+
// keyboard-shortcut extension: ProseMirror runs `handleKeyDown` before every keymap plugin, so it
|
|
133
|
+
// can tell Shift+Enter apart from Enter — an `Enter` keymap binding also fires on Shift/Cmd+Enter
|
|
134
|
+
// and would otherwise swallow the newline.
|
|
94
135
|
|
|
95
136
|
// The suggestion plugin is built once (the editor is recreated only on `interactive`) and runs
|
|
96
137
|
// outside React, so it must read the mention callbacks through refs — like onChange/onSubmit —
|
|
@@ -100,7 +141,7 @@ export function CommentComposer({
|
|
|
100
141
|
const mentionsEnabled = !!getMentionSuggestions
|
|
101
142
|
const hasCustomRow = !!renderMentionSuggestion
|
|
102
143
|
const extensions = useMemo(() => {
|
|
103
|
-
const list = [...commentTipTapExtensions
|
|
144
|
+
const list = [...commentTipTapExtensions]
|
|
104
145
|
// Always register the mention node so an existing body that contains a mention keeps it on
|
|
105
146
|
// edit (an unregistered node would be stripped by ProseMirror when the content loads). The `@`
|
|
106
147
|
// picker itself only turns on when the host provides a resolver; otherwise the node is present
|
|
@@ -122,7 +163,7 @@ export function CommentComposer({
|
|
|
122
163
|
list.push(commentMention({ suggestion: { char: '@', allow: () => false } }))
|
|
123
164
|
}
|
|
124
165
|
return list
|
|
125
|
-
}, [
|
|
166
|
+
}, [mentionsEnabled, hasCustomRow, tlEditor])
|
|
126
167
|
|
|
127
168
|
const editor = useEditor(
|
|
128
169
|
{
|
|
@@ -134,14 +175,44 @@ export function CommentComposer({
|
|
|
134
175
|
// mirrors RichTextArea's setup.
|
|
135
176
|
enableCoreExtensions: { textDirection: false },
|
|
136
177
|
textDirection: 'auto',
|
|
137
|
-
editorProps: {
|
|
178
|
+
editorProps: {
|
|
179
|
+
attributes: { class: 'tlui-cmt-input' },
|
|
180
|
+
// Runs before every keymap plugin, so it can distinguish Shift+Enter from Enter — an
|
|
181
|
+
// `Enter` keymap binding also fires on Shift+Enter and would otherwise swallow it.
|
|
182
|
+
handleKeyDown: (_view, event) => {
|
|
183
|
+
// Let the keymaps handle the synthetic Enter we replay for a Shift+Enter newline.
|
|
184
|
+
if (replayingEnter.current) return false
|
|
185
|
+
if (event.key !== 'Enter' || event.isComposing) return false
|
|
186
|
+
// While the @-mention picker is open, Enter selects the highlighted member — defer.
|
|
187
|
+
if (isMentionPickerOpen()) return false
|
|
188
|
+
// Shift+Enter inserts a new line. Replay a plain Enter through the keymaps (guarded so
|
|
189
|
+
// we don't re-enter and submit) to reuse the editor's list-aware Enter handling — a new
|
|
190
|
+
// list item in a list, a new paragraph otherwise. tldraw doesn't do soft breaks.
|
|
191
|
+
if (event.shiftKey && !event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
192
|
+
replayingEnter.current = true
|
|
193
|
+
try {
|
|
194
|
+
editorRef.current?.commands.enter()
|
|
195
|
+
} finally {
|
|
196
|
+
replayingEnter.current = false
|
|
197
|
+
}
|
|
198
|
+
return true
|
|
199
|
+
}
|
|
200
|
+
// Enter, Cmd+Enter, and Ctrl+Enter submit.
|
|
201
|
+
if (!disabledRef.current) onSubmitRef.current?.()
|
|
202
|
+
return true
|
|
203
|
+
},
|
|
204
|
+
},
|
|
138
205
|
onUpdate: ({ editor }) => {
|
|
139
206
|
setIsEmpty(editor.isEmpty)
|
|
207
|
+
// Same-value state sets don't re-render, so measure here — the editor's DOM is
|
|
208
|
+
// already updated when onUpdate fires.
|
|
209
|
+
remeasureRef.current()
|
|
140
210
|
onChangeRef.current?.(editor.getJSON() as TLRichText)
|
|
141
211
|
},
|
|
142
212
|
},
|
|
143
213
|
[interactive]
|
|
144
214
|
)
|
|
215
|
+
editorRef.current = editor
|
|
145
216
|
|
|
146
217
|
// Sync controlled resets (e.g. the parent clearing to EMPTY_COMMENT after posting) into the
|
|
147
218
|
// editor without echoing back the value the editor itself just emitted.
|
|
@@ -152,6 +223,12 @@ export function CommentComposer({
|
|
|
152
223
|
setIsEmpty(editor.isEmpty)
|
|
153
224
|
}, [editor, value])
|
|
154
225
|
|
|
226
|
+
// Measure once the editor's content is in the DOM, so a composer that mounts pre-filled (the
|
|
227
|
+
// edit-in-place composer) starts in the right layout.
|
|
228
|
+
useLayoutEffect(() => {
|
|
229
|
+
remeasureRef.current()
|
|
230
|
+
}, [editor, value])
|
|
231
|
+
|
|
155
232
|
// Focus on the next frame rather than via TipTap's autofocus: the composer often mounts from a
|
|
156
233
|
// canvas pointer event whose default focus handling would otherwise steal it back.
|
|
157
234
|
useEffect(() => {
|
|
@@ -161,16 +238,28 @@ export function CommentComposer({
|
|
|
161
238
|
}, [autoFocus, editor])
|
|
162
239
|
|
|
163
240
|
return (
|
|
164
|
-
<div className="cmt-composer">
|
|
165
|
-
{leading ?? <Avatar
|
|
166
|
-
<div
|
|
167
|
-
|
|
241
|
+
<div className="tlui-cmt-composer">
|
|
242
|
+
{leading ?? <Avatar author={author} />}
|
|
243
|
+
<div
|
|
244
|
+
className={[
|
|
245
|
+
'tlui-cmt-composer__field',
|
|
246
|
+
interactive && expanded && 'tlui-cmt-composer__field--expanded',
|
|
247
|
+
]
|
|
248
|
+
.filter(Boolean)
|
|
249
|
+
.join(' ')}
|
|
250
|
+
>
|
|
251
|
+
<div className="tlui-cmt-composer__input-wrap" ref={inputWrapRef}>
|
|
168
252
|
<EditorContent editor={editor} />
|
|
169
253
|
{isEmpty && (
|
|
170
|
-
<div className="cmt-input__placeholder" aria-hidden="true">
|
|
254
|
+
<div className="tlui-cmt-input__placeholder" aria-hidden="true">
|
|
171
255
|
{placeholder}
|
|
172
256
|
</div>
|
|
173
257
|
)}
|
|
258
|
+
<div
|
|
259
|
+
className="tlui-cmt-composer__mirror tlui-cmt-input"
|
|
260
|
+
ref={mirrorRef}
|
|
261
|
+
aria-hidden="true"
|
|
262
|
+
/>
|
|
174
263
|
</div>
|
|
175
264
|
{interactive && <SendButton label={sendLabel} onClick={onSubmit} disabled={disabled} />}
|
|
176
265
|
</div>
|
|
@@ -26,7 +26,7 @@ export interface CommentMentionOptions {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
* The comment @-mention node — TipTap's `Mention` configured to render as a `.cmt-mention` pill.
|
|
29
|
+
* The comment @-mention node — TipTap's `Mention` configured to render as a `.tlui-cmt-mention` pill.
|
|
30
30
|
*
|
|
31
31
|
* A factory rather than a shared constant because it's configured differently per context: the
|
|
32
32
|
* read-only render paths pass `resolveName` (so the stored `{ id }` node always shows the member's
|
|
@@ -35,7 +35,7 @@ export interface CommentMentionOptions {
|
|
|
35
35
|
*/
|
|
36
36
|
export function commentMention({ resolveName, suggestion }: CommentMentionOptions = {}) {
|
|
37
37
|
return Mention.configure({
|
|
38
|
-
HTMLAttributes: { class: 'cmt-mention' },
|
|
38
|
+
HTMLAttributes: { class: 'tlui-cmt-mention' },
|
|
39
39
|
renderText: ({ node }) => `@${mentionName(node.attrs as MentionNodeAttrs, resolveName)}`,
|
|
40
40
|
renderHTML: ({ node, options }) => [
|
|
41
41
|
'span',
|