livechat 0.7.1 → 0.8.0

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.
@@ -1,233 +1,226 @@
1
- :root {
1
+ /* Dashboard styles.
2
+ *
3
+ * Three layers, and the split is what lets a host replace the layout:
4
+ *
5
+ * :root custom properties, all `--lvc-` prefixed so they can
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
8
+ * layout puts this class on <body>, so inside a host admin
9
+ * these rules simply do not apply.
10
+ * .lvc-dashboard everything else, nested. Names like .container, .card
11
+ * and .tabs are confined to the dashboard's own markup, so
12
+ * loading this file inside a host cannot restyle its chrome.
13
+ *
14
+ * Nothing here styles a bare `body`, `a`, `table` or `*` at the top level.
15
+ */
16
+
17
+
18
+ :root {
2
19
  color-scheme: light dark;
3
- --bg: #f6f7f9; --surface: #fff; --text: #1c2024; --muted: #6b7280;
4
- --border: #e5e7eb; --accent: #2563eb; --accent-text: #fff;
5
- --open: #16a34a; --resolved: #6b7280; --danger: #dc2626;
6
- }
7
- @media (prefers-color-scheme: dark) {
8
- :root {
9
- --bg: #111418; --surface: #1a1f26; --text: #e6e8ea; --muted: #9aa2ab;
10
- --border: #2a313a; --accent: #3b82f6;
11
- }
12
- }
13
- * { box-sizing: border-box; }
14
- body { margin: 0; background: var(--bg); color: var(--text);
15
- font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
16
- a { color: var(--accent); text-decoration: none; }
17
- a:hover { text-decoration: underline; }
18
- .container { max-width: 860px; margin: 0 auto; padding: 24px 16px 64px; }
19
- h1 { font-size: 22px; margin: 0 0 16px; }
20
- .tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--border); margin-bottom: 16px; flex-wrap: wrap; }
21
- .tabs a { padding: 8px 14px; border-radius: 8px 8px 0 0; color: var(--muted); }
22
- .tabs a.active { color: var(--text); font-weight: 600; border: 1px solid var(--border);
23
- border-bottom: 2px solid var(--surface); background: var(--surface); margin-bottom: -1px; }
24
- .tabs a:hover { text-decoration: none; color: var(--text); }
25
- .count { display: inline-block; min-width: 20px; padding: 0 6px; margin-left: 4px; border-radius: 999px;
26
- background: var(--border); font-size: 12px; text-align: center; }
27
- .filters { margin-bottom: 16px; display: flex; gap: 8px; }
28
- .filters input[type=search] { flex: 1; max-width: 320px; padding: 6px 10px; border: 1px solid var(--border);
29
- border-radius: 8px; background: var(--surface); color: var(--text); font: inherit; }
30
- .card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
31
- .card.pad { padding: 16px; }
32
- table { width: 100%; border-collapse: collapse; }
33
- th, td { padding: 10px 14px; text-align: left; vertical-align: top; }
34
- th { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--muted);
35
- border-bottom: 1px solid var(--border); }
36
- tr + tr td { border-top: 1px solid var(--border); }
37
- tr.unread td { font-weight: 600; }
38
- .badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 12px; font-weight: 600;
39
- color: #fff; white-space: nowrap; }
40
- .badge.status-open { background: var(--open); }
41
- .badge.status-resolved { background: var(--resolved); }
42
- .badge.unread-count { background: var(--danger); }
43
- .muted { color: var(--muted); font-size: 13px; }
44
- .pager { margin-top: 16px; display: flex; gap: 16px; }
45
- .empty { padding: 48px 16px; text-align: center; color: var(--muted); }
46
- .breadcrumb { margin-bottom: 12px; font-size: 13px; }
47
- .flash { margin: 0 0 16px; padding: 10px 14px; border-radius: 10px; border: 1px solid var(--border);
48
- background: var(--surface); }
49
- .flash.alert { color: var(--danger); border-color: var(--danger); }
50
- .head-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
51
- .head-row h1 { margin: 0; flex: 1; }
52
- .head-row form { display: inline; }
53
- .status-toggle-form { flex: 0 0 auto; }
54
- .status-toggle { position: relative; display: grid; grid-template-columns: 1fr 1fr; gap: 0; width: 176px;
55
- min-height: 36px; padding: 3px; border-radius: 999px; border: 1px solid var(--border);
56
- background: var(--bg); color: var(--muted); overflow: hidden; }
57
- .status-toggle::before { content: ""; position: absolute; z-index: 0; top: 3px; bottom: 3px;
58
- width: calc(50% - 3px); border-radius: 999px; background: var(--surface);
59
- box-shadow: 0 1px 2px rgba(15, 23, 42, .12); transition: left .16s ease; }
60
- .status-toggle.is-open::before { left: 3px; }
61
- .status-toggle.is-resolved::before { left: 50%; }
62
- .status-toggle span { position: relative; z-index: 1; display: flex; align-items: center; justify-content: center;
63
- min-width: 0; padding: 0 10px; font-size: 13px; font-weight: 700; white-space: nowrap; }
64
- .status-toggle.is-open span:first-child { color: var(--open); }
65
- .status-toggle.is-resolved span:last-child { color: var(--text); }
66
- .status-toggle:hover { text-decoration: none; color: var(--text); }
67
- button, .button { padding: 8px 14px; border: 1px solid var(--border); border-radius: 8px; cursor: pointer;
68
- background: var(--surface); color: var(--text); font: inherit; }
69
- button.primary { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
70
- .case-id { color: var(--muted); font-weight: 400; font-size: 15px; }
71
- .avatars { display: inline-flex; }
72
- .avatars .avatar { width: 26px; height: 26px; border-radius: 50%; display: inline-flex;
73
- align-items: center; justify-content: center; font-size: 10px; font-weight: 700;
74
- color: #fff; border: 2px solid var(--surface); cursor: default; }
75
- .avatars .avatar + .avatar { margin-left: -8px; }
76
- .avatar.color-0 { background: #2563eb; } .avatar.color-1 { background: #16a34a; }
77
- .avatar.color-2 { background: #d97706; } .avatar.color-3 { background: #dc2626; }
78
- .avatar.color-4 { background: #7c3aed; } .avatar.color-5 { background: #0891b2; }
79
- .context-line { margin: -6px 0 16px; overflow-wrap: anywhere; }
80
- .thread { display: flex; flex-direction: column; gap: 4px; padding: 16px; }
81
- .thread .who { font-size: 12px; color: var(--muted); margin: 10px 4px 2px; }
82
- .thread .who.visitor { text-align: left; }
83
- .thread .who.agent { text-align: right; }
84
- .thread .msg { max-width: 78%; padding: 8px 12px; border-radius: 14px; white-space: pre-wrap;
85
- overflow-wrap: anywhere; }
86
- .thread .msg.visitor { align-self: flex-start; background: var(--bg); border: 1px solid var(--border);
87
- border-bottom-left-radius: 4px; }
88
- .thread .msg.agent { align-self: flex-end; background: var(--accent); color: var(--accent-text);
89
- border-bottom-right-radius: 4px; }
90
- .thread .system { align-self: center; color: var(--muted); font-size: 12px; margin: 8px 0; }
91
- .thread .typing { align-self: flex-start; color: var(--muted); font-size: 12px;
92
- font-style: italic; margin: 8px 4px 2px; }
93
- .thread .msg .atts { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; }
94
- .thread .msg .att-img img { max-width: 100%; max-height: 240px; border-radius: 8px; display: block; }
95
- .thread .msg .att-audio { display: block; width: 280px; max-width: 100%; height: 36px; }
96
- .thread .msg .att-file { display: inline-block; padding: 6px 10px; border-radius: 8px;
97
- background: rgba(0,0,0,.06); color: inherit; font-size: 13px;
98
- overflow-wrap: anywhere; }
99
- .thread .msg.agent .att-file { background: rgba(255,255,255,.2); }
100
- #new-messages { margin: 12px 0 0; }
101
- #new-messages a { font-weight: 600; }
102
- /* Unified composer box (matches the visitor widget): the reply on top, a
103
- toolbar row below (tools left, send right), all inside one bordered box
104
- that lights up on focus. */
105
- .reply { display: flex; flex-direction: column; gap: 6px; margin: 10px 12px; padding: 8px 10px;
106
- border: 1px solid var(--border); border-radius: 14px; background: var(--bg); }
107
- .reply:focus-within { border-color: var(--accent); }
108
- .reply textarea { -webkit-appearance: none; appearance: none; border: none; background: none;
109
- resize: none; outline: none; font: inherit; color: var(--text);
110
- padding: 4px 4px 0; min-height: 40px; max-height: 160px; overflow-y: auto; }
111
- .reply input[type=file].reply-file { font-size: 12px; color: var(--muted); }
112
- .reply-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
113
- .reply-tools { display: flex; align-items: center; gap: 2px; }
114
- .reply .attach { width: 32px; height: 32px; padding: 0; border: none; background: none;
115
- color: var(--muted); border-radius: 8px; cursor: pointer;
116
- display: flex; align-items: center; justify-content: center; }
117
- .reply .attach:hover { background: var(--border); color: var(--text); }
118
- .reply .attach.recording-on { color: var(--danger); }
119
- .reply.is-recording .attach { display: none; }
120
- .reply .chips { display: flex; flex-wrap: wrap; gap: 6px; }
121
- .reply .chip { display: inline-flex; align-items: center; gap: 6px; max-width: 100%;
122
- padding: 4px 6px 4px 10px; border: 1px solid var(--border); border-radius: 999px;
123
- background: var(--surface); font-size: 12px; }
124
- .reply .chip-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 220px; }
125
- .reply .chip-x { padding: 0 2px; border: none; background: none; color: var(--muted);
126
- font-size: 16px; line-height: 1; cursor: pointer; }
127
- .reply .chip-x:hover { color: var(--text); }
128
- .reply .recording { display: inline-flex; align-items: center; gap: 8px; height: 32px;
129
- padding: 0 4px 0 8px; border: 1px solid var(--border); border-radius: 999px;
130
- background: var(--surface); color: var(--muted); font-size: 12px;
131
- white-space: nowrap; }
132
- .reply .recording[hidden] { display: none; }
133
- .reply .recording-status { display: inline-grid; grid-template-columns: 8px 24px 34px;
134
- align-items: center; gap: 6px; font-variant-numeric: tabular-nums; }
135
- .reply .recording-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--danger);
136
- box-shadow: 0 0 0 3px rgba(220,38,38,.12); }
137
- .reply .recording-rec { font-size: 10px; font-weight: 700; letter-spacing: .04em; color: var(--danger); }
138
- .reply .recording-actions { display: flex; align-items: center; gap: 4px; }
139
- .reply .recording-actions button { width: 24px; height: 24px; padding: 0; border: none;
140
- border-radius: 50%; background: none; color: var(--muted);
141
- display: flex; align-items: center; justify-content: center; }
142
- .reply .recording-actions button:hover { background: var(--border); color: var(--text); }
143
- .reply .recording-actions .stop-recording { background: var(--danger); color: #fff; }
144
- .reply .recording-actions .stop-recording:hover { background: #b91c1c; color: #fff; }
145
- .reply .send { width: 32px; min-width: 32px; height: 32px; padding: 0; border: none; border-radius: 50%;
146
- background: var(--accent); color: var(--accent-text); cursor: pointer;
147
- display: flex; align-items: center; justify-content: center; }
148
- /* Desktop inbox: a durable conversation list on the left and the active
149
- thread on the right, with each pane owning its own scroll. */
150
- .lvc-inbox, .lvc-inbox .container { height: 100vh; height: 100dvh; }
151
- .lvc-inbox { overflow: hidden; }
152
- .lvc-inbox .container { max-width: 1280px; display: flex; flex-direction: column; padding-bottom: 16px; }
153
- .lvc-inbox .container > h1 { flex: 0 0 auto; margin-bottom: 14px; }
154
- .inbox-shell { flex: 1 1 auto; min-height: 0; display: grid; grid-template-columns: minmax(320px, 380px) 1fr;
155
- gap: 16px; }
156
- .inbox-sidebar { min-width: 0; min-height: 0; display: flex; flex-direction: column;
157
- background: var(--surface); border: 1px solid var(--border);
158
- border-radius: 8px; overflow: hidden; }
159
- .inbox-sidebar .tabs { flex: 0 0 auto; margin: 12px 12px 0; padding: 0; background: transparent;
160
- border: 0; border-radius: 0; gap: 6px; flex-wrap: nowrap; }
161
- .inbox-sidebar .tabs a { flex: 1 1 0; position: relative; display: flex; align-items: center;
162
- justify-content: center; gap: 6px; min-height: 34px; padding: 5px 12px 8px;
163
- border-radius: 8px; color: var(--muted); font-weight: 600; }
164
- .inbox-sidebar .tabs a.active { color: var(--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;
166
- height: 2px; border-radius: 999px; background: var(--accent); }
167
- .inbox-sidebar .tabs a:not(.active):hover { text-decoration: none; color: var(--text); background: var(--bg); }
168
- .inbox-sidebar .tabs .count { margin-left: 0; background: color-mix(in srgb, var(--muted) 14%, transparent); }
169
- .inbox-sidebar .tabs a.active .count { background: var(--border); }
170
- .inbox-sidebar .filters { flex: 0 0 auto; margin: 0; padding: 12px; border-bottom: 1px solid var(--border); }
171
- .inbox-sidebar .filters input[type=search] { max-width: none; }
172
- .conversation-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
173
- .conversation-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; padding: 13px 14px;
174
- color: var(--text); border-bottom: 1px solid var(--border); }
175
- .conversation-row:hover { text-decoration: none; background: var(--bg); }
176
- .conversation-row.active { background: color-mix(in srgb, var(--accent) 9%, var(--surface));
177
- box-shadow: inset 3px 0 0 var(--accent); }
178
- .conversation-row.unread .conversation-name, .conversation-row.unread .conversation-preview { font-weight: 700; }
179
- .conversation-main { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
180
- .conversation-topline, .conversation-meta { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
181
- .conversation-name, .conversation-preview, .conversation-meta span { overflow: hidden; text-overflow: ellipsis;
182
- 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;
187
- gap: 8px; padding-top: 1px; }
188
- .conversation-side .avatars { min-height: 26px; }
189
- .inbox-sidebar .pager { flex: 0 0 auto; margin: 0; padding: 10px 12px; border-top: 1px solid var(--border); }
190
- .inbox-thread { min-width: 0; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
191
- .inbox-thread .conversation-panel { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column;
192
- padding: 0; }
193
- .conversation-panel .head-row, .conversation-panel .context-line { flex: 0 0 auto; }
194
- .conversation-panel .card { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column;
195
- border-radius: 8px; }
196
- .conversation-panel .thread { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
197
- .conversation-panel .reply { flex: 0 0 auto; }
198
- .empty-state { margin: auto; max-width: 320px; padding: 24px; text-align: center; }
199
- .empty-state h2 { margin: 0 0 6px; font-size: 18px; }
200
- .empty-state p { margin: 0; }
201
- .mobile-back { display: none; }
202
- /* Conversation page: fill the viewport and scroll the THREAD, not the
203
- page — so a reply never jumps the page, and the composer stays put. */
204
- .lvc-convo, .lvc-convo .container { height: 100vh; height: 100dvh; }
205
- .lvc-convo { overflow: hidden; }
206
- .lvc-convo .container { display: flex; flex-direction: column; padding-bottom: 16px; }
207
- .lvc-convo .breadcrumb, .lvc-convo .head-row, .lvc-convo .context-line,
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;
23
+ }
24
+ @media (prefers-color-scheme: dark) {
25
+ :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%; }
52
+ }
53
+ }
54
+
55
+ /* Page frame the gem's own layout only. */
56
+ .lvc-page { margin: 0; background: var(--lvc-bg); color: var(--lvc-text); font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
57
+ .lvc-inbox { overflow: hidden; }
58
+ .lvc-inbox .container { max-width: 1280px; display: flex; flex-direction: column; padding-bottom: 16px; }
59
+ .lvc-inbox .container > h1 { flex: 0 0 auto; margin-bottom: 14px; }
60
+ .lvc-convo { overflow: hidden; }
61
+ .lvc-convo .container { display: flex; flex-direction: column; padding-bottom: 16px; }
62
+ .lvc-convo .breadcrumb, .lvc-convo .head-row, .lvc-convo .context-line,
208
63
  .lvc-convo .flash { flex: 0 0 auto; }
209
- .lvc-convo .conversation-panel { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
210
- .lvc-convo .card { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
211
- .lvc-convo .thread { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
212
- .lvc-convo .reply { flex: 0 0 auto; }
213
- @media (max-width: 760px) {
214
- body { font-size: 14px; }
215
- .container { padding: 14px 10px 24px; }
216
- .lvc-inbox { overflow: auto; }
217
- .lvc-inbox, .lvc-inbox .container { min-height: 100vh; height: auto; }
218
- .lvc-inbox .container { padding-bottom: 10px; }
219
- .inbox-shell { display: block; min-height: calc(100vh - 62px); }
220
- .inbox-sidebar, .inbox-thread { min-height: calc(100vh - 62px); }
221
- .inbox-shell.has-selected .inbox-sidebar { display: none; }
222
- .inbox-shell:not(.has-selected) .inbox-thread { display: none; }
223
- .conversation-list { overflow: visible; }
224
- .conversation-row { padding: 12px; }
225
- .conversation-meta { display: none; }
226
- .conversation-panel { min-height: calc(100vh - 64px); }
227
- .inbox-thread .conversation-panel { padding: 0; }
228
- .mobile-back { display: block; flex: 0 0 auto; margin: 10px 10px 0; font-size: 13px; }
229
- .conversation-panel .head-row h1 { font-size: 18px; }
230
- .status-toggle { width: 152px; min-height: 34px; }
231
- .status-toggle span { padding: 0 7px; font-size: 12px; }
232
- .thread .msg { max-width: 88%; }
233
- }
64
+ .lvc-convo .conversation-panel { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
65
+ .lvc-convo .card { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; }
66
+ .lvc-convo .thread { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
67
+ .lvc-convo .reply { flex: 0 0 auto; }
68
+
69
+ /* Dashboard components any layout. */
70
+ .lvc-dashboard {
71
+ & * { box-sizing: border-box; }
72
+ & a { color: var(--lvc-accent); text-decoration: none; }
73
+ & a:hover { text-decoration: underline; }
74
+ & .container { max-width: 860px; margin: 0 auto; padding: 24px 16px 64px; }
75
+ & h1 { font-size: 22px; margin: 0 0 16px; }
76
+ & .tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--lvc-border); margin-bottom: 16px; flex-wrap: wrap; }
77
+ & .tabs a { padding: 8px 14px; border-radius: 8px 8px 0 0; color: var(--lvc-muted); }
78
+ & .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; }
79
+ & .tabs a:hover { text-decoration: none; color: var(--lvc-text); }
80
+ & .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; }
81
+ & .filters { margin-bottom: 16px; display: flex; gap: 8px; }
82
+ & .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; }
83
+ & .card { background: var(--lvc-surface); border: 1px solid var(--lvc-border); border-radius: 12px; overflow: hidden; }
84
+ & .card.pad { padding: 16px; }
85
+ & table { width: 100%; border-collapse: collapse; }
86
+ & th,
87
+ & td { padding: 10px 14px; text-align: left; vertical-align: top; }
88
+ & th { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--lvc-muted); border-bottom: 1px solid var(--lvc-border); }
89
+ & tr + tr td { border-top: 1px solid var(--lvc-border); }
90
+ & tr.unread td { font-weight: 600; }
91
+ & .badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 12px; font-weight: 600; color: #fff; white-space: nowrap; }
92
+ & .badge.status-open { background: var(--lvc-open); }
93
+ & .badge.status-resolved { background: var(--lvc-resolved); }
94
+ & .badge.unread-count { background: var(--lvc-danger); }
95
+ & .muted { color: var(--lvc-muted); font-size: 13px; }
96
+ & .pager { margin-top: 16px; display: flex; gap: 16px; }
97
+ & .empty { padding: 48px 16px; text-align: center; color: var(--lvc-muted); }
98
+ & .breadcrumb { margin-bottom: 12px; font-size: 13px; }
99
+ & .flash { margin: 0 0 16px; padding: 10px 14px; border-radius: 10px; border: 1px solid var(--lvc-border); background: var(--lvc-surface); }
100
+ & .flash.alert { color: var(--lvc-danger); border-color: var(--lvc-danger); }
101
+ & .head-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
102
+ & .head-row h1 { margin: 0; flex: 1; }
103
+ & .head-row form { display: inline; }
104
+ & .status-toggle-form { flex: 0 0 auto; }
105
+ & .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; }
106
+ & .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; }
107
+ & .status-toggle.is-open::before { left: 3px; }
108
+ & .status-toggle.is-resolved::before { left: 50%; }
109
+ & .status-toggle span { position: relative; z-index: 1; display: flex; align-items: center; justify-content: center; min-width: 0; padding: 0 10px; font-size: 13px; font-weight: 700; white-space: nowrap; }
110
+ & .status-toggle.is-open span:first-child { color: var(--lvc-open); }
111
+ & .status-toggle.is-resolved span:last-child { color: var(--lvc-text); }
112
+ & .status-toggle:hover { text-decoration: none; color: var(--lvc-text); }
113
+ & button,
114
+ & .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; }
115
+ & button.primary { background: var(--lvc-accent); border-color: var(--lvc-accent); color: var(--lvc-accent-text); }
116
+ & .case-id { color: var(--lvc-muted); font-weight: 400; font-size: 15px; }
117
+ & .avatars { display: inline-flex; }
118
+ & .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; }
119
+ & .avatars .avatar + .avatar { margin-left: -8px; }
120
+ & .avatar.color-0 { background: #2563eb; }
121
+ & .avatar.color-1 { background: #16a34a; }
122
+ & .avatar.color-2 { background: #d97706; }
123
+ & .avatar.color-3 { background: #dc2626; }
124
+ & .avatar.color-4 { background: #7c3aed; }
125
+ & .avatar.color-5 { background: #0891b2; }
126
+ & .context-line { margin: -6px 0 16px; overflow-wrap: anywhere; }
127
+ & .thread { display: flex; flex-direction: column; gap: 4px; padding: 16px; }
128
+ & .thread .who { font-size: 12px; color: var(--lvc-muted); margin: 10px 4px 2px; }
129
+ & .thread .who.visitor { text-align: left; }
130
+ & .thread .who.agent { text-align: right; }
131
+ & .thread .msg { max-width: 78%; padding: 8px 12px; border-radius: 14px; white-space: pre-wrap; overflow-wrap: anywhere; }
132
+ & .thread .msg.visitor { align-self: flex-start; background: var(--lvc-bg); border: 1px solid var(--lvc-border); border-bottom-left-radius: 4px; }
133
+ & .thread .msg.agent { align-self: flex-end; background: var(--lvc-accent); color: var(--lvc-accent-text); border-bottom-right-radius: 4px; }
134
+ & .thread .system { align-self: center; color: var(--lvc-muted); font-size: 12px; margin: 8px 0; }
135
+ & .thread .typing { align-self: flex-start; color: var(--lvc-muted); font-size: 12px; font-style: italic; margin: 8px 4px 2px; }
136
+ & .thread .msg .atts { display: flex; flex-direction: column; gap: 6px; margin-top: 6px; }
137
+ & .thread .msg .att-img img { max-width: 100%; max-height: 240px; border-radius: 8px; display: block; }
138
+ & .thread .msg .att-audio { display: block; width: 280px; max-width: 100%; height: 36px; }
139
+ & .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; }
140
+ & .thread .msg.agent .att-file { background: rgba(255,255,255,.2); }
141
+ & #new-messages { margin: 12px 0 0; }
142
+ & #new-messages a { font-weight: 600; }
143
+ & /* Unified composer box (matches the visitor widget): the reply on top,
144
+ & a
145
+ toolbar row below (tools left,
146
+ & send right),
147
+ & all inside one bordered box
148
+ that lights up on focus. */
149
+ .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); }
150
+ & .reply:focus-within { border-color: var(--lvc-accent); }
151
+ & .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; }
152
+ & .reply input[type=file].reply-file { font-size: 12px; color: var(--lvc-muted); }
153
+ & .reply-toolbar { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
154
+ & .reply-tools { display: flex; align-items: center; gap: 2px; }
155
+ & .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; }
156
+ & .reply .attach:hover { background: var(--lvc-border); color: var(--lvc-text); }
157
+ & .reply .attach.recording-on { color: var(--lvc-danger); }
158
+ & .reply.is-recording .attach { display: none; }
159
+ & .reply .chips { display: flex; flex-wrap: wrap; gap: 6px; }
160
+ & .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; }
161
+ & .reply .chip-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 220px; }
162
+ & .reply .chip-x { padding: 0 2px; border: none; background: none; color: var(--lvc-muted); font-size: 16px; line-height: 1; cursor: pointer; }
163
+ & .reply .chip-x:hover { color: var(--lvc-text); }
164
+ & .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; }
165
+ & .reply .recording[hidden] { display: none; }
166
+ & .reply .recording-status { display: inline-grid; grid-template-columns: 8px 24px 34px; align-items: center; gap: 6px; font-variant-numeric: tabular-nums; }
167
+ & .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); }
168
+ & .reply .recording-rec { font-size: 10px; font-weight: 700; letter-spacing: .04em; color: var(--lvc-danger); }
169
+ & .reply .recording-actions { display: flex; align-items: center; gap: 4px; }
170
+ & .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; }
171
+ & .reply .recording-actions button:hover { background: var(--lvc-border); color: var(--lvc-text); }
172
+ & .reply .recording-actions .stop-recording { background: var(--lvc-danger); color: #fff; }
173
+ & .reply .recording-actions .stop-recording:hover { background: #b91c1c; color: #fff; }
174
+ & .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; }
175
+ & /* Desktop inbox: a durable conversation list on the left and the active
176
+ thread on the right,
177
+ & with each pane owning its own scroll. */
178
+ .lvc-inbox,
179
+ & .lvc-inbox .container { height: 100vh; height: 100dvh; }
180
+ & .inbox-shell { flex: 1 1 auto; min-height: 0; display: grid; grid-template-columns: minmax(320px, 380px) 1fr; gap: 16px; }
181
+ & .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; }
182
+ & .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; }
183
+ & .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; }
184
+ & .inbox-sidebar .tabs a.active { color: var(--lvc-text); border: 0; background: transparent; box-shadow: none; margin: 0; }
185
+ & .inbox-sidebar .tabs a.active::after { content: ""; position: absolute; left: 24px; right: 24px; bottom: 0; height: 2px; border-radius: 999px; background: var(--lvc-accent); }
186
+ & .inbox-sidebar .tabs a:not(.active):hover { text-decoration: none; color: var(--lvc-text); background: var(--lvc-bg); }
187
+ & .inbox-sidebar .tabs .count { margin-left: 0; background: color-mix(in srgb, var(--lvc-muted) 14%, transparent); }
188
+ & .inbox-sidebar .tabs a.active .count { background: var(--lvc-border); }
189
+ & .inbox-sidebar .filters { flex: 0 0 auto; margin: 0; padding: 12px; border-bottom: 1px solid var(--lvc-border); }
190
+ & .inbox-sidebar .filters input[type=search] { max-width: none; }
191
+ & .conversation-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
192
+ & .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); }
193
+ & .conversation-row:hover { text-decoration: none; background: var(--lvc-bg); }
194
+ & .conversation-row.active { background: color-mix(in srgb, var(--lvc-accent) 9%, var(--lvc-surface)); box-shadow: inset 3px 0 0 var(--lvc-accent); }
195
+ & .conversation-row.unread .conversation-name,
196
+ & .conversation-row.unread .conversation-preview { font-weight: 700; }
197
+ & .conversation-main { min-width: 0; display: flex; flex-direction: column; gap: 3px; }
198
+ & .conversation-topline,
199
+ & .conversation-meta { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
200
+ & .conversation-name,
201
+ & .conversation-preview,
202
+ & .conversation-meta span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
203
+ & .conversation-name { font-weight: 600; }
204
+ & .conversation-time { margin-left: auto; white-space: nowrap; }
205
+ & .conversation-preview { display: block; }
206
+ & .conversation-side { display: flex; flex-direction: column; align-items: flex-end; justify-content: space-between; gap: 8px; padding-top: 1px; }
207
+ & .conversation-side .avatars { min-height: 26px; }
208
+ & .inbox-sidebar .pager { flex: 0 0 auto; margin: 0; padding: 10px 12px; border-top: 1px solid var(--lvc-border); }
209
+ & .inbox-thread { min-width: 0; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
210
+ & .inbox-thread .conversation-panel { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; padding: 0; }
211
+ & .conversation-panel .head-row,
212
+ & .conversation-panel .context-line { flex: 0 0 auto; }
213
+ & .conversation-panel .card { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; border-radius: 8px; }
214
+ & .conversation-panel .thread { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
215
+ & .conversation-panel .reply { flex: 0 0 auto; }
216
+ & .empty-state { margin: auto; max-width: 320px; padding: 24px; text-align: center; }
217
+ & .empty-state h2 { margin: 0 0 6px; font-size: 18px; }
218
+ & .empty-state p { margin: 0; }
219
+ & .mobile-back { display: none; }
220
+ & /* Conversation page: fill the viewport and scroll the THREAD,
221
+ & not the
222
+ page — so a reply never jumps the page,
223
+ & and the composer stays put. */
224
+ .lvc-convo,
225
+ & .lvc-convo .container { height: 100vh; height: 100dvh; }
226
+ }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Livechat
4
- VERSION = '0.7.1'
4
+ VERSION = '0.8.0'
5
5
  end
data/lib/livechat.rb CHANGED
@@ -29,6 +29,13 @@ module Livechat
29
29
  end
30
30
 
31
31
  # Can this request work the inbox? Checked by every inbox action.
32
+ # The class Livechat::DashboardController inherits from. Resolved on every
33
+ # call rather than memoized, so a host that reassigns base_controller_class
34
+ # in a reloadable initializer is not pinned to a stale, unloaded constant.
35
+ def base_controller
36
+ config.base_controller_class.to_s.constantize
37
+ end
38
+
32
39
  def agent?(request)
33
40
  !!config.authorize_agent.call(request)
34
41
  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.7.1
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yaroslav Shmarov
@@ -38,13 +38,16 @@ executables: []
38
38
  extensions: []
39
39
  extra_rdoc_files: []
40
40
  files:
41
+ - AGENTS.md
41
42
  - CHANGELOG.md
42
43
  - MIT-LICENSE
43
44
  - README.md
44
45
  - Rakefile
46
+ - app/controllers/concerns/livechat/request_context.rb
45
47
  - app/controllers/livechat/application_controller.rb
46
48
  - app/controllers/livechat/attachments_controller.rb
47
49
  - app/controllers/livechat/conversations_controller.rb
50
+ - app/controllers/livechat/dashboard_controller.rb
48
51
  - app/controllers/livechat/messages_controller.rb
49
52
  - app/controllers/livechat/visitor_controller.rb
50
53
  - app/controllers/livechat/widgets_controller.rb
@@ -60,6 +63,7 @@ files:
60
63
  - app/views/livechat/conversations/show.html.erb
61
64
  - app/views/livechat/mailer/new_agent_reply.text.erb
62
65
  - app/views/livechat/mailer/new_visitor_message.text.erb
66
+ - app/views/livechat/shared/_dashboard.html.erb
63
67
  - config/locales/livechat.ar.yml
64
68
  - config/locales/livechat.bg.yml
65
69
  - config/locales/livechat.bn.yml
@@ -90,6 +94,7 @@ files:
90
94
  - lib/generators/livechat/install/install_generator.rb
91
95
  - lib/generators/livechat/install/templates/create_livechat_tables.rb.tt
92
96
  - lib/generators/livechat/install/templates/initializer.rb
97
+ - lib/generators/livechat/migration_helpers.rb
93
98
  - lib/livechat.rb
94
99
  - lib/livechat/channels.rb
95
100
  - lib/livechat/configuration.rb