@tldraw/commenting 5.3.0-next.2fa9c61a8de6 → 5.3.0-next.7654e7ac2a02
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 +63 -71
- 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 +0 -1
- 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 +77 -870
- 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 +134 -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 +323 -202
- 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 +12 -0
- package/dist-cjs/ui/format-time.js.map +2 -2
- package/dist-cjs/ui/icons.js +115 -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 +0 -1
- 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 +54 -878
- 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 +126 -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 +323 -202
- 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 +12 -0
- package/dist-esm/ui/format-time.mjs.map +2 -2
- package/dist-esm/ui/icons.mjs +95 -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 +5 -5
- 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.tsx +8 -12
- package/src/canvas/comments-filter-menu.tsx +9 -16
- package/src/canvas/comments-overflow-menu.tsx +8 -3
- package/src/canvas/comments-overlay.tsx +154 -1344
- 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 +157 -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 +418 -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 +58 -60
- package/src/ui/format-time.test.ts +69 -0
- package/src/ui/format-time.ts +20 -0
- package/src/ui/icons.tsx +116 -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
|
@@ -197,8 +197,6 @@
|
|
|
197
197
|
pointer-events: none;
|
|
198
198
|
white-space: nowrap;
|
|
199
199
|
width: max-content;
|
|
200
|
-
max-height: none;
|
|
201
|
-
overflow: visible;
|
|
202
200
|
}
|
|
203
201
|
|
|
204
202
|
.tlui-cmt-input {
|
|
@@ -238,6 +236,17 @@
|
|
|
238
236
|
pointer-events: none;
|
|
239
237
|
}
|
|
240
238
|
|
|
239
|
+
/* iOS Safari zooms the whole page in when a text input with a sub-16px font takes focus, and doesn't
|
|
240
|
+
zoom back out. Raising the font to 16px on touch devices suppresses that without disabling
|
|
241
|
+
pinch-zoom of the canvas (which a `maximum-scale` viewport lock would). Must come after both base
|
|
242
|
+
rules above: a media query adds no specificity, so it only wins the placeholder on source order. */
|
|
243
|
+
@media (pointer: coarse) {
|
|
244
|
+
.tlui-cmt-input,
|
|
245
|
+
.tlui-cmt-input__placeholder {
|
|
246
|
+
font-size: 16px;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
241
250
|
/* A bare up-arrow icon button (matching the placement composer): no fill by default, a grey square
|
|
242
251
|
on hover, grey while there's nothing to send and dark once there is. */
|
|
243
252
|
.tlui-cmt-send {
|
|
@@ -414,12 +423,17 @@
|
|
|
414
423
|
font-size: 10px;
|
|
415
424
|
font-weight: 400;
|
|
416
425
|
color: var(--tl-color-text-1);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/* An inert pill renders as a span (e.g. in a notification row); only the button variant is
|
|
429
|
+
clickable, so only it gets the pointer cursor and hover shade. */
|
|
430
|
+
button.tlui-cmt-reaction {
|
|
417
431
|
cursor: pointer;
|
|
418
432
|
}
|
|
419
433
|
|
|
420
434
|
/* Hover deepens the grey — no border to tint now. Mixing toward the text colour is theme-aware:
|
|
421
435
|
it darkens the fill in light mode and lightens it in dark, tracking the pill's own contrast. */
|
|
422
|
-
.tlui-cmt-reaction:hover {
|
|
436
|
+
button.tlui-cmt-reaction:hover {
|
|
423
437
|
background: var(--tl-color-muted-1);
|
|
424
438
|
}
|
|
425
439
|
|
|
@@ -429,14 +443,15 @@
|
|
|
429
443
|
background: color-mix(in srgb, var(--tl-color-selected) 28%, var(--tl-color-panel));
|
|
430
444
|
}
|
|
431
445
|
|
|
432
|
-
.tlui-cmt-reaction--active:hover {
|
|
446
|
+
button.tlui-cmt-reaction--active:hover {
|
|
433
447
|
background: color-mix(in srgb, var(--tl-color-selected) 38%, var(--tl-color-panel));
|
|
434
448
|
}
|
|
435
449
|
|
|
436
|
-
/*
|
|
450
|
+
/* The emoji renders larger than the 10px count, but its pinned line-height keeps the pill the
|
|
451
|
+
count's height — the glyph overflows the line box rather than growing it. */
|
|
437
452
|
.tlui-cmt-reaction__emoji {
|
|
438
|
-
font-size:
|
|
439
|
-
line-height:
|
|
453
|
+
font-size: 13px;
|
|
454
|
+
line-height: 10px;
|
|
440
455
|
}
|
|
441
456
|
|
|
442
457
|
.tlui-cmt-reaction__count {
|
|
@@ -539,7 +554,8 @@
|
|
|
539
554
|
--tlui-cmt-marker-transition: 0.08s ease;
|
|
540
555
|
--tlui-cmt-thread-padding: 12px;
|
|
541
556
|
--tlui-cmt-thread-gap: 0px;
|
|
542
|
-
|
|
557
|
+
/* Hit-area width of one .tlui-cmt-thread__action button (drawn as a 32px square inset 4px). */
|
|
558
|
+
--tlui-cmt-thread-action-size: 40px;
|
|
543
559
|
/* A floating pill's edge — its hairline ring is the only thing separating it from the panel it
|
|
544
560
|
overlaps, so the ring is a theme token rather than a literal black: a near-black hairline
|
|
545
561
|
vanishes on a dark panel, where the divider token lightens instead. The drop below it stays
|
|
@@ -602,47 +618,28 @@
|
|
|
602
618
|
space-between alone would leave them at the left). */
|
|
603
619
|
margin-left: auto;
|
|
604
620
|
}
|
|
605
|
-
/*
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
.
|
|
610
|
-
|
|
611
|
-
display: flex;
|
|
612
|
-
align-items: center;
|
|
613
|
-
justify-content: center;
|
|
621
|
+
/* These are TldrawUiButtons (type="icon"), so the 40px box, the 32px ::after square carrying the
|
|
622
|
+
hover wash, and the hover z-index flip all come from .tlui-button — the hit area (not the
|
|
623
|
+
::after) is what's clickable, and it never reaches past its row. Only the bits that differ from
|
|
624
|
+
a toolbar button are set here, at .tlui-button's specificity + 1 so stylesheet order can't
|
|
625
|
+
decide the winner. */
|
|
626
|
+
.tlui-button.tlui-cmt-thread__action {
|
|
614
627
|
width: 40px;
|
|
615
|
-
height: 40px;
|
|
616
628
|
padding: 0;
|
|
617
|
-
border: none;
|
|
618
629
|
border-radius: 6px;
|
|
619
|
-
background: transparent;
|
|
620
630
|
color: var(--tl-color-text-3);
|
|
621
631
|
font-size: 14px;
|
|
622
632
|
line-height: 1;
|
|
623
|
-
cursor: pointer;
|
|
624
633
|
}
|
|
625
634
|
/* Overlap each button 4px into the previous so the 32px visual squares sit 4px apart, not 8px.
|
|
626
635
|
Left-only, so the last button's hit area still stops at its row's right edge. */
|
|
627
636
|
.tlui-cmt-thread__action + .tlui-cmt-thread__action {
|
|
628
637
|
margin-left: -4px;
|
|
629
638
|
}
|
|
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 {
|
|
639
|
+
.tlui-button.tlui-cmt-thread__action:hover {
|
|
637
640
|
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
641
|
}
|
|
645
|
-
.tlui-cmt-thread__action--danger:hover {
|
|
642
|
+
.tlui-button.tlui-cmt-thread__action--danger:hover {
|
|
646
643
|
color: var(--tl-color-danger);
|
|
647
644
|
}
|
|
648
645
|
.tlui-cmt-thread__resolved {
|
|
@@ -725,7 +722,7 @@
|
|
|
725
722
|
.tlui-cmt-canvas-preview--thread .tlui-cmt-card {
|
|
726
723
|
width: 100%;
|
|
727
724
|
gap: 0;
|
|
728
|
-
padding: 0
|
|
725
|
+
padding: 0 var(--tlui-cmt-thread-padding);
|
|
729
726
|
background: transparent;
|
|
730
727
|
border: none;
|
|
731
728
|
}
|
|
@@ -754,14 +751,17 @@
|
|
|
754
751
|
.tlui-cmt-stack-list .tlui-cmt-card .tlui-cmt-head {
|
|
755
752
|
min-height: 40px;
|
|
756
753
|
}
|
|
757
|
-
/*
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
754
|
+
/* Reserve head room only for pills that actually rendered (one child / two children) — pill count
|
|
755
|
+
is a permissions question (canModifyComment), not ownership, and empty slots (read-only viewers)
|
|
756
|
+
keep the full row. Sized from the 40px hit areas (4px overlap), minus the card padding the
|
|
757
|
+
right:0 pills sit over; text stops 4px shy of the visible square. */
|
|
758
|
+
.tlui-cmt-thread .tlui-cmt-card:has(.tlui-cmt-card__actions > *) .tlui-cmt-head {
|
|
759
|
+
padding-right: calc(var(--tlui-cmt-thread-action-size) - var(--tlui-cmt-thread-padding));
|
|
762
760
|
}
|
|
763
|
-
.tlui-cmt-thread .tlui-cmt-card
|
|
764
|
-
padding-right: calc(
|
|
761
|
+
.tlui-cmt-thread .tlui-cmt-card:has(.tlui-cmt-card__actions > * + *) .tlui-cmt-head {
|
|
762
|
+
padding-right: calc(
|
|
763
|
+
2 * var(--tlui-cmt-thread-action-size) - 4px - var(--tlui-cmt-thread-padding)
|
|
764
|
+
);
|
|
765
765
|
}
|
|
766
766
|
|
|
767
767
|
/* comments list — thread summaries, one per row (used by the canvas sidebar) */
|
|
@@ -786,36 +786,34 @@
|
|
|
786
786
|
color: var(--tl-color-text-1);
|
|
787
787
|
}
|
|
788
788
|
|
|
789
|
-
/*
|
|
790
|
-
.tlui-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
justify-content: center;
|
|
789
|
+
/* A small square icon button in the sidebar header (filter funnel, overflow …). Also a
|
|
790
|
+
TldrawUiButton, but a bespoke 26px size — so it has to override .tlui-button's 40px box, and
|
|
791
|
+
`min-width` in particular would otherwise beat `width`. Selectors carry .tlui-button so
|
|
792
|
+
stylesheet order can't decide the winner. */
|
|
793
|
+
.tlui-button.tlui-cmt-header-btn {
|
|
795
794
|
width: 26px;
|
|
795
|
+
min-width: 26px;
|
|
796
796
|
height: 26px;
|
|
797
797
|
padding: 0;
|
|
798
|
-
border: none;
|
|
799
798
|
border-radius: 6px;
|
|
800
|
-
background: transparent;
|
|
801
799
|
color: var(--tl-color-text-2);
|
|
802
|
-
cursor: pointer;
|
|
803
800
|
}
|
|
804
801
|
/* 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-
|
|
807
|
-
|
|
808
|
-
|
|
802
|
+
(see `.tlui-cmt-list__header-actions`). Unlike the thread actions, the hover fill sits on the
|
|
803
|
+
button itself, so .tlui-button's ::after wash is cleared and the pseudo-element is left doing
|
|
804
|
+
nothing but widening the hit area. */
|
|
805
|
+
.tlui-button.tlui-cmt-header-btn::after {
|
|
809
806
|
inset: -6px -1px;
|
|
807
|
+
background: none;
|
|
810
808
|
}
|
|
811
|
-
.tlui-cmt-header-btn:first-child::after {
|
|
809
|
+
.tlui-button.tlui-cmt-header-btn:first-child::after {
|
|
812
810
|
left: -6px;
|
|
813
811
|
}
|
|
814
|
-
.tlui-cmt-header-btn:last-child::after {
|
|
812
|
+
.tlui-button.tlui-cmt-header-btn:last-child::after {
|
|
815
813
|
right: -6px;
|
|
816
814
|
}
|
|
817
|
-
.tlui-cmt-header-btn:hover,
|
|
818
|
-
.tlui-cmt-header-btn[data-state='open'] {
|
|
815
|
+
.tlui-button.tlui-cmt-header-btn:hover,
|
|
816
|
+
.tlui-button.tlui-cmt-header-btn[data-state='open'] {
|
|
819
817
|
background: var(--tl-color-muted-2);
|
|
820
818
|
color: var(--tl-color-text-1);
|
|
821
819
|
}
|
|
@@ -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: 'short' }).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: 'short' }).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: 'short' }).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
|
@@ -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,116 @@
|
|
|
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, the eye pair, a filter — or because the asset
|
|
6
|
+
* reads wrong at the size it's needed, which is the case for both resolved checks below. Inline
|
|
7
|
+
* also means they render without an `AssetUrlsProvider` and without the consumer serving files.
|
|
8
|
+
*
|
|
9
|
+
* All internal — none are exported from the package. */
|
|
10
|
+
|
|
11
|
+
/** The check inside a resolved comment pin. A drawn check, not the '✓' glyph — that character sits
|
|
12
|
+
* off-baseline and varies by font. Heavier and rounder than `CheckIcon`: it has to hold its weight
|
|
13
|
+
* against the pin's 28px filled face, where the asset check reads too light. */
|
|
14
|
+
export function PinCheckIcon() {
|
|
15
|
+
return (
|
|
16
|
+
<svg
|
|
17
|
+
viewBox="0 0 24 24"
|
|
18
|
+
width="15"
|
|
19
|
+
height="15"
|
|
20
|
+
fill="none"
|
|
21
|
+
stroke="currentColor"
|
|
22
|
+
strokeWidth="2.5"
|
|
23
|
+
strokeLinecap="round"
|
|
24
|
+
strokeLinejoin="round"
|
|
25
|
+
aria-hidden="true"
|
|
26
|
+
>
|
|
27
|
+
<path d="M4 12.5l5 5L20 6.5" />
|
|
28
|
+
</svg>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The check beside the "Resolved" label on a comments list row. Thinner than `PinCheckIcon` — it
|
|
33
|
+
* sits next to text at 12px, so it's drawn to match the label's weight rather than shout. */
|
|
34
|
+
export function CheckIcon() {
|
|
35
|
+
return (
|
|
36
|
+
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true">
|
|
37
|
+
<path
|
|
38
|
+
d="M2.5 6.2 4.7 8.4 9.5 3.6"
|
|
39
|
+
stroke="currentColor"
|
|
40
|
+
strokeWidth="1.5"
|
|
41
|
+
strokeLinecap="round"
|
|
42
|
+
strokeLinejoin="round"
|
|
43
|
+
/>
|
|
44
|
+
</svg>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The reaction picker's trigger. */
|
|
49
|
+
export function SmileyIcon() {
|
|
50
|
+
return (
|
|
51
|
+
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" aria-hidden="true">
|
|
52
|
+
<path
|
|
53
|
+
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"
|
|
54
|
+
fill="currentColor"
|
|
55
|
+
fillRule="evenodd"
|
|
56
|
+
clipRule="evenodd"
|
|
57
|
+
/>
|
|
58
|
+
</svg>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The composer's post button — an up arrow. */
|
|
63
|
+
export function SendIcon() {
|
|
64
|
+
return (
|
|
65
|
+
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" aria-hidden="true">
|
|
66
|
+
<path
|
|
67
|
+
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"
|
|
68
|
+
fill="currentColor"
|
|
69
|
+
fillRule="evenodd"
|
|
70
|
+
clipRule="evenodd"
|
|
71
|
+
/>
|
|
72
|
+
</svg>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Comment pins are showing. Pairs with `EyeClosedIcon` — the two must stay the same family. */
|
|
77
|
+
export function EyeOpenIcon() {
|
|
78
|
+
return (
|
|
79
|
+
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" aria-hidden="true">
|
|
80
|
+
<path
|
|
81
|
+
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"
|
|
82
|
+
fill="currentColor"
|
|
83
|
+
fillRule="evenodd"
|
|
84
|
+
clipRule="evenodd"
|
|
85
|
+
/>
|
|
86
|
+
</svg>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Comment pins are hidden. */
|
|
91
|
+
export function EyeClosedIcon() {
|
|
92
|
+
return (
|
|
93
|
+
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" aria-hidden="true">
|
|
94
|
+
<path
|
|
95
|
+
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"
|
|
96
|
+
fill="currentColor"
|
|
97
|
+
fillRule="evenodd"
|
|
98
|
+
clipRule="evenodd"
|
|
99
|
+
/>
|
|
100
|
+
</svg>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The sidebar's filter menu trigger. */
|
|
105
|
+
export function FilterIcon() {
|
|
106
|
+
return (
|
|
107
|
+
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" aria-hidden="true">
|
|
108
|
+
<path
|
|
109
|
+
d="M5.5 3C4.67157 3 4 3.67157 4 4.5C4 5.32843 4.67157 6 5.5 6C6.32843 6 7 5.32843 7 4.5C7 3.67157 6.32843 3 5.5 3ZM3 5C3.01671 5 3.03323 4.99918 3.04952 4.99758C3.28022 6.1399 4.28967 7 5.5 7C6.71033 7 7.71978 6.1399 7.95048 4.99758C7.96677 4.99918 7.98329 5 8 5H13.5C13.7761 5 14 4.77614 14 4.5C14 4.22386 13.7761 4 13.5 4H8C7.98329 4 7.96677 4.00082 7.95048 4.00242C7.71978 2.86009 6.71033 2 5.5 2C4.28967 2 3.28022 2.86009 3.04952 4.00242C3.03323 4.00082 3.01671 4 3 4H1.5C1.22386 4 1 4.22386 1 4.5C1 4.77614 1.22386 5 1.5 5H3ZM11.9505 10.9976C11.7198 12.1399 10.7103 13 9.5 13C8.28967 13 7.28022 12.1399 7.04952 10.9976C7.03323 10.9992 7.01671 11 7 11H1.5C1.22386 11 1 10.7761 1 10.5C1 10.2239 1.22386 10 1.5 10H7C7.01671 10 7.03323 10.0008 7.04952 10.0024C7.28022 8.8601 8.28967 8 9.5 8C10.7103 8 11.7198 8.8601 11.9505 10.0024C11.9668 10.0008 11.9833 10 12 10H13.5C13.7761 10 14 10.2239 14 10.5C14 10.7761 13.7761 11 13.5 11H12C11.9833 11 11.9668 10.9992 11.9505 10.9976ZM8 10.5C8 9.67157 8.67157 9 9.5 9C10.3284 9 11 9.67157 11 10.5C11 11.3284 10.3284 12 9.5 12C8.67157 12 8 11.3284 8 10.5Z"
|
|
110
|
+
fill="currentColor"
|
|
111
|
+
fillRule="evenodd"
|
|
112
|
+
clipRule="evenodd"
|
|
113
|
+
/>
|
|
114
|
+
</svg>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { getVisibleViewport } from './visual-viewport'
|
|
3
|
+
|
|
4
|
+
function makeWindow(
|
|
5
|
+
innerWidth: number,
|
|
6
|
+
innerHeight: number,
|
|
7
|
+
vv?: { offsetTop: number; offsetLeft: number; width: number; height: number }
|
|
8
|
+
): Window {
|
|
9
|
+
return { innerWidth, innerHeight, visualViewport: vv ?? null } as unknown as Window
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe('getVisibleViewport', () => {
|
|
13
|
+
it('reads the visual viewport, carrying its offset into the derived edges', () => {
|
|
14
|
+
const win = makeWindow(1024, 768, { offsetTop: 40, offsetLeft: 10, width: 900, height: 500 })
|
|
15
|
+
expect(getVisibleViewport(win)).toEqual({
|
|
16
|
+
top: 40,
|
|
17
|
+
left: 10,
|
|
18
|
+
width: 900,
|
|
19
|
+
height: 500,
|
|
20
|
+
bottom: 540,
|
|
21
|
+
right: 910,
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('falls back to the layout viewport when visualViewport is absent', () => {
|
|
26
|
+
const win = makeWindow(1024, 768)
|
|
27
|
+
expect(getVisibleViewport(win)).toEqual({
|
|
28
|
+
top: 0,
|
|
29
|
+
left: 0,
|
|
30
|
+
width: 1024,
|
|
31
|
+
height: 768,
|
|
32
|
+
bottom: 768,
|
|
33
|
+
right: 1024,
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
})
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// The visual viewport is the region actually on screen. The software keyboard (and pinch-zoom)
|
|
2
|
+
// shrink it while leaving the layout viewport — and CSS `dvh` — unchanged, most notably on iOS.
|
|
3
|
+
// So placing a panel above the keyboard means measuring against `window.visualViewport`, not CSS.
|
|
4
|
+
// These helpers are the single place the commenting layer reads it.
|
|
5
|
+
|
|
6
|
+
/** The on-screen viewport region in window coordinates, falling back to the layout viewport where
|
|
7
|
+
* `visualViewport` is unavailable. `top`/`left` carry the viewport's own offset — on iOS this is
|
|
8
|
+
* non-zero while the keyboard is open, and dropping it misplaces anything measured against it. */
|
|
9
|
+
export interface VisibleViewport {
|
|
10
|
+
top: number
|
|
11
|
+
left: number
|
|
12
|
+
width: number
|
|
13
|
+
height: number
|
|
14
|
+
bottom: number
|
|
15
|
+
right: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Read the visible viewport once. Call it inside a `visualViewport` resize/scroll handler to keep
|
|
19
|
+
* a measurement current as the keyboard opens and closes. */
|
|
20
|
+
export function getVisibleViewport(win: Window): VisibleViewport {
|
|
21
|
+
const vv = win.visualViewport
|
|
22
|
+
const top = vv ? vv.offsetTop : 0
|
|
23
|
+
const left = vv ? vv.offsetLeft : 0
|
|
24
|
+
const width = vv ? vv.width : win.innerWidth
|
|
25
|
+
const height = vv ? vv.height : win.innerHeight
|
|
26
|
+
return { top, left, width, height, bottom: top + height, right: left + width }
|
|
27
|
+
}
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../src/ui/tooltip-button.tsx"],
|
|
4
|
-
"sourcesContent": ["import { type ButtonHTMLAttributes, forwardRef } from 'react'\nimport { TldrawUiTooltip } from 'tldraw'\n\ninterface TooltipButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n\ttooltip: string\n}\n\n/** An icon button labelled by a styled tooltip. Forwards ref and props to the underlying button\n * so `asChild` triggers (dropdown, tooltip) both compose onto the real element. */\nexport const TooltipButton = forwardRef<HTMLButtonElement, TooltipButtonProps>(\n\tfunction TooltipButton({ tooltip, children, ...props }, ref) {\n\t\treturn (\n\t\t\t<TldrawUiTooltip content={tooltip}>\n\t\t\t\t<button ref={ref} type=\"button\" aria-label={tooltip} {...props}>\n\t\t\t\t\t{children}\n\t\t\t\t</button>\n\t\t\t</TldrawUiTooltip>\n\t\t)\n\t}\n)\n"],
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAaI;AAbJ,mBAAsD;AACtD,oBAAgC;AAQzB,MAAM,oBAAgB;AAAA,EAC5B,SAASA,eAAc,EAAE,SAAS,UAAU,GAAG,MAAM,GAAG,KAAK;AAC5D,WACC,4CAAC,iCAAgB,SAAS,SACzB,sDAAC,YAAO,KAAU,MAAK,UAAS,cAAY,SAAU,GAAG,OACvD,UACF,GACD;AAAA,EAEF;AACD;",
|
|
6
|
-
"names": ["TooltipButton"]
|
|
7
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { jsx } from "react/jsx-runtime";
|
|
2
|
-
import { forwardRef } from "react";
|
|
3
|
-
import { TldrawUiTooltip } from "tldraw";
|
|
4
|
-
const TooltipButton = forwardRef(
|
|
5
|
-
function TooltipButton2({ tooltip, children, ...props }, ref) {
|
|
6
|
-
return /* @__PURE__ */ jsx(TldrawUiTooltip, { content: tooltip, children: /* @__PURE__ */ jsx("button", { ref, type: "button", "aria-label": tooltip, ...props, children }) });
|
|
7
|
-
}
|
|
8
|
-
);
|
|
9
|
-
export {
|
|
10
|
-
TooltipButton
|
|
11
|
-
};
|
|
12
|
-
//# sourceMappingURL=tooltip-button.mjs.map
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["../../src/ui/tooltip-button.tsx"],
|
|
4
|
-
"sourcesContent": ["import { type ButtonHTMLAttributes, forwardRef } from 'react'\nimport { TldrawUiTooltip } from 'tldraw'\n\ninterface TooltipButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {\n\ttooltip: string\n}\n\n/** An icon button labelled by a styled tooltip. Forwards ref and props to the underlying button\n * so `asChild` triggers (dropdown, tooltip) both compose onto the real element. */\nexport const TooltipButton = forwardRef<HTMLButtonElement, TooltipButtonProps>(\n\tfunction TooltipButton({ tooltip, children, ...props }, ref) {\n\t\treturn (\n\t\t\t<TldrawUiTooltip content={tooltip}>\n\t\t\t\t<button ref={ref} type=\"button\" aria-label={tooltip} {...props}>\n\t\t\t\t\t{children}\n\t\t\t\t</button>\n\t\t\t</TldrawUiTooltip>\n\t\t)\n\t}\n)\n"],
|
|
5
|
-
"mappings": "AAaI;AAbJ,SAAoC,kBAAkB;AACtD,SAAS,uBAAuB;AAQzB,MAAM,gBAAgB;AAAA,EAC5B,SAASA,eAAc,EAAE,SAAS,UAAU,GAAG,MAAM,GAAG,KAAK;AAC5D,WACC,oBAAC,mBAAgB,SAAS,SACzB,8BAAC,YAAO,KAAU,MAAK,UAAS,cAAY,SAAU,GAAG,OACvD,UACF,GACD;AAAA,EAEF;AACD;",
|
|
6
|
-
"names": ["TooltipButton"]
|
|
7
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { type ButtonHTMLAttributes, forwardRef } from 'react'
|
|
2
|
-
import { TldrawUiTooltip } from 'tldraw'
|
|
3
|
-
|
|
4
|
-
interface TooltipButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
|
5
|
-
tooltip: string
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/** An icon button labelled by a styled tooltip. Forwards ref and props to the underlying button
|
|
9
|
-
* so `asChild` triggers (dropdown, tooltip) both compose onto the real element. */
|
|
10
|
-
export const TooltipButton = forwardRef<HTMLButtonElement, TooltipButtonProps>(
|
|
11
|
-
function TooltipButton({ tooltip, children, ...props }, ref) {
|
|
12
|
-
return (
|
|
13
|
-
<TldrawUiTooltip content={tooltip}>
|
|
14
|
-
<button ref={ref} type="button" aria-label={tooltip} {...props}>
|
|
15
|
-
{children}
|
|
16
|
-
</button>
|
|
17
|
-
</TldrawUiTooltip>
|
|
18
|
-
)
|
|
19
|
-
}
|
|
20
|
-
)
|