@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
package/src/canvas/canvas.css
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
1
1
|
/* The comments layer is portaled into the editor container. Pins live in the canvas-in-front layer
|
|
2
2
|
(beneath the UI panels at z-index 300), so the toolbar and style panel draw over them; the open
|
|
3
|
-
thread's popover portals up to the menus layer separately (`.cmt-canvas-popover`) so it isn't
|
|
3
|
+
thread's popover portals up to the menus layer separately (`.tlui-cmt-canvas-popover`) so it isn't
|
|
4
4
|
clipped. Click-through except on its own children. */
|
|
5
|
-
.cmt-canvas-layer {
|
|
5
|
+
.tlui-cmt-canvas-layer {
|
|
6
6
|
position: absolute;
|
|
7
7
|
inset: 0;
|
|
8
8
|
z-index: var(--tl-layer-canvas-in-front);
|
|
9
9
|
pointer-events: none;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
.cmt-canvas-pin,
|
|
13
|
-
.cmt-canvas-cluster,
|
|
14
|
-
.cmt-canvas-composer {
|
|
12
|
+
.tlui-cmt-canvas-pin,
|
|
13
|
+
.tlui-cmt-canvas-cluster,
|
|
14
|
+
.tlui-cmt-canvas-composer {
|
|
15
15
|
position: absolute;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
.cmt-canvas-pin,
|
|
19
|
-
.cmt-canvas-composer {
|
|
18
|
+
.tlui-cmt-canvas-pin,
|
|
19
|
+
.tlui-cmt-canvas-composer {
|
|
20
20
|
pointer-events: auto;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/* Badges are clickable (zoom to the cluster's first split). */
|
|
24
|
-
.cmt-canvas-cluster {
|
|
24
|
+
.tlui-cmt-canvas-cluster {
|
|
25
25
|
transform: translate(-50%, -50%);
|
|
26
26
|
pointer-events: auto;
|
|
27
27
|
cursor: pointer;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
.cmt-cluster-fade {
|
|
30
|
+
.tlui-cmt-cluster-fade {
|
|
31
31
|
opacity: 1;
|
|
32
32
|
transition: opacity 150ms ease;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
.cmt-cluster-fade--entering {
|
|
35
|
+
.tlui-cmt-cluster-fade--entering {
|
|
36
36
|
opacity: 0;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
.cmt-cluster-fade--present {
|
|
39
|
+
.tlui-cmt-cluster-fade--present {
|
|
40
40
|
opacity: 1;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
.cmt-cluster-fade--exiting {
|
|
43
|
+
.tlui-cmt-cluster-fade--exiting {
|
|
44
44
|
opacity: 0;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
.cmt-cluster-fade--entering *,
|
|
48
|
-
.cmt-cluster-fade--exiting * {
|
|
47
|
+
.tlui-cmt-cluster-fade--entering *,
|
|
48
|
+
.tlui-cmt-cluster-fade--exiting * {
|
|
49
49
|
pointer-events: none !important;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
@media (prefers-reduced-motion: reduce) {
|
|
53
|
-
.cmt-cluster-fade {
|
|
53
|
+
.tlui-cmt-cluster-fade {
|
|
54
54
|
transition: none;
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/* The open thread (its pin + popover) sits above the other pins, so nearby markers don't
|
|
59
59
|
overlap its popover — they tuck behind it. */
|
|
60
|
-
.cmt-canvas-pin--open {
|
|
60
|
+
.tlui-cmt-canvas-pin--open {
|
|
61
61
|
z-index: 1;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/* A region anchor's area: a dashed box under the pins, non-interactive so canvas events pass
|
|
65
65
|
through. Drawn for the live drag draft and for a region thread while hovered or open. */
|
|
66
|
-
.cmt-canvas-region {
|
|
66
|
+
.tlui-cmt-canvas-region {
|
|
67
67
|
position: absolute;
|
|
68
68
|
pointer-events: none;
|
|
69
69
|
border: 2px dashed var(--tl-color-selected);
|
|
@@ -73,13 +73,13 @@
|
|
|
73
73
|
|
|
74
74
|
/* A movable region body (the 'body'/'both' move modes): draggable to translate the whole region.
|
|
75
75
|
Interactive, so it captures the interior while shown — the tradeoff of dragging the body. */
|
|
76
|
-
.cmt-canvas-region--movable {
|
|
76
|
+
.tlui-cmt-canvas-region--movable {
|
|
77
77
|
pointer-events: auto;
|
|
78
78
|
cursor: move;
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/* Corner resize handles on a region box (three corners; the pin corner is the move handle). */
|
|
82
|
-
.cmt-canvas-region-handle {
|
|
82
|
+
.tlui-cmt-canvas-region-handle {
|
|
83
83
|
position: absolute;
|
|
84
84
|
width: 9px;
|
|
85
85
|
height: 9px;
|
|
@@ -92,18 +92,18 @@
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
/* Centre the pin marker on its anchor point. */
|
|
95
|
-
.cmt-canvas-pin__marker {
|
|
95
|
+
.tlui-cmt-canvas-pin__marker {
|
|
96
96
|
width: max-content;
|
|
97
97
|
transform: translate(-50%, -50%);
|
|
98
98
|
cursor: grab;
|
|
99
99
|
touch-action: none;
|
|
100
100
|
}
|
|
101
|
-
.cmt-canvas-pin__marker:active {
|
|
101
|
+
.tlui-cmt-canvas-pin__marker:active {
|
|
102
102
|
cursor: grabbing;
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
/* The open thread's popover — portaled to the menus layer (above the UI), positioned at the pin. */
|
|
106
|
-
.cmt-canvas-popover {
|
|
106
|
+
.tlui-cmt-canvas-popover {
|
|
107
107
|
position: absolute;
|
|
108
108
|
z-index: var(--tl-layer-menus);
|
|
109
109
|
pointer-events: auto;
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
/* The placement composer — a distinct floating view (portaled to the menus layer, below the click
|
|
113
113
|
point): a pencil "draft" avatar beside a pill field that is itself the surface (no wrapping
|
|
114
114
|
panel). Scoped here so the in-thread reply composer keeps its own squarer look. */
|
|
115
|
-
.cmt-canvas-composer {
|
|
115
|
+
.tlui-cmt-canvas-composer {
|
|
116
116
|
position: absolute;
|
|
117
117
|
z-index: var(--tl-layer-menus);
|
|
118
118
|
pointer-events: auto;
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
placement composer a definite width or it collapses to nothing. */
|
|
122
122
|
width: 280px;
|
|
123
123
|
}
|
|
124
|
-
.cmt-canvas-composer .cmt-composer__field {
|
|
124
|
+
.tlui-cmt-canvas-composer .tlui-cmt-composer__field {
|
|
125
125
|
background: var(--tl-color-panel);
|
|
126
126
|
border-color: transparent;
|
|
127
127
|
border-radius: var(--tl-radius-4);
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
the popover layer, so an open thread still draws over it. Sits under the top-right share panel
|
|
133
133
|
(top offset clears its ~48px height) and sizes to its content, scrolling past max-height rather
|
|
134
134
|
than stretching the full canvas height. */
|
|
135
|
-
.cmt-canvas-sidebar {
|
|
135
|
+
.tlui-cmt-canvas-sidebar {
|
|
136
136
|
position: absolute;
|
|
137
137
|
top: 56px;
|
|
138
138
|
right: 8px;
|
|
@@ -17,5 +17,5 @@ export interface CommentBodyProps {
|
|
|
17
17
|
*/
|
|
18
18
|
export function CommentBody({ richText, resolveName }: CommentBodyProps) {
|
|
19
19
|
const html = useMemo(() => renderCommentHtml(richText, resolveName), [richText, resolveName])
|
|
20
|
-
return <div className="cmt-text" dangerouslySetInnerHTML={{ __html: html }} />
|
|
20
|
+
return <div className="tlui-cmt-text" dangerouslySetInnerHTML={{ __html: html }} />
|
|
21
21
|
}
|
|
@@ -38,7 +38,7 @@ describe('renderCommentHtml', () => {
|
|
|
38
38
|
it('renders a mention as a pill and resolves its id to the current name', () => {
|
|
39
39
|
const body = doc(para(text('hey '), mention('u1', 'Ada')))
|
|
40
40
|
const html = renderCommentHtml(body, (id) => (id === 'u1' ? 'Ada Lovelace' : '?'))
|
|
41
|
-
expect(html).toContain('cmt-mention')
|
|
41
|
+
expect(html).toContain('tlui-cmt-mention')
|
|
42
42
|
// the live name from the resolver, not the label stored at insert time
|
|
43
43
|
expect(html).toContain('@Ada Lovelace')
|
|
44
44
|
expect(html).not.toContain('@Ada<')
|
|
@@ -26,7 +26,7 @@ export function CommentsOverflowMenu() {
|
|
|
26
26
|
<TldrawUiDropdownMenuTrigger>
|
|
27
27
|
<button
|
|
28
28
|
type="button"
|
|
29
|
-
className="cmt-header-btn"
|
|
29
|
+
className="tlui-cmt-header-btn"
|
|
30
30
|
title={msg('comments.more-options')}
|
|
31
31
|
aria-label={msg('comments.more-options')}
|
|
32
32
|
>
|
|
@@ -38,11 +38,11 @@ export function CommentsOverflowMenu() {
|
|
|
38
38
|
<TldrawUiDropdownMenuItem>
|
|
39
39
|
<button
|
|
40
40
|
type="button"
|
|
41
|
-
className="cmt-menu-item"
|
|
41
|
+
className="tlui-cmt-menu-item"
|
|
42
42
|
onClick={() => toggleCommentsHidden(editor)}
|
|
43
43
|
>
|
|
44
44
|
<span>{hidden ? msg('comments.show') : msg('comments.hide')}</span>
|
|
45
|
-
<span className="cmt-menu-item__shortcut">{HIDE_SHORTCUT}</span>
|
|
45
|
+
<span className="tlui-cmt-menu-item__shortcut">{HIDE_SHORTCUT}</span>
|
|
46
46
|
</button>
|
|
47
47
|
</TldrawUiDropdownMenuItem>
|
|
48
48
|
</TldrawUiDropdownMenuGroup>
|
|
@@ -186,21 +186,21 @@ function CanvasCommentsLayer(props: CanvasCommentsProps) {
|
|
|
186
186
|
const pending = usePendingComment()
|
|
187
187
|
const openId = useValue('open thread id', () => openThreadId.get(editor), [editor])
|
|
188
188
|
const impreciseShapeAnchor = props.impreciseShapeAnchor ?? options.impreciseShapeAnchor
|
|
189
|
-
// Threads
|
|
190
|
-
//
|
|
191
|
-
// and
|
|
192
|
-
const [
|
|
189
|
+
// Threads held out of clustering because their anchor moved while folded inside a badge
|
|
190
|
+
// (drag, nudge, align, undo, a collaborator — detected by position, not gesture). They render
|
|
191
|
+
// as live pins riding their anchor and rejoin clustering on the next zoom-out.
|
|
192
|
+
const [heldThreadIds, setHeldThreadIds] = useState<ReadonlySet<string>>(EMPTY_SET)
|
|
193
193
|
const adoptOnRebuild = useRef(false)
|
|
194
194
|
const clusterLeaves = useValue(
|
|
195
195
|
'comment cluster leaves',
|
|
196
196
|
() =>
|
|
197
197
|
collectClusterLeaves(
|
|
198
198
|
editor,
|
|
199
|
-
threads.filter((thread) => !
|
|
199
|
+
threads.filter((thread) => !heldThreadIds.has(thread.id)),
|
|
200
200
|
openThreadId.get(editor),
|
|
201
201
|
impreciseShapeAnchor
|
|
202
202
|
),
|
|
203
|
-
[editor, threads, impreciseShapeAnchor,
|
|
203
|
+
[editor, threads, impreciseShapeAnchor, heldThreadIds]
|
|
204
204
|
)
|
|
205
205
|
const clusterZoomBounds = useValue(
|
|
206
206
|
'comment cluster zoom bounds',
|
|
@@ -213,44 +213,69 @@ function CanvasCommentsLayer(props: CanvasCommentsProps) {
|
|
|
213
213
|
runtime.seed(editor.getZoomLevel())
|
|
214
214
|
return { runtime, table }
|
|
215
215
|
}, [clusterLeaves, clusterZoomBounds, editor])
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
//
|
|
221
|
-
// badge
|
|
216
|
+
// The core invariant: the only thing that re-flows clustering doc-wide is zoom. Every rebuild
|
|
217
|
+
// (add / move / delete / open / pop-out) is computed immediately as `latestModel` — the MST
|
|
218
|
+
// stays correct — but the on-screen partition is `renderedModel`, and it only ever changes via
|
|
219
|
+
// (a) the cursor walking on zoom, (b) adoption of the pending rebuild on zoom-out, or
|
|
220
|
+
// (c) LOCAL detach patches: a leaf that left the input (deleted, opened, popped out) is
|
|
221
|
+
// detached from its own badge in place — count and centroid update for that badge alone,
|
|
222
|
+
// and nothing else on the canvas moves.
|
|
222
223
|
const [renderedModel, setRenderedModel] = useState(latestModel)
|
|
223
224
|
let clusterModel = renderedModel
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
) {
|
|
225
|
+
// A page switch replaces the whole scene: hard-reset rather than detach the world.
|
|
226
|
+
const pageId = useValue('comment cluster page', () => editor.getCurrentPageId(), [editor])
|
|
227
|
+
const pageRef = useRef(pageId)
|
|
228
|
+
if (pageRef.current !== pageId) {
|
|
229
|
+
pageRef.current = pageId
|
|
228
230
|
adoptOnRebuild.current = false
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
231
|
+
latestModel.runtime.seed(editor.getZoomLevel())
|
|
232
|
+
if (heldThreadIds.size > 0) setHeldThreadIds(EMPTY_SET)
|
|
233
|
+
setRenderedModel(latestModel)
|
|
234
|
+
clusterModel = latestModel
|
|
235
|
+
}
|
|
236
|
+
// adoptOnRebuild is set by the rejoin reaction below, outside React's render cycle, paired
|
|
237
|
+
// with clearing heldThreadIds. Only trust it once that pairing is actually visible here
|
|
238
|
+
// (heldThreadIds confirmed empty) — an unrelated re-render can land in the gap between the
|
|
239
|
+
// ref being set and the state update it was paired with being applied.
|
|
240
|
+
const rejoinPending = heldThreadIds.size === 0 && adoptOnRebuild.current
|
|
241
|
+
if (renderedModel !== latestModel && rejoinPending) {
|
|
242
|
+
adoptOnRebuild.current = false
|
|
243
|
+
// Carryover seed: band events inherit the outgoing partition's merged/unmerged state, so
|
|
244
|
+
// nothing changes state because of the swap alone. Idempotent, so safe during render.
|
|
233
245
|
latestModel.runtime.seedFrom(editor.getZoomLevel(), renderedModel.runtime.getVisible())
|
|
234
246
|
setRenderedModel(latestModel)
|
|
235
247
|
clusterModel = latestModel
|
|
248
|
+
} else if (heldThreadIds.size === 0 && renderedModel === latestModel) {
|
|
249
|
+
// Nothing pending and nothing to adopt: clear any leftover force-adopt intent so it can't
|
|
250
|
+
// survive to force-adopt a later, unrelated rebuild.
|
|
251
|
+
adoptOnRebuild.current = false
|
|
236
252
|
}
|
|
237
253
|
// Pop-out detection: a leaf folded inside a badge can't follow its anchor (the badge position
|
|
238
|
-
// is baked into the model), so when its live position drifts from the baked one it
|
|
239
|
-
//
|
|
240
|
-
// re-clusters the rest of its pile immediately; the pin itself renders live below.
|
|
254
|
+
// is baked into the model), so when its live position drifts from the baked one, hold it out.
|
|
255
|
+
// It renders as a live pin riding the anchor; the detach loop below shrinks its badge locally.
|
|
241
256
|
const newlyMovedIds = findMovedClusteredLeafIds(clusterModel, latestModel)
|
|
242
257
|
if (newlyMovedIds.length > 0) {
|
|
243
|
-
|
|
244
|
-
console.debug(`[comments] pins popped out of clustering: ${newlyMovedIds.join(', ')}`)
|
|
245
|
-
const next = new Set(movedThreadIds)
|
|
258
|
+
const next = new Set(heldThreadIds)
|
|
246
259
|
for (const id of newlyMovedIds) next.add(id)
|
|
247
|
-
|
|
260
|
+
setHeldThreadIds(next)
|
|
261
|
+
}
|
|
262
|
+
// Local partition maintenance — the only non-zoom visual change, and it is local by
|
|
263
|
+
// construction: any displayed leaf that has left the cluster input (deleted, thread opened,
|
|
264
|
+
// popped out above) is detached from its badge in place. The corrected rebuild is already
|
|
265
|
+
// sitting in latestModel awaiting the next zoom-out.
|
|
266
|
+
{
|
|
267
|
+
const latestLeafIds = new Set(latestModel.table.leaves.map((leaf) => leaf.id))
|
|
268
|
+
for (const leaf of clusterModel.table.leaves) {
|
|
269
|
+
if (!latestLeafIds.has(leaf.id)) {
|
|
270
|
+
clusterModel.runtime.detachLeaf(leaf.id)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
248
273
|
}
|
|
249
274
|
// Moved pins rejoin clustering on the next zoom-out motion: clear the set (so the rebuild
|
|
250
275
|
// includes them again) and adopt that rebuild immediately instead of deferring it. Zooming in
|
|
251
276
|
// never folds pins into clusters — merging is a zoom-out-only move, matching the runtime.
|
|
252
277
|
useEffect(() => {
|
|
253
|
-
if (
|
|
278
|
+
if (heldThreadIds.size === 0) return
|
|
254
279
|
let lastZoom = editor.getZoomLevel()
|
|
255
280
|
return react('rejoin moved comment pins on zoom out', () => {
|
|
256
281
|
const zoom = editor.getZoomLevel()
|
|
@@ -258,9 +283,9 @@ function CanvasCommentsLayer(props: CanvasCommentsProps) {
|
|
|
258
283
|
lastZoom = zoom
|
|
259
284
|
if (zoom >= prevZoom) return
|
|
260
285
|
adoptOnRebuild.current = true
|
|
261
|
-
|
|
286
|
+
setHeldThreadIds(EMPTY_SET)
|
|
262
287
|
})
|
|
263
|
-
}, [
|
|
288
|
+
}, [heldThreadIds, editor])
|
|
264
289
|
// Adopt a pending rebuild only on zoom-out motion: folding deferred additions into clusters is
|
|
265
290
|
// a merge, and merging only happens while zooming out. While zooming in, the stale table still
|
|
266
291
|
// splits correctly on its own (split thresholds are direction-safe by the hysteresis invariant).
|
|
@@ -276,36 +301,44 @@ function CanvasCommentsLayer(props: CanvasCommentsProps) {
|
|
|
276
301
|
setRenderedModel(latestModel)
|
|
277
302
|
})
|
|
278
303
|
}, [clusterModel, latestModel, editor])
|
|
304
|
+
// Threads in the current input that the displayed partition doesn't show anywhere (new
|
|
305
|
+
// comments, reopened threads, undone deletions): render as plain pins until the next
|
|
306
|
+
// zoom-out folds them in. Membership is judged against the *displayed* partition (with
|
|
307
|
+
// detaches applied), not the rendered table, so a detached-then-restored leaf reappears.
|
|
308
|
+
const partitionVersion = clusterModel.runtime.version
|
|
279
309
|
const orphanThreads = useMemo(() => {
|
|
280
310
|
if (clusterModel === latestModel) return []
|
|
281
|
-
const
|
|
311
|
+
const displayed = new Set<string>()
|
|
312
|
+
for (const node of clusterModel.runtime.getVisible().values()) {
|
|
313
|
+
for (const member of node.members) displayed.add(member)
|
|
314
|
+
}
|
|
282
315
|
const latestIds = new Set(latestModel.table.leaves.map((leaf) => leaf.id))
|
|
283
|
-
return threads.filter((thread) => latestIds.has(thread.id) && !
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
316
|
+
return threads.filter((thread) => latestIds.has(thread.id) && !displayed.has(thread.id))
|
|
317
|
+
// The runtime mutates its partition in place; partitionVersion is its change stamp.
|
|
318
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
319
|
+
}, [clusterModel, latestModel, threads, partitionVersion])
|
|
320
|
+
const heldThreads = useMemo(
|
|
321
|
+
() => threads.filter((thread) => heldThreadIds.has(thread.id) && thread.id !== openId),
|
|
322
|
+
[threads, heldThreadIds, openId]
|
|
288
323
|
)
|
|
289
|
-
// Subscribe to the runtime
|
|
290
|
-
// O(1) threshold checks
|
|
291
|
-
//
|
|
292
|
-
// on
|
|
293
|
-
|
|
294
|
-
|
|
324
|
+
// Subscribe to the runtime's partition version, not the raw zoom: onCamera runs on every zoom
|
|
325
|
+
// tick (O(1) threshold checks) but the version only moves when the partition actually changes
|
|
326
|
+
// — so this component only re-renders on cluster changes, not on every camera frame. The memo
|
|
327
|
+
// below keys on a fresh inline read of the version rather than the subscribed value, because
|
|
328
|
+
// render-time detaches (above) bump it after the subscription's computed already evaluated.
|
|
329
|
+
useValue(
|
|
330
|
+
'comment cluster version',
|
|
295
331
|
() => {
|
|
296
332
|
clusterModel.runtime.onCamera(editor.getZoomLevel())
|
|
297
|
-
return clusterModel.runtime.
|
|
333
|
+
return clusterModel.runtime.version
|
|
298
334
|
},
|
|
299
335
|
[clusterModel, editor]
|
|
300
336
|
)
|
|
301
337
|
const visibleNodes = useMemo(() => {
|
|
302
|
-
|
|
303
|
-
//
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
)
|
|
307
|
-
return nodes
|
|
308
|
-
}, [clusterModel, clusterCursor])
|
|
338
|
+
return Array.from(clusterModel.runtime.getVisible().values())
|
|
339
|
+
// The runtime mutates its partition in place; partitionVersion is its change stamp.
|
|
340
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
341
|
+
}, [clusterModel, partitionVersion])
|
|
309
342
|
const fadeNodes = useFadeVisibleNodes(visibleNodes, clusterModel)
|
|
310
343
|
const threadsById = useMemo(
|
|
311
344
|
() => new Map<string, TLCommentThread>(threads.map((thread) => [thread.id, thread])),
|
|
@@ -384,7 +417,7 @@ function CanvasCommentsLayer(props: CanvasCommentsProps) {
|
|
|
384
417
|
// The mention picker owns Escape while it's open — let it dismiss the roster alone.
|
|
385
418
|
if (isMentionPickerOpen()) return
|
|
386
419
|
const target = e.target as HTMLElement | null
|
|
387
|
-
if (target && target.closest('.cmt-editing')) return
|
|
420
|
+
if (target && target.closest('.tlui-cmt-editing')) return
|
|
388
421
|
openThreadId.set(editor, null)
|
|
389
422
|
e.preventDefault()
|
|
390
423
|
e.stopPropagation()
|
|
@@ -414,7 +447,7 @@ function CanvasCommentsLayer(props: CanvasCommentsProps) {
|
|
|
414
447
|
// Render into the container (above the panels' stacking context) so the pins and popovers
|
|
415
448
|
// live in the UI layer rather than being clipped by the canvas layer.
|
|
416
449
|
return createPortal(
|
|
417
|
-
<div ref={layerRef} className="cmt-canvas-layer">
|
|
450
|
+
<div ref={layerRef} className="tlui-cmt-canvas-layer">
|
|
418
451
|
{options.enableClustering ? (
|
|
419
452
|
<>
|
|
420
453
|
{fadeNodes.map(({ node, phase }) => {
|
|
@@ -448,7 +481,7 @@ function CanvasCommentsLayer(props: CanvasCommentsProps) {
|
|
|
448
481
|
regionOptions={regionOptions}
|
|
449
482
|
/>
|
|
450
483
|
))}
|
|
451
|
-
{
|
|
484
|
+
{heldThreads.map((thread) => (
|
|
452
485
|
<ThreadPin
|
|
453
486
|
key={thread.id}
|
|
454
487
|
editor={editor}
|
|
@@ -597,7 +630,7 @@ function cancelClusterFadeFrame(frame: number) {
|
|
|
597
630
|
}
|
|
598
631
|
|
|
599
632
|
function clusterFadeClassName(phase: ClusterFadePhase): string {
|
|
600
|
-
return `cmt-cluster-fade cmt-cluster-fade--${phase}`
|
|
633
|
+
return `tlui-cmt-cluster-fade tlui-cmt-cluster-fade--${phase}`
|
|
601
634
|
}
|
|
602
635
|
|
|
603
636
|
/**
|
|
@@ -627,12 +660,6 @@ function findMovedClusteredLeafIds(
|
|
|
627
660
|
return moved
|
|
628
661
|
}
|
|
629
662
|
|
|
630
|
-
function hasRemovedLeaves(rendered: ClusterTable, latest: ClusterTable): boolean {
|
|
631
|
-
if (rendered.leaves.length === 0) return false
|
|
632
|
-
const latestIds = new Set(latest.leaves.map((leaf) => leaf.id))
|
|
633
|
-
return rendered.leaves.some((leaf) => !latestIds.has(leaf.id))
|
|
634
|
-
}
|
|
635
|
-
|
|
636
663
|
function getClusterZoomBounds(editor: Editor): { minZoom: number; maxZoom: number } {
|
|
637
664
|
const cameraOptions = editor.getCameraOptions()
|
|
638
665
|
const baseZoom = cameraOptions.constraints ? editor.getBaseZoom() : 1
|
|
@@ -733,7 +760,7 @@ const ClusterBadge = memo(function ClusterBadge({
|
|
|
733
760
|
|
|
734
761
|
return (
|
|
735
762
|
<div
|
|
736
|
-
className="cmt-canvas-cluster"
|
|
763
|
+
className="tlui-cmt-canvas-cluster"
|
|
737
764
|
style={{ left: point.x, top: point.y }}
|
|
738
765
|
onPointerDown={stop}
|
|
739
766
|
onClick={(e) => {
|
|
@@ -772,7 +799,7 @@ function ThreadPopover({
|
|
|
772
799
|
usePassThroughWheelEvents(ref)
|
|
773
800
|
usePassThroughMouseOverEvents(ref)
|
|
774
801
|
return createPortal(
|
|
775
|
-
<div ref={ref} className="cmt-canvas-popover" style={style} onPointerDown={stop}>
|
|
802
|
+
<div ref={ref} className="tlui-cmt-canvas-popover" style={style} onPointerDown={stop}>
|
|
776
803
|
{children}
|
|
777
804
|
</div>,
|
|
778
805
|
container
|
|
@@ -831,7 +858,11 @@ function RegionBox({
|
|
|
831
858
|
}
|
|
832
859
|
return (
|
|
833
860
|
<div
|
|
834
|
-
className={
|
|
861
|
+
className={
|
|
862
|
+
movable
|
|
863
|
+
? 'tlui-cmt-canvas-region tlui-cmt-canvas-region--movable'
|
|
864
|
+
: 'tlui-cmt-canvas-region'
|
|
865
|
+
}
|
|
835
866
|
style={rect}
|
|
836
867
|
onPointerDown={movable ? startMove : undefined}
|
|
837
868
|
onPointerMove={movable ? onMove : undefined}
|
|
@@ -939,7 +970,7 @@ function RegionResizeHandles({
|
|
|
939
970
|
{points.map((h) => (
|
|
940
971
|
<div
|
|
941
972
|
key={h.key}
|
|
942
|
-
className="cmt-canvas-region-handle"
|
|
973
|
+
className="tlui-cmt-canvas-region-handle"
|
|
943
974
|
style={{ left: h.left, top: h.top, cursor: h.cursor }}
|
|
944
975
|
onPointerDown={startResize}
|
|
945
976
|
onPointerMove={onResize(h)}
|
|
@@ -1032,15 +1063,17 @@ const ThreadPin = memo(function ThreadPin({
|
|
|
1032
1063
|
const onPointerDown = (e: PointerEvent) => {
|
|
1033
1064
|
const target = e.target as HTMLElement | null
|
|
1034
1065
|
if (!target) return
|
|
1035
|
-
if (target.closest('.cmt-canvas-popover')) return
|
|
1066
|
+
if (target.closest('.tlui-cmt-canvas-popover')) return
|
|
1036
1067
|
const marker = markerRef.current
|
|
1037
1068
|
if (marker && marker.contains(target)) return
|
|
1038
1069
|
// A press on a region's resize handle or movable body edits this thread — don't dismiss it.
|
|
1039
|
-
if (target.closest('.cmt-canvas-region-handle, .cmt-canvas-region--movable')) return
|
|
1070
|
+
if (target.closest('.tlui-cmt-canvas-region-handle, .tlui-cmt-canvas-region--movable')) return
|
|
1040
1071
|
// A click inside a menu/popover layered above us (the sidebar's filter or overflow
|
|
1041
1072
|
// dropdown, or the composer's mention picker — all portaled elsewhere) belongs to that
|
|
1042
1073
|
// layer; defer to its own dismissal instead of closing the thread out from under it.
|
|
1043
|
-
if (
|
|
1074
|
+
if (
|
|
1075
|
+
target.closest('.tlui-menu, [data-radix-popper-content-wrapper], .tlui-cmt-mention-popup')
|
|
1076
|
+
)
|
|
1044
1077
|
return
|
|
1045
1078
|
openThreadId.set(editor, null)
|
|
1046
1079
|
}
|
|
@@ -1128,7 +1161,7 @@ const ThreadPin = memo(function ThreadPin({
|
|
|
1128
1161
|
if (editingId === comment.id) {
|
|
1129
1162
|
return (
|
|
1130
1163
|
<div
|
|
1131
|
-
className="cmt-editing"
|
|
1164
|
+
className="tlui-cmt-editing"
|
|
1132
1165
|
onKeyDown={(e) => {
|
|
1133
1166
|
if (e.key === 'Escape') {
|
|
1134
1167
|
setEditingId(null)
|
|
@@ -1157,7 +1190,7 @@ const ThreadPin = memo(function ThreadPin({
|
|
|
1157
1190
|
actions={
|
|
1158
1191
|
comment.authorId === currentUserId ? (
|
|
1159
1192
|
<button
|
|
1160
|
-
className="cmt-thread__action"
|
|
1193
|
+
className="tlui-cmt-thread__action"
|
|
1161
1194
|
title={msg('comments.edit')}
|
|
1162
1195
|
onClick={() => startEdit(comment)}
|
|
1163
1196
|
>
|
|
@@ -1173,7 +1206,7 @@ const ThreadPin = memo(function ThreadPin({
|
|
|
1173
1206
|
<>
|
|
1174
1207
|
{currentUserId && (
|
|
1175
1208
|
<button
|
|
1176
|
-
className="cmt-thread__action"
|
|
1209
|
+
className="tlui-cmt-thread__action"
|
|
1177
1210
|
title={msg(thread.resolved ? 'comments.reopen' : 'comments.resolve')}
|
|
1178
1211
|
onClick={toggleResolve}
|
|
1179
1212
|
>
|
|
@@ -1186,7 +1219,7 @@ const ThreadPin = memo(function ThreadPin({
|
|
|
1186
1219
|
)}
|
|
1187
1220
|
{currentUserId && (
|
|
1188
1221
|
<button
|
|
1189
|
-
className="cmt-thread__action"
|
|
1222
|
+
className="tlui-cmt-thread__action"
|
|
1190
1223
|
title={msg('comments.delete')}
|
|
1191
1224
|
onClick={deleteThread}
|
|
1192
1225
|
>
|
|
@@ -1194,7 +1227,7 @@ const ThreadPin = memo(function ThreadPin({
|
|
|
1194
1227
|
</button>
|
|
1195
1228
|
)}
|
|
1196
1229
|
<button
|
|
1197
|
-
className="cmt-thread__action"
|
|
1230
|
+
className="tlui-cmt-thread__action"
|
|
1198
1231
|
title={msg('comments.dismiss')}
|
|
1199
1232
|
onClick={() => openThreadId.set(editor, null)}
|
|
1200
1233
|
>
|
|
@@ -1313,12 +1346,12 @@ const ThreadPin = memo(function ThreadPin({
|
|
|
1313
1346
|
/>
|
|
1314
1347
|
)}
|
|
1315
1348
|
<div
|
|
1316
|
-
className={open ? 'cmt-canvas-pin cmt-canvas-pin--open' : 'cmt-canvas-pin'}
|
|
1349
|
+
className={open ? 'tlui-cmt-canvas-pin tlui-cmt-canvas-pin--open' : 'tlui-cmt-canvas-pin'}
|
|
1317
1350
|
style={{ left: renderPoint.x, top: renderPoint.y }}
|
|
1318
1351
|
>
|
|
1319
1352
|
<div
|
|
1320
1353
|
ref={markerRef}
|
|
1321
|
-
className="cmt-canvas-pin__marker"
|
|
1354
|
+
className="tlui-cmt-canvas-pin__marker"
|
|
1322
1355
|
onPointerDown={startDrag}
|
|
1323
1356
|
onPointerMove={onDrag}
|
|
1324
1357
|
onPointerUp={endDrag}
|
|
@@ -1402,7 +1435,7 @@ function PendingComposer({
|
|
|
1402
1435
|
if (!el || !target) return
|
|
1403
1436
|
// A click in the composer, or in the mention picker it spawns (portaled elsewhere), is
|
|
1404
1437
|
// not "outside" — keep the draft open so the pick can insert.
|
|
1405
|
-
if (el.contains(target) || target.closest('.cmt-mention-popup')) return
|
|
1438
|
+
if (el.contains(target) || target.closest('.tlui-cmt-mention-popup')) return
|
|
1406
1439
|
pendingComment.set(editor, null)
|
|
1407
1440
|
}
|
|
1408
1441
|
document.addEventListener('pointerdown', onPointerDown, true)
|
|
@@ -1434,7 +1467,7 @@ function PendingComposer({
|
|
|
1434
1467
|
return createPortal(
|
|
1435
1468
|
<div
|
|
1436
1469
|
ref={ref}
|
|
1437
|
-
className="cmt-canvas-composer"
|
|
1470
|
+
className="tlui-cmt-canvas-composer"
|
|
1438
1471
|
style={{ left: point.x, top: point.y }}
|
|
1439
1472
|
onPointerDown={stop}
|
|
1440
1473
|
onKeyDown={(e) => {
|
|
@@ -149,7 +149,7 @@ export function CanvasCommentsSidebar(props: CanvasCommentsSidebarProps) {
|
|
|
149
149
|
items={items}
|
|
150
150
|
header={header ?? msg('comments.title')}
|
|
151
151
|
headerAction={
|
|
152
|
-
<div className="cmt-list__header-actions">
|
|
152
|
+
<div className="tlui-cmt-list__header-actions">
|
|
153
153
|
<CommentsFilterMenu
|
|
154
154
|
canFilterByAuthor={currentUserId !== undefined}
|
|
155
155
|
canFilterByUnread={isCommentUnread !== undefined}
|
|
@@ -176,7 +176,7 @@ function SidebarPanel({ container, children }: { container: HTMLElement; childre
|
|
|
176
176
|
const ref = useRef<HTMLDivElement>(null)
|
|
177
177
|
usePassThroughMouseOverEvents(ref)
|
|
178
178
|
return createPortal(
|
|
179
|
-
<div ref={ref} className="cmt-canvas-sidebar">
|
|
179
|
+
<div ref={ref} className="tlui-cmt-canvas-sidebar">
|
|
180
180
|
{children}
|
|
181
181
|
</div>,
|
|
182
182
|
container
|