product_tours 0.2.2 → 0.3.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 +19 -2
- data/CHANGELOG.md +40 -3
- data/README.md +21 -15
- data/app/controllers/product_tours/tours_controller.rb +9 -1
- data/app/views/product_tours/posts/_post_panel.html.erb +1 -1
- data/lib/generators/product_tours/install/templates/initializer.rb +7 -0
- data/lib/product_tours/configuration.rb +2 -1
- data/lib/product_tours/dashboard.css +243 -102
- 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: bca51e6535dbdd38d6d683ee0ea95e430acbefd41479f356e1c0e7c330c64697
|
|
4
|
+
data.tar.gz: fce14b868cabf23946d5658aa5c9362d5b0e3df92e326b27f31164b9590e01cd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1dec9e04d694dd26b2baafbe9eb6226370ccb728c500c086ebc387e8cf6bdfa7433f5588772b9a9e61be26e2d5558af59dd4d096424a429cc9004abed73b3084
|
|
7
|
+
data.tar.gz: b86e2d428a86bb9708cbb709eb80e0e5de1ead8bf3a18d32e78c38d64a9e24ce7d41a2bbaea3ee7d214e8d4dadecc74bc35dce1ffcd74e570eb7a78ea3095281
|
data/AGENTS.md
CHANGED
|
@@ -81,7 +81,8 @@ YouTube, Vimeo, Loom, Tella, Voomly, a direct MP4/WebM URL, or an uploaded file
|
|
|
81
81
|
|
|
82
82
|
### Lifecycle events
|
|
83
83
|
|
|
84
|
-
Subscribe in an initializer
|
|
84
|
+
Subscribe in an initializer when your normal request middleware already sets
|
|
85
|
+
host identity:
|
|
85
86
|
|
|
86
87
|
```ruby
|
|
87
88
|
ActiveSupport::Notifications.subscribe("product_tours.completed") do |*, payload|
|
|
@@ -91,6 +92,20 @@ end
|
|
|
91
92
|
|
|
92
93
|
Names: `product_tours.viewed`, `product_tours.dismissed`, `product_tours.completed`, and `product_tours.unresolved_trigger` — the last one fires when a `data-product-tour` button names a key that does not resolve. Subscribe to it in development; it turns "my button does nothing" into a log line naming the key.
|
|
93
94
|
|
|
95
|
+
The public widget controller deliberately does not inherit the host's controller.
|
|
96
|
+
If that means a subscriber cannot see the host's current user/account context,
|
|
97
|
+
configure the request-aware lifecycle hook instead:
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
config.on_event = lambda do |name, payload, request|
|
|
101
|
+
user = request.env["warden"]&.user
|
|
102
|
+
ProductTourEventJob.perform_later(name, payload, user&.id)
|
|
103
|
+
end
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
It receives only `viewed`, `dismissed`, and `completed`, runs inline, and logs
|
|
107
|
+
exceptions without breaking the visitor flow. Keep it fast or enqueue a job.
|
|
108
|
+
|
|
94
109
|
### Do not
|
|
95
110
|
|
|
96
111
|
- **Do not copy the widget JavaScript into `app/javascript`, or add a `<script>` tag for it.** `product_tours_tag` renders what is needed and the engine serves the code. There is no build step and nothing for esbuild/importmap/Tailwind to know about.
|
|
@@ -101,7 +116,7 @@ Names: `product_tours.viewed`, `product_tours.dismissed`, `product_tours.complet
|
|
|
101
116
|
|
|
102
117
|
### Configuration
|
|
103
118
|
|
|
104
|
-
There are
|
|
119
|
+
There are seven options. That is the whole surface.
|
|
105
120
|
|
|
106
121
|
| Option | Default | What it does |
|
|
107
122
|
| --- | --- | --- |
|
|
@@ -109,6 +124,7 @@ There are five options. That is the whole surface.
|
|
|
109
124
|
| `enabled` | everyone | Per-request gate for the widget and its endpoints |
|
|
110
125
|
| `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. |
|
|
111
126
|
| `admin_layout` | `product_tours/application` | Render the dashboard inside your admin shell |
|
|
127
|
+
| `on_event` | no-op | Handle lifecycle events with `(name, payload, request)` when host identity must be resolved from the raw request |
|
|
112
128
|
| `mount_path` | `"/product_tours"` | Keep in sync with `mount_product_tours at:` |
|
|
113
129
|
| `storage_service` | app default | Active Storage service for uploaded video (a `storage.yml` key) |
|
|
114
130
|
|
|
@@ -152,5 +168,6 @@ Conventions this codebase holds to — follow them rather than the first thing t
|
|
|
152
168
|
- **Uploaded media streams through the engine's gate**, never a public blob URL.
|
|
153
169
|
- **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.
|
|
154
170
|
- **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.
|
|
171
|
+
- **`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`/`.status-switch`, 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.
|
|
155
172
|
- 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.
|
|
156
173
|
- Commit messages are prose that explains the tradeoff — read `git log` before writing one.
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.3.1] - 2026-08-08
|
|
6
|
+
|
|
7
|
+
- **Hosts can handle lifecycle events with their own request context.** Set
|
|
8
|
+
`config.on_event` to a callable accepting `(name, payload, request)` when a
|
|
9
|
+
`viewed`, `dismissed`, or `completed` signal must resolve a signed session,
|
|
10
|
+
user, account, or tenant that deliberately does not belong in the gem. The
|
|
11
|
+
existing `ActiveSupport::Notifications` events and their minimal payloads are
|
|
12
|
+
unchanged. The hook runs inline, so slow work should be enqueued; exceptions
|
|
13
|
+
are logged and never turn a working tutorial action into a visitor-facing
|
|
14
|
+
failure.
|
|
15
|
+
|
|
16
|
+
## 0.3.0
|
|
17
|
+
|
|
18
|
+
- **One design system across the family.** The stylesheet now opens with a
|
|
19
|
+
shared core — the colour tokens, `.page-head`, `.tabs`, `.filters`, `.card`,
|
|
20
|
+
`.badge`, buttons and form controls, and the `.dashboard-shell` +
|
|
21
|
+
`.record-row` + `.detail-panel` two-pane dashboard — identical in all five
|
|
22
|
+
gems of the family, apart from the `--pt-` prefix. The five had drifted:
|
|
23
|
+
sidebars between 380px and 430px, reading columns between 860px and 1020px,
|
|
24
|
+
two different tab styles and three different button styles. The sidebar is
|
|
25
|
+
now 330–430px everywhere, a reading page 1020px, a dashboard 1280px.
|
|
26
|
+
Everything below the `GEM-SPECIFIC` banner is what only this gem has.
|
|
27
|
+
- **The dashboard markup uses the shared class names.** `.post-panel` is
|
|
28
|
+
`.detail-panel`; everything else already used the shared vocabulary, which
|
|
29
|
+
this gem's dashboard had the most of. If you styled or scripted
|
|
30
|
+
`.post-panel` in a host app, that is the breaking change — no configuration,
|
|
31
|
+
route or database change is involved.
|
|
32
|
+
- **The narrow-screen rules work again.** The `max-width: 760px` block sat above
|
|
33
|
+
the component rules it means to override, and CSS nesting adds no specificity,
|
|
34
|
+
so the desktop grid won every tie: showing one pane at a time on a phone had
|
|
35
|
+
quietly stopped working. The media query moved to the end of the file.
|
|
36
|
+
- Smaller fixes that came with the shared core: a submit input is styled as a
|
|
37
|
+
button rather than a full-width field, the filter row keeps its search box and
|
|
38
|
+
button on one line, and a `code.key` truncates inside a list row instead of
|
|
39
|
+
wrapping over three lines.
|
|
40
|
+
|
|
3
41
|
## 0.2.0
|
|
4
42
|
|
|
5
43
|
- **`config.admin_layout` now works on its own.** The dashboard's stylesheet and
|
|
@@ -35,8 +73,6 @@
|
|
|
35
73
|
`remove_index :product_tours_posts, :locale`.
|
|
36
74
|
- A `BackboneTest` now fails the build on any of the above regressing.
|
|
37
75
|
|
|
38
|
-
## [Unreleased]
|
|
39
|
-
|
|
40
76
|
## [0.1.2] - 2026-08-04
|
|
41
77
|
|
|
42
78
|
- Added `AGENTS.md`: install and integration instructions written for coding
|
|
@@ -97,6 +133,7 @@
|
|
|
97
133
|
- Removed generic tutorial duplication now that translations provide the only
|
|
98
134
|
intentional content-copying workflow.
|
|
99
135
|
|
|
100
|
-
[Unreleased]: https://github.com/yshmarov/product_tours/compare/v0.
|
|
136
|
+
[Unreleased]: https://github.com/yshmarov/product_tours/compare/v0.3.1...HEAD
|
|
137
|
+
[0.3.1]: https://github.com/yshmarov/product_tours/compare/v0.3.0...v0.3.1
|
|
101
138
|
[0.1.1]: https://github.com/yshmarov/product_tours/compare/v0.1.0...v0.1.1
|
|
102
139
|
[0.1.0]: https://github.com/yshmarov/product_tours/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -228,6 +228,7 @@ Everything is optional — a development install works with zero config. In
|
|
|
228
228
|
| `authorize_admin` | development only | **Who can manage content at the mount path** |
|
|
229
229
|
| `base_controller_class` | `ActionController::Base` | Controller the dashboard inherits — name your admin's and it adopts its layout, helpers and auth |
|
|
230
230
|
| `admin_layout` | gem layout | Just the shell, if you don't want the whole controller |
|
|
231
|
+
| `on_event` | no-op | Handle lifecycle events with the raw request and host-owned identity/account context |
|
|
231
232
|
| `storage_service` | app default | Named Active Storage service for uploaded videos |
|
|
232
233
|
| `mount_path` | `/product_tours` | Keep in sync only when mounting the engine manually |
|
|
233
234
|
|
|
@@ -236,6 +237,9 @@ ProductTours.configure do |config|
|
|
|
236
237
|
config.enabled = ->(request) { request.env["warden"]&.user.present? }
|
|
237
238
|
config.authorize_admin = ->(request) { request.env["warden"]&.user&.admin? }
|
|
238
239
|
config.admin_layout = "admin/application"
|
|
240
|
+
config.on_event = lambda do |name, payload, request|
|
|
241
|
+
AnalyticsJob.perform_later(name, payload, request.session[:user_id])
|
|
242
|
+
end
|
|
239
243
|
config.storage_service = :product_tours
|
|
240
244
|
end
|
|
241
245
|
```
|
|
@@ -302,6 +306,20 @@ end
|
|
|
302
306
|
Your subscriber can attach `Current.user` or account context. That identity does
|
|
303
307
|
not need to become product-tour configuration.
|
|
304
308
|
|
|
309
|
+
If the engine's public controller does not run your host controller callbacks,
|
|
310
|
+
use `on_event` instead. It receives the same event name and payload plus the raw
|
|
311
|
+
request, so it can safely resolve the host's signed session:
|
|
312
|
+
|
|
313
|
+
```ruby
|
|
314
|
+
config.on_event = lambda do |name, payload, request|
|
|
315
|
+
user = request.env["warden"]&.user
|
|
316
|
+
ProductTourEventJob.perform_later(name, payload, user&.id)
|
|
317
|
+
end
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
The hook runs inline; enqueue slow work. Exceptions are logged and do not break
|
|
321
|
+
the visitor flow. `product_tours.unresolved_trigger` remains a notification only.
|
|
322
|
+
|
|
305
323
|
## Broken triggers fail loudly, not publicly
|
|
306
324
|
|
|
307
325
|
`data-product-tour="key"` is a contract between code and dashboard content.
|
|
@@ -347,18 +365,6 @@ CI runs Rails 7.1 / 7.2 / 8.0 / 8.1 against Ruby 3.2 / 3.3 / 3.4.
|
|
|
347
365
|
Bug reports and pull requests are welcome. The most useful report is a real
|
|
348
366
|
Rails app and the exact point where installation or authoring felt confusing.
|
|
349
367
|
|
|
350
|
-
## Also by the same author
|
|
351
|
-
|
|
352
|
-
- [testimonials](https://github.com/yshmarov/testimonials) — testimonials,
|
|
353
|
-
video reviews, and NPS for Rails.
|
|
354
|
-
- [livechat](https://github.com/yshmarov/livechat) — in-app support messaging
|
|
355
|
-
for Rails.
|
|
356
|
-
- [ideasbugs](https://github.com/yshmarov/ideasbugs) — in-app bug reports and
|
|
357
|
-
feature requests.
|
|
358
|
-
- [i18n_proofreading](https://github.com/yshmarov/i18n_proofreading) — in-context
|
|
359
|
-
translation proofreading.
|
|
360
|
-
- [SupeRails](https://superails.com) — Rails screencasts.
|
|
361
|
-
|
|
362
368
|
## One family
|
|
363
369
|
|
|
364
370
|
Five Rails engines built on the same backbone, so adopting a second one is
|
|
@@ -374,9 +380,9 @@ mostly muscle memory:
|
|
|
374
380
|
|
|
375
381
|
They share the install shape (`generate <gem>:install`, mount, one initializer),
|
|
376
382
|
the same host hooks (`base_controller_class` to inherit your admin's controller,
|
|
377
|
-
`admin_layout` for just the shell),
|
|
378
|
-
|
|
379
|
-
`primary_key_type`.
|
|
383
|
+
`admin_layout` for just the shell), one dashboard design system — the same
|
|
384
|
+
two-pane layout, colour tokens and components in all five, scoped so it cannot
|
|
385
|
+
touch your own CSS — and migrations that follow your app's `primary_key_type`.
|
|
380
386
|
|
|
381
387
|
## License
|
|
382
388
|
|
|
@@ -32,12 +32,20 @@ module ProductTours
|
|
|
32
32
|
page_url: clean_page_url(params[:page_url]),
|
|
33
33
|
source: params[:source].to_s.presence
|
|
34
34
|
}
|
|
35
|
-
|
|
35
|
+
event_name = "product_tours.#{action}"
|
|
36
|
+
ActiveSupport::Notifications.instrument(event_name, payload)
|
|
37
|
+
notify_host(event_name, payload)
|
|
36
38
|
head :no_content
|
|
37
39
|
end
|
|
38
40
|
|
|
39
41
|
private
|
|
40
42
|
|
|
43
|
+
def notify_host(event_name, payload)
|
|
44
|
+
ProductTours.config.on_event.call(event_name, payload, request)
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
Rails.logger.error("product_tours: on_event hook raised #{e.class}: #{e.message}")
|
|
47
|
+
end
|
|
48
|
+
|
|
41
49
|
def post_payload(post)
|
|
42
50
|
video = post.resolved_video
|
|
43
51
|
video[:url] = media_path(post) if video&.dig(:kind) == 'upload'
|
|
@@ -21,6 +21,13 @@ ProductTours.configure do |config|
|
|
|
21
21
|
# Tutorials resolve in the page's current I18n.locale and fall back to
|
|
22
22
|
# I18n.default_locale when no translation exists.
|
|
23
23
|
|
|
24
|
+
# Optional lifecycle hook. Receives the event name, minimal payload, and raw
|
|
25
|
+
# request so the host can resolve its own user/account context. Runs inline;
|
|
26
|
+
# enqueue slow work. Hook errors are logged and never break the visitor flow.
|
|
27
|
+
# config.on_event = lambda do |name, payload, request|
|
|
28
|
+
# AnalyticsJob.perform_later(name, payload, request.session[:user_id])
|
|
29
|
+
# end
|
|
30
|
+
|
|
24
31
|
# Optional dedicated Active Storage service for uploaded videos.
|
|
25
32
|
# config.storage_service = :product_tours
|
|
26
33
|
|
|
@@ -7,7 +7,7 @@ module ProductTours
|
|
|
7
7
|
DEFAULT_ADMIN_LAYOUT = 'product_tours/application'
|
|
8
8
|
|
|
9
9
|
attr_accessor :enabled, :authorize_admin, :admin_layout, :mount_path,
|
|
10
|
-
:storage_service
|
|
10
|
+
:storage_service, :on_event
|
|
11
11
|
|
|
12
12
|
# The controller the DASHBOARD inherits from, as a String so it resolves
|
|
13
13
|
# lazily rather than at config time. Default: a plain
|
|
@@ -31,6 +31,7 @@ module ProductTours
|
|
|
31
31
|
@base_controller_class = 'ActionController::Base'
|
|
32
32
|
@mount_path = '/product_tours'
|
|
33
33
|
@storage_service = nil
|
|
34
|
+
@on_event = ->(_name, _payload, _request) {}
|
|
34
35
|
end
|
|
35
36
|
|
|
36
37
|
def widget_endpoint = "#{mount_path.to_s.chomp('/')}/widget"
|
|
@@ -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,38 +54,6 @@
|
|
|
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
|
|
|
@@ -83,58 +67,137 @@
|
|
|
83
67
|
& a { color: var(--pt-accent); text-decoration: none; }
|
|
84
68
|
& a:hover { text-decoration: underline; }
|
|
85
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. */
|
|
86
73
|
& .container { max-width: 1020px; margin: 0 auto; padding: 24px 16px 64px; }
|
|
87
74
|
& h1 { margin: 0 0 16px; font-size: 22px; }
|
|
88
75
|
& h2 { margin: 0 0 12px; font-size: 16px; }
|
|
89
|
-
& .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; }
|
|
90
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; }
|
|
91
80
|
& .muted { color: var(--pt-muted); font-size: 13px; }
|
|
92
|
-
&
|
|
93
|
-
& .
|
|
94
|
-
&
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
&
|
|
99
|
-
& .
|
|
100
|
-
&
|
|
101
|
-
& .
|
|
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. */
|
|
102
95
|
& .tabs { display: flex; gap: 6px; }
|
|
103
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; }
|
|
104
97
|
& .tabs a:hover { background: var(--pt-bg); color: var(--pt-text); text-decoration: none; }
|
|
105
98
|
& .tabs a.active { color: var(--pt-text); }
|
|
106
99
|
& .tabs a.active::after { content: ""; position: absolute; right: 20px; bottom: 0; left: 20px; height: 2px; border-radius: 999px; background: var(--pt-accent); }
|
|
107
|
-
& .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; }
|
|
108
|
-
& .
|
|
109
|
-
|
|
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. */
|
|
110
115
|
& input,
|
|
111
116
|
& select,
|
|
112
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; }
|
|
113
118
|
& textarea { resize: vertical; }
|
|
119
|
+
& input[type="checkbox"],
|
|
120
|
+
& input[type="radio"] { width: auto; min-width: 0; min-height: 0; }
|
|
114
121
|
& input:focus,
|
|
115
122
|
& select:focus,
|
|
116
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
|
+
|
|
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(--pt-border); border-radius: 999px; background: var(--pt-bg); }
|
|
154
|
+
& .status-switch button { min-height: 30px; padding: 0 12px; border: 0; border-radius: 999px; background: none; color: var(--pt-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(--pt-muted) 12%, transparent); color: var(--pt-text); }
|
|
156
|
+
& .status-switch button.current { background: var(--pt-surface); color: var(--pt-text); box-shadow: 0 1px 2px rgba(15, 23, 42, .12); cursor: default; }
|
|
157
|
+
& .status-switch button.current:hover { background: var(--pt-surface); }
|
|
158
|
+
& .panel-head .status-switch { margin-left: auto; }
|
|
159
|
+
|
|
117
160
|
& .card { overflow: hidden; border: 1px solid var(--pt-border); border-radius: 12px; background: var(--pt-surface); }
|
|
118
161
|
& .card.pad { padding: 16px; overflow: visible; }
|
|
119
|
-
&
|
|
120
|
-
&
|
|
121
|
-
&
|
|
162
|
+
& table { width: 100%; border-collapse: collapse; }
|
|
163
|
+
& th,
|
|
164
|
+
& td { padding: 10px 14px; text-align: left; vertical-align: top; }
|
|
165
|
+
& th { font-size: 12px; text-transform: uppercase; letter-spacing: .04em; color: var(--pt-muted); border-bottom: 1px solid var(--pt-border); }
|
|
166
|
+
& tr + tr td { border-top: 1px solid var(--pt-border); }
|
|
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; }
|
|
122
172
|
& .badge.locale { border: 1px solid var(--pt-border); background: transparent; color: var(--pt-muted); }
|
|
123
173
|
& 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; }
|
|
174
|
+
|
|
124
175
|
& .flash,
|
|
125
176
|
& .errors { margin-bottom: 16px; padding: 10px 12px; border: 1px solid var(--pt-border); border-radius: 8px; background: var(--pt-surface); }
|
|
126
|
-
& .flash.notice { border-color: var(--pt-
|
|
177
|
+
& .flash.notice { border-color: var(--pt-success); }
|
|
127
178
|
& .flash.alert,
|
|
128
179
|
& .errors { border-color: var(--pt-danger); }
|
|
129
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(--pt-muted); }
|
|
184
|
+
& dd { margin: 0; overflow-wrap: anywhere; }
|
|
185
|
+
& .message { margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
186
|
+
|
|
130
187
|
& .empty { padding: 48px 16px; color: var(--pt-muted); text-align: center; }
|
|
131
188
|
& .empty-state { max-width: 320px; margin: auto; padding: 24px; text-align: center; }
|
|
132
189
|
& .empty-state h2 { margin: 0 0 6px; font-size: 18px; }
|
|
133
190
|
& .empty-state p { margin: 0; }
|
|
134
|
-
|
|
135
|
-
|
|
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; }
|
|
136
195
|
& .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); }
|
|
137
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. */
|
|
138
201
|
& .record-list { flex: 1 1 auto; min-height: 0; overflow-y: auto; }
|
|
139
202
|
& .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); }
|
|
140
203
|
& .record-row:hover { background: var(--pt-bg); text-decoration: none; }
|
|
@@ -142,50 +205,134 @@
|
|
|
142
205
|
& .record-main { display: flex; min-width: 0; flex-direction: column; gap: 5px; }
|
|
143
206
|
& .record-topline,
|
|
144
207
|
& .record-meta { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
145
|
-
& .record-
|
|
146
|
-
& .record-
|
|
147
|
-
& .record-
|
|
148
|
-
|
|
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(--pt-border); }
|
|
219
|
+
|
|
220
|
+
/* The selected record. `.detail-scroll` is the only part that scrolls, so the
|
|
221
|
+
panel heading and its actions stay put. */
|
|
149
222
|
& .dashboard-detail { display: flex; min-width: 0; min-height: 0; flex-direction: column; overflow: hidden; }
|
|
150
|
-
& .dashboard-detail .
|
|
223
|
+
& .dashboard-detail .detail-panel { flex: 1 1 auto; min-height: 0; }
|
|
151
224
|
& .dashboard-detail .detail-scroll { flex: 1 1 auto; overflow-y: auto; padding-right: 2px; }
|
|
152
|
-
& .
|
|
153
|
-
& .
|
|
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; }
|
|
154
231
|
& .detail-scroll { display: flex; min-height: 0; flex-direction: column; gap: 12px; }
|
|
155
|
-
& .
|
|
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
|
+
.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; }
|
|
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
|
+
.pt-index, .pt-index .container { height: 100vh; height: 100dvh; }
|
|
241
|
+
.pt-index { overflow: hidden; }
|
|
242
|
+
.pt-index .container { display: flex; max-width: 1280px; flex-direction: column; padding-bottom: 16px; }
|
|
243
|
+
.pt-index .nav,
|
|
244
|
+
.pt-index .page-head { flex: 0 0 auto; }
|
|
245
|
+
/* Standalone show pages keep normal page scrolling. */
|
|
246
|
+
.pt-show { min-height: 100vh; overflow: auto; }
|
|
247
|
+
.pt-show .detail-panel { width: 100%; }
|
|
248
|
+
.pt-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
|
+
.pt-page { font-size: 14px; }
|
|
254
|
+
.pt-index, .pt-show { overflow-x: hidden; }
|
|
255
|
+
.pt-index { overflow-y: auto; }
|
|
256
|
+
.pt-index, .pt-index .container { min-height: 100vh; height: auto; }
|
|
257
|
+
.pt-index .container { padding-bottom: 10px; }
|
|
258
|
+
.pt-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 — product_tours only: the video preview, the tour authoring
|
|
290
|
+
* form with its action and video-source pickers, and the rich text editor.
|
|
291
|
+
* ------------------------------------------------------------------------- */
|
|
292
|
+
|
|
293
|
+
:root {
|
|
294
|
+
--pt-draft: var(--pt-neutral);
|
|
295
|
+
--pt-published: var(--pt-success);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
.pt-dashboard {
|
|
299
|
+
& .badge.status-draft { background: var(--pt-draft); }
|
|
300
|
+
& .badge.status-published { background: var(--pt-published); }
|
|
301
|
+
& .record-side { align-self: center; justify-content: center; color: var(--pt-muted); }
|
|
302
|
+
|
|
303
|
+
/* Preview: the tutorial as a reader meets it. */
|
|
304
|
+
& .preview-card,
|
|
305
|
+
& .details-card,
|
|
306
|
+
& .post-form { width: 100%; }
|
|
156
307
|
& .video-preview { width: 100%; aspect-ratio: 16 / 9; margin-bottom: 16px; overflow: hidden; border-radius: 6px; background: #05070a; }
|
|
157
308
|
& .video-preview iframe,
|
|
158
309
|
& .video-preview video { display: block; width: 100%; height: 100%; border: 0; object-fit: contain; }
|
|
159
310
|
& .description-preview { color: var(--pt-muted); overflow-wrap: anywhere; }
|
|
160
|
-
& .preview-action { display: flex; justify-content: flex-end; margin-top: 16px; }
|
|
161
|
-
& .
|
|
162
|
-
&
|
|
163
|
-
& dt { color: var(--pt-muted); }
|
|
164
|
-
& dd { margin: 0; overflow-wrap: anywhere; }
|
|
311
|
+
& .preview-action { display: flex; align-items: center; justify-content: flex-end; gap: 12px; margin-top: 16px; }
|
|
312
|
+
& .preview-back { margin-right: auto; color: var(--pt-muted); }
|
|
313
|
+
& .details-actions { margin-top: 16px; }
|
|
165
314
|
& .refresh-button { margin-top: 14px; }
|
|
166
315
|
& .metadata { max-width: 100%; overflow-x: auto; margin: 14px 0 0; padding: 12px; border-radius: 6px; background: var(--pt-code); font-size: 12px; }
|
|
316
|
+
|
|
167
317
|
& .translation-list { border-top: 1px solid var(--pt-border); }
|
|
168
318
|
& .translation-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--pt-border); }
|
|
169
319
|
& .translation-name { display: flex; min-width: 0; align-items: center; flex-wrap: wrap; gap: 8px; }
|
|
170
320
|
& .translation-form { display: flex; align-items: flex-end; gap: 8px; margin-top: 16px; }
|
|
171
321
|
& .translation-form .field { flex: 1 1 auto; margin: 0; }
|
|
172
322
|
& .translation-complete { margin: 14px 0 0; }
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
& .actions { margin: 0; }
|
|
176
|
-
& .details-actions { margin-top: 16px; }
|
|
177
|
-
& .actions form { display: inline; }
|
|
178
|
-
& .form-actions input[type="submit"] { width: auto; }
|
|
179
|
-
& .post-form { width: 100%; }
|
|
323
|
+
|
|
324
|
+
/* Authoring form. */
|
|
180
325
|
& .form-card { padding: 0; }
|
|
181
326
|
& .form-section { padding: 12px 0 4px; }
|
|
182
327
|
& .form-section + .form-section { margin-top: 4px; }
|
|
183
328
|
& .form-section h2 { margin-bottom: 12px; }
|
|
184
|
-
& .field { margin-bottom: 16px; }
|
|
185
|
-
& .field label { display: block; margin-bottom: 5px; font-weight: 600; }
|
|
186
|
-
& .field small { display: block; margin-top: 5px; color: var(--pt-muted); }
|
|
187
329
|
& .section-intro { margin: -4px 0 12px; color: var(--pt-muted); }
|
|
188
|
-
& .
|
|
330
|
+
& .form-video-preview { margin: 2px 0 16px; }
|
|
331
|
+
& .form-video-preview .video-preview { margin-bottom: 6px; }
|
|
332
|
+
& .form-video-preview > small { color: var(--pt-muted); }
|
|
333
|
+
& .checkbox { display: flex !important; align-items: center; gap: 8px; margin-top: 8px; font-weight: 400 !important; }
|
|
334
|
+
& .checkbox input { width: 18px; min-height: 18px; }
|
|
335
|
+
/* Radio cards: what the tour does, and where its video comes from. */
|
|
189
336
|
& .action-picker,
|
|
190
337
|
& .video-source-picker { display: grid; gap: 10px; min-width: 0; margin: 0 0 18px; padding: 0; border: 0; }
|
|
191
338
|
& .action-picker { grid-template-columns: repeat(3, 1fr); }
|
|
@@ -198,14 +345,7 @@
|
|
|
198
345
|
& .action-choice strong,
|
|
199
346
|
& .action-choice small { display: block; }
|
|
200
347
|
& .action-choice small { margin-top: 3px; color: var(--pt-muted); font-weight: 400; line-height: 1.35; }
|
|
201
|
-
|
|
202
|
-
& .preview-back { margin-right: auto; color: var(--pt-muted); }
|
|
203
|
-
& .checkbox { display: flex !important; align-items: center; gap: 8px; margin-top: 8px; font-weight: 400 !important; }
|
|
204
|
-
& .checkbox input { width: 18px; min-height: 18px; }
|
|
205
|
-
& .form-actions { padding-top: 12px; }
|
|
206
|
-
& .form-video-preview { margin: 2px 0 16px; }
|
|
207
|
-
& .form-video-preview .video-preview { margin-bottom: 6px; }
|
|
208
|
-
& .form-video-preview > small { color: var(--pt-muted); }
|
|
348
|
+
|
|
209
349
|
& .rich-editor { overflow: hidden; border: 1px solid var(--pt-border); border-radius: 8px; background: var(--pt-surface); }
|
|
210
350
|
& .rich-toolbar { display: flex; gap: 4px; padding: 7px; border-bottom: 1px solid var(--pt-border); background: var(--pt-bg); }
|
|
211
351
|
& .rich-toolbar button { width: 36px; height: 34px; min-height: 34px; padding: 0; border-color: transparent; background: transparent; font-weight: 600; }
|
|
@@ -213,15 +353,16 @@
|
|
|
213
353
|
& .rich-content:focus { box-shadow: inset 0 0 0 2px var(--pt-accent); }
|
|
214
354
|
}
|
|
215
355
|
|
|
216
|
-
/*
|
|
217
|
-
|
|
218
|
-
.pt-page {
|
|
219
|
-
.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; }
|
|
220
|
-
.pt-index, .pt-index .container { height: 100vh; height: 100dvh; }
|
|
221
|
-
.pt-index { overflow: hidden; }
|
|
222
|
-
.pt-index .container { display: flex; max-width: 1280px; flex-direction: column; padding-bottom: 16px; }
|
|
223
|
-
.pt-index .page-head { flex: 0 0 auto; }
|
|
224
|
-
.pt-show { min-height: 100vh; overflow: auto; }
|
|
225
|
-
.pt-show .post-panel { width: 100%; }
|
|
226
|
-
.pt-show .detail-scroll { overflow: visible; }
|
|
356
|
+
/* New and edit are ordinary form pages: one column, narrower than a reading
|
|
357
|
+
page, normal scrolling. */
|
|
358
|
+
.pt-form-page { min-height: 100vh; overflow: auto; }
|
|
227
359
|
.pt-form-page .container { max-width: 820px; }
|
|
360
|
+
|
|
361
|
+
@media (max-width: 760px) {
|
|
362
|
+
.pt-dashboard {
|
|
363
|
+
& .page-head .button { min-height: 36px; }
|
|
364
|
+
& .action-picker,
|
|
365
|
+
& .video-source-picker { grid-template-columns: 1fr; }
|
|
366
|
+
& .form-card { padding: 0; }
|
|
367
|
+
}
|
|
368
|
+
}
|