testimonials 0.8.3 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/AGENTS.md +1 -0
- data/CHANGELOG.md +39 -0
- data/README.md +3 -9
- data/app/views/testimonials/nps_responses/_response_panel.html.erb +2 -2
- data/app/views/testimonials/nps_responses/index.html.erb +2 -2
- data/app/views/testimonials/nps_responses/show.html.erb +1 -1
- data/app/views/testimonials/testimonials/_testimonial_panel.html.erb +1 -1
- data/app/views/testimonials/testimonials/index.html.erb +2 -2
- data/app/views/testimonials/testimonials/show.html.erb +1 -1
- data/lib/testimonials/dashboard.css +301 -165
- data/lib/testimonials/seeds.rb +22 -12
- data/lib/testimonials/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0cbc7f22314c712ecb0e1eb5d92a3d66981193bc1fc19f8664f635d381db0045
|
|
4
|
+
data.tar.gz: '020981c67a45702104ebf9cdfd9f685ffc17d44653a3476560c0fb662176e445'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1ca4a49fd102d401ae88b344c951655e68e88af3fb0da1a11819b7b600edb9ad11543feb5440a25f2127e138f4da54d9d7447a3b260a79d7e3cb59b5abcdbfb2
|
|
7
|
+
data.tar.gz: d8c94fe02b56b3e91adf3eb39e1c87e88b70d4f0d9ed4102e8058901b019f0a1c5fb4d1d02327be255c18b5ca58afedae06e7386efd58978d16d017fde1bc8d5
|
data/AGENTS.md
CHANGED
|
@@ -210,5 +210,6 @@ Conventions this codebase holds to — follow them rather than the first thing t
|
|
|
210
210
|
- **Every install shape gets a test that drops the table for real** (`test/integration/skip_nps_test.rb`, `skip_prompt_events_test.rb`), so a slipped guard raises `no such table` instead of passing quietly. Generator tests pin each migration and initializer shape and check the ERB still compiles as Ruby.
|
|
211
211
|
- **The widget is plain ES5-style JS in `lib/testimonials/widget.js`**, served by the engine, no build step, no framework. It reads its config from a JSON block on every `turbo:load`.
|
|
212
212
|
- **Display stays headless.** New display UI belongs in `examples/`, not in `app/views`.
|
|
213
|
+
- **`lib/testimonials/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 `tml` 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.
|
|
213
214
|
- Every user-facing change bumps `lib/testimonials/version.rb` and adds a `CHANGELOG.md` entry that says what it costs, not only what it adds.
|
|
214
215
|
- Commit messages are prose that explains the tradeoff — read `git log` before writing one.
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.1
|
|
4
|
+
|
|
5
|
+
- **The demo data now teaches responsible collection.** Its testimonials cover
|
|
6
|
+
success-moment prompting, pending moderation, public consent, private
|
|
7
|
+
archiving, best lines, and the shared approval rules for text and video. The
|
|
8
|
+
promoter, passive, and detractor comments explain why the balanced sample has
|
|
9
|
+
an NPS of zero.
|
|
10
|
+
- The bundled video remains intentionally generic: it proves protected upload
|
|
11
|
+
and playback without pretending to be customer evidence. Rerunning
|
|
12
|
+
`testimonials:seed_demo` refreshes the existing demo rows and can overwrite
|
|
13
|
+
edits to them; non-demo customer records are not touched.
|
|
14
|
+
|
|
15
|
+
## 0.9.0
|
|
16
|
+
|
|
17
|
+
- **One design system across the family.** The stylesheet now opens with a
|
|
18
|
+
shared core — the colour tokens, `.page-head`, `.tabs`, `.filters`, `.card`,
|
|
19
|
+
`.badge`, buttons and form controls, and the `.dashboard-shell` +
|
|
20
|
+
`.record-row` + `.detail-panel` two-pane dashboard — identical in all five
|
|
21
|
+
gems of the family, apart from the `--tml-` prefix. The five had drifted:
|
|
22
|
+
sidebars between 380px and 430px, reading columns between 860px and 1020px,
|
|
23
|
+
two different tab styles and three different button styles. The sidebar is
|
|
24
|
+
now 330–430px everywhere, a reading page 1020px, a dashboard 1280px.
|
|
25
|
+
Everything below the `GEM-SPECIFIC` banner is what only this gem has.
|
|
26
|
+
- **The dashboard markup uses the shared class names.** `.testimonial-panel`
|
|
27
|
+
and `.nps-panel` are both `.detail-panel` now, and the body classes are
|
|
28
|
+
`tml-index`, `tml-show` and `tml-index tml-nps-index` where they were
|
|
29
|
+
`tm-index`, `tm-show` and `tm-nps-index`. The row classes were already
|
|
30
|
+
shared. If you styled or scripted any of those names in a host app, that is
|
|
31
|
+
the breaking change — no configuration, route or database change is
|
|
32
|
+
involved.
|
|
33
|
+
- **The narrow-screen rules work again.** The `max-width: 760px` block sat above
|
|
34
|
+
the component rules it means to override, and CSS nesting adds no specificity,
|
|
35
|
+
so the desktop grid won every tie: showing one pane at a time on a phone had
|
|
36
|
+
quietly stopped working. The media query moved to the end of the file.
|
|
37
|
+
- Smaller fixes that came with the shared core: a submit input is styled as a
|
|
38
|
+
button rather than a full-width field, the filter row keeps its search box and
|
|
39
|
+
button on one line, and a `code.key` truncates inside a list row instead of
|
|
40
|
+
wrapping over three lines.
|
|
41
|
+
|
|
3
42
|
## 0.8.1
|
|
4
43
|
|
|
5
44
|
- Dropped the redundant `(tenant, status)` index from the install and tenant
|
data/README.md
CHANGED
|
@@ -376,12 +376,6 @@ Bug reports and pull requests welcome. The fastest way to help is to install it
|
|
|
376
376
|
in a real app and
|
|
377
377
|
[tell me where it hurt](https://github.com/yshmarov/testimonials/issues).
|
|
378
378
|
|
|
379
|
-
## Also by the same author
|
|
380
|
-
|
|
381
|
-
- [ideasbugs](https://github.com/yshmarov/ideasbugs) — in-app bug reports and
|
|
382
|
-
feature requests. Pairs with this gem's NPS detractor hook.
|
|
383
|
-
- [SupeRails](https://superails.com) — Rails screencasts.
|
|
384
|
-
|
|
385
379
|
## One family
|
|
386
380
|
|
|
387
381
|
Five Rails engines built on the same backbone, so adopting a second one is
|
|
@@ -397,9 +391,9 @@ mostly muscle memory:
|
|
|
397
391
|
|
|
398
392
|
They share the install shape (`generate <gem>:install`, mount, one initializer),
|
|
399
393
|
the same host hooks (`base_controller_class` to inherit your admin's controller,
|
|
400
|
-
`admin_layout` for just the shell),
|
|
401
|
-
|
|
402
|
-
`primary_key_type`.
|
|
394
|
+
`admin_layout` for just the shell), one dashboard design system — the same
|
|
395
|
+
two-pane layout, colour tokens and components in all five, scoped so it cannot
|
|
396
|
+
touch your own CSS — and migrations that follow your app's `primary_key_type`.
|
|
403
397
|
|
|
404
398
|
## License
|
|
405
399
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
<section class="
|
|
1
|
+
<section class="detail-panel">
|
|
2
2
|
<div class="panel-head">
|
|
3
|
-
<h1
|
|
3
|
+
<h1>
|
|
4
4
|
<span class="badge nps-<%= response.promoter? ? 'promoter' : (response.detractor? ? 'detractor' : 'passive') %>">
|
|
5
5
|
<%= response.score %>
|
|
6
6
|
</span>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<% content_for :body_class, '
|
|
1
|
+
<% content_for :body_class, 'tml-index tml-nps-index' %>
|
|
2
2
|
|
|
3
3
|
<%= render layout: 'testimonials/shared/dashboard' do %>
|
|
4
4
|
<div class="page-head">
|
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
<% if @responses.any? %>
|
|
115
115
|
<% @responses.each do |response| %>
|
|
116
116
|
<%= link_to nps_responses_path(page: @page, response_id: response.id),
|
|
117
|
-
class: ['record-row
|
|
117
|
+
class: ['record-row', ('active' if @selected_response&.id == response.id)].compact do %>
|
|
118
118
|
<span class="record-main">
|
|
119
119
|
<span class="record-topline">
|
|
120
120
|
<span class="badge nps-<%= response.promoter? ? 'promoter' : (response.detractor? ? 'detractor' : 'passive') %>">
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<% content_for :body_class, '
|
|
1
|
+
<% content_for :body_class, 'tml-index' %>
|
|
2
2
|
|
|
3
3
|
<%= render layout: 'testimonials/shared/dashboard' do %>
|
|
4
4
|
<div class="page-head">
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
<% @testimonials.each do |testimonial| %>
|
|
47
47
|
<%= link_to testimonials_path(status: @status, kind: @kind, q: @query, page: @page,
|
|
48
48
|
testimonial_id: testimonial.id),
|
|
49
|
-
class: ['record-row
|
|
49
|
+
class: ['record-row',
|
|
50
50
|
('active' if @selected_testimonial&.id == testimonial.id)].compact do %>
|
|
51
51
|
<span class="record-main">
|
|
52
52
|
<span class="record-topline">
|
|
@@ -4,83 +4,56 @@
|
|
|
4
4
|
*
|
|
5
5
|
* :root custom properties, all `--tml-` prefixed so they can
|
|
6
6
|
* neither overwrite a host's nor be overwritten by them.
|
|
7
|
-
* .tml-page
|
|
7
|
+
* .tml-page the page frame (was bare html/body). Only the gem's own
|
|
8
8
|
* layout puts this class on <body>, so inside a host admin
|
|
9
9
|
* these rules simply do not apply.
|
|
10
|
-
* .tml-dashboard
|
|
10
|
+
* .tml-dashboard everything else, nested. Names like .container, .card
|
|
11
11
|
* and .tabs are confined to the dashboard's own markup, so
|
|
12
12
|
* loading this file inside a host cannot restyle its chrome.
|
|
13
13
|
*
|
|
14
14
|
* Nothing here styles a bare `body`, `a`, `table` or `*` at the top level.
|
|
15
|
+
*
|
|
16
|
+
* Two sections follow: the design system shared by the whole gem family,
|
|
17
|
+
* then what only this gem has. A rule that belongs in the shared section
|
|
18
|
+
* belongs in all five copies of it.
|
|
19
|
+
*
|
|
20
|
+
* Order inside a section matters — components, then the page frame, then the
|
|
21
|
+
* narrow-screen overrides. Nesting adds no specificity, so the last rule
|
|
22
|
+
* wins the ties, and an override placed before the rule it means to beat
|
|
23
|
+
* silently does nothing.
|
|
15
24
|
*/
|
|
16
25
|
|
|
26
|
+
/* ---------------------------------------------------------------------------
|
|
27
|
+
* SHARED CORE — identical in testimonials, ideasbugs, livechat, product_tours
|
|
28
|
+
* and i18n_proofreading apart from the prefix. Diff it against a sibling gem
|
|
29
|
+
* before you change it, and carry the change to all five.
|
|
30
|
+
* ------------------------------------------------------------------------- */
|
|
17
31
|
|
|
18
32
|
:root {
|
|
19
33
|
color-scheme: light dark;
|
|
20
|
-
--tml-bg: #f6f7f9;
|
|
21
|
-
--tml-
|
|
22
|
-
--tml-
|
|
23
|
-
--tml-
|
|
24
|
-
--tml-
|
|
25
|
-
|
|
26
|
-
--tml-
|
|
34
|
+
--tml-bg: #f6f7f9;
|
|
35
|
+
--tml-surface: #fff;
|
|
36
|
+
--tml-text: #1c2024;
|
|
37
|
+
--tml-muted: #6b7280;
|
|
38
|
+
--tml-border: #e5e7eb;
|
|
39
|
+
--tml-accent: #2563eb;
|
|
40
|
+
--tml-accent-text: #fff;
|
|
41
|
+
--tml-code: #f0f1f3;
|
|
42
|
+
/* The four semantic states every gem in the family badges something with. */
|
|
43
|
+
--tml-success: #16a34a;
|
|
44
|
+
--tml-warning: #d97706;
|
|
45
|
+
--tml-danger: #dc2626;
|
|
46
|
+
--tml-neutral: #6b7280;
|
|
27
47
|
}
|
|
28
48
|
@media (prefers-color-scheme: dark) {
|
|
29
49
|
:root {
|
|
30
|
-
--tml-bg: #111418;
|
|
31
|
-
--tml-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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; }
|
|
50
|
+
--tml-bg: #111418;
|
|
51
|
+
--tml-surface: #1a1f26;
|
|
52
|
+
--tml-text: #e6e8ea;
|
|
53
|
+
--tml-muted: #9aa2ab;
|
|
54
|
+
--tml-border: #2a313a;
|
|
55
|
+
--tml-accent: #3b82f6;
|
|
56
|
+
--tml-code: #232a33;
|
|
84
57
|
}
|
|
85
58
|
}
|
|
86
59
|
|
|
@@ -93,70 +66,273 @@
|
|
|
93
66
|
& * { box-sizing: border-box; }
|
|
94
67
|
& a { color: var(--tml-accent); text-decoration: none; }
|
|
95
68
|
& a:hover { text-decoration: underline; }
|
|
69
|
+
& :focus-visible { outline: 2px solid var(--tml-accent); outline-offset: 2px; }
|
|
70
|
+
|
|
71
|
+
/* Page furniture. `.container` is the reading width; an index page widens it
|
|
72
|
+
to 1280px in the page frame below. */
|
|
96
73
|
& .container { max-width: 1020px; margin: 0 auto; padding: 24px 16px 64px; }
|
|
97
|
-
& h1 {
|
|
98
|
-
&
|
|
99
|
-
|
|
100
|
-
& .page-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; }
|
|
74
|
+
& h1 { margin: 0 0 16px; font-size: 22px; }
|
|
75
|
+
& h2 { margin: 0 0 12px; font-size: 16px; }
|
|
76
|
+
& .page-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 14px; }
|
|
101
77
|
& .page-head h1 { margin: 0; }
|
|
102
|
-
& .
|
|
78
|
+
& .lede { margin: 4px 0 0; color: var(--tml-muted); }
|
|
79
|
+
& .breadcrumb { margin: 0 0 12px; font-size: 13px; }
|
|
80
|
+
& .muted { color: var(--tml-muted); font-size: 13px; }
|
|
81
|
+
& .case-id { color: var(--tml-muted); font-weight: 400; font-size: 15px; }
|
|
82
|
+
& .spacer { flex: 1; }
|
|
83
|
+
& .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
|
|
84
|
+
|
|
85
|
+
/* Section switcher: the pills above the dashboard when a gem has more than
|
|
86
|
+
one section. Lives on the page, so a host `admin_layout` can drop it. */
|
|
87
|
+
& .nav { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 16px; }
|
|
103
88
|
& .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
89
|
& .nav a:hover { text-decoration: none; color: var(--tml-text); }
|
|
105
|
-
& .nav a.active {
|
|
90
|
+
& .nav a.active { border-color: var(--tml-accent); background: var(--tml-accent); color: var(--tml-accent-text); }
|
|
106
91
|
& .nav a.active:hover { color: var(--tml-accent-text); }
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
& .tabs
|
|
111
|
-
& .tabs a
|
|
112
|
-
& .
|
|
113
|
-
& .
|
|
114
|
-
& .
|
|
115
|
-
& .
|
|
116
|
-
& .
|
|
117
|
-
|
|
92
|
+
|
|
93
|
+
/* Tabs: one system for the whole family — an underlined row of equal-width
|
|
94
|
+
tabs, sized to sit at the top of the sidebar. */
|
|
95
|
+
& .tabs { display: flex; gap: 6px; }
|
|
96
|
+
& .tabs a { position: relative; display: flex; flex: 1 1 0; align-items: center; justify-content: center; gap: 6px; min-height: 34px; padding: 5px 10px 8px; border-radius: 8px; color: var(--tml-muted); font-weight: 600; }
|
|
97
|
+
& .tabs a:hover { background: var(--tml-bg); color: var(--tml-text); text-decoration: none; }
|
|
98
|
+
& .tabs a.active { color: var(--tml-text); }
|
|
99
|
+
& .tabs a.active::after { content: ""; position: absolute; right: 20px; bottom: 0; left: 20px; height: 2px; border-radius: 999px; background: var(--tml-accent); }
|
|
100
|
+
& .count { display: inline-block; min-width: 20px; padding: 0 6px; border-radius: 999px; background: color-mix(in srgb, var(--tml-muted) 14%, transparent); font-size: 12px; text-align: center; font-variant-numeric: tabular-nums; }
|
|
101
|
+
& .tabs a.active .count { background: var(--tml-border); }
|
|
102
|
+
|
|
103
|
+
/* Filter row under the tabs: a full-width select over a search + submit row,
|
|
104
|
+
however many of the three a gem actually has. */
|
|
105
|
+
& .filters { display: flex; flex-wrap: wrap; gap: 8px; margin: 0; padding: 12px; border-bottom: 1px solid var(--tml-border); }
|
|
106
|
+
& .filters label { flex: 1 1 100%; color: var(--tml-muted); font-size: 13px; }
|
|
107
|
+
& .filters select { flex: 1 1 100%; }
|
|
108
|
+
/* Basis 0, not auto: the controls below are `width: 100%`, which would
|
|
109
|
+
otherwise push the submit button onto its own row. */
|
|
110
|
+
& .filters input[type="search"] { flex: 1 1 0; min-width: 0; }
|
|
111
|
+
& .filters button,
|
|
112
|
+
& .filters input[type="submit"] { flex: 0 0 auto; }
|
|
113
|
+
|
|
114
|
+
/* Form controls. 16px so iOS does not zoom the page on focus. */
|
|
115
|
+
& input,
|
|
116
|
+
& select,
|
|
117
|
+
& textarea { width: 100%; min-height: 38px; padding: 7px 9px; border: 1px solid var(--tml-border); border-radius: 8px; background: var(--tml-surface); color: var(--tml-text); font: inherit; font-size: 16px; letter-spacing: 0; }
|
|
118
|
+
& textarea { resize: vertical; }
|
|
119
|
+
& input[type="checkbox"],
|
|
120
|
+
& input[type="radio"] { width: auto; min-width: 0; min-height: 0; }
|
|
121
|
+
& input:focus,
|
|
122
|
+
& select:focus,
|
|
123
|
+
& textarea:focus { border-color: var(--tml-accent); outline: 2px solid color-mix(in srgb, var(--tml-accent) 22%, transparent); outline-offset: 0; }
|
|
124
|
+
& .field { margin-bottom: 16px; }
|
|
125
|
+
& .field label { display: block; margin-bottom: 5px; font-weight: 600; }
|
|
126
|
+
& .field small { display: block; margin-top: 5px; color: var(--tml-muted); }
|
|
127
|
+
|
|
128
|
+
/* A submit input is a button, whatever the markup says. */
|
|
129
|
+
& button,
|
|
130
|
+
& .button,
|
|
131
|
+
& input[type="submit"] { display: inline-flex; width: auto; align-items: center; justify-content: center; min-height: 38px; padding: 8px 14px; border: 1px solid var(--tml-border); border-radius: 8px; background: var(--tml-surface); color: var(--tml-text); font: inherit; cursor: pointer; }
|
|
132
|
+
& button:hover,
|
|
133
|
+
& .button:hover,
|
|
134
|
+
& input[type="submit"]:hover { border-color: var(--tml-muted); text-decoration: none; }
|
|
135
|
+
& button.primary,
|
|
136
|
+
& .button.primary,
|
|
137
|
+
& input[type="submit"].primary { border-color: var(--tml-accent); background: var(--tml-accent); color: var(--tml-accent-text); }
|
|
138
|
+
& button.danger,
|
|
139
|
+
& .button.danger { color: var(--tml-danger); }
|
|
140
|
+
& button.small,
|
|
141
|
+
& .button.small { min-height: 32px; padding: 4px 10px; font-size: 13px; }
|
|
142
|
+
& .actions,
|
|
143
|
+
& .form-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
|
|
144
|
+
& .actions form { display: inline; }
|
|
145
|
+
& .form-actions { padding-top: 12px; }
|
|
146
|
+
|
|
147
|
+
/* 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(--tml-border); border-radius: 999px; background: var(--tml-bg); }
|
|
154
|
+
& .status-switch button { min-height: 30px; padding: 0 12px; border: 0; border-radius: 999px; background: none; color: var(--tml-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(--tml-muted) 12%, transparent); color: var(--tml-text); }
|
|
156
|
+
& .status-switch button.current { background: var(--tml-surface); color: var(--tml-text); box-shadow: 0 1px 2px rgba(15, 23, 42, .12); cursor: default; }
|
|
157
|
+
& .status-switch button.current:hover { background: var(--tml-surface); }
|
|
158
|
+
& .panel-head .status-switch { margin-left: auto; }
|
|
159
|
+
|
|
160
|
+
& .card { overflow: hidden; border: 1px solid var(--tml-border); border-radius: 12px; background: var(--tml-surface); }
|
|
118
161
|
& .card.pad { padding: 16px; overflow: visible; }
|
|
119
162
|
& table { width: 100%; border-collapse: collapse; }
|
|
120
163
|
& th,
|
|
121
164
|
& td { padding: 10px 14px; text-align: left; vertical-align: top; }
|
|
122
165
|
& th { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--tml-muted); border-bottom: 1px solid var(--tml-border); }
|
|
123
166
|
& tr + tr td { border-top: 1px solid var(--tml-border); }
|
|
124
|
-
& .
|
|
167
|
+
& td.row-actions { white-space: nowrap; text-align: right; }
|
|
168
|
+
& td.row-actions form { display: inline-block; }
|
|
169
|
+
& td.row-actions form + form { margin-left: 6px; }
|
|
170
|
+
|
|
171
|
+
& .badge { display: inline-flex; padding: 2px 10px; border-radius: 999px; color: #fff; font-size: 12px; font-weight: 600; white-space: nowrap; }
|
|
172
|
+
& .badge.locale { border: 1px solid var(--tml-border); background: transparent; color: var(--tml-muted); }
|
|
173
|
+
& code.key { max-width: 100%; padding: 2px 7px; border-radius: 6px; background: var(--tml-code); font: 12.5px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; overflow-wrap: anywhere; }
|
|
174
|
+
|
|
175
|
+
& .flash,
|
|
176
|
+
& .errors { margin-bottom: 16px; padding: 10px 12px; border: 1px solid var(--tml-border); border-radius: 8px; background: var(--tml-surface); }
|
|
177
|
+
& .flash.notice { border-color: var(--tml-success); }
|
|
178
|
+
& .flash.alert,
|
|
179
|
+
& .errors { border-color: var(--tml-danger); }
|
|
180
|
+
& .errors ul { margin-bottom: 0; }
|
|
181
|
+
|
|
182
|
+
& dl { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 6px 16px; margin: 0; }
|
|
183
|
+
& dt { color: var(--tml-muted); }
|
|
184
|
+
& dd { margin: 0; overflow-wrap: anywhere; }
|
|
185
|
+
& .message { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
186
|
+
|
|
187
|
+
& .empty { padding: 48px 16px; color: var(--tml-muted); text-align: center; }
|
|
188
|
+
& .empty-state { max-width: 320px; margin: auto; padding: 24px; text-align: center; }
|
|
189
|
+
& .empty-state h2 { margin: 0 0 6px; font-size: 18px; }
|
|
190
|
+
& .empty-state p { margin: 0; }
|
|
191
|
+
|
|
192
|
+
/* The two-pane dashboard: the queue on the left, the selected record on the
|
|
193
|
+
right, each pane owning its own scroll. */
|
|
194
|
+
& .dashboard-shell { display: grid; flex: 1 1 auto; grid-template-columns: minmax(330px, 430px) 1fr; gap: 16px; min-height: 0; }
|
|
195
|
+
& .dashboard-sidebar { display: flex; min-width: 0; min-height: 0; flex-direction: column; overflow: hidden; border: 1px solid var(--tml-border); border-radius: 8px; background: var(--tml-surface); }
|
|
196
|
+
& .dashboard-sidebar .tabs { flex: 0 0 auto; margin: 12px 12px 0; }
|
|
197
|
+
& .dashboard-sidebar .filters { flex: 0 0 auto; }
|
|
198
|
+
|
|
199
|
+
/* One row shape for every gem: a topline of badges and a timestamp, the copy
|
|
200
|
+
that identifies the record, then a muted line of ids and authors. */
|
|
201
|
+
& .record-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
|
|
202
|
+
& .record-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; padding: 13px 14px; border-bottom: 1px solid var(--tml-border); color: var(--tml-text); }
|
|
203
|
+
& .record-row:hover { background: var(--tml-bg); text-decoration: none; }
|
|
204
|
+
& .record-row.active { background: color-mix(in srgb, var(--tml-accent) 9%, var(--tml-surface)); box-shadow: inset 3px 0 0 var(--tml-accent); }
|
|
205
|
+
& .record-main { display: flex; min-width: 0; flex-direction: column; gap: 5px; }
|
|
206
|
+
& .record-topline,
|
|
207
|
+
& .record-meta { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
208
|
+
& .record-name,
|
|
209
|
+
& .record-copy,
|
|
210
|
+
& .record-meta span,
|
|
211
|
+
/* A key is `overflow-wrap: anywhere` in prose; in a row it truncates. */
|
|
212
|
+
& .record-topline code.key,
|
|
213
|
+
& .record-meta code.key { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
214
|
+
& .record-name { font-weight: 600; }
|
|
215
|
+
& .record-copy { display: block; font-weight: 600; }
|
|
216
|
+
& .record-time { margin-left: auto; white-space: nowrap; }
|
|
217
|
+
& .record-side { display: flex; flex-direction: column; align-items: flex-end; justify-content: space-between; gap: 8px; padding-top: 1px; }
|
|
218
|
+
& .pager { display: flex; flex: 0 0 auto; justify-content: space-between; gap: 16px; min-height: 42px; margin: 0; padding: 10px 12px; border-top: 1px solid var(--tml-border); }
|
|
219
|
+
|
|
220
|
+
/* The selected record. `.detail-scroll` is the only part that scrolls, so the
|
|
221
|
+
panel heading and its actions stay put. */
|
|
222
|
+
& .dashboard-detail { display: flex; min-width: 0; min-height: 0; flex-direction: column; overflow: hidden; }
|
|
223
|
+
& .dashboard-detail .detail-panel { flex: 1 1 auto; min-height: 0; }
|
|
224
|
+
& .dashboard-detail .detail-scroll { flex: 1 1 auto; overflow-y: auto; padding-right: 2px; }
|
|
225
|
+
& .dashboard-detail .actions { margin-bottom: 0; }
|
|
226
|
+
& .detail-panel { display: flex; min-width: 0; flex-direction: column; }
|
|
227
|
+
/* The panel's own heading row: the title on the left, whatever acts on the
|
|
228
|
+
whole record on the right. */
|
|
229
|
+
& .panel-head { display: flex; flex: 0 0 auto; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
|
|
230
|
+
& .panel-head h1 { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 0; }
|
|
231
|
+
& .detail-scroll { display: flex; min-height: 0; flex-direction: column; gap: 12px; }
|
|
232
|
+
& .mobile-back { display: none; }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/* Page frame — the gem's own layout only. After the components, so it wins the
|
|
236
|
+
specificity ties against them. */
|
|
237
|
+
.tml-page { margin: 0; min-height: 100vh; background: var(--tml-bg); color: var(--tml-text); font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; letter-spacing: 0; }
|
|
238
|
+
/* An index page is the dashboard: it fills the viewport and never scrolls
|
|
239
|
+
itself, because the queue and the detail pane each scroll on their own. */
|
|
240
|
+
.tml-index, .tml-index .container { height: 100vh; height: 100dvh; }
|
|
241
|
+
.tml-index { overflow: hidden; }
|
|
242
|
+
.tml-index .container { display: flex; max-width: 1280px; flex-direction: column; padding-bottom: 16px; }
|
|
243
|
+
.tml-index .nav,
|
|
244
|
+
.tml-index .page-head { flex: 0 0 auto; }
|
|
245
|
+
/* Standalone show pages keep normal page scrolling. */
|
|
246
|
+
.tml-show { min-height: 100vh; overflow: auto; }
|
|
247
|
+
.tml-show .detail-panel { width: 100%; }
|
|
248
|
+
.tml-show .detail-scroll { overflow: visible; }
|
|
249
|
+
|
|
250
|
+
/* Narrow screens: one pane at a time, the queue until a record is picked.
|
|
251
|
+
Last in the file, so these overrides actually reach the rules above. */
|
|
252
|
+
@media (max-width: 760px) {
|
|
253
|
+
.tml-page { font-size: 14px; }
|
|
254
|
+
.tml-index, .tml-show { overflow-x: hidden; }
|
|
255
|
+
.tml-index { overflow-y: auto; }
|
|
256
|
+
.tml-index, .tml-index .container { min-height: 100vh; height: auto; }
|
|
257
|
+
.tml-index .container { padding-bottom: 10px; }
|
|
258
|
+
.tml-dashboard {
|
|
259
|
+
& .container { padding: 14px 10px 24px; }
|
|
260
|
+
& .page-head { margin-bottom: 12px; }
|
|
261
|
+
& .detail-panel,
|
|
262
|
+
& .detail-scroll,
|
|
263
|
+
& .card,
|
|
264
|
+
& dl,
|
|
265
|
+
& dd { min-width: 0; max-width: 100%; }
|
|
266
|
+
& .dashboard-detail .card { width: 100%; }
|
|
267
|
+
& .message,
|
|
268
|
+
& .lede,
|
|
269
|
+
& dd,
|
|
270
|
+
& .muted { overflow-wrap: anywhere; }
|
|
271
|
+
& .dashboard-shell { display: block; min-height: calc(100vh - 62px); }
|
|
272
|
+
& .dashboard-sidebar,
|
|
273
|
+
& .dashboard-detail { min-height: calc(100vh - 62px); }
|
|
274
|
+
& .dashboard-shell.has-selected .dashboard-sidebar { display: none; }
|
|
275
|
+
& .dashboard-shell:not(.has-selected) .dashboard-detail { display: none; }
|
|
276
|
+
& .record-list { overflow: visible; }
|
|
277
|
+
& .record-row { padding: 12px; }
|
|
278
|
+
& .record-meta { display: none; }
|
|
279
|
+
& .dashboard-detail .detail-panel { min-height: calc(100vh - 64px); }
|
|
280
|
+
& .mobile-back { display: block; flex: 0 0 auto; margin: 4px 0 10px; font-size: 13px; }
|
|
281
|
+
& .panel-head h1 { font-size: 18px; }
|
|
282
|
+
& .status-switch button { padding: 0 9px; font-size: 12px; }
|
|
283
|
+
& dl { grid-template-columns: 1fr; gap: 3px; }
|
|
284
|
+
& dd { margin-bottom: 8px; }
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/* ---------------------------------------------------------------------------
|
|
289
|
+
* GEM-SPECIFIC — testimonials only: star ratings, the video player, the best
|
|
290
|
+
* line editor, and the NPS score card with its meter and benchmarks.
|
|
291
|
+
* ------------------------------------------------------------------------- */
|
|
292
|
+
|
|
293
|
+
:root {
|
|
294
|
+
--tml-pending: var(--tml-warning);
|
|
295
|
+
--tml-approved: var(--tml-success);
|
|
296
|
+
--tml-archived: var(--tml-neutral);
|
|
297
|
+
--tml-star: #f59e0b;
|
|
298
|
+
--tml-promoter: var(--tml-success);
|
|
299
|
+
--tml-passive: var(--tml-warning);
|
|
300
|
+
--tml-detractor: var(--tml-danger);
|
|
301
|
+
/* The four bands an NPS number falls into: bad, ordinary, strong, rare. */
|
|
302
|
+
--tml-nps-needs-work: var(--tml-danger);
|
|
303
|
+
--tml-nps-good: var(--tml-warning);
|
|
304
|
+
--tml-nps-great: var(--tml-success);
|
|
305
|
+
--tml-nps-excellent: #0f766e;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
.tml-dashboard {
|
|
309
|
+
/* The shareable public form, sitting in the index heading row. */
|
|
310
|
+
& .share-link { font-size: 13px; font-weight: 400; }
|
|
311
|
+
|
|
125
312
|
& .badge.status-pending { background: var(--tml-pending); }
|
|
126
313
|
& .badge.status-approved { background: var(--tml-approved); }
|
|
127
314
|
& .badge.status-archived { background: var(--tml-archived); }
|
|
128
315
|
& .badge.nps-promoter { background: var(--tml-promoter); }
|
|
129
316
|
& .badge.nps-passive { background: var(--tml-passive); }
|
|
130
317
|
& .badge.nps-detractor { background: var(--tml-detractor); }
|
|
131
|
-
& .badge.locale { background: transparent; color: var(--tml-muted); border: 1px solid var(--tml-border); }
|
|
132
318
|
& .stars { color: var(--tml-star); letter-spacing: 1px; white-space: nowrap; }
|
|
133
319
|
& .stars .off { color: var(--tml-border); }
|
|
134
|
-
|
|
135
|
-
& .pager { margin-top: 16px; display: flex; gap: 16px; }
|
|
136
|
-
& .actions { display: flex; gap: 8px; flex-wrap: wrap; margin: 16px 0; }
|
|
137
|
-
& .actions form { display: inline; }
|
|
138
|
-
& td.row-actions { white-space: nowrap; text-align: right; }
|
|
139
|
-
& td.row-actions form { display: inline-block; }
|
|
140
|
-
& td.row-actions form + form { margin-left: 6px; }
|
|
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; }
|
|
143
|
-
& button.primary { background: var(--tml-accent); border-color: var(--tml-accent); color: var(--tml-accent-text); }
|
|
144
|
-
& button.danger { color: var(--tml-danger); }
|
|
145
|
-
& button.small { padding: 3px 10px; font-size: 13px; }
|
|
146
|
-
& .message { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
147
|
-
& dl { display: grid; grid-template-columns: max-content 1fr; gap: 6px 16px; margin: 0; }
|
|
148
|
-
& dt { color: var(--tml-muted); }
|
|
149
|
-
& dd { margin: 0; overflow-wrap: anywhere; }
|
|
320
|
+
|
|
150
321
|
& video.playback { width: 100%; max-width: 480px; border-radius: 10px; background: #000; }
|
|
151
322
|
& img.avatar { width: 48px; height: 48px; border-radius: 50%; object-fit: cover; }
|
|
152
|
-
|
|
153
|
-
& .
|
|
154
|
-
& .
|
|
323
|
+
|
|
324
|
+
& .best-line-form { margin-top: 14px; }
|
|
325
|
+
& .best-line-form label { font-weight: 600; }
|
|
326
|
+
& .best-line-hint { margin: 2px 0 6px; }
|
|
327
|
+
& .best-line-save { margin-top: 8px; }
|
|
328
|
+
|
|
329
|
+
/* NPS summary row. */
|
|
155
330
|
& .stat-row { display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap; }
|
|
156
|
-
& .stat { flex: 1; min-width: 140px;
|
|
331
|
+
& .stat { flex: 1; min-width: 140px; padding: 14px 16px; border: 1px solid var(--tml-border); border-radius: 12px; background: var(--tml-surface); }
|
|
157
332
|
& .stat b { display: block; font-size: 26px; }
|
|
158
333
|
& .stat span { color: var(--tml-muted); font-size: 13px; }
|
|
159
|
-
/*
|
|
334
|
+
/* The score card: the number, the band it falls in, and where it sits on the
|
|
335
|
+
−100..100 scale — so nobody has to google whether 50 is any good. */
|
|
160
336
|
& .stat-nps { flex: 2.4; min-width: 240px; }
|
|
161
337
|
& .stat-headline { display: flex; align-items: center; gap: 10px; }
|
|
162
338
|
& .stat-headline b { display: inline-block; }
|
|
@@ -173,7 +349,8 @@
|
|
|
173
349
|
& .nps-seg.nps-band-good { flex: 30; background: var(--tml-nps-good); }
|
|
174
350
|
& .nps-seg.nps-band-great { flex: 40; background: var(--tml-nps-great); }
|
|
175
351
|
& .nps-seg.nps-band-excellent { flex: 30; background: var(--tml-nps-excellent); }
|
|
176
|
-
/* Hidden until dashboard.js reads data-nps-marker and positions it, so a score
|
|
352
|
+
/* Hidden until dashboard.js reads data-nps-marker and positions it, so a score
|
|
353
|
+
of 50 never renders as if it were −100. */
|
|
177
354
|
& .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); }
|
|
178
355
|
& .nps-marker.placed { display: block; }
|
|
179
356
|
& .nps-meter-ticks { position: relative; height: 14px; margin-top: 3px; color: var(--tml-muted); font-size: 11px; }
|
|
@@ -185,7 +362,7 @@
|
|
|
185
362
|
& .nps-meter-ticks .tick-max { left: 100%; transform: translateX(-100%); }
|
|
186
363
|
& .nps-sample { margin: 8px 0 0; font-size: 12px; }
|
|
187
364
|
/* The formula and the benchmarks, tucked behind a disclosure. */
|
|
188
|
-
& .nps-guide { flex: 0 0 auto; margin-bottom: 14px;
|
|
365
|
+
& .nps-guide { flex: 0 0 auto; margin-bottom: 14px; border: 1px solid var(--tml-border); border-radius: 12px; background: var(--tml-surface); }
|
|
189
366
|
& .nps-guide summary { padding: 10px 16px; color: var(--tml-muted); font-size: 13px; cursor: pointer; }
|
|
190
367
|
& .nps-guide summary:hover { color: var(--tml-text); }
|
|
191
368
|
& .nps-guide[open] summary { border-bottom: 1px solid var(--tml-border); }
|
|
@@ -194,65 +371,24 @@
|
|
|
194
371
|
& .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; }
|
|
195
372
|
& .nps-bands li { display: contents; }
|
|
196
373
|
& .nps-bands b { font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
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; }
|
|
198
|
-
& .best-line-hint { margin: 2px 0 6px; }
|
|
199
|
-
& .best-line-save { margin-top: 8px; }
|
|
200
|
-
& .panel-head { flex: 0 0 auto; }
|
|
201
|
-
& .panel-head h1 { margin-bottom: 12px; }
|
|
202
|
-
& .detail-scroll { min-height: 0; display: flex; flex-direction: column; gap: 12px; }
|
|
203
|
-
& .testimonial-panel,
|
|
204
|
-
& .nps-panel { min-width: 0; display: flex; flex-direction: column; }
|
|
205
|
-
& .testimonial-panel .actions { flex: 0 0 auto; }
|
|
206
|
-
& .testimonial-panel .detail-scroll > .actions { margin: 0; }
|
|
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); }
|
|
214
|
-
& .dashboard-sidebar .tabs .count { margin-left: 0; background: color-mix(in srgb, var(--tml-muted) 14%, transparent); }
|
|
215
|
-
& .dashboard-sidebar .tabs a.active .count { background: 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); }
|
|
217
|
-
& .dashboard-sidebar .filters select { grid-column: 1 / -1; width: 100%; }
|
|
218
|
-
& .dashboard-sidebar .filters input[type=search] { min-width: 0; max-width: none; }
|
|
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); }
|
|
221
|
-
& .record-row:hover { text-decoration: none; background: var(--tml-bg); }
|
|
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); }
|
|
223
|
-
& .record-main { min-width: 0; display: flex; flex-direction: column; gap: 5px; }
|
|
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; }
|
|
228
|
-
& .record-copy { display: block; font-weight: 600; }
|
|
229
|
-
& .record-time { margin-left: auto; white-space: nowrap; }
|
|
230
|
-
& .record-side { display: flex; flex-direction: column; align-items: flex-end; justify-content: space-between; gap: 8px; padding-top: 1px; }
|
|
231
|
-
& .dashboard-sidebar .pager { flex: 0 0 auto; margin: 0; padding: 10px 12px; border-top: 1px solid var(--tml-border); }
|
|
232
|
-
& .dashboard-detail { min-width: 0; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
|
|
233
|
-
& .dashboard-detail .testimonial-panel,
|
|
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; }
|
|
236
|
-
& .dashboard-detail .actions { margin-bottom: 0; }
|
|
237
|
-
& .empty-state { margin: auto; max-width: 320px; padding: 24px; text-align: center; }
|
|
238
|
-
& .empty-state h2 { margin: 0 0 6px; font-size: 18px; }
|
|
239
|
-
& .empty-state p { margin: 0; }
|
|
240
|
-
& .mobile-back { display: none; }
|
|
241
374
|
}
|
|
242
375
|
|
|
243
|
-
/*
|
|
244
|
-
|
|
245
|
-
.tml-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
.
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
.
|
|
252
|
-
.
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
.
|
|
258
|
-
.
|
|
376
|
+
/* The NPS page carries `tml-index tml-nps-index`: the shared index frame plus
|
|
377
|
+
the summary row and the guide, which sit above the two panes. */
|
|
378
|
+
.tml-nps-index .stat-row { flex: 0 0 auto; margin-bottom: 14px; }
|
|
379
|
+
|
|
380
|
+
@media (max-width: 760px) {
|
|
381
|
+
.tml-dashboard {
|
|
382
|
+
& .best-line-form,
|
|
383
|
+
& .best-line-form textarea { min-width: 0; max-width: 100%; }
|
|
384
|
+
& .best-line-hint { overflow-wrap: anywhere; }
|
|
385
|
+
& .stat-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
|
|
386
|
+
& .stat { min-width: 0; padding: 10px 12px; }
|
|
387
|
+
& .stat b { font-size: 22px; }
|
|
388
|
+
& .stat-nps { grid-column: 1 / -1; }
|
|
389
|
+
& .nps-bands { grid-template-columns: max-content 1fr; }
|
|
390
|
+
& .nps-bands b { grid-column: 2; }
|
|
391
|
+
& .nps-bands li > .muted { grid-column: 1 / -1; }
|
|
392
|
+
& .share-link { flex: 1 0 100%; text-align: center; }
|
|
393
|
+
}
|
|
394
|
+
}
|
data/lib/testimonials/seeds.rb
CHANGED
|
@@ -9,8 +9,10 @@ module Testimonials
|
|
|
9
9
|
{
|
|
10
10
|
seed_id: 'approved-founder',
|
|
11
11
|
kind: 'text',
|
|
12
|
-
body: 'We
|
|
13
|
-
|
|
12
|
+
body: 'We call testimonial_prompt! after a customer completes onboarding, when the value is still ' \
|
|
13
|
+
'fresh. The prompt feels earned, and the quotes are much more specific than the ones we used ' \
|
|
14
|
+
'to request by email.',
|
|
15
|
+
best_line: 'Ask after a real success moment and the quotes become specific.',
|
|
14
16
|
rating: 5,
|
|
15
17
|
status: 'approved',
|
|
16
18
|
featured: true,
|
|
@@ -18,7 +20,7 @@ module Testimonials
|
|
|
18
20
|
consent_text: 'You can use my testimonial publicly in your marketing and sales.',
|
|
19
21
|
name: 'Maya Chen',
|
|
20
22
|
email: 'maya@example.com',
|
|
21
|
-
title_company: 'Founder,
|
|
23
|
+
title_company: 'Founder, Demo CRM',
|
|
22
24
|
source: 'widget',
|
|
23
25
|
page_url: '/onboarding/complete',
|
|
24
26
|
locale: 'en'
|
|
@@ -26,7 +28,9 @@ module Testimonials
|
|
|
26
28
|
{
|
|
27
29
|
seed_id: 'pending-operator',
|
|
28
30
|
kind: 'text',
|
|
29
|
-
body: '
|
|
31
|
+
body: 'This response has public consent, but it is still pending. That separation gives our team a ' \
|
|
32
|
+
'chance to verify the quote, choose a best line, and approve it before anything appears on the ' \
|
|
33
|
+
'wall of love.',
|
|
30
34
|
rating: 4,
|
|
31
35
|
status: 'pending',
|
|
32
36
|
featured: false,
|
|
@@ -34,7 +38,7 @@ module Testimonials
|
|
|
34
38
|
consent_text: 'You can use my testimonial publicly in your marketing and sales.',
|
|
35
39
|
name: 'Jon Bell',
|
|
36
40
|
email: 'jon@example.com',
|
|
37
|
-
title_company: 'Operations Lead,
|
|
41
|
+
title_company: 'Operations Lead, Example Co',
|
|
38
42
|
source: 'nps',
|
|
39
43
|
page_url: '/reports',
|
|
40
44
|
locale: 'en'
|
|
@@ -42,7 +46,8 @@ module Testimonials
|
|
|
42
46
|
{
|
|
43
47
|
seed_id: 'private-archive',
|
|
44
48
|
kind: 'text',
|
|
45
|
-
body: '
|
|
49
|
+
body: 'I am happy for the team to keep this feedback internally, but I did not consent to public ' \
|
|
50
|
+
'use. Archiving it keeps the learning without making it publishable.',
|
|
46
51
|
rating: 3,
|
|
47
52
|
status: 'archived',
|
|
48
53
|
featured: false,
|
|
@@ -58,8 +63,10 @@ module Testimonials
|
|
|
58
63
|
{
|
|
59
64
|
seed_id: 'approved-video',
|
|
60
65
|
kind: 'video',
|
|
61
|
-
body: '
|
|
62
|
-
|
|
66
|
+
body: 'The attached clip is intentionally generic demo media. It proves the upload, review, and ' \
|
|
67
|
+
'protected playback path; replace it with a real customer recording before using this record ' \
|
|
68
|
+
'anywhere public.',
|
|
69
|
+
best_line: 'Video follows the same consent and approval workflow as text.',
|
|
63
70
|
rating: 5,
|
|
64
71
|
status: 'approved',
|
|
65
72
|
featured: true,
|
|
@@ -67,7 +74,7 @@ module Testimonials
|
|
|
67
74
|
consent_text: 'You can use my testimonial publicly in your marketing and sales.',
|
|
68
75
|
name: 'Alex Rivera',
|
|
69
76
|
email: 'alex@example.com',
|
|
70
|
-
title_company: 'Head of Product,
|
|
77
|
+
title_company: 'Head of Product, Demo Video Co',
|
|
71
78
|
source: 'page',
|
|
72
79
|
page_url: '/testimonials/new',
|
|
73
80
|
locale: 'en'
|
|
@@ -76,9 +83,12 @@ module Testimonials
|
|
|
76
83
|
|
|
77
84
|
NPS_RESPONSES = [
|
|
78
85
|
{ seed_id: 'promoter', score: 10,
|
|
79
|
-
comment: '
|
|
80
|
-
{ seed_id: 'passive', score: 8,
|
|
81
|
-
|
|
86
|
+
comment: 'A 9 or 10 is a promoter. One promoter contributes positively to the NPS score.' },
|
|
87
|
+
{ seed_id: 'passive', score: 8,
|
|
88
|
+
comment: 'A 7 or 8 is passive. Keep the feedback, but this response does not move the NPS score.' },
|
|
89
|
+
{ seed_id: 'detractor', score: 4,
|
|
90
|
+
comment: 'A score from 0 to 6 is a detractor. With one promoter and one detractor, this demo board ' \
|
|
91
|
+
'totals 0 NPS.' }
|
|
82
92
|
].freeze
|
|
83
93
|
|
|
84
94
|
PROMPT_EVENTS = [
|
data/lib/testimonials/version.rb
CHANGED
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.
|
|
4
|
+
version: 0.9.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yaroslav Shmarov
|
|
@@ -152,7 +152,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
152
152
|
- !ruby/object:Gem::Version
|
|
153
153
|
version: '0'
|
|
154
154
|
requirements: []
|
|
155
|
-
rubygems_version:
|
|
155
|
+
rubygems_version: 4.0.10
|
|
156
156
|
specification_version: 4
|
|
157
157
|
summary: 'Testimonials, reviews and NPS for Rails: in-app collection widget, public
|
|
158
158
|
collection page, triage dashboard, and a read API.'
|