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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +1037 -0
- data/README.md +621 -40
- data/app/assets/stylesheets/chats.css +906 -2
- data/app/controllers/chats/application_controller.rb +45 -4
- data/app/controllers/chats/conversations_controller.rb +396 -86
- data/app/controllers/chats/messages_controller.rb +35 -13
- data/app/controllers/chats/reactions_controller.rb +14 -0
- data/app/helpers/chats/engine_helper.rb +230 -7
- data/app/javascript/chats/composer_controller.js +14 -1
- data/app/javascript/chats/dock_controller.js +1120 -0
- data/app/javascript/chats/recovery.js +539 -0
- data/app/javascript/chats/refresh_inbox_controller.js +76 -0
- data/app/javascript/chats/split_controller.js +131 -0
- data/app/javascript/chats/thread_controller.js +322 -60
- data/app/javascript/chats/unread_badge_controller.js +24 -0
- data/app/views/chats/conversations/_conversation_pane.html.erb +53 -0
- data/app/views/chats/conversations/_conversation_row.html.erb +37 -4
- data/app/views/chats/conversations/_empty_pane.html.erb +23 -0
- data/app/views/chats/conversations/_group.html.erb +61 -0
- data/app/views/chats/conversations/_history.html.erb +54 -0
- data/app/views/chats/conversations/_locked_composer.html.erb +20 -0
- data/app/views/chats/conversations/_thread.html.erb +147 -0
- data/app/views/chats/conversations/_thread_header.html.erb +83 -0
- data/app/views/chats/conversations/badge.turbo_stream.erb +6 -0
- data/app/views/chats/conversations/dock.html.erb +40 -0
- data/app/views/chats/conversations/index.html.erb +142 -15
- data/app/views/chats/conversations/pane.html.erb +14 -0
- data/app/views/chats/conversations/refresh.turbo_stream.erb +24 -4
- data/app/views/chats/conversations/show.html.erb +18 -134
- data/app/views/chats/conversations/window.html.erb +26 -0
- data/app/views/chats/dock/_badge.html.erb +12 -0
- data/app/views/chats/dock/_dock.html.erb +139 -0
- data/app/views/chats/dock/_signal.html.erb +26 -0
- data/app/views/chats/dock/_update.turbo_stream.erb +10 -0
- data/app/views/chats/dock/_window.html.erb +23 -0
- data/app/views/chats/dock/_window_header.html.erb +75 -0
- data/app/views/chats/dock/_window_shell.html.erb +17 -0
- data/app/views/chats/messages/_action.html.erb +15 -0
- data/app/views/chats/messages/_composer.html.erb +84 -56
- data/app/views/chats/messages/_message.html.erb +43 -15
- data/app/views/chats/messages/locked.turbo_stream.erb +6 -0
- data/app/views/chats/shared/_unread_badge.html.erb +4 -1
- data/app/views/chats/shared/_verified_badge.html.erb +32 -0
- data/config/importmap.rb +10 -1
- data/config/locales/en.yml +40 -0
- data/config/locales/es.yml +40 -0
- data/config/routes.rb +8 -0
- data/context7.json +4 -0
- data/docs/PRD.md +1 -1
- data/docs/campfire_review.md +1 -1
- data/docs/screenshots/composer-actions.png +0 -0
- data/docs/screenshots/dock-bar.png +0 -0
- data/docs/screenshots/dock-desktop.png +0 -0
- data/docs/screenshots/dock-phone-button.png +0 -0
- data/docs/screenshots/dock-phone-panel.png +0 -0
- data/docs/screenshots/dock-phone-window.png +0 -0
- data/docs/screenshots/inbox.png +0 -0
- data/docs/screenshots/split-inbox-empty.png +0 -0
- data/docs/screenshots/split-inbox-phone-list.png +0 -0
- data/docs/screenshots/split-inbox-phone-thread.png +0 -0
- data/docs/screenshots/split-inbox.png +0 -0
- data/docs/screenshots/system-note-action.png +0 -0
- data/docs/screenshots/thread-actions-phone.png +0 -0
- data/gemfiles/rails_7.1.gemfile +3 -0
- data/gemfiles/rails_7.2.gemfile +3 -0
- data/gemfiles/rails_8.1.gemfile +3 -0
- data/lib/chats/action.rb +85 -0
- data/lib/chats/broadcasts.rb +178 -17
- data/lib/chats/configuration.rb +115 -1
- data/lib/chats/engine.rb +29 -7
- data/lib/chats/errors.rb +16 -0
- data/lib/chats/inbox.rb +363 -0
- data/lib/chats/inbox_group.rb +75 -0
- data/lib/chats/inbox_refresh_job.rb +14 -0
- data/lib/chats/macros.rb +33 -1
- data/lib/chats/models/concerns/chat_subject.rb +23 -0
- data/lib/chats/models/concerns/messager.rb +122 -6
- data/lib/chats/models/conversation.rb +153 -19
- data/lib/chats/models/message.rb +292 -21
- data/lib/chats/models/participant.rb +67 -0
- data/lib/chats/models/reaction.rb +18 -0
- data/lib/chats/send_rate_limited.rb +30 -0
- data/lib/chats/subscribers.rb +156 -0
- data/lib/chats/version.rb +1 -1
- data/lib/chats.rb +123 -15
- data/lib/generators/chats/templates/add_action_to_chats_messages.rb.erb +37 -0
- data/lib/generators/chats/templates/add_author_to_chats_messages.rb.erb +44 -0
- data/lib/generators/chats/templates/create_chats_tables.rb.erb +22 -2
- data/lib/generators/chats/templates/initializer.rb +125 -14
- data/lib/generators/chats/upgrade_generator.rb +79 -0
- data/lib/generators/chats/views_generator.rb +3 -2
- 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
|
-
|
|
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
|
-
|
|
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
|
+
}
|