@tldraw/collaboration 0.0.0-bootstrap → 5.3.0-canary.04044ed9e96d

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/collaboration.css CHANGED
@@ -2,87 +2,227 @@
2
2
  /* This file is created by the copy-css-files.mjs script in packages/collaboration. */
3
3
  /* It combines the stylesheets of the collaboration umbrella's sub-packages. */
4
4
 
5
+ /* avatar — a person's image, or a single-initial coloured circle. Shared by the mention picker and,
6
+ via @tldraw/commenting, the comment UI. */
7
+ .tlui-cmt-avatar {
8
+ width: 28px;
9
+ height: 28px;
10
+ border-radius: 50%;
11
+ background: var(--tl-color-selected);
12
+ color: #fff;
13
+ display: flex;
14
+ align-items: center;
15
+ justify-content: center;
16
+ font-size: 11px;
17
+ font-weight: 600;
18
+ flex-shrink: 0;
19
+ }
20
+
21
+ .tlui-cmt-avatar--image {
22
+ object-fit: cover;
23
+ background: none;
24
+ }
25
+
26
+ /* mention pill */
27
+ .tlui-cmt-mention {
28
+ color: var(--tl-color-selected);
29
+ font-weight: 600;
30
+ background: color-mix(in srgb, var(--tl-color-selected) 12%, transparent);
31
+ padding: 0 4px;
32
+ border-radius: 4px;
33
+ }
34
+
35
+ /* mention picker */
36
+ .tlui-cmt-mention-list {
37
+ display: flex;
38
+ flex-direction: column;
39
+ gap: 4px;
40
+ padding: 4px;
41
+ width: 100%;
42
+ box-sizing: border-box;
43
+ max-height: 260px;
44
+ overflow-y: auto;
45
+ color: var(--tl-color-text-1, #1d1d1d);
46
+ background: var(--tl-color-panel, #fff);
47
+ border-radius: 10px;
48
+ /* shadow-3 (like the context menu) already carries a 1px inset edge — a separate `border`
49
+ on top of it double-outlines the popup. */
50
+ box-shadow: var(--tl-shadow-3, 0 2px 12px rgba(0, 0, 0, 0.14));
51
+ }
52
+
53
+ .tlui-cmt-mention-list__item {
54
+ display: flex;
55
+ align-items: center;
56
+ gap: 8px;
57
+ padding: 6px 8px;
58
+ border: none;
59
+ background: transparent;
60
+ border-radius: 6px;
61
+ cursor: pointer;
62
+ text-align: left;
63
+ color: var(--tl-color-text-1);
64
+ font: inherit;
65
+ }
66
+
67
+ .tlui-cmt-mention-list__item--active,
68
+ .tlui-cmt-mention-list__item:hover {
69
+ background: var(--tl-color-muted-2, rgba(0, 0, 0, 0.06));
70
+ }
71
+
72
+ .tlui-cmt-mention-list__text {
73
+ display: flex;
74
+ flex-direction: column;
75
+ min-width: 0;
76
+ line-height: 1.3;
77
+ }
78
+
79
+ .tlui-cmt-mention-list__name {
80
+ font-size: 12px;
81
+ font-weight: 500;
82
+ white-space: nowrap;
83
+ overflow: hidden;
84
+ text-overflow: ellipsis;
85
+ }
86
+
87
+ .tlui-cmt-mention-list__you {
88
+ margin-left: 4px;
89
+ font-weight: 400;
90
+ color: var(--tl-color-text-3);
91
+ }
92
+
93
+ .tlui-cmt-mention-list__secondary {
94
+ font-size: 12px;
95
+ color: var(--tl-color-text-3);
96
+ white-space: nowrap;
97
+ overflow: hidden;
98
+ text-overflow: ellipsis;
99
+ }
100
+
101
+ .tlui-cmt-mention-list--empty {
102
+ padding: 8px 10px;
103
+ width: 100%;
104
+ box-sizing: border-box;
105
+ color: var(--tl-color-text-3);
106
+ font-size: 13px;
107
+ }
108
+
109
+ /* the @-picker popup — anchored under the editor field, positioned by the suggestion plugin */
110
+ .tlui-cmt-mention-popup {
111
+ position: fixed;
112
+ z-index: var(--tl-layer-menus);
113
+ }
114
+
5
115
  /* Real comment UI components. Co-located so styles travel to any view (studio,
6
116
  canvas), and themed via tldraw's --tl-color tokens so they follow light/dark. */
7
- .cmt-card {
117
+ .tlui-cmt-card {
8
118
  position: relative;
9
119
  display: flex;
10
- gap: 10px;
11
- padding: 12px;
120
+ flex-direction: column;
121
+ gap: 8px;
122
+ padding: 8px;
12
123
  width: 260px;
13
124
  background: var(--tl-color-panel);
14
125
  border: 1px solid var(--tl-color-divider);
15
- border-radius: 12px;
126
+ border-radius: var(--tl-radius-3);
127
+ }
128
+
129
+ /* The avatar lives in the byline row (see Byline), so the body text below indents to line up with
130
+ the name — the avatar's width plus the head's gap. */
131
+ .tlui-cmt-body {
132
+ padding-left: calc(28px + 6px);
16
133
  }
17
134
 
18
- /* hover-revealed controls (edit…) at the card's top-right */
19
- .cmt-card__actions {
135
+ /* hover-revealed controls (edit, react…) stacked at the card's top-right */
136
+ .tlui-cmt-card__actions {
20
137
  position: absolute;
21
138
  top: 6px;
22
139
  right: 6px;
23
140
  display: flex;
24
- gap: 2px;
141
+ gap: 0;
25
142
  opacity: 0;
26
143
  }
27
- .cmt-card:hover .cmt-card__actions,
28
- .cmt-card:focus-within .cmt-card__actions {
144
+ .tlui-cmt-card:hover .tlui-cmt-card__actions,
145
+ .tlui-cmt-card:focus-within .tlui-cmt-card__actions,
146
+ /* an open menu is portaled out of the card, so neither :hover nor :focus-within survives moving
147
+ the pointer into it — keep the controls up while their menu is open */
148
+ .tlui-cmt-card__actions:has([data-state='open']) {
29
149
  opacity: 1;
30
150
  }
31
151
 
32
- .cmt-body {
152
+ .tlui-cmt-body {
33
153
  flex: 1;
34
154
  min-width: 0;
35
155
  }
36
156
 
37
- .cmt-head {
157
+ .tlui-cmt-head {
38
158
  display: flex;
39
- align-items: baseline;
159
+ align-items: center;
40
160
  gap: 6px;
41
161
  }
42
162
 
43
- .cmt-author {
44
- font-weight: 600;
45
- font-size: 13px;
163
+ .tlui-cmt-author {
164
+ font-weight: 500;
165
+ font-size: 12px;
46
166
  color: var(--tl-color-text-1);
167
+ /* a long name ellipsizes rather than pushing the time out or running under the actions */
168
+ min-width: 0;
169
+ overflow: hidden;
170
+ text-overflow: ellipsis;
171
+ white-space: nowrap;
47
172
  }
48
173
 
49
- .cmt-time {
50
- font-size: 11px;
174
+ .tlui-cmt-time {
175
+ font-size: 12px;
51
176
  color: var(--tl-color-text-3);
177
+ flex-shrink: 0;
178
+ white-space: nowrap;
52
179
  }
53
180
 
54
- .cmt-text {
55
- margin: 3px 0 0;
56
- font-size: 13px;
181
+ /* In the hover preview a long comment truncates with an ellipsis rather than growing the card —
182
+ ~7 lines (≈120px at this line height). Preview only. */
183
+ .tlui-cmt-canvas-preview--thread .tlui-cmt-text {
184
+ display: -webkit-box;
185
+ -webkit-box-orient: vertical;
186
+ -webkit-line-clamp: 7;
187
+ line-clamp: 7;
188
+ overflow: hidden;
189
+ }
190
+
191
+ .tlui-cmt-text {
192
+ margin: 0 0 6px;
193
+ font-size: 12px;
57
194
  line-height: 1.4;
58
195
  color: var(--tl-color-text-1);
196
+ /* Break long unbroken strings (URLs, keyboard-mashing) so they wrap inside the card instead of
197
+ overflowing it. `anywhere` also lets the flex body shrink to the card width. */
198
+ overflow-wrap: anywhere;
59
199
  }
60
200
 
61
- .cmt-edited {
201
+ .tlui-cmt-edited {
62
202
  font-style: italic;
63
203
  }
64
204
 
65
205
  /* rendered rich text inside a comment body — bare tags emitted by the comment renderer
66
206
  (paragraphs, lists, code, highlight, links), matching the limited comment extension set. */
67
- .cmt-text p {
207
+ .tlui-cmt-text p {
68
208
  margin: 0;
69
209
  }
70
210
 
71
- .cmt-text p + p,
72
- .cmt-text p + ul,
73
- .cmt-text p + ol,
74
- .cmt-text ul + p,
75
- .cmt-text ol + p {
211
+ .tlui-cmt-text p + p,
212
+ .tlui-cmt-text p + ul,
213
+ .tlui-cmt-text p + ol,
214
+ .tlui-cmt-text ul + p,
215
+ .tlui-cmt-text ol + p {
76
216
  margin-top: 6px;
77
217
  }
78
218
 
79
- .cmt-text ul,
80
- .cmt-text ol {
219
+ .tlui-cmt-text ul,
220
+ .tlui-cmt-text ol {
81
221
  margin: 0;
82
222
  padding-left: 18px;
83
223
  }
84
224
 
85
- .cmt-text code {
225
+ .tlui-cmt-text code {
86
226
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
87
227
  font-size: 0.92em;
88
228
  background: var(--tl-color-muted-2);
@@ -90,212 +230,272 @@
90
230
  padding: 0 3px;
91
231
  }
92
232
 
93
- .cmt-text mark {
233
+ .tlui-cmt-text mark {
94
234
  background: color-mix(in srgb, #ffeb3b 60%, transparent);
95
235
  color: inherit;
96
236
  border-radius: 2px;
97
237
  }
98
238
 
99
- .cmt-text a {
239
+ .tlui-cmt-text a {
100
240
  color: var(--tl-color-selected);
101
241
  text-decoration: underline;
102
242
  }
103
243
 
104
- .cmt-avatar {
105
- width: 28px;
106
- height: 28px;
107
- border-radius: 50%;
108
- background: var(--tl-color-selected);
109
- color: #fff;
110
- display: flex;
111
- align-items: center;
112
- justify-content: center;
113
- font-size: 11px;
114
- font-weight: 600;
115
- flex-shrink: 0;
116
- }
117
-
118
- .cmt-avatar--image {
119
- object-fit: cover;
120
- background: none;
121
- }
244
+ /* Avatar and mention styles live in @tldraw/mentions (src/mention.css) and are bundled into
245
+ commenting.css by copy-css-files.mjs. */
122
246
 
123
247
  /* composer — an avatar beside a rounded-rect field holding the rich-text input and the send button.
124
248
  Formatting is applied via markdown and keyboard shortcuts (no toolbar). The avatar aligns to the
125
249
  top so it stays put as the field grows to multiple lines. */
126
- .cmt-composer {
250
+ .tlui-cmt-composer {
127
251
  display: flex;
128
252
  align-items: flex-start;
129
253
  gap: 10px;
130
254
  padding: 4px 0 8px 4px;
131
255
  }
132
256
 
133
- .cmt-composer__field {
257
+ .tlui-cmt-composer__field {
134
258
  flex: 1;
135
259
  min-width: 0;
136
260
  display: flex;
137
261
  align-items: center;
138
262
  gap: 6px;
139
- padding: 3px 3px 3px 12px;
140
- background: var(--tl-color-muted-2);
263
+ padding: 3px 3px 3px 8px;
264
+ background: var(--tl-color-muted-0);
141
265
  border: 1px solid var(--tl-color-divider);
142
- border-radius: var(--tl-radius-3);
266
+ border-radius: var(--tl-radius-2);
267
+ /* The whole field reads as the text input — clicking its empty area focuses the editor
268
+ (see CommentComposer). The send button restores its own pointer cursor. */
269
+ cursor: text;
143
270
  }
144
271
 
145
- .cmt-composer__field:focus-within {
272
+ .tlui-cmt-composer__field:focus-within {
146
273
  border-color: var(--tl-color-selected);
274
+ background: var(--tl-color-panel);
275
+ }
276
+
277
+ /* Expanded: the content no longer fits on one line beside the send button, so the input takes
278
+ the full width and the send button drops to its own row, right-aligned. */
279
+ .tlui-cmt-composer__field--expanded {
280
+ flex-wrap: wrap;
281
+ /* No vertical gap between the input and the send-button row it wraps onto. */
282
+ row-gap: 0;
283
+ }
284
+ .tlui-cmt-composer__field--expanded .tlui-cmt-composer__input-wrap {
285
+ flex-basis: 100%;
286
+ /* The field pads 8px left but only 3px right (the send button's inset). With the input
287
+ spanning the full width, give the text the same 8px on the right. */
288
+ padding-right: 5px;
289
+ }
290
+ .tlui-cmt-composer__field--expanded .tlui-cmt-send {
291
+ margin-left: auto;
147
292
  }
148
293
 
149
294
  /* the editable area, with the placeholder overlaid while it's empty */
150
- .cmt-composer__input-wrap {
295
+ .tlui-cmt-composer__input-wrap {
151
296
  position: relative;
152
297
  flex: 1;
153
298
  min-width: 0;
154
- padding: 3px 0;
299
+ /* Frames the input's line height symmetrically so the text sits centred in the field. Paired
300
+ with the input's min-height, which equals its line height (no gap for the text to top-align
301
+ into). Keeps the field's overall height. */
302
+ padding: 4.5px 0;
155
303
  }
156
304
 
157
- .cmt-input {
305
+ /* The hidden single-line measuring clone of the editor's content (see CommentComposer). */
306
+ .tlui-cmt-composer__mirror {
307
+ position: absolute;
308
+ top: 0;
309
+ left: 0;
310
+ visibility: hidden;
311
+ pointer-events: none;
312
+ white-space: nowrap;
313
+ width: max-content;
314
+ }
315
+
316
+ .tlui-cmt-input {
158
317
  font: inherit;
159
- font-size: 13px;
318
+ font-size: 12px;
160
319
  line-height: 1.4;
161
320
  color: var(--tl-color-text-1);
162
321
  outline: none;
163
- min-height: 20px;
164
- max-height: 40vh;
165
- overflow-y: auto;
322
+ min-height: 17px;
166
323
  white-space: pre-wrap;
167
324
  word-break: break-word;
168
325
  }
169
326
 
170
- .cmt-input p {
327
+ .tlui-cmt-input p {
171
328
  margin: 0;
172
329
  }
173
330
 
174
- .cmt-input p + p,
175
- .cmt-input p + ul,
176
- .cmt-input p + ol {
331
+ .tlui-cmt-input p + p,
332
+ .tlui-cmt-input p + ul,
333
+ .tlui-cmt-input p + ol {
177
334
  margin-top: 4px;
178
335
  }
179
336
 
180
- .cmt-input ul,
181
- .cmt-input ol {
337
+ .tlui-cmt-input ul,
338
+ .tlui-cmt-input ol {
182
339
  margin: 0;
183
340
  padding-left: 18px;
184
341
  }
185
342
 
186
- .cmt-input__placeholder {
343
+ .tlui-cmt-input__placeholder {
187
344
  position: absolute;
188
- top: 3px;
345
+ top: 4.5px;
189
346
  left: 0;
190
- font-size: 13px;
347
+ font-size: 12px;
191
348
  line-height: 1.4;
192
349
  color: var(--tl-color-text-3);
193
350
  pointer-events: none;
194
351
  }
195
352
 
196
- .cmt-send {
353
+ /* iOS Safari zooms the whole page in when a text input with a sub-16px font takes focus, and doesn't
354
+ zoom back out. Raising the font to 16px on touch devices suppresses that without disabling
355
+ pinch-zoom of the canvas (which a `maximum-scale` viewport lock would). Must come after both base
356
+ rules above: a media query adds no specificity, so it only wins the placeholder on source order. */
357
+ @media (pointer: coarse) {
358
+ .tlui-cmt-input,
359
+ .tlui-cmt-input__placeholder {
360
+ font-size: 16px;
361
+ }
362
+ }
363
+
364
+ /* A bare up-arrow icon button (matching the placement composer): no fill by default, a grey square
365
+ on hover, grey while there's nothing to send and dark once there is. */
366
+ .tlui-cmt-send {
367
+ flex-shrink: 0;
368
+ display: inline-flex;
369
+ align-items: center;
370
+ justify-content: center;
371
+ width: 28px;
372
+ height: 28px;
197
373
  border: 0;
198
374
  border-radius: var(--tl-radius-2);
199
- background: var(--tl-color-selected);
200
- color: #fff;
201
- font: inherit;
202
- font-size: 12px;
203
- font-weight: 600;
204
- padding: 6px 12px;
375
+ background: transparent;
376
+ color: var(--tl-color-text-3);
205
377
  cursor: pointer;
206
- align-self: flex-end;
378
+ align-self: center;
207
379
  }
208
380
 
209
- .cmt-send:hover,
210
- .pseudo-hover .cmt-send {
211
- background: color-mix(in srgb, var(--tl-color-selected) 88%, black);
381
+ .tlui-cmt-send:not(:disabled) {
382
+ color: var(--tl-color-text-1);
212
383
  }
213
384
 
214
- .cmt-send:active,
215
- .pseudo-active .cmt-send {
216
- background: color-mix(in srgb, var(--tl-color-selected) 78%, black);
385
+ .tlui-cmt-send:hover:not(:disabled) {
386
+ background: var(--tl-color-muted-2);
217
387
  }
218
388
 
219
- .cmt-send:focus-visible,
220
- .pseudo-focus-visible .cmt-send {
389
+ .tlui-cmt-send:active:not(:disabled),
390
+ .pseudo-active .tlui-cmt-send:not(:disabled) {
391
+ background: var(--tl-color-muted-1);
392
+ }
393
+
394
+ .tlui-cmt-send:focus-visible,
395
+ .pseudo-focus-visible .tlui-cmt-send {
221
396
  outline: 2px solid var(--tl-color-selected);
222
397
  outline-offset: 2px;
223
398
  }
224
399
 
225
- .cmt-send:disabled {
226
- opacity: 0.45;
400
+ .tlui-cmt-send:disabled {
401
+ background: transparent;
402
+ color: var(--tl-color-text-3);
227
403
  cursor: not-allowed;
228
404
  }
229
405
 
230
- /* pin */
231
- .cmt-pin {
232
- width: 34px;
233
- height: 34px;
234
- border-radius: 50% 50% 50% 3px;
235
- background: var(--tl-color-selected);
406
+ /* The shared marker treatment. To the pointer a comment pin and a count badge are the same
407
+ object — a token sitting on the canvas — so they rest and lift identically; everything shared
408
+ lives here so the two can't drift apart. */
409
+ .tlui-cmt-marker {
410
+ box-shadow: var(--tlui-cmt-marker-shadow);
411
+ cursor: pointer;
412
+ transition:
413
+ transform var(--tlui-cmt-marker-transition),
414
+ box-shadow var(--tlui-cmt-marker-transition);
415
+ }
416
+
417
+ .tlui-cmt-marker:hover {
418
+ transform: scale(var(--tlui-cmt-marker-hover-scale));
419
+ }
420
+
421
+ /* The hover shadow is for closed markers only — an open one keeps its selection ring, which this
422
+ box-shadow would otherwise override (the hover selector outranks the --open rule). */
423
+ .tlui-cmt-marker:not(.tlui-cmt-marker--open):hover {
424
+ box-shadow: var(--tlui-cmt-marker-shadow-hover);
425
+ }
426
+
427
+ /* Count badges don't react to hover — no scale, no shadow lift. Only pins do. */
428
+ .tlui-cmt-count-badge:hover {
429
+ transform: none;
430
+ box-shadow: var(--tlui-cmt-marker-shadow);
431
+ }
432
+
433
+ /* Pins don't scale on hover — only the shadow lifts. */
434
+ .tlui-cmt-pin:hover {
435
+ transform: none;
436
+ }
437
+
438
+ /* open: the thread (or a stack's list) is showing — the active marker just keeps its lift shadow,
439
+ no selection ring. */
440
+ .tlui-cmt-marker--open {
441
+ box-shadow: var(--tlui-cmt-marker-shadow);
442
+ }
443
+
444
+ /* pin — a canvas marker: a white teardrop holding the author's avatar circle, so a thread is
445
+ identifiable at a glance. */
446
+ .tlui-cmt-pin {
447
+ --tlui-cmt-marker-size: var(--tlui-cmt-pin-size);
448
+ width: var(--tlui-cmt-marker-size);
449
+ height: var(--tlui-cmt-marker-size);
450
+ border-radius: 50% 50% 50% 0;
451
+ background: var(--tl-color-panel);
236
452
  color: #fff;
237
453
  display: flex;
238
454
  align-items: center;
239
455
  justify-content: center;
240
456
  font-size: 14px;
241
457
  font-weight: 600;
242
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
243
- cursor: pointer;
244
- transition:
245
- transform 0.08s ease,
246
- box-shadow 0.08s ease;
247
- }
248
-
249
- .cmt-pin:hover,
250
- .pseudo-hover .cmt-pin {
251
- transform: scale(1.08);
252
- box-shadow: 0 3px 10px rgba(0, 0, 0, 0.32);
253
458
  }
254
-
255
- /* open: the thread is showing — a selection ring marks the active pin */
256
- .cmt-pin--open {
257
- box-shadow:
258
- 0 0 0 3px var(--tl-color-panel),
259
- 0 0 0 5px var(--tl-color-selected),
260
- 0 2px 6px rgba(0, 0, 0, 0.25);
459
+ /* The pin holds the author's 22px avatar circle, centred in its 28px face. */
460
+ .tlui-cmt-pin .tlui-cmt-avatar {
461
+ width: 22px;
462
+ height: 22px;
261
463
  }
262
464
 
263
465
  /* resolved: a solid, muted grey (the default --tl-color-muted-1 is 10% black, so it goes
264
466
  translucent over the canvas). Dark mode gets its own grey — the token's dark value is near-black. */
265
- .cmt-pin--resolved {
467
+ .tlui-cmt-pin--resolved {
266
468
  background: #dfe0e1;
267
469
  color: hsl(214, 8%, 42%);
268
470
  }
269
- .tl-theme__dark .cmt-pin--resolved {
471
+ .tl-theme__dark .tlui-cmt-pin--resolved {
270
472
  background: hsl(240, 5%, 30%);
271
473
  color: hsl(0, 0%, 72%);
272
474
  }
273
475
 
274
- /* count badge */
275
- .cmt-count-badge {
276
- width: 34px;
277
- height: 34px;
476
+ /* count badge — the same marker as a pin. */
477
+ .tlui-cmt-count-badge {
478
+ width: var(--tlui-cmt-marker-size);
479
+ height: var(--tlui-cmt-marker-size);
278
480
  border-radius: 50%;
279
481
  display: flex;
280
482
  align-items: center;
281
483
  justify-content: center;
282
484
  background: var(--tl-color-text-1);
283
485
  color: var(--tl-color-panel);
284
- font-size: 13px;
486
+ font-size: 12px;
285
487
  font-weight: 700;
286
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.25);
287
488
  user-select: none;
288
489
  }
289
490
 
290
491
  /* thread */
291
- .cmt-thread {
492
+ .tlui-cmt-thread {
292
493
  display: flex;
293
494
  flex-direction: column;
294
- gap: 8px;
295
495
  width: 300px;
296
496
  }
297
497
 
298
- .cmt-thread__head {
498
+ .tlui-cmt-thread__head {
299
499
  display: flex;
300
500
  align-items: center;
301
501
  gap: 8px;
@@ -306,7 +506,7 @@
306
506
  letter-spacing: 0.04em;
307
507
  }
308
508
 
309
- .cmt-badge {
509
+ .tlui-cmt-badge {
310
510
  font-size: 10px;
311
511
  font-weight: 600;
312
512
  text-transform: none;
@@ -318,134 +518,111 @@
318
518
  }
319
519
 
320
520
  /* reactions */
321
- .cmt-reactions {
521
+ .tlui-cmt-reactions {
322
522
  display: flex;
323
- gap: 6px;
523
+ flex-wrap: wrap;
524
+ gap: 4px;
525
+ margin-top: 6px;
324
526
  }
325
527
 
326
- .cmt-reaction {
528
+ .tlui-cmt-reaction {
327
529
  display: inline-flex;
328
530
  align-items: center;
329
- gap: 5px;
330
- padding: 3px 9px;
331
- border-radius: 999px;
332
- border: 1px solid var(--tl-color-divider);
333
- background: var(--tl-color-panel);
531
+ gap: 4px;
532
+ padding: 2px 6px;
533
+ border: none;
534
+ border-radius: var(--tl-radius-1);
535
+ background: var(--tl-color-muted-2);
334
536
  font: inherit;
335
- font-size: 12px;
537
+ font-size: 10px;
538
+ font-weight: 400;
336
539
  color: var(--tl-color-text-1);
540
+ }
541
+
542
+ /* An inert pill renders as a span (e.g. in a notification row); only the button variant is
543
+ clickable, so only it gets the pointer cursor and hover shade. */
544
+ button.tlui-cmt-reaction {
337
545
  cursor: pointer;
338
546
  }
339
547
 
340
- .cmt-reaction:hover,
341
- .pseudo-hover .cmt-reaction {
342
- border-color: var(--tl-color-selected);
548
+ /* Hover deepens the grey — no border to tint now. Mixing toward the text colour is theme-aware:
549
+ it darkens the fill in light mode and lightens it in dark, tracking the pill's own contrast. */
550
+ button.tlui-cmt-reaction:hover {
551
+ background: var(--tl-color-muted-1);
343
552
  }
344
553
 
345
- .cmt-reaction--active {
346
- border-color: var(--tl-color-selected);
347
- background: color-mix(in srgb, var(--tl-color-selected) 12%, var(--tl-color-panel));
554
+ /* a reaction you're part of fills with the selection colour, so "I reacted with this" stands out
555
+ from the grey pills of reactions you haven't joined. */
556
+ .tlui-cmt-reaction--active {
557
+ background: color-mix(in srgb, var(--tl-color-selected) 28%, var(--tl-color-panel));
348
558
  }
349
559
 
350
- .cmt-reaction__count {
351
- color: var(--tl-color-text-3);
560
+ button.tlui-cmt-reaction--active:hover {
561
+ background: color-mix(in srgb, var(--tl-color-selected) 38%, var(--tl-color-panel));
352
562
  }
353
563
 
354
- .cmt-reaction--add {
564
+ /* The emoji renders larger than the 10px count, but its pinned line-height keeps the pill the
565
+ count's height — the glyph overflows the line box rather than growing it. */
566
+ .tlui-cmt-reaction__emoji {
567
+ font-size: 13px;
568
+ line-height: 10px;
569
+ }
570
+
571
+ .tlui-cmt-reaction__count {
355
572
  color: var(--tl-color-text-3);
356
- font-weight: 600;
573
+ font-variant-numeric: tabular-nums;
357
574
  }
358
575
 
359
- /* mention */
360
- .cmt-mention {
361
- color: var(--tl-color-selected);
362
- font-weight: 600;
363
- background: color-mix(in srgb, var(--tl-color-selected) 12%, transparent);
364
- padding: 0 4px;
365
- border-radius: 4px;
576
+ .tlui-cmt-reaction--active .tlui-cmt-reaction__count {
577
+ color: var(--tl-color-text-1);
366
578
  }
367
579
 
368
- /* mention picker */
369
- .cmt-mention-list {
370
- display: flex;
371
- flex-direction: column;
372
- gap: 1px;
580
+ /* emoji picker */
581
+ .tlui-cmt-emoji-picker {
582
+ display: grid;
583
+ grid-template-columns: repeat(5, 40px);
584
+ gap: 0;
373
585
  padding: 4px;
374
- width: 100%;
375
- box-sizing: border-box;
376
- max-height: 260px;
377
- overflow-y: auto;
378
- color: var(--tl-color-text-1, #1d1d1d);
379
- background: var(--tl-color-panel, #fff);
380
- border: 1px solid var(--tl-color-divider, rgba(0, 0, 0, 0.08));
381
- border-radius: 10px;
382
- box-shadow: var(--tl-shadow-2, 0 2px 12px rgba(0, 0, 0, 0.14));
383
586
  }
384
587
 
385
- .cmt-mention-list__item {
588
+ .tlui-cmt-emoji-picker__item {
589
+ position: relative;
386
590
  display: flex;
387
591
  align-items: center;
388
- gap: 8px;
389
- padding: 6px 8px;
592
+ justify-content: center;
593
+ width: 40px;
594
+ height: 40px;
595
+ padding: 0;
390
596
  border: none;
391
- background: transparent;
392
- border-radius: 6px;
393
- cursor: pointer;
394
- text-align: left;
395
- color: var(--tl-color-text-1);
597
+ background: none;
396
598
  font: inherit;
599
+ font-size: 21px;
600
+ line-height: 1;
601
+ cursor: pointer;
397
602
  }
398
603
 
399
- .cmt-mention-list__item--active,
400
- .cmt-mention-list__item:hover {
401
- background: var(--tl-color-muted-2, rgba(0, 0, 0, 0.06));
402
- }
403
-
404
- .cmt-mention-list__text {
405
- display: flex;
406
- flex-direction: column;
407
- min-width: 0;
408
- line-height: 1.3;
409
- }
410
-
411
- .cmt-mention-list__name {
412
- font-size: 13px;
413
- font-weight: 500;
414
- white-space: nowrap;
415
- overflow: hidden;
416
- text-overflow: ellipsis;
417
- }
418
-
419
- .cmt-mention-list__you {
420
- margin-left: 4px;
421
- font-weight: 400;
422
- color: var(--tl-color-text-3);
604
+ /* Inset wash behind the glyph — the 40px hit cells tile edge-to-edge, the squares sit 4px apart,
605
+ like the thread action buttons. Sits under the emoji so an opaque selected fill can't hide it. */
606
+ .tlui-cmt-emoji-picker__item::after {
607
+ content: '';
608
+ position: absolute;
609
+ inset: 2px;
610
+ z-index: -1;
611
+ border-radius: var(--tl-radius-2);
423
612
  }
424
-
425
- .cmt-mention-list__secondary {
426
- font-size: 12px;
427
- color: var(--tl-color-text-3);
428
- white-space: nowrap;
429
- overflow: hidden;
430
- text-overflow: ellipsis;
613
+ .tlui-cmt-emoji-picker__item:hover {
614
+ z-index: 1;
431
615
  }
432
-
433
- .cmt-mention-list--empty {
434
- padding: 8px 10px;
435
- width: 100%;
436
- box-sizing: border-box;
437
- color: var(--tl-color-text-3);
438
- font-size: 13px;
616
+ .tlui-cmt-emoji-picker__item:hover::after {
617
+ background: var(--tl-color-muted-2);
439
618
  }
440
619
 
441
- /* the @-picker popup — caret-anchored above the page, positioned by the suggestion plugin */
442
- .cmt-mention-popup {
443
- position: fixed;
444
- z-index: var(--tl-layer-menus);
620
+ .tlui-cmt-emoji-picker__item--active::after {
621
+ background: color-mix(in srgb, var(--tl-color-selected) 28%, var(--tl-color-panel));
445
622
  }
446
623
 
447
624
  /* empty state */
448
- .cmt-empty {
625
+ .tlui-cmt-empty {
449
626
  display: flex;
450
627
  flex-direction: column;
451
628
  align-items: center;
@@ -455,92 +632,260 @@
455
632
  width: 240px;
456
633
  }
457
634
 
458
- .cmt-empty__icon {
635
+ .tlui-cmt-empty__icon {
459
636
  font-size: 28px;
460
637
  }
461
638
 
462
- .cmt-empty__message {
639
+ .tlui-cmt-empty__message {
463
640
  margin: 0;
464
641
  color: var(--tl-color-text-3);
465
- font-size: 13px;
642
+ font-size: 12px;
466
643
  line-height: 1.4;
467
644
  }
468
645
 
469
- /* a comment thread: an elevated panel the same surface treatment as the toolbar — holding a
470
- header, an optional resolved banner, the comments, and a reply composer */
471
- .cmt-thread {
646
+ /* a comment thread: an elevated panel holding a header, an optional resolved banner, the
647
+ comments, and a reply composer */
648
+ /* The thread panel's layout metrics live on the container because two separate portals need them:
649
+ the panel itself, and the canvas hover preview, which has to land its first comment on exactly
650
+ the pixel the panel would. Deriving the preview's offset from these means the two can't drift —
651
+ retune the padding or the header here and the preview follows. */
652
+ .tl-container {
653
+ /* Everything that has to line up against a pin — its preview's bridge, the draft composer's
654
+ offsets, `PIN_SIZE` in thread-view.tsx — derives from this, so the pin's size stays one
655
+ number. */
656
+ --tlui-cmt-pin-size: 28px;
657
+ /* A cluster badge and a stack badge are this square — the same size as a pin, so the marker
658
+ kinds read as one family. The hover preview measures its bridge against it, so the bridge
659
+ stops at the marker's edge instead of covering it. Mirrored by `MARKER_SIZE` in
660
+ thread-view.tsx. */
661
+ --tlui-cmt-marker-size: var(--tlui-cmt-pin-size);
662
+ /* How a marker sits, lifts, and rings. Shared so a pin and a badge respond identically.
663
+ shadow-3's depth without its `inset 1px panel-contrast` edge — that inset reads as a white
664
+ border on a coloured pin (it's invisible on the panel-white menus shadow-3 is meant for). */
665
+ --tlui-cmt-marker-shadow: 0px 1px 2px hsl(0, 0%, 0%, 28%), 0px 2px 6px hsl(0, 0%, 0%, 14%);
666
+ --tlui-cmt-marker-shadow-hover: var(--tlui-cmt-marker-shadow);
667
+ --tlui-cmt-marker-hover-scale: 1.08;
668
+ --tlui-cmt-marker-transition: 0.08s ease;
669
+ --tlui-cmt-thread-padding: 12px;
670
+ --tlui-cmt-thread-gap: 0px;
671
+ /* Hit-area width of one .tlui-cmt-thread__action button (drawn as a 32px square inset 4px). */
672
+ --tlui-cmt-thread-action-size: 40px;
673
+ /* A floating pill's edge — its hairline ring is the only thing separating it from the panel it
674
+ overlaps, so the ring is a theme token rather than a literal black: a near-black hairline
675
+ vanishes on a dark panel, where the divider token lightens instead. The drop below it stays
676
+ literal; a soft dark blur reads on both themes. */
677
+ --tlui-cmt-pill-shadow: 0 0 0 1px var(--tl-color-divider), 0 1px 3px hsl(0, 0%, 0%, 0.05);
678
+ }
679
+
680
+ .tlui-cmt-thread {
681
+ position: relative;
472
682
  display: flex;
473
683
  flex-direction: column;
474
- gap: 6px;
475
684
  width: 300px;
476
- padding: 8px;
477
685
  background: var(--tl-color-panel);
478
686
  border-radius: var(--tl-radius-4);
479
- box-shadow: var(--tl-shadow-2);
687
+ box-shadow: var(--tl-shadow-3);
480
688
  }
481
- .cmt-thread__header {
689
+ /* A pill of its own, floating above the panel's top-right corner and overlapping its top edge. */
690
+ .tlui-cmt-thread__header {
691
+ position: absolute;
692
+ right: 10px;
693
+ bottom: calc(100% - 15px);
482
694
  display: flex;
695
+ gap: 2px;
696
+ padding: 2px;
697
+ background: var(--tl-color-panel);
698
+ border-radius: var(--tl-radius-3);
699
+ box-shadow: var(--tlui-cmt-pill-shadow);
700
+ transition: opacity 0.1s ease;
701
+ }
702
+ /* The first comment's hover actions surface right where the header pill sits, so hide the pill
703
+ while that comment is hovered — one pill in the corner, never two stacked. */
704
+ .tlui-cmt-thread:has(.tlui-cmt-thread__list > :first-child:hover)
705
+ .tlui-cmt-thread__header:not(:has(.tlui-cmt-thread__title)) {
706
+ opacity: 0;
707
+ pointer-events: none;
708
+ }
709
+ /* A title puts the header back in the panel, in flow. The pill above is tuned for the actions-only
710
+ case tldraw itself uses: right-anchored and only as wide as its buttons, so a title inside it
711
+ would grow leftwards off the panel's edge and float over whatever sits above. `header` is public
712
+ API (see `CommentThreadProps`), so it has to keep laying out sensibly for anyone passing one. */
713
+ .tlui-cmt-thread__header:has(.tlui-cmt-thread__title) {
714
+ position: static;
483
715
  align-items: center;
484
- justify-content: space-between;
485
- padding: 2px 2px 2px 4px;
716
+ padding: 0;
717
+ background: none;
718
+ border-radius: 0;
719
+ box-shadow: none;
486
720
  }
487
- .cmt-thread__title {
721
+ .tlui-cmt-thread__title {
722
+ padding: 10px 12px;
488
723
  font-size: 12px;
489
- font-weight: 600;
724
+ line-height: 20px;
725
+ font-weight: 500;
490
726
  color: var(--tl-color-text-1);
491
727
  }
492
- .cmt-thread__actions {
493
- display: flex;
494
- gap: 2px;
495
- }
496
- .cmt-thread__action {
728
+ .tlui-cmt-thread__actions {
497
729
  display: flex;
498
- align-items: center;
499
- justify-content: center;
500
- width: 22px;
501
- height: 22px;
730
+ gap: 0;
731
+ /* Right-align the actions whether or not the header has a title (it currently has none, so
732
+ space-between alone would leave them at the left). */
733
+ margin-left: auto;
734
+ }
735
+ /* These are TldrawUiButtons (type="icon"), so the 40px box, the 32px ::after square carrying the
736
+ hover wash, and the hover z-index flip all come from .tlui-button — the hit area (not the
737
+ ::after) is what's clickable, and it never reaches past its row. Only the bits that differ from
738
+ a toolbar button are set here, at .tlui-button's specificity + 1 so stylesheet order can't
739
+ decide the winner. */
740
+ .tlui-button.tlui-cmt-thread__action {
741
+ width: 40px;
502
742
  padding: 0;
503
- border: none;
504
743
  border-radius: 6px;
505
- background: transparent;
506
744
  color: var(--tl-color-text-3);
507
745
  font-size: 14px;
508
746
  line-height: 1;
509
- cursor: pointer;
510
747
  }
511
- .cmt-thread__action:hover {
512
- background: var(--tl-color-muted-2);
748
+ /* Overlap each button 4px into the previous so the 32px visual squares sit 4px apart, not 8px.
749
+ Left-only, so the last button's hit area still stops at its row's right edge. */
750
+ .tlui-cmt-thread__action + .tlui-cmt-thread__action {
751
+ margin-left: -4px;
752
+ }
753
+ .tlui-button.tlui-cmt-thread__action:hover {
513
754
  color: var(--tl-color-text-1);
514
755
  }
515
- .cmt-thread__resolved {
756
+ .tlui-button.tlui-cmt-thread__action--danger:hover {
757
+ color: var(--tl-color-danger);
758
+ }
759
+ .tlui-cmt-thread__resolved {
760
+ margin: 4px 12px;
516
761
  padding: 6px 10px;
517
762
  border-radius: 8px;
518
763
  background: var(--tl-color-muted-2);
519
764
  font-size: 12px;
520
765
  color: var(--tl-color-text-3);
521
766
  }
522
- .cmt-thread__list {
767
+ .tlui-cmt-thread__list {
523
768
  display: flex;
524
769
  flex-direction: column;
525
- gap: 6px;
770
+ gap: var(--tlui-cmt-thread-gap);
771
+ }
772
+ /* When nothing follows the list (a resolved thread has no composer or footer) the last comment
773
+ would sit against the panel's rounded bottom — restore the trailing inset the composer supplies. */
774
+ .tlui-cmt-thread__list:last-child {
775
+ padding-bottom: 12px;
526
776
  }
777
+ /* The footer slot sits where the composer would — its content (e.g. a sign-in prompt) sits at
778
+ the thread's trailing edge, like the composer's send button. */
779
+ .tlui-cmt-thread__footer {
780
+ display: flex;
781
+ justify-content: flex-end;
782
+ padding: 4px 2px 2px;
783
+ }
784
+ /* Inside a thread, author identity is a small colour dot, no initial — the letter doesn't fit
785
+ at this size. */
786
+ .tlui-cmt-thread .tlui-cmt-avatar,
787
+ .tlui-cmt-canvas-preview--thread .tlui-cmt-avatar,
788
+ .tlui-cmt-canvas-preview--list .tlui-cmt-avatar,
789
+ .tlui-cmt-stack-list .tlui-cmt-avatar {
790
+ width: 12px;
791
+ height: 12px;
792
+ font-size: 0;
793
+ }
794
+
795
+ .tlui-cmt-thread .tlui-cmt-composer {
796
+ padding: 12px;
797
+ }
798
+ .tlui-cmt-thread .tlui-cmt-composer .tlui-cmt-avatar {
799
+ display: none;
800
+ }
801
+
802
+ /* The sidebar's rows use the same small colour dot as the thread view. */
803
+ .tlui-cmt-list .tlui-cmt-avatar {
804
+ width: 14px;
805
+ height: 14px;
806
+ font-size: 0;
807
+ margin-top: 2px;
808
+ }
809
+ /* The sidebar row already leads with its own avatar (the flex row's first column); the byline's
810
+ avatar — added for the canvas thread views — would double it, so hide the byline's inside a row. */
811
+ .tlui-cmt-list .tlui-cmt-head .tlui-cmt-avatar {
812
+ display: none;
813
+ }
814
+
815
+ /* The dot treatment is for colour-only identity — an image avatar stays full size. */
816
+ .tlui-cmt-thread .tlui-cmt-avatar--image,
817
+ .tlui-cmt-canvas-preview--thread .tlui-cmt-avatar--image,
818
+ .tlui-cmt-canvas-preview--list .tlui-cmt-avatar--image,
819
+ .tlui-cmt-stack-list .tlui-cmt-avatar--image {
820
+ /* Match the 12px colour dot's width so the body's calc(12px + 6px) indent still lines up under
821
+ the byline name when the author has an image rather than a dot. */
822
+ width: 12px;
823
+ height: 12px;
824
+ margin-top: 0;
825
+ }
826
+ .tlui-cmt-list .tlui-cmt-avatar--image {
827
+ width: 24px;
828
+ height: 24px;
829
+ margin-top: 0;
830
+ }
831
+
527
832
  /* inside the panel, comments are plain content — no surrounding box; the composer's field carries
528
- its own surface. */
529
- .cmt-thread .cmt-card {
833
+ its own surface. The hover preview is the same panel without a header, so its comment is laid
834
+ out by the same rule rather than a copy of it. */
835
+ .tlui-cmt-thread .tlui-cmt-card,
836
+ .tlui-cmt-canvas-preview--thread .tlui-cmt-card {
530
837
  width: 100%;
531
- padding: 6px 0 6px 4px;
838
+ gap: 0;
839
+ padding: 0 var(--tlui-cmt-thread-padding);
532
840
  background: transparent;
533
841
  border: none;
534
842
  }
843
+ /* The avatar is a 12px dot, so the body lines up 12px + the head's 6px gap in from the row. */
844
+ .tlui-cmt-thread .tlui-cmt-body,
845
+ .tlui-cmt-canvas-preview--thread .tlui-cmt-body,
846
+ .tlui-cmt-canvas-preview--list .tlui-cmt-body,
847
+ .tlui-cmt-stack-list .tlui-cmt-body {
848
+ padding-left: calc(12px + 6px);
849
+ }
850
+ /* Inside a panel the actions float as their own pill — the same pill the thread header is, in the
851
+ same corner (which is why the header hides while the first comment is hovered). It sits just
852
+ above the card's top edge rather than on the byline. */
853
+ /* Line the card actions up with the header actions: flush to the full-width card's right edge (the
854
+ panel edge, matching the header ✕) and flush on the 40px byline row, so the 40px buttons fill it. */
855
+ .tlui-cmt-thread .tlui-cmt-card .tlui-cmt-card__actions {
856
+ top: 0;
857
+ right: 0;
858
+ }
859
+ /* The byline row is 40px: on the opened thread it fits the action pills (react/edit/delete) that
860
+ float in its corner without the text running underneath; previews and collapsed cards carry the
861
+ same height so every comment row reads identically, solo or in a stack. */
862
+ .tlui-cmt-thread .tlui-cmt-card .tlui-cmt-head,
863
+ .tlui-cmt-canvas-preview--thread .tlui-cmt-card .tlui-cmt-head,
864
+ .tlui-cmt-canvas-preview--list .tlui-cmt-card .tlui-cmt-head,
865
+ .tlui-cmt-stack-list .tlui-cmt-card .tlui-cmt-head {
866
+ min-height: 40px;
867
+ }
868
+ /* Reserve head room only for pills that actually rendered (one child / two children) — pill count
869
+ is a permissions question (canModifyComment), not ownership, and empty slots (read-only viewers)
870
+ keep the full row. Sized from the 40px hit areas (4px overlap), minus the card padding the
871
+ right:0 pills sit over; text stops 4px shy of the visible square. */
872
+ .tlui-cmt-thread .tlui-cmt-card:has(.tlui-cmt-card__actions > *) .tlui-cmt-head {
873
+ padding-right: calc(var(--tlui-cmt-thread-action-size) - var(--tlui-cmt-thread-padding));
874
+ }
875
+ .tlui-cmt-thread .tlui-cmt-card:has(.tlui-cmt-card__actions > * + *) .tlui-cmt-head {
876
+ padding-right: calc(
877
+ 2 * var(--tlui-cmt-thread-action-size) - 4px - var(--tlui-cmt-thread-padding)
878
+ );
879
+ }
535
880
 
536
881
  /* comments list — thread summaries, one per row (used by the canvas sidebar) */
537
- .cmt-list {
882
+ .tlui-cmt-list {
538
883
  display: flex;
539
884
  flex-direction: column;
540
885
  width: 100%;
541
886
  min-height: 0;
542
887
  }
543
- .cmt-list__header {
888
+ .tlui-cmt-list__header {
544
889
  flex-shrink: 0;
545
890
  display: flex;
546
891
  align-items: center;
@@ -549,73 +894,102 @@
549
894
  padding: 6px 6px 6px 14px;
550
895
  border-bottom: 1px solid var(--tl-color-divider);
551
896
  }
552
- .cmt-list__header-title {
553
- font-size: 11px;
554
- font-weight: 600;
555
- letter-spacing: 0.06em;
556
- text-transform: uppercase;
557
- color: var(--tl-color-text-3);
897
+ .tlui-cmt-list__header-title {
898
+ font-size: 12px;
899
+ font-weight: 500;
900
+ color: var(--tl-color-text-1);
558
901
  }
559
902
 
560
- /* a small square icon button in the sidebar header (filter funnel, overflow …) */
561
- .cmt-header-btn {
562
- display: flex;
563
- align-items: center;
564
- justify-content: center;
903
+ /* A small square icon button in the sidebar header (filter funnel, overflow …). Also a
904
+ TldrawUiButton, but a bespoke 26px size — so it has to override .tlui-button's 40px box, and
905
+ `min-width` in particular would otherwise beat `width`. Selectors carry .tlui-button so
906
+ stylesheet order can't decide the winner. */
907
+ .tlui-button.tlui-cmt-header-btn {
565
908
  width: 26px;
909
+ min-width: 26px;
566
910
  height: 26px;
567
911
  padding: 0;
568
- border: none;
569
912
  border-radius: 6px;
570
- background: transparent;
571
913
  color: var(--tl-color-text-2);
572
- cursor: pointer;
573
914
  }
574
- .cmt-header-btn:hover,
575
- .cmt-header-btn[data-state='open'] {
915
+ /* Same hit-target expansion as the thread actions, tiled the same way — this row's gap is 2px too
916
+ (see `.tlui-cmt-list__header-actions`). Unlike the thread actions, the hover fill sits on the
917
+ button itself, so .tlui-button's ::after wash is cleared and the pseudo-element is left doing
918
+ nothing but widening the hit area. */
919
+ .tlui-button.tlui-cmt-header-btn::after {
920
+ inset: -6px -1px;
921
+ background: none;
922
+ }
923
+ .tlui-button.tlui-cmt-header-btn:first-child::after {
924
+ left: -6px;
925
+ }
926
+ .tlui-button.tlui-cmt-header-btn:last-child::after {
927
+ right: -6px;
928
+ }
929
+ .tlui-button.tlui-cmt-header-btn:hover,
930
+ .tlui-button.tlui-cmt-header-btn[data-state='open'] {
576
931
  background: var(--tl-color-muted-2);
577
932
  color: var(--tl-color-text-1);
578
933
  }
579
- .cmt-list__header-actions {
934
+ .tlui-cmt-list__header-actions {
580
935
  display: flex;
581
936
  align-items: center;
582
937
  gap: 2px;
583
938
  }
584
939
 
940
+ /* The commenting dropdowns' surface — a little air between the items and the menu edge. */
941
+ .tlui-cmt-menu {
942
+ min-width: 100px;
943
+ padding: 4px;
944
+ }
945
+
585
946
  /* a menu row with a label and a right-aligned shortcut hint (overflow menu) */
586
- .cmt-menu-item {
947
+ .tlui-cmt-menu-item {
948
+ position: relative;
587
949
  display: flex;
588
950
  align-items: center;
589
951
  justify-content: space-between;
590
952
  gap: 24px;
591
953
  width: 100%;
592
- padding: 6px 8px;
954
+ min-height: 40px;
955
+ padding: 0 10px;
593
956
  border: none;
594
- border-radius: 6px;
595
957
  background: transparent;
596
958
  font: inherit;
597
- font-size: 13px;
959
+ font-size: 12px;
598
960
  color: var(--tl-color-text-1);
599
961
  cursor: pointer;
600
962
  }
601
- .cmt-menu-item:hover {
963
+ /* 36px visual inset within the 40px row — the hover wash the action and emoji buttons use. */
964
+ .tlui-cmt-menu-item::after {
965
+ content: '';
966
+ position: absolute;
967
+ inset: 2px;
968
+ border-radius: var(--tl-radius-2);
969
+ }
970
+ .tlui-cmt-menu-item:hover::after {
602
971
  background: var(--tl-color-muted-2);
603
972
  }
604
- .cmt-menu-item__shortcut {
973
+ /* Destructive menu entries (delete) — the same danger colour as tldraw's danger buttons. */
974
+ .tlui-cmt-menu-item--danger {
975
+ color: var(--tl-color-danger);
976
+ }
977
+
978
+ .tlui-cmt-menu-item__shortcut {
605
979
  color: var(--tl-color-text-3);
606
980
  font-size: 12px;
607
981
  }
608
- .cmt-list__items {
982
+ .tlui-cmt-list__items {
609
983
  flex: 1;
610
984
  min-height: 0;
611
985
  overflow-y: auto;
612
986
  }
613
- .cmt-list__empty {
987
+ .tlui-cmt-list__empty {
614
988
  padding: 20px 14px;
615
989
  color: var(--tl-color-text-3);
616
- font-size: 13px;
990
+ font-size: 12px;
617
991
  }
618
- .cmt-list__item {
992
+ .tlui-cmt-list__item {
619
993
  display: flex;
620
994
  gap: 10px;
621
995
  width: 100%;
@@ -626,34 +1000,48 @@
626
1000
  font: inherit;
627
1001
  text-align: left;
628
1002
  cursor: pointer;
1003
+ /* rows can render as anchors (for open-in-new-tab); keep them looking like list rows */
1004
+ color: inherit;
1005
+ text-decoration: none;
629
1006
  }
630
- .cmt-list__item:hover {
1007
+ .tlui-cmt-list__item:hover {
631
1008
  background: var(--tl-color-muted-2);
632
1009
  }
633
- .cmt-list__item--selected {
634
- background: var(--tl-color-muted-1);
1010
+ /* Selected sits a step above hover so the open thread's row stays distinguishable while
1011
+ pointing at other rows — but only a step: the muted-1 token (10%) reads too heavy here.
1012
+ These rows sit on the opaque sidebar panel, so a translucent tint composes safely. */
1013
+ .tlui-cmt-list__item--selected,
1014
+ .tlui-cmt-list__item--selected:hover {
1015
+ background: hsl(0, 0%, 0%, 5%);
1016
+ }
1017
+ .tl-theme__dark .tlui-cmt-list__item--selected,
1018
+ .tl-theme__dark .tlui-cmt-list__item--selected:hover {
1019
+ background: hsl(0, 0%, 100%, 5%);
635
1020
  }
636
1021
  /* resolved rows mute their content but keep the "Resolved" marker legible */
637
- .cmt-list__item[data-resolved] .cmt-head,
638
- .cmt-list__item[data-resolved] .cmt-list__item-preview {
1022
+ .tlui-cmt-list__item[data-resolved] .tlui-cmt-head,
1023
+ .tlui-cmt-list__item[data-resolved] .tlui-cmt-list__item-preview {
639
1024
  opacity: 0.55;
640
1025
  }
641
- .cmt-list__item-body {
1026
+ .tlui-cmt-list__item-body {
642
1027
  flex: 1;
643
1028
  min-width: 0;
644
1029
  }
645
- .cmt-list__item-preview {
1030
+ .tlui-cmt-list__item-preview {
646
1031
  margin-top: 2px;
647
- font-size: 13px;
1032
+ font-size: 12px;
648
1033
  color: var(--tl-color-text-1);
649
1034
  overflow: hidden;
650
1035
  text-overflow: ellipsis;
651
1036
  display: -webkit-box;
652
1037
  -webkit-line-clamp: 2;
653
1038
  -webkit-box-orient: vertical;
1039
+ /* Long unbroken strings (URLs, mashed keys) wrap onto the clamped lines instead of clipping
1040
+ mid-string — same treatment as the card body. */
1041
+ overflow-wrap: anywhere;
654
1042
  }
655
1043
  /* meta line under the preview: page label and/or a resolved marker */
656
- .cmt-list__item-meta {
1044
+ .tlui-cmt-list__item-meta {
657
1045
  display: flex;
658
1046
  align-items: center;
659
1047
  gap: 8px;
@@ -661,92 +1049,372 @@
661
1049
  font-size: 11px;
662
1050
  color: var(--tl-color-text-3);
663
1051
  }
664
- .cmt-list__item-resolved {
1052
+ .tlui-cmt-list__item-resolved {
665
1053
  display: inline-flex;
666
1054
  align-items: center;
667
1055
  gap: 3px;
668
1056
  font-weight: 600;
669
1057
  }
670
- .cmt-list__item-page {
1058
+ .tlui-cmt-list__item-page {
671
1059
  display: inline-flex;
672
1060
  align-items: center;
673
1061
  padding: 1px 6px;
674
1062
  border-radius: 4px;
675
1063
  background: var(--tl-color-muted-2);
676
1064
  }
677
- .cmt-list__item-count {
678
- flex-shrink: 0;
679
- align-self: center;
1065
+ .tlui-cmt-list__item-replies {
1066
+ display: inline-flex;
1067
+ align-items: center;
1068
+ }
1069
+ /* reply count under a preview card's opening comment — the thread continues past what's shown */
1070
+ .tlui-cmt-card__replies {
1071
+ display: block;
1072
+ margin-top: 6px;
680
1073
  font-size: 12px;
681
1074
  color: var(--tl-color-text-3);
682
1075
  }
683
1076
 
684
- /* The comments layer is portaled into the editor container. Pins live in the canvas-in-front layer
685
- (beneath the UI panels at z-index 300), so the toolbar and style panel draw over them; the open
686
- thread's popover portals up to the menus layer separately (`.cmt-canvas-popover`) so it isn't
687
- clipped. Click-through except on its own children. */
688
- .cmt-canvas-layer {
1077
+ /* The comments layer renders through `EditorPortal`, so its pieces can pick their own layer rather
1078
+ than the one the component is mounted in. Pins live in the canvas-in-front layer (beneath the UI
1079
+ panels at z-index 300), so the toolbar and style panel draw over them; the open thread's popover
1080
+ goes to the menus layer instead (`.tlui-cmt-canvas-popover`) so it isn't clipped. Click-through
1081
+ except on its own children. */
1082
+ .tlui-cmt-canvas-layer {
689
1083
  position: absolute;
690
1084
  inset: 0;
691
- z-index: var(--tl-layer-canvas-in-front);
1085
+ /* Just below the collaborator cursor layer, so cursors ride over the pins — and below the
1086
+ in-front content (250), so active tool UI covers both. Derived, not a literal, so the
1087
+ ordering can't silently drift if the cursor slot moves. */
1088
+ z-index: calc(var(--tl-layer-collaborators) - 5);
692
1089
  pointer-events: none;
693
1090
  }
694
1091
 
695
- .cmt-canvas-pin,
696
- .cmt-canvas-cluster,
697
- .cmt-canvas-composer {
1092
+ .tlui-cmt-canvas-pin,
1093
+ .tlui-cmt-canvas-cluster,
1094
+ .tlui-cmt-canvas-composer {
698
1095
  position: absolute;
699
1096
  }
700
1097
 
701
- .cmt-canvas-pin,
702
- .cmt-canvas-composer {
1098
+ .tlui-cmt-canvas-composer {
703
1099
  pointer-events: auto;
704
1100
  }
705
1101
 
706
- /* Badges are clickable (zoom to the cluster's first split). */
707
- .cmt-canvas-cluster {
708
- transform: translate(-50%, -50%);
1102
+ /* The pin's only interactive surface is the square hit layer below — the wrapper itself is
1103
+ inert. Its box sits at the anchor point, not over the (translated) visual, so letting it take
1104
+ events would put a phantom hit zone beside the pin. */
1105
+ .tlui-cmt-canvas-pin {
1106
+ pointer-events: none;
1107
+ }
1108
+
1109
+ .tlui-cmt-button {
1110
+ appearance: none;
1111
+ margin: 0;
1112
+ padding: 0;
1113
+ border: none;
1114
+ background: none;
1115
+ color: inherit;
1116
+ font: inherit;
1117
+ }
1118
+
1119
+ /* Anchored by its bottom-left corner like a pin, so the badge sits up-and-right of the cluster's
1120
+ centroid — the same visual-to-anchor relationship a single comment pin has. */
1121
+ .tlui-cmt-canvas-cluster {
1122
+ transform: translate(0, -100%);
709
1123
  pointer-events: auto;
710
1124
  cursor: pointer;
711
1125
  }
712
1126
 
713
- .cmt-cluster-fade {
1127
+ /* Keyboard focus has to be visible on the canvas, where there's no surrounding chrome to imply
1128
+ it. The ring goes on the marker visual (the rounded pin/badge), not the button box, so it
1129
+ traces the shape rather than a rectangle around it; the button's own outline is suppressed
1130
+ because its box is offset from what you see. */
1131
+ .tlui-cmt-canvas-pin__marker:focus-visible,
1132
+ .tlui-cmt-canvas-cluster:focus-visible {
1133
+ outline: none;
1134
+ }
1135
+ .tlui-cmt-canvas-pin__marker:focus-visible .tlui-cmt-marker,
1136
+ .tlui-cmt-canvas-cluster:focus-visible .tlui-cmt-marker {
1137
+ outline: 2px solid var(--tl-color-selected);
1138
+ outline-offset: 2px;
1139
+ }
1140
+ /* Focus should read like hover, so the pin lifts the same way when tabbed to. */
1141
+ .tlui-cmt-canvas-pin__marker:focus-visible .tlui-cmt-marker {
1142
+ transform: scale(var(--tlui-cmt-marker-hover-scale));
1143
+ }
1144
+
1145
+ .tlui-cmt-cluster-fade {
714
1146
  opacity: 1;
715
1147
  transition: opacity 150ms ease;
716
1148
  }
717
1149
 
718
- .cmt-cluster-fade--entering {
1150
+ .tlui-cmt-cluster-fade--entering {
719
1151
  opacity: 0;
720
1152
  }
721
1153
 
722
- .cmt-cluster-fade--present {
1154
+ .tlui-cmt-cluster-fade--present {
723
1155
  opacity: 1;
724
1156
  }
725
1157
 
726
- .cmt-cluster-fade--exiting {
1158
+ .tlui-cmt-cluster-fade--exiting {
727
1159
  opacity: 0;
728
1160
  }
729
1161
 
730
- .cmt-cluster-fade--entering *,
731
- .cmt-cluster-fade--exiting * {
1162
+ .tlui-cmt-cluster-fade--entering *,
1163
+ .tlui-cmt-cluster-fade--exiting * {
732
1164
  pointer-events: none !important;
733
1165
  }
734
1166
 
735
1167
  @media (prefers-reduced-motion: reduce) {
736
- .cmt-cluster-fade {
1168
+ .tlui-cmt-cluster-fade {
737
1169
  transition: none;
738
1170
  }
739
1171
  }
740
1172
 
741
1173
  /* The open thread (its pin + popover) sits above the other pins, so nearby markers don't
742
1174
  overlap its popover — they tuck behind it. */
743
- .cmt-canvas-pin--open {
1175
+ .tlui-cmt-canvas-pin--open {
744
1176
  z-index: 1;
745
1177
  }
746
1178
 
1179
+ /* The count badge standing in for a stack of coincident pins — same face as a cluster badge,
1180
+ but clicking opens the stack's thread list rather than zooming (no zoom can separate them).
1181
+ Re-enables pointer events: the badge lives inside `.tlui-cmt-canvas-pin`, which is inert
1182
+ (`pointer-events: none`) so its box doesn't shadow the canvas — without this the badge renders
1183
+ but can't be hovered or clicked, like the cluster badge's own `pointer-events: auto`. */
1184
+ .tlui-cmt-canvas-stack-badge {
1185
+ /* Bottom-left anchored like the pins it stands in for, and like the cluster badge. */
1186
+ transform: translate(0, -100%);
1187
+ width: max-content;
1188
+ pointer-events: auto;
1189
+ cursor: pointer;
1190
+ }
1191
+
1192
+ .tlui-cmt-canvas-stack-badge:focus-visible {
1193
+ outline: none;
1194
+ }
1195
+ .tlui-cmt-canvas-stack-badge:focus-visible .tlui-cmt-marker {
1196
+ outline: 2px solid var(--tl-color-selected);
1197
+ outline-offset: 2px;
1198
+ transform: scale(var(--tlui-cmt-marker-hover-scale));
1199
+ }
1200
+
1201
+ /* The stack's popover content: one card per thread, the expanded thread inline among them. */
1202
+ .tlui-cmt-stack-list {
1203
+ display: flex;
1204
+ flex-direction: column;
1205
+ gap: 10px;
1206
+ }
1207
+
1208
+ /* The expanded thread is the only entry with pills floating above its panel — its header actions,
1209
+ and its first comment's hover actions. Those reach ~15px up, past the list's 10px gap, so give
1210
+ this one entry extra room: without it the pills land on the card above, and against the scroll
1211
+ box's clip edge. Collapsed cards keep their own actions inside, so they need none of this. */
1212
+ .tlui-cmt-stack-list__thread {
1213
+ margin-top: 8px;
1214
+ }
1215
+
1216
+ /* Collapsed entries match the expanded thread's surface (same width, panel, elevation) so the
1217
+ list reads as one column of cards with one of them opened up. */
1218
+ .tlui-cmt-stack-list__card {
1219
+ box-sizing: border-box;
1220
+ position: relative;
1221
+ width: 300px;
1222
+ background: var(--tl-color-panel);
1223
+ border-radius: 12px;
1224
+ box-shadow: var(--tl-shadow-3);
1225
+ padding: 2px 12px 10px;
1226
+ cursor: pointer;
1227
+ text-align: left;
1228
+ display: block;
1229
+ }
1230
+
1231
+ /* The card's keyboard affordance, covering the whole surface so it's one target rather than a
1232
+ control tucked in a corner. It's laid under the card's content instead of wrapping it: a comment
1233
+ body renders its links as anchors, which can't be nested inside a button (see the note in
1234
+ `StackThreadCard`). Pointer clicks anywhere still reach the card's own handler by bubbling. */
1235
+ .tlui-cmt-stack-list__card-action {
1236
+ position: absolute;
1237
+ inset: 0;
1238
+ border-radius: inherit;
1239
+ cursor: pointer;
1240
+ }
1241
+
1242
+ .tlui-cmt-stack-list__card-action:focus-visible {
1243
+ outline: 2px solid var(--tl-color-selected);
1244
+ outline-offset: 2px;
1245
+ }
1246
+
1247
+ /* The wrapper is the card's surface — inside it the comment is plain content, mirroring how the
1248
+ thread panel flattens its own cards. Stacked above the action overlay so the body's links stay
1249
+ clickable. */
1250
+ .tlui-cmt-stack-list__card .tlui-cmt-card {
1251
+ position: relative;
1252
+ width: 100%;
1253
+ gap: 0;
1254
+ padding: 0;
1255
+ background: transparent;
1256
+ border: none;
1257
+ }
1258
+
1259
+ /* The hover preview, shown for any marker — a pin, a coincident stack, or a cluster badge. Live,
1260
+ not a tooltip: the pointer travels into it to hover and click the cards.
1261
+
1262
+ Two elements, deliberately: this root is the hover *region* (it reaches back over the marker via
1263
+ the bridge below), and `__panel` inside it is the visible surface. A pseudo-element's hover
1264
+ counts as its host's, so a surface on this root would light up whenever the pointer was on the
1265
+ marker — the pin would appear pressed just for being hovered. */
1266
+ .tlui-cmt-canvas-preview {
1267
+ position: absolute;
1268
+ z-index: var(--tl-layer-menus);
1269
+ pointer-events: auto;
1270
+ }
1271
+
1272
+ /* The bridge over the gap between the marker and the panel. Moving right off the pin would
1273
+ otherwise cross bare canvas, and that leave would retract the panel before the pointer arrived.
1274
+ Invisible, part of the panel's hover region, and only present while the panel is.
1275
+
1276
+ It spans the gap and stops there. Reaching any further would put it over the marker itself, and
1277
+ since it sits in the menus layer — above the pins — it would take the marker's hover and clicks:
1278
+ the pin would go dead exactly where it looks most alive. The marker needs no covering anyway, as
1279
+ it carries the same hover handlers.
1280
+
1281
+ The gap is the panel's own offset (`--tlui-cmt-preview-offset`, set from `POPOVER_OFFSET` so
1282
+ there's one source) less however far the marker reaches toward it, per variant below. */
1283
+ .tlui-cmt-canvas-preview::before {
1284
+ content: '';
1285
+ position: absolute;
1286
+ top: 0;
1287
+ bottom: 0;
1288
+ right: 100%;
1289
+ width: max(0px, var(--tlui-cmt-preview-bridge, 0px));
1290
+ }
1291
+
1292
+ /* A pin is anchored at its bottom-left corner, so its full width lies between anchor and panel.
1293
+ The popover and preview share a first-comment position now that the actions float outside the
1294
+ panel rather than reserving a header row, so no vertical shift is needed. */
1295
+ .tlui-cmt-canvas-preview--thread {
1296
+ --tlui-cmt-preview-bridge: calc(var(--tlui-cmt-preview-offset) - var(--tlui-cmt-pin-size));
1297
+ }
1298
+
1299
+ /* A badge is anchored at its bottom-left corner like a pin, so its full width does too. */
1300
+ .tlui-cmt-canvas-preview--list {
1301
+ --tlui-cmt-preview-bridge: calc(var(--tlui-cmt-preview-offset) - var(--tlui-cmt-marker-size));
1302
+ }
1303
+
1304
+ .tlui-cmt-canvas-preview__panel {
1305
+ display: flex;
1306
+ flex-direction: column;
1307
+ }
1308
+
1309
+ /* Previewing a single pin: the thread panel itself, minus the header — the same surface, width,
1310
+ and flattened comment (see `.tlui-cmt-thread .tlui-cmt-card`), so the hover shows exactly what
1311
+ the click opens. */
1312
+ .tlui-cmt-canvas-preview__panel--thread {
1313
+ box-sizing: border-box;
1314
+ width: 300px;
1315
+ gap: var(--tlui-cmt-thread-gap);
1316
+ /* Top/bottom only — the card pads its own sides now (12px), matching the expanded thread. A
1317
+ little extra below so the (often truncated) last line doesn't sit against the rounded edge. */
1318
+ padding: 4px 0 8px;
1319
+ background: var(--tl-color-panel);
1320
+ border-radius: var(--tl-radius-4);
1321
+ box-shadow: var(--tl-shadow-3);
1322
+ }
1323
+ /* The reply count is a short line that sits closer to the edge than a wrapped comment does, so give
1324
+ it more room below when it's the panel's last line. */
1325
+ .tlui-cmt-canvas-preview__panel--thread:has(.tlui-cmt-card__replies) {
1326
+ padding-bottom: 12px;
1327
+ }
1328
+
1329
+ /* Previewing a stack or cluster: one card per thread, matching the card list a click opens. */
1330
+ .tlui-cmt-canvas-preview__panel--list {
1331
+ gap: 8px;
1332
+ padding: 4px 0 8px;
1333
+ }
1334
+
1335
+ /* Matches the stack list's card surface, so a preview and the list it previews read as the same
1336
+ object seen twice — once on hover, once opened. */
1337
+ .tlui-cmt-preview-card {
1338
+ box-sizing: border-box;
1339
+ width: 300px;
1340
+ background: var(--tl-color-panel);
1341
+ border-radius: 12px;
1342
+ box-shadow: var(--tl-shadow-3);
1343
+ /* A 2px nudge above the byline's centring — a boxed card reads a touch better with it than the
1344
+ single preview's flat card, which pads 0 vertically and leans on the panel + byline. */
1345
+ padding: 2px 12px 10px;
1346
+ }
1347
+
1348
+ .tlui-cmt-preview-card--selectable {
1349
+ cursor: pointer;
1350
+ }
1351
+
1352
+ /* Every card surface that tints on hover — a collapsed thread in a stack list, a preview card, and
1353
+ the single-comment preview panel. One rule, because they're the same object to the eye.
1354
+
1355
+ The opaque low-surface token, not a translucent muted one: these float directly over the canvas,
1356
+ where a translucent fill would let shapes show through.
1357
+
1358
+ The second line is why this isn't a one-liner. `--tl-shadow-3` ends with a 1px *inset* ring in
1359
+ `--tl-color-panel-contrast` — pure white in light mode — and an inset shadow paints over the
1360
+ background. At rest that's invisible (the panel is 99% white under a 100% white ring), but tint
1361
+ the background to grey and the ring stays white, hugging the edge as a pale outline. Re-tinting
1362
+ `--tl-color-panel-contrast` here wouldn't reach it: `--tl-shadow-3` is declared on the container,
1363
+ so its var() was already substituted there and the white is baked into what we inherit. Instead
1364
+ lay a ring of the new surface over the old one — first shadow in the list paints on top. */
1365
+ .tlui-cmt-stack-list__card:hover,
1366
+ .tlui-cmt-preview-card--selectable:hover,
1367
+ .tlui-cmt-canvas-preview__panel--thread.tlui-cmt-canvas-preview__panel--selectable:hover {
1368
+ --tlui-cmt-preview-hover-surface: color-mix(
1369
+ in srgb,
1370
+ var(--tl-color-low) 50%,
1371
+ var(--tl-color-panel)
1372
+ );
1373
+ background: var(--tlui-cmt-preview-hover-surface);
1374
+ box-shadow:
1375
+ inset 0 0 0 1px var(--tlui-cmt-preview-hover-surface),
1376
+ var(--tl-shadow-3);
1377
+ cursor: pointer;
1378
+ }
1379
+
1380
+ .tlui-cmt-preview-card .tlui-cmt-card {
1381
+ width: 100%;
1382
+ gap: 0;
1383
+ padding: 0;
1384
+ background: transparent;
1385
+ border: none;
1386
+ }
1387
+
1388
+ /* A preview is a glance, not a list — past a few cards it says how many were left out rather than
1389
+ growing to the height of the cluster. */
1390
+ .tlui-cmt-preview-more {
1391
+ box-sizing: border-box;
1392
+ width: 300px;
1393
+ padding: 6px 14px;
1394
+ border-radius: 12px;
1395
+ background: var(--tl-color-panel);
1396
+ box-shadow: var(--tl-shadow-3);
1397
+ color: var(--tl-color-text-3);
1398
+ font-size: 12px;
1399
+ }
1400
+
1401
+ @media (prefers-reduced-motion: no-preference) {
1402
+ .tlui-cmt-canvas-preview {
1403
+ animation: tlui-cmt-preview-in 120ms ease;
1404
+ }
1405
+ @keyframes tlui-cmt-preview-in {
1406
+ from {
1407
+ opacity: 0;
1408
+ }
1409
+ to {
1410
+ opacity: 1;
1411
+ }
1412
+ }
1413
+ }
1414
+
747
1415
  /* A region anchor's area: a dashed box under the pins, non-interactive so canvas events pass
748
1416
  through. Drawn for the live drag draft and for a region thread while hovered or open. */
749
- .cmt-canvas-region {
1417
+ .tlui-cmt-canvas-region {
750
1418
  position: absolute;
751
1419
  pointer-events: none;
752
1420
  border: 2px dashed var(--tl-color-selected);
@@ -754,15 +1422,8 @@
754
1422
  background: color-mix(in srgb, var(--tl-color-selected) 10%, transparent);
755
1423
  }
756
1424
 
757
- /* A movable region body (the 'body'/'both' move modes): draggable to translate the whole region.
758
- Interactive, so it captures the interior while shown — the tradeoff of dragging the body. */
759
- .cmt-canvas-region--movable {
760
- pointer-events: auto;
761
- cursor: move;
762
- }
763
-
764
1425
  /* Corner resize handles on a region box (three corners; the pin corner is the move handle). */
765
- .cmt-canvas-region-handle {
1426
+ .tlui-cmt-canvas-region-handle {
766
1427
  position: absolute;
767
1428
  width: 9px;
768
1429
  height: 9px;
@@ -774,59 +1435,200 @@
774
1435
  touch-action: none;
775
1436
  }
776
1437
 
777
- /* Centre the pin marker on its anchor point. */
778
- .cmt-canvas-pin__marker {
1438
+ /* Anchor the pin by its bottom-left corner, so the marker's one sharp corner (the pin shape's
1439
+ 3px bottom-left) points at the exact anchor point. */
1440
+ .tlui-cmt-canvas-pin__marker {
1441
+ position: relative;
779
1442
  width: max-content;
780
- transform: translate(-50%, -50%);
781
- cursor: grab;
1443
+ transform: translate(0, -100%);
1444
+ /* Default at rest, like hovering a shape; the editor's move cursor while dragging (below). */
1445
+ cursor: var(--tl-cursor-default);
782
1446
  touch-action: none;
1447
+ pointer-events: none;
1448
+ }
1449
+
1450
+ /* Dragging a pin shows the same move cursor as dragging a shape. */
1451
+ .tlui-cmt-canvas-pin--dragging .tlui-cmt-canvas-pin__marker {
1452
+ cursor: var(--tl-cursor-move);
1453
+ }
1454
+
1455
+ /* The predictable hit area: a fixed square over the marker's box, and the only part of the pin
1456
+ that takes pointer events. Events over it target the marker (a pseudo-element's events report
1457
+ its originating element), so the marker's handlers and :hover keep working — but the hit area
1458
+ no longer stretches with pin content or scales with the hover transform. */
1459
+ .tlui-cmt-canvas-pin__marker::after {
1460
+ content: '';
1461
+ position: absolute;
1462
+ inset: 0;
1463
+ pointer-events: auto;
1464
+ }
1465
+
1466
+ /* With the hit layer targeting the marker, the pin visual is never the hovered element itself
1467
+ (`:hover` matches the target and its ancestors, and the pin is the marker's child) — so the
1468
+ canvas pin's hover has to key off the hit layer instead of the marker's own `:hover`. Only the
1469
+ selector differs: the values come from the same tokens the shared rule uses, so a pin on the
1470
+ canvas and a badge (which is hovered directly) lift by exactly the same amount. */
1471
+ .tlui-cmt-canvas-pin__marker:hover .tlui-cmt-marker {
1472
+ transform: scale(var(--tlui-cmt-marker-hover-scale));
783
1473
  }
784
- .cmt-canvas-pin__marker:active {
785
- cursor: grabbing;
1474
+ .tlui-cmt-canvas-pin__marker:hover .tlui-cmt-marker:not(.tlui-cmt-marker--open) {
1475
+ box-shadow: var(--tlui-cmt-marker-shadow-hover);
1476
+ }
1477
+ /* The comment pin doesn't scale on hover — only the shadow lifts (badges still scale). */
1478
+ .tlui-cmt-canvas-pin__marker:hover .tlui-cmt-pin {
1479
+ transform: none;
786
1480
  }
787
1481
 
788
1482
  /* The open thread's popover — portaled to the menus layer (above the UI), positioned at the pin. */
789
- .cmt-canvas-popover {
1483
+ .tlui-cmt-canvas-popover {
790
1484
  position: absolute;
791
1485
  z-index: var(--tl-layer-menus);
792
1486
  pointer-events: auto;
793
1487
  }
794
1488
 
795
1489
  /* The placement composer — a distinct floating view (portaled to the menus layer, below the click
796
- point): a pencil "draft" avatar beside a pill field that is itself the surface (no wrapping
797
- panel). Scoped here so the in-thread reply composer keeps its own squarer look. */
798
- .cmt-canvas-composer {
1490
+ point): a draft avatar pin beside a pill field that is itself the surface (no wrapping panel).
1491
+ Scoped here so the in-thread reply composer keeps its own squarer look. */
1492
+ .tlui-cmt-canvas-composer {
799
1493
  position: absolute;
800
1494
  z-index: var(--tl-layer-menus);
801
1495
  pointer-events: auto;
802
- transform: translateY(8px);
803
- /* The rich-text editor has no intrinsic width (unlike the old <input>), so give the floating
804
- placement composer a definite width or it collapses to nothing. */
805
- width: 280px;
806
- }
807
- .cmt-canvas-composer .cmt-composer__field {
1496
+ /* Anchor the draft avatar bottom-left like the pins it becomes: it is a pin wide and sits 4px
1497
+ into the row, so its bottom-left corner is 4px right of and 4px + a pin below the composer's
1498
+ top-left corner. */
1499
+ transform: translate(-4px, calc(-4px - var(--tlui-cmt-pin-size)));
1500
+ }
1501
+
1502
+ /* A region placement centres the draft avatar on the region's pin corner — the pin it becomes
1503
+ straddles that corner. The avatar's centre sits 4px + half a pin into the composer. */
1504
+ .tlui-cmt-canvas-composer--region {
1505
+ --tlui-cmt-draft-avatar-centre: calc(4px + var(--tlui-cmt-pin-size) / 2);
1506
+ transform: translate(
1507
+ calc(-1 * var(--tlui-cmt-draft-avatar-centre)),
1508
+ calc(-1 * var(--tlui-cmt-draft-avatar-centre))
1509
+ );
1510
+ }
1511
+
1512
+ /* Fallback placement (no composer to show): no draft avatar to anchor by, so no offset — a small
1513
+ panel hanging off the click point, sized to its content (e.g. the host's sign-in prompt). */
1514
+ .tlui-cmt-canvas-composer--fallback {
1515
+ transform: none;
1516
+ width: auto;
1517
+ display: flex;
1518
+ flex-direction: column;
1519
+ padding: 8px;
808
1520
  background: var(--tl-color-panel);
809
- border-color: transparent;
810
1521
  border-radius: var(--tl-radius-4);
1522
+ box-shadow: var(--tl-shadow-3);
1523
+ }
1524
+ .tlui-cmt-canvas-composer .tlui-cmt-composer {
1525
+ gap: 6px;
1526
+ }
1527
+ /* The card: a fixed 300×42 surface whose uniform 3px padding derives the 294×36 focusable input. */
1528
+ .tlui-cmt-canvas-composer .tlui-cmt-composer__field {
1529
+ position: relative;
1530
+ flex: none;
1531
+ width: 300px;
1532
+ min-height: 42px;
1533
+ padding: 3px;
1534
+ border: none;
1535
+ background: var(--tl-color-panel);
1536
+ border-radius: var(--tl-radius-3);
1537
+ box-shadow: var(--tl-shadow-2);
1538
+ /* The pin is anchored by its bottom corner at the click point and can't move; raise the taller
1539
+ card onto the shorter pin's centre instead. */
1540
+ margin-top: -6px;
1541
+ }
1542
+ /* Focused (the placement composer autofocuses on mount): a selection-blue ring inset 3px from the
1543
+ card edge, so the card frames a bordered input rather than the border sitting on the card edge. */
1544
+ .tlui-cmt-canvas-composer .tlui-cmt-composer__field:focus-within::before {
1545
+ content: '';
1546
+ position: absolute;
1547
+ inset: 3px;
1548
+ border: 1px solid var(--tl-color-selected);
1549
+ border-radius: var(--tl-radius-2);
1550
+ pointer-events: none;
1551
+ }
1552
+ /* The text sits 12px in from the focus ring rather than against it. The placeholder is absolutely
1553
+ positioned (it ignores the wrap's padding), so it carries the same offset itself. */
1554
+ .tlui-cmt-canvas-composer .tlui-cmt-composer__input-wrap {
1555
+ padding: 4.5px 12px;
1556
+ }
1557
+ .tlui-cmt-canvas-composer .tlui-cmt-composer__field--expanded .tlui-cmt-composer__input-wrap {
1558
+ /* The base expanded rule re-pads the right edge for the squarer composer's geometry; here the
1559
+ 12px is already symmetric. */
1560
+ padding-right: 12px;
1561
+ }
1562
+ .tlui-cmt-canvas-composer .tlui-cmt-input__placeholder {
1563
+ left: 12px;
1564
+ }
1565
+ /* The draft avatar previews the pin it becomes — a white 28px pin holding the author's 22px avatar
1566
+ circle — sharing the composer's lift. Not interactive: it drops the pin's pointer cursor and
1567
+ hover affordances (scale + shadow bump), but keeps a 40×40 hit area (the ::after below). */
1568
+ .tlui-cmt-canvas-composer .tlui-cmt-pin {
1569
+ position: relative;
1570
+ box-shadow: var(--tl-shadow-2);
1571
+ cursor: default;
1572
+ }
1573
+ .tlui-cmt-canvas-composer .tlui-cmt-pin::after {
1574
+ content: '';
1575
+ position: absolute;
1576
+ inset: -6px;
1577
+ }
1578
+ .tlui-cmt-canvas-composer .tlui-cmt-pin:hover {
1579
+ transform: none;
811
1580
  box-shadow: var(--tl-shadow-2);
812
1581
  }
1582
+ /* The placement composer's send button — a bare arrow, no pill: grey while empty, black once
1583
+ there's something to send. A 28px square centred in the 36px input height, so its hover wash
1584
+ clears the focus ring rather than butting against it; the ::after keeps a 40×40 hit area. */
1585
+ .tlui-cmt-canvas-composer .tlui-cmt-send {
1586
+ position: relative;
1587
+ width: 28px;
1588
+ height: 28px;
1589
+ margin-right: 4px;
1590
+ background: transparent;
1591
+ border-radius: var(--tl-radius-2);
1592
+ color: var(--tl-color-text-3);
1593
+ }
1594
+ .tlui-cmt-canvas-composer .tlui-cmt-send::after {
1595
+ content: '';
1596
+ position: absolute;
1597
+ inset: -6px;
1598
+ }
1599
+ .tlui-cmt-canvas-composer .tlui-cmt-send:not(:disabled) {
1600
+ color: var(--tl-color-text-1);
1601
+ }
1602
+ /* Hover and press wash grey behind the bare arrow (the same treatment as the thread actions),
1603
+ overriding the base button's blue fill. */
1604
+ .tlui-cmt-canvas-composer .tlui-cmt-send:hover:not(:disabled) {
1605
+ background: var(--tl-color-muted-2);
1606
+ }
1607
+ .tlui-cmt-canvas-composer .tlui-cmt-send:active:not(:disabled) {
1608
+ background: var(--tl-color-muted-1);
1609
+ }
1610
+ /* In the expanded (multi-line) layout the send drops to the bottom-right corner; give it the same
1611
+ 4px gap from the focus ring on the bottom that it already keeps on the right. */
1612
+ .tlui-cmt-canvas-composer .tlui-cmt-composer__field--expanded .tlui-cmt-send {
1613
+ margin-bottom: 4px;
1614
+ }
813
1615
 
814
- /* The comments list panel — a right-side surface shown while the comment tool is active. Below
815
- the popover layer, so an open thread still draws over it. Sits under the top-right share panel
816
- (top offset clears its ~48px height) and sizes to its content, scrolling past max-height rather
817
- than stretching the full canvas height. */
818
- .cmt-canvas-sidebar {
1616
+ /* The comments list panel — a right-side surface shown while the comments sidebar is open. Below
1617
+ the popover layer, so an open thread still draws over it. Its top aligns flush with the style
1618
+ panel (48px from the top, clearing the share panel) and it sizes to its content, scrolling past
1619
+ max-height rather than stretching the full canvas height. */
1620
+ .tlui-cmt-canvas-sidebar {
819
1621
  position: absolute;
820
- top: 56px;
1622
+ top: 48px;
821
1623
  right: 8px;
822
1624
  width: 280px;
823
- max-height: calc(100% - 64px);
1625
+ max-height: calc(100% - 56px);
824
1626
  z-index: var(--tl-layer-panels);
825
1627
  pointer-events: auto;
826
1628
  display: flex;
827
1629
  background: var(--tl-color-panel);
828
1630
  border-radius: var(--tl-radius-4);
829
- box-shadow: var(--tl-shadow-2);
1631
+ box-shadow: var(--tl-shadow-3);
830
1632
  overflow: hidden;
831
1633
  }
832
1634