livechat 0.9.1 → 0.10.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: 77c8f578ea3bb8e56ae6e93c3ffc3194bbec65a1952d7e7f3e2152986bc12388
4
- data.tar.gz: bb5e62c493d8aef5e20a82884ed43213f2671543bee6e06d2aa1a352a55f9d25
3
+ metadata.gz: 5f7868af4ac547374ec04ba6aac09d12db7f9ae53776cceb7967cc32c7bf56ef
4
+ data.tar.gz: 40faba066cf75b325672728f1317a9dd1e56771cb2436c1e3a621ad36a560516
5
5
  SHA512:
6
- metadata.gz: f77110e02430ef730ac85ad64be4a05f4fd588035c384284e14374ebebbb40b3c5537df63e3d6128d20db2f1891836f54c90f223a087f123fa946893b8ea63d1
7
- data.tar.gz: da4da0a2cfa71838e93434cdf3d0d88fd5a6fc15a24754369977d7e3064d5052b28cf6d29055db044b3132acbaf50d832f97a4eb6ef2a565d58edd37e27bbd84
6
+ metadata.gz: 429aaa62a6b365b2314c2193faa3377879aee0b0c564c935065cc4df798e5471a5571c7e2f8b229fec2490b6ca382dfe8e50c2d88d809c063b5bf83b8d9183c3
7
+ data.tar.gz: 8658bd68fe0bfd233f3ddddd30079bd71f6443394650c306171ecc7db2184602e93bff51622d049b1e0d33b6e355676e077eab8177f9c3c833a13f924cbd71d8
data/AGENTS.md CHANGED
@@ -176,6 +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
+ - **`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`/`.status-switch`, 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.
180
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.
181
181
  - Commit messages are prose that explains the tradeoff — read `git log` before writing one.
data/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.1
4
+
5
+ - **The demo inbox is now a live setup walkthrough.** The open conversation
6
+ teaches the visitor-to-agent reply loop, captured page and identity context,
7
+ and unread state. The resolved conversation records the production checklist:
8
+ render the tag, protect the staff inbox, and configure signed-in visitors.
9
+ - `livechat:seed_demo` still rebuilds messages inside its two demo conversations
10
+ on every run, so replies or edits made in those threads are discarded. It
11
+ does not change real conversations.
12
+
13
+ ## 0.10.0
14
+
15
+ - **The open/resolved switch is now the family's shared component.** It looks the
16
+ same and sits where it sat, but it is `.status-switch` in the shared core of
17
+ `dashboard.css` rather than this gem's own `.status-toggle`, so ideasbugs could
18
+ adopt it for its three statuses instead of growing a second near-identical
19
+ control. `.status-toggle`, `.status-toggle-form`, `.is-open` and `.is-resolved`
20
+ are gone — that is the breaking change if you styled them.
21
+ - Each half is now its own target: `Open` reopens, `Resolved` resolves, via
22
+ `formaction` on two submit buttons in one form. Before, the whole pill was one
23
+ button that flipped whichever way it was not. The half that is already current
24
+ is inert, so clicking it can no longer log a reopen of an open chat. The
25
+ sliding highlight went with the rewrite — it never animated anyway, since
26
+ resolving reloads the page and the pill arrives already in its new position.
27
+ `POST /:id/resolve` and `POST /:id/reopen` are unchanged.
28
+
3
29
  ## 0.9.1
4
30
 
5
31
  - **The conversation heading reads as one block.** The visitor's email and locale
@@ -12,21 +12,24 @@
12
12
  <%= safe_join(context, ' · ') %>
13
13
  </p>
14
14
  </div>
