livechat 0.8.2 → 0.9.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fae79e3391f4f16e1823d4d9b8755061c5ae66a4651d36f19b47448cae320f25
4
- data.tar.gz: a65a2fad39a547c6ce06c4b9d6d25cea355868ae848819f155e01038fe6d1ee4
3
+ metadata.gz: 77c8f578ea3bb8e56ae6e93c3ffc3194bbec65a1952d7e7f3e2152986bc12388
4
+ data.tar.gz: bb5e62c493d8aef5e20a82884ed43213f2671543bee6e06d2aa1a352a55f9d25
5
5
  SHA512:
6
- metadata.gz: 812f2d09748da467a8d79d1921520f03b643d3430d1e53b159e3d1801a5fb79e9e64310db905edb2332ce4f8a5b6dfbcdfdaf2a68bb0efc4d4c017df9337c35c
7
- data.tar.gz: 0417f271610f0139a39e9b921d16a59c30b6bda3f226563dca618a3d6405b928675755c6a7ece46c677e3cab516dcbe20e8d1b3e3c394592fe901fe7c1cd717a
6
+ metadata.gz: f77110e02430ef730ac85ad64be4a05f4fd588035c384284e14374ebebbb40b3c5537df63e3d6128d20db2f1891836f54c90f223a087f123fa946893b8ea63d1
7
+ data.tar.gz: da4da0a2cfa71838e93434cdf3d0d88fd5a6fc15a24754369977d7e3064d5052b28cf6d29055db044b3132acbaf50d832f97a4eb6ef2a565d58edd37e27bbd84
data/AGENTS.md CHANGED
@@ -176,5 +176,6 @@ Conventions this codebase holds to — follow them rather than the first thing t
176
176
  - **The widget is plain ES5-style JS served by the engine**, no build step, no framework, config read from a JSON block so a Turbo visit re-reads the current page's settings.
177
177
  - **Visitor scoping is never by conversation id.** The widget's endpoints resolve the thread from the signed-in id or the guest cookie, so no id in a request can address someone else's conversation. Keep it that way.
178
178
  - **The dummy app pins `config.active_job.queue_adapter = :test`.** Do not remove it or let it drift back to the `:async` default. Attaching a file enqueues Active Storage's analysis job, and `:async` runs it on a background thread that checks out its own connection — writes no test transaction covers, landing in the middle of whatever runs next. That is a suite that fails order-dependently in a test which never created a row, and it is miserable to trace back.
179
+ - **`lib/livechat/dashboard.css` is half shared.** Everything above the `GEM-SPECIFIC` banner is the design system all five gems in the family ship — the same tokens, the same `.page-head`/`.tabs`/`.filters`/`.card`/`.badge`/`button`, the same `.dashboard-shell` + `.record-row` + `.detail-panel` two-pane dashboard — identical in every repo apart from the `lvc` prefix. Diff it against a sibling before changing it, and carry the change to the other four. Anything only this gem has goes below the banner. New dashboard markup reuses the shared class names rather than inventing a domain-specific one.
179
180
  - Every user-facing change bumps `lib/livechat/version.rb` and adds a `CHANGELOG.md` entry that says what it costs, not only what it adds.
180
181
  - Commit messages are prose that explains the tradeoff — read `git log` before writing one.
data/CHANGELOG.md CHANGED
@@ -1,5 +1,48 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.1
4
+
5
+ - **The conversation heading reads as one block.** The visitor's email and locale
6
+ moved out of their own row and sit directly under the name, inside a
7
+ `.panel-titles` wrapper, and the open/resolved switch is pinned to the right
8
+ edge of the heading row with `margin-left: auto` rather than relying on the
9
+ row's `space-between` — a long name no longer drags the switch inward. If you
10
+ targeted `.context-line` as a child of `.detail-panel`, it is now a child of
11
+ `.panel-head`.
12
+
13
+ ## 0.9.0
14
+
15
+ - **One design system across the family.** The stylesheet now opens with a
16
+ shared core — the colour tokens, `.page-head`, `.tabs`, `.filters`, `.card`,
17
+ `.badge`, buttons and form controls, and the `.dashboard-shell` +
18
+ `.record-row` + `.detail-panel` two-pane dashboard — identical in all five
19
+ gems of the family, apart from the `--lvc-` prefix. The five had drifted:
20
+ sidebars between 380px and 430px, reading columns between 860px and 1020px,
21
+ two different tab styles and three different button styles. The sidebar is
22
+ now 330–430px everywhere, a reading page 1020px, a dashboard 1280px.
23
+ Everything below the `GEM-SPECIFIC` banner is what only this gem has.
24
+ - **The inbox markup uses the shared class names.** `.inbox-shell`,
25
+ `.inbox-sidebar` and `.inbox-thread` are `.dashboard-shell`,
26
+ `.dashboard-sidebar` and `.dashboard-detail`; `.conversation-list`,
27
+ `.conversation-row`, `.conversation-main`, `.conversation-topline`,
28
+ `.conversation-meta`, `.conversation-time` and `.conversation-side` are
29
+ `.record-*`, with `.conversation-name` becoming `.record-name` and
30
+ `.conversation-preview` `.record-copy`; `.conversation-panel` is
31
+ `.detail-panel` and its `.head-row` is `.panel-head`. The body class on the
32
+ inbox is `lvc-index` (was `lvc-inbox`) and the polling element is
33
+ `#lvc-poll` (was `#lvc-index`). A conversation row now reads name, then
34
+ message preview, then ids — the order the sibling gems use. If you styled or
35
+ scripted any of those names in a host app, that is the breaking change — no
36
+ configuration, route or database change is involved.
37
+ - **The narrow-screen rules work again.** The `max-width: 760px` block sat above
38
+ the component rules it means to override, and CSS nesting adds no specificity,
39
+ so the desktop grid won every tie: showing one pane at a time on a phone had
40
+ quietly stopped working. The media query moved to the end of the file.
41
+ - Smaller fixes that came with the shared core: a submit input is styled as a
42
+ button rather than a full-width field, the filter row keeps its search box and
43
+ button on one line, and a `code.key` truncates inside a list row instead of
44
+ wrapping over three lines.
45
+
3
46
  ## 0.8.0
4
47
 
5
48
  - **`config.agent_layout` now works on its own.** The inbox's stylesheet and
data/README.md CHANGED
@@ -316,16 +316,6 @@ bundle exec rake test
316
316
  bundle exec rubocop
