testimonials 0.8.0 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1472a2172dce3259de44e1af6de8a7c6466d0589a6afeb037900cb154b08e5b9
4
- data.tar.gz: a928753201311457628c7e38adf8164227d329d5bb753395a267c916dbdeaa08
3
+ metadata.gz: f6e6a966ec88019419566c0524b32c1e2cfbc01126ad9d7ee807063343fbadec
4
+ data.tar.gz: 1a3982c027490e0e31f342058e390c4aec208f9a90b4242998629aa6718a3504
5
5
  SHA512:
6
- metadata.gz: 0c312e4bdff8a87463bbcbdd842a4bbe711ac2557ead095ce86f70918f70eba937ec08f00a6477c969ed1bcbfa1f6ca654c31c414c040b40fa64852f14a250d8
7
- data.tar.gz: 272c88fc2b1bad4baf46849be45c2b5b9c6e2ce969cd2f0285c7cd04121e805e716026c17d4c9b8312792e85a5b35f3ad8c6d43de6876774f412c99cfe479cbf
6
+ metadata.gz: 4c92786e0beccb39fe665405ce100d997e0db155a7bc2c56e881747f417f705414b57cfac650afa37d4473beec64a2d61c916a49e3ad489f92aaed889bd5e267
7
+ data.tar.gz: 2e3ee818dcbb6c619e3efdc1740ddec85421c0194b0778d72767d7935f0547a88fdf19a247bc8479a06d9fcd7e74199448b350b5368d41bcc9f3ec250b48742a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.1
4
+
5
+ - Dropped the redundant `(tenant, status)` index from the install and tenant
6
+ migration templates. A B-tree serves any leftmost prefix, so
7
+ `(tenant, status, consent_given)` already covers it, and every scope on the
8
+ model starts with `for_tenant` — carrying both only cost write time and disk.
9
+ Flagged by `active_record_doctor` in a host app.
10
+ - Existing installs keep the index until they drop it themselves; nothing
11
+ breaks either way. To reclaim it:
12
+
13
+ ```ruby
14
+ class RemoveRedundantTestimonialsIndex < ActiveRecord::Migration[8.0]
15
+ def change
16
+ remove_index :testimonials_testimonials, %i[tenant status]
17
+ end
18
+ end
19
+ ```
20
+
21
+ - A generator test now fails the build if any index is a leftmost prefix of
22
+ another on the same table.
23
+
3
24
  ## 0.8.0
4
25
 
5
26
  - **`config.admin_layout` now works on its own.** The dashboard's stylesheet and
@@ -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>
@@ -24,7 +24,6 @@ class CreateTestimonialsTables < ActiveRecord::Migration<%= migration_version %>
24
24
  t.timestamps
25
25
  end
26
26
  add_index :testimonials_testimonials, :status
27
- add_index :testimonials_testimonials, %i[tenant status]
28
27
  add_index :testimonials_testimonials, %i[tenant status consent_given]
29
28
  <% unless options[:skip_nps] -%>
30
29
 
@@ -3,7 +3,6 @@
3
3
  class AddTenantToTestimonials < ActiveRecord::Migration<%= migration_version %>
4
4
  def change
5
5
  add_column :testimonials_testimonials, :tenant, :string
6
- add_index :testimonials_testimonials, %i[tenant status]
7
6
  add_index :testimonials_testimonials, %i[tenant status consent_given]
8
7
 
9
8
  # The optional tables: an install run with --skip-nps or