15
- <% if conversation.open? %>
16
- <%= button_to resolve_conversation_path(conversation), method: :post,
17
- form_class: 'status-toggle-form',
18
- class: 'status-toggle is-open',
19
- aria: { label: t('livechat.dashboard.resolve', default: 'Resolve') } do %>
20
- <span><%= t('livechat.statuses.open', default: 'Open') %></span>
21
- <span><%= t('livechat.statuses.resolved', default: 'Resolved') %></span>
22
- <% end %>
23
- <% else %>
24
- <%= button_to reopen_conversation_path(conversation), method: :post,
25
- form_class: 'status-toggle-form',
26
- class: 'status-toggle is-resolved',
27
- aria: { label: t('livechat.dashboard.reopen', default: 'Reopen') } do %>
28
- <span><%= t('livechat.statuses.open', default: 'Open') %></span>
29
- <span><%= t('livechat.statuses.resolved', default: 'Resolved') %></span>
15
+ <%# The shared status switch: the state it is in is lit and inert, the other
16
+ one submits to its own route via `formaction`. One form, no JS, and
17
+ clicking the lit half cannot log a reopen of an already-open chat. %>
18
+ <%= form_with url: resolve_conversation_path(conversation), method: :post, class: 'status-switch' do %>
19
+ <% if conversation.open? %>
20
+ <button type="button" class="status-open current" aria-current="true">
21
+ <%= t('livechat.statuses.open', default: 'Open') %>
22
+ </button>
23
+ <button formaction="<%= resolve_conversation_path(conversation) %>" class="status-resolved">
24
+ <%= t('livechat.statuses.resolved', default: 'Resolved') %>
25
+ </button>
26
+ <% else %>
27
+ <button formaction="<%= reopen_conversation_path(conversation) %>" class="status-open">
28
+ <%= t('livechat.statuses.open', default: 'Open') %>
29
+ </button>
30
+ <button type="button" class="status-resolved current" aria-current="true">
31
+ <%= t('livechat.statuses.resolved', default: 'Resolved') %>
32
+ </button>
30
33
  <% end %>
31
34
  <% end %>
32
35
  </div>
@@ -144,6 +144,19 @@
144
144
  & .actions form { display: inline; }
145
145
  & .form-actions { padding-top: 12px; }
146
146
 
147
+ /* Status switch: one pill holding every state a record can be in, the state it
148
+ is in now lit. Each other segment is a submit button, so moving a record
149
+ takes one click and no JavaScript; the current segment is a `type="button"`
150
+ so clicking it cannot re-file the record it is already filed under. Two
151
+ states or five, the pill sizes itself. A gem tints the lit segment with its
152
+ own status colour — see the `GEM-SPECIFIC` section. */
153
+ & .status-switch { display: inline-flex; flex: 0 0 auto; margin: 0; padding: 3px; border: 1px solid var(--lvc-border); border-radius: 999px; background: var(--lvc-bg); }
154
+ & .status-switch button { min-height: 30px; padding: 0 12px; border: 0; border-radius: 999px; background: none; color: var(--lvc-muted); font-size: 13px; font-weight: 700; white-space: nowrap; transition: background .16s ease, color .16s ease; }
155
+ & .status-switch button:hover { border: 0; background: color-mix(in srgb, var(--lvc-muted) 12%, transparent); color: var(--lvc-text); }
156
+ & .status-switch button.current { background: var(--lvc-surface); color: var(--lvc-text); box-shadow: 0 1px 2px rgba(15, 23, 42, .12); cursor: default; }
157
+ & .status-switch button.current:hover { background: var(--lvc-surface); }
158
+ & .panel-head .status-switch { margin-left: auto; }
159
+
147
160
  & .card { overflow: hidden; border: 1px solid var(--lvc-border); border-radius: 12px; background: var(--lvc-surface); }
148
161
  & .card.pad { padding: 16px; overflow: visible; }
149
162
  & table { width: 100%; border-collapse: collapse; }
@@ -266,6 +279,7 @@
266
279
  & .dashboard-detail .detail-panel { min-height: calc(100vh - 64px); }
267
280
  & .mobile-back { display: block; flex: 0 0 auto; margin: 4px 0 10px; font-size: 13px; }
268
281
  & .panel-head h1 { font-size: 18px; }
282
+ & .status-switch button { padding: 0 9px; font-size: 12px; }
269
283
  & dl { grid-template-columns: 1fr; gap: 3px; }
270
284
  & dd { margin-bottom: 8px; }
271
285
  }
@@ -291,22 +305,12 @@
291
305
  & .record-side .avatars { min-height: 26px; }
292
306
 
293
307
  /* 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. */
308
+ and the shared status switch pushed to the far right of the row whatever the
309
+ name's length. */
297
310
  & .panel-titles { display: flex; min-width: 0; flex-direction: column; gap: 2px; }
