chats 0.1.1 → 0.6.2

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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1037 -0
  3. data/README.md +621 -40
  4. data/app/assets/stylesheets/chats.css +906 -2
  5. data/app/controllers/chats/application_controller.rb +45 -4
  6. data/app/controllers/chats/conversations_controller.rb +396 -86
  7. data/app/controllers/chats/messages_controller.rb +35 -13
  8. data/app/controllers/chats/reactions_controller.rb +14 -0
  9. data/app/helpers/chats/engine_helper.rb +230 -7
  10. data/app/javascript/chats/composer_controller.js +14 -1
  11. data/app/javascript/chats/dock_controller.js +1120 -0
  12. data/app/javascript/chats/recovery.js +539 -0
  13. data/app/javascript/chats/refresh_inbox_controller.js +76 -0
  14. data/app/javascript/chats/split_controller.js +131 -0
  15. data/app/javascript/chats/thread_controller.js +322 -60
  16. data/app/javascript/chats/unread_badge_controller.js +24 -0
  17. data/app/views/chats/conversations/_conversation_pane.html.erb +53 -0
  18. data/app/views/chats/conversations/_conversation_row.html.erb +37 -4
  19. data/app/views/chats/conversations/_empty_pane.html.erb +23 -0
  20. data/app/views/chats/conversations/_group.html.erb +61 -0
  21. data/app/views/chats/conversations/_history.html.erb +54 -0
  22. data/app/views/chats/conversations/_locked_composer.html.erb +20 -0
  23. data/app/views/chats/conversations/_thread.html.erb +147 -0
  24. data/app/views/chats/conversations/_thread_header.html.erb +83 -0
  25. data/app/views/chats/conversations/badge.turbo_stream.erb +6 -0
  26. data/app/views/chats/conversations/dock.html.erb +40 -0
  27. data/app/views/chats/conversations/index.html.erb +142 -15
  28. data/app/views/chats/conversations/pane.html.erb +14 -0
  29. data/app/views/chats/conversations/refresh.turbo_stream.erb +24 -4
  30. data/app/views/chats/conversations/show.html.erb +18 -134
  31. data/app/views/chats/conversations/window.html.erb +26 -0
  32. data/app/views/chats/dock/_badge.html.erb +12 -0
  33. data/app/views/chats/dock/_dock.html.erb +139 -0
  34. data/app/views/chats/dock/_signal.html.erb +26 -0
  35. data/app/views/chats/dock/_update.turbo_stream.erb +10 -0
  36. data/app/views/chats/dock/_window.html.erb +23 -0
  37. data/app/views/chats/dock/_window_header.html.erb +75 -0
  38. data/app/views/chats/dock/_window_shell.html.erb +17 -0
  39. data/app/views/chats/messages/_action.html.erb +15 -0
  40. data/app/views/chats/messages/_composer.html.erb +84 -56
  41. data/app/views/chats/messages/_message.html.erb +43 -15
  42. data/app/views/chats/messages/locked.turbo_stream.erb +6 -0
  43. data/app/views/chats/shared/_unread_badge.html.erb +4 -1
  44. data/app/views/chats/shared/_verified_badge.html.erb +32 -0
  45. data/config/importmap.rb +10 -1
  46. data/config/locales/en.yml +40 -0
  47. data/config/locales/es.yml +40 -0
  48. data/config/routes.rb +8 -0
  49. data/context7.json +4 -0
  50. data/docs/PRD.md +1 -1
  51. data/docs/campfire_review.md +1 -1
  52. data/docs/screenshots/composer-actions.png +0 -0
  53. data/docs/screenshots/dock-bar.png +0 -0
  54. data/docs/screenshots/dock-desktop.png +0 -0
  55. data/docs/screenshots/dock-phone-button.png +0 -0
  56. data/docs/screenshots/dock-phone-panel.png +0 -0
  57. data/docs/screenshots/dock-phone-window.png +0 -0
  58. data/docs/screenshots/inbox.png +0 -0
  59. data/docs/screenshots/split-inbox-empty.png +0 -0
  60. data/docs/screenshots/split-inbox-phone-list.png +0 -0
  61. data/docs/screenshots/split-inbox-phone-thread.png +0 -0
  62. data/docs/screenshots/split-inbox.png +0 -0
  63. data/docs/screenshots/system-note-action.png +0 -0
  64. data/docs/screenshots/thread-actions-phone.png +0 -0
  65. data/gemfiles/rails_7.1.gemfile +3 -0
  66. data/gemfiles/rails_7.2.gemfile +3 -0
  67. data/gemfiles/rails_8.1.gemfile +3 -0
  68. data/lib/chats/action.rb +85 -0
  69. data/lib/chats/broadcasts.rb +178 -17
  70. data/lib/chats/configuration.rb +115 -1
  71. data/lib/chats/engine.rb +29 -7
  72. data/lib/chats/errors.rb +16 -0
  73. data/lib/chats/inbox.rb +363 -0
  74. data/lib/chats/inbox_group.rb +75 -0
  75. data/lib/chats/inbox_refresh_job.rb +14 -0
  76. data/lib/chats/macros.rb +33 -1
  77. data/lib/chats/models/concerns/chat_subject.rb +23 -0
  78. data/lib/chats/models/concerns/messager.rb +122 -6
  79. data/lib/chats/models/conversation.rb +153 -19
  80. data/lib/chats/models/message.rb +292 -21
  81. data/lib/chats/models/participant.rb +67 -0
  82. data/lib/chats/models/reaction.rb +18 -0
  83. data/lib/chats/send_rate_limited.rb +30 -0
  84. data/lib/chats/subscribers.rb +156 -0
  85. data/lib/chats/version.rb +1 -1
  86. data/lib/chats.rb +123 -15
  87. data/lib/generators/chats/templates/add_action_to_chats_messages.rb.erb +37 -0
  88. data/lib/generators/chats/templates/add_author_to_chats_messages.rb.erb +44 -0
  89. data/lib/generators/chats/templates/create_chats_tables.rb.erb +22 -2
  90. data/lib/generators/chats/templates/initializer.rb +125 -14
  91. data/lib/generators/chats/upgrade_generator.rb +79 -0
  92. data/lib/generators/chats/views_generator.rb +3 -2
  93. metadata +51 -2
@@ -11,6 +11,7 @@
11
11
  * :root {
12
12
  * --chats-accent: #facc15; /\* brand color: own bubbles, send button *\/
13
13
  * --chats-accent-contrast: #111827; /\* text on top of the accent *\/
14
+ * --chats-verified: #0284c7; /\* the "official account" badge *\/
14
15
  * }
15
16
  *
16
17
  * Want a completely different look? `rails g chats:views` ejects the
@@ -26,10 +27,24 @@
26
27
  --chats-border: rgba(0, 0, 0, 0.08);
27
28
  --chats-text: #111827;
28
29
  --chats-text-muted: #6b7280;
30
+ --chats-verified: #0284c7;
29
31
  --chats-bubble-bg: #f3f4f6;
30
32
  --chats-bubble-text: #111827;
31
33
  --chats-radius: 1.1rem;
32
34
  --chats-max-width: 42rem;
