testimonials 0.8.1 → 0.8.3

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: bfd44ded7b03a811fbbe8d6db999c3d04c9a1ca3d5401e1ae650fc407487e3a5
4
- data.tar.gz: c683180efb5063687ca5ac8410caa6f51dff44ffe1de7ef65c83d30b4bc5052e
3
+ metadata.gz: 3d3fdcc20cbf11e28b251af6265184f717d9242f2dc6fea1c3e0dec158e9db34
4
+ data.tar.gz: 875396b557db1480c42a557f1db615aa106e5b00fca4e00af0fcd8081454718f
5
5
  SHA512:
6
- metadata.gz: 4ec04128d604bceebd38bec356b7511e4fa2823256ec1e6a59c5224d51aca6359bc8420d0f5e76bc303f46df001494b024e38b94ee888536fa4f97b0ff47f179
7
- data.tar.gz: 94512ae9beeca12776e3aadcf5641e550b5733650c233d60e16ec449568d88ea38e4a4a84e36ceda03f2e0f80c133cd47315569ee14b73a64703183c9053d823
6
+ metadata.gz: 2a627aedbd6441c0eb6a8220c7c265e3aa52d0fcc27b5305d1d49230e362d7235ce347f30ad699724976030120668fb9cd458839a89a775b921069631e3a5767
7
+ data.tar.gz: b1007ae2757a2c2f728770e2b0cf7bda1923d4864f1ec7c1b72b2f776b0c18ba4483a80dc14bdb1534684471ccbfd37da980d226e7ead1c1606bcec5d528f30b
data/AGENTS.md CHANGED
@@ -190,6 +190,10 @@ Turbo Drive and nonce-based CSP work out of the box; the widget config rides in
190
190
 
191
191
  ---
192
192
 
193
+ ## One family
194
+
195
+ `ideasbugs`, `livechat`, `product_tours`, `i18n_proofreading` are the sibling engines. Same install shape, same host hooks (`base_controller_class`, `admin_layout`), same scoped dashboard CSS, same `primary_key_type`-aware migrations — so what you learn here transfers.
196
+
193
197
  ## Working on the gem itself
194
198
 
