@tldraw/commenting 5.3.0-internal.c5c7f1d817d0 → 5.3.0-next.0362a8b4a246

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