@tldraw/commenting 5.3.0-internal.c5c7f1d817d0 → 5.3.0-next.2fa9c61a8de6
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/README.md +0 -2
- package/commenting.css +1040 -230
- package/dist-cjs/canvas/anchor-lifecycle.js +148 -0
- package/dist-cjs/canvas/anchor-lifecycle.js.map +7 -0
- package/dist-cjs/canvas/cluster-input.js +2 -2
- package/dist-cjs/canvas/cluster-input.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-mutations.js +123 -0
- package/dist-cjs/canvas/comment-mutations.js.map +7 -0
- package/dist-cjs/canvas/comment-reactions.js +163 -0
- package/dist-cjs/canvas/comment-reactions.js.map +7 -0
- package/dist-cjs/canvas/comment-render.js +5 -3
- package/dist-cjs/canvas/comment-render.js.map +2 -2
- package/dist-cjs/canvas/comment-store.js +19 -9
- package/dist-cjs/canvas/comment-store.js.map +2 -2
- package/dist-cjs/canvas/comment-tool.js +64 -14
- package/dist-cjs/canvas/comment-tool.js.map +2 -2
- package/dist-cjs/canvas/comments-filter-menu.js +20 -38
- package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
- package/dist-cjs/canvas/comments-overflow-menu.js +22 -27
- package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
- package/dist-cjs/canvas/comments-overlay.js +377 -437
- package/dist-cjs/canvas/comments-overlay.js.map +2 -2
- package/dist-cjs/canvas/comments-sidebar.js +34 -31
- package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
- package/dist-cjs/canvas/comments-visibility-toggle.js +65 -0
- package/dist-cjs/canvas/comments-visibility-toggle.js.map +7 -0
- package/dist-cjs/canvas/context.js +17 -0
- package/dist-cjs/canvas/context.js.map +7 -0
- package/dist-cjs/canvas/hooks.js +22 -3
- package/dist-cjs/canvas/hooks.js.map +2 -2
- package/dist-cjs/canvas/options.js +19 -2
- package/dist-cjs/canvas/options.js.map +2 -2
- package/dist-cjs/canvas/pin-stacking.js +56 -0
- package/dist-cjs/canvas/pin-stacking.js.map +7 -0
- 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 +28 -14
- package/dist-cjs/canvas/state.js.map +2 -2
- package/dist-cjs/canvas/thread-preview.js +185 -0
- package/dist-cjs/canvas/thread-preview.js.map +7 -0
- package/dist-cjs/canvas/thread-stack.js +199 -0
- package/dist-cjs/canvas/thread-stack.js.map +7 -0
- package/dist-cjs/canvas/thread-state.js +67 -21
- package/dist-cjs/canvas/thread-state.js.map +2 -2
- package/dist-cjs/canvas/thread-view.js +397 -0
- package/dist-cjs/canvas/thread-view.js.map +7 -0
- package/dist-cjs/clustering/computeClusterTable.js +4 -4
- package/dist-cjs/clustering/computeClusterTable.js.map +2 -2
- package/dist-cjs/clustering/runtime.js +9 -9
- package/dist-cjs/clustering/runtime.js.map +2 -2
- package/dist-cjs/clustering/types.js.map +1 -1
- package/dist-cjs/index.d.ts +763 -353
- package/dist-cjs/index.js +46 -28
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/ui/byline.js +3 -1
- package/dist-cjs/ui/byline.js.map +2 -2
- package/dist-cjs/ui/comment-card.js +12 -5
- package/dist-cjs/ui/comment-card.js.map +2 -2
- package/dist-cjs/ui/comment-composer.js +112 -17
- package/dist-cjs/ui/comment-composer.js.map +2 -2
- package/dist-cjs/ui/comment-pin.js +20 -2
- package/dist-cjs/ui/comment-pin.js.map +2 -2
- package/dist-cjs/ui/comment-thread.js +4 -2
- package/dist-cjs/ui/comment-thread.js.map +2 -2
- package/dist-cjs/ui/comments-list.js +22 -10
- package/dist-cjs/ui/comments-list.js.map +2 -2
- package/dist-cjs/ui/count-badge.js +3 -2
- package/dist-cjs/ui/count-badge.js.map +2 -2
- package/dist-cjs/ui/emoji-picker.js +54 -0
- package/dist-cjs/ui/emoji-picker.js.map +7 -0
- package/dist-cjs/ui/format-time.js +4 -1
- package/dist-cjs/ui/format-time.js.map +2 -2
- package/dist-cjs/ui/reaction-picker.js +73 -0
- package/dist-cjs/ui/reaction-picker.js.map +7 -0
- package/dist-cjs/ui/reaction.js +50 -4
- package/dist-cjs/ui/reaction.js.map +2 -2
- package/dist-cjs/ui/reactions.js +20 -15
- package/dist-cjs/ui/reactions.js.map +2 -2
- package/dist-cjs/ui/{mention.js → reply-count.js} +9 -11
- package/dist-cjs/ui/reply-count.js.map +7 -0
- package/dist-cjs/ui/send-button.js +20 -1
- package/dist-cjs/ui/send-button.js.map +2 -2
- package/dist-cjs/ui/{comment-text.js → tooltip-button.js} +12 -9
- package/dist-cjs/ui/tooltip-button.js.map +7 -0
- package/dist-esm/canvas/anchor-lifecycle.mjs +128 -0
- package/dist-esm/canvas/anchor-lifecycle.mjs.map +7 -0
- package/dist-esm/canvas/cluster-input.mjs +2 -2
- package/dist-esm/canvas/cluster-input.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-mutations.mjs +103 -0
- package/dist-esm/canvas/comment-mutations.mjs.map +7 -0
- package/dist-esm/canvas/comment-reactions.mjs +152 -0
- package/dist-esm/canvas/comment-reactions.mjs.map +7 -0
- package/dist-esm/canvas/comment-render.mjs +5 -3
- package/dist-esm/canvas/comment-render.mjs.map +2 -2
- package/dist-esm/canvas/comment-store.mjs +19 -9
- package/dist-esm/canvas/comment-store.mjs.map +2 -2
- package/dist-esm/canvas/comment-tool.mjs +67 -17
- package/dist-esm/canvas/comment-tool.mjs.map +2 -2
- package/dist-esm/canvas/comments-filter-menu.mjs +20 -38
- package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
- package/dist-esm/canvas/comments-overflow-menu.mjs +23 -27
- package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
- package/dist-esm/canvas/comments-overlay.mjs +398 -442
- package/dist-esm/canvas/comments-overlay.mjs.map +3 -3
- package/dist-esm/canvas/comments-sidebar.mjs +37 -34
- package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
- package/dist-esm/canvas/comments-visibility-toggle.mjs +45 -0
- package/dist-esm/canvas/comments-visibility-toggle.mjs.map +7 -0
- package/dist-esm/canvas/context.mjs +1 -0
- package/dist-esm/canvas/context.mjs.map +7 -0
- package/dist-esm/canvas/hooks.mjs +28 -5
- package/dist-esm/canvas/hooks.mjs.map +2 -2
- package/dist-esm/canvas/options.mjs +21 -3
- package/dist-esm/canvas/options.mjs.map +2 -2
- package/dist-esm/canvas/pin-stacking.mjs +36 -0
- package/dist-esm/canvas/pin-stacking.mjs.map +7 -0
- 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 +29 -19
- package/dist-esm/canvas/state.mjs.map +2 -2
- package/dist-esm/canvas/thread-preview.mjs +170 -0
- package/dist-esm/canvas/thread-preview.mjs.map +7 -0
- package/dist-esm/canvas/thread-stack.mjs +190 -0
- package/dist-esm/canvas/thread-stack.mjs.map +7 -0
- package/dist-esm/canvas/thread-state.mjs +71 -22
- package/dist-esm/canvas/thread-state.mjs.map +2 -2
- package/dist-esm/canvas/thread-view.mjs +402 -0
- package/dist-esm/canvas/thread-view.mjs.map +7 -0
- package/dist-esm/clustering/computeClusterTable.mjs +4 -4
- package/dist-esm/clustering/computeClusterTable.mjs.map +2 -2
- package/dist-esm/clustering/runtime.mjs +9 -9
- package/dist-esm/clustering/runtime.mjs.map +2 -2
- package/dist-esm/index.d.mts +763 -353
- package/dist-esm/index.mjs +87 -41
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/ui/byline.mjs +3 -1
- package/dist-esm/ui/byline.mjs.map +2 -2
- package/dist-esm/ui/comment-card.mjs +12 -5
- package/dist-esm/ui/comment-card.mjs.map +2 -2
- package/dist-esm/ui/comment-composer.mjs +122 -16
- package/dist-esm/ui/comment-composer.mjs.map +2 -2
- package/dist-esm/ui/comment-pin.mjs +20 -2
- package/dist-esm/ui/comment-pin.mjs.map +2 -2
- package/dist-esm/ui/comment-thread.mjs +4 -2
- package/dist-esm/ui/comment-thread.mjs.map +2 -2
- package/dist-esm/ui/comments-list.mjs +39 -25
- package/dist-esm/ui/comments-list.mjs.map +2 -2
- package/dist-esm/ui/count-badge.mjs +3 -2
- package/dist-esm/ui/count-badge.mjs.map +2 -2
- package/dist-esm/ui/emoji-picker.mjs +34 -0
- package/dist-esm/ui/emoji-picker.mjs.map +7 -0
- package/dist-esm/ui/format-time.mjs +4 -1
- package/dist-esm/ui/format-time.mjs.map +2 -2
- package/dist-esm/ui/reaction-picker.mjs +60 -0
- package/dist-esm/ui/reaction-picker.mjs.map +7 -0
- package/dist-esm/ui/reaction.mjs +51 -5
- package/dist-esm/ui/reaction.mjs.map +2 -2
- package/dist-esm/ui/reactions.mjs +21 -16
- package/dist-esm/ui/reactions.mjs.map +2 -2
- package/dist-esm/ui/reply-count.mjs +9 -0
- package/dist-esm/ui/reply-count.mjs.map +7 -0
- package/dist-esm/ui/send-button.mjs +20 -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 +6 -6
- package/src/canvas/anchor-lifecycle.test.ts +290 -0
- package/src/canvas/anchor-lifecycle.ts +205 -0
- package/src/canvas/canvas.css +439 -24
- package/src/canvas/cluster-input.test.ts +31 -23
- package/src/canvas/cluster-input.ts +3 -4
- package/src/canvas/comment-drafts.test.ts +55 -0
- package/src/canvas/comment-drafts.ts +46 -0
- package/src/canvas/comment-mutations.test.ts +410 -0
- package/src/canvas/comment-mutations.ts +288 -0
- package/src/canvas/comment-reactions.test.ts +132 -0
- package/src/canvas/comment-reactions.tsx +245 -0
- package/src/canvas/comment-render.ts +9 -5
- package/src/canvas/comment-store.ts +67 -27
- package/src/canvas/comment-tool.test.ts +162 -0
- package/src/canvas/comment-tool.tsx +105 -23
- package/src/canvas/comments-filter-menu.tsx +23 -32
- package/src/canvas/comments-overflow-menu.tsx +12 -19
- package/src/canvas/comments-overlay.tsx +602 -562
- package/src/canvas/comments-sidebar.test.ts +45 -0
- package/src/canvas/comments-sidebar.tsx +75 -57
- package/src/canvas/comments-visibility-toggle.tsx +49 -0
- package/src/canvas/context.ts +47 -0
- package/src/canvas/hooks.ts +52 -11
- package/src/canvas/options.test.ts +52 -2
- package/src/canvas/options.ts +133 -9
- package/src/canvas/pin-stacking.test.ts +114 -0
- package/src/canvas/pin-stacking.ts +57 -0
- package/src/canvas/sidebar-filters.ts +4 -3
- package/src/canvas/state.ts +92 -35
- package/src/canvas/thread-preview.test.ts +91 -0
- package/src/canvas/thread-preview.tsx +304 -0
- package/src/canvas/thread-stack.tsx +252 -0
- package/src/canvas/thread-state.test.ts +209 -0
- package/src/canvas/thread-state.ts +137 -36
- package/src/canvas/thread-view.tsx +516 -0
- package/src/clustering/computeClusterTable.test.ts +24 -23
- package/src/clustering/computeClusterTable.ts +5 -5
- package/src/clustering/runtime.test.ts +34 -0
- package/src/clustering/runtime.ts +20 -15
- package/src/clustering/types.ts +9 -9
- package/src/index.ts +76 -40
- package/src/ui/byline.tsx +4 -2
- package/src/ui/comment-card.tsx +17 -7
- package/src/ui/comment-composer.tsx +160 -13
- package/src/ui/comment-pin.tsx +22 -2
- package/src/ui/comment-thread.tsx +6 -1
- package/src/ui/comments-list.test.ts +31 -0
- package/src/ui/comments-list.tsx +32 -12
- package/src/ui/comments.css +489 -204
- package/src/ui/count-badge.tsx +9 -2
- package/src/ui/emoji-picker.test.ts +25 -0
- package/src/ui/emoji-picker.tsx +76 -0
- package/src/ui/format-time.ts +7 -2
- package/src/ui/reaction-picker.tsx +109 -0
- package/src/ui/reaction.tsx +116 -4
- package/src/ui/reactions.tsx +68 -13
- package/src/ui/reply-count.ts +16 -0
- package/src/ui/send-button.tsx +18 -3
- package/src/ui/tooltip-button.tsx +20 -0
- package/dist-cjs/canvas/region-options.js +0 -49
- package/dist-cjs/canvas/region-options.js.map +0 -7
- package/dist-cjs/ui/avatar.js +0 -51
- package/dist-cjs/ui/avatar.js.map +0 -7
- package/dist-cjs/ui/comment-mention.js +0 -42
- package/dist-cjs/ui/comment-mention.js.map +0 -7
- package/dist-cjs/ui/comment-text.js.map +0 -7
- package/dist-cjs/ui/mention-list.js +0 -70
- package/dist-cjs/ui/mention-list.js.map +0 -7
- package/dist-cjs/ui/mention-suggestion.js +0 -206
- package/dist-cjs/ui/mention-suggestion.js.map +0 -7
- package/dist-cjs/ui/mention.js.map +0 -7
- package/dist-cjs/ui/render-markdown.js +0 -63
- package/dist-cjs/ui/render-markdown.js.map +0 -7
- package/dist-esm/canvas/region-options.mjs +0 -29
- package/dist-esm/canvas/region-options.mjs.map +0 -7
- package/dist-esm/ui/avatar.mjs +0 -31
- package/dist-esm/ui/avatar.mjs.map +0 -7
- package/dist-esm/ui/comment-mention.mjs +0 -22
- package/dist-esm/ui/comment-mention.mjs.map +0 -7
- package/dist-esm/ui/comment-text.mjs +0 -9
- package/dist-esm/ui/comment-text.mjs.map +0 -7
- package/dist-esm/ui/mention-list.mjs +0 -50
- package/dist-esm/ui/mention-list.mjs.map +0 -7
- package/dist-esm/ui/mention-suggestion.mjs +0 -186
- package/dist-esm/ui/mention-suggestion.mjs.map +0 -7
- package/dist-esm/ui/mention.mjs +0 -11
- package/dist-esm/ui/mention.mjs.map +0 -7
- package/dist-esm/ui/render-markdown.mjs +0 -45
- package/dist-esm/ui/render-markdown.mjs.map +0 -7
- package/src/canvas/region-options.ts +0 -57
- package/src/ui/avatar.tsx +0 -38
- package/src/ui/comment-mention.ts +0 -47
- package/src/ui/comment-text.tsx +0 -12
- package/src/ui/mention-list.tsx +0 -106
- package/src/ui/mention-suggestion.test.ts +0 -18
- package/src/ui/mention-suggestion.tsx +0 -285
- package/src/ui/mention.tsx +0 -9
- package/src/ui/render-markdown.tsx +0 -72
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ClusterNode, ClusterTable, MergeEvent } from './types'
|
|
2
2
|
|
|
3
|
-
/** @
|
|
3
|
+
/** @internal */
|
|
4
4
|
export interface ClusterRuntime {
|
|
5
5
|
/** Events[0..k) are in the cursor (applied unless suppressed). Exposed for tests/debugging. */
|
|
6
6
|
readonly k: number
|
|
@@ -42,7 +42,7 @@ export interface ClusterRuntime {
|
|
|
42
42
|
getVisible(): ReadonlyMap<string, ClusterNode>
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/** @
|
|
45
|
+
/** @internal */
|
|
46
46
|
export function createClusterRuntime(table: ClusterTable): ClusterRuntime {
|
|
47
47
|
return new ClusterRuntimeImpl(table)
|
|
48
48
|
}
|
|
@@ -141,6 +141,24 @@ class ClusterRuntimeImpl implements ClusterRuntime {
|
|
|
141
141
|
}
|
|
142
142
|
|
|
143
143
|
let changed = false
|
|
144
|
+
// Heal suppressed events at their own merge threshold BEFORE the merge walk: a zoom-out
|
|
145
|
+
// past zMerge merges a held-out band event exactly as if it had still been ahead of the
|
|
146
|
+
// cursor. Healing must precede the walk because a single zoom jump can cross both a
|
|
147
|
+
// suppressed event and an event that consumes its result; applying the consumer first
|
|
148
|
+
// would leave the unapplied producer's children in `visible` (its delete is a no-op) and
|
|
149
|
+
// then re-add the producer on top, double-counting those leaves. Threshold monotonicity
|
|
150
|
+
// guarantees a suppressed producer's zMerge is >= its consumer's, so anything the walk
|
|
151
|
+
// needs has already healed. Set iteration is insertion order (ascending index), so a
|
|
152
|
+
// healed event's suppressed children (larger zMerge, smaller index) heal before it.
|
|
153
|
+
if (this.suppressed.size > 0) {
|
|
154
|
+
for (const i of [...this.suppressed]) {
|
|
155
|
+
if (zoom <= this.table.events[i].zMerge) {
|
|
156
|
+
this.suppressed.delete(i)
|
|
157
|
+
applyEvent(this.visible, this.table.events[i])
|
|
158
|
+
changed = true
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
144
162
|
while (this.k < this.table.events.length && zoom <= this.table.events[this.k].zMerge) {
|
|
145
163
|
applyEvent(this.visible, this.table.events[this.k])
|
|
146
164
|
this.k++
|
|
@@ -154,19 +172,6 @@ class ClusterRuntimeImpl implements ClusterRuntime {
|
|
|
154
172
|
changed = true
|
|
155
173
|
}
|
|
156
174
|
}
|
|
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
175
|
if (changed) this.version++
|
|
171
176
|
}
|
|
172
177
|
|
package/src/clustering/types.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { VecLike } from 'tldraw'
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* One comment thread's pin, already resolved to a page-space anchor point.
|
|
5
|
-
* @
|
|
5
|
+
* @internal
|
|
6
6
|
*/
|
|
7
7
|
export interface LeafInput {
|
|
8
8
|
/** Unique. Thread id. Uniqueness is a precondition — throw on duplicates. */
|
|
@@ -23,7 +23,7 @@ export interface MstEdge {
|
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* A cluster in the merge tree: a leaf (one thread) or a merged group.
|
|
26
|
-
* @
|
|
26
|
+
* @internal
|
|
27
27
|
*/
|
|
28
28
|
export interface ClusterNode {
|
|
29
29
|
/** Leaves: the thread id verbatim. Merged nodes: `cluster:${count}:${minMemberId}`. */
|
|
@@ -58,7 +58,7 @@ export interface ContractedEvent {
|
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
60
|
* A finalized merge event, ready for the runtime.
|
|
61
|
-
* @
|
|
61
|
+
* @internal
|
|
62
62
|
*/
|
|
63
63
|
export interface MergeEvent {
|
|
64
64
|
zMerge: number
|
|
@@ -70,7 +70,7 @@ export interface MergeEvent {
|
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
72
|
* The precomputed clustering schedule for one page's comments.
|
|
73
|
-
* @
|
|
73
|
+
* @internal
|
|
74
74
|
*/
|
|
75
75
|
export interface ClusterTable {
|
|
76
76
|
/** Sorted non-increasing by zMerge; satisfies the invariants of CLUSTERING.md §7.6. */
|
|
@@ -79,15 +79,15 @@ export interface ClusterTable {
|
|
|
79
79
|
leaves: readonly ClusterNode[]
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
/** @
|
|
82
|
+
/** @internal */
|
|
83
83
|
export interface ClusterOptions {
|
|
84
|
-
/** Cluster (merge) distance, screen px. Default
|
|
84
|
+
/** Cluster (merge) distance, screen px. Default 22. */
|
|
85
85
|
Tc?: number
|
|
86
|
-
/** Uncluster (split) distance, screen px. Must be \> Tc. Default 1.
|
|
86
|
+
/** Uncluster (split) distance, screen px. Must be \> Tc. Default 1.2 · Tc. */
|
|
87
87
|
Tu?: number
|
|
88
|
-
/** Contraction window ratio. Default 0.
|
|
88
|
+
/** Contraction window ratio. Default 0.7. */
|
|
89
89
|
eps?: number
|
|
90
|
-
/** Max cluster screen extent at birth, screen px. Must be \>= Tc. Default 3 · Tc. */
|
|
90
|
+
/** Max cluster screen extent at birth, screen px. Must be \>= Tc. Default 3.75 · Tc. */
|
|
91
91
|
Dmax?: number
|
|
92
92
|
/** Camera zoom bounds — pass the editor's camera constraints. Required. */
|
|
93
93
|
minZoom: number
|
package/src/index.ts
CHANGED
|
@@ -1,58 +1,93 @@
|
|
|
1
1
|
import { registerTldrawLibraryVersion } from '@tldraw/utils'
|
|
2
2
|
|
|
3
3
|
// Presentational commenting components. These are tldraw-independent and can be used to build
|
|
4
|
-
// custom commenting UI.
|
|
5
|
-
|
|
4
|
+
// custom commenting UI. The mention picker, pill, and avatar are re-exported from @tldraw/mentions
|
|
5
|
+
// so the commenting public API is unchanged.
|
|
6
|
+
export {
|
|
7
|
+
Avatar,
|
|
8
|
+
type AvatarProps,
|
|
9
|
+
type CommentAuthor,
|
|
10
|
+
createMentionSuggestion,
|
|
11
|
+
filterMentionMembers,
|
|
12
|
+
Mention,
|
|
13
|
+
MentionList,
|
|
14
|
+
type MentionListProps,
|
|
15
|
+
type MentionMember,
|
|
16
|
+
type MentionProps,
|
|
17
|
+
type MentionSuggestionOptions,
|
|
18
|
+
} from '@tldraw/mentions'
|
|
6
19
|
export { Byline, type BylineProps } from './ui/byline'
|
|
7
20
|
export { CommentCard, type CommentCardProps } from './ui/comment-card'
|
|
8
21
|
export { CommentComposer, type CommentComposerProps } from './ui/comment-composer'
|
|
9
22
|
export { CountBadge, type CountBadgeProps } from './ui/count-badge'
|
|
10
23
|
export { CommentPin, type CommentPinProps } from './ui/comment-pin'
|
|
11
|
-
export { CommentText, type CommentTextProps } from './ui/comment-text'
|
|
12
24
|
export { CommentThread, type CommentThreadProps } from './ui/comment-thread'
|
|
13
|
-
export {
|
|
25
|
+
export {
|
|
26
|
+
CommentsList,
|
|
27
|
+
isOpenInNewTabClick,
|
|
28
|
+
type CommentListItemProps,
|
|
29
|
+
type CommentsListProps,
|
|
30
|
+
} from './ui/comments-list'
|
|
14
31
|
export { EmptyState, type EmptyStateProps } from './ui/empty-state'
|
|
15
32
|
export { formatRelativeTime } from './ui/format-time'
|
|
16
|
-
export { Mention, type MentionProps } from './ui/mention'
|
|
17
|
-
export { MentionList, type MentionListProps, type MentionMember } from './ui/mention-list'
|
|
18
33
|
export {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
type
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
export {
|
|
25
|
-
|
|
34
|
+
DEFAULT_REACTION_EMOJI,
|
|
35
|
+
EmojiPicker,
|
|
36
|
+
type EmojiPickerProps,
|
|
37
|
+
isAllowedReactionEmoji,
|
|
38
|
+
} from './ui/emoji-picker'
|
|
39
|
+
export {
|
|
40
|
+
Reaction,
|
|
41
|
+
type ReactionProps,
|
|
42
|
+
type RenderReaction,
|
|
43
|
+
defaultRenderReaction,
|
|
44
|
+
DefaultReactionTooltip,
|
|
45
|
+
DefaultReactionTooltipContent,
|
|
46
|
+
type ReactionTooltipProps,
|
|
47
|
+
} from './ui/reaction'
|
|
48
|
+
export { ReactionPicker, type ReactionPickerProps } from './ui/reaction-picker'
|
|
49
|
+
export {
|
|
50
|
+
Reactions,
|
|
51
|
+
type ReactionReactor,
|
|
52
|
+
type ReactionsProps,
|
|
53
|
+
type ReactionSummary,
|
|
54
|
+
} from './ui/reactions'
|
|
26
55
|
export { SendButton, type SendButtonProps } from './ui/send-button'
|
|
27
56
|
|
|
28
57
|
// The tldraw-coupled commenting layer: the comment tool, reactive hooks over the comment
|
|
29
58
|
// records, a rich-text body renderer, and a batteries-included <CanvasComments> overlay. Pairs
|
|
30
59
|
// with the presentational components above.
|
|
60
|
+
export { registerCommentAnchorLifecycle } from './canvas/anchor-lifecycle'
|
|
31
61
|
export { CommentBody, type CommentBodyProps } from './canvas/comment-body'
|
|
32
62
|
export {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
63
|
+
deleteComment,
|
|
64
|
+
deleteThread,
|
|
65
|
+
editComment,
|
|
66
|
+
putCommentRecords,
|
|
67
|
+
removeCommentRecords,
|
|
68
|
+
reopenThread,
|
|
69
|
+
resolveThread,
|
|
70
|
+
} from './canvas/comment-mutations'
|
|
71
|
+
export {
|
|
72
|
+
CommentReactionPicker,
|
|
73
|
+
type CommentReactionPickerProps,
|
|
74
|
+
CommentReactions,
|
|
75
|
+
type CommentReactionsProps,
|
|
76
|
+
summarizeReactions,
|
|
77
|
+
toggleCommentReaction,
|
|
78
|
+
useCommentReactions,
|
|
79
|
+
} from './canvas/comment-reactions'
|
|
80
|
+
export { CommentTool, commentToolOverrides, commentTools } from './canvas/comment-tool'
|
|
40
81
|
export {
|
|
82
|
+
getCommentReactions,
|
|
41
83
|
getCommentRecord,
|
|
42
84
|
getComments,
|
|
43
85
|
getCommentThreads,
|
|
44
|
-
|
|
45
|
-
|
|
86
|
+
getLiveComments,
|
|
87
|
+
getLiveCommentThreads,
|
|
46
88
|
type TLCommentRecord,
|
|
47
89
|
} from './canvas/comment-store'
|
|
48
|
-
export {
|
|
49
|
-
export type {
|
|
50
|
-
ClusterNode,
|
|
51
|
-
ClusterOptions,
|
|
52
|
-
ClusterTable,
|
|
53
|
-
LeafInput,
|
|
54
|
-
MergeEvent,
|
|
55
|
-
} from './clustering/types'
|
|
90
|
+
export { type CommentingContext } from './canvas/context'
|
|
56
91
|
export { CommentsFilterMenu, type CommentsFilterMenuProps } from './canvas/comments-filter-menu'
|
|
57
92
|
export { CommentsMenuItem } from './canvas/comments-menu-item'
|
|
58
93
|
export { CanvasComments, type CanvasCommentsProps } from './canvas/comments-overlay'
|
|
@@ -60,34 +95,35 @@ export {
|
|
|
60
95
|
type CommentingComponents,
|
|
61
96
|
type CommentingOptions,
|
|
62
97
|
defaultCommentingOptions,
|
|
98
|
+
getCanComment,
|
|
63
99
|
getCommentingOptions,
|
|
100
|
+
type ShapeCommentPrecisionContext,
|
|
101
|
+
useCanComment,
|
|
64
102
|
useCommentingOptions,
|
|
65
103
|
} from './canvas/options'
|
|
66
104
|
export { CommentsOverflowMenu } from './canvas/comments-overflow-menu'
|
|
105
|
+
export { CommentsVisibilityToggle } from './canvas/comments-visibility-toggle'
|
|
67
106
|
export { CanvasCommentsSidebar, type CanvasCommentsSidebarProps } from './canvas/comments-sidebar'
|
|
68
107
|
export { useComments, useCommentThreads, useThreadComments } from './canvas/hooks'
|
|
69
108
|
export { useCommentingEnabled } from './canvas/license'
|
|
70
|
-
export { DEFAULT_REGION_COMMENT_OPTIONS, type RegionCommentOptions } from './canvas/region-options'
|
|
71
109
|
export { richTextToPlaintext } from './canvas/rich-text'
|
|
72
110
|
export { DEFAULT_SIDEBAR_FILTERS, type SidebarFilters } from './canvas/sidebar-filters'
|
|
73
111
|
export {
|
|
74
112
|
commentsHidden,
|
|
75
|
-
|
|
113
|
+
commentsSidebarOpen,
|
|
114
|
+
getRevealThreadPending,
|
|
76
115
|
openThreadId,
|
|
77
|
-
|
|
116
|
+
revealThread,
|
|
78
117
|
sidebarFilters,
|
|
79
118
|
toggleCommentsHidden,
|
|
119
|
+
toggleCommentsSidebar,
|
|
80
120
|
useCommentsHidden,
|
|
121
|
+
useCommentsSidebarOpen,
|
|
81
122
|
useOpenThreadId,
|
|
82
|
-
|
|
123
|
+
useRevealThreadPending,
|
|
83
124
|
useSidebarFilters,
|
|
84
125
|
} from './canvas/state'
|
|
85
|
-
export {
|
|
86
|
-
anchorPagePoint,
|
|
87
|
-
DEFAULT_IMPRECISE_SHAPE_ANCHOR,
|
|
88
|
-
focusThread,
|
|
89
|
-
shapeAnchorAt,
|
|
90
|
-
} from './canvas/thread-state'
|
|
126
|
+
export { anchorPagePoint, focusThread, shapeAnchorAt } from './canvas/thread-state'
|
|
91
127
|
|
|
92
128
|
registerTldrawLibraryVersion(
|
|
93
129
|
(globalThis as any).TLDRAW_LIBRARY_NAME,
|
package/src/ui/byline.tsx
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { Avatar, type CommentAuthor } from '@tldraw/mentions'
|
|
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. */
|
|
@@ -13,7 +14,8 @@ export interface BylineProps {
|
|
|
13
14
|
export function Byline({ author, date, edited }: BylineProps) {
|
|
14
15
|
return (
|
|
15
16
|
<div className="tlui-cmt-head">
|
|
16
|
-
<
|
|
17
|
+
<Avatar author={author} />
|
|
18
|
+
<span className="tlui-cmt-author">{author.name}</span>
|
|
17
19
|
<span className="tlui-cmt-time">
|
|
18
20
|
{formatRelativeTime(date)}
|
|
19
21
|
{edited && <span className="tlui-cmt-edited"> · edited</span>}
|
package/src/ui/comment-card.tsx
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
+
import { type CommentAuthor } from '@tldraw/mentions'
|
|
1
2
|
import { ReactNode } from 'react'
|
|
2
|
-
import { Avatar } from './avatar'
|
|
3
3
|
import { Byline } from './byline'
|
|
4
4
|
|
|
5
5
|
/** @public */
|
|
6
6
|
export interface CommentCardProps {
|
|
7
|
-
author:
|
|
8
|
-
/** The rendered comment body. The card doesn't dictate a format — pass a `<
|
|
9
|
-
* for
|
|
7
|
+
author: CommentAuthor
|
|
8
|
+
/** The rendered comment body. The card doesn't dictate a format — pass a `<CommentBody>`
|
|
9
|
+
* for a comment's rich text, or any node of your own. */
|
|
10
10
|
body: ReactNode
|
|
11
11
|
/** ISO datetime; formatted to relative time by the component. */
|
|
12
12
|
date: string
|
|
@@ -15,16 +15,26 @@ export interface CommentCardProps {
|
|
|
15
15
|
edited?: boolean
|
|
16
16
|
/** Hover-revealed controls at the card's top-right (e.g. an edit affordance). */
|
|
17
17
|
actions?: ReactNode
|
|
18
|
+
/** Content under the body, aligned with it rather than the avatar (e.g. a `<Reactions>` row). */
|
|
19
|
+
footer?: ReactNode
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
/** A single comment: Avatar, Byline, and a body slot the consumer renders. @public @react */
|
|
21
|
-
export function CommentCard({
|
|
23
|
+
export function CommentCard({
|
|
24
|
+
author,
|
|
25
|
+
body,
|
|
26
|
+
date,
|
|
27
|
+
you,
|
|
28
|
+
edited,
|
|
29
|
+
actions,
|
|
30
|
+
footer,
|
|
31
|
+
}: CommentCardProps) {
|
|
22
32
|
return (
|
|
23
33
|
<div className={you ? 'tlui-cmt-card tlui-cmt-card--you' : 'tlui-cmt-card'}>
|
|
24
|
-
<
|
|
34
|
+
<Byline author={author} date={date} edited={edited} />
|
|
25
35
|
<div className="tlui-cmt-body">
|
|
26
|
-
<Byline author={author} date={date} edited={edited} />
|
|
27
36
|
{body}
|
|
37
|
+
{footer}
|
|
28
38
|
</div>
|
|
29
39
|
{actions !== undefined && <div className="tlui-cmt-card__actions">{actions}</div>}
|
|
30
40
|
</div>
|
|
@@ -1,17 +1,29 @@
|
|
|
1
1
|
import { JSONContent } from '@tiptap/core'
|
|
2
2
|
import { EditorContent, useEditor } from '@tiptap/react'
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
Avatar,
|
|
5
|
+
type CommentAuthor,
|
|
6
|
+
createMentionExtension,
|
|
7
|
+
createMentionSuggestion,
|
|
8
|
+
isMentionPickerOpen,
|
|
9
|
+
MentionMember,
|
|
10
|
+
} from '@tldraw/mentions'
|
|
11
|
+
import {
|
|
12
|
+
type MouseEvent as ReactMouseEvent,
|
|
13
|
+
ReactNode,
|
|
14
|
+
useEffect,
|
|
15
|
+
useLayoutEffect,
|
|
16
|
+
useMemo,
|
|
17
|
+
useRef,
|
|
18
|
+
useState,
|
|
19
|
+
} from 'react'
|
|
4
20
|
import { isEqual, TLRichText, useMaybeEditor } from 'tldraw'
|
|
5
|
-
import { Avatar } from './avatar'
|
|
6
21
|
import { commentTipTapExtensions, EMPTY_COMMENT, isCommentEmpty } from './comment-extensions'
|
|
7
|
-
import { commentMention } from './comment-mention'
|
|
8
|
-
import { MentionMember } from './mention-list'
|
|
9
|
-
import { createMentionSuggestion, isMentionPickerOpen } from './mention-suggestion'
|
|
10
22
|
import { SendButton } from './send-button'
|
|
11
23
|
|
|
12
24
|
/** @public */
|
|
13
25
|
export interface CommentComposerProps {
|
|
14
|
-
author:
|
|
26
|
+
author: CommentAuthor
|
|
15
27
|
placeholder: string
|
|
16
28
|
/** Controlled rich-text value. Omit for the presentational (display-only) composer. */
|
|
17
29
|
value?: TLRichText
|
|
@@ -19,6 +31,9 @@ export interface CommentComposerProps {
|
|
|
19
31
|
/** Called on Send click or Enter. When set, the composer is interactive. */
|
|
20
32
|
onSubmit?(): void
|
|
21
33
|
sendLabel?: string
|
|
34
|
+
/** Called when Up is pressed in an empty composer — e.g. to start editing the comment above,
|
|
35
|
+
* the way chat apps edit your last message. With content in the field, Up moves the cursor. */
|
|
36
|
+
onArrowUpWhenEmpty?(): void
|
|
22
37
|
disabled?: boolean
|
|
23
38
|
autoFocus?: boolean
|
|
24
39
|
/** The leading element before the field. Defaults to the author's avatar. */
|
|
@@ -43,6 +58,7 @@ export function CommentComposer({
|
|
|
43
58
|
onChange,
|
|
44
59
|
onSubmit,
|
|
45
60
|
sendLabel = 'Send',
|
|
61
|
+
onArrowUpWhenEmpty,
|
|
46
62
|
disabled,
|
|
47
63
|
autoFocus,
|
|
48
64
|
leading,
|
|
@@ -60,6 +76,8 @@ export function CommentComposer({
|
|
|
60
76
|
onChangeRef.current = onChange
|
|
61
77
|
const onSubmitRef = useRef(onSubmit)
|
|
62
78
|
onSubmitRef.current = onSubmit
|
|
79
|
+
const onArrowUpWhenEmptyRef = useRef(onArrowUpWhenEmpty)
|
|
80
|
+
onArrowUpWhenEmptyRef.current = onArrowUpWhenEmpty
|
|
63
81
|
const disabledRef = useRef(disabled)
|
|
64
82
|
disabledRef.current = disabled
|
|
65
83
|
const getMentionSuggestionsRef = useRef(getMentionSuggestions)
|
|
@@ -68,6 +86,60 @@ export function CommentComposer({
|
|
|
68
86
|
renderMentionSuggestionRef.current = renderMentionSuggestion
|
|
69
87
|
|
|
70
88
|
const [isEmpty, setIsEmpty] = useState(() => !value || isCommentEmpty(value))
|
|
89
|
+
// Whether the field has grown to two rows: input across the full width, send button below.
|
|
90
|
+
// Flips when the (single-line) content width reaches the space left beside the send button.
|
|
91
|
+
const [expanded, setExpanded] = useState(false)
|
|
92
|
+
const expandedRef = useRef(expanded)
|
|
93
|
+
expandedRef.current = expanded
|
|
94
|
+
const inputWrapRef = useRef<HTMLDivElement>(null)
|
|
95
|
+
const mirrorRef = useRef<HTMLDivElement>(null)
|
|
96
|
+
|
|
97
|
+
// Measure whether the content still fits on one line beside the send button. The mirror is a
|
|
98
|
+
// hidden, nowrap clone of the editor's rendered content, so marks and mention chips measure at
|
|
99
|
+
// their true width. A small dead zone between the expand and collapse thresholds keeps the
|
|
100
|
+
// layout from flapping while typing at the boundary.
|
|
101
|
+
const remeasure = () => {
|
|
102
|
+
const wrap = inputWrapRef.current
|
|
103
|
+
const mirror = mirrorRef.current
|
|
104
|
+
const editor = editorRef.current
|
|
105
|
+
if (!wrap || !mirror || !editor) return
|
|
106
|
+
// Empty always fits — and measuring before TipTap's DOM has laid out reads zero widths,
|
|
107
|
+
// which would flash the expanded layout for a frame on mount.
|
|
108
|
+
if (editor.isEmpty) {
|
|
109
|
+
if (expandedRef.current) setExpanded(false)
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
if (wrap.clientWidth === 0) return
|
|
113
|
+
const doc = editor.state.doc
|
|
114
|
+
const multiBlock =
|
|
115
|
+
doc.childCount > 1 || (doc.firstChild !== null && doc.firstChild.type.name !== 'paragraph')
|
|
116
|
+
if (multiBlock) {
|
|
117
|
+
if (!expandedRef.current) setExpanded(true)
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
const field = wrap.parentElement
|
|
121
|
+
const send = field ? field.querySelector<HTMLElement>('.tlui-cmt-send') : null
|
|
122
|
+
const input = wrap.querySelector('.tlui-cmt-input')
|
|
123
|
+
if (!send || !input) return
|
|
124
|
+
mirror.innerHTML = input.innerHTML
|
|
125
|
+
const textWidth = mirror.offsetWidth
|
|
126
|
+
// The single-line space the input has beside the send button: the wrap's *content* box
|
|
127
|
+
// (clientWidth minus its own horizontal padding — the text wraps there, not at clientWidth),
|
|
128
|
+
// less the button plus the field's 6px gap when already expanded (the wrap then spans the
|
|
129
|
+
// full field). Without subtracting the padding, a padded wrap wraps to a second line ~padding
|
|
130
|
+
// px before expansion fires, so the input grows a line and then snaps to the expanded layout.
|
|
131
|
+
const wrapStyle = getComputedStyle(wrap)
|
|
132
|
+
const wrapPadX = parseFloat(wrapStyle.paddingLeft) + parseFloat(wrapStyle.paddingRight)
|
|
133
|
+
const collapsedAvailable =
|
|
134
|
+
wrap.clientWidth - wrapPadX - (expandedRef.current ? send.offsetWidth + 6 : 0)
|
|
135
|
+
if (!expandedRef.current && textWidth > collapsedAvailable - 8) {
|
|
136
|
+
setExpanded(true)
|
|
137
|
+
} else if (expandedRef.current && textWidth < collapsedAvailable - 24) {
|
|
138
|
+
setExpanded(false)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const remeasureRef = useRef(remeasure)
|
|
142
|
+
remeasureRef.current = remeasure
|
|
71
143
|
|
|
72
144
|
// The editor instance, reachable from `handleKeyDown` (which is created before `useEditor`
|
|
73
145
|
// returns). Updated on every render so the ref never points at a stale editor.
|
|
@@ -103,7 +175,7 @@ export function CommentComposer({
|
|
|
103
175
|
? (member: MentionMember) => renderMentionSuggestionRef.current?.(member)
|
|
104
176
|
: undefined
|
|
105
177
|
list.push(
|
|
106
|
-
|
|
178
|
+
createMentionExtension({
|
|
107
179
|
suggestion: createMentionSuggestion(resolveSuggestions, {
|
|
108
180
|
renderMember: renderRow,
|
|
109
181
|
editor: tlEditor,
|
|
@@ -111,7 +183,7 @@ export function CommentComposer({
|
|
|
111
183
|
})
|
|
112
184
|
)
|
|
113
185
|
} else {
|
|
114
|
-
list.push(
|
|
186
|
+
list.push(createMentionExtension({ suggestion: { char: '@', allow: () => false } }))
|
|
115
187
|
}
|
|
116
188
|
return list
|
|
117
189
|
}, [mentionsEnabled, hasCustomRow, tlEditor])
|
|
@@ -127,12 +199,30 @@ export function CommentComposer({
|
|
|
127
199
|
enableCoreExtensions: { textDirection: false },
|
|
128
200
|
textDirection: 'auto',
|
|
129
201
|
editorProps: {
|
|
130
|
-
attributes: {
|
|
202
|
+
attributes: {
|
|
203
|
+
class: 'tlui-cmt-input',
|
|
204
|
+
'aria-label': placeholder,
|
|
205
|
+
role: 'textbox',
|
|
206
|
+
'aria-multiline': 'true',
|
|
207
|
+
},
|
|
131
208
|
// Runs before every keymap plugin, so it can distinguish Shift+Enter from Enter — an
|
|
132
209
|
// `Enter` keymap binding also fires on Shift+Enter and would otherwise swallow it.
|
|
133
210
|
handleKeyDown: (_view, event) => {
|
|
134
211
|
// Let the keymaps handle the synthetic Enter we replay for a Shift+Enter newline.
|
|
135
212
|
if (replayingEnter.current) return false
|
|
213
|
+
// Up in an empty composer hands off to the host (edit the comment above). With
|
|
214
|
+
// content, Up stays cursor movement; with the mention picker open, it navigates it.
|
|
215
|
+
if (event.key === 'ArrowUp' && !event.isComposing) {
|
|
216
|
+
if (
|
|
217
|
+
onArrowUpWhenEmptyRef.current &&
|
|
218
|
+
editorRef.current?.isEmpty &&
|
|
219
|
+
!isMentionPickerOpen()
|
|
220
|
+
) {
|
|
221
|
+
onArrowUpWhenEmptyRef.current()
|
|
222
|
+
return true
|
|
223
|
+
}
|
|
224
|
+
return false
|
|
225
|
+
}
|
|
136
226
|
if (event.key !== 'Enter' || event.isComposing) return false
|
|
137
227
|
// While the @-mention picker is open, Enter selects the highlighted member — defer.
|
|
138
228
|
if (isMentionPickerOpen()) return false
|
|
@@ -140,6 +230,9 @@ export function CommentComposer({
|
|
|
140
230
|
// we don't re-enter and submit) to reuse the editor's list-aware Enter handling — a new
|
|
141
231
|
// list item in a list, a new paragraph otherwise. tldraw doesn't do soft breaks.
|
|
142
232
|
if (event.shiftKey && !event.metaKey && !event.ctrlKey && !event.altKey) {
|
|
233
|
+
// An empty field has nothing to break onto — swallow the keypress so it doesn't
|
|
234
|
+
// open the comment with a stray leading blank line.
|
|
235
|
+
if (editorRef.current?.isEmpty) return true
|
|
143
236
|
replayingEnter.current = true
|
|
144
237
|
try {
|
|
145
238
|
editorRef.current?.commands.enter()
|
|
@@ -155,10 +248,13 @@ export function CommentComposer({
|
|
|
155
248
|
},
|
|
156
249
|
onUpdate: ({ editor }) => {
|
|
157
250
|
setIsEmpty(editor.isEmpty)
|
|
251
|
+
// Same-value state sets don't re-render, so measure here — the editor's DOM is
|
|
252
|
+
// already updated when onUpdate fires.
|
|
253
|
+
remeasureRef.current()
|
|
158
254
|
onChangeRef.current?.(editor.getJSON() as TLRichText)
|
|
159
255
|
},
|
|
160
256
|
},
|
|
161
|
-
[interactive]
|
|
257
|
+
[interactive, placeholder]
|
|
162
258
|
)
|
|
163
259
|
editorRef.current = editor
|
|
164
260
|
|
|
@@ -171,6 +267,12 @@ export function CommentComposer({
|
|
|
171
267
|
setIsEmpty(editor.isEmpty)
|
|
172
268
|
}, [editor, value])
|
|
173
269
|
|
|
270
|
+
// Measure once the editor's content is in the DOM, so a composer that mounts pre-filled (the
|
|
271
|
+
// edit-in-place composer) starts in the right layout.
|
|
272
|
+
useLayoutEffect(() => {
|
|
273
|
+
remeasureRef.current()
|
|
274
|
+
}, [editor, value])
|
|
275
|
+
|
|
174
276
|
// Focus on the next frame rather than via TipTap's autofocus: the composer often mounts from a
|
|
175
277
|
// canvas pointer event whose default focus handling would otherwise steal it back.
|
|
176
278
|
useEffect(() => {
|
|
@@ -179,17 +281,62 @@ export function CommentComposer({
|
|
|
179
281
|
return () => cancelAnimationFrame(raf)
|
|
180
282
|
}, [autoFocus, editor])
|
|
181
283
|
|
|
284
|
+
// tldraw's pass-through-wheel hook (on the floating composer's wrapper) forwards a wheel to the
|
|
285
|
+
// canvas unless ITS element is scrollable — but the scrollable element is this input, not that
|
|
286
|
+
// wrapper, so an overflowing input never scrolled: the wheel panned the canvas instead. Catch the
|
|
287
|
+
// wheel on the wrap first (a native listener here fires before the wrapper's, deeper in the tree)
|
|
288
|
+
// and stop it while the input can still scroll that way; at the scroll boundary let it through so
|
|
289
|
+
// the canvas still zooms.
|
|
290
|
+
useEffect(() => {
|
|
291
|
+
const wrap = inputWrapRef.current
|
|
292
|
+
if (!wrap) return
|
|
293
|
+
const onWheel = (e: WheelEvent) => {
|
|
294
|
+
const input = wrap.querySelector<HTMLElement>('.tlui-cmt-input')
|
|
295
|
+
if (!input || input.scrollHeight <= input.clientHeight) return
|
|
296
|
+
const atTop = input.scrollTop <= 0
|
|
297
|
+
const atBottom = input.scrollTop >= input.scrollHeight - input.clientHeight - 1
|
|
298
|
+
if ((e.deltaY < 0 && !atTop) || (e.deltaY > 0 && !atBottom)) {
|
|
299
|
+
e.stopPropagation()
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
wrap.addEventListener('wheel', onWheel)
|
|
303
|
+
return () => wrap.removeEventListener('wheel', onWheel)
|
|
304
|
+
}, [])
|
|
305
|
+
|
|
306
|
+
// The whole field behaves like the text input: clicking its empty area (the padding, or the
|
|
307
|
+
// space beside/below a short line) focuses the editor rather than only the text glyphs being
|
|
308
|
+
// clickable. The input (caret placement) and the send button keep their own click handling.
|
|
309
|
+
const focusEditorFromField = (e: ReactMouseEvent<HTMLDivElement>) => {
|
|
310
|
+
const target = e.target as HTMLElement
|
|
311
|
+
if (target.closest('.tlui-cmt-input') || target.closest('.tlui-cmt-send')) return
|
|
312
|
+
e.preventDefault()
|
|
313
|
+
editor?.commands.focus('end')
|
|
314
|
+
}
|
|
315
|
+
|
|
182
316
|
return (
|
|
183
317
|
<div className="tlui-cmt-composer">
|
|
184
|
-
{leading ?? <Avatar
|
|
185
|
-
<div
|
|
186
|
-
|
|
318
|
+
{leading ?? <Avatar author={author} />}
|
|
319
|
+
<div
|
|
320
|
+
className={[
|
|
321
|
+
'tlui-cmt-composer__field',
|
|
322
|
+
interactive && expanded && 'tlui-cmt-composer__field--expanded',
|
|
323
|
+
]
|
|
324
|
+
.filter(Boolean)
|
|
325
|
+
.join(' ')}
|
|
326
|
+
onMouseDown={interactive ? focusEditorFromField : undefined}
|
|
327
|
+
>
|
|
328
|
+
<div className="tlui-cmt-composer__input-wrap" ref={inputWrapRef}>
|
|
187
329
|
<EditorContent editor={editor} />
|
|
188
330
|
{isEmpty && (
|
|
189
331
|
<div className="tlui-cmt-input__placeholder" aria-hidden="true">
|
|
190
332
|
{placeholder}
|
|
191
333
|
</div>
|
|
192
334
|
)}
|
|
335
|
+
<div
|
|
336
|
+
className="tlui-cmt-composer__mirror tlui-cmt-input"
|
|
337
|
+
ref={mirrorRef}
|
|
338
|
+
aria-hidden="true"
|
|
339
|
+
/>
|
|
193
340
|
</div>
|
|
194
341
|
{interactive && <SendButton label={sendLabel} onClick={onSubmit} disabled={disabled} />}
|
|
195
342
|
</div>
|
package/src/ui/comment-pin.tsx
CHANGED
|
@@ -10,16 +10,36 @@ export interface CommentPinProps {
|
|
|
10
10
|
open?: boolean
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/* An inline check, not a text glyph — the '✓' character sits off-baseline and varies by font. */
|
|
14
|
+
const resolvedCheck = (
|
|
15
|
+
<svg
|
|
16
|
+
viewBox="0 0 24 24"
|
|
17
|
+
width="15"
|
|
18
|
+
height="15"
|
|
19
|
+
fill="none"
|
|
20
|
+
stroke="currentColor"
|
|
21
|
+
strokeWidth="2.5"
|
|
22
|
+
strokeLinecap="round"
|
|
23
|
+
strokeLinejoin="round"
|
|
24
|
+
aria-hidden="true"
|
|
25
|
+
>
|
|
26
|
+
<path d="M4 12.5l5 5L20 6.5" />
|
|
27
|
+
</svg>
|
|
28
|
+
)
|
|
29
|
+
|
|
13
30
|
/** A canvas comment marker: shows its `children` (or a check when resolved). Purely
|
|
14
31
|
* presentational — it reflects open/resolved state via CSS; wrap it to make it clickable.
|
|
15
32
|
* @public @react */
|
|
16
33
|
export function CommentPin({ children, resolved, open }: CommentPinProps) {
|
|
17
34
|
const className = [
|
|
35
|
+
// `tlui-cmt-marker` carries the resting shadow and the hover lift — the same treatment a
|
|
36
|
+
// count badge wears, so the two markers behave alike.
|
|
37
|
+
'tlui-cmt-marker',
|
|
18
38
|
'tlui-cmt-pin',
|
|
19
39
|
resolved && 'tlui-cmt-pin--resolved',
|
|
20
|
-
open && 'tlui-cmt-
|
|
40
|
+
open && 'tlui-cmt-marker--open',
|
|
21
41
|
]
|
|
22
42
|
.filter(Boolean)
|
|
23
43
|
.join(' ')
|
|
24
|
-
return <div className={className}>{resolved ?
|
|
44
|
+
return <div className={className}>{resolved ? resolvedCheck : children}</div>
|
|
25
45
|
}
|