195
199
  ```bash
data/README.md CHANGED
@@ -382,6 +382,25 @@ in a real app and
382
382
  feature requests. Pairs with this gem's NPS detractor hook.
383
383
  - [SupeRails](https://superails.com) — Rails screencasts.
384
384
 
385
+ ## One family
386
+
387
+ Five Rails engines built on the same backbone, so adopting a second one is
388
+ mostly muscle memory:
389
+
390
+ | Gem | What it does |
391
+ | --- | --- |
392
+ | **testimonials** *(this gem)* | Testimonials, reviews and NPS — text and video, collected in your own app |
393
+ | [ideasbugs](https://github.com/yshmarov/ideasbugs) | In-app bug reports and feature requests, with a triage queue |
394
+ | [livechat](https://github.com/yshmarov/livechat) | Live chat between your visitors and your agents, self-hosted |
395
+ | [product_tours](https://github.com/yshmarov/product_tours) | Product tours and video tutorials, shown in-app at the right moment |
396
+ | [i18n_proofreading](https://github.com/yshmarov/i18n_proofreading) | In-context translation fixes suggested by your own users |
397
+
398
+ They share the install shape (`generate <gem>:install`, mount, one initializer),
399
+ the same host hooks (`base_controller_class` to inherit your admin's controller,
400
+ `admin_layout` for just the shell), a dashboard stylesheet scoped so it
401
+ cannot touch your own CSS, and migrations that follow your app's
402
+ `primary_key_type`.
403
+
385
404
  ## License
386
405
 
387
406
  MIT. If it saved you a subscription, a
@@ -9,19 +9,21 @@
9
9
  (testimonials/shared/_dashboard), not here, so they survive a host
10
10
  replacing this layout via config.admin_layout. %>
11
11
  </head>
12
- <body class="<%= content_for?(:body_class) ? yield(:body_class) : '' %>">
13
- <div class="tml-page">
14
- <div class="tml-nav">
15
- <%= link_to t('testimonials.dashboard.title', default: 'Testimonials'), root_path,
16
- class: ('active' unless controller_name == 'nps_responses') %>
17
- <% if Testimonials.config.nps %>
18
- <%= link_to t('testimonials.dashboard.nps', default: 'NPS'), nps_responses_path,
19
- class: ('active' if controller_name == 'nps_responses') %>
20
- <% end %>
21
- <%# The shareable links live on the index pages themselves, next to the
22
- heading a host admin_layout replaces this nav, not those. %>
12
+ <body class="tml-page <%= content_for?(:body_class) ? yield(:body_class) : '' %>">
13
+ <div class="tml-dashboard">
14
+ <div class="container">
15
+ <div class="nav">
16
+ <%= link_to t('testimonials.dashboard.title', default: 'Testimonials'), root_path,
17
+ class: ('active' unless controller_name == 'nps_responses') %>
18
+ <% if Testimonials.config.nps %>
19
+ <%= link_to t('testimonials.dashboard.nps', default: 'NPS'), nps_responses_path,
20
+ class: ('active' if controller_name == 'nps_responses') %>
21
+ <% end %>
22
+ <%# The shareable links live on the index pages themselves, next to the
23
+ heading — a host admin_layout replaces this nav, not those. %>
24
+ </div>
25
+ <%= yield %>
23
26
  </div>
24
- <%= yield %>
25
27
  </div>
26
28
  </body>
27
29
  </html>
@@ -1,20 +1,20 @@
1
1
  /* Dashboard styles.
2
2
  *
3
- * Two layers, and the split is what lets `config.admin_layout` work:
3
+ * Three layers, and the split is what lets a host replace the layout:
4
4
  *
5
- * 1. Page shell only ever applies under the gem's own layout, which is the
6
- * only thing that sets the `tm-index` / `tm-nps-index` / `tm-show` body
7
- * class. These are the full-viewport rules (100dvh, overflow hidden) that
8
- * would fight a host's admin chrome for control of the page.
9
- * 2. Component styles everything inside `.tml-dashboard`, the wrapper the
10
- * dashboard views render themselves. These travel with the views into any
11
- * layout, host or gem.
5
+ * :root custom properties, all `--tml-` prefixed so they can
6
+ * neither overwrite a host's nor be overwritten by them.
7
+ * .tml-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
+ * .tml-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.
12
13
  *
13
- * Nothing here styles a bare `body`, `a`, `table` or `*`, and every class and
14
- * custom property is `tml-`-prefixed or nested, so loading this file inside a
15
- * host admin cannot restyle the host's sidebar, topbar or anything else.
14
+ * Nothing here styles a bare `body`, `a`, `table` or `*` at the top level.
16
15
  */
17
16
 
17
+
18
18
  :root {
19
19
  color-scheme: light dark;
20
20
  --tml-bg: #f6f7f9; --tml-surface: #fff; --tml-text: #1c2024; --tml-muted: #6b7280;
@@ -29,89 +29,99 @@
29
29
  :root {
30
30
  --tml-bg: #111418; --tml-surface: #1a1f26; --tml-text: #e6e8ea; --tml-muted: #9aa2ab;
31
31
  --tml-border: #2a313a; --tml-accent: #3b82f6;
32
- }
33
32
  }
34
-
35
- /* ── 1. Page shell (gem layout only) ─────────────────────────────────────── */
36
-
37
- .tm-index, .tm-nps-index, .tm-show {
38
- margin: 0; background: var(--tml-bg); color: var(--tml-text);
39
- font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
40
33
  }
41
- .tml-page { max-width: 1020px; margin: 0 auto; padding: 24px 16px 64px; }
42
- .tml-nav { display: flex; gap: 8px; align-items: center; margin-bottom: 16px; flex-wrap: wrap; }
43
- .tml-nav a { padding: 6px 16px; border: 1px solid var(--tml-border); border-radius: 999px;
44
- background: var(--tml-surface); color: var(--tml-muted); font-weight: 600;
45
- text-decoration: none; }
46
- .tml-nav a:hover { text-decoration: none; color: var(--tml-text); }
47
- .tml-nav a.active { background: var(--tml-accent); border-color: var(--tml-accent); color: var(--tml-accent-text); }
48
- .tml-nav a.active:hover { color: var(--tml-accent-text); }
49
-
50
- /* Desktop dashboard: queue on the left, selected record on the right. Locked to
51
- the viewport, so it is the gem layout's business alone — under a host layout
52
- the dashboard is a normal block that scrolls with the host's page. */
53
- .tm-index, .tm-index .tml-page,
54
- .tm-nps-index, .tm-nps-index .tml-page { height: 100vh; height: 100dvh; }
55
- .tm-index, .tm-nps-index { overflow: hidden; }
56
- .tm-index .tml-page, .tm-nps-index .tml-page { max-width: 1280px; display: flex; flex-direction: column;
57
- padding-bottom: 16px; }
58
- .tm-index .tml-nav, .tm-nps-index .tml-nav { flex: 0 0 auto; }
59
- .tm-index .tml-dashboard, .tm-nps-index .tml-dashboard { flex: 1 1 auto; min-height: 0;
60
- display: flex; flex-direction: column; }
61
- .tm-index .page-head, .tm-nps-index .page-head { flex: 0 0 auto; margin-bottom: 14px; }
62
- .tm-nps-index .stat-row { flex: 0 0 auto; margin-bottom: 14px; }
63
- .tm-index .dashboard-shell, .tm-nps-index .dashboard-shell { flex: 1 1 auto; min-height: 0; }
64
- .tm-index .dashboard-sidebar, .tm-nps-index .dashboard-sidebar { min-height: 0; }
65
- .tm-index .dashboard-detail, .tm-nps-index .dashboard-detail { min-height: 0; overflow: hidden; }
66
- .tm-index .record-list, .tm-nps-index .record-list { overflow-y: auto; }
67
- .tm-index .dashboard-detail .detail-scroll,
68
- .tm-nps-index .dashboard-detail .detail-scroll { flex: 1 1 auto; overflow-y: auto; padding-right: 2px; }
69
-
70
- /* Standalone show pages keep normal page scrolling. */
71
- .tm-show { min-height: 100vh; overflow: auto; }
72
- .tm-show .detail-scroll { overflow: visible; }
73
-
74
- /* ── 2. Dashboard components (any layout) ────────────────────────────────── */
34
+ @media (max-width: 760px) {
35
+ .tml-page { font-size: 14px; }
36
+ .tm-index, .tm-nps-index, .tm-show { overflow-x: hidden; }
37
+ .tm-index, .tm-nps-index { overflow-y: auto; overflow-x: hidden; }
38
+ .tm-index, .tm-index .container,
39
+ .tm-nps-index, .tm-nps-index .container { min-height: 100vh; height: auto; }
40
+ .tm-index .container, .tm-nps-index .container { padding-bottom: 10px; }
41
+ .tml-dashboard {
42
+ & .container { padding: 14px 10px 24px; }
43
+ & .dashboard-detail,
44
+ & .testimonial-panel,
45
+ & .nps-panel,
46
+ & .detail-scroll,
47
+ & .card,
48
+ & dl,
49
+ & dd,
50
+ & .best-line-form,
51
+ & .best-line-form textarea { min-width: 0; max-width: 100%; }
52
+ & .testimonial-panel .card,
53
+ & .nps-panel .card { width: calc(100vw - 20px); }
54
+ & .best-line-form textarea { width: 100%; }
55
+ & .message,
56
+ & .best-line-hint,
57
+ & dd,
58
+ & .muted { overflow-wrap: anywhere; }
59
+ & .message,
60
+ & .best-line-hint,
61
+ & dd,
62
+ & .muted { max-width: calc(100vw - 54px); }
63
+ & .dashboard-shell { display: block; min-height: calc(100vh - 76px); }
64
+ & .dashboard-sidebar,
65
+ & .dashboard-detail { min-height: calc(100vh - 76px); }
66
+ & .dashboard-shell.has-selected .dashboard-sidebar { display: none; }
67
+ & .dashboard-shell:not(.has-selected) .dashboard-detail { display: none; }
68
+ & .record-list { overflow: visible; }
69
+ & .record-row { padding: 12px; }
70
+ & .record-meta { display: none; }
71
+ & .dashboard-detail .testimonial-panel,
72
+ & .dashboard-detail .nps-panel { min-height: calc(100vh - 64px); }
73
+ & .mobile-back { display: block; flex: 0 0 auto; margin: 10px 10px 0; font-size: 13px; }
74
+ & .panel-head h1 { font-size: 18px; }
75
+ & dl { grid-template-columns: 1fr; gap: 3px; }
76
+ & .stat-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
77
+ & .stat { min-width: 0; padding: 10px 12px; }
78
+ & .stat b { font-size: 22px; }
79
+ & .stat-nps { grid-column: 1 / -1; }
80
+ & .nps-bands { grid-template-columns: max-content 1fr; }
81
+ & .nps-bands b { grid-column: 2; }
82
+ & .nps-bands li > .muted { grid-column: 1 / -1; }
83
+ & .share-link { flex: 1 0 100%; margin-left: 0; text-align: center; }
84
+ }
85
+ }
75
86
 
87
+ /* Dashboard components — any layout. */
76
88
  .tml-dashboard {
77
- color: var(--tml-text);
89
+ /* A scoping device, not a layout one: without this the wrapper would
90
+ become the flex/grid child that the content should be. */
91
+ display: contents;
78
92
 
79
93
  & * { box-sizing: border-box; }
80
94
  & a { color: var(--tml-accent); text-decoration: none; }
81
95
  & a:hover { text-decoration: underline; }
96
+ & .container { max-width: 1020px; margin: 0 auto; padding: 24px 16px 64px; }
82
97
  & h1 { font-size: 22px; margin: 0 0 16px; }
83
98
  & h1.title-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
84
-
85
99
  /* Index heading row: title on the left, the shareable public link on the right. */
86
- & .page-head { display: flex; align-items: center; justify-content: space-between;
87
- gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
100
+ & .page-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
88
101
  & .page-head h1 { margin: 0; }
102
+ & .nav { display: flex; gap: 8px; align-items: center; margin-bottom: 16px; flex-wrap: wrap; }
103
+ & .nav a { padding: 6px 16px; border: 1px solid var(--tml-border); border-radius: 999px; background: var(--tml-surface); color: var(--tml-muted); font-weight: 600; }
104
+ & .nav a:hover { text-decoration: none; color: var(--tml-text); }
105
+ & .nav a.active { background: var(--tml-accent); border-color: var(--tml-accent); color: var(--tml-accent-text); }
106
+ & .nav a.active:hover { color: var(--tml-accent-text); }
89
107
  & .share-link { margin-left: auto; font-size: 13px; font-weight: 400; }
90
-
91
108
  & .tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--tml-border); margin-bottom: 16px; flex-wrap: wrap; }
92
109
  & .tabs a { padding: 8px 14px; border-radius: 8px 8px 0 0; color: var(--tml-muted); }
93
- & .tabs a.active { color: var(--tml-text); font-weight: 600; border: 1px solid var(--tml-border);
94
- border-bottom: 2px solid var(--tml-surface); background: var(--tml-surface); margin-bottom: -1px; }
110
+ & .tabs a.active { color: var(--tml-text); font-weight: 600; border: 1px solid var(--tml-border); border-bottom: 2px solid var(--tml-surface); background: var(--tml-surface); margin-bottom: -1px; }
95
111
  & .tabs a:hover { text-decoration: none; color: var(--tml-text); }
96
- & .count { display: inline-block; min-width: 20px; padding: 0 6px; margin-left: 4px; border-radius: 999px;
97
- background: var(--tml-border); font-size: 12px; text-align: center; }
98
-
112
+ & .count { display: inline-block; min-width: 20px; padding: 0 6px; margin-left: 4px; border-radius: 999px; background: var(--tml-border); font-size: 12px; text-align: center; }
99
113
  & .filters { margin-bottom: 16px; display: flex; gap: 8px; align-items: center; }
100
- & .filters select, & .filters input[type=search] { padding: 6px 8px; border: 1px solid var(--tml-border);
101
- border-radius: 8px; background: var(--tml-surface); color: var(--tml-text); font: inherit; }
114
+ & .filters select,
115
+ & .filters input[type=search] { padding: 6px 8px; border: 1px solid var(--tml-border); border-radius: 8px; background: var(--tml-surface); color: var(--tml-text); font: inherit; }
102
116
  & .filters input[type=search] { flex: 1; max-width: 320px; }
103
-
104
117
  & .card { background: var(--tml-surface); border: 1px solid var(--tml-border); border-radius: 12px; overflow: hidden; }
105
118
  & .card.pad { padding: 16px; overflow: visible; }
106
-
107
119
  & table { width: 100%; border-collapse: collapse; }
108
- & th, & td { padding: 10px 14px; text-align: left; vertical-align: top; }
109
- & th { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--tml-muted);
110
- border-bottom: 1px solid var(--tml-border); }
120
+ & th,
121
+ & td { padding: 10px 14px; text-align: left; vertical-align: top; }
122
+ & th { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--tml-muted); border-bottom: 1px solid var(--tml-border); }
111
123
  & tr + tr td { border-top: 1px solid var(--tml-border); }
112
-
113
- & .badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 12px; font-weight: 600;
114
- color: #fff; white-space: nowrap; }
124
+ & .badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 12px; font-weight: 600; color: #fff; white-space: nowrap; }
115
125
  & .badge.status-pending { background: var(--tml-pending); }
116
126
  & .badge.status-approved { background: var(--tml-approved); }
117
127
  & .badge.status-archived { background: var(--tml-archived); }
@@ -119,7 +129,6 @@
119
129
  & .badge.nps-passive { background: var(--tml-passive); }
120
130
  & .badge.nps-detractor { background: var(--tml-detractor); }
121
131
  & .badge.locale { background: transparent; color: var(--tml-muted); border: 1px solid var(--tml-border); }
122
-
123
132
  & .stars { color: var(--tml-star); letter-spacing: 1px; white-space: nowrap; }
124
133
  & .stars .off { color: var(--tml-border); }
125
134
  & .muted { color: var(--tml-muted); font-size: 13px; }
@@ -129,13 +138,11 @@
129
138
  & td.row-actions { white-space: nowrap; text-align: right; }
130
139
  & td.row-actions form { display: inline-block; }
131
140
  & td.row-actions form + form { margin-left: 6px; }
132
-
133
- & button, & .button { padding: 8px 14px; border: 1px solid var(--tml-border); border-radius: 8px; cursor: pointer;
134
- background: var(--tml-surface); color: var(--tml-text); font: inherit; }
141
+ & button,
142
+ & .button { padding: 8px 14px; border: 1px solid var(--tml-border); border-radius: 8px; cursor: pointer; background: var(--tml-surface); color: var(--tml-text); font: inherit; }
135
143
  & button.primary { background: var(--tml-accent); border-color: var(--tml-accent); color: var(--tml-accent-text); }
136
144
  & button.danger { color: var(--tml-danger); }
137
145
  & button.small { padding: 3px 10px; font-size: 13px; }
138
-
139
146
  & .message { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
140
147
  & dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; margin: 0; }
141
148
  & dt { color: var(--tml-muted); }
@@ -145,15 +152,11 @@
145
152
  & .empty { padding: 48px 16px; text-align: center; color: var(--tml-muted); }
146
153
  & .breadcrumb { margin-bottom: 12px; font-size: 13px; }
147
154
  & .case-id { color: var(--tml-muted); font-weight: 400; font-size: 15px; }
148
-
149
155
  & .stat-row { display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
150
- & .stat { flex: 1; min-width: 140px; background: var(--tml-surface); border: 1px solid var(--tml-border);
151
- border-radius: 12px; padding: 14px 16px; }
156
+ & .stat { flex: 1; min-width: 140px; background: var(--tml-surface); border: 1px solid var(--tml-border); border-radius: 12px; padding: 14px 16px; }
152
157
  & .stat b { display: block; font-size: 26px; }
153
158
  & .stat span { color: var(--tml-muted); font-size: 13px; }
154
-
155
- /* NPS score card: the number, the band it falls in, and where it sits on the
156
- −100..100 scale — so nobody has to google whether 50 is any good. */
159
+ /* NPS score card: the number, the band it falls in, and where it sits on the −100..100 scale — so nobody has to google whether 50 is any good. */
157
160
  & .stat-nps { flex: 2.4; min-width: 240px; }
158
161
  & .stat-headline { display: flex; align-items: center; gap: 10px; }
159
162
  & .stat-headline b { display: inline-block; }
@@ -170,10 +173,8 @@
170
173
  & .nps-seg.nps-band-good { flex: 30; background: var(--tml-nps-good); }
171
174
  & .nps-seg.nps-band-great { flex: 40; background: var(--tml-nps-great); }
172
175
  & .nps-seg.nps-band-excellent { flex: 30; background: var(--tml-nps-excellent); }
173
- /* Hidden until dashboard.js reads data-nps-marker and positions it, so a
174
- score of 50 never renders as if it were −100. */
175
- & .nps-marker { display: none; position: absolute; top: -3px; width: 4px; height: 12px; margin-left: -2px;
176
- border-radius: 2px; background: var(--tml-text); box-shadow: 0 0 0 2px var(--tml-surface); }
176
+ /* Hidden until dashboard.js reads data-nps-marker and positions it, so a score of 50 never renders as if it were −100. */
177
+ & .nps-marker { display: none; position: absolute; top: -3px; width: 4px; height: 12px; margin-left: -2px; border-radius: 2px; background: var(--tml-text); box-shadow: 0 0 0 2px var(--tml-surface); }
177
178
  & .nps-marker.placed { display: block; }
178
179
  & .nps-meter-ticks { position: relative; height: 14px; margin-top: 3px; color: var(--tml-muted); font-size: 11px; }
179
180
  & .nps-meter-ticks span { position: absolute; transform: translateX(-50%); }
@@ -183,69 +184,55 @@
183
184
  & .nps-meter-ticks .tick-great { left: 85%; }
184
185
  & .nps-meter-ticks .tick-max { left: 100%; transform: translateX(-100%); }
185
186
  & .nps-sample { margin: 8px 0 0; font-size: 12px; }
186
-
187
187
  /* The formula and the benchmarks, tucked behind a disclosure. */
188
- & .nps-guide { flex: 0 0 auto; margin-bottom: 14px; background: var(--tml-surface);
189
- border: 1px solid var(--tml-border); border-radius: 12px; }
188
+ & .nps-guide { flex: 0 0 auto; margin-bottom: 14px; background: var(--tml-surface); border: 1px solid var(--tml-border); border-radius: 12px; }
190
189
  & .nps-guide summary { padding: 10px 16px; color: var(--tml-muted); font-size: 13px; cursor: pointer; }
191
190
  & .nps-guide summary:hover { color: var(--tml-text); }
192
191
  & .nps-guide[open] summary { border-bottom: 1px solid var(--tml-border); }
193
192
  & .nps-guide-body { padding: 12px 16px 14px; font-size: 13px; }
194
193
  & .nps-guide-body p { margin: 0; max-width: 78ch; }
195
- & .nps-bands { display: grid; grid-template-columns: max-content max-content 1fr; gap: 8px 14px;
196
- margin: 12px 0; padding: 0; list-style: none; align-items: baseline; }
194
+ & .nps-bands { display: grid; grid-template-columns: max-content max-content 1fr; gap: 8px 14px; margin: 12px 0; padding: 0; list-style: none; align-items: baseline; }
197
195
  & .nps-bands li { display: contents; }
198
196
  & .nps-bands b { font-variant-numeric: tabular-nums; white-space: nowrap; }
199
-
200
- & .best-line-form textarea { width: 100%; padding: 8px; border: 1px solid var(--tml-border); border-radius: 8px;
201
- background: var(--tml-surface); color: var(--tml-text); font: inherit; resize: vertical; }
197
+ & .best-line-form textarea { width: 100%; padding: 8px; border: 1px solid var(--tml-border); border-radius: 8px; background: var(--tml-surface); color: var(--tml-text); font: inherit; resize: vertical; }
202
198
  & .best-line-hint { margin: 2px 0 6px; }
203
199
  & .best-line-save { margin-top: 8px; }
204
200
  & .panel-head { flex: 0 0 auto; }
205
201
  & .panel-head h1 { margin-bottom: 12px; }
206
202
  & .detail-scroll { min-height: 0; display: flex; flex-direction: column; gap: 12px; }
207
- & .testimonial-panel, & .nps-panel { min-width: 0; display: flex; flex-direction: column; }
203
+ & .testimonial-panel,
204
+ & .nps-panel { min-width: 0; display: flex; flex-direction: column; }
208
205
  & .testimonial-panel .actions { flex: 0 0 auto; }
209
206
  & .testimonial-panel .detail-scroll > .actions { margin: 0; }
210
-
211
- & .dashboard-shell { display: grid; grid-template-columns: minmax(330px, 430px) 1fr; gap: 16px; }
212
- & .dashboard-sidebar { min-width: 0; display: flex; flex-direction: column;
213
- background: var(--tml-surface); border: 1px solid var(--tml-border);
214
- border-radius: 8px; overflow: hidden; }
215
- & .dashboard-sidebar .tabs { flex: 0 0 auto; margin: 12px 12px 0; padding: 0; background: transparent;
216
- border: 0; border-radius: 0; gap: 6px; flex-wrap: nowrap; }
217
- & .dashboard-sidebar .tabs a { flex: 1 1 0; position: relative; display: flex; align-items: center;
218
- justify-content: center; gap: 6px; min-height: 34px; padding: 5px 10px 8px;
219
- border-radius: 8px; color: var(--tml-muted); font-weight: 600; }
220
- & .dashboard-sidebar .tabs a.active { color: var(--tml-text); border: 0; background: transparent;
221
- box-shadow: none; margin: 0; }
222
- & .dashboard-sidebar .tabs a.active::after { content: ""; position: absolute; left: 20px; right: 20px; bottom: 0;
223
- height: 2px; border-radius: 999px; background: var(--tml-accent); }
224
- & .dashboard-sidebar .tabs a:not(.active):hover { text-decoration: none; color: var(--tml-text);
225
- background: var(--tml-bg); }
207
+ & .dashboard-shell { flex: 1 1 auto; min-height: 0; display: grid; grid-template-columns: minmax(330px, 430px) 1fr; gap: 16px; }
208
+ & .dashboard-sidebar { min-width: 0; min-height: 0; display: flex; flex-direction: column; background: var(--tml-surface); border: 1px solid var(--tml-border); border-radius: 8px; overflow: hidden; }
209
+ & .dashboard-sidebar .tabs { flex: 0 0 auto; margin: 12px 12px 0; padding: 0; background: transparent; border: 0; border-radius: 0; gap: 6px; flex-wrap: nowrap; }
210
+ & .dashboard-sidebar .tabs a { flex: 1 1 0; position: relative; display: flex; align-items: center; justify-content: center; gap: 6px; min-height: 34px; padding: 5px 10px 8px; border-radius: 8px; color: var(--tml-muted); font-weight: 600; }
211
+ & .dashboard-sidebar .tabs a.active { color: var(--tml-text); border: 0; background: transparent; box-shadow: none; margin: 0; }
212
+ & .dashboard-sidebar .tabs a.active::after { content: ""; position: absolute; left: 20px; right: 20px; bottom: 0; height: 2px; border-radius: 999px; background: var(--tml-accent); }
213
+ & .dashboard-sidebar .tabs a:not(.active):hover { text-decoration: none; color: var(--tml-text); background: var(--tml-bg); }
226
214
  & .dashboard-sidebar .tabs .count { margin-left: 0; background: color-mix(in srgb, var(--tml-muted) 14%, transparent); }
227
215
  & .dashboard-sidebar .tabs a.active .count { background: var(--tml-border); }
228
- & .dashboard-sidebar .filters { flex: 0 0 auto; display: grid; grid-template-columns: minmax(0, 1fr) auto;
229
- margin: 0; padding: 12px; border-bottom: 1px solid var(--tml-border); }
216
+ & .dashboard-sidebar .filters { flex: 0 0 auto; display: grid; grid-template-columns: minmax(0, 1fr) auto; margin: 0; padding: 12px; border-bottom: 1px solid var(--tml-border); }
230
217
  & .dashboard-sidebar .filters select { grid-column: 1 / -1; width: 100%; }
231
218
  & .dashboard-sidebar .filters input[type=search] { min-width: 0; max-width: none; }
232
- & .record-list { flex: 1 1 auto; min-height: 0; }
233
- & .record-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; padding: 13px 14px;
234
- color: var(--tml-text); border-bottom: 1px solid var(--tml-border); }
219
+ & .record-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
220
+ & .record-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; padding: 13px 14px; color: var(--tml-text); border-bottom: 1px solid var(--tml-border); }
235
221
  & .record-row:hover { text-decoration: none; background: var(--tml-bg); }
236
- & .record-row.active { background: color-mix(in srgb, var(--tml-accent) 9%, var(--tml-surface));
237
- box-shadow: inset 3px 0 0 var(--tml-accent); }
222
+ & .record-row.active { background: color-mix(in srgb, var(--tml-accent) 9%, var(--tml-surface)); box-shadow: inset 3px 0 0 var(--tml-accent); }
238
223
  & .record-main { min-width: 0; display: flex; flex-direction: column; gap: 5px; }
239
- & .record-topline, & .record-meta { display: flex; align-items: center; gap: 8px; min-width: 0; }
240
- & .record-copy, & .record-meta span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
224
+ & .record-topline,
225
+ & .record-meta { display: flex; align-items: center; gap: 8px; min-width: 0; }
226
+ & .record-copy,
227
+ & .record-meta span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
241
228
  & .record-copy { display: block; font-weight: 600; }
242
229
  & .record-time { margin-left: auto; white-space: nowrap; }
243
- & .record-side { display: flex; flex-direction: column; align-items: flex-end; justify-content: space-between;
244
- gap: 8px; padding-top: 1px; }
230
+ & .record-side { display: flex; flex-direction: column; align-items: flex-end; justify-content: space-between; gap: 8px; padding-top: 1px; }
245
231
  & .dashboard-sidebar .pager { flex: 0 0 auto; margin: 0; padding: 10px 12px; border-top: 1px solid var(--tml-border); }
246
- & .dashboard-detail { min-width: 0; display: flex; flex-direction: column; }
232
+ & .dashboard-detail { min-width: 0; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
247
233
  & .dashboard-detail .testimonial-panel,
248
234
  & .dashboard-detail .nps-panel { flex: 1 1 auto; min-height: 0; }
235
+ & .dashboard-detail .detail-scroll { flex: 1 1 auto; overflow-y: auto; padding-right: 2px; }
249
236
  & .dashboard-detail .actions { margin-bottom: 0; }
250
237
  & .empty-state { margin: auto; max-width: 320px; padding: 24px; text-align: center; }
251
238
  & .empty-state h2 { margin: 0 0 6px; font-size: 18px; }
@@ -253,43 +240,19 @@
253
240
  & .mobile-back { display: none; }
254
241
  }
255
242
 
256
- @media (max-width: 760px) {
257
- .tm-index, .tm-nps-index, .tm-show { font-size: 14px; }
258
- .tml-page { padding: 14px 10px 24px; }
259
- .tm-index, .tm-nps-index, .tm-show { overflow-x: hidden; }
260
- .tm-index, .tm-nps-index { overflow-y: auto; overflow-x: hidden; }
261
- .tm-index, .tm-index .tml-page,
262
- .tm-nps-index, .tm-nps-index .tml-page { min-height: 100vh; height: auto; }
263
- .tm-index .tml-page, .tm-nps-index .tml-page { padding-bottom: 10px; }
264
- .tm-index .dashboard-shell, .tm-nps-index .dashboard-shell { min-height: calc(100vh - 76px); }
265
- .tm-index .dashboard-sidebar, .tm-index .dashboard-detail,
266
- .tm-nps-index .dashboard-sidebar, .tm-nps-index .dashboard-detail { min-height: calc(100vh - 76px); }
267
- .tm-index .dashboard-detail, .tm-nps-index .dashboard-detail { overflow: visible; }
268
- .tm-index .record-list, .tm-nps-index .record-list { overflow: visible; }
269
- .tm-index .dashboard-detail .testimonial-panel,
270
- .tm-nps-index .dashboard-detail .nps-panel { min-height: calc(100vh - 64px); }
271
-
272
- .tml-dashboard {
273
- & .dashboard-detail, & .testimonial-panel, & .nps-panel, & .detail-scroll, & .card, & dl, & dd,
274
- & .best-line-form, & .best-line-form textarea { min-width: 0; max-width: 100%; }
275
- & .testimonial-panel .card, & .nps-panel .card { width: calc(100vw - 20px); }
276
- & .best-line-form textarea { width: 100%; }
277
- & .message, & .best-line-hint, & dd, & .muted { overflow-wrap: anywhere; max-width: calc(100vw - 54px); }
278
- & .dashboard-shell { display: block; }
279
- & .dashboard-shell.has-selected .dashboard-sidebar { display: none; }
280
- & .dashboard-shell:not(.has-selected) .dashboard-detail { display: none; }
281
- & .record-row { padding: 12px; }
282
- & .record-meta { display: none; }
283
- & .mobile-back { display: block; flex: 0 0 auto; margin: 10px 10px 0; font-size: 13px; }
284
- & .panel-head h1 { font-size: 18px; }
285
- & dl { grid-template-columns: 1fr; gap: 3px; }
286
- & .stat-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
287
- & .stat { min-width: 0; padding: 10px 12px; }
288
- & .stat b { font-size: 22px; }
289
- & .stat-nps { grid-column: 1 / -1; }
290
- & .nps-bands { grid-template-columns: max-content 1fr; }
291
- & .nps-bands b { grid-column: 2; }
292
- & .nps-bands li > .muted { grid-column: 1 / -1; }
293
- & .share-link { flex: 1 0 100%; margin-left: 0; text-align: center; }
294
- }
295
- }
243
+ /* Page frame — the gem's own layout only. Last, so it wins the
244
+ specificity ties against the component layer above. */
245
+ .tml-page { margin: 0; background: var(--tml-bg); color: var(--tml-text); font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
246
+ /* Desktop dashboard: queue on the left, selected record on the right. */
247
+ .tm-index, .tm-index .container,
248
+ .tm-nps-index, .tm-nps-index .container { height: 100vh; height: 100dvh; }
249
+ .tm-index, .tm-nps-index { overflow: hidden; }
250
+ .tm-index .container, .tm-nps-index .container { max-width: 1280px; display: flex; flex-direction: column; padding-bottom: 16px; }
251
+ .tm-index .nav, .tm-nps-index .nav,
252
+ .tm-index .container .page-head, .tm-nps-index .container .page-head,
253
+ .tm-nps-index .stat-row { flex: 0 0 auto; }
254
+ .tm-index .container .page-head, .tm-nps-index .container .page-head { margin-bottom: 14px; }
255
+ .tm-nps-index .stat-row { margin-bottom: 14px; }
256
+ /* Standalone show pages keep normal page scrolling. */
257
+ .tm-show { min-height: 100vh; overflow: auto; }
258
+ .tm-show .detail-scroll { overflow: visible; }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Testimonials
4
- VERSION = '0.8.1'
4
+ VERSION = '0.8.3'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: testimonials
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.1
4
+ version: 0.8.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yaroslav Shmarov