298
311
  & .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; }
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; }
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; }
304
- & .status-toggle.is-open::before { left: 3px; }
305
- & .status-toggle.is-resolved::before { left: 50%; }
306
- & .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; }
307
- & .status-toggle.is-open span:first-child { color: var(--lvc-open); }
308
- & .status-toggle.is-resolved span:last-child { color: var(--lvc-text); }
309
- & .status-toggle:hover { text-decoration: none; color: var(--lvc-text); }
312
+ /* An open conversation is the one that wants an answer, so say so in green. */
313
+ & .status-switch button.current.status-open { color: var(--lvc-open); }
310
314
 
311
315
  & .avatars { display: inline-flex; }
312
316
  & .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; }
@@ -387,8 +391,6 @@
387
391
  @media (max-width: 760px) {
388
392
  .lvc-dashboard {
389
393
  & .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
394
  & .thread .msg { max-width: 88%; }
393
395
  }
394
396
  }
@@ -5,46 +5,52 @@ module Livechat
5
5
  THREADS = [
6
6
  {
7
7
  visitor_token: 'livechat-demo-checkout',
8
- visitor_label: 'Demo Customer',
8
+ visitor_label: 'Demo visitor · reply to this thread',
9
9
  visitor_email: 'customer@example.com',
10
- page_url: '/checkout',
10
+ page_url: '/checkout?demo=livechat',
11
11
  locale: 'en',
12
12
  status: 'open',
13
13
  messages: [
14
14
  {
15
15
  author_type: 'visitor',
16
- body: 'I am trying to finish checkout, but the payment button keeps spinning.',
16
+ body: 'I installed Livechat. What should I test first?',
17
17
  read_at: true
18
18
  },
19
19
  {
20
20
  author_type: 'agent',
21
21
  agent_id: 'livechat-demo-agent',
22
- agent_label: 'Demo Support',
23
- body: 'Thanks for the details. I am checking the payment logs now.',
22
+ agent_label: 'Demo support guide',
23
+ body: 'Open the chat widget in your app, send a message, then reply from this inbox. The page URL, ' \
24
+ 'locale, and configured visitor identity stay attached to the conversation.',
24
25
  read_at: true
25
26
  },
26
- { author_type: 'visitor', body: 'The order number is #DEMO-1042.' }
27
+ {
28
+ author_type: 'visitor',
29
+ body: 'This last message is intentionally unread. Reply from the composer to complete the ' \
30
+ 'visitor-to-agent loop.'
31
+ }
27
32
  ]
28
33
  },
29
34
  {
30
35
  visitor_token: 'livechat-demo-resolved',
31
- visitor_label: 'Demo Admin',
36
+ visitor_label: 'Demo admin · production checklist',
32
37
  visitor_email: 'admin@example.com',
33
- page_url: '/settings/billing',
38
+ page_url: '/settings?demo=livechat',
34
39
  locale: 'en',
35
40
  status: 'resolved',
36
41
  messages: [
37
- { author_type: 'visitor', body: 'Where can I download last month\'s invoice?' },
42
+ { author_type: 'visitor', body: 'What must I configure before this chat goes to production?' },
38
43
  {
39
44
  author_type: 'agent',
40
45
  agent_id: 'livechat-demo-agent',
41
- agent_label: 'Demo Support',
42
- body: 'Open Settings, then Billing, then click Download next to the invoice.',
46
+ agent_label: 'Demo support guide',
47
+ body: 'Render livechat_tag in the layout, set authorize_admin so only staff can read the inbox, ' \
48
+ 'and configure current_user when signed-in visitors should keep their identity across pages.',
43
49
  read_at: true
44
50
  },
45
51
  {
46
52
  author_type: 'system',
47
- agent_label: 'Demo Support',
53
+ agent_label: 'Demo support guide',
48
54
  event: 'resolved'
49
55
  }
50
56
  ]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Livechat
4
- VERSION = '0.9.1'
4
+ VERSION = '0.10.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.9.1
4
+ version: 0.10.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yaroslav Shmarov
@@ -130,7 +130,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
130
130
  - !ruby/object:Gem::Version
131
131
  version: '0'
132
132
  requirements: []
133
- rubygems_version: 3.6.9
133
+ rubygems_version: 4.0.10
134
134
  specification_version: 4
135
135
  summary: 'Open-source live chat for Rails: a drop-in support widget plus a team inbox,
136
136
  in your own database.'