35
+ /* The conversation list beside an open conversation (two panes). */
36
+ --chats-list-width: 320px;
37
+ /* --chats-split-height, how tall the two-pane page is, is NOT declared
38
+ here: the rules read it with the whole screen (100dvh) as the fallback,
39
+ so a host's own declaration wins wherever it is, even on a :root rule
40
+ loaded before this file (the bundled views link it in the body).
41
+ Under a host header: calc(100dvh - <the header's height>). */
42
+
43
+ /* Safe areas, re-pointable by a host with its own source of truth. */
44
+ --chats-safe-top: env(safe-area-inset-top, 0px);
45
+ --chats-safe-right: env(safe-area-inset-right, 0px);
46
+ --chats-safe-bottom: env(safe-area-inset-bottom, 0px);
47
+ --chats-safe-left: env(safe-area-inset-left, 0px);
33
48
  }
34
49
 
35
50
  /* --- Shell ----------------------------------------------------------------- */
@@ -71,7 +86,11 @@
71
86
  text-decoration: none;
72
87
  color: inherit;
73
88
  }
74
- .chats-row__link:hover { background: var(--chats-surface); }
89
+ /* Hover only where there is one: a touch screen "hovers" what it tapped and
90
+ keeps the tint after the finger is gone (the open row stayed grey). */
91
+ @media (hover: hover) {
92
+ .chats-row__link:hover { background: var(--chats-surface); }
93
+ }
75
94
  .chats-row__body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.1rem; }
76
95
  .chats-row__top, .chats-row__bottom {
77
96
  display: flex;
@@ -79,6 +98,12 @@
79
98
  justify-content: space-between;
80
99
  gap: 0.75rem;
81
100
  }
101
+ .chats-row__name {
102
+ display: flex;
103
+ align-items: center;
104
+ gap: 0.25rem;
105
+ min-width: 0;
106
+ }
82
107
  .chats-row__title {
83
108
  font-weight: 650;
84
109
  white-space: nowrap;
@@ -119,6 +144,9 @@
119
144
  flex-shrink: 0;
120
145
  }
121
146
  .chats-badge--floating[hidden] { display: none; }
147
+ /* chats_unread_badge's wrapper (its catch-up controller): no box of its own,
148
+ so the badge sits in the host's layout exactly as if it weren't there. */
149
+ .chats-unread-badge-live { display: contents; }
122
150
 
123
151
  /* --- Empty states ------------------------------------------------------------ */
124
152
 
@@ -147,6 +175,34 @@
147
175
  user-select: none;
148
176
  }
149
177
 
178
+ /* --- Verified messagers ------------------------------------------------------
179
+ *
180
+ * The "official account" mark (`acts_as_messager verified: true`). It sizes
181
+ * itself from the text it sits next to, so it reads right in an inbox row and
182
+ * in the thread header without either one hard-coding a pixel value. Recolour
183
+ * it with --chats-verified; replace the glyph with config.verified_badge.
184
+ *
185
+ * On #0284c7 rather than the more familiar #1d9bf0: the badge is a meaningful
186
+ * graphic, so WCAG 1.4.11 asks for 3:1 against what it sits on, and #1d9bf0
187
+ * is 3.00:1 on --chats-bg but only 2.73:1 on --chats-surface — which is the
188
+ * row hover background, so the mark failed exactly while someone was pointing
189
+ * at it. #0284c7 clears 3:1 on both (4.10 and 3.72) and on a dark ground too
190
+ * (4.33 on #111827), so a host that inverts the palette inherits a badge that
191
+ * still passes instead of one it has to remember to fix. Pinned by
192
+ * test/verified_badge_contrast_test.rb.
193
+ */
194
+ .chats-verified {
195
+ flex-shrink: 0;
196
+ display: inline-flex;
197
+ align-items: center;
198
+ color: var(--chats-verified);
199
+ line-height: 0;
200
+ }
201
+ .chats-verified__glyph {
202
+ width: 1em;
203
+ height: 1em;
204
+ }
205
+
150
206
  /* --- Thread -------------------------------------------------------------------- */
151
207
 