317
317
  ```
318
318
 
319
- ## Also by the same author
320
-
321
- - [testimonials](https://github.com/yshmarov/testimonials) — testimonials,
322
- reviews and NPS for Rails.
323
- - [ideasbugs](https://github.com/yshmarov/ideasbugs) — in-app bug reports and
324
- feature requests.
325
- - [i18n_proofreading](https://github.com/yshmarov/i18n_proofreading) — in-context
326
- translation proofreading.
327
- - [SupeRails](https://superails.com) — Rails screencasts.
328
-
329
319
  ## One family
330
320
 
331
321
  Five Rails engines built on the same backbone, so adopting a second one is
@@ -341,9 +331,9 @@ mostly muscle memory:
341
331
 
342
332
  They share the install shape (`generate <gem>:install`, mount, one initializer),
343
333
  the same host hooks (`base_controller_class` to inherit your admin's controller,
344
- `agent_layout` for just the shell), a dashboard stylesheet scoped so it
345
- cannot touch your own CSS, and migrations that follow your app's
346
- `primary_key_type`.
334
+ `agent_layout` for just the shell), one dashboard design system the same
335
+ two-pane layout, colour tokens and components in all five, scoped so it cannot
336
+ touch your own CSS — and migrations that follow your app's `primary_key_type`.
347
337
 
348
338
  ## License
349
339
 
@@ -1,6 +1,17 @@
1
- <div class="conversation-panel">
2
- <div class="head-row">
3
- <h1><%= conversation.display_name %> <span class="case-id">#<%= conversation.id %></span></h1>
1
+ <div class="detail-panel">
2
+ <div class="panel-head">
3
+ <%# The name with the visitor's context directly under it, so the status
4
+ toggle on the right of the row clears both. %>
5
+ <div class="panel-titles">
6
+ <h1><%= conversation.display_name %> <span class="case-id">#<%= conversation.id %></span></h1>
7
+ <%# One quiet line of live context: who the visitor is and the language to answer in. %>
8
+ <p class="muted context-line">
9
+ <% context = [] %>
10
+ <% context << conversation.visitor_email if conversation.visitor_email.present? %>
11
+ <% context << conversation.locale if conversation.locale.present? %>
12
+ <%= safe_join(context, ' · ') %>
13
+ </p>
14
+ </div>
4
15
  <% if conversation.open? %>
5
16
  <%= button_to resolve_conversation_path(conversation), method: :post,
6
17
  form_class: 'status-toggle-form',
@@ -20,14 +31,6 @@
20
31
  <% end %>
21
32
  </div>
22
33
 
23
- <%# One quiet line of live context: who the visitor is and the language to answer in. %>
24
- <p class="muted context-line">
25
- <% context = [] %>
26
- <% context << conversation.visitor_email if conversation.visitor_email.present? %>
27
- <% context << conversation.locale if conversation.locale.present? %>
28
- <%= safe_join(context, ' · ') %>
29
- </p>
30
-
31
34
  <div class="card">
32
35
  <% last = messages.last %>
33
36
  <% cable = livechat_cable_data("livechat:conversation:#{conversation.id}") %>
@@ -1,10 +1,12 @@
1
- <% content_for :body_class, 'lvc-inbox' %>
1
+ <% content_for :body_class, 'lvc-index' %>
2
2
 
3
3
  <%= render layout: 'livechat/shared/dashboard' do %>
4
- <h1><%= t('livechat.dashboard.title', default: 'LiveChat') %></h1>
4
+ <div class="page-head">
5
+ <h1><%= t('livechat.dashboard.title', default: 'LiveChat') %></h1>
6
+ </div>
5
7
 
6
- <div class="inbox-shell <%= 'has-selected' if @selected_conversation %>">
7
- <aside class="inbox-sidebar" aria-label="<%= t('livechat.dashboard.conversations', default: 'Conversations') %>">
8
+ <div class="dashboard-shell <%= 'has-selected' if @selected_conversation %>">
9
+ <aside class="dashboard-sidebar" aria-label="<%= t('livechat.dashboard.conversations', default: 'Conversations') %>">
8
10
  <div class="tabs">
9
11
  <% Livechat::Conversation::STATUSES.each do |status| %>
10
12
  <%= link_to conversations_path(status: status, q: @query), class: ('active' if @status == status) do %>
@@ -25,12 +27,12 @@
25
27
  <%# The list keeps itself fresh (dashboard.js reloads when the token moves) —
26
28
  unless the agent is mid-search or mid-reply, whose typing must never be eaten. %>
27
29
  <% cable = livechat_cable_data('livechat:inbox') %>
28
- <div id="lvc-index" data-poll-url="<%= poll_conversations_path %>"
30
+ <div id="lvc-poll" data-poll-url="<%= poll_conversations_path %>"
29
31
  data-token="<%= [Livechat::Message.maximum(:id).to_i, Livechat::Conversation.count,
30
32
  @counts.fetch('open', 0)].join('-') %>"
31
33
  <%= tag.attributes(data: cable) if cable.any? %>></div>
32
34
 
33
- <div class="conversation-list">
35
+ <div class="record-list">
34
36
  <% if @conversations.empty? %>
35
37
  <p class="empty"><%= t('livechat.dashboard.empty',
36
38
  default: 'Nothing here. When a visitor writes, the conversation lands on this page.') %></p>
@@ -39,27 +41,29 @@
39
41
  <% unread = @unread.fetch(conversation.id, 0) %>
40
42
  <%= link_to conversations_path(status: @status, q: @query, offset: @offset,
41
43
  conversation_id: conversation.id),
42
- class: ['conversation-row',
44
+ class: ['record-row',
43
45
  ('active' if @selected_conversation&.id == conversation.id),
44
46
  ('unread' if unread.positive?)].compact do %>
45
- <span class="conversation-main">
46
- <span class="conversation-topline">
47
- <span class="conversation-name"><%= conversation.display_name %></span>
48
- <span class="muted conversation-time">
47
+ <span class="record-main">
48
+ <span class="record-topline">
49
+ <span class="record-name"><%= conversation.display_name %></span>
50
+ <span class="muted record-time">
49
51
  <% if conversation.last_activity_at %>
50
52
  <%= time_ago_in_words(conversation.last_activity_at) %>
51
53
  <% end %>
52
54
  </span>
53
55
  </span>
54
- <span class="muted conversation-meta">
56
+ <%# Same row shape as the sibling gems: the name and time on top,
57
+ the copy that identifies the record, then the muted ids. %>
58
+ <span class="muted record-copy"><%= conversation.last_message_preview %></span>
59
+ <span class="muted record-meta">
55
60
  <span class="case-id">#<%= conversation.id %></span>
56
61
  <% if conversation.visitor_email.present? && conversation.visitor_email != conversation.display_name %>
57
62
  <span><%= conversation.visitor_email %></span>
58
63
  <% end %>
59
64
  </span>
60
- <span class="muted conversation-preview"><%= conversation.last_message_preview %></span>
61
65
  </span>
62
- <span class="conversation-side">
66
+ <span class="record-side">
63
67
  <% if unread.positive? %>
64
68
  <span class="badge unread-count"><%= unread %></span>
65
69
  <% end %>
@@ -88,7 +92,7 @@
88
92
  </div>
89
93
  </aside>
90
94
 
91
- <main class="inbox-thread" aria-label="<%= t('livechat.dashboard.current_chat', default: 'Current chat') %>">
95
+ <main class="dashboard-detail" aria-label="<%= t('livechat.dashboard.current_chat', default: 'Current chat') %>">
92
96
  <% if @selected_conversation %>
93
97
  <p class="mobile-back">
94
98
  <%= link_to "← #{t('livechat.dashboard.back', default: 'All conversations')}",
@@ -4,51 +4,56 @@
4
4
  *
5
5
  * :root custom properties, all `--lvc-` prefixed so they can
6
6
  * neither overwrite a host's nor be overwritten by them.
7
- * .lvc-page the page frame (was bare html/body). Only the gem's own
7
+ * .lvc-page the page frame (was bare html/body). Only the gem's own
8
8
  * layout puts this class on <body>, so inside a host admin
9
9
  * these rules simply do not apply.
10
- * .lvc-dashboard everything else, nested. Names like .container, .card
10
+ * .lvc-dashboard everything else, nested. Names like .container, .card
11
11
  * and .tabs are confined to the dashboard's own markup, so
12
12
  * loading this file inside a host cannot restyle its chrome.
13
13
  *
14
14
  * Nothing here styles a bare `body`, `a`, `table` or `*` at the top level.
15
+ *
16
+ * Two sections follow: the design system shared by the whole gem family,
17
+ * then what only this gem has. A rule that belongs in the shared section
18
+ * belongs in all five copies of it.
19
+ *
20
+ * Order inside a section matters — components, then the page frame, then the
21
+ * narrow-screen overrides. Nesting adds no specificity, so the last rule
22
+ * wins the ties, and an override placed before the rule it means to beat
23
+ * silently does nothing.
15
24
  */
16
25
 
26
+ /* ---------------------------------------------------------------------------
27
+ * SHARED CORE — identical in testimonials, ideasbugs, livechat, product_tours
28
+ * and i18n_proofreading apart from the prefix. Diff it against a sibling gem
29
+ * before you change it, and carry the change to all five.
30
+ * ------------------------------------------------------------------------- */
17
31
 
18
32
  :root {
19
- color-scheme: light dark;
20
- --lvc-bg: #f6f7f9; --lvc-surface: #fff; --lvc-text: #1c2024; --lvc-muted: #6b7280;
21
- --lvc-border: #e5e7eb; --lvc-accent: #2563eb; --lvc-accent-text: #fff;
22
- --lvc-open: #16a34a; --lvc-resolved: #6b7280; --lvc-danger: #dc2626;
33
+ color-scheme: light dark;
34
+ --lvc-bg: #f6f7f9;
35
+ --lvc-surface: #fff;
36
+ --lvc-text: #1c2024;
37
+ --lvc-muted: #6b7280;
38
+ --lvc-border: #e5e7eb;
39
+ --lvc-accent: #2563eb;
40
+ --lvc-accent-text: #fff;
41
+ --lvc-code: #f0f1f3;
42
+ /* The four semantic states every gem in the family badges something with. */
43
+ --lvc-success: #16a34a;
44
+ --lvc-warning: #d97706;
45
+ --lvc-danger: #dc2626;
46
+ --lvc-neutral: #6b7280;
23
47
  }
24
48
  @media (prefers-color-scheme: dark) {
25
49
  :root {
26
- --lvc-bg: #111418; --lvc-surface: #1a1f26; --lvc-text: #e6e8ea; --lvc-muted: #9aa2ab;
27
- --lvc-border: #2a313a; --lvc-accent: #3b82f6;
28
- }
29
- }
30
- @media (max-width: 760px) {
31
- .lvc-page { font-size: 14px; }
32
- .lvc-inbox { overflow: auto; }
33
- .lvc-inbox, .lvc-inbox .container { min-height: 100vh; height: auto; }
34
- .lvc-inbox .container { padding-bottom: 10px; }
35
- .lvc-dashboard {
36
- & .container { padding: 14px 10px 24px; }
37
- & .inbox-shell { display: block; min-height: calc(100vh - 62px); }
38
- & .inbox-sidebar,
39
- & .inbox-thread { min-height: calc(100vh - 62px); }
40
- & .inbox-shell.has-selected .inbox-sidebar { display: none; }
41
- & .inbox-shell:not(.has-selected) .inbox-thread { display: none; }
42
- & .conversation-list { overflow: visible; }
43
- & .conversation-row { padding: 12px; }
44
- & .conversation-meta { display: none; }
45
- & .conversation-panel { min-height: calc(100vh - 64px); }
46
- & .inbox-thread .conversation-panel { padding: 0; }
47
- & .mobile-back { display: block; flex: 0 0 auto; margin: 10px 10px 0; font-size: 13px; }
48
- & .conversation-panel .head-row h1 { font-size: 18px; }
49
- & .status-toggle { width: 152px; min-height: 34px; }
50
- & .status-toggle span { padding: 0 7px; font-size: 12px; }
51
- & .thread .msg { max-width: 88%; }
50
+ --lvc-bg: #111418;
51
+ --lvc-surface: #1a1f26;
52
+ --lvc-text: #e6e8ea;
53
+ --lvc-muted: #9aa2ab;
54
+ --lvc-border: #2a313a;
55
+ --lvc-accent: #3b82f6;
56
+ --lvc-code: #232a33;
52
57
  }
53
58
  }
54
59
 
@@ -61,37 +66,239 @@
61
66
  & * { box-sizing: border-box; }
62
67
  & a { color: var(--lvc-accent); text-decoration: none; }
63
68
  & a:hover { text-decoration: underline; }
64
- & .container { max-width: 860px; margin: 0 auto; padding: 24px 16px 64px; }
65
- & h1 { font-size: 22px; margin: 0 0 16px; }
66
- & .tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--lvc-border); margin-bottom: 16px; flex-wrap: wrap; }
67
- & .tabs a { padding: 8px 14px; border-radius: 8px 8px 0 0; color: var(--lvc-muted); }
68
- & .tabs a.active { color: var(--lvc-text); font-weight: 600; border: 1px solid var(--lvc-border); border-bottom: 2px solid var(--lvc-surface); background: var(--lvc-surface); margin-bottom: -1px; }
69
- & .tabs a:hover { text-decoration: none; color: var(--lvc-text); }
70
- & .count { display: inline-block; min-width: 20px; padding: 0 6px; margin-left: 4px; border-radius: 999px; background: var(--lvc-border); font-size: 12px; text-align: center; }
71
- & .filters { margin-bottom: 16px; display: flex; gap: 8px; }
72
- & .filters input[type=search] { flex: 1; max-width: 320px; padding: 6px 10px; border: 1px solid var(--lvc-border); border-radius: 8px; background: var(--lvc-surface); color: var(--lvc-text); font: inherit; }
73
- & .card { background: var(--lvc-surface); border: 1px solid var(--lvc-border); border-radius: 12px; overflow: hidden; }
74
- & .card.pad { padding: 16px; }
69
+ & :focus-visible { outline: 2px solid var(--lvc-accent); outline-offset: 2px; }
70
+
71
+ /* Page furniture. `.container` is the reading width; an index page widens it
72
+ to 1280px in the page frame below. */
73
+ & .container { max-width: 1020px; margin: 0 auto; padding: 24px 16px 64px; }
74
+ & h1 { margin: 0 0 16px; font-size: 22px; }
75
+ & h2 { margin: 0 0 12px; font-size: 16px; }
76
+ & .page-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 14px; }
77
+ & .page-head h1 { margin: 0; }
78
+ & .lede { margin: 4px 0 0; color: var(--lvc-muted); }
79
+ & .breadcrumb { margin: 0 0 12px; font-size: 13px; }
80
+ & .muted { color: var(--lvc-muted); font-size: 13px; }
81
+ & .case-id { color: var(--lvc-muted); font-weight: 400; font-size: 15px; }
82
+ & .spacer { flex: 1; }
83
+ & .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
84
+
85
+ /* Section switcher: the pills above the dashboard when a gem has more than
86
+ one section. Lives on the page, so a host `admin_layout` can drop it. */
87
+ & .nav { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 16px; }
88
+ & .nav a { padding: 6px 16px; border: 1px solid var(--lvc-border); border-radius: 999px; background: var(--lvc-surface); color: var(--lvc-muted); font-weight: 600; }
89
+ & .nav a:hover { text-decoration: none; color: var(--lvc-text); }
90
+ & .nav a.active { border-color: var(--lvc-accent); background: var(--lvc-accent); color: var(--lvc-accent-text); }
91
+ & .nav a.active:hover { color: var(--lvc-accent-text); }
92
+
93
+ /* Tabs: one system for the whole family — an underlined row of equal-width
94
+ tabs, sized to sit at the top of the sidebar. */
95
+ & .tabs { display: flex; gap: 6px; }
96
+ & .tabs a { position: relative; display: flex; flex: 1 1 0; align-items: center; justify-content: center; gap: 6px; min-height: 34px; padding: 5px 10px 8px; border-radius: 8px; color: var(--lvc-muted); font-weight: 600; }
97
+ & .tabs a:hover { background: var(--lvc-bg); color: var(--lvc-text); text-decoration: none; }
98
+ & .tabs a.active { color: var(--lvc-text); }
99
+ & .tabs a.active::after { content: ""; position: absolute; right: 20px; bottom: 0; left: 20px; height: 2px; border-radius: 999px; background: var(--lvc-accent); }
100
+ & .count { display: inline-block; min-width: 20px; padding: 0 6px; border-radius: 999px; background: color-mix(in srgb, var(--lvc-muted) 14%, transparent); font-size: 12px; text-align: center; font-variant-numeric: tabular-nums; }
101
+ & .tabs a.active .count { background: var(--lvc-border); }
102
+
103
+ /* Filter row under the tabs: a full-width select over a search + submit row,
104
+ however many of the three a gem actually has. */
105
+ & .filters { display: flex; flex-wrap: wrap; gap: 8px; margin: 0; padding: 12px; border-bottom: 1px solid var(--lvc-border); }
106
+ & .filters label { flex: 1 1 100%; color: var(--lvc-muted); font-size: 13px; }
107
+ & .filters select { flex: 1 1 100%; }
108
+ /* Basis 0, not auto: the controls below are `width: 100%`, which would
109
+ otherwise push the submit button onto its own row. */
110
+ & .filters input[type="search"] { flex: 1 1 0; min-width: 0; }
111
+ & .filters button,
112
+ & .filters input[type="submit"] { flex: 0 0 auto; }
113
+
114
+ /* Form controls. 16px so iOS does not zoom the page on focus. */
115
+ & input,
116
+ & select,
117
+ & textarea { width: 100%; min-height: 38px; padding: 7px 9px; border: 1px solid var(--lvc-border); border-radius: 8px; background: var(--lvc-surface); color: var(--lvc-text); font: inherit; font-size: 16px; letter-spacing: 0; }
118
+ & textarea { resize: vertical; }
119
+ & input[type="checkbox"],
120
+ & input[type="radio"] { width: auto; min-width: 0; min-height: 0; }
121
+ & input:focus,
122
+ & select:focus,
123
+ & textarea:focus { border-color: var(--lvc-accent); outline: 2px solid color-mix(in srgb, var(--lvc-accent) 22%, transparent); outline-offset: 0; }
124
+ & .field { margin-bottom: 16px; }
125
+ & .field label { display: block; margin-bottom: 5px; font-weight: 600; }
126
+ & .field small { display: block; margin-top: 5px; color: var(--lvc-muted); }
127
+
128
+ /* A submit input is a button, whatever the markup says. */
129
+ & button,
130
+ & .button,
131
+ & input[type="submit"] { display: inline-flex; width: auto; align-items: center; justify-content: center; min-height: 38px; padding: 8px 14px; border: 1px solid var(--lvc-border); border-radius: 8px; background: var(--lvc-surface); color: var(--lvc-text); font: inherit; cursor: pointer; }
132
+ & button:hover,
133
+ & .button:hover,
134
+ & input[type="submit"]:hover { border-color: var(--lvc-muted); text-decoration: none; }
135
+ & button.primary,
136
+ & .button.primary,
137
+ & input[type="submit"].primary { border-color: var(--lvc-accent); background: var(--lvc-accent); color: var(--lvc-accent-text); }
138
+ & button.danger,
139
+ & .button.danger { color: var(--lvc-danger); }
140
+ & button.small,
141
+ & .button.small { min-height: 32px; padding: 4px 10px; font-size: 13px; }
142
+ & .actions,
143
+ & .form-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
144
+ & .actions form { display: inline; }
145
+ & .form-actions { padding-top: 12px; }
146
+
147
+ & .card { overflow: hidden; border: 1px solid var(--lvc-border); border-radius: 12px; background: var(--lvc-surface); }
148
+ & .card.pad { padding: 16px; overflow: visible; }
75
149
  & table { width: 100%; border-collapse: collapse; }
76
150
  & th,
77
151
  & td { padding: 10px 14px; text-align: left; vertical-align: top; }
78
152
  & th { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--lvc-muted); border-bottom: 1px solid var(--lvc-border); }
79
153
  & tr + tr td { border-top: 1px solid var(--lvc-border); }
80
- & tr.unread td { font-weight: 600; }
81
- & .badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 12px; font-weight: 600; color: #fff; white-space: nowrap; }
154
+ & td.row-actions { white-space: nowrap; text-align: right; }
155
+ & td.row-actions form { display: inline-block; }
156
+ & td.row-actions form + form { margin-left: 6px; }
157
+
158
+ & .badge { display: inline-flex; padding: 2px 10px; border-radius: 999px; color: #fff; font-size: 12px; font-weight: 600; white-space: nowrap; }
159
+ & .badge.locale { border: 1px solid var(--lvc-border); background: transparent; color: var(--lvc-muted); }
160
+ & code.key { max-width: 100%; padding: 2px 7px; border-radius: 6px; background: var(--lvc-code); font: 12.5px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; overflow-wrap: anywhere; }
161
+
162
+ & .flash,
163
+ & .errors { margin-bottom: 16px; padding: 10px 12px; border: 1px solid var(--lvc-border); border-radius: 8px; background: var(--lvc-surface); }
164
+ & .flash.notice { border-color: var(--lvc-success); }
165
+ & .flash.alert,
166
+ & .errors { border-color: var(--lvc-danger); }
167
+ & .errors ul { margin-bottom: 0; }
168
+
169
+ & dl { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 6px 16px; margin: 0; }
170
+ & dt { color: var(--lvc-muted); }
171
+ & dd { margin: 0; overflow-wrap: anywhere; }
172
+ & .message { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
173
+
174
+ & .empty { padding: 48px 16px; color: var(--lvc-muted); text-align: center; }
175
+ & .empty-state { max-width: 320px; margin: auto; padding: 24px; text-align: center; }
176
+ & .empty-state h2 { margin: 0 0 6px; font-size: 18px; }
177
+ & .empty-state p { margin: 0; }
178
+
179
+ /* The two-pane dashboard: the queue on the left, the selected record on the
180
+ right, each pane owning its own scroll. */
181
+ & .dashboard-shell { display: grid; flex: 1 1 auto; grid-template-columns: minmax(330px, 430px) 1fr; gap: 16px; min-height: 0; }
182
+ & .dashboard-sidebar { display: flex; min-width: 0; min-height: 0; flex-direction: column; overflow: hidden; border: 1px solid var(--lvc-border); border-radius: 8px; background: var(--lvc-surface); }
183
+ & .dashboard-sidebar .tabs { flex: 0 0 auto; margin: 12px 12px 0; }
184
+ & .dashboard-sidebar .filters { flex: 0 0 auto; }
185
+
186
+ /* One row shape for every gem: a topline of badges and a timestamp, the copy
187
+ that identifies the record, then a muted line of ids and authors. */
188
+ & .record-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
189
+ & .record-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; padding: 13px 14px; border-bottom: 1px solid var(--lvc-border); color: var(--lvc-text); }
190
+ & .record-row:hover { background: var(--lvc-bg); text-decoration: none; }
191
+ & .record-row.active { background: color-mix(in srgb, var(--lvc-accent) 9%, var(--lvc-surface)); box-shadow: inset 3px 0 0 var(--lvc-accent); }
192
+ & .record-main { display: flex; min-width: 0; flex-direction: column; gap: 5px; }
193
+ & .record-topline,
194
+ & .record-meta { display: flex; align-items: center; gap: 8px; min-width: 0; }
195
+ & .record-name,
196
+ & .record-copy,
197
+ & .record-meta span,
198
+ /* A key is `overflow-wrap: anywhere` in prose; in a row it truncates. */
199
+ & .record-topline code.key,
200
+ & .record-meta code.key { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
201
+ & .record-name { font-weight: 600; }
202
+ & .record-copy { display: block; font-weight: 600; }
203
+ & .record-time { margin-left: auto; white-space: nowrap; }
204
+ & .record-side { display: flex; flex-direction: column; align-items: flex-end; justify-content: space-between; gap: 8px; padding-top: 1px; }
205
+ & .pager { display: flex; flex: 0 0 auto; justify-content: space-between; gap: 16px; min-height: 42px; margin: 0; padding: 10px 12px; border-top: 1px solid var(--lvc-border); }
206
+
207
+ /* The selected record. `.detail-scroll` is the only part that scrolls, so the
208
+ panel heading and its actions stay put. */
209
+ & .dashboard-detail { display: flex; min-width: 0; min-height: 0; flex-direction: column; overflow: hidden; }
210
+ & .dashboard-detail .detail-panel { flex: 1 1 auto; min-height: 0; }
211
+ & .dashboard-detail .detail-scroll { flex: 1 1 auto; overflow-y: auto; padding-right: 2px; }
212
+ & .dashboard-detail .actions { margin-bottom: 0; }
213
+ & .detail-panel { display: flex; min-width: 0; flex-direction: column; }
214
+ /* The panel's own heading row: the title on the left, whatever acts on the
215
+ whole record on the right. */
216
+ & .panel-head { display: flex; flex: 0 0 auto; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
217
+ & .panel-head h1 { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 0; }
218
+ & .detail-scroll { display: flex; min-height: 0; flex-direction: column; gap: 12px; }
219
+ & .mobile-back { display: none; }
220
+ }
221
+
222
+ /* Page frame — the gem's own layout only. After the components, so it wins the
223
+ specificity ties against them. */
224
+ .lvc-page { margin: 0; min-height: 100vh; background: var(--lvc-bg); color: var(--lvc-text); font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; letter-spacing: 0; }
225
+ /* An index page is the dashboard: it fills the viewport and never scrolls
226
+ itself, because the queue and the detail pane each scroll on their own. */
227
+ .lvc-index, .lvc-index .container { height: 100vh; height: 100dvh; }
228
+ .lvc-index { overflow: hidden; }
229
+ .lvc-index .container { display: flex; max-width: 1280px; flex-direction: column; padding-bottom: 16px; }
230
+ .lvc-index .nav,
231
+ .lvc-index .page-head { flex: 0 0 auto; }
232
+ /* Standalone show pages keep normal page scrolling. */
233
+ .lvc-show { min-height: 100vh; overflow: auto; }
234
+ .lvc-show .detail-panel { width: 100%; }
235
+ .lvc-show .detail-scroll { overflow: visible; }
236
+
237
+ /* Narrow screens: one pane at a time, the queue until a record is picked.
238
+ Last in the file, so these overrides actually reach the rules above. */
239
+ @media (max-width: 760px) {
240
+ .lvc-page { font-size: 14px; }
241
+ .lvc-index, .lvc-show { overflow-x: hidden; }
242
+ .lvc-index { overflow-y: auto; }
243
+ .lvc-index, .lvc-index .container { min-height: 100vh; height: auto; }
244
+ .lvc-index .container { padding-bottom: 10px; }
245
+ .lvc-dashboard {
246
+ & .container { padding: 14px 10px 24px; }
247
+ & .page-head { margin-bottom: 12px; }
248
+ & .detail-panel,
249
+ & .detail-scroll,
250
+ & .card,
251
+ & dl,
252
+ & dd { min-width: 0; max-width: 100%; }
253
+ & .dashboard-detail .card { width: 100%; }
254
+ & .message,
255
+ & .lede,
256
+ & dd,
257
+ & .muted { overflow-wrap: anywhere; }
258
+ & .dashboard-shell { display: block; min-height: calc(100vh - 62px); }
259
+ & .dashboard-sidebar,
260
+ & .dashboard-detail { min-height: calc(100vh - 62px); }
261
+ & .dashboard-shell.has-selected .dashboard-sidebar { display: none; }
262
+ & .dashboard-shell:not(.has-selected) .dashboard-detail { display: none; }
263
+ & .record-list { overflow: visible; }
264
+ & .record-row { padding: 12px; }
265
+ & .record-meta { display: none; }
266
+ & .dashboard-detail .detail-panel { min-height: calc(100vh - 64px); }
267
+ & .mobile-back { display: block; flex: 0 0 auto; margin: 4px 0 10px; font-size: 13px; }
268
+ & .panel-head h1 { font-size: 18px; }
269
+ & dl { grid-template-columns: 1fr; gap: 3px; }
270
+ & dd { margin-bottom: 8px; }
271
+ }
272
+ }
273
+
274
+ /* ---------------------------------------------------------------------------
275
+ * GEM-SPECIFIC — livechat only: the open/resolved toggle, agent avatars, the
276
+ * message thread, and the composer.
277
+ * ------------------------------------------------------------------------- */
278
+
279
+ :root {
280
+ --lvc-open: var(--lvc-success);
281
+ --lvc-resolved: var(--lvc-neutral);
282
+ }
283
+
284
+ .lvc-dashboard {
82
285
  & .badge.status-open { background: var(--lvc-open); }
83
286
  & .badge.status-resolved { background: var(--lvc-resolved); }
84
287
  & .badge.unread-count { background: var(--lvc-danger); }
85
- & .muted { color: var(--lvc-muted); font-size: 13px; }
86
- & .pager { margin-top: 16px; display: flex; gap: 16px; }
87
- & .empty { padding: 48px 16px; text-align: center; color: var(--lvc-muted); }
88
- & .breadcrumb { margin-bottom: 12px; font-size: 13px; }
89
- & .flash { margin: 0 0 16px; padding: 10px 14px; border-radius: 10px; border: 1px solid var(--lvc-border); background: var(--lvc-surface); }
90
- & .flash.alert { color: var(--lvc-danger); border-color: var(--lvc-danger); }
91
- & .head-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
92
- & .head-row h1 { margin: 0; flex: 1; }
93
- & .head-row form { display: inline; }
94
- & .status-toggle-form { flex: 0 0 auto; }
288
+ & tr.unread td { font-weight: 600; }
289
+ & .record-row.unread .record-name,
290
+ & .record-row.unread .record-copy { font-weight: 700; }
291
+ & .record-side .avatars { min-height: 26px; }
292
+
293
+ /* The panel heading: the name with the visitor's context directly under it,
294
+ and the switch pushed to the far right of the row whatever the name's
295
+ length. `margin-left: auto` rather than the row's `space-between`, so a
296
+ third element in the row could never pull the switch back in. */
297
+ & .panel-titles { display: flex; min-width: 0; flex-direction: column; gap: 2px; }
298
+ & .context-line { margin: 0; overflow-wrap: anywhere; }
299
+
300
+ /* One switch reading open|resolved, sliding to the side it is on. */
301
+ & .status-toggle-form { flex: 0 0 auto; margin-left: auto; }
95
302
  & .status-toggle { position: relative; display: grid; grid-template-columns: 1fr 1fr; gap: 0; width: 176px; min-height: 36px; padding: 3px; border-radius: 999px; border: 1px solid var(--lvc-border); background: var(--lvc-bg); color: var(--lvc-muted); overflow: hidden; }
96
303
  & .status-toggle::before { content: ""; position: absolute; z-index: 0; top: 3px; bottom: 3px; width: calc(50% - 3px); border-radius: 999px; background: var(--lvc-surface); box-shadow: 0 1px 2px rgba(15, 23, 42, .12); transition: left .16s ease; }
97
304
  & .status-toggle.is-open::before { left: 3px; }
@@ -100,10 +307,7 @@
100
307
  & .status-toggle.is-open span:first-child { color: var(--lvc-open); }
101
308
  & .status-toggle.is-resolved span:last-child { color: var(--lvc-text); }
102
309
  & .status-toggle:hover { text-decoration: none; color: var(--lvc-text); }
103
- & button,
104
- & .button { padding: 8px 14px; border: 1px solid var(--lvc-border); border-radius: 8px; cursor: pointer; background: var(--lvc-surface); color: var(--lvc-text); font: inherit; }
105
- & button.primary { background: var(--lvc-accent); border-color: var(--lvc-accent); color: var(--lvc-accent-text); }
106
- & .case-id { color: var(--lvc-muted); font-weight: 400; font-size: 15px; }
310
+
107
311
  & .avatars { display: inline-flex; }
108
312
  & .avatars .avatar { width: 26px; height: 26px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 700; color: #fff; border: 2px solid var(--lvc-surface); cursor: default; }
109
313
  & .avatars .avatar + .avatar { margin-left: -8px; }
@@ -113,7 +317,8 @@
113
317
  & .avatar.color-3 { background: #dc2626; }
114
318
  & .avatar.color-4 { background: #7c3aed; }
115
319
  & .avatar.color-5 { background: #0891b2; }
116
- & .context-line { margin: -6px 0 16px; overflow-wrap: anywhere; }
320
+
321
+ /* The thread: the visitor on the left, agents on the right. */
117
322
  & .thread { display: flex; flex-direction: column; gap: 4px; padding: 16px; }
118
323
  & .thread .who { font-size: 12px; color: var(--lvc-muted); margin: 10px 4px 2px; }
119
324
  & .thread .who.visitor { text-align: left; }
@@ -126,94 +331,64 @@
126
331
  & .thread .msg .atts { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; }
127
332
  & .thread .msg .att-img img { max-width: 100%; max-height: 240px; border-radius: 8px; display: block; }
128
333
  & .thread .msg .att-audio { display: block; width: 280px; max-width: 100%; height: 36px; }
129
- & .thread .msg .att-file { display: inline-block; padding: 6px 10px; border-radius: 8px; background: rgba(0,0,0,.06); color: inherit; font-size: 13px; overflow-wrap: anywhere; }
130
- & .thread .msg.agent .att-file { background: rgba(255,255,255,.2); }
334
+ & .thread .msg .att-file { display: inline-block; padding: 6px 10px; border-radius: 8px; background: rgba(0, 0, 0, .06); color: inherit; font-size: 13px; overflow-wrap: anywhere; }
335
+ & .thread .msg.agent .att-file { background: rgba(255, 255, 255, .2); }
131
336
  & #new-messages { margin: 12px 0 0; }
132
337
  & #new-messages a { font-weight: 600; }
133
- /* Unified composer box (matches the visitor widget): the reply on top, a toolbar row below (tools left, send right), all inside one bordered box that lights up on focus. */
338
+
339
+ /* Unified composer box (matches the visitor widget): the reply on top, a
340
+ toolbar row below (tools left, send right), all inside one bordered box
341
+ that lights up on focus. */
134
342
  & .reply { display: flex; flex-direction: column; gap: 6px; margin: 10px 12px; padding: 8px 10px; border: 1px solid var(--lvc-border); border-radius: 14px; background: var(--lvc-bg); }
135
343
  & .reply:focus-within { border-color: var(--lvc-accent); }
136
- & .reply textarea { -webkit-appearance: none; appearance: none; border: none; background: none; resize: none; outline: none; font: inherit; color: var(--lvc-text); padding: 4px 4px 0; min-height: 40px; max-height: 160px; overflow-y: auto; }
137
- & .reply input[type=file].reply-file { font-size: 12px; color: var(--lvc-muted); }
344
+ & .reply textarea { -webkit-appearance: none; appearance: none; min-height: 40px; max-height: 160px; padding: 4px 4px 0; border: none; background: none; resize: none; outline: none; font: inherit; color: var(--lvc-text); overflow-y: auto; }
345
+ & .reply input[type="file"].reply-file { min-height: 0; padding: 0; border: 0; background: none; font-size: 12px; color: var(--lvc-muted); }
138
346
  & .reply-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
139
347
  & .reply-tools { display: flex; align-items: center; gap: 2px; }
140
- & .reply .attach { width: 32px; height: 32px; padding: 0; border: none; background: none; color: var(--lvc-muted); border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; }
348
+ & .reply .attach { width: 32px; min-height: 32px; height: 32px; padding: 0; border: none; background: none; color: var(--lvc-muted); border-radius: 8px; cursor: pointer; display: flex; align-items: center; justify-content: center; }
141
349
  & .reply .attach:hover { background: var(--lvc-border); color: var(--lvc-text); }
142
350
  & .reply .attach.recording-on { color: var(--lvc-danger); }
143
351
  & .reply.is-recording .attach { display: none; }
144
352
  & .reply .chips { display: flex; flex-wrap: wrap; gap: 6px; }
145
353
  & .reply .chip { display: inline-flex; align-items: center; gap: 6px; max-width: 100%; padding: 4px 6px 4px 10px; border: 1px solid var(--lvc-border); border-radius: 999px; background: var(--lvc-surface); font-size: 12px; }
146
354
  & .reply .chip-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 220px; }
147
- & .reply .chip-x { padding: 0 2px; border: none; background: none; color: var(--lvc-muted); font-size: 16px; line-height: 1; cursor: pointer; }
355
+ & .reply .chip-x { min-height: 0; padding: 0 2px; border: none; background: none; color: var(--lvc-muted); font-size: 16px; line-height: 1; cursor: pointer; }
148
356
  & .reply .chip-x:hover { color: var(--lvc-text); }
149
357
  & .reply .recording { display: inline-flex; align-items: center; gap: 8px; height: 32px; padding: 0 4px 0 8px; border: 1px solid var(--lvc-border); border-radius: 999px; background: var(--lvc-surface); color: var(--lvc-muted); font-size: 12px; white-space: nowrap; }
150
358
  & .reply .recording[hidden] { display: none; }
151
359
  & .reply .recording-status { display: inline-grid; grid-template-columns: 8px 24px 34px; align-items: center; gap: 6px; font-variant-numeric: tabular-nums; }
152
- & .reply .recording-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--lvc-danger); box-shadow: 0 0 0 3px rgba(220,38,38,.12); }
360
+ & .reply .recording-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--lvc-danger); box-shadow: 0 0 0 3px rgba(220, 38, 38, .12); }
153
361
  & .reply .recording-rec { font-size: 10px; font-weight: 700; letter-spacing: .04em; color: var(--lvc-danger); }
154
362
  & .reply .recording-actions { display: flex; align-items: center; gap: 4px; }
155
- & .reply .recording-actions button { width: 24px; height: 24px; padding: 0; border: none; border-radius: 50%; background: none; color: var(--lvc-muted); display: flex; align-items: center; justify-content: center; }
363
+ & .reply .recording-actions button { width: 24px; height: 24px; min-height: 24px; padding: 0; border: none; border-radius: 50%; background: none; color: var(--lvc-muted); display: flex; align-items: center; justify-content: center; }
156
364
  & .reply .recording-actions button:hover { background: var(--lvc-border); color: var(--lvc-text); }
157
365
  & .reply .recording-actions .stop-recording { background: var(--lvc-danger); color: #fff; }
158
366
  & .reply .recording-actions .stop-recording:hover { background: #b91c1c; color: #fff; }
159
- & .reply .send { width: 32px; min-width: 32px; height: 32px; padding: 0; border: none; border-radius: 50%; background: var(--lvc-accent); color: var(--lvc-accent-text); cursor: pointer; display: flex; align-items: center; justify-content: center; }
160
- & .inbox-shell { flex: 1 1 auto; min-height: 0; display: grid; grid-template-columns: minmax(320px, 380px) 1fr; gap: 16px; }
161
- & .inbox-sidebar { min-width: 0; min-height: 0; display: flex; flex-direction: column; background: var(--lvc-surface); border: 1px solid var(--lvc-border); border-radius: 8px; overflow: hidden; }
162
- & .inbox-sidebar .tabs { flex: 0 0 auto; margin: 12px 12px 0; padding: 0; background: transparent; border: 0; border-radius: 0; gap: 6px; flex-wrap: nowrap; }
163
- & .inbox-sidebar .tabs a { flex: 1 1 0; position: relative; display: flex; align-items: center; justify-content: center; gap: 6px; min-height: 34px; padding: 5px 12px 8px; border-radius: 8px; color: var(--lvc-muted); font-weight: 600; }
164
- & .inbox-sidebar .tabs a.active { color: var(--lvc-text); border: 0; background: transparent; box-shadow: none; margin: 0; }
165
- & .inbox-sidebar .tabs a.active::after { content: ""; position: absolute; left: 24px; right: 24px; bottom: 0; height: 2px; border-radius: 999px; background: var(--lvc-accent); }
166
- & .inbox-sidebar .tabs a:not(.active):hover { text-decoration: none; color: var(--lvc-text); background: var(--lvc-bg); }
167
- & .inbox-sidebar .tabs .count { margin-left: 0; background: color-mix(in srgb, var(--lvc-muted) 14%, transparent); }
168
- & .inbox-sidebar .tabs a.active .count { background: var(--lvc-border); }
169
- & .inbox-sidebar .filters { flex: 0 0 auto; margin: 0; padding: 12px; border-bottom: 1px solid var(--lvc-border); }
170
- & .inbox-sidebar .filters input[type=search] { max-width: none; }
171
- & .conversation-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
172
- & .conversation-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; padding: 13px 14px; color: var(--lvc-text); border-bottom: 1px solid var(--lvc-border); }
173
- & .conversation-row:hover { text-decoration: none; background: var(--lvc-bg); }
174
- & .conversation-row.active { background: color-mix(in srgb, var(--lvc-accent) 9%, var(--lvc-surface)); box-shadow: inset 3px 0 0 var(--lvc-accent); }
175
- & .conversation-row.unread .conversation-name,
176
- & .conversation-row.unread .conversation-preview { font-weight: 700; }
177
- & .conversation-main { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
178
- & .conversation-topline,
179
- & .conversation-meta { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
180
- & .conversation-name,
181
- & .conversation-preview,
182
- & .conversation-meta span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
183
- & .conversation-name { font-weight: 600; }
184
- & .conversation-time { margin-left: auto; white-space: nowrap; }
185
- & .conversation-preview { display: block; }
186
- & .conversation-side { display: flex; flex-direction: column; align-items: flex-end; justify-content: space-between; gap: 8px; padding-top: 1px; }
187
- & .conversation-side .avatars { min-height: 26px; }
188
- & .inbox-sidebar .pager { flex: 0 0 auto; margin: 0; padding: 10px 12px; border-top: 1px solid var(--lvc-border); }
189
- & .inbox-thread { min-width: 0; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
190
- & .inbox-thread .conversation-panel { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; padding: 0; }
191
- & .conversation-panel .head-row,
192
- & .conversation-panel .context-line { flex: 0 0 auto; }
193
- & .conversation-panel .card { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; border-radius: 8px; }
194
- & .conversation-panel .thread { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
195
- & .conversation-panel .reply { flex: 0 0 auto; }
196
- & .empty-state { margin: auto; max-width: 320px; padding: 24px; text-align: center; }
197
- & .empty-state h2 { margin: 0 0 6px; font-size: 18px; }
198
- & .empty-state p { margin: 0; }
199
- & .mobile-back { display: none; }
367
+ & .reply .send { width: 32px; min-width: 32px; height: 32px; min-height: 32px; padding: 0; border: none; border-radius: 50%; background: var(--lvc-accent); color: var(--lvc-accent-text); cursor: pointer; display: flex; align-items: center; justify-content: center; }
368
+
369
+ /* Inside the thread pane only the messages scroll, so a reply never jumps the
370
+ page and the composer stays put. */
371
+ & .detail-panel .panel-head { flex: 0 0 auto; }
372
+ & .detail-panel .card { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; border-radius: 8px; }
373
+ & .detail-panel .thread { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
374
+ & .detail-panel .reply { flex: 0 0 auto; }
200
375
  }
201
376
 
202
- /* Page frame the gem's own layout only. Last, so it wins the
203
- specificity ties against the component layer above. */
204
- .lvc-page { margin: 0; background: var(--lvc-bg); color: var(--lvc-text); font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
205
- /* Desktop inbox: a durable conversation list on the left and the active thread on the right, with each pane owning its own scroll. */
206
- .lvc-inbox, .lvc-inbox .container { height: 100vh; height: 100dvh; }
207
- .lvc-inbox { overflow: hidden; }
208
- .lvc-inbox .container { max-width: 1280px; display: flex; flex-direction: column; padding-bottom: 16px; }
209
- .lvc-inbox .container h1 { flex: 0 0 auto; margin-bottom: 14px; }
210
- /* Conversation page: fill the viewport and scroll the THREAD, not the page — so a reply never jumps the page, and the composer stays put. */
377
+ /* The conversation page: same thread, its own page. Fills the viewport and
378
+ scrolls the thread rather than the page. */
211
379
  .lvc-convo, .lvc-convo .container { height: 100vh; height: 100dvh; }
212
380
  .lvc-convo { overflow: hidden; }
213
381
  .lvc-convo .container { display: flex; flex-direction: column; padding-bottom: 16px; }
214
- .lvc-convo .breadcrumb, .lvc-convo .head-row, .lvc-convo .context-line,
215
- .lvc-convo .flash { flex: 0 0 auto; }
216
- .lvc-convo .conversation-panel { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
217
- .lvc-convo .card { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
218
- .lvc-convo .thread { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
219
- .lvc-convo .reply { flex: 0 0 auto; }
382
+ .lvc-convo .breadcrumb,
383
+ .lvc-convo .panel-head,
384
+ .lvc-convo .flash { flex: 0 0 auto; }
385
+ .lvc-convo .detail-panel { flex: 1 1 auto; min-height: 0; }
386
+
387
+ @media (max-width: 760px) {
388
+ .lvc-dashboard {
389
+ & .detail-panel .panel-head h1 { font-size: 18px; }
390
+ & .status-toggle { width: 152px; min-height: 34px; }
391
+ & .status-toggle span { padding: 0 7px; font-size: 12px; }
392
+ & .thread .msg { max-width: 88%; }
393
+ }
394
+ }
@@ -295,7 +295,7 @@
295
295
  // (new message, new thread, resolve/reopen), just reload — unless the
296
296
  // agent is mid-search, whose typing must never be eaten.
297
297
  function watchIndex() {
298
- var index = document.getElementById("lvc-index");
298
+ var index = document.getElementById("lvc-poll");
299
299
  if (!index) return;
300
300
 
301
301
  var token = index.getAttribute("data-token");
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Livechat
4
- VERSION = '0.8.2'
4
+ VERSION = '0.9.1'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: livechat
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.2
4
+ version: 0.9.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yaroslav Shmarov