@tldraw/commenting 5.3.0-next.d7d8ced023d5 → 5.3.0-next.dec36c5c2930
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 +85 -87
- package/dist-cjs/canvas/anchor-lifecycle.js +2 -2
- package/dist-cjs/canvas/anchor-lifecycle.js.map +2 -2
- package/dist-cjs/{ui/tooltip-button.js → canvas/canvas-events.js} +16 -13
- package/dist-cjs/canvas/canvas-events.js.map +7 -0
- package/dist-cjs/canvas/cluster-badge.js +121 -0
- package/dist-cjs/canvas/cluster-badge.js.map +7 -0
- package/dist-cjs/canvas/cluster-fade.js +85 -0
- package/dist-cjs/canvas/cluster-fade.js.map +7 -0
- package/dist-cjs/canvas/cluster-input.js +51 -1
- package/dist-cjs/canvas/cluster-input.js.map +2 -2
- package/dist-cjs/canvas/cluster-model.js +252 -0
- package/dist-cjs/canvas/cluster-model.js.map +7 -0
- package/dist-cjs/canvas/comment-mutations.js +57 -28
- package/dist-cjs/canvas/comment-mutations.js.map +2 -2
- package/dist-cjs/canvas/comment-reactions.js +4 -7
- package/dist-cjs/canvas/comment-reactions.js.map +2 -2
- package/dist-cjs/canvas/comment-render.js +4 -3
- package/dist-cjs/canvas/comment-render.js.map +2 -2
- package/dist-cjs/canvas/comment-store.js.map +2 -2
- package/dist-cjs/canvas/comment-tool.js +12 -7
- package/dist-cjs/canvas/comment-tool.js.map +2 -2
- package/dist-cjs/canvas/comments-filter-menu.js +11 -13
- package/dist-cjs/canvas/comments-filter-menu.js.map +2 -2
- package/dist-cjs/canvas/comments-overflow-menu.js +10 -2
- package/dist-cjs/canvas/comments-overflow-menu.js.map +2 -2
- package/dist-cjs/canvas/comments-overlay.js +81 -871
- package/dist-cjs/canvas/comments-overlay.js.map +3 -3
- package/dist-cjs/canvas/comments-sidebar.js +16 -11
- package/dist-cjs/canvas/comments-sidebar.js.map +2 -2
- package/dist-cjs/canvas/comments-visibility-toggle.js +5 -25
- package/dist-cjs/canvas/comments-visibility-toggle.js.map +2 -2
- package/dist-cjs/canvas/context.js.map +1 -1
- package/dist-cjs/canvas/hooks.js +8 -2
- package/dist-cjs/canvas/hooks.js.map +2 -2
- package/dist-cjs/canvas/license.js.map +1 -1
- package/dist-cjs/canvas/mobile-placement.js +91 -0
- package/dist-cjs/canvas/mobile-placement.js.map +7 -0
- package/dist-cjs/canvas/options.js +39 -1
- package/dist-cjs/canvas/options.js.map +2 -2
- package/dist-cjs/canvas/pending-composer.js +135 -0
- package/dist-cjs/canvas/pending-composer.js.map +7 -0
- package/dist-cjs/canvas/pin-stacking.js +26 -1
- package/dist-cjs/canvas/pin-stacking.js.map +2 -2
- package/dist-cjs/canvas/region-box.js +113 -0
- package/dist-cjs/canvas/region-box.js.map +7 -0
- package/dist-cjs/canvas/state.js.map +2 -2
- package/dist-cjs/canvas/thread-pin.js +304 -0
- package/dist-cjs/canvas/thread-pin.js.map +7 -0
- package/dist-cjs/canvas/thread-preview.js +41 -49
- package/dist-cjs/canvas/thread-preview.js.map +2 -2
- package/dist-cjs/canvas/thread-stack.js +7 -7
- package/dist-cjs/canvas/thread-stack.js.map +2 -2
- package/dist-cjs/canvas/thread-state.js +15 -0
- package/dist-cjs/canvas/thread-state.js.map +2 -2
- package/dist-cjs/canvas/thread-view.js +120 -67
- package/dist-cjs/canvas/thread-view.js.map +3 -3
- package/dist-cjs/clustering/computeClusterTable.js +2 -2
- package/dist-cjs/clustering/computeClusterTable.js.map +2 -2
- package/dist-cjs/clustering/replay.js +89 -22
- package/dist-cjs/clustering/replay.js.map +3 -3
- package/dist-cjs/clustering/runtime.js +19 -12
- package/dist-cjs/clustering/runtime.js.map +2 -2
- package/dist-cjs/clustering/types.js.map +1 -1
- package/dist-cjs/index.d.ts +326 -203
- package/dist-cjs/index.js +7 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/ui/byline.js +5 -3
- package/dist-cjs/ui/byline.js.map +2 -2
- package/dist-cjs/ui/comment-composer.js +2 -18
- package/dist-cjs/ui/comment-composer.js.map +2 -2
- package/dist-cjs/ui/comment-pin.js +2 -15
- package/dist-cjs/ui/comment-pin.js.map +2 -2
- package/dist-cjs/ui/comments-list.js +11 -24
- package/dist-cjs/ui/comments-list.js.map +2 -2
- package/dist-cjs/ui/format-time.js +13 -1
- package/dist-cjs/ui/format-time.js.map +2 -2
- package/dist-cjs/ui/icons.js +138 -0
- package/dist-cjs/ui/icons.js.map +7 -0
- package/dist-cjs/ui/reaction-picker.js +11 -13
- package/dist-cjs/ui/reaction-picker.js.map +2 -2
- package/dist-cjs/ui/reaction.js +3 -4
- package/dist-cjs/ui/reaction.js.map +2 -2
- package/dist-cjs/ui/send-button.js +2 -9
- package/dist-cjs/ui/send-button.js.map +2 -2
- package/dist-cjs/ui/visual-viewport.js +32 -0
- package/dist-cjs/ui/visual-viewport.js.map +7 -0
- package/dist-esm/canvas/anchor-lifecycle.mjs +3 -3
- package/dist-esm/canvas/anchor-lifecycle.mjs.map +2 -2
- package/dist-esm/canvas/canvas-events.mjs +15 -0
- package/dist-esm/canvas/canvas-events.mjs.map +7 -0
- package/dist-esm/canvas/cluster-badge.mjs +106 -0
- package/dist-esm/canvas/cluster-badge.mjs.map +7 -0
- package/dist-esm/canvas/cluster-fade.mjs +65 -0
- package/dist-esm/canvas/cluster-fade.mjs.map +7 -0
- package/dist-esm/canvas/cluster-input.mjs +52 -2
- package/dist-esm/canvas/cluster-input.mjs.map +2 -2
- package/dist-esm/canvas/cluster-model.mjs +236 -0
- package/dist-esm/canvas/cluster-model.mjs.map +7 -0
- package/dist-esm/canvas/comment-mutations.mjs +57 -28
- package/dist-esm/canvas/comment-mutations.mjs.map +2 -2
- package/dist-esm/canvas/comment-reactions.mjs +5 -12
- package/dist-esm/canvas/comment-reactions.mjs.map +2 -2
- package/dist-esm/canvas/comment-render.mjs +5 -4
- package/dist-esm/canvas/comment-render.mjs.map +2 -2
- package/dist-esm/canvas/comment-store.mjs.map +2 -2
- package/dist-esm/canvas/comment-tool.mjs +12 -7
- package/dist-esm/canvas/comment-tool.mjs.map +2 -2
- package/dist-esm/canvas/comments-filter-menu.mjs +12 -13
- package/dist-esm/canvas/comments-filter-menu.mjs.map +2 -2
- package/dist-esm/canvas/comments-overflow-menu.mjs +11 -2
- package/dist-esm/canvas/comments-overflow-menu.mjs.map +2 -2
- package/dist-esm/canvas/comments-overlay.mjs +58 -879
- package/dist-esm/canvas/comments-overlay.mjs.map +3 -3
- package/dist-esm/canvas/comments-sidebar.mjs +18 -14
- package/dist-esm/canvas/comments-sidebar.mjs.map +2 -2
- package/dist-esm/canvas/comments-visibility-toggle.mjs +5 -25
- package/dist-esm/canvas/comments-visibility-toggle.mjs.map +2 -2
- package/dist-esm/canvas/hooks.mjs +9 -2
- package/dist-esm/canvas/hooks.mjs.map +2 -2
- package/dist-esm/canvas/license.mjs.map +1 -1
- package/dist-esm/canvas/mobile-placement.mjs +71 -0
- package/dist-esm/canvas/mobile-placement.mjs.map +7 -0
- package/dist-esm/canvas/options.mjs +39 -1
- package/dist-esm/canvas/options.mjs.map +2 -2
- package/dist-esm/canvas/pending-composer.mjs +127 -0
- package/dist-esm/canvas/pending-composer.mjs.map +7 -0
- package/dist-esm/canvas/pin-stacking.mjs +26 -1
- package/dist-esm/canvas/pin-stacking.mjs.map +2 -2
- package/dist-esm/canvas/region-box.mjs +93 -0
- package/dist-esm/canvas/region-box.mjs.map +7 -0
- package/dist-esm/canvas/state.mjs.map +2 -2
- package/dist-esm/canvas/thread-pin.mjs +310 -0
- package/dist-esm/canvas/thread-pin.mjs.map +7 -0
- package/dist-esm/canvas/thread-preview.mjs +42 -49
- package/dist-esm/canvas/thread-preview.mjs.map +2 -2
- package/dist-esm/canvas/thread-stack.mjs +8 -9
- package/dist-esm/canvas/thread-stack.mjs.map +2 -2
- package/dist-esm/canvas/thread-state.mjs +15 -0
- package/dist-esm/canvas/thread-state.mjs.map +2 -2
- package/dist-esm/canvas/thread-view.mjs +131 -72
- package/dist-esm/canvas/thread-view.mjs.map +3 -3
- package/dist-esm/clustering/computeClusterTable.mjs +2 -2
- package/dist-esm/clustering/computeClusterTable.mjs.map +2 -2
- package/dist-esm/clustering/replay.mjs +89 -22
- package/dist-esm/clustering/replay.mjs.map +3 -3
- package/dist-esm/clustering/runtime.mjs +19 -12
- package/dist-esm/clustering/runtime.mjs.map +2 -2
- package/dist-esm/index.d.mts +326 -203
- package/dist-esm/index.mjs +16 -3
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/ui/byline.mjs +6 -4
- package/dist-esm/ui/byline.mjs.map +2 -2
- package/dist-esm/ui/comment-composer.mjs +2 -18
- package/dist-esm/ui/comment-composer.mjs.map +2 -2
- package/dist-esm/ui/comment-pin.mjs +2 -15
- package/dist-esm/ui/comment-pin.mjs.map +2 -2
- package/dist-esm/ui/comments-list.mjs +10 -23
- package/dist-esm/ui/comments-list.mjs.map +2 -2
- package/dist-esm/ui/format-time.mjs +13 -1
- package/dist-esm/ui/format-time.mjs.map +2 -2
- package/dist-esm/ui/icons.mjs +118 -0
- package/dist-esm/ui/icons.mjs.map +7 -0
- package/dist-esm/ui/reaction-picker.mjs +12 -13
- package/dist-esm/ui/reaction-picker.mjs.map +2 -2
- package/dist-esm/ui/reaction.mjs +3 -4
- package/dist-esm/ui/reaction.mjs.map +2 -2
- package/dist-esm/ui/send-button.mjs +2 -9
- package/dist-esm/ui/send-button.mjs.map +2 -2
- package/dist-esm/ui/visual-viewport.mjs +12 -0
- package/dist-esm/ui/visual-viewport.mjs.map +7 -0
- package/package.json +7 -6
- package/src/canvas/anchor-lifecycle.test.ts +2 -2
- package/src/canvas/anchor-lifecycle.ts +25 -39
- package/src/canvas/canvas-events.ts +18 -0
- package/src/canvas/canvas.css +5 -11
- package/src/canvas/cluster-badge.tsx +133 -0
- package/src/canvas/cluster-fade.ts +102 -0
- package/src/canvas/cluster-input.test.ts +148 -14
- package/src/canvas/cluster-input.ts +92 -5
- package/src/canvas/cluster-model.ts +378 -0
- package/src/canvas/comment-mutations.test.ts +46 -4
- package/src/canvas/comment-mutations.ts +115 -104
- package/src/canvas/comment-reactions.test.ts +22 -0
- package/src/canvas/comment-reactions.tsx +33 -32
- package/src/canvas/comment-render.ts +8 -8
- package/src/canvas/comment-store.ts +12 -17
- package/src/canvas/comment-tool.test.ts +109 -4
- package/src/canvas/comment-tool.tsx +31 -19
- package/src/canvas/comments-filter-menu.tsx +10 -17
- package/src/canvas/comments-overflow-menu.tsx +8 -3
- package/src/canvas/comments-overlay.tsx +160 -1345
- package/src/canvas/comments-sidebar.tsx +40 -19
- package/src/canvas/comments-visibility-toggle.tsx +6 -30
- package/src/canvas/context.ts +4 -3
- package/src/canvas/hooks.test.ts +94 -0
- package/src/canvas/hooks.ts +17 -3
- package/src/canvas/license.ts +1 -1
- package/src/canvas/mobile-placement.ts +115 -0
- package/src/canvas/options.test.ts +124 -2
- package/src/canvas/options.ts +192 -58
- package/src/canvas/pending-composer.tsx +161 -0
- package/src/canvas/pin-stacking.test.ts +111 -1
- package/src/canvas/pin-stacking.ts +46 -0
- package/src/canvas/region-box.tsx +124 -0
- package/src/canvas/state.ts +20 -28
- package/src/canvas/thread-pin.tsx +419 -0
- package/src/canvas/thread-preview.tsx +77 -87
- package/src/canvas/thread-stack.tsx +19 -29
- package/src/canvas/thread-state.test.ts +51 -0
- package/src/canvas/thread-state.ts +56 -23
- package/src/canvas/thread-view.test.ts +72 -0
- package/src/canvas/thread-view.tsx +231 -133
- package/src/clustering/computeClusterTable.ts +12 -3
- package/src/clustering/replay.test.ts +0 -7
- package/src/clustering/replay.ts +131 -32
- package/src/clustering/runtime.test.ts +50 -6
- package/src/clustering/runtime.ts +42 -39
- package/src/clustering/schedule.test.ts +0 -6
- package/src/clustering/screen-offsets.test.ts +171 -0
- package/src/clustering/types.ts +10 -0
- package/src/index.ts +15 -2
- package/src/ui/byline.tsx +16 -6
- package/src/ui/comment-composer.tsx +25 -69
- package/src/ui/comment-pin.tsx +3 -16
- package/src/ui/comments-list.tsx +40 -29
- package/src/ui/comments.css +80 -76
- package/src/ui/format-time.test.ts +69 -0
- package/src/ui/format-time.ts +22 -2
- package/src/ui/icons.tsx +138 -0
- package/src/ui/reaction-picker.tsx +9 -16
- package/src/ui/reaction.tsx +10 -6
- package/src/ui/send-button.tsx +3 -8
- package/src/ui/visual-viewport.test.ts +36 -0
- package/src/ui/visual-viewport.ts +27 -0
- package/dist-cjs/ui/tooltip-button.js.map +0 -7
- package/dist-esm/ui/tooltip-button.mjs +0 -12
- package/dist-esm/ui/tooltip-button.mjs.map +0 -7
- package/src/ui/tooltip-button.tsx +0 -20
package/src/ui/comments.css
CHANGED
|
@@ -84,6 +84,15 @@
|
|
|
84
84
|
overflow-wrap: anywhere;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/* A comment body is read-only but its text must stay selectable so it can be copied. tldraw's
|
|
88
|
+
container sets `user-select: none` on every descendant (`.tl-container *`) to keep canvas gestures
|
|
89
|
+
from selecting UI text, so the body and its rendered children have to opt back in. */
|
|
90
|
+
.tlui-cmt-text,
|
|
91
|
+
.tlui-cmt-text * {
|
|
92
|
+
-webkit-user-select: text;
|
|
93
|
+
user-select: text;
|
|
94
|
+
}
|
|
95
|
+
|
|
87
96
|
.tlui-cmt-edited {
|
|
88
97
|
font-style: italic;
|
|
89
98
|
}
|
|
@@ -197,8 +206,6 @@
|
|
|
197
206
|
pointer-events: none;
|
|
198
207
|
white-space: nowrap;
|
|
199
208
|
width: max-content;
|
|
200
|
-
max-height: none;
|
|
201
|
-
overflow: visible;
|
|
202
209
|
}
|
|
203
210
|
|
|
204
211
|
.tlui-cmt-input {
|
|
@@ -238,6 +245,17 @@
|
|
|
238
245
|
pointer-events: none;
|
|
239
246
|
}
|
|
240
247
|
|
|
248
|
+
/* iOS Safari zooms the whole page in when a text input with a sub-16px font takes focus, and doesn't
|
|
249
|
+
zoom back out. Raising the font to 16px on touch devices suppresses that without disabling
|
|
250
|
+
pinch-zoom of the canvas (which a `maximum-scale` viewport lock would). Must come after both base
|
|
251
|
+
rules above: a media query adds no specificity, so it only wins the placeholder on source order. */
|
|
252
|
+
@media (pointer: coarse) {
|
|
253
|
+
.tlui-cmt-input,
|
|
254
|
+
.tlui-cmt-input__placeholder {
|
|
255
|
+
font-size: 16px;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
241
259
|
/* A bare up-arrow icon button (matching the placement composer): no fill by default, a grey square
|
|
242
260
|
on hover, grey while there's nothing to send and dark once there is. */
|
|
243
261
|
.tlui-cmt-send {
|
|
@@ -414,12 +432,17 @@
|
|
|
414
432
|
font-size: 10px;
|
|
415
433
|
font-weight: 400;
|
|
416
434
|
color: var(--tl-color-text-1);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/* An inert pill renders as a span (e.g. in a notification row); only the button variant is
|
|
438
|
+
clickable, so only it gets the pointer cursor and hover shade. */
|
|
439
|
+
button.tlui-cmt-reaction {
|
|
417
440
|
cursor: pointer;
|
|
418
441
|
}
|
|
419
442
|
|
|
420
443
|
/* Hover deepens the grey — no border to tint now. Mixing toward the text colour is theme-aware:
|
|
421
444
|
it darkens the fill in light mode and lightens it in dark, tracking the pill's own contrast. */
|
|
422
|
-
.tlui-cmt-reaction:hover {
|
|
445
|
+
button.tlui-cmt-reaction:hover {
|
|
423
446
|
background: var(--tl-color-muted-1);
|
|
424
447
|
}
|
|
425
448
|
|
|
@@ -429,14 +452,15 @@
|
|
|
429
452
|
background: color-mix(in srgb, var(--tl-color-selected) 28%, var(--tl-color-panel));
|
|
430
453
|
}
|
|
431
454
|
|
|
432
|
-
.tlui-cmt-reaction--active:hover {
|
|
455
|
+
button.tlui-cmt-reaction--active:hover {
|
|
433
456
|
background: color-mix(in srgb, var(--tl-color-selected) 38%, var(--tl-color-panel));
|
|
434
457
|
}
|
|
435
458
|
|
|
436
|
-
/*
|
|
459
|
+
/* The emoji renders larger than the 10px count, but its pinned line-height keeps the pill the
|
|
460
|
+
count's height — the glyph overflows the line box rather than growing it. */
|
|
437
461
|
.tlui-cmt-reaction__emoji {
|
|
438
|
-
font-size:
|
|
439
|
-
line-height:
|
|
462
|
+
font-size: 13px;
|
|
463
|
+
line-height: 10px;
|
|
440
464
|
}
|
|
441
465
|
|
|
442
466
|
.tlui-cmt-reaction__count {
|
|
@@ -539,7 +563,8 @@
|
|
|
539
563
|
--tlui-cmt-marker-transition: 0.08s ease;
|
|
540
564
|
--tlui-cmt-thread-padding: 12px;
|
|
541
565
|
--tlui-cmt-thread-gap: 0px;
|
|
542
|
-
|
|
566
|
+
/* Hit-area width of one .tlui-cmt-thread__action button (drawn as a 32px square inset 4px). */
|
|
567
|
+
--tlui-cmt-thread-action-size: 40px;
|
|
543
568
|
/* A floating pill's edge — its hairline ring is the only thing separating it from the panel it
|
|
544
569
|
overlaps, so the ring is a theme token rather than a literal black: a near-black hairline
|
|
545
570
|
vanishes on a dark panel, where the divider token lightens instead. The drop below it stays
|
|
@@ -602,47 +627,28 @@
|
|
|
602
627
|
space-between alone would leave them at the left). */
|
|
603
628
|
margin-left: auto;
|
|
604
629
|
}
|
|
605
|
-
/*
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
.
|
|
610
|
-
|
|
611
|
-
display: flex;
|
|
612
|
-
align-items: center;
|
|
613
|
-
justify-content: center;
|
|
630
|
+
/* These are TldrawUiButtons (type="icon"), so the 40px box, the 32px ::after square carrying the
|
|
631
|
+
hover wash, and the hover z-index flip all come from .tlui-button — the hit area (not the
|
|
632
|
+
::after) is what's clickable, and it never reaches past its row. Only the bits that differ from
|
|
633
|
+
a toolbar button are set here, at .tlui-button's specificity + 1 so stylesheet order can't
|
|
634
|
+
decide the winner. */
|
|
635
|
+
.tlui-button.tlui-cmt-thread__action {
|
|
614
636
|
width: 40px;
|
|
615
|
-
height: 40px;
|
|
616
637
|
padding: 0;
|
|
617
|
-
border: none;
|
|
618
638
|
border-radius: 6px;
|
|
619
|
-
background: transparent;
|
|
620
639
|
color: var(--tl-color-text-3);
|
|
621
640
|
font-size: 14px;
|
|
622
641
|
line-height: 1;
|
|
623
|
-
cursor: pointer;
|
|
624
642
|
}
|
|
625
643
|
/* Overlap each button 4px into the previous so the 32px visual squares sit 4px apart, not 8px.
|
|
626
644
|
Left-only, so the last button's hit area still stops at its row's right edge. */
|
|
627
645
|
.tlui-cmt-thread__action + .tlui-cmt-thread__action {
|
|
628
646
|
margin-left: -4px;
|
|
629
647
|
}
|
|
630
|
-
.tlui-cmt-thread__action
|
|
631
|
-
content: '';
|
|
632
|
-
position: absolute;
|
|
633
|
-
inset: 4px;
|
|
634
|
-
border-radius: var(--tl-radius-2);
|
|
635
|
-
}
|
|
636
|
-
.tlui-cmt-thread__action:hover {
|
|
648
|
+
.tlui-button.tlui-cmt-thread__action:hover {
|
|
637
649
|
color: var(--tl-color-text-1);
|
|
638
|
-
/* Raise the hovered button above its overlapping neighbours so its 40px hit area wins the
|
|
639
|
-
overlap zone — the same z-index flip tldraw's own toolbar buttons use. */
|
|
640
|
-
z-index: 1;
|
|
641
|
-
}
|
|
642
|
-
.tlui-cmt-thread__action:hover::after {
|
|
643
|
-
background: var(--tl-color-muted-2);
|
|
644
650
|
}
|
|
645
|
-
.tlui-cmt-thread__action--danger:hover {
|
|
651
|
+
.tlui-button.tlui-cmt-thread__action--danger:hover {
|
|
646
652
|
color: var(--tl-color-danger);
|
|
647
653
|
}
|
|
648
654
|
.tlui-cmt-thread__resolved {
|
|
@@ -725,7 +731,7 @@
|
|
|
725
731
|
.tlui-cmt-canvas-preview--thread .tlui-cmt-card {
|
|
726
732
|
width: 100%;
|
|
727
733
|
gap: 0;
|
|
728
|
-
padding: 0
|
|
734
|
+
padding: 0 var(--tlui-cmt-thread-padding);
|
|
729
735
|
background: transparent;
|
|
730
736
|
border: none;
|
|
731
737
|
}
|
|
@@ -754,14 +760,17 @@
|
|
|
754
760
|
.tlui-cmt-stack-list .tlui-cmt-card .tlui-cmt-head {
|
|
755
761
|
min-height: 40px;
|
|
756
762
|
}
|
|
757
|
-
/*
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
763
|
+
/* Reserve head room only for pills that actually rendered (one child / two children) — pill count
|
|
764
|
+
is a permissions question (canModifyComment), not ownership, and empty slots (read-only viewers)
|
|
765
|
+
keep the full row. Sized from the 40px hit areas (4px overlap), minus the card padding the
|
|
766
|
+
right:0 pills sit over; text stops 4px shy of the visible square. */
|
|
767
|
+
.tlui-cmt-thread .tlui-cmt-card:has(.tlui-cmt-card__actions > *) .tlui-cmt-head {
|
|
768
|
+
padding-right: calc(var(--tlui-cmt-thread-action-size) - var(--tlui-cmt-thread-padding));
|
|
762
769
|
}
|
|
763
|
-
.tlui-cmt-thread .tlui-cmt-card
|
|
764
|
-
padding-right: calc(
|
|
770
|
+
.tlui-cmt-thread .tlui-cmt-card:has(.tlui-cmt-card__actions > * + *) .tlui-cmt-head {
|
|
771
|
+
padding-right: calc(
|
|
772
|
+
2 * var(--tlui-cmt-thread-action-size) - 4px - var(--tlui-cmt-thread-padding)
|
|
773
|
+
);
|
|
765
774
|
}
|
|
766
775
|
|
|
767
776
|
/* comments list — thread summaries, one per row (used by the canvas sidebar) */
|
|
@@ -777,7 +786,7 @@
|
|
|
777
786
|
align-items: center;
|
|
778
787
|
justify-content: space-between;
|
|
779
788
|
gap: 8px;
|
|
780
|
-
padding:
|
|
789
|
+
padding: 2px 2px 2px 14px;
|
|
781
790
|
border-bottom: 1px solid var(--tl-color-divider);
|
|
782
791
|
}
|
|
783
792
|
.tlui-cmt-list__header-title {
|
|
@@ -786,43 +795,28 @@
|
|
|
786
795
|
color: var(--tl-color-text-1);
|
|
787
796
|
}
|
|
788
797
|
|
|
789
|
-
/*
|
|
790
|
-
.tlui-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
width: 26px;
|
|
796
|
-
height: 26px;
|
|
798
|
+
/* An icon button in the sidebar header (filter funnel, overflow …). Sized like the thread actions:
|
|
799
|
+
a TldrawUiButton (type="icon") left at its native 40px box, with .tlui-button's 32px ::after
|
|
800
|
+
square carrying the hover wash. Only the bits that differ are set here, at .tlui-button's
|
|
801
|
+
specificity + 1 so stylesheet order can't decide the winner. */
|
|
802
|
+
.tlui-button.tlui-cmt-header-btn {
|
|
803
|
+
width: 40px;
|
|
797
804
|
padding: 0;
|
|
798
|
-
border: none;
|
|
799
805
|
border-radius: 6px;
|
|
800
|
-
background: transparent;
|
|
801
806
|
color: var(--tl-color-text-2);
|
|
802
|
-
cursor: pointer;
|
|
803
|
-
}
|
|
804
|
-
/* Same hit-target expansion as the thread actions, tiled the same way — this row's gap is 2px too
|
|
805
|
-
(see `.tlui-cmt-list__header-actions`). */
|
|
806
|
-
.tlui-cmt-header-btn::after {
|
|
807
|
-
content: '';
|
|
808
|
-
position: absolute;
|
|
809
|
-
inset: -6px -1px;
|
|
810
807
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
right: -6px;
|
|
808
|
+
/* Overlap each button 4px into the previous so the 32px visual squares sit 4px apart, not 8px —
|
|
809
|
+
the same tiling the thread actions use. */
|
|
810
|
+
.tlui-cmt-header-btn + .tlui-cmt-header-btn {
|
|
811
|
+
margin-left: -4px;
|
|
816
812
|
}
|
|
817
|
-
.tlui-cmt-header-btn:hover,
|
|
818
|
-
.tlui-cmt-header-btn[data-state='open'] {
|
|
819
|
-
background: var(--tl-color-muted-2);
|
|
813
|
+
.tlui-button.tlui-cmt-header-btn:hover,
|
|
814
|
+
.tlui-button.tlui-cmt-header-btn[data-state='open'] {
|
|
820
815
|
color: var(--tl-color-text-1);
|
|
821
816
|
}
|
|
822
817
|
.tlui-cmt-list__header-actions {
|
|
823
818
|
display: flex;
|
|
824
819
|
align-items: center;
|
|
825
|
-
gap: 2px;
|
|
826
820
|
}
|
|
827
821
|
|
|
828
822
|
/* The commenting dropdowns' surface — a little air between the items and the menu edge. */
|
|
@@ -878,6 +872,7 @@
|
|
|
878
872
|
font-size: 12px;
|
|
879
873
|
}
|
|
880
874
|
.tlui-cmt-list__item {
|
|
875
|
+
position: relative;
|
|
881
876
|
display: flex;
|
|
882
877
|
gap: 10px;
|
|
883
878
|
width: 100%;
|
|
@@ -892,18 +887,27 @@
|
|
|
892
887
|
color: inherit;
|
|
893
888
|
text-decoration: none;
|
|
894
889
|
}
|
|
895
|
-
|
|
890
|
+
/* The highlight is an inset, rounded wash like tldraw's buttons and menu items — not an
|
|
891
|
+
edge-to-edge band — so a comment row lights up the same way every other interactive row does. */
|
|
892
|
+
.tlui-cmt-list__item::after {
|
|
893
|
+
content: '';
|
|
894
|
+
position: absolute;
|
|
895
|
+
inset: 4px;
|
|
896
|
+
border-radius: var(--tl-radius-2);
|
|
897
|
+
pointer-events: none;
|
|
898
|
+
}
|
|
899
|
+
.tlui-cmt-list__item:hover::after {
|
|
896
900
|
background: var(--tl-color-muted-2);
|
|
897
901
|
}
|
|
898
902
|
/* Selected sits a step above hover so the open thread's row stays distinguishable while
|
|
899
903
|
pointing at other rows — but only a step: the muted-1 token (10%) reads too heavy here.
|
|
900
904
|
These rows sit on the opaque sidebar panel, so a translucent tint composes safely. */
|
|
901
|
-
.tlui-cmt-list__item--selected,
|
|
902
|
-
.tlui-cmt-list__item--selected:hover {
|
|
905
|
+
.tlui-cmt-list__item--selected::after,
|
|
906
|
+
.tlui-cmt-list__item--selected:hover::after {
|
|
903
907
|
background: hsl(0, 0%, 0%, 5%);
|
|
904
908
|
}
|
|
905
|
-
.tl-theme__dark .tlui-cmt-list__item--selected,
|
|
906
|
-
.tl-theme__dark .tlui-cmt-list__item--selected:hover {
|
|
909
|
+
.tl-theme__dark .tlui-cmt-list__item--selected::after,
|
|
910
|
+
.tl-theme__dark .tlui-cmt-list__item--selected:hover::after {
|
|
907
911
|
background: hsl(0, 0%, 100%, 5%);
|
|
908
912
|
}
|
|
909
913
|
/* resolved rows mute their content but keep the "Resolved" marker legible */
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { formatFullDateTime, formatRelativeTime } from './format-time'
|
|
3
|
+
|
|
4
|
+
/** An ISO timestamp `seconds` in the past, relative to now. */
|
|
5
|
+
function agoIso(seconds: number): string {
|
|
6
|
+
return new Date(Date.now() - seconds * 1000).toISOString()
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const TWO_HOURS = 2 * 60 * 60
|
|
10
|
+
|
|
11
|
+
describe('formatRelativeTime', () => {
|
|
12
|
+
it('formats in English by default', () => {
|
|
13
|
+
expect(formatRelativeTime(agoIso(TWO_HOURS))).toBe(
|
|
14
|
+
new Intl.RelativeTimeFormat('en', { numeric: 'auto', style: 'narrow' }).format(-2, 'hour')
|
|
15
|
+
)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
// `Byline` passes the UI's current translation locale; without this a localized app showed
|
|
19
|
+
// English timestamps under localized everything else.
|
|
20
|
+
it('formats in the given locale', () => {
|
|
21
|
+
const french = formatRelativeTime(agoIso(TWO_HOURS), 'fr')
|
|
22
|
+
expect(french).toBe(
|
|
23
|
+
new Intl.RelativeTimeFormat('fr', { numeric: 'auto', style: 'narrow' }).format(-2, 'hour')
|
|
24
|
+
)
|
|
25
|
+
expect(french).not.toBe(formatRelativeTime(agoIso(TWO_HOURS), 'en'))
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// The parameter is optional, so undefined has to mean "the default", not "no locale".
|
|
29
|
+
it('treats an undefined locale as the default', () => {
|
|
30
|
+
expect(formatRelativeTime(agoIso(TWO_HOURS), undefined)).toBe(
|
|
31
|
+
formatRelativeTime(agoIso(TWO_HOURS))
|
|
32
|
+
)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('reads under a minute as "now" in the given locale', () => {
|
|
36
|
+
expect(formatRelativeTime(agoIso(5), 'fr')).toBe(
|
|
37
|
+
new Intl.RelativeTimeFormat('fr', { numeric: 'auto', style: 'narrow' }).format(0, 'second')
|
|
38
|
+
)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('returns nothing for an unparseable date', () => {
|
|
42
|
+
expect(formatRelativeTime('not a date', 'fr')).toBe('')
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('formatFullDateTime', () => {
|
|
47
|
+
it('formats an ISO datetime as a full date and time', () => {
|
|
48
|
+
const iso = '2025-07-22T16:44:00.000Z'
|
|
49
|
+
expect(formatFullDateTime(iso)).toBe(
|
|
50
|
+
new Intl.DateTimeFormat('en', { dateStyle: 'full', timeStyle: 'short' }).format(new Date(iso))
|
|
51
|
+
)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('formats in the given locale', () => {
|
|
55
|
+
const french = formatFullDateTime('2025-07-22T16:44:00.000Z', 'fr')
|
|
56
|
+
expect(french).toContain('juillet')
|
|
57
|
+
expect(french).not.toBe(formatFullDateTime('2025-07-22T16:44:00.000Z', 'en'))
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// The parameter is optional, so undefined has to mean "the default", not "no locale".
|
|
61
|
+
it('treats an undefined locale as the default', () => {
|
|
62
|
+
const iso = '2025-07-22T16:44:00.000Z'
|
|
63
|
+
expect(formatFullDateTime(iso, undefined)).toBe(formatFullDateTime(iso))
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('returns an empty string for invalid dates', () => {
|
|
67
|
+
expect(formatFullDateTime('not a date')).toBe('')
|
|
68
|
+
})
|
|
69
|
+
})
|
package/src/ui/format-time.ts
CHANGED
|
@@ -9,7 +9,7 @@ const DIVISIONS = [
|
|
|
9
9
|
] as const
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* Format an ISO datetime as
|
|
12
|
+
* Format an ISO datetime as compact relative time ("2h ago", "yesterday", "last wk.").
|
|
13
13
|
* Locale-aware via Intl.RelativeTimeFormat.
|
|
14
14
|
* @public
|
|
15
15
|
*/
|
|
@@ -17,7 +17,7 @@ export function formatRelativeTime(iso: string, locale = 'en'): string {
|
|
|
17
17
|
const then = new Date(iso).getTime()
|
|
18
18
|
if (Number.isNaN(then)) return ''
|
|
19
19
|
|
|
20
|
-
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto', style: '
|
|
20
|
+
const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto', style: 'narrow' })
|
|
21
21
|
let duration = (then - Date.now()) / 1000
|
|
22
22
|
// Under a minute is just "now" — with second granularity the label visibly ticks while
|
|
23
23
|
// typing a reply (every keystroke re-renders the card).
|
|
@@ -32,3 +32,23 @@ export function formatRelativeTime(iso: string, locale = 'en'): string {
|
|
|
32
32
|
}
|
|
33
33
|
return ''
|
|
34
34
|
}
|
|
35
|
+
|
|
36
|
+
// Cached per locale — comment cards re-render on every reply keystroke, and constructing a
|
|
37
|
+
// DateTimeFormat is far more expensive than formatting with one.
|
|
38
|
+
const fullDateTimeFormatters = new Map<string, Intl.DateTimeFormat>()
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Format an ISO datetime as a full date and time ("Tuesday, July 22, 2025 at 4:44 PM").
|
|
42
|
+
* Locale-aware via Intl.DateTimeFormat.
|
|
43
|
+
* @public
|
|
44
|
+
*/
|
|
45
|
+
export function formatFullDateTime(iso: string, locale = 'en'): string {
|
|
46
|
+
const date = new Date(iso)
|
|
47
|
+
if (Number.isNaN(date.getTime())) return ''
|
|
48
|
+
let formatter = fullDateTimeFormatters.get(locale)
|
|
49
|
+
if (!formatter) {
|
|
50
|
+
formatter = new Intl.DateTimeFormat(locale, { dateStyle: 'full', timeStyle: 'short' })
|
|
51
|
+
fullDateTimeFormatters.set(locale, formatter)
|
|
52
|
+
}
|
|
53
|
+
return formatter.format(date)
|
|
54
|
+
}
|
package/src/ui/icons.tsx
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/* The commenting UI's inline icons, kept together so the set is visible in one place.
|
|
2
|
+
*
|
|
3
|
+
* Anything with an equivalent in tldraw's icon assets goes through `TldrawUiIcon` instead (the ⋯
|
|
4
|
+
* menu, the dismiss cross, the resolve button's check). What's left lives here because the asset
|
|
5
|
+
* set has no counterpart — a smiley, a send arrow, a filter — or because the asset reads wrong at
|
|
6
|
+
* the size it's needed, which is the case for both resolved checks below. The eye pair mirrors
|
|
7
|
+
* tldraw's presence glyphs: the open state is the `follow` icon — the almond eye whose pupil
|
|
8
|
+
* tracks presence — and the closed state the `closed` icon, its shut-lid companion. Both are in
|
|
9
|
+
* the asset set, but they're inlined because the package renders without an `AssetUrlsProvider`,
|
|
10
|
+
* so `TldrawUiIcon` can't reach them. Inline also means they render without the consumer serving
|
|
11
|
+
* files.
|
|
12
|
+
*
|
|
13
|
+
* All internal — none are exported from the package. */
|
|
14
|
+
|
|
15
|
+
/** The check inside a resolved comment pin. A drawn check, not the '✓' glyph — that character sits
|
|
16
|
+
* off-baseline and varies by font. Heavier and rounder than `CheckIcon`: it has to hold its weight
|
|
17
|
+
* against the pin's 28px filled face, where the asset check reads too light. */
|
|
18
|
+
export function PinCheckIcon() {
|
|
19
|
+
return (
|
|
20
|
+
<svg
|
|
21
|
+
viewBox="0 0 24 24"
|
|
22
|
+
width="15"
|
|
23
|
+
height="15"
|
|
24
|
+
fill="none"
|
|
25
|
+
stroke="currentColor"
|
|
26
|
+
strokeWidth="2.5"
|
|
27
|
+
strokeLinecap="round"
|
|
28
|
+
strokeLinejoin="round"
|
|
29
|
+
aria-hidden="true"
|
|
30
|
+
>
|
|
31
|
+
<path d="M4 12.5l5 5L20 6.5" />
|
|
32
|
+
</svg>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The check beside the "Resolved" label on a comments list row. Thinner than `PinCheckIcon` — it
|
|
37
|
+
* sits next to text at 12px, so it's drawn to match the label's weight rather than shout. */
|
|
38
|
+
export function CheckIcon() {
|
|
39
|
+
return (
|
|
40
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
|
41
|
+
<path
|
|
42
|
+
d="M2.5 6.2 4.7 8.4 9.5 3.6"
|
|
43
|
+
stroke="currentColor"
|
|
44
|
+
strokeWidth="1.5"
|
|
45
|
+
strokeLinecap="round"
|
|
46
|
+
strokeLinejoin="round"
|
|
47
|
+
/>
|
|
48
|
+
</svg>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** The reaction picker's trigger. */
|
|
53
|
+
export function SmileyIcon() {
|
|
54
|
+
return (
|
|
55
|
+
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" aria-hidden="true">
|
|
56
|
+
<path
|
|
57
|
+
d="M7.49991 0.876892C3.84222 0.876892 0.877075 3.84204 0.877075 7.49972C0.877075 11.1574 3.84222 14.1226 7.49991 14.1226C11.1576 14.1226 14.1227 11.1574 14.1227 7.49972C14.1227 3.84204 11.1576 0.876892 7.49991 0.876892ZM1.82708 7.49972C1.82708 4.36671 4.36689 1.82689 7.49991 1.82689C10.6329 1.82689 13.1727 4.36671 13.1727 7.49972C13.1727 10.6327 10.6329 13.1726 7.49991 13.1726C4.36689 13.1726 1.82708 10.6327 1.82708 7.49972ZM5.03747 9.21395C4.87949 8.98746 4.56782 8.93193 4.34133 9.08991C4.11484 9.24789 4.05931 9.55956 4.21729 9.78605C4.93926 10.8211 6.14033 11.5 7.50004 11.5C8.85974 11.5 10.0608 10.8211 10.7828 9.78605C10.9408 9.55956 10.8852 9.24789 10.6587 9.08991C10.4323 8.93193 10.1206 8.98746 9.9626 9.21395C9.41963 9.99238 8.51907 10.5 7.50004 10.5C6.481 10.5 5.58044 9.99238 5.03747 9.21395ZM5.37503 6.84998C5.85828 6.84998 6.25003 6.45815 6.25003 5.97498C6.25003 5.4918 5.85828 5.09998 5.37503 5.09998C4.89179 5.09998 4.50003 5.4918 4.50003 5.97498C4.50003 6.45815 4.89179 6.84998 5.37503 6.84998ZM10.5 5.97498C10.5 6.45815 10.1083 6.84998 9.62503 6.84998C9.14179 6.84998 8.75003 6.45815 8.75003 5.97498C8.75003 5.4918 9.14179 5.09998 9.62503 5.09998C10.1083 5.09998 10.5 5.4918 10.5 5.97498Z"
|
|
58
|
+
fill="currentColor"
|
|
59
|
+
fillRule="evenodd"
|
|
60
|
+
clipRule="evenodd"
|
|
61
|
+
/>
|
|
62
|
+
</svg>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The composer's post button — an up arrow. */
|
|
67
|
+
export function SendIcon() {
|
|
68
|
+
return (
|
|
69
|
+
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" aria-hidden="true">
|
|
70
|
+
<path
|
|
71
|
+
d="M7.14645 2.14645C7.34171 1.95118 7.65829 1.95118 7.85355 2.14645L11.8536 6.14645C12.0488 6.34171 12.0488 6.65829 11.8536 6.85355C11.6583 7.04882 11.3417 7.04882 11.1464 6.85355L8 3.70711L8 12.5C8 12.7761 7.77614 13 7.5 13C7.22386 13 7 12.7761 7 12.5L7 3.70711L3.85355 6.85355C3.65829 7.04882 3.34171 7.04882 3.14645 6.85355C2.95118 6.65829 2.95118 6.34171 3.14645 6.14645L7.14645 2.14645Z"
|
|
72
|
+
fill="currentColor"
|
|
73
|
+
fillRule="evenodd"
|
|
74
|
+
clipRule="evenodd"
|
|
75
|
+
/>
|
|
76
|
+
</svg>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Comment pins are showing. tldraw's `follow` glyph (icons/icon/follow.svg) inlined at its native
|
|
81
|
+
* weight — the almond eye with a centred pupil. Pairs with `EyeClosedIcon`; the two must stay the
|
|
82
|
+
* same family. */
|
|
83
|
+
export function EyeOpenIcon() {
|
|
84
|
+
return (
|
|
85
|
+
<svg width="15" height="15" viewBox="0 0 30 30" fill="none" aria-hidden="true">
|
|
86
|
+
<path fill="currentColor" d="M19 15a4 4 0 1 1-8 0 4 4 0 0 1 8 0" />
|
|
87
|
+
<path
|
|
88
|
+
stroke="currentColor"
|
|
89
|
+
strokeWidth="2"
|
|
90
|
+
d="M26 15c0 1.77-1.077 3.496-3.07 4.825C20.946 21.149 18.145 22 15 22s-5.945-.851-7.93-2.175C5.076 18.496 4 16.77 4 15c0-1.77 1.077-3.496 3.07-4.825C9.054 8.851 11.855 8 15 8s5.945.851 7.93 2.175C24.924 11.504 26 13.23 26 15Z"
|
|
91
|
+
/>
|
|
92
|
+
</svg>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Comment pins are hidden. tldraw's `closed` eye glyph (icons/icon/closed.svg) — a shut lower lid
|
|
97
|
+
* with lashes, the design pair to `follow`. Inlined like the open eye: the package renders without
|
|
98
|
+
* an `AssetUrlsProvider`. Lash dots are `r=1.5` (vs the asset's `r=1`) because at the 15px header
|
|
99
|
+
* size the asset's finer lashes fall below a pixel; the lid keeps `follow`'s native stroke. */
|
|
100
|
+
export function EyeClosedIcon() {
|
|
101
|
+
return (
|
|
102
|
+
<svg width="15" height="15" viewBox="0 0 30 30" fill="none" aria-hidden="true">
|
|
103
|
+
<path
|
|
104
|
+
stroke="currentColor"
|
|
105
|
+
strokeWidth="2"
|
|
106
|
+
strokeLinecap="round"
|
|
107
|
+
d="M27 15C27 19.4183 21.6274 23 15 23C8.37258 23 3 19.4183 3 15"
|
|
108
|
+
/>
|
|
109
|
+
<circle cx="4" cy="20" r="1.5" fill="currentColor" />
|
|
110
|
+
<circle cx="9" cy="23" r="1.5" fill="currentColor" />
|
|
111
|
+
<circle cx="15" cy="24" r="1.5" fill="currentColor" />
|
|
112
|
+
<circle cx="21" cy="23" r="1.5" fill="currentColor" />
|
|
113
|
+
<circle cx="26" cy="20" r="1.5" fill="currentColor" />
|
|
114
|
+
</svg>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The sidebar's comment-menu trigger — tldraw's `dots-vertical` glyph (icons/icon/dots-vertical.svg)
|
|
119
|
+
* inlined, a vertical kebab. Inlined like the eye pair because the package renders without an
|
|
120
|
+
* `AssetUrlsProvider`. */
|
|
121
|
+
export function MoreMenuIcon() {
|
|
122
|
+
return (
|
|
123
|
+
<svg width="15" height="15" viewBox="0 0 30 30" fill="none" aria-hidden="true">
|
|
124
|
+
<path
|
|
125
|
+
fill="currentColor"
|
|
126
|
+
d="M15 9.48975C14.3167 9.48975 13.7292 9.24747 13.2375 8.76293C12.7458 8.27003 12.5 7.67688 12.5 6.98348C12.5 6.29843 12.7458 5.71364 13.2375 5.22909C13.7292 4.73619 14.3167 4.48975 15 4.48975C15.6833 4.48975 16.2708 4.73619 16.7625 5.22909C17.2542 5.71364 17.5 6.29843 17.5 6.98348C17.5 7.44296 17.3833 7.86485 17.15 8.24914C16.925 8.62508 16.625 8.92583 16.25 9.1514C15.875 9.37696 15.4583 9.48975 15 9.48975Z"
|
|
127
|
+
/>
|
|
128
|
+
<path
|
|
129
|
+
fill="currentColor"
|
|
130
|
+
d="M15 17.5C14.3167 17.5 13.7292 17.2577 13.2375 16.7732C12.7458 16.2803 12.5 15.6871 12.5 14.9937C12.5 14.3087 12.7458 13.7239 13.2375 13.2393C13.7292 12.7464 14.3167 12.5 15 12.5C15.6833 12.5 16.2708 12.7464 16.7625 13.2393C17.2542 13.7239 17.5 14.3087 17.5 14.9937C17.5 15.4532 17.3833 15.8751 17.15 16.2594C16.925 16.6353 16.625 16.9361 16.25 17.1617C15.875 17.3872 15.4583 17.5 15 17.5Z"
|
|
131
|
+
/>
|
|
132
|
+
<path
|
|
133
|
+
fill="currentColor"
|
|
134
|
+
d="M15 25.5103C14.3167 25.5103 13.7292 25.268 13.2375 24.7834C12.7458 24.2905 12.5 23.6974 12.5 23.004C12.5 22.319 12.7458 21.7342 13.2375 21.2496C13.7292 20.7567 14.3167 20.5103 15 20.5103C15.6833 20.5103 16.2708 20.7567 16.7625 21.2496C17.2542 21.7342 17.5 22.319 17.5 23.004C17.5 23.4635 17.3833 23.8854 17.15 24.2697C16.925 24.6456 16.625 24.9463 16.25 25.1719C15.875 25.3975 15.4583 25.5103 15 25.5103Z"
|
|
135
|
+
/>
|
|
136
|
+
</svg>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useId, type ComponentType } from 'react'
|
|
2
2
|
import {
|
|
3
|
+
TldrawUiButton,
|
|
3
4
|
TldrawUiDropdownMenuContent,
|
|
4
5
|
TldrawUiDropdownMenuRoot,
|
|
5
6
|
TldrawUiDropdownMenuTrigger,
|
|
@@ -8,8 +9,8 @@ import {
|
|
|
8
9
|
useTranslation,
|
|
9
10
|
} from 'tldraw'
|
|
10
11
|
import { EmojiPicker, type EmojiPickerProps } from './emoji-picker'
|
|
12
|
+
import { SmileyIcon } from './icons'
|
|
11
13
|
import { RenderReaction } from './reaction'
|
|
12
|
-
import { TooltipButton } from './tooltip-button'
|
|
13
14
|
|
|
14
15
|
/** @public */
|
|
15
16
|
export interface ReactionPickerProps {
|
|
@@ -78,9 +79,14 @@ export function ReactionPicker({
|
|
|
78
79
|
return (
|
|
79
80
|
<TldrawUiDropdownMenuRoot id={id}>
|
|
80
81
|
<TldrawUiDropdownMenuTrigger>
|
|
81
|
-
<
|
|
82
|
+
<TldrawUiButton
|
|
83
|
+
type="icon"
|
|
84
|
+
tooltip={msg('comments.add-reaction')}
|
|
85
|
+
title={msg('comments.add-reaction')}
|
|
86
|
+
className={className}
|
|
87
|
+
>
|
|
82
88
|
<SmileyIcon />
|
|
83
|
-
</
|
|
89
|
+
</TldrawUiButton>
|
|
84
90
|
</TldrawUiDropdownMenuTrigger>
|
|
85
91
|
{/* left-aligned under the trigger, so the grid opens rightward off the card's edge */}
|
|
86
92
|
<TldrawUiDropdownMenuContent side="bottom" align="start" alignOffset={0} sideOffset={4}>
|
|
@@ -94,16 +100,3 @@ export function ReactionPicker({
|
|
|
94
100
|
</TldrawUiDropdownMenuRoot>
|
|
95
101
|
)
|
|
96
102
|
}
|
|
97
|
-
|
|
98
|
-
function SmileyIcon() {
|
|
99
|
-
return (
|
|
100
|
-
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" aria-hidden="true">
|
|
101
|
-
<path
|
|
102
|
-
d="M7.49991 0.876892C3.84222 0.876892 0.877075 3.84204 0.877075 7.49972C0.877075 11.1574 3.84222 14.1226 7.49991 14.1226C11.1576 14.1226 14.1227 11.1574 14.1227 7.49972C14.1227 3.84204 11.1576 0.876892 7.49991 0.876892ZM1.82708 7.49972C1.82708 4.36671 4.36689 1.82689 7.49991 1.82689C10.6329 1.82689 13.1727 4.36671 13.1727 7.49972C13.1727 10.6327 10.6329 13.1726 7.49991 13.1726C4.36689 13.1726 1.82708 10.6327 1.82708 7.49972ZM5.03747 9.21395C4.87949 8.98746 4.56782 8.93193 4.34133 9.08991C4.11484 9.24789 4.05931 9.55956 4.21729 9.78605C4.93926 10.8211 6.14033 11.5 7.50004 11.5C8.85974 11.5 10.0608 10.8211 10.7828 9.78605C10.9408 9.55956 10.8852 9.24789 10.6587 9.08991C10.4323 8.93193 10.1206 8.98746 9.9626 9.21395C9.41963 9.99238 8.51907 10.5 7.50004 10.5C6.481 10.5 5.58044 9.99238 5.03747 9.21395ZM5.37503 6.84998C5.85828 6.84998 6.25003 6.45815 6.25003 5.97498C6.25003 5.4918 5.85828 5.09998 5.37503 5.09998C4.89179 5.09998 4.50003 5.4918 4.50003 5.97498C4.50003 6.45815 4.89179 6.84998 5.37503 6.84998ZM10.5 5.97498C10.5 6.45815 10.1083 6.84998 9.62503 6.84998C9.14179 6.84998 8.75003 6.45815 8.75003 5.97498C8.75003 5.4918 9.14179 5.09998 9.62503 5.09998C10.1083 5.09998 10.5 5.4918 10.5 5.97498Z"
|
|
103
|
-
fill="currentColor"
|
|
104
|
-
fillRule="evenodd"
|
|
105
|
-
clipRule="evenodd"
|
|
106
|
-
/>
|
|
107
|
-
</svg>
|
|
108
|
-
)
|
|
109
|
-
}
|
package/src/ui/reaction.tsx
CHANGED
|
@@ -46,19 +46,23 @@ export function Reaction({
|
|
|
46
46
|
ReactionTooltip = DefaultReactionTooltip,
|
|
47
47
|
onClick,
|
|
48
48
|
}: ReactionProps) {
|
|
49
|
+
// An inert pill (no handler) is plain content, not a control — rendering it as a span keeps it
|
|
50
|
+
// valid inside an anchor row (e.g. a notification list item) and out of the tab order.
|
|
51
|
+
const Tag = onClick ? 'button' : 'span'
|
|
49
52
|
const pill = (
|
|
50
|
-
<
|
|
53
|
+
<Tag
|
|
51
54
|
className={active ? 'tlui-cmt-reaction tlui-cmt-reaction--active' : 'tlui-cmt-reaction'}
|
|
52
|
-
|
|
55
|
+
{...(onClick
|
|
56
|
+
? { type: 'button' as const, 'aria-pressed': active, onClick }
|
|
57
|
+
: { role: 'img' as const })}
|
|
53
58
|
// The emoji alone announces as its unicode name ("thumbs up"); pairing it with the count
|
|
54
|
-
// is what makes the pill's state legible to a screen reader.
|
|
59
|
+
// is what makes the pill's state legible to a screen reader. The inert span needs the
|
|
60
|
+
// img role for the label to be announced at all.
|
|
55
61
|
aria-label={`${emoji} ${count}`}
|
|
56
|
-
aria-pressed={active}
|
|
57
|
-
onClick={onClick}
|
|
58
62
|
>
|
|
59
63
|
<span className="tlui-cmt-reaction__emoji">{renderReaction(emoji)}</span>
|
|
60
64
|
<span className="tlui-cmt-reaction__count">{count}</span>
|
|
61
|
-
</
|
|
65
|
+
</Tag>
|
|
62
66
|
)
|
|
63
67
|
|
|
64
68
|
// A bare pill when hovering is suppressed, or when there's no one to name.
|
package/src/ui/send-button.tsx
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { SendIcon } from './icons'
|
|
2
|
+
|
|
1
3
|
/** @public */
|
|
2
4
|
export interface SendButtonProps {
|
|
3
5
|
/** The button's accessible name (e.g. "Send"). Shown as an up-arrow icon, so this is its label. */
|
|
@@ -17,14 +19,7 @@ export function SendButton({ label, disabled, onClick }: SendButtonProps) {
|
|
|
17
19
|
aria-label={label}
|
|
18
20
|
title={label}
|
|
19
21
|
>
|
|
20
|
-
<
|
|
21
|
-
<path
|
|
22
|
-
d="M7.14645 2.14645C7.34171 1.95118 7.65829 1.95118 7.85355 2.14645L11.8536 6.14645C12.0488 6.34171 12.0488 6.65829 11.8536 6.85355C11.6583 7.04882 11.3417 7.04882 11.1464 6.85355L8 3.70711L8 12.5C8 12.7761 7.77614 13 7.5 13C7.22386 13 7 12.7761 7 12.5L7 3.70711L3.85355 6.85355C3.65829 7.04882 3.34171 7.04882 3.14645 6.85355C2.95118 6.65829 2.95118 6.34171 3.14645 6.14645L7.14645 2.14645Z"
|
|
23
|
-
fill="currentColor"
|
|
24
|
-
fillRule="evenodd"
|
|
25
|
-
clipRule="evenodd"
|
|
26
|
-
/>
|
|
27
|
-
</svg>
|
|
22
|
+
<SendIcon />
|
|
28
23
|
</button>
|
|
29
24
|
)
|
|
30
25
|
}
|