@tldraw/commenting 5.3.0-next.7654e7ac2a02 → 5.3.0-next.aefb8773a7eb
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 +34 -28
- package/dist-cjs/canvas/comment-tool.js +12 -6
- package/dist-cjs/canvas/comment-tool.js.map +2 -2
- package/dist-cjs/canvas/comments-filter-menu.js +1 -1
- package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
- package/dist-cjs/canvas/comments-overlay.js +4 -1
- package/dist-cjs/canvas/comments-overlay.js.map +2 -2
- package/dist-cjs/canvas/pending-composer.js +1 -0
- package/dist-cjs/canvas/pending-composer.js.map +2 -2
- package/dist-cjs/canvas/thread-pin.js +1 -1
- package/dist-cjs/canvas/thread-pin.js.map +2 -2
- package/dist-cjs/index.d.ts +4 -2
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/ui/format-time.js +1 -1
- package/dist-cjs/ui/format-time.js.map +2 -2
- package/dist-cjs/ui/icons.js +52 -29
- package/dist-cjs/ui/icons.js.map +2 -2
- package/dist-esm/canvas/comment-tool.mjs +12 -6
- package/dist-esm/canvas/comment-tool.mjs.map +2 -2
- package/dist-esm/canvas/comments-filter-menu.mjs +2 -2
- package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
- package/dist-esm/canvas/comments-overlay.mjs +4 -1
- package/dist-esm/canvas/comments-overlay.mjs.map +2 -2
- package/dist-esm/canvas/pending-composer.mjs +1 -0
- package/dist-esm/canvas/pending-composer.mjs.map +2 -2
- package/dist-esm/canvas/thread-pin.mjs +1 -1
- package/dist-esm/canvas/thread-pin.mjs.map +2 -2
- package/dist-esm/index.d.mts +4 -2
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/ui/format-time.mjs +1 -1
- package/dist-esm/ui/format-time.mjs.map +2 -2
- package/dist-esm/ui/icons.mjs +53 -30
- package/dist-esm/ui/icons.mjs.map +2 -2
- package/package.json +7 -6
- package/src/canvas/comment-tool.test.ts +109 -4
- package/src/canvas/comment-tool.tsx +24 -8
- package/src/canvas/comments-filter-menu.tsx +2 -2
- package/src/canvas/comments-overlay.tsx +7 -2
- package/src/canvas/pending-composer.tsx +4 -0
- package/src/canvas/thread-pin.tsx +3 -2
- package/src/ui/comments.css +34 -28
- package/src/ui/format-time.test.ts +3 -3
- package/src/ui/format-time.ts +2 -2
- package/src/ui/icons.tsx +43 -21
package/commenting.css
CHANGED
|
@@ -198,6 +198,15 @@
|
|
|
198
198
|
overflow-wrap: anywhere;
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
/* A comment body is read-only but its text must stay selectable so it can be copied. tldraw's
|
|
202
|
+
container sets `user-select: none` on every descendant (`.tl-container *`) to keep canvas gestures
|
|
203
|
+
from selecting UI text, so the body and its rendered children have to opt back in. */
|
|
204
|
+
.tlui-cmt-text,
|
|
205
|
+
.tlui-cmt-text * {
|
|
206
|
+
-webkit-user-select: text;
|
|
207
|
+
user-select: text;
|
|
208
|
+
}
|
|
209
|
+
|
|
201
210
|
.tlui-cmt-edited {
|
|
202
211
|
font-style: italic;
|
|
203
212
|
}
|
|
@@ -891,7 +900,7 @@ button.tlui-cmt-reaction--active:hover {
|
|
|
891
900
|
align-items: center;
|
|
892
901
|
justify-content: space-between;
|
|
893
902
|
gap: 8px;
|
|
894
|
-
padding:
|
|
903
|
+
padding: 2px 2px 2px 14px;
|
|
895
904
|
border-bottom: 1px solid var(--tl-color-divider);
|
|
896
905
|
}
|
|
897
906
|
.tlui-cmt-list__header-title {
|
|
@@ -900,41 +909,28 @@ button.tlui-cmt-reaction--active:hover {
|
|
|
900
909
|
color: var(--tl-color-text-1);
|
|
901
910
|
}
|
|
902
911
|
|
|
903
|
-
/*
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
stylesheet order can't decide the winner. */
|
|
912
|
+
/* An icon button in the sidebar header (filter funnel, overflow …). Sized like the thread actions:
|
|
913
|
+
a TldrawUiButton (type="icon") left at its native 40px box, with .tlui-button's 32px ::after
|
|
914
|
+
square carrying the hover wash. Only the bits that differ are set here, at .tlui-button's
|
|
915
|
+
specificity + 1 so stylesheet order can't decide the winner. */
|
|
907
916
|
.tlui-button.tlui-cmt-header-btn {
|
|
908
|
-
width:
|
|
909
|
-
min-width: 26px;
|
|
910
|
-
height: 26px;
|
|
917
|
+
width: 40px;
|
|
911
918
|
padding: 0;
|
|
912
919
|
border-radius: 6px;
|
|
913
920
|
color: var(--tl-color-text-2);
|
|
914
921
|
}
|
|
915
|
-
/*
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
.tlui-button.tlui-cmt-header-btn::after {
|
|
920
|
-
inset: -6px -1px;
|
|
921
|
-
background: none;
|
|
922
|
-
}
|
|
923
|
-
.tlui-button.tlui-cmt-header-btn:first-child::after {
|
|
924
|
-
left: -6px;
|
|
925
|
-
}
|
|
926
|
-
.tlui-button.tlui-cmt-header-btn:last-child::after {
|
|
927
|
-
right: -6px;
|
|
922
|
+
/* Overlap each button 4px into the previous so the 32px visual squares sit 4px apart, not 8px —
|
|
923
|
+
the same tiling the thread actions use. */
|
|
924
|
+
.tlui-cmt-header-btn + .tlui-cmt-header-btn {
|
|
925
|
+
margin-left: -4px;
|
|
928
926
|
}
|
|
929
927
|
.tlui-button.tlui-cmt-header-btn:hover,
|
|
930
928
|
.tlui-button.tlui-cmt-header-btn[data-state='open'] {
|
|
931
|
-
background: var(--tl-color-muted-2);
|
|
932
929
|
color: var(--tl-color-text-1);
|
|
933
930
|
}
|
|
934
931
|
.tlui-cmt-list__header-actions {
|
|
935
932
|
display: flex;
|
|
936
933
|
align-items: center;
|
|
937
|
-
gap: 2px;
|
|
938
934
|
}
|
|
939
935
|
|
|
940
936
|
/* The commenting dropdowns' surface — a little air between the items and the menu edge. */
|
|
@@ -990,6 +986,7 @@ button.tlui-cmt-reaction--active:hover {
|
|
|
990
986
|
font-size: 12px;
|
|
991
987
|
}
|
|
992
988
|
.tlui-cmt-list__item {
|
|
989
|
+
position: relative;
|
|
993
990
|
display: flex;
|
|
994
991
|
gap: 10px;
|
|
995
992
|
width: 100%;
|
|
@@ -1004,18 +1001,27 @@ button.tlui-cmt-reaction--active:hover {
|
|
|
1004
1001
|
color: inherit;
|
|
1005
1002
|
text-decoration: none;
|
|
1006
1003
|
}
|
|
1007
|
-
|
|
1004
|
+
/* The highlight is an inset, rounded wash like tldraw's buttons and menu items — not an
|
|
1005
|
+
edge-to-edge band — so a comment row lights up the same way every other interactive row does. */
|
|
1006
|
+
.tlui-cmt-list__item::after {
|
|
1007
|
+
content: '';
|
|
1008
|
+
position: absolute;
|
|
1009
|
+
inset: 4px;
|
|
1010
|
+
border-radius: var(--tl-radius-2);
|
|
1011
|
+
pointer-events: none;
|
|
1012
|
+
}
|
|
1013
|
+
.tlui-cmt-list__item:hover::after {
|
|
1008
1014
|
background: var(--tl-color-muted-2);
|
|
1009
1015
|
}
|
|
1010
1016
|
/* Selected sits a step above hover so the open thread's row stays distinguishable while
|
|
1011
1017
|
pointing at other rows — but only a step: the muted-1 token (10%) reads too heavy here.
|
|
1012
1018
|
These rows sit on the opaque sidebar panel, so a translucent tint composes safely. */
|
|
1013
|
-
.tlui-cmt-list__item--selected,
|
|
1014
|
-
.tlui-cmt-list__item--selected:hover {
|
|
1019
|
+
.tlui-cmt-list__item--selected::after,
|
|
1020
|
+
.tlui-cmt-list__item--selected:hover::after {
|
|
1015
1021
|
background: hsl(0, 0%, 0%, 5%);
|
|
1016
1022
|
}
|
|
1017
|
-
.tl-theme__dark .tlui-cmt-list__item--selected,
|
|
1018
|
-
.tl-theme__dark .tlui-cmt-list__item--selected:hover {
|
|
1023
|
+
.tl-theme__dark .tlui-cmt-list__item--selected::after,
|
|
1024
|
+
.tl-theme__dark .tlui-cmt-list__item--selected:hover::after {
|
|
1019
1025
|
background: hsl(0, 0%, 100%, 5%);
|
|
1020
1026
|
}
|
|
1021
1027
|
/* resolved rows mute their content but keep the "Resolved" marker legible */
|
|
@@ -76,6 +76,8 @@ class CommentTool extends import_tldraw.StateNode {
|
|
|
76
76
|
}
|
|
77
77
|
onExit() {
|
|
78
78
|
this.editor.setHintingShapes([]);
|
|
79
|
+
import_state.pendingComment.set(this.editor, null);
|
|
80
|
+
import_state.regionDraft.set(this.editor, null);
|
|
79
81
|
}
|
|
80
82
|
// Escape leaves the tool, like the built-in tools (the editor dispatches `cancel` on Escape).
|
|
81
83
|
onCancel() {
|
|
@@ -90,7 +92,11 @@ class CommentIdle extends import_tldraw.StateNode {
|
|
|
90
92
|
static id = "idle";
|
|
91
93
|
onEnter() {
|
|
92
94
|
this.editor.setCursor({ type: "comment", rotation: 0 });
|
|
93
|
-
|
|
95
|
+
if (import_state.pendingComment.get(this.editor)?.anchor.type === "region") {
|
|
96
|
+
this.editor.setHintingShapes([]);
|
|
97
|
+
} else {
|
|
98
|
+
updateAnchorHint(this.editor);
|
|
99
|
+
}
|
|
94
100
|
}
|
|
95
101
|
onPointerMove() {
|
|
96
102
|
updateAnchorHint(this.editor);
|
|
@@ -136,7 +142,7 @@ class CommentPointing extends import_tldraw.StateNode {
|
|
|
136
142
|
})
|
|
137
143
|
) : { type: "point", x: point.x, y: point.y };
|
|
138
144
|
import_state.pendingComment.set(editor, { anchor, point: { x: point.x, y: point.y } });
|
|
139
|
-
|
|
145
|
+
this.parent.transition("idle");
|
|
140
146
|
}
|
|
141
147
|
onCancel() {
|
|
142
148
|
this.cancel();
|
|
@@ -144,9 +150,9 @@ class CommentPointing extends import_tldraw.StateNode {
|
|
|
144
150
|
onInterrupt() {
|
|
145
151
|
this.cancel();
|
|
146
152
|
}
|
|
147
|
-
// Abandon the follow composer if placement is interrupted (Escape, focus loss, etc.).
|
|
153
|
+
// Abandon the follow composer if placement is interrupted (Escape, focus loss, etc.). The
|
|
154
|
+
// tool's onExit drops the draft.
|
|
148
155
|
cancel() {
|
|
149
|
-
import_state.pendingComment.set(this.editor, null);
|
|
150
156
|
this.editor.setCurrentTool("select");
|
|
151
157
|
}
|
|
152
158
|
}
|
|
@@ -173,7 +179,7 @@ class CommentDragging extends import_tldraw.StateNode {
|
|
|
173
179
|
anchor: { type: "region", ...region, pinX: pin.x, pinY: pin.y },
|
|
174
180
|
point: (0, import_thread_state.regionPinPoint)(region, pin)
|
|
175
181
|
});
|
|
176
|
-
|
|
182
|
+
this.parent.transition("idle");
|
|
177
183
|
}
|
|
178
184
|
onCancel() {
|
|
179
185
|
this.cancel();
|
|
@@ -188,8 +194,8 @@ class CommentDragging extends import_tldraw.StateNode {
|
|
|
188
194
|
regionBetween(editor.inputs.getOriginPagePoint(), editor.inputs.getCurrentPagePoint())
|
|
189
195
|
);
|
|
190
196
|
}
|
|
197
|
+
// The tool's onExit drops the region draft.
|
|
191
198
|
cancel() {
|
|
192
|
-
import_state.regionDraft.set(this.editor, null);
|
|
193
199
|
this.editor.setCurrentTool("select");
|
|
194
200
|
}
|
|
195
201
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/canvas/comment-tool.tsx"],
|
|
4
|
-
"sourcesContent": ["import {\n\tBoxModel,\n\tEditor,\n\tStateNode,\n\tTLCommentAnchor,\n\tTLStateNodeConstructor,\n\tTLUiOverrides,\n\tVecLike,\n} from 'tldraw'\nimport { type CommentingOptions, defaultCommentingOptions, getCommentingOptions } from './options'\nimport { commentsSidebarOpen, pendingComment, regionDraft } from './state'\nimport { commentTargetShapeAt, regionPinPoint, shapeAnchorAt } from './thread-state'\n\n/** A comment being placed but not yet posted: where its composer sits and what it will anchor\n * to. Shared between the tool (which sets it on click) and the overlay (which renders the\n * composer) through the internal `pendingComment` atom.\n * @internal */\nexport interface PendingComment {\n\tanchor: TLCommentAnchor\n\t/** Page point where the composer opens (the click location, or a region's pin corner). */\n\tpoint: VecLike\n}\n\n/**\n * Merge configure options over a base. Shallow for scalars; `components` is merged rather than\n * replaced so chained `configure` calls layer their slots \u2014 a later `{ components: { PinContent } }`\n * doesn't drop an earlier `{ components: { CommentBody } }`.\n */\nfunction mergeCommentingOptions(\n\tbase: CommentingOptions,\n\toverrides: Partial<CommentingOptions>\n): CommentingOptions {\n\treturn {\n\t\t...base,\n\t\t...overrides,\n\t\tcomponents: { ...base.components, ...overrides.components },\n\t}\n}\n\n/** The page-space rectangle spanned by two points, normalized so w/h are non-negative. Shared by\n * region creation (pointer-down \u2192 cursor) and corner resize (fixed corner \u2192 cursor). */\nexport function regionBetween(a: VecLike, b: VecLike): BoxModel {\n\treturn {\n\t\tx: Math.min(a.x, b.x),\n\t\ty: Math.min(a.y, b.y),\n\t\tw: Math.abs(a.x - b.x),\n\t\th: Math.abs(a.y - b.y),\n\t}\n}\n\n/**\n * The comment tool. Pressing down opens the comment composer at the pointer and it follows until\n * release \u2014 like placing a sticky note \u2014 settling on a point, or on a shape when released over one.\n * With region comments enabled, dragging past the threshold draws a region rectangle instead.\n * Placement only opens a composer; the records are created when the comment is posted.\n * @public\n */\nexport class CommentTool extends StateNode {\n\tstatic override id = 'comment'\n\tstatic override initial = 'idle'\n\tstatic override children(): TLStateNodeConstructor[] {\n\t\treturn [CommentIdle, CommentPointing, CommentDragging]\n\t}\n\n\t/**\n\t * Configure this tool's {@link CommentTool.options | `options`}, returning a configured subclass\n\t * to register via `tools`. Mirrors `ShapeUtil.configure`. Layers over any prior `configure`, so\n\t * calls can be chained.\n\t *\n\t * @example\n\t * ```tsx\n\t * <Tldraw tools={[CommentTool.configure({ history: 'ignore', enableClustering: false })]} />\n\t * ```\n\t */\n\tstatic configure<T extends TLStateNodeConstructor>(\n\t\tthis: T,\n\t\toptions: T extends new (...args: any[]) => { options: infer Options } ? Partial<Options> : never\n\t): T {\n\t\t// @ts-expect-error -- mirrors ShapeUtil.configure; extending `this` is sound at runtime.\n\t\treturn class extends this {\n\t\t\t// @ts-expect-error\n\t\t\toptions = mergeCommentingOptions(this.options, options)\n\t\t}\n\t}\n\n\t/**\n\t * The merged commenting options for this editor. Read from anywhere via\n\t * {@link getCommentingOptions}. Override with {@link CommentTool.configure}.\n\t */\n\toptions: CommentingOptions = defaultCommentingOptions\n\n\toverride onEnter() {\n\t\tthis.editor.setCursor({ type: 'comment', rotation: 0 })\n\t\t// Placing comments is canvas-focused \u2014 the thread list gets out of the way while the tool is\n\t\t// active. Reopened via its own control (a button), never by leaving the tool.\n\t\tcommentsSidebarOpen.set(this.editor, false)\n\t}\n\n\toverride onExit() {\n\t\t// Drop the hover hint painted while pointing at shapes (see CommentIdle). The cursor resets\n\t\t// when the next tool takes over.\n\t\tthis.editor.setHintingShapes([])\n\t}\n\n\t// Escape leaves the tool, like the built-in tools (the editor dispatches `cancel` on Escape).\n\toverride onCancel() {\n\t\tthis.editor.setCurrentTool('select')\n\t}\n}\n\n/** Hint the shape a comment placed at the pointer would anchor to, using the same hit-test as the\n * anchor resolution on release. Hinting shapes render an indicator ungated by the active tool, so\n * this shows the select-style outline while the comment tool \u2014 not select \u2014 is active. */\nfunction updateAnchorHint(editor: Editor) {\n\tconst hit = commentTargetShapeAt(editor, editor.inputs.getCurrentPagePoint())\n\teditor.setHintingShapes(hit ? [hit.id] : [])\n}\n\nclass CommentIdle extends StateNode {\n\tstatic override id = 'idle'\n\n\toverride onEnter() {\n\t\t// Back to hovering: restore the pin cursor (a prior placing state may have hidden it).\n\t\tthis.editor.setCursor({ type: 'comment', rotation: 0 })\n\t\tupdateAnchorHint(this.editor)\n\t}\n\n\toverride onPointerMove() {\n\t\tupdateAnchorHint(this.editor)\n\t}\n\n\toverride onPointerDown() {\n\t\tthis.parent.transition('pointing')\n\t}\n}\n\nclass CommentPointing extends StateNode {\n\tstatic override id = 'pointing'\n\n\toverride onEnter() {\n\t\t// Open the composer immediately at the press point so it's visible from pointer-down (not just\n\t\t// on release), and let it trail the pointer while dragging \u2014 like placing a sticky note. The\n\t\t// anchor is a bare point for now; it's resolved (shape or point) on pointer up.\n\t\tconst { editor } = this\n\t\t// Hide the cursor while placing: the draft composer is the pointer's stand-in now, so the pin\n\t\t// cursor sitting over it just reads as clutter.\n\t\teditor.setCursor({ type: 'none', rotation: 0 })\n\t\tconst point = editor.inputs.getCurrentPagePoint()\n\t\tpendingComment.set(editor, {\n\t\t\tanchor: { type: 'point', x: point.x, y: point.y },\n\t\t\tpoint: { x: point.x, y: point.y },\n\t\t})\n\t}\n\n\toverride onPointerMove() {\n\t\tconst { editor } = this\n\t\t// Once the pointer passes the drag threshold with region comments enabled, this is a region,\n\t\t// not a follow \u2014 hand off to the region drag (which clears this composer and draws the box).\n\t\tif (getCommentingOptions(editor).enableRegions && editor.inputs.getIsDragging()) {\n\t\t\tthis.parent.transition('dragging')\n\t\t\treturn\n\t\t}\n\t\t// Otherwise the composer trails the pointer \u2014 keep hinting the shape a release here would\n\t\t// anchor to, like the idle hover does.\n\t\tupdateAnchorHint(editor)\n\t\tconst point = editor.inputs.getCurrentPagePoint()\n\t\tpendingComment.update(editor, (p) => (p ? { ...p, point: { x: point.x, y: point.y } } : p))\n\t}\n\n\t// Settle where the pointer is released: anchor to the shape under it, or drop a point.\n\toverride onPointerUp() {\n\t\tconst { editor } = this\n\t\tconst point = editor.inputs.getCurrentPagePoint()\n\t\tconst hit = commentTargetShapeAt(editor, point)\n\t\tconst anchor: TLCommentAnchor = hit\n\t\t\t? shapeAnchorAt(\n\t\t\t\t\teditor,\n\t\t\t\t\thit.id,\n\t\t\t\t\tpoint,\n\t\t\t\t\tgetCommentingOptions(editor).shouldBePrecise(editor, {\n\t\t\t\t\t\tshapeId: hit.id,\n\t\t\t\t\t\tpoint,\n\t\t\t\t\t\taltKey: editor.inputs.getAltKey(),\n\t\t\t\t\t})\n\t\t\t\t)\n\t\t\t: { type: 'point', x: point.x, y: point.y }\n\t\tpendingComment.set(editor, { anchor, point: { x: point.x, y: point.y } })\n\t\teditor.setCurrentTool('select')\n\t}\n\n\toverride onCancel() {\n\t\tthis.cancel()\n\t}\n\n\toverride onInterrupt() {\n\t\tthis.cancel()\n\t}\n\n\t// Abandon the follow composer if placement is interrupted (Escape, focus loss, etc.).\n\tprivate cancel() {\n\t\tpendingComment.set(this.editor, null)\n\t\tthis.editor.setCurrentTool('select')\n\t}\n}\n\nclass CommentDragging extends StateNode {\n\tstatic override id = 'dragging'\n\n\toverride onEnter() {\n\t\t// A region drag supersedes the point-follow composer opened in `pointing`. Show a crosshair again,\n\t\t// since the composer no longer stands in for the pointer, and drop the placement hint \u2014 a region\n\t\t// anchors to its rectangle, so a single-shape outline would be stale.\n\t\tpendingComment.set(this.editor, null)\n\t\tthis.editor.setHintingShapes([])\n\t\tthis.editor.setCursor({ type: 'cross', rotation: 0 })\n\t\tthis.updateDraft()\n\t}\n\n\toverride onPointerMove() {\n\t\tthis.updateDraft()\n\t}\n\n\t// Commit the dragged rectangle as a region anchor; the composer opens at its pin corner.\n\toverride onPointerUp() {\n\t\tconst { editor } = this\n\t\tconst origin = editor.inputs.getOriginPagePoint()\n\t\tconst current = editor.inputs.getCurrentPagePoint()\n\t\tconst region = regionBetween(origin, current)\n\t\t// The pin lives on the corner the drag released on \u2014 drag up-left, pin top-left.\n\t\tconst pin = { x: current.x >= origin.x ? 1 : 0, y: current.y >= origin.y ? 1 : 0 }\n\t\tregionDraft.set(editor, null)\n\t\tpendingComment.set(editor, {\n\t\t\tanchor: { type: 'region', ...region, pinX: pin.x, pinY: pin.y },\n\t\t\tpoint: regionPinPoint(region, pin),\n\t\t})\n\t\teditor.setCurrentTool('select')\n\t}\n\n\toverride onCancel() {\n\t\tthis.cancel()\n\t}\n\n\toverride onInterrupt() {\n\t\tthis.cancel()\n\t}\n\n\tprivate updateDraft() {\n\t\tconst { editor } = this\n\t\tregionDraft.set(\n\t\t\teditor,\n\t\t\tregionBetween(editor.inputs.getOriginPagePoint(), editor.inputs.getCurrentPagePoint())\n\t\t)\n\t}\n\n\tprivate cancel() {\n\t\tregionDraft.set(this.editor, null)\n\t\tthis.editor.setCurrentTool('select')\n\t}\n}\n\n/** @public */\nexport const commentTools = [CommentTool]\n\n/** Registers the comment tool in the UI (icon, label, shortcut). Compose into your overrides.\n * Once registered, tldraw's `DefaultQuickActionsContent` shows the comment button.\n * @public */\nexport const commentToolOverrides: TLUiOverrides = {\n\ttools(editor, tools) {\n\t\ttools.comment = {\n\t\t\tid: 'comment',\n\t\t\ticon: 'comment',\n\t\t\tlabel: 'Comment',\n\t\t\tkbd: 'c',\n\t\t\tonSelect: () => editor.setCurrentTool('comment'),\n\t\t}\n\t\treturn tools\n\t},\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAQO;AACP,qBAAuF;AACvF,mBAAiE;AACjE,0BAAoE;AAiBpE,SAAS,uBACR,MACA,WACoB;AACpB,SAAO;AAAA,IACN,GAAG;AAAA,IACH,GAAG;AAAA,IACH,YAAY,EAAE,GAAG,KAAK,YAAY,GAAG,UAAU,WAAW;AAAA,EAC3D;AACD;AAIO,SAAS,cAAc,GAAY,GAAsB;AAC/D,SAAO;AAAA,IACN,GAAG,KAAK,IAAI,EAAE,GAAG,EAAE,CAAC;AAAA,IACpB,GAAG,KAAK,IAAI,EAAE,GAAG,EAAE,CAAC;AAAA,IACpB,GAAG,KAAK,IAAI,EAAE,IAAI,EAAE,CAAC;AAAA,IACrB,GAAG,KAAK,IAAI,EAAE,IAAI,EAAE,CAAC;AAAA,EACtB;AACD;
|
|
4
|
+
"sourcesContent": ["import {\n\tBoxModel,\n\tEditor,\n\tStateNode,\n\tTLCommentAnchor,\n\tTLStateNodeConstructor,\n\tTLUiOverrides,\n\tVecLike,\n} from 'tldraw'\nimport { type CommentingOptions, defaultCommentingOptions, getCommentingOptions } from './options'\nimport { commentsSidebarOpen, pendingComment, regionDraft } from './state'\nimport { commentTargetShapeAt, regionPinPoint, shapeAnchorAt } from './thread-state'\n\n/** A comment being placed but not yet posted: where its composer sits and what it will anchor\n * to. Shared between the tool (which sets it on click) and the overlay (which renders the\n * composer) through the internal `pendingComment` atom.\n * @internal */\nexport interface PendingComment {\n\tanchor: TLCommentAnchor\n\t/** Page point where the composer opens (the click location, or a region's pin corner). */\n\tpoint: VecLike\n}\n\n/**\n * Merge configure options over a base. Shallow for scalars; `components` is merged rather than\n * replaced so chained `configure` calls layer their slots \u2014 a later `{ components: { PinContent } }`\n * doesn't drop an earlier `{ components: { CommentBody } }`.\n */\nfunction mergeCommentingOptions(\n\tbase: CommentingOptions,\n\toverrides: Partial<CommentingOptions>\n): CommentingOptions {\n\treturn {\n\t\t...base,\n\t\t...overrides,\n\t\tcomponents: { ...base.components, ...overrides.components },\n\t}\n}\n\n/** The page-space rectangle spanned by two points, normalized so w/h are non-negative. Shared by\n * region creation (pointer-down \u2192 cursor) and corner resize (fixed corner \u2192 cursor). */\nexport function regionBetween(a: VecLike, b: VecLike): BoxModel {\n\treturn {\n\t\tx: Math.min(a.x, b.x),\n\t\ty: Math.min(a.y, b.y),\n\t\tw: Math.abs(a.x - b.x),\n\t\th: Math.abs(a.y - b.y),\n\t}\n}\n\n/**\n * The comment tool. Pressing down opens the comment composer at the pointer and it follows until\n * release \u2014 like placing a sticky note \u2014 settling on a point, or on a shape when released over one.\n * With region comments enabled, dragging past the threshold draws a region rectangle instead.\n * Placement only opens a composer; the records are created when the comment is posted. The tool\n * stays active while the composer is open \u2014 posting returns to select, and clicking elsewhere\n * re-places the composer.\n * @public\n */\nexport class CommentTool extends StateNode {\n\tstatic override id = 'comment'\n\tstatic override initial = 'idle'\n\tstatic override children(): TLStateNodeConstructor[] {\n\t\treturn [CommentIdle, CommentPointing, CommentDragging]\n\t}\n\n\t/**\n\t * Configure this tool's {@link CommentTool.options | `options`}, returning a configured subclass\n\t * to register via `tools`. Mirrors `ShapeUtil.configure`. Layers over any prior `configure`, so\n\t * calls can be chained.\n\t *\n\t * @example\n\t * ```tsx\n\t * <Tldraw tools={[CommentTool.configure({ history: 'ignore', enableClustering: false })]} />\n\t * ```\n\t */\n\tstatic configure<T extends TLStateNodeConstructor>(\n\t\tthis: T,\n\t\toptions: T extends new (...args: any[]) => { options: infer Options } ? Partial<Options> : never\n\t): T {\n\t\t// @ts-expect-error -- mirrors ShapeUtil.configure; extending `this` is sound at runtime.\n\t\treturn class extends this {\n\t\t\t// @ts-expect-error\n\t\t\toptions = mergeCommentingOptions(this.options, options)\n\t\t}\n\t}\n\n\t/**\n\t * The merged commenting options for this editor. Read from anywhere via\n\t * {@link getCommentingOptions}. Override with {@link CommentTool.configure}.\n\t */\n\toptions: CommentingOptions = defaultCommentingOptions\n\n\toverride onEnter() {\n\t\tthis.editor.setCursor({ type: 'comment', rotation: 0 })\n\t\t// Placing comments is canvas-focused \u2014 the thread list gets out of the way while the tool is\n\t\t// active. Reopened via its own control (a button), never by leaving the tool.\n\t\tcommentsSidebarOpen.set(this.editor, false)\n\t}\n\n\toverride onExit() {\n\t\t// Drop the hover hint painted while pointing at shapes (see CommentIdle). The cursor resets\n\t\t// when the next tool takes over. The draft composer and region draft belong to the tool, so\n\t\t// they leave with it; the draft's text survives in the comment draft store.\n\t\tthis.editor.setHintingShapes([])\n\t\tpendingComment.set(this.editor, null)\n\t\tregionDraft.set(this.editor, null)\n\t}\n\n\t// Escape leaves the tool, like the built-in tools (the editor dispatches `cancel` on Escape).\n\toverride onCancel() {\n\t\tthis.editor.setCurrentTool('select')\n\t}\n}\n\n/** Hint the shape a comment placed at the pointer would anchor to, using the same hit-test as the\n * anchor resolution on release. Hinting shapes render an indicator ungated by the active tool, so\n * this shows the select-style outline while the comment tool \u2014 not select \u2014 is active. */\nfunction updateAnchorHint(editor: Editor) {\n\tconst hit = commentTargetShapeAt(editor, editor.inputs.getCurrentPagePoint())\n\teditor.setHintingShapes(hit ? [hit.id] : [])\n}\n\nclass CommentIdle extends StateNode {\n\tstatic override id = 'idle'\n\n\toverride onEnter() {\n\t\t// Back to hovering: restore the pin cursor (a prior placing state may have hidden it).\n\t\tthis.editor.setCursor({ type: 'comment', rotation: 0 })\n\t\t// With a region composer open, a shape outline under its pin corner would imply a shape\n\t\t// anchor the region will never use \u2014 and it would stick, since moves over the composer\n\t\t// never reach the canvas. Point placements keep the hint: it previews the next click.\n\t\tif (pendingComment.get(this.editor)?.anchor.type === 'region') {\n\t\t\tthis.editor.setHintingShapes([])\n\t\t} else {\n\t\t\tupdateAnchorHint(this.editor)\n\t\t}\n\t}\n\n\toverride onPointerMove() {\n\t\tupdateAnchorHint(this.editor)\n\t}\n\n\toverride onPointerDown() {\n\t\tthis.parent.transition('pointing')\n\t}\n}\n\nclass CommentPointing extends StateNode {\n\tstatic override id = 'pointing'\n\n\toverride onEnter() {\n\t\t// Open the composer immediately at the press point so it's visible from pointer-down (not just\n\t\t// on release), and let it trail the pointer while dragging \u2014 like placing a sticky note. The\n\t\t// anchor is a bare point for now; it's resolved (shape or point) on pointer up.\n\t\tconst { editor } = this\n\t\t// Hide the cursor while placing: the draft composer is the pointer's stand-in now, so the pin\n\t\t// cursor sitting over it just reads as clutter.\n\t\teditor.setCursor({ type: 'none', rotation: 0 })\n\t\tconst point = editor.inputs.getCurrentPagePoint()\n\t\tpendingComment.set(editor, {\n\t\t\tanchor: { type: 'point', x: point.x, y: point.y },\n\t\t\tpoint: { x: point.x, y: point.y },\n\t\t})\n\t}\n\n\toverride onPointerMove() {\n\t\tconst { editor } = this\n\t\t// Once the pointer passes the drag threshold with region comments enabled, this is a region,\n\t\t// not a follow \u2014 hand off to the region drag (which clears this composer and draws the box).\n\t\tif (getCommentingOptions(editor).enableRegions && editor.inputs.getIsDragging()) {\n\t\t\tthis.parent.transition('dragging')\n\t\t\treturn\n\t\t}\n\t\t// Otherwise the composer trails the pointer \u2014 keep hinting the shape a release here would\n\t\t// anchor to, like the idle hover does.\n\t\tupdateAnchorHint(editor)\n\t\tconst point = editor.inputs.getCurrentPagePoint()\n\t\tpendingComment.update(editor, (p) => (p ? { ...p, point: { x: point.x, y: point.y } } : p))\n\t}\n\n\t// Settle where the pointer is released: anchor to the shape under it, or drop a point.\n\toverride onPointerUp() {\n\t\tconst { editor } = this\n\t\tconst point = editor.inputs.getCurrentPagePoint()\n\t\tconst hit = commentTargetShapeAt(editor, point)\n\t\tconst anchor: TLCommentAnchor = hit\n\t\t\t? shapeAnchorAt(\n\t\t\t\t\teditor,\n\t\t\t\t\thit.id,\n\t\t\t\t\tpoint,\n\t\t\t\t\tgetCommentingOptions(editor).shouldBePrecise(editor, {\n\t\t\t\t\t\tshapeId: hit.id,\n\t\t\t\t\t\tpoint,\n\t\t\t\t\t\taltKey: editor.inputs.getAltKey(),\n\t\t\t\t\t})\n\t\t\t\t)\n\t\t\t: { type: 'point', x: point.x, y: point.y }\n\t\tpendingComment.set(editor, { anchor, point: { x: point.x, y: point.y } })\n\t\t// Stay in the tool while the composer is open \u2014 the interaction isn't over until the\n\t\t// comment is posted or dismissed, and staying keeps the surrounding UI (style panel,\n\t\t// sidebar) from churning mid-placement.\n\t\tthis.parent.transition('idle')\n\t}\n\n\toverride onCancel() {\n\t\tthis.cancel()\n\t}\n\n\toverride onInterrupt() {\n\t\tthis.cancel()\n\t}\n\n\t// Abandon the follow composer if placement is interrupted (Escape, focus loss, etc.). The\n\t// tool's onExit drops the draft.\n\tprivate cancel() {\n\t\tthis.editor.setCurrentTool('select')\n\t}\n}\n\nclass CommentDragging extends StateNode {\n\tstatic override id = 'dragging'\n\n\toverride onEnter() {\n\t\t// A region drag supersedes the point-follow composer opened in `pointing`. Show a crosshair again,\n\t\t// since the composer no longer stands in for the pointer, and drop the placement hint \u2014 a region\n\t\t// anchors to its rectangle, so a single-shape outline would be stale.\n\t\tpendingComment.set(this.editor, null)\n\t\tthis.editor.setHintingShapes([])\n\t\tthis.editor.setCursor({ type: 'cross', rotation: 0 })\n\t\tthis.updateDraft()\n\t}\n\n\toverride onPointerMove() {\n\t\tthis.updateDraft()\n\t}\n\n\t// Commit the dragged rectangle as a region anchor; the composer opens at its pin corner.\n\toverride onPointerUp() {\n\t\tconst { editor } = this\n\t\tconst origin = editor.inputs.getOriginPagePoint()\n\t\tconst current = editor.inputs.getCurrentPagePoint()\n\t\tconst region = regionBetween(origin, current)\n\t\t// The pin lives on the corner the drag released on \u2014 drag up-left, pin top-left.\n\t\tconst pin = { x: current.x >= origin.x ? 1 : 0, y: current.y >= origin.y ? 1 : 0 }\n\t\tregionDraft.set(editor, null)\n\t\tpendingComment.set(editor, {\n\t\t\tanchor: { type: 'region', ...region, pinX: pin.x, pinY: pin.y },\n\t\t\tpoint: regionPinPoint(region, pin),\n\t\t})\n\t\t// Same as the point placement: the tool stays active while the composer is open.\n\t\tthis.parent.transition('idle')\n\t}\n\n\toverride onCancel() {\n\t\tthis.cancel()\n\t}\n\n\toverride onInterrupt() {\n\t\tthis.cancel()\n\t}\n\n\tprivate updateDraft() {\n\t\tconst { editor } = this\n\t\tregionDraft.set(\n\t\t\teditor,\n\t\t\tregionBetween(editor.inputs.getOriginPagePoint(), editor.inputs.getCurrentPagePoint())\n\t\t)\n\t}\n\n\t// The tool's onExit drops the region draft.\n\tprivate cancel() {\n\t\tthis.editor.setCurrentTool('select')\n\t}\n}\n\n/** @public */\nexport const commentTools = [CommentTool]\n\n/** Registers the comment tool in the UI (icon, label, shortcut). Compose into your overrides.\n * Once registered, tldraw's `DefaultQuickActionsContent` shows the comment button.\n * @public */\nexport const commentToolOverrides: TLUiOverrides = {\n\ttools(editor, tools) {\n\t\ttools.comment = {\n\t\t\tid: 'comment',\n\t\t\ticon: 'comment',\n\t\t\tlabel: 'Comment',\n\t\t\tkbd: 'c',\n\t\t\tonSelect: () => editor.setCurrentTool('comment'),\n\t\t}\n\t\treturn tools\n\t},\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -43,7 +43,7 @@ function CommentsFilterMenu({
|
|
|
43
43
|
tooltip: msg("comments.filter"),
|
|
44
44
|
title: msg("comments.filter"),
|
|
45
45
|
className: "tlui-cmt-header-btn",
|
|
46
|
-
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.
|
|
46
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.MoreMenuIcon, {})
|
|
47
47
|
}
|
|
48
48
|
) }),
|
|
49
49
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_tldraw.TldrawUiDropdownMenuContent, { side: "bottom", align: "end", alignOffset: 0, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_tldraw.TldrawUiMenuContextProvider, { type: "menu", sourceId: "menu", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_tldraw.TldrawUiMenuGroup, { id: "comments-filter", children: [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/canvas/comments-filter-menu.tsx"],
|
|
4
|
-
"sourcesContent": ["import {\n\tTldrawUiButton,\n\tTldrawUiDropdownMenuContent,\n\tTldrawUiDropdownMenuRoot,\n\tTldrawUiDropdownMenuTrigger,\n\tTldrawUiMenuCheckboxItem,\n\tTldrawUiMenuContextProvider,\n\tTldrawUiMenuGroup,\n\tuseEditor,\n\tuseTranslation,\n\tuseValue,\n} from 'tldraw'\nimport {
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA8CK;AA9CL,oBAWO;AACP,
|
|
4
|
+
"sourcesContent": ["import {\n\tTldrawUiButton,\n\tTldrawUiDropdownMenuContent,\n\tTldrawUiDropdownMenuRoot,\n\tTldrawUiDropdownMenuTrigger,\n\tTldrawUiMenuCheckboxItem,\n\tTldrawUiMenuContextProvider,\n\tTldrawUiMenuGroup,\n\tuseEditor,\n\tuseTranslation,\n\tuseValue,\n} from 'tldraw'\nimport { MoreMenuIcon } from '../ui/icons'\nimport { SidebarFilters } from './sidebar-filters'\nimport { sidebarFilters } from './state'\n\n/** @public */\nexport interface CommentsFilterMenuProps {\n\t/** Whether to offer the \"only your threads\" toggle (needs a known current user). */\n\tcanFilterByAuthor?: boolean\n\t/** Whether to offer the \"only unread\" toggle (needs a read-status source). */\n\tcanFilterByUnread?: boolean\n}\n\n/** The funnel dropdown in the sidebar header: toggles for which threads the list shows.\n * @public @react */\nexport function CommentsFilterMenu({\n\tcanFilterByAuthor,\n\tcanFilterByUnread,\n}: CommentsFilterMenuProps) {\n\tconst editor = useEditor()\n\tconst msg = useTranslation()\n\tconst filters = useValue('sidebar filters', () => sidebarFilters.get(editor), [editor])\n\tconst toggle = (key: keyof SidebarFilters) => {\n\t\tsidebarFilters.update(editor, (f) => ({ ...f, [key]: !f[key] }))\n\t}\n\n\treturn (\n\t\t<TldrawUiDropdownMenuRoot id=\"comments-filter\">\n\t\t\t<TldrawUiDropdownMenuTrigger>\n\t\t\t\t<TldrawUiButton\n\t\t\t\t\ttype=\"icon\"\n\t\t\t\t\ttooltip={msg('comments.filter')}\n\t\t\t\t\ttitle={msg('comments.filter')}\n\t\t\t\t\tclassName=\"tlui-cmt-header-btn\"\n\t\t\t\t>\n\t\t\t\t\t<MoreMenuIcon />\n\t\t\t\t</TldrawUiButton>\n\t\t\t</TldrawUiDropdownMenuTrigger>\n\t\t\t{/* No tlui-cmt-menu here: the rows are tldraw menu items, which carry their own insets \u2014\n\t\t\t the extra padding would set this menu apart from the canvas menus it should match. */}\n\t\t\t<TldrawUiDropdownMenuContent side=\"bottom\" align=\"end\" alignOffset={0}>\n\t\t\t\t<TldrawUiMenuContextProvider type=\"menu\" sourceId=\"menu\">\n\t\t\t\t\t<TldrawUiMenuGroup id=\"comments-filter\">\n\t\t\t\t\t\t<TldrawUiMenuCheckboxItem\n\t\t\t\t\t\t\tid=\"show-all-pages\"\n\t\t\t\t\t\t\tlabel=\"comments.show-all-pages\"\n\t\t\t\t\t\t\tchecked={!filters.onlyCurrentPage}\n\t\t\t\t\t\t\tonSelect={() => toggle('onlyCurrentPage')}\n\t\t\t\t\t\t/>\n\t\t\t\t\t\t{canFilterByAuthor && (\n\t\t\t\t\t\t\t<TldrawUiMenuCheckboxItem\n\t\t\t\t\t\t\t\tid=\"only-my-comments\"\n\t\t\t\t\t\t\t\tlabel=\"comments.only-my-comments\"\n\t\t\t\t\t\t\t\tchecked={filters.onlyMine}\n\t\t\t\t\t\t\t\tonSelect={() => toggle('onlyMine')}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t{canFilterByUnread && (\n\t\t\t\t\t\t\t<TldrawUiMenuCheckboxItem\n\t\t\t\t\t\t\t\tid=\"only-unread\"\n\t\t\t\t\t\t\t\tlabel=\"comments.only-unread\"\n\t\t\t\t\t\t\t\tchecked={filters.onlyUnread}\n\t\t\t\t\t\t\t\tonSelect={() => toggle('onlyUnread')}\n\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t)}\n\t\t\t\t\t\t<TldrawUiMenuCheckboxItem\n\t\t\t\t\t\t\tid=\"show-resolved\"\n\t\t\t\t\t\t\tlabel=\"comments.show-resolved\"\n\t\t\t\t\t\t\tchecked={filters.showResolved}\n\t\t\t\t\t\t\tonSelect={() => toggle('showResolved')}\n\t\t\t\t\t\t/>\n\t\t\t\t\t</TldrawUiMenuGroup>\n\t\t\t\t</TldrawUiMenuContextProvider>\n\t\t\t</TldrawUiDropdownMenuContent>\n\t\t</TldrawUiDropdownMenuRoot>\n\t)\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA8CK;AA9CL,oBAWO;AACP,mBAA6B;AAE7B,mBAA+B;AAYxB,SAAS,mBAAmB;AAAA,EAClC;AAAA,EACA;AACD,GAA4B;AAC3B,QAAM,aAAS,yBAAU;AACzB,QAAM,UAAM,8BAAe;AAC3B,QAAM,cAAU,wBAAS,mBAAmB,MAAM,4BAAe,IAAI,MAAM,GAAG,CAAC,MAAM,CAAC;AACtF,QAAM,SAAS,CAAC,QAA8B;AAC7C,gCAAe,OAAO,QAAQ,CAAC,OAAO,EAAE,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,EAAE,GAAG,EAAE,EAAE;AAAA,EAChE;AAEA,SACC,6CAAC,0CAAyB,IAAG,mBAC5B;AAAA,gDAAC,6CACA;AAAA,MAAC;AAAA;AAAA,QACA,MAAK;AAAA,QACL,SAAS,IAAI,iBAAiB;AAAA,QAC9B,OAAO,IAAI,iBAAiB;AAAA,QAC5B,WAAU;AAAA,QAEV,sDAAC,6BAAa;AAAA;AAAA,IACf,GACD;AAAA,IAGA,4CAAC,6CAA4B,MAAK,UAAS,OAAM,OAAM,aAAa,GACnE,sDAAC,6CAA4B,MAAK,QAAO,UAAS,QACjD,uDAAC,mCAAkB,IAAG,mBACrB;AAAA;AAAA,QAAC;AAAA;AAAA,UACA,IAAG;AAAA,UACH,OAAM;AAAA,UACN,SAAS,CAAC,QAAQ;AAAA,UAClB,UAAU,MAAM,OAAO,iBAAiB;AAAA;AAAA,MACzC;AAAA,MACC,qBACA;AAAA,QAAC;AAAA;AAAA,UACA,IAAG;AAAA,UACH,OAAM;AAAA,UACN,SAAS,QAAQ;AAAA,UACjB,UAAU,MAAM,OAAO,UAAU;AAAA;AAAA,MAClC;AAAA,MAEA,qBACA;AAAA,QAAC;AAAA;AAAA,UACA,IAAG;AAAA,UACH,OAAM;AAAA,UACN,SAAS,QAAQ;AAAA,UACjB,UAAU,MAAM,OAAO,YAAY;AAAA;AAAA,MACpC;AAAA,MAED;AAAA,QAAC;AAAA;AAAA,UACA,IAAG;AAAA,UACH,OAAM;AAAA,UACN,SAAS,QAAQ;AAAA,UACjB,UAAU,MAAM,OAAO,cAAc;AAAA;AAAA,MACtC;AAAA,OACD,GACD,GACD;AAAA,KACD;AAEF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -53,7 +53,10 @@ function CanvasCommentsLayer(props) {
|
|
|
53
53
|
const canRenderComposer = canComment || options.components.ComposerFallback != null;
|
|
54
54
|
const showPendingComposer = pending != null && canRenderComposer;
|
|
55
55
|
(0, import_react.useEffect)(() => {
|
|
56
|
-
if (pending && !showPendingComposer)
|
|
56
|
+
if (pending && !showPendingComposer) {
|
|
57
|
+
import_state.pendingComment.set(editor, null);
|
|
58
|
+
if (editor.isIn("comment")) editor.setCurrentTool("select");
|
|
59
|
+
}
|
|
57
60
|
}, [editor, pending, showPendingComposer]);
|
|
58
61
|
const openId = (0, import_tldraw.useValue)("open thread id", () => import_state.openThreadId.get(editor), [editor]);
|
|
59
62
|
const showResolved = (0, import_tldraw.useValue)("show resolved", () => import_state.sidebarFilters.get(editor).showResolved, [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/canvas/comments-overlay.tsx"],
|
|
4
|
-
"sourcesContent": ["import { isMentionPickerOpen } from '@tldraw/mentions'\nimport { Fragment, ReactNode, useCallback, useEffect, useMemo, useRef } from 'react'\nimport { EditorPortal, TLCommentThread, useEditor, useValue } from 'tldraw'\nimport type { ClusterNode } from '../clustering/types'\nimport { registerCommentAnchorLifecycle } from './anchor-lifecycle'\nimport { ClusterBadge } from './cluster-badge'\nimport { clusterFadeClassName } from './cluster-fade'\nimport {\n\tCLUSTER_EXPAND_ZOOM_MS,\n\trevealThreadPin,\n\tuseClusterModel,\n\tzoomToClusterSplit,\n} from './cluster-model'\nimport { getCommentRecord } from './comment-store'\nimport { type CommentingContext } from './context'\nimport { useCommentThreads } from './hooks'\nimport { useCommentingEnabled } from './license'\nimport { useCanComment, useCommentingOptions } from './options'\nimport { PendingComposer } from './pending-composer'\nimport { computePinStacks, isOpenStackKeyLive, pinStacksEqual } from './pin-stacking'\nimport { RegionBox, RegionDraftBox } from './region-box'\nimport {\n\tcommentsHidden,\n\topenStackId,\n\topenThreadId,\n\tpendingComment,\n\trevealThreadRequest,\n\tsidebarFilters,\n\ttoggleCommentsHidden,\n\tusePendingComment,\n} from './state'\nimport { ThreadPin } from './thread-pin'\nimport { ThreadStackPin } from './thread-stack'\n\n/**\n * The host wiring for {@link CanvasComments} \u2014 see {@link CommentingContext}, which the sidebar\n * takes the same fields from.\n *\n * @public\n */\nexport type CanvasCommentsProps = CommentingContext\n\n/**\n * A ready-to-use comments layer for a tldraw canvas: pins each thread at its anchor, opens a\n * thread popover (with a reply composer) on click, and shows a composer where the comment tool\n * placed a new thread. Reads/writes comment records straight from `editor.store`.\n *\n * It's the batteries-included default: every visible piece is a slot on\n * `CommentTool.configure({ components })`, and the pieces it composes (`CommentPin`,\n * `CommentThread`, `CommentComposer`, the hooks, the tool) are all exported, so a consumer can\n * rebuild it from parts instead. The host wiring is the {@link CommentingContext}, which\n * `CanvasCommentsSidebar` takes too.\n *\n * @public @react\n */\nexport function CanvasComments(props: CanvasCommentsProps) {\n\t// The inner component holds every other hook, so mounting it as the license resolves keeps hook\n\t// order stable here.\n\tconst commentingEnabled = useCommentingEnabled()\n\tif (!commentingEnabled) return null\n\treturn <CanvasCommentsLayer {...props} />\n}\n\nfunction CanvasCommentsLayer(props: CommentingContext) {\n\tconst editor = useEditor()\n\tconst options = useCommentingOptions()\n\tconst allThreads = useCommentThreads(editor)\n\tconst pending = usePendingComment()\n\tconst canComment = useCanComment(props.currentUserId)\n\t// Nothing renders a pending comment when composing is blocked and there's no fallback slot, and\n\t// the dismiss handlers live inside PendingComposer \u2014 so clear the atom rather than strand it.\n\tconst canRenderComposer = canComment || options.components.ComposerFallback != null\n\tconst showPendingComposer = pending != null && canRenderComposer\n\tuseEffect(() => {\n\t\tif (pending && !showPendingComposer) pendingComment.set(editor, null)\n\t}, [editor, pending, showPendingComposer])\n\tconst openId = useValue('open thread id', () => openThreadId.get(editor), [editor])\n\t// Matches the sidebar's `showResolved` filter. The open thread stays in \u2014 resolving from its own\n\t// popover shouldn't make the pin vanish under it.\n\tconst showResolved = useValue('show resolved', () => sidebarFilters.get(editor).showResolved, [\n\t\teditor,\n\t])\n\tconst threads = useMemo(\n\t\t() => allThreads.filter((t) => showResolved || t.resolved == null || t.id === openId),\n\t\t[allThreads, showResolved, openId]\n\t)\n\tuseEffect(() => registerCommentAnchorLifecycle(editor), [editor])\n\tconst {\n\t\tmodel: clusterModel,\n\t\tzoomBounds: clusterZoomBounds,\n\t\tfadeNodes,\n\t\torphanThreads,\n\t\theldThreads,\n\t} = useClusterModel(editor, threads, openId)\n\tconst threadsById = useMemo(\n\t\t() => new Map<string, TLCommentThread>(threads.map((thread) => [thread.id, thread])),\n\t\t[threads]\n\t)\n\t// Pins with the *same* anchor point coincide at every zoom, so they render as one count-badge\n\t// stack. Keyed on page-space anchors, so camera moves never recompute this. Holding the map's\n\t// identity while the grouping is unchanged keeps a reply from re-rendering every pin \u2014 but the\n\t// map has no positions in it, so anything needing a stack's *point* must read the anchors itself.\n\tconst pinStacksRef = useRef<Map<string, readonly string[]>>(new Map())\n\tconst pinStacks = useValue(\n\t\t'comment pin stacks',\n\t\t() => {\n\t\t\tconst stacks = computePinStacks(editor, threads)\n\t\t\tif (pinStacksEqual(pinStacksRef.current, stacks)) return pinStacksRef.current\n\t\t\tpinStacksRef.current = stacks\n\t\t\treturn stacks\n\t\t},\n\t\t[editor, threads]\n\t)\n\tconst openThread = openId ? threadsById.get(openId) : null\n\tconst hidden = useValue('comments hidden', () => commentsHidden.get(editor), [editor])\n\n\tuseEffect(() => {\n\t\treturn () => {\n\t\t\topenThreadId.set(editor, null)\n\t\t\topenStackId.set(editor, null)\n\t\t\tpendingComment.set(editor, null)\n\t\t\trevealThreadRequest.set(editor, null)\n\t\t}\n\t}, [editor])\n\n\t// Clear a stale open-stack key: only the stack's own mounted handlers clear it, so collapsing to\n\t// a single pin strands it \u2014 and `useMarkerPreview` reads any non-null value as \"a stack is open\"\n\t// and suppresses every hover preview. Kept while any live stack still sits at that key. Reads the\n\t// anchors rather than keying off `pinStacks`, whose identity survives a stack moving as a whole.\n\tconst openStackKeyIsStale = useValue(\n\t\t'open stack key stale',\n\t\t() => {\n\t\t\tconst key = openStackId.get(editor)\n\t\t\tif (!key) return false\n\t\t\treturn !isOpenStackKeyLive(editor, key, pinStacks, threadsById)\n\t\t},\n\t\t[editor, pinStacks, threadsById]\n\t)\n\tuseEffect(() => {\n\t\tif (openStackKeyIsStale) openStackId.set(editor, null)\n\t}, [editor, openStackKeyIsStale])\n\n\t// The requested thread, once it (and, for a comment id, its parent thread) has synced into the\n\t// store; null while records are still arriving or when no request is pending.\n\tconst requestedRevealThread = useValue(\n\t\t'requested reveal thread',\n\t\t() => {\n\t\t\tconst id = revealThreadRequest.get(editor)\n\t\t\tif (!id) return null\n\t\t\tconst record = getCommentRecord(editor, id)\n\t\t\tif (!record) return null\n\t\t\tconst thread =\n\t\t\t\trecord.typeName === 'comment' ? getCommentRecord(editor, record.threadId) : record\n\t\t\treturn thread?.typeName === 'comment-thread' ? thread : null\n\t\t},\n\t\t[editor]\n\t)\n\n\t// Serve a pending reveal request, zooming to the first cluster split that reveals the pin if it's\n\t// folded into a badge. Also unhides pins: the popover opens on the layer that hiding withholds.\n\tuseEffect(() => {\n\t\tif (!requestedRevealThread) return\n\t\trevealThreadRequest.set(editor, null)\n\t\tcommentsHidden.set(editor, false)\n\t\trevealThreadPin(editor, requestedRevealThread, clusterModel.table, clusterZoomBounds, options)\n\t\topenThreadId.set(editor, requestedRevealThread.id)\n\t}, [requestedRevealThread, clusterModel.table, clusterZoomBounds, editor, options])\n\n\t// Picking a thread out of a cluster's hover preview. `openThreadId` alone would work, but would\n\t// cut straight there from the badge \u2014 zoom in first, the same move the badge's own click makes.\n\tconst revealClusteredThread = useCallback(\n\t\t(thread: TLCommentThread) => {\n\t\t\trevealThreadPin(\n\t\t\t\teditor,\n\t\t\t\tthread,\n\t\t\t\tclusterModel.table,\n\t\t\t\tclusterZoomBounds,\n\t\t\t\toptions,\n\t\t\t\tCLUSTER_EXPAND_ZOOM_MS\n\t\t\t)\n\t\t\topenThreadId.set(editor, thread.id)\n\t\t},\n\t\t[clusterModel.table, clusterZoomBounds, editor, options]\n\t)\n\n\tconst expandCluster = useCallback(\n\t\t(node: ClusterNode) => {\n\t\t\tzoomToClusterSplit(editor, clusterModel.table, clusterZoomBounds, node)\n\t\t},\n\t\t[clusterModel.table, clusterZoomBounds, editor]\n\t)\n\n\t// Escape collapses the open thread. Capture-phase so it runs ahead of the editor, which would\n\t// otherwise cancel the tool or clear the selection.\n\tuseEffect(() => {\n\t\tconst onKeyDown = (e: KeyboardEvent) => {\n\t\t\tif (e.key !== 'Escape' || openThreadId.get(editor) === null) return\n\t\t\t// The mention picker owns Escape while it's open \u2014 let it dismiss the roster alone.\n\t\t\tif (isMentionPickerOpen()) return\n\t\t\tconst target = e.target as HTMLElement | null\n\t\t\tif (target && target.closest('.tlui-cmt-editing')) return\n\t\t\topenThreadId.set(editor, null)\n\t\t\te.preventDefault()\n\t\t\te.stopPropagation()\n\t\t}\n\t\tdocument.addEventListener('keydown', onKeyDown, true)\n\t\treturn () => document.removeEventListener('keydown', onKeyDown, true)\n\t}, [editor])\n\n\t// Shift+C toggles pin visibility. Physical `KeyC` so it's layout-independent, and skipped while\n\t// typing so it never fires from inside a composer.\n\tuseEffect(() => {\n\t\tconst onKeyDown = (e: KeyboardEvent) => {\n\t\t\tif (e.code !== 'KeyC' || !e.shiftKey || e.metaKey || e.ctrlKey || e.altKey) return\n\t\t\tconst target = e.target as HTMLElement | null\n\t\t\tif (target && target.closest('input, textarea, [contenteditable=\"true\"]')) return\n\t\t\ttoggleCommentsHidden(editor)\n\t\t\te.preventDefault()\n\t\t}\n\t\tdocument.addEventListener('keydown', onKeyDown, true)\n\t\treturn () => document.removeEventListener('keydown', onKeyDown, true)\n\t}, [editor])\n\n\t// The signal is read above so this stays mounted and its shortcut/Escape effects keep running.\n\tif (hidden) return null\n\n\t// The node's pin-stack group when every member shares one \u2014 a stack standing on its own, which\n\t// renders as a cascading count-badge list rather than a zoom-to-split cluster badge. The group\n\t// can include an open or orphan member the node's own leaves omit.\n\tconst stackGroupOf = (node: ClusterNode): readonly string[] | null => {\n\t\tconst group = pinStacks.get(node.members[0])\n\t\tif (!group) return null\n\t\treturn node.members.every((id) => group.includes(id)) ? group : null\n\t}\n\n\t// Which threads are on screen this render. A stack renders once, owned by its first on-screen\n\t// member \u2014 members arrive by different paths, so ownership can't be decided per-path.\n\tconst renderedThreadIds = new Set<string>()\n\tif (options.enableClustering) {\n\t\tfor (const { node } of fadeNodes) {\n\t\t\tif (node.count === 1) renderedThreadIds.add(node.id)\n\t\t\t// A pure-stack node's members aren't count-1 leaves, so register them here for the owner\n\t\t\t// logic to pick from.\n\t\t\telse if (stackGroupOf(node)) for (const id of node.members) renderedThreadIds.add(id)\n\t\t}\n\t\tfor (const thread of orphanThreads) renderedThreadIds.add(thread.id)\n\t\tfor (const thread of heldThreads) renderedThreadIds.add(thread.id)\n\t} else {\n\t\tfor (const thread of threads) renderedThreadIds.add(thread.id)\n\t}\n\tif (openThread) renderedThreadIds.add(openThread.id)\n\n\t// A coincident-stack member renders as the group's single count-badge stack (if it owns it)\n\t// or not at all; everything else is an ordinary pin.\n\tconst renderThreadPin = (thread: TLCommentThread): ReactNode => {\n\t\tconst group = pinStacks.get(thread.id)\n\t\tif (group) {\n\t\t\tconst owner = group.find((id) => renderedThreadIds.has(id))\n\t\t\tif (owner !== thread.id) return null\n\t\t\tconst stackThreads = group\n\t\t\t\t.map((id) => threadsById.get(id))\n\t\t\t\t.filter((t): t is TLCommentThread => t !== undefined)\n\t\t\treturn <ThreadStackPin editor={editor} threads={stackThreads} {...props} />\n\t\t}\n\t\treturn <ThreadPin editor={editor} thread={thread} {...props} />\n\t}\n\n\t// Portalled rather than rendered into this component's slot: pins sit below the collaborator\n\t// cursors and popovers above the UI panels, and no single canvas layer spans both. The portal\n\t// also fixes where the layer lands among the container's children, keeping it behind the UI's\n\t// skip link in the tab order.\n\treturn (\n\t\t<EditorPortal>\n\t\t\t{/* Wheel pass-through lives on each interactive element, not this root: the root spans the\n\t\t\t whole canvas, so a pin past its bottom/right edge inflates scrollHeight and the wheel\n\t\t\t hook's is-this-scrollable guard silently disables pass-through. */}\n\t\t\t<div className=\"tlui-cmt-canvas-layer\">\n\t\t\t\t{options.enableClustering ? (\n\t\t\t\t\t<>\n\t\t\t\t\t\t{fadeNodes.map(({ node, phase }) => {\n\t\t\t\t\t\t\tlet content: ReactNode\n\t\t\t\t\t\t\tconst stackGroup = node.count > 1 ? stackGroupOf(node) : null\n\t\t\t\t\t\t\tif (node.count === 1) {\n\t\t\t\t\t\t\t\tconst thread = threadsById.get(node.id)\n\t\t\t\t\t\t\t\tif (!thread) return null\n\t\t\t\t\t\t\t\tcontent = renderThreadPin(thread)\n\t\t\t\t\t\t\t} else if (stackGroup) {\n\t\t\t\t\t\t\t\t// Routed through the stack's owner so the open/orphan/held slots stay deduped:\n\t\t\t\t\t\t\t\t// when the owner is one of them, that slot draws the stack and this draws nothing.\n\t\t\t\t\t\t\t\tconst owner = stackGroup.find((id) => renderedThreadIds.has(id))\n\t\t\t\t\t\t\t\tcontent =\n\t\t\t\t\t\t\t\t\towner && node.members.includes(owner)\n\t\t\t\t\t\t\t\t\t\t? renderThreadPin(threadsById.get(owner)!)\n\t\t\t\t\t\t\t\t\t\t: null\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tcontent = (\n\t\t\t\t\t\t\t\t\t<ClusterBadge\n\t\t\t\t\t\t\t\t\t\teditor={editor}\n\t\t\t\t\t\t\t\t\t\tnode={node}\n\t\t\t\t\t\t\t\t\t\tonExpand={expandCluster}\n\t\t\t\t\t\t\t\t\t\tonSelectThread={revealClusteredThread}\n\t\t\t\t\t\t\t\t\t\tthreadsById={threadsById}\n\t\t\t\t\t\t\t\t\t\tcurrentUserId={props.currentUserId}\n\t\t\t\t\t\t\t\t\t\tresolveAuthor={props.resolveAuthor}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<div key={`cluster-fade:${node.id}`} className={clusterFadeClassName(phase)}>\n\t\t\t\t\t\t\t\t\t{content}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t})}\n\t\t\t\t\t\t{orphanThreads.map((thread) => (\n\t\t\t\t\t\t\t<Fragment key={thread.id}>{renderThreadPin(thread)}</Fragment>\n\t\t\t\t\t\t))}\n\t\t\t\t\t\t{heldThreads.map((thread) => (\n\t\t\t\t\t\t\t<Fragment key={thread.id}>{renderThreadPin(thread)}</Fragment>\n\t\t\t\t\t\t))}\n\t\t\t\t\t</>\n\t\t\t\t) : (\n\t\t\t\t\t// Clustering off: every thread renders its own pin. The open thread is excluded and\n\t\t\t\t\t// rendered once below, or it would mount a second, stacked pin.\n\t\t\t\t\tthreads\n\t\t\t\t\t\t.filter((thread) => thread.id !== openId)\n\t\t\t\t\t\t.map((thread) => <Fragment key={thread.id}>{renderThreadPin(thread)}</Fragment>)\n\t\t\t\t)}\n\t\t\t\t{openThread && (\n\t\t\t\t\t<Fragment key={`open:${openThread.id}`}>{renderThreadPin(openThread)}</Fragment>\n\t\t\t\t)}\n\t\t\t\t<RegionDraftBox editor={editor} />\n\t\t\t\t{/* Keep the region visible while composing \u2014 the drag draft is gone by now, and no thread\n\t\t\t\t exists yet, so the pending anchor is what shows the area under the open composer. */}\n\t\t\t\t{pending?.anchor.type === 'region' && showPendingComposer && (\n\t\t\t\t\t<RegionBox editor={editor} box={pending.anchor} />\n\t\t\t\t)}\n\t\t\t\t{pending && showPendingComposer && (\n\t\t\t\t\t<PendingComposer editor={editor} pending={pending} {...props} />\n\t\t\t\t)}\n\t\t\t</div>\n\t\t</EditorPortal>\n\t)\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA4DQ;AA5DR,sBAAoC;AACpC,mBAA6E;AAC7E,oBAAmE;AAEnE,8BAA+C;AAC/C,2BAA6B;AAC7B,0BAAqC;AACrC,2BAKO;AACP,2BAAiC;AAEjC,mBAAkC;AAClC,qBAAqC;AACrC,qBAAoD;AACpD,8BAAgC;AAChC,0BAAqE;AACrE,wBAA0C;AAC1C,mBASO;AACP,wBAA0B;AAC1B,0BAA+B;AAuBxB,SAAS,eAAe,OAA4B;AAG1D,QAAM,wBAAoB,qCAAqB;AAC/C,MAAI,CAAC,kBAAmB,QAAO;AAC/B,SAAO,4CAAC,uBAAqB,GAAG,OAAO;AACxC;AAEA,SAAS,oBAAoB,OAA0B;AACtD,QAAM,aAAS,yBAAU;AACzB,QAAM,cAAU,qCAAqB;AACrC,QAAM,iBAAa,gCAAkB,MAAM;AAC3C,QAAM,cAAU,gCAAkB;AAClC,QAAM,iBAAa,8BAAc,MAAM,aAAa;
|
|
4
|
+
"sourcesContent": ["import { isMentionPickerOpen } from '@tldraw/mentions'\nimport { Fragment, ReactNode, useCallback, useEffect, useMemo, useRef } from 'react'\nimport { EditorPortal, TLCommentThread, useEditor, useValue } from 'tldraw'\nimport type { ClusterNode } from '../clustering/types'\nimport { registerCommentAnchorLifecycle } from './anchor-lifecycle'\nimport { ClusterBadge } from './cluster-badge'\nimport { clusterFadeClassName } from './cluster-fade'\nimport {\n\tCLUSTER_EXPAND_ZOOM_MS,\n\trevealThreadPin,\n\tuseClusterModel,\n\tzoomToClusterSplit,\n} from './cluster-model'\nimport { getCommentRecord } from './comment-store'\nimport { type CommentingContext } from './context'\nimport { useCommentThreads } from './hooks'\nimport { useCommentingEnabled } from './license'\nimport { useCanComment, useCommentingOptions } from './options'\nimport { PendingComposer } from './pending-composer'\nimport { computePinStacks, isOpenStackKeyLive, pinStacksEqual } from './pin-stacking'\nimport { RegionBox, RegionDraftBox } from './region-box'\nimport {\n\tcommentsHidden,\n\topenStackId,\n\topenThreadId,\n\tpendingComment,\n\trevealThreadRequest,\n\tsidebarFilters,\n\ttoggleCommentsHidden,\n\tusePendingComment,\n} from './state'\nimport { ThreadPin } from './thread-pin'\nimport { ThreadStackPin } from './thread-stack'\n\n/**\n * The host wiring for {@link CanvasComments} \u2014 see {@link CommentingContext}, which the sidebar\n * takes the same fields from.\n *\n * @public\n */\nexport type CanvasCommentsProps = CommentingContext\n\n/**\n * A ready-to-use comments layer for a tldraw canvas: pins each thread at its anchor, opens a\n * thread popover (with a reply composer) on click, and shows a composer where the comment tool\n * placed a new thread. Reads/writes comment records straight from `editor.store`.\n *\n * It's the batteries-included default: every visible piece is a slot on\n * `CommentTool.configure({ components })`, and the pieces it composes (`CommentPin`,\n * `CommentThread`, `CommentComposer`, the hooks, the tool) are all exported, so a consumer can\n * rebuild it from parts instead. The host wiring is the {@link CommentingContext}, which\n * `CanvasCommentsSidebar` takes too.\n *\n * @public @react\n */\nexport function CanvasComments(props: CanvasCommentsProps) {\n\t// The inner component holds every other hook, so mounting it as the license resolves keeps hook\n\t// order stable here.\n\tconst commentingEnabled = useCommentingEnabled()\n\tif (!commentingEnabled) return null\n\treturn <CanvasCommentsLayer {...props} />\n}\n\nfunction CanvasCommentsLayer(props: CommentingContext) {\n\tconst editor = useEditor()\n\tconst options = useCommentingOptions()\n\tconst allThreads = useCommentThreads(editor)\n\tconst pending = usePendingComment()\n\tconst canComment = useCanComment(props.currentUserId)\n\t// Nothing renders a pending comment when composing is blocked and there's no fallback slot, and\n\t// the dismiss handlers live inside PendingComposer \u2014 so clear the atom rather than strand it,\n\t// and leave the tool too: it holds while a composer is open, so staying would turn every click\n\t// into a silently swallowed placement.\n\tconst canRenderComposer = canComment || options.components.ComposerFallback != null\n\tconst showPendingComposer = pending != null && canRenderComposer\n\tuseEffect(() => {\n\t\tif (pending && !showPendingComposer) {\n\t\t\tpendingComment.set(editor, null)\n\t\t\tif (editor.isIn('comment')) editor.setCurrentTool('select')\n\t\t}\n\t}, [editor, pending, showPendingComposer])\n\tconst openId = useValue('open thread id', () => openThreadId.get(editor), [editor])\n\t// Matches the sidebar's `showResolved` filter. The open thread stays in \u2014 resolving from its own\n\t// popover shouldn't make the pin vanish under it.\n\tconst showResolved = useValue('show resolved', () => sidebarFilters.get(editor).showResolved, [\n\t\teditor,\n\t])\n\tconst threads = useMemo(\n\t\t() => allThreads.filter((t) => showResolved || t.resolved == null || t.id === openId),\n\t\t[allThreads, showResolved, openId]\n\t)\n\tuseEffect(() => registerCommentAnchorLifecycle(editor), [editor])\n\tconst {\n\t\tmodel: clusterModel,\n\t\tzoomBounds: clusterZoomBounds,\n\t\tfadeNodes,\n\t\torphanThreads,\n\t\theldThreads,\n\t} = useClusterModel(editor, threads, openId)\n\tconst threadsById = useMemo(\n\t\t() => new Map<string, TLCommentThread>(threads.map((thread) => [thread.id, thread])),\n\t\t[threads]\n\t)\n\t// Pins with the *same* anchor point coincide at every zoom, so they render as one count-badge\n\t// stack. Keyed on page-space anchors, so camera moves never recompute this. Holding the map's\n\t// identity while the grouping is unchanged keeps a reply from re-rendering every pin \u2014 but the\n\t// map has no positions in it, so anything needing a stack's *point* must read the anchors itself.\n\tconst pinStacksRef = useRef<Map<string, readonly string[]>>(new Map())\n\tconst pinStacks = useValue(\n\t\t'comment pin stacks',\n\t\t() => {\n\t\t\tconst stacks = computePinStacks(editor, threads)\n\t\t\tif (pinStacksEqual(pinStacksRef.current, stacks)) return pinStacksRef.current\n\t\t\tpinStacksRef.current = stacks\n\t\t\treturn stacks\n\t\t},\n\t\t[editor, threads]\n\t)\n\tconst openThread = openId ? threadsById.get(openId) : null\n\tconst hidden = useValue('comments hidden', () => commentsHidden.get(editor), [editor])\n\n\tuseEffect(() => {\n\t\treturn () => {\n\t\t\topenThreadId.set(editor, null)\n\t\t\topenStackId.set(editor, null)\n\t\t\tpendingComment.set(editor, null)\n\t\t\trevealThreadRequest.set(editor, null)\n\t\t}\n\t}, [editor])\n\n\t// Clear a stale open-stack key: only the stack's own mounted handlers clear it, so collapsing to\n\t// a single pin strands it \u2014 and `useMarkerPreview` reads any non-null value as \"a stack is open\"\n\t// and suppresses every hover preview. Kept while any live stack still sits at that key. Reads the\n\t// anchors rather than keying off `pinStacks`, whose identity survives a stack moving as a whole.\n\tconst openStackKeyIsStale = useValue(\n\t\t'open stack key stale',\n\t\t() => {\n\t\t\tconst key = openStackId.get(editor)\n\t\t\tif (!key) return false\n\t\t\treturn !isOpenStackKeyLive(editor, key, pinStacks, threadsById)\n\t\t},\n\t\t[editor, pinStacks, threadsById]\n\t)\n\tuseEffect(() => {\n\t\tif (openStackKeyIsStale) openStackId.set(editor, null)\n\t}, [editor, openStackKeyIsStale])\n\n\t// The requested thread, once it (and, for a comment id, its parent thread) has synced into the\n\t// store; null while records are still arriving or when no request is pending.\n\tconst requestedRevealThread = useValue(\n\t\t'requested reveal thread',\n\t\t() => {\n\t\t\tconst id = revealThreadRequest.get(editor)\n\t\t\tif (!id) return null\n\t\t\tconst record = getCommentRecord(editor, id)\n\t\t\tif (!record) return null\n\t\t\tconst thread =\n\t\t\t\trecord.typeName === 'comment' ? getCommentRecord(editor, record.threadId) : record\n\t\t\treturn thread?.typeName === 'comment-thread' ? thread : null\n\t\t},\n\t\t[editor]\n\t)\n\n\t// Serve a pending reveal request, zooming to the first cluster split that reveals the pin if it's\n\t// folded into a badge. Also unhides pins: the popover opens on the layer that hiding withholds.\n\tuseEffect(() => {\n\t\tif (!requestedRevealThread) return\n\t\trevealThreadRequest.set(editor, null)\n\t\tcommentsHidden.set(editor, false)\n\t\trevealThreadPin(editor, requestedRevealThread, clusterModel.table, clusterZoomBounds, options)\n\t\topenThreadId.set(editor, requestedRevealThread.id)\n\t}, [requestedRevealThread, clusterModel.table, clusterZoomBounds, editor, options])\n\n\t// Picking a thread out of a cluster's hover preview. `openThreadId` alone would work, but would\n\t// cut straight there from the badge \u2014 zoom in first, the same move the badge's own click makes.\n\tconst revealClusteredThread = useCallback(\n\t\t(thread: TLCommentThread) => {\n\t\t\trevealThreadPin(\n\t\t\t\teditor,\n\t\t\t\tthread,\n\t\t\t\tclusterModel.table,\n\t\t\t\tclusterZoomBounds,\n\t\t\t\toptions,\n\t\t\t\tCLUSTER_EXPAND_ZOOM_MS\n\t\t\t)\n\t\t\topenThreadId.set(editor, thread.id)\n\t\t},\n\t\t[clusterModel.table, clusterZoomBounds, editor, options]\n\t)\n\n\tconst expandCluster = useCallback(\n\t\t(node: ClusterNode) => {\n\t\t\tzoomToClusterSplit(editor, clusterModel.table, clusterZoomBounds, node)\n\t\t},\n\t\t[clusterModel.table, clusterZoomBounds, editor]\n\t)\n\n\t// Escape collapses the open thread. Capture-phase so it runs ahead of the editor, which would\n\t// otherwise cancel the tool or clear the selection.\n\tuseEffect(() => {\n\t\tconst onKeyDown = (e: KeyboardEvent) => {\n\t\t\tif (e.key !== 'Escape' || openThreadId.get(editor) === null) return\n\t\t\t// The mention picker owns Escape while it's open \u2014 let it dismiss the roster alone.\n\t\t\tif (isMentionPickerOpen()) return\n\t\t\tconst target = e.target as HTMLElement | null\n\t\t\tif (target && target.closest('.tlui-cmt-editing')) return\n\t\t\topenThreadId.set(editor, null)\n\t\t\te.preventDefault()\n\t\t\te.stopPropagation()\n\t\t}\n\t\tdocument.addEventListener('keydown', onKeyDown, true)\n\t\treturn () => document.removeEventListener('keydown', onKeyDown, true)\n\t}, [editor])\n\n\t// Shift+C toggles pin visibility. Physical `KeyC` so it's layout-independent, and skipped while\n\t// typing so it never fires from inside a composer.\n\tuseEffect(() => {\n\t\tconst onKeyDown = (e: KeyboardEvent) => {\n\t\t\tif (e.code !== 'KeyC' || !e.shiftKey || e.metaKey || e.ctrlKey || e.altKey) return\n\t\t\tconst target = e.target as HTMLElement | null\n\t\t\tif (target && target.closest('input, textarea, [contenteditable=\"true\"]')) return\n\t\t\ttoggleCommentsHidden(editor)\n\t\t\te.preventDefault()\n\t\t}\n\t\tdocument.addEventListener('keydown', onKeyDown, true)\n\t\treturn () => document.removeEventListener('keydown', onKeyDown, true)\n\t}, [editor])\n\n\t// The signal is read above so this stays mounted and its shortcut/Escape effects keep running.\n\tif (hidden) return null\n\n\t// The node's pin-stack group when every member shares one \u2014 a stack standing on its own, which\n\t// renders as a cascading count-badge list rather than a zoom-to-split cluster badge. The group\n\t// can include an open or orphan member the node's own leaves omit.\n\tconst stackGroupOf = (node: ClusterNode): readonly string[] | null => {\n\t\tconst group = pinStacks.get(node.members[0])\n\t\tif (!group) return null\n\t\treturn node.members.every((id) => group.includes(id)) ? group : null\n\t}\n\n\t// Which threads are on screen this render. A stack renders once, owned by its first on-screen\n\t// member \u2014 members arrive by different paths, so ownership can't be decided per-path.\n\tconst renderedThreadIds = new Set<string>()\n\tif (options.enableClustering) {\n\t\tfor (const { node } of fadeNodes) {\n\t\t\tif (node.count === 1) renderedThreadIds.add(node.id)\n\t\t\t// A pure-stack node's members aren't count-1 leaves, so register them here for the owner\n\t\t\t// logic to pick from.\n\t\t\telse if (stackGroupOf(node)) for (const id of node.members) renderedThreadIds.add(id)\n\t\t}\n\t\tfor (const thread of orphanThreads) renderedThreadIds.add(thread.id)\n\t\tfor (const thread of heldThreads) renderedThreadIds.add(thread.id)\n\t} else {\n\t\tfor (const thread of threads) renderedThreadIds.add(thread.id)\n\t}\n\tif (openThread) renderedThreadIds.add(openThread.id)\n\n\t// A coincident-stack member renders as the group's single count-badge stack (if it owns it)\n\t// or not at all; everything else is an ordinary pin.\n\tconst renderThreadPin = (thread: TLCommentThread): ReactNode => {\n\t\tconst group = pinStacks.get(thread.id)\n\t\tif (group) {\n\t\t\tconst owner = group.find((id) => renderedThreadIds.has(id))\n\t\t\tif (owner !== thread.id) return null\n\t\t\tconst stackThreads = group\n\t\t\t\t.map((id) => threadsById.get(id))\n\t\t\t\t.filter((t): t is TLCommentThread => t !== undefined)\n\t\t\treturn <ThreadStackPin editor={editor} threads={stackThreads} {...props} />\n\t\t}\n\t\treturn <ThreadPin editor={editor} thread={thread} {...props} />\n\t}\n\n\t// Portalled rather than rendered into this component's slot: pins sit below the collaborator\n\t// cursors and popovers above the UI panels, and no single canvas layer spans both. The portal\n\t// also fixes where the layer lands among the container's children, keeping it behind the UI's\n\t// skip link in the tab order.\n\treturn (\n\t\t<EditorPortal>\n\t\t\t{/* Wheel pass-through lives on each interactive element, not this root: the root spans the\n\t\t\t whole canvas, so a pin past its bottom/right edge inflates scrollHeight and the wheel\n\t\t\t hook's is-this-scrollable guard silently disables pass-through. */}\n\t\t\t<div className=\"tlui-cmt-canvas-layer\">\n\t\t\t\t{options.enableClustering ? (\n\t\t\t\t\t<>\n\t\t\t\t\t\t{fadeNodes.map(({ node, phase }) => {\n\t\t\t\t\t\t\tlet content: ReactNode\n\t\t\t\t\t\t\tconst stackGroup = node.count > 1 ? stackGroupOf(node) : null\n\t\t\t\t\t\t\tif (node.count === 1) {\n\t\t\t\t\t\t\t\tconst thread = threadsById.get(node.id)\n\t\t\t\t\t\t\t\tif (!thread) return null\n\t\t\t\t\t\t\t\tcontent = renderThreadPin(thread)\n\t\t\t\t\t\t\t} else if (stackGroup) {\n\t\t\t\t\t\t\t\t// Routed through the stack's owner so the open/orphan/held slots stay deduped:\n\t\t\t\t\t\t\t\t// when the owner is one of them, that slot draws the stack and this draws nothing.\n\t\t\t\t\t\t\t\tconst owner = stackGroup.find((id) => renderedThreadIds.has(id))\n\t\t\t\t\t\t\t\tcontent =\n\t\t\t\t\t\t\t\t\towner && node.members.includes(owner)\n\t\t\t\t\t\t\t\t\t\t? renderThreadPin(threadsById.get(owner)!)\n\t\t\t\t\t\t\t\t\t\t: null\n\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\tcontent = (\n\t\t\t\t\t\t\t\t\t<ClusterBadge\n\t\t\t\t\t\t\t\t\t\teditor={editor}\n\t\t\t\t\t\t\t\t\t\tnode={node}\n\t\t\t\t\t\t\t\t\t\tonExpand={expandCluster}\n\t\t\t\t\t\t\t\t\t\tonSelectThread={revealClusteredThread}\n\t\t\t\t\t\t\t\t\t\tthreadsById={threadsById}\n\t\t\t\t\t\t\t\t\t\tcurrentUserId={props.currentUserId}\n\t\t\t\t\t\t\t\t\t\tresolveAuthor={props.resolveAuthor}\n\t\t\t\t\t\t\t\t\t/>\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn (\n\t\t\t\t\t\t\t\t<div key={`cluster-fade:${node.id}`} className={clusterFadeClassName(phase)}>\n\t\t\t\t\t\t\t\t\t{content}\n\t\t\t\t\t\t\t\t</div>\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t})}\n\t\t\t\t\t\t{orphanThreads.map((thread) => (\n\t\t\t\t\t\t\t<Fragment key={thread.id}>{renderThreadPin(thread)}</Fragment>\n\t\t\t\t\t\t))}\n\t\t\t\t\t\t{heldThreads.map((thread) => (\n\t\t\t\t\t\t\t<Fragment key={thread.id}>{renderThreadPin(thread)}</Fragment>\n\t\t\t\t\t\t))}\n\t\t\t\t\t</>\n\t\t\t\t) : (\n\t\t\t\t\t// Clustering off: every thread renders its own pin. The open thread is excluded and\n\t\t\t\t\t// rendered once below, or it would mount a second, stacked pin.\n\t\t\t\t\tthreads\n\t\t\t\t\t\t.filter((thread) => thread.id !== openId)\n\t\t\t\t\t\t.map((thread) => <Fragment key={thread.id}>{renderThreadPin(thread)}</Fragment>)\n\t\t\t\t)}\n\t\t\t\t{openThread && (\n\t\t\t\t\t<Fragment key={`open:${openThread.id}`}>{renderThreadPin(openThread)}</Fragment>\n\t\t\t\t)}\n\t\t\t\t<RegionDraftBox editor={editor} />\n\t\t\t\t{/* Keep the region visible while composing \u2014 the drag draft is gone by now, and no thread\n\t\t\t\t exists yet, so the pending anchor is what shows the area under the open composer. */}\n\t\t\t\t{pending?.anchor.type === 'region' && showPendingComposer && (\n\t\t\t\t\t<RegionBox editor={editor} box={pending.anchor} />\n\t\t\t\t)}\n\t\t\t\t{pending && showPendingComposer && (\n\t\t\t\t\t<PendingComposer editor={editor} pending={pending} {...props} />\n\t\t\t\t)}\n\t\t\t</div>\n\t\t</EditorPortal>\n\t)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -92,6 +92,7 @@ function PendingComposer({
|
|
|
92
92
|
setText(import_comment_extensions.EMPTY_COMMENT);
|
|
93
93
|
(0, import_comment_drafts.clearCommentDraft)(import_comment_drafts.NEW_COMMENT_DRAFT);
|
|
94
94
|
import_state.pendingComment.set(editor, null);
|
|
95
|
+
if (editor.isIn("comment.idle")) editor.setCurrentTool("select");
|
|
95
96
|
onPostComment?.(comment);
|
|
96
97
|
};
|
|
97
98
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_tldraw.EditorPortal, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/canvas/pending-composer.tsx"],
|
|
4
|
-
"sourcesContent": ["import { Avatar, isMentionPickerOpen } from '@tldraw/mentions'\nimport { useEffect, useRef, useState } from 'react'\nimport {\n\tcreateComment,\n\tcreateCommentThread,\n\tEditor,\n\tEditorPortal,\n\tTLRichText,\n\tusePassThroughWheelEvents,\n\tuseTranslation,\n\tuseValue,\n} from 'tldraw'\nimport { CommentComposer } from '../ui/comment-composer'\nimport { EMPTY_COMMENT, isCommentEmpty } from '../ui/comment-extensions'\nimport { CommentPin } from '../ui/comment-pin'\nimport {\n\tclearCommentDraft,\n\tgetCommentDraft,\n\tNEW_COMMENT_DRAFT,\n\tsaveCommentDraft,\n} from './comment-drafts'\nimport { commitCommentMutation } from './comment-mutations'\nimport { UNKNOWN_COMMENT_AUTHOR } from './comment-render'\nimport { PendingComment } from './comment-tool'\nimport { type CommentingContext } from './context'\nimport { useIsMobileCommenting, useMobilePlacement } from './mobile-placement'\nimport { useCanComment, useCommentingOptions } from './options'\nimport { pendingComment } from './state'\n\nconst stop = (e: { stopPropagation(): void }) => e.stopPropagation()\n\n/**\n * The composer for a thread that doesn't exist yet: the comment tool has placed a point (or\n * region) and is waiting on a first comment. Submitting creates the thread and its comment\n * together; clicking away keeps the draft for the next placement.\n */\nexport function PendingComposer({\n\teditor,\n\tpending,\n\tcurrentUserId,\n\tresolveAuthor,\n\tonPostComment,\n\tgetMentionSuggestions,\n\trenderMentionSuggestion,\n}: CommentingContext & { editor: Editor; pending: PendingComment }) {\n\tconst ComposerFallback = useCommentingOptions().components.ComposerFallback\n\tconst canComment = useCanComment(currentUserId)\n\tconst me = currentUserId ? resolveAuthor(currentUserId) : undefined\n\t// The leading pin previews the pin this draft becomes: a white pin holding the author's avatar.\n\tconst draftAvatar = (\n\t\t<CommentPin>\n\t\t\t<Avatar author={me ?? UNKNOWN_COMMENT_AUTHOR} />\n\t\t</CommentPin>\n\t)\n\t// Click-away keeps the draft (saved on every change) and the next placement composer\n\t// restores it \u2014 the flip side of dismissing without a discard warning.\n\tconst [text, setText] = useState<TLRichText>(\n\t\t() => getCommentDraft(NEW_COMMENT_DRAFT) ?? EMPTY_COMMENT\n\t)\n\tconst ref = useRef<HTMLDivElement>(null)\n\tconst msg = useTranslation()\n\t// Over this floating panel, a scroll reaches the canvas (except where it scrolls itself).\n\tusePassThroughWheelEvents(ref)\n\n\tconst point = useValue('composer point', () => editor.pageToViewport(pending.point), [\n\t\teditor,\n\t\tpending.point,\n\t])\n\t// On mobile the composer floats free of the pin so it can clear the software keyboard; desktop\n\t// keeps it pinned to the point.\n\tconst isMobile = useIsMobileCommenting()\n\tconst placed = useMobilePlacement(ref, point, isMobile)\n\n\t// Dismiss on a click anywhere outside the composer (capture-phase, ahead of stopPropagation).\n\tuseEffect(() => {\n\t\tconst onPointerDown = (e: PointerEvent) => {\n\t\t\tconst el = ref.current\n\t\t\tconst target = e.target as HTMLElement | null\n\t\t\tif (!el || !target) return\n\t\t\t// A click in the composer, or in the mention picker it spawns (portaled elsewhere), is\n\t\t\t// not \"outside\" \u2014 keep the draft open so the pick can insert.\n\t\t\tif (el.contains(target) || target.closest('.tlui-cmt-mention-popup')) return\n\t\t\tpendingComment.set(editor, null)\n\t\t}\n\t\tdocument.addEventListener('pointerdown', onPointerDown, true)\n\t\treturn () => document.removeEventListener('pointerdown', onPointerDown, true)\n\t}, [editor])\n\n\tconst submit = () => {\n\t\tif (isCommentEmpty(text) || !currentUserId) return\n\t\tconst comment = commitCommentMutation(editor, ({ put }) => {\n\t\t\tconst pageId = editor.getCurrentPageId()\n\t\t\tconst thread = createCommentThread({\n\t\t\t\tpageId,\n\t\t\t\tanchor: pending.anchor,\n\t\t\t\tcreatedBy: currentUserId,\n\t\t\t})\n\t\t\tconst comment = createComment({\n\t\t\t\tthreadId: thread.id,\n\t\t\t\tpageId,\n\t\t\t\tauthorId: currentUserId,\n\t\t\t\tbody: text,\n\t\t\t})\n\t\t\tput([thread, comment])\n\t\t\treturn comment\n\t\t})\n\t\tsetText(EMPTY_COMMENT)\n\t\tclearCommentDraft(NEW_COMMENT_DRAFT)\n\t\tpendingComment.set(editor, null)\n\t\t// The host's callback is its own operation, not part of the post's history scope. It runs\n\t\t// last so a throwing host can't strand the composer holding a draft of a posted comment.\n\t\tonPostComment?.(comment)\n\t}\n\n\treturn (\n\t\t<EditorPortal>\n\t\t\t<div\n\t\t\t\tref={ref}\n\t\t\t\tclassName={[\n\t\t\t\t\t'tlui-cmt-canvas-composer',\n\t\t\t\t\tpending.anchor.type === 'region' && 'tlui-cmt-canvas-composer--region',\n\t\t\t\t\t!canComment && 'tlui-cmt-canvas-composer--fallback',\n\t\t\t\t]\n\t\t\t\t\t.filter(Boolean)\n\t\t\t\t\t.join(' ')}\n\t\t\t\tstyle={{ left: placed.left, top: placed.top }}\n\t\t\t\tonPointerDown={stop}\n\t\t\t\tonContextMenu={stop}\n\t\t\t\tonKeyDown={(e) => {\n\t\t\t\t\tif (e.key === 'Escape' && !isMentionPickerOpen()) pendingComment.set(editor, null)\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{canComment ? (\n\t\t\t\t\t<CommentComposer\n\t\t\t\t\t\tauthor={me ?? UNKNOWN_COMMENT_AUTHOR}\n\t\t\t\t\t\tplaceholder={msg('comments.add-placeholder')}\n\t\t\t\t\t\tsendLabel={msg('comments.send')}\n\t\t\t\t\t\tvalue={text}\n\t\t\t\t\t\tonChange={(value) => {\n\t\t\t\t\t\t\tsetText(value)\n\t\t\t\t\t\t\tsaveCommentDraft(NEW_COMMENT_DRAFT, value)\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tonSubmit={submit}\n\t\t\t\t\t\t// No user, no author for the record \u2014 dead send button.\n\t\t\t\t\t\tdisabled={isCommentEmpty(text) || !currentUserId}\n\t\t\t\t\t\tgetMentionSuggestions={getMentionSuggestions}\n\t\t\t\t\t\trenderMentionSuggestion={renderMentionSuggestion}\n\t\t\t\t\t\tautoFocus\n\t\t\t\t\t\tleading={draftAvatar}\n\t\t\t\t\t/>\n\t\t\t\t) : (\n\t\t\t\t\tComposerFallback && <ComposerFallback context=\"pending\" />\n\t\t\t\t)}\n\t\t\t</div>\n\t\t</EditorPortal>\n\t)\n}\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAmDG;AAnDH,sBAA4C;AAC5C,mBAA4C;AAC5C,oBASO;AACP,8BAAgC;AAChC,gCAA8C;AAC9C,yBAA2B;AAC3B,4BAKO;AACP,+BAAsC;AACtC,4BAAuC;AAGvC,8BAA0D;AAC1D,qBAAoD;AACpD,mBAA+B;AAE/B,MAAM,OAAO,CAAC,MAAmC,EAAE,gBAAgB;AAO5D,SAAS,gBAAgB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACD,GAAoE;AACnE,QAAM,uBAAmB,qCAAqB,EAAE,WAAW;AAC3D,QAAM,iBAAa,8BAAc,aAAa;AAC9C,QAAM,KAAK,gBAAgB,cAAc,aAAa,IAAI;AAE1D,QAAM,cACL,4CAAC,iCACA,sDAAC,0BAAO,QAAQ,MAAM,8CAAwB,GAC/C;AAID,QAAM,CAAC,MAAM,OAAO,QAAI;AAAA,IACvB,UAAM,uCAAgB,uCAAiB,KAAK;AAAA,EAC7C;AACA,QAAM,UAAM,qBAAuB,IAAI;AACvC,QAAM,UAAM,8BAAe;AAE3B,+CAA0B,GAAG;AAE7B,QAAM,YAAQ,wBAAS,kBAAkB,MAAM,OAAO,eAAe,QAAQ,KAAK,GAAG;AAAA,IACpF;AAAA,IACA,QAAQ;AAAA,EACT,CAAC;AAGD,QAAM,eAAW,+CAAsB;AACvC,QAAM,aAAS,4CAAmB,KAAK,OAAO,QAAQ;AAGtD,8BAAU,MAAM;AACf,UAAM,gBAAgB,CAAC,MAAoB;AAC1C,YAAM,KAAK,IAAI;AACf,YAAM,SAAS,EAAE;AACjB,UAAI,CAAC,MAAM,CAAC,OAAQ;AAGpB,UAAI,GAAG,SAAS,MAAM,KAAK,OAAO,QAAQ,yBAAyB,EAAG;AACtE,kCAAe,IAAI,QAAQ,IAAI;AAAA,IAChC;AACA,aAAS,iBAAiB,eAAe,eAAe,IAAI;AAC5D,WAAO,MAAM,SAAS,oBAAoB,eAAe,eAAe,IAAI;AAAA,EAC7E,GAAG,CAAC,MAAM,CAAC;AAEX,QAAM,SAAS,MAAM;AACpB,YAAI,0CAAe,IAAI,KAAK,CAAC,cAAe;AAC5C,UAAM,cAAU,gDAAsB,QAAQ,CAAC,EAAE,IAAI,MAAM;AAC1D,YAAM,SAAS,OAAO,iBAAiB;AACvC,YAAM,aAAS,mCAAoB;AAAA,QAClC;AAAA,QACA,QAAQ,QAAQ;AAAA,QAChB,WAAW;AAAA,MACZ,CAAC;AACD,YAAMA,eAAU,6BAAc;AAAA,QAC7B,UAAU,OAAO;AAAA,QACjB;AAAA,QACA,UAAU;AAAA,QACV,MAAM;AAAA,MACP,CAAC;AACD,UAAI,CAAC,QAAQA,QAAO,CAAC;AACrB,aAAOA;AAAA,IACR,CAAC;AACD,YAAQ,uCAAa;AACrB,iDAAkB,uCAAiB;AACnC,gCAAe,IAAI,QAAQ,IAAI;
|
|
4
|
+
"sourcesContent": ["import { Avatar, isMentionPickerOpen } from '@tldraw/mentions'\nimport { useEffect, useRef, useState } from 'react'\nimport {\n\tcreateComment,\n\tcreateCommentThread,\n\tEditor,\n\tEditorPortal,\n\tTLRichText,\n\tusePassThroughWheelEvents,\n\tuseTranslation,\n\tuseValue,\n} from 'tldraw'\nimport { CommentComposer } from '../ui/comment-composer'\nimport { EMPTY_COMMENT, isCommentEmpty } from '../ui/comment-extensions'\nimport { CommentPin } from '../ui/comment-pin'\nimport {\n\tclearCommentDraft,\n\tgetCommentDraft,\n\tNEW_COMMENT_DRAFT,\n\tsaveCommentDraft,\n} from './comment-drafts'\nimport { commitCommentMutation } from './comment-mutations'\nimport { UNKNOWN_COMMENT_AUTHOR } from './comment-render'\nimport { PendingComment } from './comment-tool'\nimport { type CommentingContext } from './context'\nimport { useIsMobileCommenting, useMobilePlacement } from './mobile-placement'\nimport { useCanComment, useCommentingOptions } from './options'\nimport { pendingComment } from './state'\n\nconst stop = (e: { stopPropagation(): void }) => e.stopPropagation()\n\n/**\n * The composer for a thread that doesn't exist yet: the comment tool has placed a point (or\n * region) and is waiting on a first comment. Submitting creates the thread and its comment\n * together; clicking away keeps the draft for the next placement.\n */\nexport function PendingComposer({\n\teditor,\n\tpending,\n\tcurrentUserId,\n\tresolveAuthor,\n\tonPostComment,\n\tgetMentionSuggestions,\n\trenderMentionSuggestion,\n}: CommentingContext & { editor: Editor; pending: PendingComment }) {\n\tconst ComposerFallback = useCommentingOptions().components.ComposerFallback\n\tconst canComment = useCanComment(currentUserId)\n\tconst me = currentUserId ? resolveAuthor(currentUserId) : undefined\n\t// The leading pin previews the pin this draft becomes: a white pin holding the author's avatar.\n\tconst draftAvatar = (\n\t\t<CommentPin>\n\t\t\t<Avatar author={me ?? UNKNOWN_COMMENT_AUTHOR} />\n\t\t</CommentPin>\n\t)\n\t// Click-away keeps the draft (saved on every change) and the next placement composer\n\t// restores it \u2014 the flip side of dismissing without a discard warning.\n\tconst [text, setText] = useState<TLRichText>(\n\t\t() => getCommentDraft(NEW_COMMENT_DRAFT) ?? EMPTY_COMMENT\n\t)\n\tconst ref = useRef<HTMLDivElement>(null)\n\tconst msg = useTranslation()\n\t// Over this floating panel, a scroll reaches the canvas (except where it scrolls itself).\n\tusePassThroughWheelEvents(ref)\n\n\tconst point = useValue('composer point', () => editor.pageToViewport(pending.point), [\n\t\teditor,\n\t\tpending.point,\n\t])\n\t// On mobile the composer floats free of the pin so it can clear the software keyboard; desktop\n\t// keeps it pinned to the point.\n\tconst isMobile = useIsMobileCommenting()\n\tconst placed = useMobilePlacement(ref, point, isMobile)\n\n\t// Dismiss on a click anywhere outside the composer (capture-phase, ahead of stopPropagation).\n\tuseEffect(() => {\n\t\tconst onPointerDown = (e: PointerEvent) => {\n\t\t\tconst el = ref.current\n\t\t\tconst target = e.target as HTMLElement | null\n\t\t\tif (!el || !target) return\n\t\t\t// A click in the composer, or in the mention picker it spawns (portaled elsewhere), is\n\t\t\t// not \"outside\" \u2014 keep the draft open so the pick can insert.\n\t\t\tif (el.contains(target) || target.closest('.tlui-cmt-mention-popup')) return\n\t\t\tpendingComment.set(editor, null)\n\t\t}\n\t\tdocument.addEventListener('pointerdown', onPointerDown, true)\n\t\treturn () => document.removeEventListener('pointerdown', onPointerDown, true)\n\t}, [editor])\n\n\tconst submit = () => {\n\t\tif (isCommentEmpty(text) || !currentUserId) return\n\t\tconst comment = commitCommentMutation(editor, ({ put }) => {\n\t\t\tconst pageId = editor.getCurrentPageId()\n\t\t\tconst thread = createCommentThread({\n\t\t\t\tpageId,\n\t\t\t\tanchor: pending.anchor,\n\t\t\t\tcreatedBy: currentUserId,\n\t\t\t})\n\t\t\tconst comment = createComment({\n\t\t\t\tthreadId: thread.id,\n\t\t\t\tpageId,\n\t\t\t\tauthorId: currentUserId,\n\t\t\t\tbody: text,\n\t\t\t})\n\t\t\tput([thread, comment])\n\t\t\treturn comment\n\t\t})\n\t\tsetText(EMPTY_COMMENT)\n\t\tclearCommentDraft(NEW_COMMENT_DRAFT)\n\t\tpendingComment.set(editor, null)\n\t\t// Posting ends the placement interaction \u2014 hand the comment tool back to select. Only from\n\t\t// the settled idle state: Enter can land while a fresh press is mid-gesture (the composer\n\t\t// trails the pointer), and switching tools under a held pointer would strand the gesture.\n\t\tif (editor.isIn('comment.idle')) editor.setCurrentTool('select')\n\t\t// The host's callback is its own operation, not part of the post's history scope. It runs\n\t\t// last so a throwing host can't strand the composer holding a draft of a posted comment.\n\t\tonPostComment?.(comment)\n\t}\n\n\treturn (\n\t\t<EditorPortal>\n\t\t\t<div\n\t\t\t\tref={ref}\n\t\t\t\tclassName={[\n\t\t\t\t\t'tlui-cmt-canvas-composer',\n\t\t\t\t\tpending.anchor.type === 'region' && 'tlui-cmt-canvas-composer--region',\n\t\t\t\t\t!canComment && 'tlui-cmt-canvas-composer--fallback',\n\t\t\t\t]\n\t\t\t\t\t.filter(Boolean)\n\t\t\t\t\t.join(' ')}\n\t\t\t\tstyle={{ left: placed.left, top: placed.top }}\n\t\t\t\tonPointerDown={stop}\n\t\t\t\tonContextMenu={stop}\n\t\t\t\tonKeyDown={(e) => {\n\t\t\t\t\tif (e.key === 'Escape' && !isMentionPickerOpen()) pendingComment.set(editor, null)\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{canComment ? (\n\t\t\t\t\t<CommentComposer\n\t\t\t\t\t\tauthor={me ?? UNKNOWN_COMMENT_AUTHOR}\n\t\t\t\t\t\tplaceholder={msg('comments.add-placeholder')}\n\t\t\t\t\t\tsendLabel={msg('comments.send')}\n\t\t\t\t\t\tvalue={text}\n\t\t\t\t\t\tonChange={(value) => {\n\t\t\t\t\t\t\tsetText(value)\n\t\t\t\t\t\t\tsaveCommentDraft(NEW_COMMENT_DRAFT, value)\n\t\t\t\t\t\t}}\n\t\t\t\t\t\tonSubmit={submit}\n\t\t\t\t\t\t// No user, no author for the record \u2014 dead send button.\n\t\t\t\t\t\tdisabled={isCommentEmpty(text) || !currentUserId}\n\t\t\t\t\t\tgetMentionSuggestions={getMentionSuggestions}\n\t\t\t\t\t\trenderMentionSuggestion={renderMentionSuggestion}\n\t\t\t\t\t\tautoFocus\n\t\t\t\t\t\tleading={draftAvatar}\n\t\t\t\t\t/>\n\t\t\t\t) : (\n\t\t\t\t\tComposerFallback && <ComposerFallback context=\"pending\" />\n\t\t\t\t)}\n\t\t\t</div>\n\t\t</EditorPortal>\n\t)\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["comment"]
|
|
7
7
|
}
|
|
@@ -210,7 +210,7 @@ const ThreadPin = (0, import_react.memo)(function ThreadPin2({
|
|
|
210
210
|
};
|
|
211
211
|
const livePinPage = resizeBounds ? (0, import_thread_state.regionPinPoint)(resizeBounds, pinCorner) : dragPagePoint;
|
|
212
212
|
const renderPointBase = livePinPage ? editor.pageToViewport(livePinPage) : point;
|
|
213
|
-
const renderPoint = isRegion ? { x: renderPointBase.x -
|
|
213
|
+
const renderPoint = isRegion ? { x: renderPointBase.x - 14, y: renderPointBase.y + 14 } : renderPointBase;
|
|
214
214
|
const regionAnchor = thread.anchor.type === "region" ? thread.anchor : void 0;
|
|
215
215
|
const movedRegion = regionAnchor && dragPagePoint ? {
|
|
216
216
|
...regionAnchor,
|