@tldraw/commenting 0.0.0-bootstrap → 5.3.0-internal.1640468db8fd
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commenting.css +284 -153
- package/dist-cjs/canvas/comment-body.js +1 -1
- package/dist-cjs/canvas/comment-body.js.map +2 -2
- package/dist-cjs/canvas/comment-drafts.js +54 -0
- package/dist-cjs/canvas/comment-drafts.js.map +7 -0
- package/dist-cjs/canvas/comment-render.js +2 -0
- package/dist-cjs/canvas/comment-render.js.map +2 -2
- package/dist-cjs/canvas/comment-tool.js +65 -12
- package/dist-cjs/canvas/comment-tool.js.map +2 -2
- package/dist-cjs/canvas/comments-filter-menu.js +54 -63
- package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
- package/dist-cjs/canvas/comments-overflow-menu.js +21 -27
- package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
- package/dist-cjs/canvas/comments-overlay.js +304 -112
- package/dist-cjs/canvas/comments-overlay.js.map +2 -2
- package/dist-cjs/canvas/comments-sidebar.js +12 -18
- package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
- package/dist-cjs/canvas/comments-visibility-toggle.js +66 -0
- package/dist-cjs/canvas/comments-visibility-toggle.js.map +7 -0
- package/dist-cjs/canvas/options.js +1 -0
- package/dist-cjs/canvas/options.js.map +2 -2
- package/dist-cjs/canvas/sidebar-filters.js +2 -2
- package/dist-cjs/canvas/sidebar-filters.js.map +2 -2
- package/dist-cjs/canvas/state.js +11 -0
- package/dist-cjs/canvas/state.js.map +2 -2
- package/dist-cjs/canvas/thread-state.js +8 -1
- package/dist-cjs/canvas/thread-state.js.map +2 -2
- package/dist-cjs/clustering/runtime.js +119 -7
- package/dist-cjs/clustering/runtime.js.map +2 -2
- package/dist-cjs/index.d.ts +126 -41
- package/dist-cjs/index.js +7 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/ui/avatar.js +14 -6
- package/dist-cjs/ui/avatar.js.map +2 -2
- package/dist-cjs/ui/byline.js +4 -4
- package/dist-cjs/ui/byline.js.map +2 -2
- package/dist-cjs/ui/comment-author.js +17 -0
- package/dist-cjs/ui/comment-author.js.map +7 -0
- package/dist-cjs/ui/comment-card.js +4 -4
- package/dist-cjs/ui/comment-card.js.map +2 -2
- package/dist-cjs/ui/comment-composer.js +92 -29
- package/dist-cjs/ui/comment-composer.js.map +2 -2
- package/dist-cjs/ui/comment-mention.js +1 -1
- package/dist-cjs/ui/comment-mention.js.map +2 -2
- package/dist-cjs/ui/comment-pin.js +23 -3
- package/dist-cjs/ui/comment-pin.js.map +2 -2
- package/dist-cjs/ui/comment-text.js +1 -1
- package/dist-cjs/ui/comment-text.js.map +2 -2
- package/dist-cjs/ui/comment-thread.js +6 -6
- package/dist-cjs/ui/comment-thread.js.map +2 -2
- package/dist-cjs/ui/comments-list.js +12 -12
- package/dist-cjs/ui/comments-list.js.map +2 -2
- package/dist-cjs/ui/count-badge.js +1 -1
- package/dist-cjs/ui/count-badge.js.map +2 -2
- package/dist-cjs/ui/empty-state.js +3 -3
- package/dist-cjs/ui/empty-state.js.map +2 -2
- package/dist-cjs/ui/format-time.js +3 -0
- package/dist-cjs/ui/format-time.js.map +2 -2
- package/dist-cjs/ui/mention-list.js +8 -8
- package/dist-cjs/ui/mention-list.js.map +2 -2
- package/dist-cjs/ui/mention-suggestion.js +3 -3
- package/dist-cjs/ui/mention-suggestion.js.map +2 -2
- package/dist-cjs/ui/mention.js +1 -1
- package/dist-cjs/ui/mention.js.map +2 -2
- package/dist-cjs/ui/reaction.js +11 -4
- package/dist-cjs/ui/reaction.js.map +2 -2
- package/dist-cjs/ui/reactions.js +10 -2
- package/dist-cjs/ui/reactions.js.map +2 -2
- package/dist-cjs/ui/send-button.js +1 -1
- package/dist-cjs/ui/send-button.js.map +2 -2
- package/dist-cjs/ui/tooltip-button.js +32 -0
- package/dist-cjs/ui/tooltip-button.js.map +7 -0
- package/dist-esm/canvas/comment-body.mjs +1 -1
- package/dist-esm/canvas/comment-body.mjs.map +2 -2
- package/dist-esm/canvas/comment-drafts.mjs +34 -0
- package/dist-esm/canvas/comment-drafts.mjs.map +7 -0
- package/dist-esm/canvas/comment-render.mjs +2 -0
- package/dist-esm/canvas/comment-render.mjs.map +2 -2
- package/dist-esm/canvas/comment-tool.mjs +67 -14
- package/dist-esm/canvas/comment-tool.mjs.map +2 -2
- package/dist-esm/canvas/comments-filter-menu.mjs +54 -63
- package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
- package/dist-esm/canvas/comments-overflow-menu.mjs +22 -27
- package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
- package/dist-esm/canvas/comments-overlay.mjs +322 -114
- package/dist-esm/canvas/comments-overlay.mjs.map +2 -2
- package/dist-esm/canvas/comments-sidebar.mjs +15 -21
- package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
- package/dist-esm/canvas/comments-visibility-toggle.mjs +46 -0
- package/dist-esm/canvas/comments-visibility-toggle.mjs.map +7 -0
- package/dist-esm/canvas/options.mjs +1 -0
- package/dist-esm/canvas/options.mjs.map +2 -2
- package/dist-esm/canvas/sidebar-filters.mjs +2 -2
- package/dist-esm/canvas/sidebar-filters.mjs.map +2 -2
- package/dist-esm/canvas/state.mjs +11 -0
- package/dist-esm/canvas/state.mjs.map +2 -2
- package/dist-esm/canvas/thread-state.mjs +8 -1
- package/dist-esm/canvas/thread-state.mjs.map +2 -2
- package/dist-esm/clustering/runtime.mjs +119 -7
- package/dist-esm/clustering/runtime.mjs.map +2 -2
- package/dist-esm/index.d.mts +126 -41
- package/dist-esm/index.mjs +11 -1
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/ui/avatar.mjs +14 -6
- package/dist-esm/ui/avatar.mjs.map +2 -2
- package/dist-esm/ui/byline.mjs +4 -4
- package/dist-esm/ui/byline.mjs.map +2 -2
- package/dist-esm/ui/comment-author.mjs +1 -0
- package/dist-esm/ui/comment-author.mjs.map +7 -0
- package/dist-esm/ui/comment-card.mjs +4 -4
- package/dist-esm/ui/comment-card.mjs.map +2 -2
- package/dist-esm/ui/comment-composer.mjs +93 -30
- package/dist-esm/ui/comment-composer.mjs.map +2 -2
- package/dist-esm/ui/comment-mention.mjs +1 -1
- package/dist-esm/ui/comment-mention.mjs.map +2 -2
- package/dist-esm/ui/comment-pin.mjs +23 -3
- package/dist-esm/ui/comment-pin.mjs.map +2 -2
- package/dist-esm/ui/comment-text.mjs +1 -1
- package/dist-esm/ui/comment-text.mjs.map +2 -2
- package/dist-esm/ui/comment-thread.mjs +6 -6
- package/dist-esm/ui/comment-thread.mjs.map +2 -2
- package/dist-esm/ui/comments-list.mjs +12 -12
- package/dist-esm/ui/comments-list.mjs.map +2 -2
- package/dist-esm/ui/count-badge.mjs +1 -1
- package/dist-esm/ui/count-badge.mjs.map +2 -2
- package/dist-esm/ui/empty-state.mjs +3 -3
- package/dist-esm/ui/empty-state.mjs.map +2 -2
- package/dist-esm/ui/format-time.mjs +3 -0
- package/dist-esm/ui/format-time.mjs.map +2 -2
- package/dist-esm/ui/mention-list.mjs +8 -8
- package/dist-esm/ui/mention-list.mjs.map +2 -2
- package/dist-esm/ui/mention-suggestion.mjs +3 -3
- package/dist-esm/ui/mention-suggestion.mjs.map +2 -2
- package/dist-esm/ui/mention.mjs +1 -1
- package/dist-esm/ui/mention.mjs.map +2 -2
- package/dist-esm/ui/reaction.mjs +11 -4
- package/dist-esm/ui/reaction.mjs.map +2 -2
- package/dist-esm/ui/reactions.mjs +10 -2
- package/dist-esm/ui/reactions.mjs.map +2 -2
- package/dist-esm/ui/send-button.mjs +1 -1
- package/dist-esm/ui/send-button.mjs.map +2 -2
- package/dist-esm/ui/tooltip-button.mjs +12 -0
- package/dist-esm/ui/tooltip-button.mjs.map +7 -0
- package/package.json +3 -3
- package/src/canvas/canvas.css +79 -36
- package/src/canvas/comment-body.tsx +1 -1
- package/src/canvas/comment-drafts.test.ts +55 -0
- package/src/canvas/comment-drafts.ts +46 -0
- package/src/canvas/comment-render.test.ts +1 -1
- package/src/canvas/comment-render.ts +7 -2
- package/src/canvas/comment-tool.tsx +98 -17
- package/src/canvas/comments-filter-menu.tsx +27 -33
- package/src/canvas/comments-overflow-menu.tsx +13 -21
- package/src/canvas/comments-overlay.tsx +441 -145
- package/src/canvas/comments-sidebar.tsx +27 -28
- package/src/canvas/comments-visibility-toggle.tsx +50 -0
- package/src/canvas/options.ts +25 -0
- package/src/canvas/sidebar-filters.ts +4 -3
- package/src/canvas/state.ts +22 -0
- package/src/canvas/thread-state.ts +16 -3
- package/src/clustering/runtime.test.ts +183 -7
- package/src/clustering/runtime.ts +164 -15
- package/src/index.ts +7 -0
- package/src/ui/avatar.tsx +15 -11
- package/src/ui/byline.tsx +6 -5
- package/src/ui/comment-author.ts +15 -0
- package/src/ui/comment-card.tsx +6 -5
- package/src/ui/comment-composer.tsx +122 -33
- package/src/ui/comment-mention.ts +2 -2
- package/src/ui/comment-pin.tsx +35 -4
- package/src/ui/comment-text.tsx +1 -1
- package/src/ui/comment-thread.tsx +8 -6
- package/src/ui/comments-list.tsx +19 -14
- package/src/ui/comments.css +205 -117
- package/src/ui/count-badge.tsx +1 -1
- package/src/ui/empty-state.tsx +3 -3
- package/src/ui/format-time.ts +5 -0
- package/src/ui/mention-list.tsx +13 -16
- package/src/ui/mention-suggestion.tsx +3 -3
- package/src/ui/mention.tsx +1 -1
- package/src/ui/reaction.tsx +6 -3
- package/src/ui/reactions.tsx +6 -2
- package/src/ui/send-button.tsx +1 -1
- package/src/ui/tooltip-button.tsx +20 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ReactNode, useRef } from 'react'
|
|
1
|
+
import { ReactNode, useCallback, useRef } from 'react'
|
|
2
2
|
import { createPortal } from 'react-dom'
|
|
3
3
|
import {
|
|
4
4
|
TLComment,
|
|
@@ -9,21 +9,22 @@ import {
|
|
|
9
9
|
useTranslation,
|
|
10
10
|
useValue,
|
|
11
11
|
} from 'tldraw'
|
|
12
|
+
import { CommentAuthor } from '../ui/comment-author'
|
|
12
13
|
import { CommentListItemProps, CommentsList } from '../ui/comments-list'
|
|
13
|
-
import {
|
|
14
|
+
import { UNKNOWN_COMMENT_AUTHOR } from './comment-render'
|
|
14
15
|
import { CommentsFilterMenu } from './comments-filter-menu'
|
|
15
|
-
import {
|
|
16
|
+
import { CommentsVisibilityToggle } from './comments-visibility-toggle'
|
|
16
17
|
import { useComments, useCommentThreads } from './hooks'
|
|
17
18
|
import { useCommentingEnabled } from './license'
|
|
18
19
|
import { useCommentingOptions } from './options'
|
|
19
20
|
import { richTextToPlaintext } from './rich-text'
|
|
20
|
-
import { openThreadId, sidebarFilters } from './state'
|
|
21
|
+
import { commentsSidebarOpen, openThreadId, sidebarFilters } from './state'
|
|
21
22
|
import { focusThread } from './thread-state'
|
|
22
23
|
|
|
23
24
|
/** @public */
|
|
24
25
|
export interface CanvasCommentsSidebarProps {
|
|
25
|
-
/** Map an author id to
|
|
26
|
-
|
|
26
|
+
/** Map an author id to their display info, or `undefined` when the id can't be resolved. */
|
|
27
|
+
resolveAuthor(id: string): CommentAuthor | undefined
|
|
27
28
|
/** The signed-in user's id. Enables the "only your threads" filter when present. */
|
|
28
29
|
currentUserId?: string
|
|
29
30
|
/**
|
|
@@ -31,8 +32,6 @@ export interface CanvasCommentsSidebarProps {
|
|
|
31
32
|
* "only unread" filter when present.
|
|
32
33
|
*/
|
|
33
34
|
isCommentUnread?(commentId: TLCommentId): boolean
|
|
34
|
-
/** Tool ids that show the sidebar. Defaults to the comment tool. */
|
|
35
|
-
tools?: string[]
|
|
36
35
|
/** Header above the list. */
|
|
37
36
|
header?: ReactNode
|
|
38
37
|
/** Shown when the page has no threads. */
|
|
@@ -42,31 +41,26 @@ export interface CanvasCommentsSidebarProps {
|
|
|
42
41
|
}
|
|
43
42
|
|
|
44
43
|
/**
|
|
45
|
-
* A comments list panel for the current page, shown while
|
|
46
|
-
* thread brings its pin into view and opens it. Batteries-included
|
|
47
|
-
* `CanvasComments`); `CommentsList` is exported for a differently-placed
|
|
44
|
+
* A comments list panel for the current page, shown while {@link commentsSidebarOpen} is set (e.g.
|
|
45
|
+
* toggled by a button). Clicking a thread brings its pin into view and opens it. Batteries-included
|
|
46
|
+
* over the store (a sibling to `CanvasComments`); `CommentsList` is exported for a differently-placed
|
|
47
|
+
* or always-on list.
|
|
48
48
|
* @public @react
|
|
49
49
|
*/
|
|
50
50
|
export function CanvasCommentsSidebar(props: CanvasCommentsSidebarProps) {
|
|
51
|
-
const {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
tools,
|
|
56
|
-
header,
|
|
57
|
-
empty,
|
|
58
|
-
impreciseShapeAnchor,
|
|
59
|
-
} = props
|
|
51
|
+
const { resolveAuthor, currentUserId, isCommentUnread, header, empty, impreciseShapeAnchor } =
|
|
52
|
+
props
|
|
53
|
+
// Name-only view of the resolver, for the plaintext previews (which resolve @-mentions).
|
|
54
|
+
const resolveName = useCallback((id: string) => resolveAuthor(id)?.name, [resolveAuthor])
|
|
60
55
|
const editor = useEditor()
|
|
61
56
|
const options = useCommentingOptions()
|
|
62
|
-
const sidebarTools = tools ?? ['comment']
|
|
63
57
|
const container = useContainer()
|
|
64
58
|
const commentingEnabled = useCommentingEnabled()
|
|
65
59
|
const msg = useTranslation()
|
|
66
60
|
const threads = useCommentThreads(editor)
|
|
67
61
|
const comments = useComments(editor)
|
|
68
62
|
const currentPageId = useValue('page id', () => editor.getCurrentPageId(), [editor])
|
|
69
|
-
const
|
|
63
|
+
const open = useValue('sidebar open', () => commentsSidebarOpen.get(editor), [editor])
|
|
70
64
|
const openId = useValue('open thread', () => openThreadId.get(editor), [editor])
|
|
71
65
|
const filters = useValue('sidebar filters', () => sidebarFilters.get(editor), [editor])
|
|
72
66
|
const pageNames = useValue(
|
|
@@ -75,7 +69,7 @@ export function CanvasCommentsSidebar(props: CanvasCommentsSidebarProps) {
|
|
|
75
69
|
[editor]
|
|
76
70
|
)
|
|
77
71
|
|
|
78
|
-
if (!commentingEnabled || !
|
|
72
|
+
if (!commentingEnabled || !open) return null
|
|
79
73
|
|
|
80
74
|
// Group comments by thread (they arrive oldest-first, so [0] is each thread's first comment).
|
|
81
75
|
const byThread = new Map<string, TLComment[]>()
|
|
@@ -121,11 +115,16 @@ export function CanvasCommentsSidebar(props: CanvasCommentsSidebarProps) {
|
|
|
121
115
|
}
|
|
122
116
|
return {
|
|
123
117
|
id: thread.id,
|
|
124
|
-
author:
|
|
118
|
+
author: resolveAuthor(thread.createdBy) ?? UNKNOWN_COMMENT_AUTHOR,
|
|
125
119
|
preview,
|
|
126
120
|
date: new Date((first ?? thread).createdAt).toISOString(),
|
|
127
121
|
resolved: thread.resolved != null,
|
|
128
|
-
page:
|
|
122
|
+
// The page label only earns its place when it adds information: multiple pages
|
|
123
|
+
// exist, and the thread is somewhere other than where you already are.
|
|
124
|
+
page:
|
|
125
|
+
pageNames.size > 1 && thread.pageId !== currentPageId
|
|
126
|
+
? pageNames.get(thread.pageId)
|
|
127
|
+
: undefined,
|
|
129
128
|
count: threadComments.length,
|
|
130
129
|
selected: openId === thread.id,
|
|
131
130
|
}
|
|
@@ -149,12 +148,12 @@ export function CanvasCommentsSidebar(props: CanvasCommentsSidebarProps) {
|
|
|
149
148
|
items={items}
|
|
150
149
|
header={header ?? msg('comments.title')}
|
|
151
150
|
headerAction={
|
|
152
|
-
<div className="cmt-list__header-actions">
|
|
151
|
+
<div className="tlui-cmt-list__header-actions">
|
|
153
152
|
<CommentsFilterMenu
|
|
154
153
|
canFilterByAuthor={currentUserId !== undefined}
|
|
155
154
|
canFilterByUnread={isCommentUnread !== undefined}
|
|
156
155
|
/>
|
|
157
|
-
<
|
|
156
|
+
<CommentsVisibilityToggle />
|
|
158
157
|
</div>
|
|
159
158
|
}
|
|
160
159
|
empty={
|
|
@@ -176,7 +175,7 @@ function SidebarPanel({ container, children }: { container: HTMLElement; childre
|
|
|
176
175
|
const ref = useRef<HTMLDivElement>(null)
|
|
177
176
|
usePassThroughMouseOverEvents(ref)
|
|
178
177
|
return createPortal(
|
|
179
|
-
<div ref={ref} className="cmt-canvas-sidebar">
|
|
178
|
+
<div ref={ref} className="tlui-cmt-canvas-sidebar" onContextMenu={(e) => e.stopPropagation()}>
|
|
180
179
|
{children}
|
|
181
180
|
</div>,
|
|
182
181
|
container
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useEditor, useTranslation, useValue } from 'tldraw'
|
|
2
|
+
import { TooltipButton } from '../ui/tooltip-button'
|
|
3
|
+
import { commentsHidden, toggleCommentsHidden } from './state'
|
|
4
|
+
|
|
5
|
+
/** The sidebar header's show/hide toggle for comment pins — an eye that closes while comments
|
|
6
|
+
* are hidden. The same state as the Shift+C shortcut.
|
|
7
|
+
* @public @react */
|
|
8
|
+
export function CommentsVisibilityToggle() {
|
|
9
|
+
const editor = useEditor()
|
|
10
|
+
const msg = useTranslation()
|
|
11
|
+
const hidden = useValue('comments hidden', () => commentsHidden.get(editor), [editor])
|
|
12
|
+
const label = hidden ? msg('comments.show') : msg('comments.hide')
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<TooltipButton
|
|
16
|
+
tooltip={label}
|
|
17
|
+
className="tlui-cmt-header-btn"
|
|
18
|
+
aria-pressed={hidden}
|
|
19
|
+
onClick={() => toggleCommentsHidden(editor)}
|
|
20
|
+
>
|
|
21
|
+
{hidden ? <EyeClosedIcon /> : <EyeOpenIcon />}
|
|
22
|
+
</TooltipButton>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function EyeOpenIcon() {
|
|
27
|
+
return (
|
|
28
|
+
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" aria-hidden="true">
|
|
29
|
+
<path
|
|
30
|
+
d="M7.5 11C4.80285 11 2.52952 9.62184 1.09622 7.50001C2.52952 5.37816 4.80285 4 7.5 4C10.1971 4 12.4705 5.37816 13.9038 7.5C12.4705 9.62183 10.1971 11 7.5 11ZM7.5 3C4.30786 3 1.65639 4.70638 0.0760002 7.23501C-0.0253338 7.39715 -0.0253334 7.60288 0.0760014 7.76501C1.65639 10.2936 4.30786 12 7.5 12C10.6921 12 13.3436 10.2936 14.924 7.76501C15.0253 7.60288 15.0253 7.39715 14.924 7.23501C13.3436 4.70638 10.6921 3 7.5 3ZM7.5 9.5C8.60457 9.5 9.5 8.60457 9.5 7.5C9.5 6.39543 8.60457 5.5 7.5 5.5C6.39543 5.5 5.5 6.39543 5.5 7.5C5.5 8.60457 6.39543 9.5 7.5 9.5Z"
|
|
31
|
+
fill="currentColor"
|
|
32
|
+
fillRule="evenodd"
|
|
33
|
+
clipRule="evenodd"
|
|
34
|
+
/>
|
|
35
|
+
</svg>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function EyeClosedIcon() {
|
|
40
|
+
return (
|
|
41
|
+
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" aria-hidden="true">
|
|
42
|
+
<path
|
|
43
|
+
d="M14.7649 6.07596C14.9991 6.22231 15.0703 6.53079 14.9239 6.76495C14.4849 7.46743 13.9632 8.10645 13.3702 8.66305L14.5712 9.86406C14.7664 10.0593 14.7664 10.3759 14.5712 10.5712C14.3759 10.7664 14.0593 10.7664 13.8641 10.5712L12.6011 9.30817C11.805 9.90283 10.9089 10.3621 9.93375 10.651L10.383 12.3277C10.4544 12.5944 10.2961 12.8685 10.0294 12.94C9.76267 13.0115 9.4885 12.8532 9.41704 12.5865L8.95917 10.8775C8.48743 10.958 8.00036 11 7.50001 11C6.99965 11 6.51257 10.958 6.04082 10.8775L5.58299 12.5864C5.51153 12.8532 5.23737 13.0115 4.97064 12.94C4.7039 12.8686 4.5456 12.5944 4.61706 12.3277L5.06625 10.651C4.09111 10.3621 3.19503 9.90282 2.39889 9.30815L1.1359 10.5712C0.940638 10.7664 0.624058 10.7664 0.428798 10.5712C0.233537 10.3759 0.233537 10.0593 0.428798 9.86405L1.62982 8.66303C1.03682 8.10643 0.515113 7.46742 0.0760677 6.76495C-0.0702867 6.53079 0.000898544 6.22231 0.235065 6.07596C0.469231 5.9296 0.777703 6.00079 0.924058 6.23496C1.40354 7.00213 1.989 7.68057 2.66233 8.2427C2.67315 8.25096 2.6837 8.25972 2.69397 8.26898C4.00897 9.35527 5.65537 10 7.50001 10C10.3078 10 12.6564 8.5063 14.076 6.23495C14.2223 6.00079 14.5308 5.9296 14.7649 6.07596Z"
|
|
44
|
+
fill="currentColor"
|
|
45
|
+
fillRule="evenodd"
|
|
46
|
+
clipRule="evenodd"
|
|
47
|
+
/>
|
|
48
|
+
</svg>
|
|
49
|
+
)
|
|
50
|
+
}
|
package/src/canvas/options.ts
CHANGED
|
@@ -4,9 +4,24 @@ import {
|
|
|
4
4
|
type TLComment,
|
|
5
5
|
type TLCommentThread,
|
|
6
6
|
type TLHistoryBatchOptions,
|
|
7
|
+
type TLShapeId,
|
|
8
|
+
type VecLike,
|
|
7
9
|
useEditor,
|
|
8
10
|
} from 'tldraw'
|
|
9
11
|
|
|
12
|
+
/**
|
|
13
|
+
* The gesture that's creating a shape anchor, passed to
|
|
14
|
+
* {@link CommentingOptions.shouldBePrecise}: the target shape, the page point of the release, and
|
|
15
|
+
* whether Alt was held.
|
|
16
|
+
*
|
|
17
|
+
* @public
|
|
18
|
+
*/
|
|
19
|
+
export interface ShapeCommentPrecisionContext {
|
|
20
|
+
readonly shapeId: TLShapeId
|
|
21
|
+
readonly point: VecLike
|
|
22
|
+
readonly altKey: boolean
|
|
23
|
+
}
|
|
24
|
+
|
|
10
25
|
/**
|
|
11
26
|
* Component overrides for the batteries-included comments layer. Each slot replaces a built-in
|
|
12
27
|
* piece; leave a slot unset to keep its default.
|
|
@@ -59,6 +74,15 @@ export interface CommentingOptions {
|
|
|
59
74
|
// ── Anchoring ────────────────────────────────────────────────────────────────────────────
|
|
60
75
|
/** Normalized (0–1) spot within a shape where imprecise shape pins sit. Default top-right. */
|
|
61
76
|
readonly impreciseShapeAnchor: { readonly x: number; readonly y: number }
|
|
77
|
+
/**
|
|
78
|
+
* Whether a comment landing on a shape anchors precisely — pinned to the exact clicked spot
|
|
79
|
+
* within the shape — or imprecisely — pinned to the shape as a whole, rendered at
|
|
80
|
+
* `impreciseShapeAnchor`. Called wherever a shape anchor is created (placing with the comment
|
|
81
|
+
* tool, dropping a dragged pin onto a shape). Always precise by default. Return `false` for
|
|
82
|
+
* shape-level anchoring, or decide from the context — the Alt key's state, or the shape itself,
|
|
83
|
+
* e.g. precise only on notes. Governs new placements only; existing anchors render as stored.
|
|
84
|
+
*/
|
|
85
|
+
shouldBePrecise(editor: Editor, context: ShapeCommentPrecisionContext): boolean
|
|
62
86
|
|
|
63
87
|
// ── Clustering tuning ─────────────────────────────────────────────────────────────────────
|
|
64
88
|
/** Screen-pixel margin by which the viewport is inflated when culling cluster badges. */
|
|
@@ -81,6 +105,7 @@ export const defaultCommentingOptions = {
|
|
|
81
105
|
dragHistory: undefined,
|
|
82
106
|
enableClustering: true,
|
|
83
107
|
impreciseShapeAnchor: { x: 1, y: 0 },
|
|
108
|
+
shouldBePrecise: () => true,
|
|
84
109
|
clusterCullMargin: 120,
|
|
85
110
|
clusterSplitZoomFactor: 1.05,
|
|
86
111
|
components: {},
|
|
@@ -16,10 +16,11 @@ export interface SidebarFilters {
|
|
|
16
16
|
onlyCurrentPage: boolean
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
/**
|
|
19
|
+
/** The out-of-the-box view: every page's threads, resolved ones hidden until asked for.
|
|
20
|
+
* @public */
|
|
20
21
|
export const DEFAULT_SIDEBAR_FILTERS: SidebarFilters = {
|
|
21
|
-
showResolved:
|
|
22
|
+
showResolved: false,
|
|
22
23
|
onlyMine: false,
|
|
23
24
|
onlyUnread: false,
|
|
24
|
-
onlyCurrentPage:
|
|
25
|
+
onlyCurrentPage: false,
|
|
25
26
|
}
|
package/src/canvas/state.ts
CHANGED
|
@@ -36,6 +36,15 @@ export const regionDraft = new EditorAtom<BoxModel | null>('regionDraft', () =>
|
|
|
36
36
|
*/
|
|
37
37
|
export const commentsHidden = new EditorAtom<boolean>('commentsHidden', () => false)
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Whether the comments sidebar (the thread list) is open. Driven by an explicit control — a button
|
|
41
|
+
* next to Share on dotcom — rather than by which tool is active, so browsing threads is separate
|
|
42
|
+
* from placing them. The comment tool additionally closes it on enter, keeping placement
|
|
43
|
+
* canvas-focused.
|
|
44
|
+
* @public
|
|
45
|
+
*/
|
|
46
|
+
export const commentsSidebarOpen = new EditorAtom<boolean>('commentsSidebarOpen', () => false)
|
|
47
|
+
|
|
39
48
|
/** Which threads the comments sidebar shows.
|
|
40
49
|
* @public */
|
|
41
50
|
export const sidebarFilters = new EditorAtom<SidebarFilters>(
|
|
@@ -49,6 +58,12 @@ export function toggleCommentsHidden(editor: Editor): void {
|
|
|
49
58
|
commentsHidden.update(editor, (hidden) => !hidden)
|
|
50
59
|
}
|
|
51
60
|
|
|
61
|
+
/** Open or close the comments sidebar for an editor.
|
|
62
|
+
* @public */
|
|
63
|
+
export function toggleCommentsSidebar(editor: Editor): void {
|
|
64
|
+
commentsSidebarOpen.update(editor, (open) => !open)
|
|
65
|
+
}
|
|
66
|
+
|
|
52
67
|
/** React hook for the open thread id.
|
|
53
68
|
* @public */
|
|
54
69
|
export function useOpenThreadId(): string | null {
|
|
@@ -70,6 +85,13 @@ export function useCommentsHidden(): boolean {
|
|
|
70
85
|
return useValue('comments hidden', () => commentsHidden.get(editor), [editor])
|
|
71
86
|
}
|
|
72
87
|
|
|
88
|
+
/** React hook for whether the comments sidebar is open.
|
|
89
|
+
* @public */
|
|
90
|
+
export function useCommentsSidebarOpen(): boolean {
|
|
91
|
+
const editor = useEditor()
|
|
92
|
+
return useValue('comments sidebar open', () => commentsSidebarOpen.get(editor), [editor])
|
|
93
|
+
}
|
|
94
|
+
|
|
73
95
|
/** React hook for the current sidebar filters.
|
|
74
96
|
* @public */
|
|
75
97
|
export function useSidebarFilters(): SidebarFilters {
|
|
@@ -10,6 +10,18 @@ export const DEFAULT_IMPRECISE_SHAPE_ANCHOR = { x: 1, y: 0 }
|
|
|
10
10
|
* which corner has no resize handle all derive from the chosen corner. */
|
|
11
11
|
export const REGION_PIN_CORNER: VecLike = { x: 1, y: 1 }
|
|
12
12
|
|
|
13
|
+
/** A region anchor's pin corner: the corner its creating drag released on, when recorded, else
|
|
14
|
+
* the editor's configured default. @public */
|
|
15
|
+
export function regionAnchorPinCorner(
|
|
16
|
+
editor: Editor,
|
|
17
|
+
anchor: Extract<TLCommentAnchor, { type: 'region' }>
|
|
18
|
+
): VecLike {
|
|
19
|
+
if (anchor.pinX !== undefined && anchor.pinY !== undefined) {
|
|
20
|
+
return { x: anchor.pinX, y: anchor.pinY }
|
|
21
|
+
}
|
|
22
|
+
return getRegionCommentOptions(editor).pinCorner
|
|
23
|
+
}
|
|
24
|
+
|
|
13
25
|
/** The page point of a region's pin corner. */
|
|
14
26
|
export function regionPinPoint(region: BoxModel, corner: VecLike = REGION_PIN_CORNER): VecLike {
|
|
15
27
|
return {
|
|
@@ -45,7 +57,7 @@ export function anchorPagePoint(
|
|
|
45
57
|
case 'point':
|
|
46
58
|
return { x: anchor.x, y: anchor.y }
|
|
47
59
|
case 'region':
|
|
48
|
-
return regionPinPoint(anchor,
|
|
60
|
+
return regionPinPoint(anchor, regionAnchorPinCorner(editor, anchor))
|
|
49
61
|
case 'page':
|
|
50
62
|
return null
|
|
51
63
|
}
|
|
@@ -53,8 +65,9 @@ export function anchorPagePoint(
|
|
|
53
65
|
|
|
54
66
|
/**
|
|
55
67
|
* A shape anchor for a page point. `x`/`y` are the point's normalized (0–1) offset within the
|
|
56
|
-
* shape's page bounds, remembered either way. When `precise`
|
|
57
|
-
*
|
|
68
|
+
* shape's page bounds, remembered either way. When `precise` the pin sits at exactly `x`/`y`;
|
|
69
|
+
* otherwise it sits at the consumer's imprecise default (top-right out of the box). Placement
|
|
70
|
+
* gestures get `precise` from the `shouldBePrecise` commenting option (always precise, by default).
|
|
58
71
|
* @public
|
|
59
72
|
*/
|
|
60
73
|
export function shapeAnchorAt(
|
|
@@ -460,6 +460,7 @@ describe('createClusterRuntime seedFrom (carryover seeding)', () => {
|
|
|
460
460
|
])
|
|
461
461
|
)
|
|
462
462
|
expect(rt.k).toBe(1)
|
|
463
|
+
expect(rt.getSuppressedCount()).toBe(0)
|
|
463
464
|
expect(visibleIds(rt)).toEqual(['cluster:3:a', 'd'])
|
|
464
465
|
})
|
|
465
466
|
|
|
@@ -477,7 +478,8 @@ describe('createClusterRuntime seedFrom (carryover seeding)', () => {
|
|
|
477
478
|
[D.id, D],
|
|
478
479
|
])
|
|
479
480
|
)
|
|
480
|
-
expect(rt.k).toBe(
|
|
481
|
+
expect(rt.k).toBe(1)
|
|
482
|
+
expect(rt.getSuppressedCount()).toBe(1)
|
|
481
483
|
expect(visibleIds(rt)).toEqual(['a', 'b', 'c', 'd'])
|
|
482
484
|
})
|
|
483
485
|
|
|
@@ -512,7 +514,9 @@ describe('createClusterRuntime seedFrom (carryover seeding)', () => {
|
|
|
512
514
|
const table = microTraceTable()
|
|
513
515
|
const rt = createClusterRuntime(table)
|
|
514
516
|
rt.seedFrom(5, new Map())
|
|
515
|
-
expect(rt.k).toBe(
|
|
517
|
+
expect(rt.k).toBe(1)
|
|
518
|
+
expect(rt.getSuppressedCount()).toBe(1)
|
|
519
|
+
expect(visibleIds(rt)).toEqual(['a', 'b', 'c', 'd'])
|
|
516
520
|
const partial = node(['a', 'b'], 5, 0)
|
|
517
521
|
rt.seedFrom(
|
|
518
522
|
5,
|
|
@@ -522,7 +526,9 @@ describe('createClusterRuntime seedFrom (carryover seeding)', () => {
|
|
|
522
526
|
[D.id, D],
|
|
523
527
|
])
|
|
524
528
|
)
|
|
525
|
-
expect(rt.k).toBe(
|
|
529
|
+
expect(rt.k).toBe(1)
|
|
530
|
+
expect(rt.getSuppressedCount()).toBe(1)
|
|
531
|
+
expect(visibleIds(rt)).toEqual(['a', 'b', 'c', 'd'])
|
|
526
532
|
})
|
|
527
533
|
|
|
528
534
|
it('membership in a superset cluster counts as merged', () => {
|
|
@@ -534,10 +540,58 @@ describe('createClusterRuntime seedFrom (carryover seeding)', () => {
|
|
|
534
540
|
expect(visibleIds(rt)).toEqual(['cluster:4:a'])
|
|
535
541
|
})
|
|
536
542
|
|
|
537
|
-
it('
|
|
538
|
-
// Two independent band events at zoom 4.5
|
|
539
|
-
//
|
|
540
|
-
//
|
|
543
|
+
it('carries over mixed band states exactly (split before merged in table order)', () => {
|
|
544
|
+
// Two independent band events at zoom 4.5: the earlier-sorted one split in history,
|
|
545
|
+
// the later one merged. The old prefix cut forced the second to split (the mass-split
|
|
546
|
+
// bug); with suppression the carryover is exact — each keeps its own state.
|
|
547
|
+
const AB = node(['a', 'b'], 5, 0)
|
|
548
|
+
const CD = node(['c', 'd'], 105, 0)
|
|
549
|
+
const table: ClusterTable = {
|
|
550
|
+
events: [mev(4, 6, [A, B], AB), mev(3, 5, [C, D], CD)],
|
|
551
|
+
leaves: [A, B, C, D],
|
|
552
|
+
}
|
|
553
|
+
const rt = createClusterRuntime(table)
|
|
554
|
+
rt.seedFrom(
|
|
555
|
+
4.5,
|
|
556
|
+
new Map([
|
|
557
|
+
[A.id, A],
|
|
558
|
+
[B.id, B],
|
|
559
|
+
[CD.id, CD],
|
|
560
|
+
])
|
|
561
|
+
)
|
|
562
|
+
expect(rt.k).toBe(2)
|
|
563
|
+
expect(rt.getSuppressedCount()).toBe(1)
|
|
564
|
+
expect(visibleIds(rt)).toEqual(['a', 'b', 'cluster:2:c'])
|
|
565
|
+
})
|
|
566
|
+
|
|
567
|
+
it('a suppressed event merges (heals) when a zoom-out crosses its own zMerge', () => {
|
|
568
|
+
const AB = node(['a', 'b'], 5, 0)
|
|
569
|
+
const CD = node(['c', 'd'], 105, 0)
|
|
570
|
+
const table: ClusterTable = {
|
|
571
|
+
events: [mev(4, 6, [A, B], AB), mev(3, 5, [C, D], CD)],
|
|
572
|
+
leaves: [A, B, C, D],
|
|
573
|
+
}
|
|
574
|
+
const rt = createClusterRuntime(table)
|
|
575
|
+
rt.seedFrom(
|
|
576
|
+
4.5,
|
|
577
|
+
new Map([
|
|
578
|
+
[A.id, A],
|
|
579
|
+
[B.id, B],
|
|
580
|
+
[CD.id, CD],
|
|
581
|
+
])
|
|
582
|
+
)
|
|
583
|
+
const versionBefore = rt.version
|
|
584
|
+
rt.onCamera(4.4) // still inside AB's band: nothing happens
|
|
585
|
+
expect(rt.version).toBe(versionBefore)
|
|
586
|
+
rt.onCamera(3.9) // below AB's zMerge (4): the held-out merge fires at its own threshold
|
|
587
|
+
expect(rt.version).toBeGreaterThan(versionBefore)
|
|
588
|
+
expect(rt.k).toBe(2)
|
|
589
|
+
expect(rt.getSuppressedCount()).toBe(0)
|
|
590
|
+
expect(visibleIds(rt)).toEqual(['cluster:2:a', 'cluster:2:c'])
|
|
591
|
+
expectPostconditions(rt, table, 3.9)
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
it('the split walk retreats past a suppressed event without corrupting the partition', () => {
|
|
541
595
|
const AB = node(['a', 'b'], 5, 0)
|
|
542
596
|
const CD = node(['c', 'd'], 105, 0)
|
|
543
597
|
const table: ClusterTable = {
|
|
@@ -553,8 +607,18 @@ describe('createClusterRuntime seedFrom (carryover seeding)', () => {
|
|
|
553
607
|
[CD.id, CD],
|
|
554
608
|
])
|
|
555
609
|
)
|
|
610
|
+
rt.onCamera(5.5) // past CD's zSplit (5): CD splits; suppressed AB stays put
|
|
611
|
+
expect(rt.k).toBe(1)
|
|
612
|
+
expect(rt.getSuppressedCount()).toBe(1)
|
|
613
|
+
expect(visibleIds(rt)).toEqual(['a', 'b', 'c', 'd'])
|
|
614
|
+
rt.onCamera(6.5) // past AB's zSplit (6): retreat past the suppressed (never-applied) event
|
|
556
615
|
expect(rt.k).toBe(0)
|
|
616
|
+
expect(rt.getSuppressedCount()).toBe(0)
|
|
557
617
|
expect(visibleIds(rt)).toEqual(['a', 'b', 'c', 'd'])
|
|
618
|
+
rt.onCamera(3.9) // zoom back out: normal cursor walk from a clean state
|
|
619
|
+
expect(rt.k).toBe(1)
|
|
620
|
+
expect(visibleIds(rt)).toEqual(['c', 'cluster:2:a', 'd'])
|
|
621
|
+
expectPostconditions(rt, table, 3.9)
|
|
558
622
|
})
|
|
559
623
|
|
|
560
624
|
it('seedFrom followed by onCamera at the same zoom is a no-op', () => {
|
|
@@ -594,3 +658,115 @@ describe('createClusterRuntime seedFrom (carryover seeding)', () => {
|
|
|
594
658
|
expect(() => rt.seedFrom(NaN, new Map())).toThrow()
|
|
595
659
|
})
|
|
596
660
|
})
|
|
661
|
+
|
|
662
|
+
describe('createClusterRuntime detachLeaf (local partition edits)', () => {
|
|
663
|
+
it('shrinks the containing badge in place: members, count, centroid', () => {
|
|
664
|
+
const table = microTraceTable()
|
|
665
|
+
const rt = createClusterRuntime(table)
|
|
666
|
+
rt.seed(4) // P = a+b+c visible, d separate
|
|
667
|
+
const versionBefore = rt.version
|
|
668
|
+
rt.detachLeaf('a')
|
|
669
|
+
expect(rt.version).toBeGreaterThan(versionBefore)
|
|
670
|
+
expect(rt.getDetachedCount()).toBe(1)
|
|
671
|
+
const patchedP = rt.getVisible().get(P.id)!
|
|
672
|
+
expect(patchedP.members).toEqual(['b', 'c'])
|
|
673
|
+
expect(patchedP.count).toBe(2)
|
|
674
|
+
// centroid recomputed from the remaining leaves: B (10,0), C (20,0)
|
|
675
|
+
expect(patchedP.centroid).toEqual({ x: 15, y: 0 })
|
|
676
|
+
// keeps the structural id so cursor events keep addressing it
|
|
677
|
+
expect(patchedP.id).toBe(P.id)
|
|
678
|
+
// everything else untouched
|
|
679
|
+
expect(rt.getVisible().get('d')).toEqual(D)
|
|
680
|
+
expect(rt.getVisible().size).toBe(2)
|
|
681
|
+
})
|
|
682
|
+
|
|
683
|
+
it('is idempotent and ignores unknown ids', () => {
|
|
684
|
+
const table = microTraceTable()
|
|
685
|
+
const rt = createClusterRuntime(table)
|
|
686
|
+
rt.seed(4)
|
|
687
|
+
rt.detachLeaf('a')
|
|
688
|
+
const version = rt.version
|
|
689
|
+
rt.detachLeaf('a')
|
|
690
|
+
rt.detachLeaf('nonexistent')
|
|
691
|
+
expect(rt.version).toBe(version)
|
|
692
|
+
expect(rt.getDetachedCount()).toBe(1)
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
it('collapses a badge to its surviving leaf, and to nothing', () => {
|
|
696
|
+
const table = microTraceTable()
|
|
697
|
+
const rt = createClusterRuntime(table)
|
|
698
|
+
rt.seed(4)
|
|
699
|
+
rt.detachLeaf('a')
|
|
700
|
+
rt.detachLeaf('b')
|
|
701
|
+
// P = {a,b,c} minus a,b → the leaf node c, keyed by its own id
|
|
702
|
+
expect(rt.getVisible().get('c')).toEqual(C)
|
|
703
|
+
expect(rt.getVisible().has(P.id)).toBe(false)
|
|
704
|
+
rt.detachLeaf('c')
|
|
705
|
+
expect(visibleIds(rt)).toEqual(['d'])
|
|
706
|
+
})
|
|
707
|
+
|
|
708
|
+
it('removes a leaf that is visible as its own pin', () => {
|
|
709
|
+
const table = microTraceTable()
|
|
710
|
+
const rt = createClusterRuntime(table)
|
|
711
|
+
rt.seed(8) // everything split
|
|
712
|
+
rt.detachLeaf('a')
|
|
713
|
+
expect(visibleIds(rt)).toEqual(['b', 'c', 'd'])
|
|
714
|
+
})
|
|
715
|
+
|
|
716
|
+
it('zoom walks stay correct while patches are active (split)', () => {
|
|
717
|
+
const table = microTraceTable()
|
|
718
|
+
const rt = createClusterRuntime(table)
|
|
719
|
+
rt.seed(4)
|
|
720
|
+
rt.detachLeaf('a')
|
|
721
|
+
rt.onCamera(6.5) // past P's zSplit (6): structural split of a+b+c
|
|
722
|
+
expect(rt.k).toBe(0)
|
|
723
|
+
// resolved view: a stays gone, b/c/d as pins — the split itself changed nothing for a
|
|
724
|
+
expect(visibleIds(rt)).toEqual(['b', 'c', 'd'])
|
|
725
|
+
})
|
|
726
|
+
|
|
727
|
+
it('zoom walks stay correct while patches are active (merge)', () => {
|
|
728
|
+
const table = microTraceTable()
|
|
729
|
+
const rt = createClusterRuntime(table)
|
|
730
|
+
rt.seed(4)
|
|
731
|
+
rt.detachLeaf('a')
|
|
732
|
+
rt.onCamera(0.9) // below E1's zMerge (1): P+D merge into Q
|
|
733
|
+
expect(rt.k).toBe(2)
|
|
734
|
+
const patchedQ = rt.getVisible().get(Q.id)!
|
|
735
|
+
expect(patchedQ.members).toEqual(['b', 'c', 'd'])
|
|
736
|
+
expect(patchedQ.count).toBe(3)
|
|
737
|
+
// mean of B (10,0), C (20,0), D (100,0)
|
|
738
|
+
expect(patchedQ.centroid.x).toBeCloseTo(130 / 3)
|
|
739
|
+
expect(rt.getVisible().size).toBe(1)
|
|
740
|
+
})
|
|
741
|
+
|
|
742
|
+
it('seed and seedFrom clear detaches', () => {
|
|
743
|
+
const table = microTraceTable()
|
|
744
|
+
const rt = createClusterRuntime(table)
|
|
745
|
+
rt.seed(4)
|
|
746
|
+
rt.detachLeaf('a')
|
|
747
|
+
rt.seed(4)
|
|
748
|
+
expect(rt.getDetachedCount()).toBe(0)
|
|
749
|
+
expect(rt.getVisible().get(P.id)).toEqual(P)
|
|
750
|
+
rt.detachLeaf('a')
|
|
751
|
+
rt.seedFrom(
|
|
752
|
+
4,
|
|
753
|
+
new Map([
|
|
754
|
+
[P.id, P],
|
|
755
|
+
[D.id, D],
|
|
756
|
+
])
|
|
757
|
+
)
|
|
758
|
+
expect(rt.getDetachedCount()).toBe(0)
|
|
759
|
+
expect(rt.getVisible().get(P.id)).toEqual(P)
|
|
760
|
+
})
|
|
761
|
+
|
|
762
|
+
it('getVisible returns a stable reference until the partition changes', () => {
|
|
763
|
+
const table = microTraceTable()
|
|
764
|
+
const rt = createClusterRuntime(table)
|
|
765
|
+
rt.seed(4)
|
|
766
|
+
rt.detachLeaf('a')
|
|
767
|
+
const first = rt.getVisible()
|
|
768
|
+
expect(rt.getVisible()).toBe(first)
|
|
769
|
+
rt.detachLeaf('b')
|
|
770
|
+
expect(rt.getVisible()).not.toBe(first)
|
|
771
|
+
})
|
|
772
|
+
})
|