152
208
  .chats-thread {
@@ -179,6 +235,12 @@
179
235
  font-size: 1rem;
180
236
  font-weight: 750;
181
237
  margin: 0;
238
+ display: flex;
239
+ align-items: center;
240
+ gap: 0.25rem;
241
+ min-width: 0;
242
+ }
243
+ .chats-thread__name {
182
244
  white-space: nowrap;
183
245
  overflow: hidden;
184
246
  text-overflow: ellipsis;
@@ -375,6 +437,64 @@ turbo-frame[id^="chats_page_"] {
375
437
  .chats-message--deleted .chats-message__bubble { background: transparent; border: 1px dashed var(--chats-border); }
376
438
  .chats-message__tombstone { color: var(--chats-text-muted); font-size: 0.85rem; }
377
439
 
440
+ /* --- Actions (Chats::Action) ------------------------------------------------------- */
441
+ /*
442
+ A message's one featured button, under its words: full width, so it reads
443
+ as something to tap rather than a link in the text. It is a sibling of
444
+ .chats-message__text, never inside it, so the text's pre-wrap stays off it.
445
+
446
+ Contrast comes from the pair a host already tunes together: the accent and
447
+ the text that sits on it. A foreign bubble (and a system note) gets an
448
+ accent button; an own bubble IS the accent, so its button swaps the pair.
449
+ Same two colours either way, so whatever passes for the send button and
450
+ own bubbles passes here — default theme or host theme alike.
451
+
452
+ Scoped to the bubble with `>`: 0.1.0's inline actions row used this class
453
+ name, and views ejected back then may still carry it on other elements.
454
+ The long-press popup clones .chats-message__bubble, so this applies there
455
+ too.
456
+ */
457
+ .chats-message__bubble > .chats-message__action,
458
+ .chats-message__system > .chats-message__action {
459
+ display: block;
460
+ margin-top: 0.5rem;
461
+ padding: 0.55rem 0.9rem;
462
+ border-radius: calc(var(--chats-radius) * 0.7);
463
+ background: var(--chats-accent);
464
+ color: var(--chats-accent-contrast);
465
+ font-size: 0.88rem;
466
+ font-weight: 700;
467
+ line-height: 1.25;
468
+ text-align: center;
469
+ text-decoration: none;
470
+ overflow-wrap: anywhere;
471
+ cursor: pointer;
472
+ }
473
+ .chats-message--own .chats-message__bubble > .chats-message__action {
474
+ background: var(--chats-accent-contrast);
475
+ color: var(--chats-accent);
476
+ }
477
+ .chats-message__bubble > .chats-message__action:hover,
478
+ .chats-message__system > .chats-message__action:hover { filter: brightness(0.92); }
479
+ /* The focus ring sits on the BUBBLE, just outside the button, so it takes
480
+ the colour of the bubble's own text — which is what contrasts there. */
481
+ .chats-message__bubble > .chats-message__action:focus-visible,
482
+ .chats-message__system > .chats-message__action:focus-visible {
483
+ outline: 2px solid var(--chats-bubble-text);
484
+ outline-offset: 2px;
485
+ }
486
+ .chats-message--own .chats-message__bubble > .chats-message__action:focus-visible {
487
+ outline-color: var(--chats-accent-contrast);
488
+ }
489
+ .chats-message__system > .chats-message__action:focus-visible { outline-color: var(--chats-text); }
490
+ /* A system note is a pill; one holding a button becomes a card, or the
491
+ button's corners would poke through the pill's rounded ends. */
492
+ .chats-message__system--with-action {
493
+ border-radius: var(--chats-radius);
494
+ padding: 0.6rem 0.9rem;
495
+ }
496
+ .chats-message__system > .chats-message__action { margin-top: 0.45rem; }
497
+
378
498
  /* --- Attachments -------------------------------------------------------------------- */
379
499
 
380
500
  .chats-message__attachments {
@@ -617,12 +737,43 @@ turbo-frame[id^="chats_page_"] {
617
737
 
618
738
  /* --- Composer ----------------------------------------------------------------------------- */
619
739
 
740
+ /* The dock (since 0.4.0 a div holding the host's composer_actions and the
741
+ form; a composer ejected before that is the form itself, and this box
742
+ styles it just the same). */
620
743
  .chats-composer {
621
744
  flex: 0 0 auto;
622
745
  background: var(--chats-bg);
623
746
  border-top: 1px solid var(--chats-border);
624
747
  padding: 0.6rem 0.9rem calc(0.6rem + env(safe-area-inset-bottom, 0px));
625
748
  }
749
+ .chats-composer__form { margin: 0; }
750
+
751
+ /* The composer_actions slot: a row of the host's own buttons above the
752
+ input. `display: contents` lets button_to's wrapping <form> step aside so
753
+ its button sits in the row like a link does. Every default here is
754
+ wrapped in :where() — zero specificity — so ANY class the host puts on
755
+ its buttons or forms wins, and an unstyled button still looks like it
756
+ belongs. */
757
+ .chats-composer__actions {
758
+ display: flex;
759
+ flex-wrap: wrap;
760
+ gap: 0.4rem;
761
+ margin: 0 0 0.55rem;
762
+ }
763
+ :where(.chats-composer__actions) :where(form) { display: contents; }
764
+ :where(.chats-composer__actions) :where(a, button) {
765
+ padding: 0.35rem 0.8rem;
766
+ border: 1px solid var(--chats-border);
767
+ border-radius: 999px;
768
+ background: var(--chats-surface);
769
+ color: var(--chats-text);
770
+ font: inherit;
771
+ font-size: 0.8rem;
772
+ font-weight: 600;
773
+ text-decoration: none;
774
+ cursor: pointer;
775
+ }
776
+ :where(.chats-composer__actions) :where(a, button):hover { filter: brightness(0.96); }
626
777
  .chats-composer__bar { display: flex; align-items: flex-end; gap: 0.55rem; }
627
778
  .chats-composer__input {
628
779
  flex: 1;
@@ -718,7 +869,12 @@ turbo-frame[id^="chats_page_"] {
718
869
  padding: 0.2rem 0.4rem;
719
870
  }
720
871
 
721
- .chats-composer__previews[hidden] { display: none; }
872
+ /* `hidden` loses to any author `display`, so every element here that sets
873
+ one and starts hidden says so explicitly — the edit cue and the file
874
+ count used to show in hosts with no CSS reset of their own. */
875
+ .chats-composer__previews[hidden],
876
+ .chats-composer__edit[hidden],
877
+ .chats-composer__file-count[hidden] { display: none; }
722
878
  .chats-composer__preview {
723
879
  position: relative;
724
880
  flex-shrink: 0;
@@ -816,3 +972,751 @@ turbo-frame[id^="chats_page_"] {
816
972
  border-radius: 0.5rem;
817
973
  }
818
974
  .chats-emoji:hover { background: var(--chats-surface); }
975
+
976
+ /* ---------------------------------------------------------------------------
977
+ 0.2.0 — stacked inbox rows, locked composers, signed messages
978
+ --------------------------------------------------------------------------- */
979
+
980
+ /* A stack (Chats::InboxGroup) reads as one row; the count line sits where a
981
+ single conversation shows its subject. */
982
+ .chats-row--group .chats-row__subject { font-weight: 600; }
983
+
984
+ /* The inbox filtered to one counterpart (?with=). */
985
+ .chats-inbox__filter {
986
+ display: flex;
987
+ align-items: baseline;
988
+ gap: 0.5rem;
989
+ margin: 0 0 0.5rem;
990
+ font-size: 0.85rem;
991
+ color: var(--chats-text-muted);
992
+ }
993
+ .chats-inbox__filter-clear { color: inherit; text-decoration: underline; }
994
+
995
+ /* "See all" back to the stack, under the thread title. */
996
+ .chats-thread__see-all { margin: 0; font-size: 0.72rem; }
997
+ .chats-thread__see-all-link { color: var(--chats-text-muted); text-decoration: underline; }
998
+
999
+ /* Who wrote it, when that isn't the seat it came from. */
1000
+ .chats-message__signature {
1001
+ margin-top: 0.25rem;
1002
+ font-size: 0.7rem;
1003
+ font-style: italic;
1004
+ opacity: 0.75;
1005
+ }
1006
+
1007
+ /* The composer, replaced by the reason the conversation is closed. Same box,
1008
+ so the thread doesn't jump when a subject locks under an open composer. */
1009
+ .chats-composer--locked {
1010
+ display: block;
1011
+ padding: 0.85rem 1rem;
1012
+ text-align: center;
1013
+ }
1014
+ .chats-composer__locked-notice {
1015
+ margin: 0;
1016
+ font-size: 0.85rem;
1017
+ color: var(--chats-text-muted);
1018
+ }
1019
+
1020
+
1021
+ /* ---------------------------------------------------------------------------
1022
+ 0.6.0 — two panes (config.inbox_layout = :split)
1023
+ ---------------------------------------------------------------------------
1024
+
1025
+ The messaging page as LinkedIn has it: from 700px wide (the dock's phone
1026
+ rule, and chats--split's), the list on the left and the open conversation
1027
+ on the right, both as tall as the screen, each scrolling on its own. Below
1028
+ that, one column, exactly as the separate pages: /messages is the list,
1029
+ /messages/:id the thread. Every request renders both panes; this media
1030
+ query alone picks, so it holds before JavaScript runs and without it.
1031
+
1032
+ .chats-split the page (.chats.chats-inbox too)
1033
+ .chats-split__list the list's <nav>
1034
+ .chats-split__conversation the `chats_conversation` frame
1035
+ .chats-split__pane a conversation in it (section)
1036
+ .chats-split__thread its permanent wrapper, around .chats-thread--pane
1037
+ .chats-split__empty no conversation open (--inbox: no conversations at all)
1038
+
1039
+ The page is as wide as the list plus a thread page (--chats-max-width),
1040
+ so the conversation reads at the width it always had, and as tall as
1041
+ --chats-split-height (the screen; a host with its own header above the
1042
+ page sets what is left: calc(100dvh - 4rem)). */
1043
+ .chats-split {
1044
+ max-width: calc(var(--chats-list-width) + var(--chats-max-width));
1045
+ /* The list alone (a phone): at least what is left of the screen, never
1046
+ the shell's whole 100dvh, which would run past it under a host header. */
1047
+ min-height: var(--chats-split-height, 100dvh);
1048
+ }
1049
+ .chats-split__list { display: flex; flex-direction: column; min-width: 0; }
1050
+ .chats-split__conversation {
1051
+ display: flex;
1052
+ flex-direction: column;
1053
+ min-width: 0;
1054
+ min-height: 0;
1055
+ }
1056
+ .chats-split__pane,
1057
+ .chats-split__thread {
1058
+ flex: 1;
1059
+ min-width: 0;
1060
+ min-height: 0;
1061
+ display: flex;
1062
+ flex-direction: column;
1063
+ }
1064
+ /* The thread page's body, inside the pane: no shell of its own (the page is
1065
+ the .chats shell), and the pane's height, not the screen's. */
1066
+ .chats-thread--pane {
1067
+ flex: 1;
1068
+ min-height: 0;
1069
+ height: auto;
1070
+ display: flex;
1071
+ flex-direction: column;
1072
+ overflow: hidden;
1073
+ }
1074
+ .chats-split__empty { margin: auto; }
1075
+ .chats-row--current .chats-row__link {
1076
+ background: var(--chats-surface);
1077
+ box-shadow: inset 3px 0 0 var(--chats-accent);
1078
+ }
1079
+
1080
+ @media (min-width: 700px) {
1081
+ .chats-split {
1082
+ box-sizing: border-box;
1083
+ flex-direction: row;
1084
+ height: var(--chats-split-height, 100dvh);
1085
+ min-height: 0;
1086
+ overflow: hidden;
1087
+ /* Its edges, where a page wider than it shows the host's background. */
1088
+ border-inline: 1px solid var(--chats-border);
1089
+ }
1090
+ .chats-split__list {
1091
+ box-sizing: border-box;
1092
+ flex: 0 0 var(--chats-list-width);
1093
+ width: var(--chats-list-width);
1094
+ border-right: 1px solid var(--chats-border);
1095
+ }
1096
+ /* The rows scroll; the title and the search stay. */
1097
+ .chats-split__list > turbo-frame {
1098
+ display: block;
1099
+ flex: 1;
1100
+ min-height: 0;
1101
+ overflow-y: auto;
1102
+ overscroll-behavior-y: contain;
1103
+ }
1104
+ .chats-split__conversation { flex: 1 1 0; }
1105
+ /* The list is right there: no way back to it. */
1106
+ .chats-split .chats-thread__back { display: none; }
1107
+ }
1108
+
1109
+ @media (max-width: 699.98px) {
1110
+ /* A conversation open: the page is the thread, as tall as the screen. */
1111
+ .chats-split:has(.chats-split__pane) {
1112
+ height: var(--chats-split-height, 100dvh);
1113
+ min-height: 0;
1114
+ overflow: hidden;
1115
+ }
1116
+ .chats-split:has(.chats-split__pane) > .chats-split__list { display: none; }
1117
+ .chats-split:has(.chats-split__pane) > .chats-split__conversation { flex: 1; }
1118
+ /* None: the page is the list. The empty pane shows under it only to say
1119
+ there are no conversations at all (the inbox's empty state lives there). */
1120
+ .chats-split:not(:has(.chats-split__pane, .chats-split__empty--inbox)) > .chats-split__conversation {
1121
+ display: none;
1122
+ }
1123
+ }
1124
+
1125
+ /* ---------------------------------------------------------------------------
1126
+ 0.5.0 — the messaging dock (chats_dock)
1127
+ ---------------------------------------------------------------------------
1128
+
1129
+ Windows to the left, the recent-chats panel at the right edge, flush with
1130
+ the bottom of the viewport. One element (`.chats-dock`, permanent across
1131
+ Turbo visits) rendered by `chats_dock`; the chats--dock controller owns
1132
+ the state classes:
1133
+
1134
+ .chats-dock[data-mode] "dock" or "sheet", set once JS runs (the
1135
+ layout itself comes from the media query
1136
+ at the end, so it holds before and without)
1137
+ .chats-dock__panel[open] the recent list shows (a <details>)
1138
+ .chats-window--minimized folded to its header
1139
+ .chats-window--unread something arrived the viewer hasn't seen
1140
+ .chats-window--front the one window a phone shows
1141
+ .chats-window--entering its first 200ms; --popped: opened by a message
1142
+
1143
+ Derived variables live on :where(.chats-dock), NOT :root: a custom
1144
+ property that references another resolves where it is declared, so a host
1145
+ re-pointing --chats-bg on .chats-dock would otherwise leave --chats-dock-bg
1146
+ frozen at the :root value. :where() keeps them at zero specificity.
1147
+
1148
+ Never a lasting transform, filter, backdrop-filter, perspective or contain
1149
+ on the dock or a window: each makes the window the containing block of the
1150
+ thread's position: fixed popup and photo preview, which would then open
1151
+ inside a 336px box. Entrance animations end at `transform: none`, and
1152
+ minimizing changes a height, not a transform. */
1153
+
1154
+ :where(.chats-dock) {
1155
+ --chats-dock-z: 40; /* above the page, under host modals */
1156
+ --chats-dock-offset: 0px; /* bottom clearance (a cookie banner…) */
1157
+ --chats-dock-inset: 16px; /* from the right edge */
1158
+ --chats-dock-gap: 8px;
1159
+ --chats-dock-bar-height: 48px; /* the bar, every window header */
1160
+ --chats-dock-panel-width: 288px;
1161
+ --chats-dock-panel-height: 560px;
1162
+ --chats-dock-window-width: 336px;
1163
+ --chats-dock-window-height: 480px;
1164
+ --chats-dock-squeezed-width: 104px; /* a window folded to make room */
1165
+ --chats-dock-top-clearance: 24px;
1166
+ --chats-dock-radius: 12px;
1167
+ --chats-dock-bg: var(--chats-bg);
1168
+ --chats-dock-header-bg: var(--chats-bg);
1169
+ /* Not --chats-surface: muted text on it is 4.39:1; on this, 4.63:1. */
1170
+ --chats-dock-hover: #f9fafb;
1171
+ --chats-dock-edge: rgba(17, 24, 39, 0.12);
1172
+ --chats-dock-shadow: 0 12px 40px rgba(17, 24, 39, 0.16), 0 2px 6px rgba(17, 24, 39, 0.08);
1173
+ --chats-dock-unread-bg: var(--chats-accent);
1174
+ --chats-dock-unread-text: var(--chats-accent-contrast);
1175
+ --chats-dock-focus: var(--chats-accent);
1176
+ /* --chats-dock-title-font: unset on purpose; titles inherit the host font. */
1177
+ --chats-dock-launcher-size: 56px;
1178
+ --chats-dock-sheet-header-height: 56px;
1179
+ --chats-dock-duration: 200ms;
1180
+ --chats-dock-ease: cubic-bezier(0.2, 0.8, 0.2, 1);
1181
+ }
1182
+
1183
+ /* --- The dock ----------------------------------------------------------------- */
1184
+
1185
+ .chats-dock {
1186
+ position: fixed;
1187
+ z-index: var(--chats-dock-z);
1188
+ right: calc(var(--chats-dock-inset) + var(--chats-safe-right));
1189
+ bottom: var(--chats-dock-offset);
1190
+ display: flex;
1191
+ align-items: flex-end;
1192
+ gap: var(--chats-dock-gap);
1193
+ color: var(--chats-text);
1194
+ font-size: 0.875rem;
1195
+ line-height: 1.4;
1196
+ pointer-events: none; /* the gaps between surfaces never eat page clicks */
1197
+ }
1198
+ /* Fixed-size surfaces with padding: never trust the host to have a reset. */
1199
+ .chats-dock,
1200
+ .chats-dock *,
1201
+ .chats-dock *::before,
1202
+ .chats-dock *::after { box-sizing: border-box; }
1203
+ .chats-dock[hidden] { display: none; }
1204
+ .chats-dock__windows {
1205
+ display: flex;
1206
+ align-items: flex-end;
1207
+ gap: var(--chats-dock-gap);
1208
+ }
1209
+ .chats-window,
1210
+ .chats-dock__panel { pointer-events: auto; }
1211
+
1212
+ /* Inset: every surface clips (overflow: hidden), so an outside ring is cut. */
1213
+ .chats-dock :focus-visible {
1214
+ outline: 2px solid var(--chats-dock-focus);
1215
+ outline-offset: -2px;
1216
+ }
1217
+
1218
+ /* --- Surfaces (panel and windows share one skin) -------------------------------- */
1219
+
1220
+ .chats-window,
1221
+ .chats-dock__panel {
1222
+ background: var(--chats-dock-bg);
1223
+ border: 1px solid var(--chats-dock-edge);
1224
+ border-bottom: 0;
1225
+ border-radius: var(--chats-dock-radius) var(--chats-dock-radius) 0 0;
1226
+ box-shadow: var(--chats-dock-shadow);
1227
+ overflow: hidden;
1228
+ }
1229
+
1230
+ .chats-dock__panel {
1231
+ position: relative;
1232
+ width: var(--chats-dock-panel-width);
1233
+ padding-bottom: var(--chats-safe-bottom);
1234
+ }
1235
+ .chats-dock__panel[open] {
1236
+ height: min(var(--chats-dock-panel-height),
1237
+ calc(100dvh - var(--chats-dock-offset) - var(--chats-dock-top-clearance)));
1238
+ }
1239
+
1240
+ .chats-window {
1241
+ display: flex;
1242
+ flex-direction: column;
1243
+ flex-shrink: 0;
1244
+ width: var(--chats-dock-window-width);
1245
+ height: min(var(--chats-dock-window-height),
1246
+ calc(100dvh - var(--chats-dock-offset) - var(--chats-dock-top-clearance)));
1247
+ }
1248
+ /* Folded to its header (and the home-indicator strip). A height, not a
1249
+ transform: the thread keeps its box and its scroll position, and its
1250
+ zero-height scroller is how the thread controller knows not to read. */
1251
+ .chats-window--minimized { height: calc(var(--chats-dock-bar-height) + var(--chats-safe-bottom)); }
1252
+ /* Folded to make room (it was in use when another opened, and the row is
1253
+ full): the width the controller budgets for it, the avatar (which expands
1254
+ it) and ×. The name stays in the title's tooltip. */
1255
+ .chats-window--squeezed { width: var(--chats-dock-squeezed-width); }
1256
+ .chats-window--squeezed :is(.chats-window__identity, .chats-window__unread, .chats-window__menu,
1257
+ .chats-window__expand, .chats-window__minimize) {
1258
+ display: none;
1259
+ }
1260
+ /* Nothing of the thread keeps a height while folded, padding included, so
1261
+ the geometry says what the state says: there's nothing to read. */
1262
+ .chats-window--minimized .chats-thread--window .chats-thread__scroll { padding-top: 0; padding-bottom: 0; }
1263
+ .chats-window__card {
1264
+ display: flex;
1265
+ flex-direction: column;
1266
+ flex: 1;
1267
+ min-height: 0;
1268
+ }
1269
+
1270
+ /* --- The bar (the panel's <summary>) and window headers -------------------------- */
1271
+
1272
+ .chats-dock__bar,
1273
+ .chats-window__header {
1274
+ flex: 0 0 var(--chats-dock-bar-height);
1275
+ height: var(--chats-dock-bar-height);
1276
+ display: flex;
1277
+ align-items: center;
1278
+ background: var(--chats-dock-header-bg);
1279
+ border-bottom: 1px solid var(--chats-dock-edge);
1280
+ }
1281
+ .chats-dock__toggle {
1282
+ gap: 10px;
1283
+ color: inherit;
1284
+ padding: 0 8px 0 12px;
1285
+ list-style: none;
1286
+ cursor: pointer;
1287
+ user-select: none;
1288
+ }
1289
+ .chats-dock__toggle::-webkit-details-marker { display: none; }
1290
+ .chats-dock__panel:not([open]) .chats-dock__toggle { border-bottom-color: transparent; }
1291
+ .chats-dock__toggle .chats-avatar { width: 32px; height: 32px; font-size: 0.8rem; }
1292
+ .chats-dock__icon { display: none; width: 26px; height: 26px; flex-shrink: 0; }
1293
+ .chats-dock__title {
1294
+ flex: 1;
1295
+ min-width: 0;
1296
+ font-family: var(--chats-dock-title-font);
1297
+ font-size: 0.9375rem;
1298
+ font-weight: 700;
1299
+ white-space: nowrap;
1300
+ overflow: hidden;
1301
+ text-overflow: ellipsis;
1302
+ }
1303
+ .chats-dock__chevron,
1304
+ .chats-dock__close-glyph {
1305
+ flex: 0 0 32px;
1306
+ width: 32px;
1307
+ height: 32px;
1308
+ display: inline-grid;
1309
+ place-items: center;
1310
+ border-radius: 8px;
1311
+ color: var(--chats-text-muted);
1312
+ }
1313
+ .chats-dock__chevron svg,
1314
+ .chats-dock__close-glyph svg {
1315
+ width: 18px;
1316
+ height: 18px;
1317
+ transition: transform var(--chats-dock-duration) var(--chats-dock-ease);
1318
+ }
1319
+ @media (hover: hover) {
1320
+ .chats-dock__toggle:hover .chats-dock__chevron { background: var(--chats-dock-hover); color: var(--chats-text); }
1321
+ }
1322
+ .chats-dock__panel[open] .chats-dock__chevron svg { transform: rotate(180deg); }
1323
+ .chats-dock__close-glyph { display: none; }
1324
+
1325
+ /* Badges in the dock: one size, a hair smaller than the inbox's. */
1326
+ .chats-dock .chats-badge {
1327
+ min-width: 1.25rem;
1328
+ height: 1.25rem;
1329
+ padding: 0 0.3rem;
1330
+ font-size: 0.6875rem;
1331
+ font-variant-numeric: tabular-nums;
1332
+ }
1333
+ .chats-dock .chats-badge[hidden] { display: none; }
1334
+
1335
+ /* --- The panel body -------------------------------------------------------------- */
1336
+
1337
+ .chats-dock__body {
1338
+ /* Positioned, not a flex item: a <details>' content sits in a UA slot box,
1339
+ so flex sizing from the panel never reaches it. */
1340
+ position: absolute;
1341
+ inset: var(--chats-dock-bar-height) 0 var(--chats-safe-bottom) 0;
1342
+ display: flex;
1343
+ flex-direction: column;
1344
+ }
1345
+ .chats-dock__panel[open] .chats-dock__body {
1346
+ animation: chats-dock-rise var(--chats-dock-duration) var(--chats-dock-ease);
1347
+ }
1348
+ .chats-dock__search { padding: 8px 12px 4px; }
1349
+ .chats-dock__search .chats-search__input { padding: 0.45rem 0.8rem; font-size: 0.8125rem; }
1350
+ .chats-dock__results {
1351
+ display: block;
1352
+ flex: 1;
1353
+ min-height: 0;
1354
+ overflow-y: auto;
1355
+ overscroll-behavior: contain;
1356
+ }
1357
+ .chats-dock__list { list-style: none; margin: 0; padding: 4px 0; }
1358
+ .chats-dock__list .chats-row__link { padding: 9px 12px; gap: 10px; }
1359
+ @media (hover: hover) {
1360
+ .chats-dock__list .chats-row__link:hover { background: var(--chats-dock-hover); }
1361
+ }
1362
+ .chats-dock__list .chats-avatar { width: 40px; height: 40px; font-size: 0.85rem; }
1363
+ .chats-dock__list .chats-row__title { font-size: 0.875rem; }
1364
+ .chats-dock__list .chats-row__subject { font-size: 0.6875rem; }
1365
+ .chats-dock__list .chats-row__preview { font-size: 0.8125rem; }
1366
+ .chats-dock__list .chats-row__time { font-size: 0.6875rem; }
1367
+ /* A conversation that is open in a window right now. */
1368
+ .chats-dock__list .chats-row__link[aria-current="true"] { box-shadow: inset 3px 0 0 var(--chats-accent); }
1369
+ .chats-row__muted { flex-shrink: 0; width: 14px; height: 14px; color: var(--chats-text-muted); }
1370
+
1371
+ .chats-dock__see-all {
1372
+ flex: 0 0 44px;
1373
+ display: flex;
1374
+ align-items: center;
1375
+ justify-content: center;
1376
+ border-top: 1px solid var(--chats-dock-edge);
1377
+ color: var(--chats-text);
1378
+ font-size: 0.8125rem;
1379
+ font-weight: 650;
1380
+ text-decoration: none;
1381
+ }
1382
+ @media (hover: hover) {
1383
+ .chats-dock__see-all:hover { background: var(--chats-dock-hover); text-decoration: underline; }
1384
+ }
1385
+
1386
+ .chats-dock .chats-empty { padding: 40px 20px; }
1387
+ .chats-dock .chats-empty__icon { font-size: 2rem; margin-bottom: 0.5rem; }
1388
+ .chats-dock .chats-empty__title { font-size: 0.9375rem; }
1389
+ .chats-dock .chats-empty__hint { font-size: 0.8125rem; }
1390
+
1391
+ /* The very first load: four still rows (no shimmer). */
1392
+ .chats-dock__skeleton { display: flex; gap: 10px; align-items: center; padding: 9px 12px; }
1393
+ .chats-dock__skeleton::before {
1394
+ content: "";
1395
+ width: 40px;
1396
+ height: 40px;
1397
+ border-radius: 999px;
1398
+ background: var(--chats-surface);
1399
+ flex-shrink: 0;
1400
+ }
1401
+ .chats-dock__skeleton span { flex: 1; display: flex; flex-direction: column; gap: 6px; }
1402
+ .chats-dock__skeleton span::before,
1403
+ .chats-dock__skeleton span::after { content: ""; height: 10px; border-radius: 4px; background: var(--chats-surface); }
1404
+ .chats-dock__skeleton span::before { width: 45%; }
1405
+ .chats-dock__skeleton span::after { width: 80%; }
1406
+
1407
+ /* --- Windows --------------------------------------------------------------------- */
1408
+
1409
+ .chats-window__header { padding-right: 4px; position: relative; }
1410
+ .chats-window__title {
1411
+ flex: 1;
1412
+ min-width: 0;
1413
+ height: 100%;
1414
+ display: flex;
1415
+ align-items: center;
1416
+ gap: 8px;
1417
+ padding: 0 4px 0 8px;
1418
+ border: 0;
1419
+ background: none;
1420
+ color: inherit;
1421
+ font: inherit;
1422
+ text-align: left;
1423
+ cursor: pointer;
1424
+ }
1425
+ .chats-window__title .chats-avatar { width: 32px; height: 32px; font-size: 0.8rem; }
1426
+ .chats-window__identity { flex: 1; min-width: 0; display: flex; flex-direction: column; }
1427
+ .chats-window__name-row { display: flex; align-items: center; gap: 4px; min-width: 0; }
1428
+ .chats-window__name {
1429
+ font-family: var(--chats-dock-title-font);
1430
+ font-size: 0.875rem;
1431
+ font-weight: 700;
1432
+ line-height: 1.25;
1433
+ white-space: nowrap;
1434
+ overflow: hidden;
1435
+ text-overflow: ellipsis;
1436
+ }
1437
+ .chats-window__subject {
1438
+ font-size: 0.75rem;
1439
+ line-height: 1.25;
1440
+ color: var(--chats-text-muted);
1441
+ white-space: nowrap;
1442
+ overflow: hidden;
1443
+ text-overflow: ellipsis;
1444
+ }
1445
+ .chats-window__title .chats-badge { margin-left: 2px; }
1446
+ .chats-window__icon {
1447
+ flex: 0 0 32px;
1448
+ width: 32px;
1449
+ height: 32px;
1450
+ display: inline-grid;
1451
+ place-items: center;
1452
+ padding: 0;
1453
+ border: 0;
1454
+ border-radius: 8px;
1455
+ background: none;
1456
+ color: var(--chats-text-muted);
1457
+ font: inherit;
1458
+ font-size: 1.1rem;
1459
+ line-height: 1;
1460
+ cursor: pointer;
1461
+ text-decoration: none;
1462
+ list-style: none;
1463
+ }
1464
+ .chats-window__icon::-webkit-details-marker { display: none; }
1465
+ .chats-window__icon svg { width: 18px; height: 18px; }
1466
+ @media (hover: hover) {
1467
+ .chats-window__icon:hover { background: var(--chats-dock-hover); color: var(--chats-text); }
1468
+ }
1469
+ .chats-window__back { display: none; }
1470
+ /* The host's thread actions (conversation_header_actions). */
1471
+ .chats-window__menu { flex: 0 0 auto; }
1472
+ .chats-window__menu .chats-menu__panel { top: calc(100% - 4px); right: 4px; z-index: 30; }
1473
+ /* Touch in dock mode (a tablet): the same 48px header, fatter targets. */
1474
+ @media (pointer: coarse) {
1475
+ .chats-dock[data-mode="dock"] .chats-window__icon { flex-basis: 40px; width: 40px; height: 40px; }
1476
+ }
1477
+
1478
+ /* Focused: a 2px accent line along the top of the header. */
1479
+ .chats-window:focus-within .chats-window__header { box-shadow: inset 0 2px 0 var(--chats-dock-focus); }
1480
+
1481
+ /* Waiting to be seen (popped open, or minimized when something arrived): the
1482
+ header becomes the accent pair, the one every host already tunes. */
1483
+ .chats-window--unread .chats-window__header {
1484
+ background: var(--chats-dock-unread-bg);
1485
+ color: var(--chats-dock-unread-text);
1486
+ border-bottom-color: transparent;
1487
+ box-shadow: none;
1488
+ }
1489
+ .chats-window--unread :is(.chats-window__subject, .chats-window__icon, .chats-verified) { color: inherit; }
1490
+ @media (hover: hover) {
1491
+ .chats-window--unread .chats-window__icon:hover { background: color-mix(in srgb, currentColor 16%, transparent); color: inherit; }
1492
+ }
1493
+ .chats-window--unread .chats-window__header .chats-badge {
1494
+ background: var(--chats-dock-unread-text);
1495
+ color: var(--chats-dock-unread-bg);
1496
+ }
1497
+ .chats-window--unread .chats-window__header :focus-visible { outline-color: var(--chats-dock-unread-text); }
1498
+
1499
+ /* The thread, inside a window: the full page's body, compacted. Rooted at
1500
+ .chats-thread--window, never .chats (the full-page shell). */
1501
+ .chats-thread--window {
1502
+ --chats-radius: 0.95rem;
1503
+ flex: 1;
1504
+ min-height: 0;
1505
+ height: auto;
1506
+ display: flex;
1507
+ flex-direction: column;
1508
+ overflow: hidden;
1509
+ background: var(--chats-dock-bg);
1510
+ }
1511
+ .chats-thread--window .chats-thread__scroll { padding: 10px 12px 6px; }
1512
+ /* A short thread sits on the composer: the eye is at the bottom. */
1513
+ .chats-thread--window .chats-thread__scroll > :first-child { margin-top: auto; }
1514
+ .chats-thread--window .chats-day-separator,
1515
+ .chats-thread--window .chats-thread__unread-line { margin: 0.5rem 0; }
1516
+ .chats-thread--window .chats-message { max-width: 88%; }
1517
+ .chats-thread--window .chats-message__avatar { width: 1.6rem; height: 1.6rem; font-size: 0.65rem; }
1518
+ .chats-thread--window .chats-message__bubble { padding: 0.4rem 0.7rem; font-size: 0.875rem; line-height: 1.35; }
1519
+ .chats-thread--window .chats-message__bubble > .chats-message__action { font-size: 0.8125rem; padding: 0.45rem 0.8rem; }
1520
+ .chats-thread--window .chats-message__image { max-width: 11rem; max-height: 11rem; }
1521
+ .chats-thread--window .chats-typing { font-size: 0.75rem; padding: 0.3rem 0.25rem; }
1522
+ .chats-thread--window .chats-composer { padding: 8px 10px calc(8px + var(--chats-safe-bottom)); }
1523
+ .chats-thread--window .chats-composer__actions { margin-bottom: 6px; }
1524
+ .chats-thread--window :where(.chats-composer__actions) :where(a, button) { font-size: 0.75rem; padding: 0.25rem 0.7rem; }
1525
+ .chats-thread--window .chats-composer__bar { gap: 8px; }
1526
+ .chats-thread--window .chats-composer__input { font-size: 0.875rem; padding: 0.45rem 0.75rem; max-height: 7.5rem; }
1527
+ .chats-thread--window .chats-composer__send { width: 2.25rem; height: 2.25rem; font-size: 0.95rem; }
1528
+ .chats-thread--window .chats-composer--locked { padding: 12px 14px calc(12px + var(--chats-safe-bottom)); }
1529
+ .chats-thread--window .chats-composer__locked-notice { font-size: 0.8125rem; }
1530
+
1531
+ /* Before anybody wrote (opened from a Message button). */
1532
+ .chats-window__hello {
1533
+ margin: auto 0;
1534
+ padding: 24px 16px;
1535
+ text-align: center;
1536
+ color: var(--chats-text-muted);
1537
+ font-size: 0.8125rem;
1538
+ }
1539
+ .chats-window__hello .chats-avatar { display: flex; width: 56px; height: 56px; font-size: 1.2rem; margin: 0 auto 10px; }
1540
+ .chats-window__hello strong { display: block; color: var(--chats-text); font-size: 0.9375rem; margin-bottom: 2px; }
1541
+ .chats-thread__scroll:has(.chats-message) .chats-window__hello { display: none; }
1542
+
1543
+ /* Loading, and failure. */
1544
+ .chats-window__status {
1545
+ margin: auto;
1546
+ padding: 24px 16px;
1547
+ text-align: center;
1548
+ color: var(--chats-text-muted);
1549
+ font-size: 0.8125rem;
1550
+ }
1551
+ .chats-window__status p { margin: 0; }
1552
+ .chats-window__retry {
1553
+ margin-top: 10px;
1554
+ padding: 0.35rem 0.9rem;
1555
+ border: 1px solid var(--chats-border);
1556
+ border-radius: 999px;
1557
+ background: var(--chats-surface);
1558
+ color: var(--chats-text);
1559
+ font: inherit;
1560
+ font-size: 0.8125rem;
1561
+ font-weight: 600;
1562
+ cursor: pointer;
1563
+ }
1564
+
1565
+ /* --- Motion ---------------------------------------------------------------------- */
1566
+
1567
+ @keyframes chats-dock-rise { from { transform: translateY(16px); opacity: 0; } }
1568
+ @keyframes chats-dock-sheet-up { from { transform: translateY(100%); } }
1569
+ @keyframes chats-dock-push { from { transform: translateX(100%); } }
1570
+ @keyframes chats-dock-bump { 50% { transform: scale(1.2); } }
1571
+ @keyframes chats-dock-flash { 50% { filter: brightness(1.18); } }
1572
+
1573
+ .chats-window--entering { animation: chats-dock-rise var(--chats-dock-duration) var(--chats-dock-ease); }
1574
+ .chats-window--popped .chats-window__header { animation: chats-dock-flash 600ms ease-in-out 2; }
1575
+ .chats-dock .chats-badge--bumped { animation: chats-dock-bump 160ms ease-out; }
1576
+
1577
+ /* --- Phones: the sheet ------------------------------------------------------------
1578
+ Under 700px wide or 500px tall (one window beside the panel needs 656px;
1579
+ a 480px window needs the height). A media query, not a class, so a phone
1580
+ gets this layout before the controller connects and without JavaScript —
1581
+ the panel is a <details>, so the closed bar is the launcher and opening it
1582
+ is the sheet. The controller reads the same query for its behaviour. */
1583
+
1584
+ @media (max-width: 699.98px), (max-height: 499.98px) {
1585
+ .chats-dock {
1586
+ inset: auto 0 0 auto;
1587
+ right: 0;
1588
+ display: block;
1589
+ }
1590
+
1591
+ /* Closed: the round launcher, bottom-right above the home indicator. */
1592
+ .chats-dock__panel:not([open]) {
1593
+ position: fixed;
1594
+ right: calc(16px + var(--chats-safe-right));
1595
+ bottom: calc(16px + var(--chats-dock-offset) + var(--chats-safe-bottom));
1596
+ width: var(--chats-dock-launcher-size);
1597
+ height: var(--chats-dock-launcher-size);
1598
+ padding: 0;
1599
+ border: 0;
1600
+ border-radius: 999px;
1601
+ background: var(--chats-accent);
1602
+ color: var(--chats-accent-contrast);
1603
+ box-shadow: 0 6px 20px rgba(17, 24, 39, 0.28);
1604
+ overflow: visible;
1605
+ }
1606
+ .chats-dock__panel:not([open]) .chats-dock__toggle {
1607
+ position: relative;
1608
+ justify-content: center;
1609
+ width: 100%;
1610
+ height: 100%;
1611
+ padding: 0;
1612
+ border: 0;
1613
+ border-radius: 999px;
1614
+ background: none;
1615
+ }
1616
+ .chats-dock__panel:not([open]) :is(.chats-avatar, .chats-dock__chevron) { display: none; }
1617
+ .chats-dock__panel:not([open]) .chats-dock__title {
1618
+ /* Still read out ("Messages, 3 unread"), just not shown. */
1619
+ position: absolute;
1620
+ width: 1px;
1621
+ height: 1px;
1622
+ overflow: hidden;
1623
+ clip: rect(0, 0, 0, 0);
1624
+ }
1625
+ .chats-dock__panel:not([open]) .chats-dock__icon { display: block; }
1626
+ .chats-dock__panel:not([open]) .chats-badge {
1627
+ position: absolute;
1628
+ top: -2px;
1629
+ right: -2px;
1630
+ /* accent-on-accent would vanish: the launcher's badge is the inverse pair */
1631
+ background: var(--chats-accent-contrast);
1632
+ color: var(--chats-accent);
1633
+ box-shadow: 0 0 0 2px var(--chats-accent);
1634
+ }
1635
+ .chats-dock--no-launcher .chats-dock__panel:not([open]) { display: none; }
1636
+ /* Anything full-screen up: the launcher would only cover it. */
1637
+ .chats-dock:has(.chats-window--front) .chats-dock__panel:not([open]) { display: none; }
1638
+
1639
+ /* Open: the full-screen sheet; a window sits on top of the list. */
1640
+ .chats-dock__panel[open],
1641
+ .chats-window {
1642
+ position: fixed;
1643
+ left: 0;
1644
+ right: 0;
1645
+ top: var(--chats-vv-top, 0px);
1646
+ width: 100%;
1647
+ height: var(--chats-vvh, 100dvh);
1648
+ border: 0;
1649
+ border-radius: 0;
1650
+ box-shadow: none;
1651
+ padding: var(--chats-safe-top) var(--chats-safe-right) 0 var(--chats-safe-left);
1652
+ }
1653
+ .chats-dock__panel[open] {
1654
+ z-index: 1;
1655
+ padding-bottom: var(--chats-safe-bottom);
1656
+ animation: chats-dock-sheet-up 260ms cubic-bezier(0.32, 0.72, 0, 1);
1657
+ }
1658
+ .chats-dock__panel[open] .chats-dock__body { animation: none; }
1659
+ .chats-window { z-index: 2; }
1660
+ .chats-window:not(.chats-window--front) { display: none; }
1661
+ .chats-window--front.chats-window--entering { animation: chats-dock-push 260ms cubic-bezier(0.32, 0.72, 0, 1); }
1662
+
1663
+ .chats-dock__body {
1664
+ inset: calc(var(--chats-safe-top) + var(--chats-dock-sheet-header-height)) var(--chats-safe-right)
1665
+ var(--chats-safe-bottom) var(--chats-safe-left);
1666
+ }
1667
+ .chats-dock__panel[open] .chats-dock__bar,
1668
+ .chats-window__header {
1669
+ flex-basis: var(--chats-dock-sheet-header-height);
1670
+ height: var(--chats-dock-sheet-header-height);
1671
+ box-shadow: none;
1672
+ }
1673
+ .chats-dock__panel[open] .chats-dock__toggle { padding: 0 6px 0 16px; }
1674
+ .chats-dock__panel[open] .chats-dock__title { font-size: 1.125rem; font-weight: 800; }
1675
+ .chats-dock__panel[open] :is(.chats-dock__toggle .chats-avatar, .chats-dock__chevron) { display: none; }
1676
+ .chats-dock__panel[open] .chats-dock__close-glyph { display: inline-grid; flex-basis: 44px; width: 44px; height: 44px; }
1677
+ .chats-dock__panel[open] .chats-dock__close-glyph svg { width: 22px; height: 22px; }
1678
+ .chats-dock__search { padding: 8px 16px; }
1679
+ /* 16px or iOS Safari zooms the page on focus. */
1680
+ .chats-dock__search .chats-search__input { font-size: 16px; padding: 0.55rem 0.9rem; }
1681
+ .chats-dock__list .chats-row__link { padding: 10px 16px; gap: 12px; }
1682
+ .chats-dock__list .chats-avatar { width: 48px; height: 48px; }
1683
+ .chats-dock__list .chats-row__title { font-size: 1rem; }
1684
+ .chats-dock__list .chats-row__preview { font-size: 0.875rem; }
1685
+ .chats-dock__list .chats-row__time { font-size: 0.75rem; }
1686
+ .chats-dock__see-all { flex-basis: 52px; font-size: 0.9375rem; }
1687
+
1688
+ .chats-window__back { display: inline-grid; }
1689
+ .chats-window__header { padding: 0 4px; }
1690
+ .chats-window__header :is(.chats-window__minimize, .chats-window__close) { display: none; }
1691
+ .chats-window__icon { flex-basis: 44px; width: 44px; height: 44px; }
1692
+ .chats-window__icon svg { width: 22px; height: 22px; }
1693
+ .chats-window__title { cursor: default; padding-left: 2px; }
1694
+ .chats-window__title .chats-avatar { width: 36px; height: 36px; }
1695
+ .chats-window__name { font-size: 1rem; }
1696
+ /* Full screen: the conversation is in front of you, nothing to flag. */
1697
+ .chats-window--unread .chats-window__header {
1698
+ background: var(--chats-dock-header-bg);
1699
+ color: inherit;
1700
+ border-bottom-color: var(--chats-dock-edge);
1701
+ }
1702
+ .chats-thread--window .chats-thread__scroll { padding: 12px 14px 8px; }
1703
+ .chats-thread--window .chats-message__bubble { font-size: 1rem; padding: 0.5rem 0.8rem; }
1704
+ .chats-thread--window .chats-composer { padding: 8px 12px calc(8px + var(--chats-safe-bottom)); }
1705
+ .chats-thread--window .chats-composer__input { font-size: 16px; padding: 0.55rem 0.9rem; }
1706
+ .chats-thread--window .chats-composer__send { width: 2.75rem; height: 2.75rem; }
1707
+ /* The keyboard is up: the home-indicator strip is behind it. */
1708
+ .chats-dock--keyboard .chats-thread--window .chats-composer { padding-bottom: 8px; }
1709
+ }
1710
+
1711
+ html.chats-dock-sheet-open { overflow: hidden; }
1712
+
1713
+ @media (prefers-reduced-motion: reduce) {
1714
+ .chats-dock,
1715
+ .chats-dock *,
1716
+ .chats-dock *::before,
1717
+ .chats-dock *::after {
1718
+ animation: none !important;
1719
+ transition-duration: 0s !important;
1720
+ transition-delay: 0s !important;
1721
+ }
1722
+ }