product_tours 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/AGENTS.md +8 -0
- data/CHANGELOG.md +25 -0
- data/README.md +18 -11
- data/app/views/product_tours/posts/_post_panel.html.erb +1 -1
- data/lib/product_tours/dashboard.css +231 -103
- data/lib/product_tours/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ea098df5cd9d109566f8f7561a53e182932258742abd06b427596f2b8deb0574
|
|
4
|
+
data.tar.gz: f7607d43d7cc8eee2174c9232279cf192f6f74d446b1f4c7da246be752f4d2fe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d95991dc7543548549f11544d9f7c898b31a09fae6a06bd23c78e81df86937a2a5edca0c8127f6f7e51a10cd50b2c235fd19e87aa1fedaa8e54bcc8f66872d69
|
|
7
|
+
data.tar.gz: 1f1cd6f01c210a157d67b1b736a9a0540be356aabc875eb4e0daac5e91e3f6864dd9995413b324a90400ac35d9a5cc83bc12792d0d832111bf43c147e66c5273
|
data/AGENTS.md
CHANGED
|
@@ -107,6 +107,7 @@ There are five options. That is the whole surface.
|
|
|
107
107
|
| --- | --- | --- |
|
|
108
108
|
| `authorize_admin` | development only | **Who can read and edit tutorials. Set before deploying.** |
|
|
109
109
|
| `enabled` | everyone | Per-request gate for the widget and its endpoints |
|
|
110
|
+
| `base_controller_class` | `ActionController::Base` | Controller the dashboard inherits. Name your admin's and it adopts that layout, helpers, authentication and request context. Public endpoints never inherit it. |
|
|
110
111
|
| `admin_layout` | `product_tours/application` | Render the dashboard inside your admin shell |
|
|
111
112
|
| `mount_path` | `"/product_tours"` | Keep in sync with `mount_product_tours at:` |
|
|
112
113
|
| `storage_service` | app default | Active Storage service for uploaded video (a `storage.yml` key) |
|
|
@@ -117,6 +118,8 @@ There are five options. That is the whole surface.
|
|
|
117
118
|
|
|
118
119
|
| Symptom | Cause |
|
|
119
120
|
| --- | --- |
|
|
121
|
+
| `NameError` for one of your own helpers in the dashboard | `isolate_namespace` scopes `helper` to the engine. Use `config.base_controller_class` so the dashboard inherits your helpers, rather than `admin_layout` alone. |
|
|
122
|
+
| `NotNullViolation` attaching a file on a uuid-keyed app | The tables were generated bigint. Set `primary_key_type` in `config.generators` before installing, or migrate them to uuid. |
|
|
120
123
|
| The trigger button does nothing | No tutorial with that key, or it is still a draft, or `product_tours_tag` is missing from the layout. Subscribe to `product_tours.unresolved_trigger` to see which |
|
|
121
124
|
| `/product_tours` returns 403 "Set ProductTours.config.authorize_admin to grant access" | Exactly what it says: still at the development-only default |
|
|
122
125
|
| Key rejected on save | It must match `/\A[a-z0-9]+(?:[._-][a-z0-9]+)*\z/` — no capitals, no spaces |
|
|
@@ -127,6 +130,10 @@ There are five options. That is the whole surface.
|
|
|
127
130
|
|
|
128
131
|
---
|
|
129
132
|
|
|
133
|
+
## One family
|
|
134
|
+
|
|
135
|
+
`testimonials`, `ideasbugs`, `livechat`, `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.
|
|
136
|
+
|
|
130
137
|
## Working on the gem itself
|
|
131
138
|
|
|
132
139
|
```bash
|
|
@@ -145,5 +152,6 @@ Conventions this codebase holds to — follow them rather than the first thing t
|
|
|
145
152
|
- **Uploaded media streams through the engine's gate**, never a public blob URL.
|
|
146
153
|
- **The CSP patch is additive.** It appends to existing sources and drops `'none'` rather than replacing a host's policy — do not let it start overwriting directives.
|
|
147
154
|
- **The dummy app pins `config.active_job.queue_adapter = :test`.** Do not remove it or let it drift back to the `:async` default. Attaching a video enqueues Active Storage's analysis job, and `:async` runs it on a background thread that checks out its own connection — writes no test transaction covers, landing in the middle of whatever runs next. That is a suite that fails order-dependently in a test which never created a row, and it is miserable to trace back.
|
|
155
|
+
- **`lib/product_tours/dashboard.css` is half shared.** Everything above the `GEM-SPECIFIC` banner is the design system all five gems in the family ship — the same tokens, the same `.page-head`/`.tabs`/`.filters`/`.card`/`.badge`/`button`, the same `.dashboard-shell` + `.record-row` + `.detail-panel` two-pane dashboard — identical in every repo apart from the `pt` 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.
|
|
148
156
|
- Every user-facing change bumps `lib/product_tours/version.rb` and adds a `CHANGELOG.md` entry (Keep a Changelog format) that says what it costs, not only what it adds.
|
|
149
157
|
- Commit messages are prose that explains the tradeoff — read `git log` before writing one.
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
- **One design system across the family.** The stylesheet now opens with a
|
|
6
|
+
shared core — the colour tokens, `.page-head`, `.tabs`, `.filters`, `.card`,
|
|
7
|
+
`.badge`, buttons and form controls, and the `.dashboard-shell` +
|
|
8
|
+
`.record-row` + `.detail-panel` two-pane dashboard — identical in all five
|
|
9
|
+
gems of the family, apart from the `--pt-` prefix. The five had drifted:
|
|
10
|
+
sidebars between 380px and 430px, reading columns between 860px and 1020px,
|
|
11
|
+
two different tab styles and three different button styles. The sidebar is
|
|
12
|
+
now 330–430px everywhere, a reading page 1020px, a dashboard 1280px.
|
|
13
|
+
Everything below the `GEM-SPECIFIC` banner is what only this gem has.
|
|
14
|
+
- **The dashboard markup uses the shared class names.** `.post-panel` is
|
|
15
|
+
`.detail-panel`; everything else already used the shared vocabulary, which
|
|
16
|
+
this gem's dashboard had the most of. If you styled or scripted
|
|
17
|
+
`.post-panel` in a host app, that is the breaking change — no configuration,
|
|
18
|
+
route or database change is involved.
|
|
19
|
+
- **The narrow-screen rules work again.** The `max-width: 760px` block sat above
|
|
20
|
+
the component rules it means to override, and CSS nesting adds no specificity,
|
|
21
|
+
so the desktop grid won every tie: showing one pane at a time on a phone had
|
|
22
|
+
quietly stopped working. The media query moved to the end of the file.
|
|
23
|
+
- Smaller fixes that came with the shared core: a submit input is styled as a
|
|
24
|
+
button rather than a full-width field, the filter row keeps its search box and
|
|
25
|
+
button on one line, and a `code.key` truncates inside a list row instead of
|
|
26
|
+
wrapping over three lines.
|
|
27
|
+
|
|
3
28
|
## 0.2.0
|
|
4
29
|
|
|
5
30
|
- **`config.admin_layout` now works on its own.** The dashboard's stylesheet and
|
data/README.md
CHANGED
|
@@ -347,17 +347,24 @@ CI runs Rails 7.1 / 7.2 / 8.0 / 8.1 against Ruby 3.2 / 3.3 / 3.4.
|
|
|
347
347
|
Bug reports and pull requests are welcome. The most useful report is a real
|
|
348
348
|
Rails app and the exact point where installation or authoring felt confusing.
|
|
349
349
|
|
|
350
|
-
##
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
350
|
+
## One family
|
|
351
|
+
|
|
352
|
+
Five Rails engines built on the same backbone, so adopting a second one is
|
|
353
|
+
mostly muscle memory:
|
|
354
|
+
|
|
355
|
+
| Gem | What it does |
|
|
356
|
+
| --- | --- |
|
|
357
|
+
| [testimonials](https://github.com/yshmarov/testimonials) | Testimonials, reviews and NPS — text and video, collected in your own app |
|
|
358
|
+
| [ideasbugs](https://github.com/yshmarov/ideasbugs) | In-app bug reports and feature requests, with a triage queue |
|
|
359
|
+
| [livechat](https://github.com/yshmarov/livechat) | Live chat between your visitors and your agents, self-hosted |
|
|
360
|
+
| **product_tours** *(this gem)* | Product tours and video tutorials, shown in-app at the right moment |
|
|
361
|
+
| [i18n_proofreading](https://github.com/yshmarov/i18n_proofreading) | In-context translation fixes suggested by your own users |
|
|
362
|
+
|
|
363
|
+
They share the install shape (`generate <gem>:install`, mount, one initializer),
|
|
364
|
+
the same host hooks (`base_controller_class` to inherit your admin's controller,
|
|
365
|
+
`admin_layout` for just the shell), one dashboard design system — the same
|
|
366
|
+
two-pane layout, colour tokens and components in all five, scoped so it cannot
|
|
367
|
+
touch your own CSS — and migrations that follow your app's `primary_key_type`.
|
|
361
368
|
|
|
362
369
|
## License
|
|
363
370
|
|
|
@@ -4,30 +4,46 @@
|
|
|
4
4
|
*
|
|
5
5
|
* :root custom properties, all `--pt-` prefixed so they can
|
|
6
6
|
* neither overwrite a host's nor be overwritten by them.
|
|
7
|
-
* .pt-page
|
|
7
|
+
* .pt-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
|
-
* .pt-dashboard
|
|
10
|
+
* .pt-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
34
|
--pt-bg: #f6f7f9;
|
|
21
|
-
--pt-surface: #
|
|
35
|
+
--pt-surface: #fff;
|
|
22
36
|
--pt-text: #1c2024;
|
|
23
37
|
--pt-muted: #6b7280;
|
|
24
38
|
--pt-border: #e5e7eb;
|
|
25
39
|
--pt-accent: #2563eb;
|
|
26
|
-
--pt-accent-text: #
|
|
27
|
-
--pt-draft: #6b7280;
|
|
28
|
-
--pt-published: #16a34a;
|
|
29
|
-
--pt-danger: #dc2626;
|
|
40
|
+
--pt-accent-text: #fff;
|
|
30
41
|
--pt-code: #f0f1f3;
|
|
42
|
+
/* The four semantic states every gem in the family badges something with. */
|
|
43
|
+
--pt-success: #16a34a;
|
|
44
|
+
--pt-warning: #d97706;
|
|
45
|
+
--pt-danger: #dc2626;
|
|
46
|
+
--pt-neutral: #6b7280;
|
|
31
47
|
}
|
|
32
48
|
@media (prefers-color-scheme: dark) {
|
|
33
49
|
:root {
|
|
@@ -38,53 +54,9 @@
|
|
|
38
54
|
--pt-border: #2a313a;
|
|
39
55
|
--pt-accent: #3b82f6;
|
|
40
56
|
--pt-code: #232a33;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
@media (max-width: 760px) {
|
|
44
|
-
.pt-page { overflow-x: hidden; font-size: 14px; }
|
|
45
|
-
.pt-index { overflow-y: auto; overflow-x: hidden; }
|
|
46
|
-
.pt-index, .pt-index .container { min-height: 100vh; height: auto; }
|
|
47
|
-
.pt-index .container { padding-bottom: 10px; }
|
|
48
|
-
.pt-dashboard {
|
|
49
|
-
& .container { padding: 14px 10px 24px; }
|
|
50
|
-
& .page-head { align-items: center; }
|
|
51
|
-
& .page-head .button { min-height: 36px; }
|
|
52
|
-
& .dashboard-shell { display: block; min-height: calc(100vh - 58px); }
|
|
53
|
-
& .dashboard-sidebar,
|
|
54
|
-
& .dashboard-detail { min-height: calc(100vh - 58px); }
|
|
55
|
-
& .dashboard-shell.has-selected .dashboard-sidebar { display: none; }
|
|
56
|
-
& .dashboard-shell:not(.has-selected) .dashboard-detail { display: none; }
|
|
57
|
-
& .record-list { overflow: visible; }
|
|
58
|
-
& .record-row { padding: 12px; }
|
|
59
|
-
& .record-meta { display: none; }
|
|
60
|
-
& .dashboard-detail .post-panel { min-height: calc(100vh - 64px); }
|
|
61
|
-
& .mobile-back { display: block; flex: 0 0 auto; margin: 4px 0 10px; font-size: 13px; }
|
|
62
|
-
& .post-panel,
|
|
63
|
-
& .detail-scroll,
|
|
64
|
-
& .card,
|
|
65
|
-
& dl,
|
|
66
|
-
& dd { min-width: 0; max-width: 100%; }
|
|
67
|
-
& .dashboard-detail .card { width: 100%; }
|
|
68
|
-
& dl { grid-template-columns: 1fr; gap: 3px; }
|
|
69
|
-
& dd { margin-bottom: 8px; }
|
|
70
|
-
& .action-picker,
|
|
71
|
-
& .video-source-picker { grid-template-columns: 1fr; }
|
|
72
|
-
& .form-card { padding: 0; }
|
|
73
57
|
}
|
|
74
58
|
}
|
|
75
59
|
|
|
76
|
-
/* Page frame — the gem's own layout only. */
|
|
77
|
-
.pt-page { font-family: system-ui, -apple-system, "Segoe UI", sans-serif; letter-spacing: 0; }
|
|
78
|
-
.pt-page { margin: 0; min-height: 100vh; background: var(--pt-bg); color: var(--pt-text); font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; }
|
|
79
|
-
.pt-index, .pt-index .container { height: 100vh; height: 100dvh; }
|
|
80
|
-
.pt-index { overflow: hidden; }
|
|
81
|
-
.pt-index .container { display: flex; max-width: 1280px; flex-direction: column; padding-bottom: 16px; }
|
|
82
|
-
.pt-index .page-head { flex: 0 0 auto; }
|
|
83
|
-
.pt-show { min-height: 100vh; overflow: auto; }
|
|
84
|
-
.pt-show .post-panel { width: 100%; }
|
|
85
|
-
.pt-show .detail-scroll { overflow: visible; }
|
|
86
|
-
.pt-form-page .container { max-width: 820px; }
|
|
87
|
-
|
|
88
60
|
/* Dashboard components — any layout. */
|
|
89
61
|
.pt-dashboard {
|
|
90
62
|
/* A scoping device, not a layout one: without this the wrapper would
|
|
@@ -95,58 +67,124 @@
|
|
|
95
67
|
& a { color: var(--pt-accent); text-decoration: none; }
|
|
96
68
|
& a:hover { text-decoration: underline; }
|
|
97
69
|
& :focus-visible { outline: 2px solid var(--pt-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. */
|
|
98
73
|
& .container { max-width: 1020px; margin: 0 auto; padding: 24px 16px 64px; }
|
|
99
74
|
& h1 { margin: 0 0 16px; font-size: 22px; }
|
|
100
75
|
& h2 { margin: 0 0 12px; font-size: 16px; }
|
|
101
|
-
& .page-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
|
|
76
|
+
& .page-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; margin-bottom: 14px; }
|
|
102
77
|
& .page-head h1 { margin: 0; }
|
|
78
|
+
& .lede { margin: 4px 0 0; color: var(--pt-muted); }
|
|
79
|
+
& .breadcrumb { margin: 0 0 12px; font-size: 13px; }
|
|
103
80
|
& .muted { color: var(--pt-muted); font-size: 13px; }
|
|
104
|
-
&
|
|
105
|
-
& .
|
|
106
|
-
&
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
&
|
|
111
|
-
& .
|
|
112
|
-
&
|
|
113
|
-
& .
|
|
81
|
+
& .case-id { color: var(--pt-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; }
|
|
88
|
+
& .nav a { padding: 6px 16px; border: 1px solid var(--pt-border); border-radius: 999px; background: var(--pt-surface); color: var(--pt-muted); font-weight: 600; }
|
|
89
|
+
& .nav a:hover { text-decoration: none; color: var(--pt-text); }
|
|
90
|
+
& .nav a.active { border-color: var(--pt-accent); background: var(--pt-accent); color: var(--pt-accent-text); }
|
|
91
|
+
& .nav a.active:hover { color: var(--pt-accent-text); }
|
|
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. */
|
|
114
95
|
& .tabs { display: flex; gap: 6px; }
|
|
115
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(--pt-muted); font-weight: 600; }
|
|
116
97
|
& .tabs a:hover { background: var(--pt-bg); color: var(--pt-text); text-decoration: none; }
|
|
117
98
|
& .tabs a.active { color: var(--pt-text); }
|
|
118
99
|
& .tabs a.active::after { content: ""; position: absolute; right: 20px; bottom: 0; left: 20px; height: 2px; border-radius: 999px; background: var(--pt-accent); }
|
|
119
|
-
& .count { min-width: 20px; padding: 0 6px; border-radius: 999px; background: color-mix(in srgb, var(--pt-muted) 14%, transparent); font-size: 12px; text-align: center; }
|
|
120
|
-
& .
|
|
121
|
-
|
|
100
|
+
& .count { display: inline-block; min-width: 20px; padding: 0 6px; border-radius: 999px; background: color-mix(in srgb, var(--pt-muted) 14%, transparent); font-size: 12px; text-align: center; font-variant-numeric: tabular-nums; }
|
|
101
|
+
& .tabs a.active .count { background: var(--pt-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(--pt-border); }
|
|
106
|
+
& .filters label { flex: 1 1 100%; color: var(--pt-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. */
|
|
122
115
|
& input,
|
|
123
116
|
& select,
|
|
124
117
|
& textarea { width: 100%; min-height: 38px; padding: 7px 9px; border: 1px solid var(--pt-border); border-radius: 8px; background: var(--pt-surface); color: var(--pt-text); font: inherit; font-size: 16px; letter-spacing: 0; }
|
|
125
118
|
& textarea { resize: vertical; }
|
|
119
|
+
& input[type="checkbox"],
|
|
120
|
+
& input[type="radio"] { width: auto; min-width: 0; min-height: 0; }
|
|
126
121
|
& input:focus,
|
|
127
122
|
& select:focus,
|
|
128
123
|
& textarea:focus { border-color: var(--pt-accent); outline: 2px solid color-mix(in srgb, var(--pt-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(--pt-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(--pt-border); border-radius: 8px; background: var(--pt-surface); color: var(--pt-text); font: inherit; cursor: pointer; }
|
|
132
|
+
& button:hover,
|
|
133
|
+
& .button:hover,
|
|
134
|
+
& input[type="submit"]:hover { border-color: var(--pt-muted); text-decoration: none; }
|
|
135
|
+
& button.primary,
|
|
136
|
+
& .button.primary,
|
|
137
|
+
& input[type="submit"].primary { border-color: var(--pt-accent); background: var(--pt-accent); color: var(--pt-accent-text); }
|
|
138
|
+
& button.danger,
|
|
139
|
+
& .button.danger { color: var(--pt-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
|
+
|
|
129
147
|
& .card { overflow: hidden; border: 1px solid var(--pt-border); border-radius: 12px; background: var(--pt-surface); }
|
|
130
148
|
& .card.pad { padding: 16px; overflow: visible; }
|
|
131
|
-
&
|
|
132
|
-
&
|
|
133
|
-
&
|
|
149
|
+
& table { width: 100%; border-collapse: collapse; }
|
|
150
|
+
& th,
|
|
151
|
+
& td { padding: 10px 14px; text-align: left; vertical-align: top; }
|
|
152
|
+
& th { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--pt-muted); border-bottom: 1px solid var(--pt-border); }
|
|
153
|
+
& tr + tr td { border-top: 1px solid var(--pt-border); }
|
|
154
|
+
& td.row-actions { white-space: nowrap; text-align: right; }
|
|
155
|
+
& td.row-actions form { display: inline-block; }
|
|
156
|
+
& td.row-actions form + form { margin-left: 6px; }
|
|
157
|
+
|
|
158
|
+
& .badge { display: inline-flex; padding: 2px 10px; border-radius: 999px; color: #fff; font-size: 12px; font-weight: 600; white-space: nowrap; }
|
|
134
159
|
& .badge.locale { border: 1px solid var(--pt-border); background: transparent; color: var(--pt-muted); }
|
|
135
160
|
& code.key { max-width: 100%; padding: 2px 7px; border-radius: 6px; background: var(--pt-code); font: 12.5px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; overflow-wrap: anywhere; }
|
|
161
|
+
|
|
136
162
|
& .flash,
|
|
137
163
|
& .errors { margin-bottom: 16px; padding: 10px 12px; border: 1px solid var(--pt-border); border-radius: 8px; background: var(--pt-surface); }
|
|
138
|
-
& .flash.notice { border-color: var(--pt-
|
|
164
|
+
& .flash.notice { border-color: var(--pt-success); }
|
|
139
165
|
& .flash.alert,
|
|
140
166
|
& .errors { border-color: var(--pt-danger); }
|
|
141
167
|
& .errors ul { margin-bottom: 0; }
|
|
168
|
+
|
|
169
|
+
& dl { display: grid; grid-template-columns: max-content minmax(0, 1fr); gap: 6px 16px; margin: 0; }
|
|
170
|
+
& dt { color: var(--pt-muted); }
|
|
171
|
+
& dd { margin: 0; overflow-wrap: anywhere; }
|
|
172
|
+
& .message { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
173
|
+
|
|
142
174
|
& .empty { padding: 48px 16px; color: var(--pt-muted); text-align: center; }
|
|
143
175
|
& .empty-state { max-width: 320px; margin: auto; padding: 24px; text-align: center; }
|
|
144
176
|
& .empty-state h2 { margin: 0 0 6px; font-size: 18px; }
|
|
145
177
|
& .empty-state p { margin: 0; }
|
|
146
|
-
|
|
147
|
-
|
|
178
|
+
|
|
179
|
+
/* The two-pane dashboard: the queue on the left, the selected record on the
|
|
180
|
+
right, each pane owning its own scroll. */
|
|
181
|
+
& .dashboard-shell { display: grid; flex: 1 1 auto; grid-template-columns: minmax(330px, 430px) 1fr; gap: 16px; min-height: 0; }
|
|
148
182
|
& .dashboard-sidebar { display: flex; min-width: 0; min-height: 0; flex-direction: column; overflow: hidden; border: 1px solid var(--pt-border); border-radius: 8px; background: var(--pt-surface); }
|
|
149
183
|
& .dashboard-sidebar .tabs { flex: 0 0 auto; margin: 12px 12px 0; }
|
|
184
|
+
& .dashboard-sidebar .filters { flex: 0 0 auto; }
|
|
185
|
+
|
|
186
|
+
/* One row shape for every gem: a topline of badges and a timestamp, the copy
|
|
187
|
+
that identifies the record, then a muted line of ids and authors. */
|
|
150
188
|
& .record-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
|
|
151
189
|
& .record-row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 12px; padding: 13px 14px; border-bottom: 1px solid var(--pt-border); color: var(--pt-text); }
|
|
152
190
|
& .record-row:hover { background: var(--pt-bg); text-decoration: none; }
|
|
@@ -154,50 +192,133 @@
|
|
|
154
192
|
& .record-main { display: flex; min-width: 0; flex-direction: column; gap: 5px; }
|
|
155
193
|
& .record-topline,
|
|
156
194
|
& .record-meta { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
157
|
-
& .record-
|
|
158
|
-
& .record-
|
|
159
|
-
& .record-
|
|
160
|
-
|
|
195
|
+
& .record-name,
|
|
196
|
+
& .record-copy,
|
|
197
|
+
& .record-meta span,
|
|
198
|
+
/* A key is `overflow-wrap: anywhere` in prose; in a row it truncates. */
|
|
199
|
+
& .record-topline code.key,
|
|
200
|
+
& .record-meta code.key { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
201
|
+
& .record-name { font-weight: 600; }
|
|
202
|
+
& .record-copy { display: block; font-weight: 600; }
|
|
203
|
+
& .record-time { margin-left: auto; white-space: nowrap; }
|
|
204
|
+
& .record-side { display: flex; flex-direction: column; align-items: flex-end; justify-content: space-between; gap: 8px; padding-top: 1px; }
|
|
205
|
+
& .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(--pt-border); }
|
|
206
|
+
|
|
207
|
+
/* The selected record. `.detail-scroll` is the only part that scrolls, so the
|
|
208
|
+
panel heading and its actions stay put. */
|
|
161
209
|
& .dashboard-detail { display: flex; min-width: 0; min-height: 0; flex-direction: column; overflow: hidden; }
|
|
162
|
-
& .dashboard-detail .
|
|
210
|
+
& .dashboard-detail .detail-panel { flex: 1 1 auto; min-height: 0; }
|
|
163
211
|
& .dashboard-detail .detail-scroll { flex: 1 1 auto; overflow-y: auto; padding-right: 2px; }
|
|
164
|
-
& .
|
|
165
|
-
& .
|
|
212
|
+
& .dashboard-detail .actions { margin-bottom: 0; }
|
|
213
|
+
& .detail-panel { display: flex; min-width: 0; flex-direction: column; }
|
|
214
|
+
/* The panel's own heading row: the title on the left, whatever acts on the
|
|
215
|
+
whole record on the right. */
|
|
216
|
+
& .panel-head { display: flex; flex: 0 0 auto; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
|
|
217
|
+
& .panel-head h1 { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 0; }
|
|
166
218
|
& .detail-scroll { display: flex; min-height: 0; flex-direction: column; gap: 12px; }
|
|
167
|
-
& .
|
|
219
|
+
& .mobile-back { display: none; }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/* Page frame — the gem's own layout only. After the components, so it wins the
|
|
223
|
+
specificity ties against them. */
|
|
224
|
+
.pt-page { margin: 0; min-height: 100vh; background: var(--pt-bg); color: var(--pt-text); font: 15px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif; letter-spacing: 0; }
|
|
225
|
+
/* An index page is the dashboard: it fills the viewport and never scrolls
|
|
226
|
+
itself, because the queue and the detail pane each scroll on their own. */
|
|
227
|
+
.pt-index, .pt-index .container { height: 100vh; height: 100dvh; }
|
|
228
|
+
.pt-index { overflow: hidden; }
|
|
229
|
+
.pt-index .container { display: flex; max-width: 1280px; flex-direction: column; padding-bottom: 16px; }
|
|
230
|
+
.pt-index .nav,
|
|
231
|
+
.pt-index .page-head { flex: 0 0 auto; }
|
|
232
|
+
/* Standalone show pages keep normal page scrolling. */
|
|
233
|
+
.pt-show { min-height: 100vh; overflow: auto; }
|
|
234
|
+
.pt-show .detail-panel { width: 100%; }
|
|
235
|
+
.pt-show .detail-scroll { overflow: visible; }
|
|
236
|
+
|
|
237
|
+
/* Narrow screens: one pane at a time, the queue until a record is picked.
|
|
238
|
+
Last in the file, so these overrides actually reach the rules above. */
|
|
239
|
+
@media (max-width: 760px) {
|
|
240
|
+
.pt-page { font-size: 14px; }
|
|
241
|
+
.pt-index, .pt-show { overflow-x: hidden; }
|
|
242
|
+
.pt-index { overflow-y: auto; }
|
|
243
|
+
.pt-index, .pt-index .container { min-height: 100vh; height: auto; }
|
|
244
|
+
.pt-index .container { padding-bottom: 10px; }
|
|
245
|
+
.pt-dashboard {
|
|
246
|
+
& .container { padding: 14px 10px 24px; }
|
|
247
|
+
& .page-head { margin-bottom: 12px; }
|
|
248
|
+
& .detail-panel,
|
|
249
|
+
& .detail-scroll,
|
|
250
|
+
& .card,
|
|
251
|
+
& dl,
|
|
252
|
+
& dd { min-width: 0; max-width: 100%; }
|
|
253
|
+
& .dashboard-detail .card { width: 100%; }
|
|
254
|
+
& .message,
|
|
255
|
+
& .lede,
|
|
256
|
+
& dd,
|
|
257
|
+
& .muted { overflow-wrap: anywhere; }
|
|
258
|
+
& .dashboard-shell { display: block; min-height: calc(100vh - 62px); }
|
|
259
|
+
& .dashboard-sidebar,
|
|
260
|
+
& .dashboard-detail { min-height: calc(100vh - 62px); }
|
|
261
|
+
& .dashboard-shell.has-selected .dashboard-sidebar { display: none; }
|
|
262
|
+
& .dashboard-shell:not(.has-selected) .dashboard-detail { display: none; }
|
|
263
|
+
& .record-list { overflow: visible; }
|
|
264
|
+
& .record-row { padding: 12px; }
|
|
265
|
+
& .record-meta { display: none; }
|
|
266
|
+
& .dashboard-detail .detail-panel { min-height: calc(100vh - 64px); }
|
|
267
|
+
& .mobile-back { display: block; flex: 0 0 auto; margin: 4px 0 10px; font-size: 13px; }
|
|
268
|
+
& .panel-head h1 { font-size: 18px; }
|
|
269
|
+
& dl { grid-template-columns: 1fr; gap: 3px; }
|
|
270
|
+
& dd { margin-bottom: 8px; }
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/* ---------------------------------------------------------------------------
|
|
275
|
+
* GEM-SPECIFIC — product_tours only: the video preview, the tour authoring
|
|
276
|
+
* form with its action and video-source pickers, and the rich text editor.
|
|
277
|
+
* ------------------------------------------------------------------------- */
|
|
278
|
+
|
|
279
|
+
:root {
|
|
280
|
+
--pt-draft: var(--pt-neutral);
|
|
281
|
+
--pt-published: var(--pt-success);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.pt-dashboard {
|
|
285
|
+
& .badge.status-draft { background: var(--pt-draft); }
|
|
286
|
+
& .badge.status-published { background: var(--pt-published); }
|
|
287
|
+
& .record-side { align-self: center; justify-content: center; color: var(--pt-muted); }
|
|
288
|
+
|
|
289
|
+
/* Preview: the tutorial as a reader meets it. */
|
|
290
|
+
& .preview-card,
|
|
291
|
+
& .details-card,
|
|
292
|
+
& .post-form { width: 100%; }
|
|
168
293
|
& .video-preview { width: 100%; aspect-ratio: 16 / 9; margin-bottom: 16px; overflow: hidden; border-radius: 6px; background: #05070a; }
|
|
169
294
|
& .video-preview iframe,
|
|
170
295
|
& .video-preview video { display: block; width: 100%; height: 100%; border: 0; object-fit: contain; }
|
|
171
296
|
& .description-preview { color: var(--pt-muted); overflow-wrap: anywhere; }
|
|
172
|
-
& .preview-action { display: flex; justify-content: flex-end; margin-top: 16px; }
|
|
173
|
-
& .
|
|
174
|
-
&
|
|
175
|
-
& dt { color: var(--pt-muted); }
|
|
176
|
-
& dd { margin: 0; overflow-wrap: anywhere; }
|
|
297
|
+
& .preview-action { display: flex; align-items: center; justify-content: flex-end; gap: 12px; margin-top: 16px; }
|
|
298
|
+
& .preview-back { margin-right: auto; color: var(--pt-muted); }
|
|
299
|
+
& .details-actions { margin-top: 16px; }
|
|
177
300
|
& .refresh-button { margin-top: 14px; }
|
|
178
301
|
& .metadata { max-width: 100%; overflow-x: auto; margin: 14px 0 0; padding: 12px; border-radius: 6px; background: var(--pt-code); font-size: 12px; }
|
|
302
|
+
|
|
179
303
|
& .translation-list { border-top: 1px solid var(--pt-border); }
|
|
180
304
|
& .translation-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--pt-border); }
|
|
181
305
|
& .translation-name { display: flex; min-width: 0; align-items: center; flex-wrap: wrap; gap: 8px; }
|
|
182
306
|
& .translation-form { display: flex; align-items: flex-end; gap: 8px; margin-top: 16px; }
|
|
183
307
|
& .translation-form .field { flex: 1 1 auto; margin: 0; }
|
|
184
308
|
& .translation-complete { margin: 14px 0 0; }
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
& .actions { margin: 0; }
|
|
188
|
-
& .details-actions { margin-top: 16px; }
|
|
189
|
-
& .actions form { display: inline; }
|
|
190
|
-
& .form-actions input[type="submit"] { width: auto; }
|
|
191
|
-
& .post-form { width: 100%; }
|
|
309
|
+
|
|
310
|
+
/* Authoring form. */
|
|
192
311
|
& .form-card { padding: 0; }
|
|
193
312
|
& .form-section { padding: 12px 0 4px; }
|
|
194
313
|
& .form-section + .form-section { margin-top: 4px; }
|
|
195
314
|
& .form-section h2 { margin-bottom: 12px; }
|
|
196
|
-
& .field { margin-bottom: 16px; }
|
|
197
|
-
& .field label { display: block; margin-bottom: 5px; font-weight: 600; }
|
|
198
|
-
& .field small { display: block; margin-top: 5px; color: var(--pt-muted); }
|
|
199
315
|
& .section-intro { margin: -4px 0 12px; color: var(--pt-muted); }
|
|
200
|
-
& .
|
|
316
|
+
& .form-video-preview { margin: 2px 0 16px; }
|
|
317
|
+
& .form-video-preview .video-preview { margin-bottom: 6px; }
|
|
318
|
+
& .form-video-preview > small { color: var(--pt-muted); }
|
|
319
|
+
& .checkbox { display: flex !important; align-items: center; gap: 8px; margin-top: 8px; font-weight: 400 !important; }
|
|
320
|
+
& .checkbox input { width: 18px; min-height: 18px; }
|
|
321
|
+
/* Radio cards: what the tour does, and where its video comes from. */
|
|
201
322
|
& .action-picker,
|
|
202
323
|
& .video-source-picker { display: grid; gap: 10px; min-width: 0; margin: 0 0 18px; padding: 0; border: 0; }
|
|
203
324
|
& .action-picker { grid-template-columns: repeat(3, 1fr); }
|
|
@@ -210,17 +331,24 @@
|
|
|
210
331
|
& .action-choice strong,
|
|
211
332
|
& .action-choice small { display: block; }
|
|
212
333
|
& .action-choice small { margin-top: 3px; color: var(--pt-muted); font-weight: 400; line-height: 1.35; }
|
|
213
|
-
|
|
214
|
-
& .preview-back { margin-right: auto; color: var(--pt-muted); }
|
|
215
|
-
& .checkbox { display: flex !important; align-items: center; gap: 8px; margin-top: 8px; font-weight: 400 !important; }
|
|
216
|
-
& .checkbox input { width: 18px; min-height: 18px; }
|
|
217
|
-
& .form-actions { padding-top: 12px; }
|
|
218
|
-
& .form-video-preview { margin: 2px 0 16px; }
|
|
219
|
-
& .form-video-preview .video-preview { margin-bottom: 6px; }
|
|
220
|
-
& .form-video-preview > small { color: var(--pt-muted); }
|
|
334
|
+
|
|
221
335
|
& .rich-editor { overflow: hidden; border: 1px solid var(--pt-border); border-radius: 8px; background: var(--pt-surface); }
|
|
222
336
|
& .rich-toolbar { display: flex; gap: 4px; padding: 7px; border-bottom: 1px solid var(--pt-border); background: var(--pt-bg); }
|
|
223
337
|
& .rich-toolbar button { width: 36px; height: 34px; min-height: 34px; padding: 0; border-color: transparent; background: transparent; font-weight: 600; }
|
|
224
338
|
& .rich-content { min-height: 160px; padding: 12px; outline: none; overflow-wrap: anywhere; }
|
|
225
339
|
& .rich-content:focus { box-shadow: inset 0 0 0 2px var(--pt-accent); }
|
|
226
340
|
}
|
|
341
|
+
|
|
342
|
+
/* New and edit are ordinary form pages: one column, narrower than a reading
|
|
343
|
+
page, normal scrolling. */
|
|
344
|
+
.pt-form-page { min-height: 100vh; overflow: auto; }
|
|
345
|
+
.pt-form-page .container { max-width: 820px; }
|
|
346
|
+
|
|
347
|
+
@media (max-width: 760px) {
|
|
348
|
+
.pt-dashboard {
|
|
349
|
+
& .page-head .button { min-height: 36px; }
|
|
350
|
+
& .action-picker,
|
|
351
|
+
& .video-source-picker { grid-template-columns: 1fr; }
|
|
352
|
+
& .form-card { padding: 0; }
|
|
353
|
+
}
|
|
354
|
+
}
|