@@ -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,115 @@
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); }
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
+ }
49
86
 
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; }
87
+ /* Page frame the gem's own layout only. */
88
+ .tml-page { margin: 0; background: var(--tml-bg); color: var(--tml-text); font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
89
+ /* Desktop dashboard: queue on the left, selected record on the right. */
90
+ .tm-index, .tm-index .container,
91
+ .tm-nps-index, .tm-nps-index .container { height: 100vh; height: 100dvh; }
55
92
  .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
-
93
+ .tm-index .container, .tm-nps-index .container { max-width: 1280px; display: flex; flex-direction: column; padding-bottom: 16px; }
94
+ .tm-index .nav, .tm-nps-index .nav,
95
+ .tm-index .container .page-head, .tm-nps-index .container .page-head,
96
+ .tm-nps-index .stat-row { flex: 0 0 auto; }
97
+ .tm-index .container .page-head, .tm-nps-index .container .page-head { margin-bottom: 14px; }
98
+ .tm-nps-index .stat-row { margin-bottom: 14px; }
70
99
  /* Standalone show pages keep normal page scrolling. */
71
100
  .tm-show { min-height: 100vh; overflow: auto; }
72
101
  .tm-show .detail-scroll { overflow: visible; }
73
102
 
74
- /* ── 2. Dashboard components (any layout) ────────────────────────────────── */
75
-
103
+ /* Dashboard components any layout. */
76
104
  .tml-dashboard {
77
- color: var(--tml-text);
105
+ /* A scoping device, not a layout one: without this the wrapper would
106
+ become the flex/grid child that the content should be. */
107
+ display: contents;
78
108
 
79
109
  & * { box-sizing: border-box; }
80
110
  & a { color: var(--tml-accent); text-decoration: none; }
81
111
  & a:hover { text-decoration: underline; }
112
+ & .container { max-width: 1020px; margin: 0 auto; padding: 24px 16px 64px; }
82
113
  & h1 { font-size: 22px; margin: 0 0 16px; }
83
114
  & h1.title-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
84
-
85
115
  /* 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; }
116
+ & .page-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
88
117
  & .page-head h1 { margin: 0; }
118
+ & .nav { display: flex; gap: 8px; align-items: center; margin-bottom: 16px; flex-wrap: wrap; }
119
+ & .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; }
120
+ & .nav a:hover { text-decoration: none; color: var(--tml-text); }
121
+ & .nav a.active { background: var(--tml-accent); border-color: var(--tml-accent); color: var(--tml-accent-text); }
122
+ & .nav a.active:hover { color: var(--tml-accent-text); }
89
123
  & .share-link { margin-left: auto; font-size: 13px; font-weight: 400; }
90
-
91
124
  & .tabs { display: flex; gap: 4px; border-bottom: 1px solid var(--tml-border); margin-bottom: 16px; flex-wrap: wrap; }
92
125
  & .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; }
126
+ & .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
127
  & .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
-
128
+ & .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
129
  & .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; }
130
+ & .filters select,
131
+ & .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
132
  & .filters input[type=search] { flex: 1; max-width: 320px; }
103
-
104
133
  & .card { background: var(--tml-surface); border: 1px solid var(--tml-border); border-radius: 12px; overflow: hidden; }
105
134
  & .card.pad { padding: 16px; overflow: visible; }
106
-
107
135
  & 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); }
136
+ & th,
137
+ & td { padding: 10px 14px; text-align: left; vertical-align: top; }
138
+ & th { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--tml-muted); border-bottom: 1px solid var(--tml-border); }
111
139
  & 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; }
140
+ & .badge { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 12px; font-weight: 600; color: #fff; white-space: nowrap; }
115
141
  & .badge.status-pending { background: var(--tml-pending); }
116
142
  & .badge.status-approved { background: var(--tml-approved); }
117
143
  & .badge.status-archived { background: var(--tml-archived); }
@@ -119,7 +145,6 @@
119
145
  & .badge.nps-passive { background: var(--tml-passive); }
120
146
  & .badge.nps-detractor { background: var(--tml-detractor); }
121
147
  & .badge.locale { background: transparent; color: var(--tml-muted); border: 1px solid var(--tml-border); }
122
-
123
148
  & .stars { color: var(--tml-star); letter-spacing: 1px; white-space: nowrap; }
124
149
  & .stars .off { color: var(--tml-border); }
125
150
  & .muted { color: var(--tml-muted); font-size: 13px; }
@@ -129,13 +154,11 @@
129
154
  & td.row-actions { white-space: nowrap; text-align: right; }
130
155
  & td.row-actions form { display: inline-block; }
131
156
  & 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; }
157
+ & button,
158
+ & .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
159
  & button.primary { background: var(--tml-accent); border-color: var(--tml-accent); color: var(--tml-accent-text); }
136
160
  & button.danger { color: var(--tml-danger); }
137
161
  & button.small { padding: 3px 10px; font-size: 13px; }
138
-
139
162
  & .message { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
140
163
  & dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; margin: 0; }
141
164
  & dt { color: var(--tml-muted); }
@@ -145,15 +168,11 @@
145
168
  & .empty { padding: 48px 16px; text-align: center; color: var(--tml-muted); }
146
169
  & .breadcrumb { margin-bottom: 12px; font-size: 13px; }
147
170
  & .case-id { color: var(--tml-muted); font-weight: 400; font-size: 15px; }
148
-
149
171
  & .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; }
172
+ & .stat { flex: 1; min-width: 140px; background: var(--tml-surface); border: 1px solid var(--tml-border); border-radius: 12px; padding: 14px 16px; }
152
173
  & .stat b { display: block; font-size: 26px; }
153
174
  & .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. */
175
+ /* 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
176
  & .stat-nps { flex: 2.4; min-width: 240px; }
158
177
  & .stat-headline { display: flex; align-items: center; gap: 10px; }
159
178
  & .stat-headline b { display: inline-block; }
@@ -170,10 +189,8 @@
170
189
  & .nps-seg.nps-band-good { flex: 30; background: var(--tml-nps-good); }
171
190
  & .nps-seg.nps-band-great { flex: 40; background: var(--tml-nps-great); }
172
191
  & .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); }
192
+ /* Hidden until dashboard.js reads data-nps-marker and positions it, so a score of 50 never renders as if it were −100. */
193
+ & .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
194
  & .nps-marker.placed { display: block; }
178
195
  & .nps-meter-ticks { position: relative; height: 14px; margin-top: 3px; color: var(--tml-muted); font-size: 11px; }
179
196
  & .nps-meter-ticks span { position: absolute; transform: translateX(-50%); }
@@ -183,113 +200,58 @@
183
200
  & .nps-meter-ticks .tick-great { left: 85%; }
184
201
  & .nps-meter-ticks .tick-max { left: 100%; transform: translateX(-100%); }
185
202
  & .nps-sample { margin: 8px 0 0; font-size: 12px; }
186
-
187
203
  /* 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; }
204
+ & .nps-guide { flex: 0 0 auto; margin-bottom: 14px; background: var(--tml-surface); border: 1px solid var(--tml-border); border-radius: 12px; }
190
205
  & .nps-guide summary { padding: 10px 16px; color: var(--tml-muted); font-size: 13px; cursor: pointer; }
191
206
  & .nps-guide summary:hover { color: var(--tml-text); }
192
207
  & .nps-guide[open] summary { border-bottom: 1px solid var(--tml-border); }
193
208
  & .nps-guide-body { padding: 12px 16px 14px; font-size: 13px; }
194
209
  & .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; }
210
+ & .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
211
  & .nps-bands li { display: contents; }
198
212
  & .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; }
213
+ & .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
214
  & .best-line-hint { margin: 2px 0 6px; }
203
215
  & .best-line-save { margin-top: 8px; }
204
216
  & .panel-head { flex: 0 0 auto; }
205
217
  & .panel-head h1 { margin-bottom: 12px; }
206
218
  & .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; }
219
+ & .testimonial-panel,
220
+ & .nps-panel { min-width: 0; display: flex; flex-direction: column; }
208
221
  & .testimonial-panel .actions { flex: 0 0 auto; }
209
222
  & .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); }
223
+ & .dashboard-shell { flex: 1 1 auto; min-height: 0; display: grid; grid-template-columns: minmax(330px, 430px) 1fr; gap: 16px; }
224
+ & .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; }
225
+ & .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; }
226
+ & .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; }
227
+ & .dashboard-sidebar .tabs a.active { color: var(--tml-text); border: 0; background: transparent; box-shadow: none; margin: 0; }
228
+ & .dashboard-sidebar .tabs a.active::after { content: ""; position: absolute; left: 20px; right: 20px; bottom: 0; height: 2px; border-radius: 999px; background: var(--tml-accent); }
229
+ & .dashboard-sidebar .tabs a:not(.active):hover { text-decoration: none; color: var(--tml-text); background: var(--tml-bg); }
226
230
  & .dashboard-sidebar .tabs .count { margin-left: 0; background: color-mix(in srgb, var(--tml-muted) 14%, transparent); }
227
231
  & .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); }
232
+ & .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
233
  & .dashboard-sidebar .filters select { grid-column: 1 / -1; width: 100%; }
231
234
  & .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); }
235
+ & .record-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
236
+ & .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
237
  & .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); }
238
+ & .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
239
  & .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; }
240
+ & .record-topline,
241
+ & .record-meta { display: flex; align-items: center; gap: 8px; min-width: 0; }
242
+ & .record-copy,
243
+ & .record-meta span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
241
244
  & .record-copy { display: block; font-weight: 600; }
242
245
  & .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; }
246
+ & .record-side { display: flex; flex-direction: column; align-items: flex-end; justify-content: space-between; gap: 8px; padding-top: 1px; }
245
247
  & .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; }
248
+ & .dashboard-detail { min-width: 0; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
247
249
  & .dashboard-detail .testimonial-panel,
248
250
  & .dashboard-detail .nps-panel { flex: 1 1 auto; min-height: 0; }
251
+ & .dashboard-detail .detail-scroll { flex: 1 1 auto; overflow-y: auto; padding-right: 2px; }
249
252
  & .dashboard-detail .actions { margin-bottom: 0; }
250
253
  & .empty-state { margin: auto; max-width: 320px; padding: 24px; text-align: center; }
251
254
  & .empty-state h2 { margin: 0 0 6px; font-size: 18px; }
252
255
  & .empty-state p { margin: 0; }
253
256
  & .mobile-back { display: none; }
254
257
  }
255
-
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
- }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Testimonials
4
- VERSION = '0.8.0'
4
+ VERSION = '0.8.2'
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.0
4
+ version: 0.8.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yaroslav Shmarov