studio-engine 0.23.0 → 0.24.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/CHANGELOG.md +44 -0
- data/README.md +22 -0
- data/app/assets/tailwind/studio_engine/engine.css +59 -0
- data/app/controllers/concerns/studio/error_handling.rb +34 -6
- data/app/views/layouts/studio/_head.html.erb +4 -2
- data/app/views/layouts/studio/_smooth_load.html.erb +15 -0
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +14 -0
- metadata +4 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2aed8dd0e8d45e41602035462f0b60d01414ac3ea2f41484cbff9d4a40384cb8
|
|
4
|
+
data.tar.gz: e7aa1bfef279150c47cf3bd709de81b5b174f9027252a177a58a19e4609cf85c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2b0ed377abc9501ce2a0fa19cc17d547e02498673ac15265ceac38555bf1fa241996e52c7dc5af337677a3436cfeb2209dd47f75fe70757bea96cebca21194e1
|
|
7
|
+
data.tar.gz: 1bd498dc182950382bfe3dca7fb5056bf6e2ff5760161ee8edf5481ef3c444a35ca7d4c51fd682b5d62f8e6c30aec787297cf537daba0956b8aefcc810b6172c
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,50 @@
|
|
|
2
2
|
|
|
3
3
|
The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html) — `MAJOR.MINOR.PATCH`. Consumer Rails apps install the released RubyGems package with `gem "studio-engine", "~> 0.6"`; bumping the gem version and updating consumer lockfiles is a release.
|
|
4
4
|
|
|
5
|
+
## 0.24.0 — 2026-07-28
|
|
6
|
+
|
|
7
|
+
The **smooth-load convention**, opt-in per app. Pages materialize behind the
|
|
8
|
+
current one and present themselves with a view transition — navigation renders
|
|
9
|
+
exactly once, with no stale-preview flash. Modifies an existing primitive:
|
|
10
|
+
`layouts/studio/_head` gains the convention partial and a configurable nav
|
|
11
|
+
spinner minimum.
|
|
12
|
+
|
|
13
|
+
**One ungated visual change on the bump, opt-in or not:** `.turbo-progress-bar`
|
|
14
|
+
in `engine.css` restyles every consumer's Turbo progress bar from Turbo's
|
|
15
|
+
default blue to the app's theme CTA color (3px, `var(--color-cta)` — defined in
|
|
16
|
+
both themes by the theme resolver, and the rule wins Turbo's injected-first
|
|
17
|
+
stylesheet cascade). Everything else is inert until an app sets
|
|
18
|
+
`Studio.smooth_load = true`.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **Smooth-load convention (opt-in)** — `Studio.smooth_load` (default OFF)
|
|
23
|
+
renders `layouts/studio/_smooth_load` from `_head`: the `view-transition`
|
|
24
|
+
meta (Turbo 8 wraps page swaps in `document.startViewTransition`) plus
|
|
25
|
+
`turbo-cache-control: no-preview`, so the next page materializes behind the
|
|
26
|
+
current one and presents itself — navigation renders exactly once. An app
|
|
27
|
+
with known multi-second pages should fix those before opting in (no-preview
|
|
28
|
+
holds the old page until the fresh response arrives).
|
|
29
|
+
- **`Studio.nav_spinner_min_ms`** — the nav spinner's minimum display time,
|
|
30
|
+
previously hardcoded to 2500 in `_head`. Default unchanged (2500); smooth-load
|
|
31
|
+
apps typically drop it to ~300 so fast loads never linger on a spinner.
|
|
32
|
+
- **Smooth-load CSS in `engine.css`** — root fade-out / rise-in view-transition
|
|
33
|
+
keyframes (inert until the metas render), the `.vt-pinned-header` opt-in
|
|
34
|
+
utility (exactly one per page — a duplicate `view-transition-name` silently
|
|
35
|
+
disables the transition), the theme-colored `.turbo-progress-bar` noted
|
|
36
|
+
above, and a `prefers-reduced-motion` kill switch app e2e suites can lean on
|
|
37
|
+
(`reducedMotion: "reduce"`).
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- **`rescue_from` order in `Studio::ErrorHandling`** — `RecordNotFound` now
|
|
42
|
+
resolves to `handle_not_found` (Rescuable matches last-registered first, so
|
|
43
|
+
the catch-all must register first). A missing record renders a real 404 from
|
|
44
|
+
`public/404.html` and **creates no ErrorLog row**; previously it was shadowed
|
|
45
|
+
by the catch-all, logged as an unexpected error, and soft-404'd (302 to root)
|
|
46
|
+
in production HTML. Per-host behavior changes only as each app bumps to this
|
|
47
|
+
release.
|
|
48
|
+
|
|
5
49
|
## 0.23.0 — 2026-07-28
|
|
6
50
|
|
|
7
51
|
Phase D, slice 1 — the **board primitive**. The three near-identical McRitchie
|
data/README.md
CHANGED
|
@@ -42,6 +42,16 @@ Studio.configure do |config|
|
|
|
42
42
|
)
|
|
43
43
|
config.theme_primary = "#4BAF50" # Override default violet
|
|
44
44
|
config.theme_logos = ["logo.svg"]
|
|
45
|
+
|
|
46
|
+
# Smooth-load convention (default OFF). Renders the view-transition +
|
|
47
|
+
# no-preview metas: Turbo page swaps materialize behind the current page and
|
|
48
|
+
# present with a view transition, exactly one render per navigation. Fix any
|
|
49
|
+
# multi-second pages BEFORE opting in — no-preview holds the old page until
|
|
50
|
+
# the fresh response arrives.
|
|
51
|
+
config.smooth_load = true
|
|
52
|
+
# Nav spinner minimum display (default 2500). Smooth-load apps typically
|
|
53
|
+
# drop to ~300; keep the high floor if multi-second ops ride the spinner.
|
|
54
|
+
config.nav_spinner_min_ms = 300
|
|
45
55
|
end
|
|
46
56
|
```
|
|
47
57
|
|
|
@@ -114,6 +124,18 @@ privileges while impersonating.
|
|
|
114
124
|
|
|
115
125
|
## UI Primitives
|
|
116
126
|
|
|
127
|
+
### Smooth-load header pin — `.vt-pinned-header`
|
|
128
|
+
|
|
129
|
+
When `Studio.smooth_load` is on, put `vt-pinned-header` on the app's sticky
|
|
130
|
+
header: it gets its own named view-transition group, so page content
|
|
131
|
+
transitions beneath a navbar that stays put (or smoothly morphs heights).
|
|
132
|
+
**Exactly one element per page** — a duplicate `view-transition-name` makes
|
|
133
|
+
the browser silently skip the whole transition, with no error and no animation.
|
|
134
|
+
|
|
135
|
+
```erb
|
|
136
|
+
<header class="sticky top-0 vt-pinned-header ...">
|
|
137
|
+
```
|
|
138
|
+
|
|
117
139
|
Render `components/emoji_swap` inside a link or button with the `group` class to
|
|
118
140
|
slide between two emoji on hover and keyboard focus. The CSS ships through
|
|
119
141
|
`studio_theme_css_tag`, including a reduced-motion fade fallback.
|
|
@@ -180,3 +180,62 @@
|
|
|
180
180
|
@utility btn-lg {
|
|
181
181
|
@apply px-8 py-3 text-base rounded-xl shadow-sm;
|
|
182
182
|
}
|
|
183
|
+
|
|
184
|
+
/* -- Smooth-load convention ------------------------------------------------
|
|
185
|
+
Paired with layouts/studio/_smooth_load.html.erb (the Studio.smooth_load
|
|
186
|
+
metas that opt Turbo into document.startViewTransition). These rules are
|
|
187
|
+
inert until an app renders those metas, so shipping them in the auto-bundled
|
|
188
|
+
layer costs nothing. The outgoing page fades while the incoming one rises in
|
|
189
|
+
underneath — quick enough to read as polish, not as a wait. */
|
|
190
|
+
::view-transition-old(root) {
|
|
191
|
+
animation: vt-page-out 160ms cubic-bezier(0.4, 0, 1, 1) both;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
::view-transition-new(root) {
|
|
195
|
+
animation: vt-page-in 220ms cubic-bezier(0, 0, 0.2, 1) both;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@keyframes vt-page-out {
|
|
199
|
+
to {
|
|
200
|
+
opacity: 0;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
@keyframes vt-page-in {
|
|
205
|
+
from {
|
|
206
|
+
opacity: 0;
|
|
207
|
+
transform: translateY(8px);
|
|
208
|
+
}
|
|
209
|
+
to {
|
|
210
|
+
opacity: 1;
|
|
211
|
+
transform: none;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* Opt-in header pin: put this on the app's sticky header so it gets its own
|
|
216
|
+
named transition group, lifted out of the root snapshot — page content
|
|
217
|
+
transitions beneath a navbar that stays put (or smoothly morphs heights).
|
|
218
|
+
EXACTLY ONE element per page: a duplicate view-transition-name makes the
|
|
219
|
+
browser silently skip the whole transition. */
|
|
220
|
+
@utility vt-pinned-header {
|
|
221
|
+
view-transition-name: studio-header;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* Turbo's progress bar is the slow-load feedback under no-preview (the old
|
|
225
|
+
page holds until the fresh response). Theme it so every app's bar matches
|
|
226
|
+
its brand instead of Turbo's default. */
|
|
227
|
+
.turbo-progress-bar {
|
|
228
|
+
height: 3px;
|
|
229
|
+
background: var(--color-cta);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/* Reduced motion: keep the single-render swap, drop the choreography. Also
|
|
233
|
+
what the apps' e2e suites lean on (reducedMotion: "reduce") for stable
|
|
234
|
+
clicks. */
|
|
235
|
+
@media (prefers-reduced-motion: reduce) {
|
|
236
|
+
::view-transition-group(*),
|
|
237
|
+
::view-transition-old(*),
|
|
238
|
+
::view-transition-new(*) {
|
|
239
|
+
animation: none !important;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -3,8 +3,15 @@ module Studio
|
|
|
3
3
|
extend ActiveSupport::Concern
|
|
4
4
|
|
|
5
5
|
included do
|
|
6
|
-
|
|
6
|
+
# ORDER IS LOAD-BEARING — ActiveSupport::Rescuable resolves handlers with
|
|
7
|
+
# `reverse_each`, so the LAST matching rescue_from registered wins. The
|
|
8
|
+
# catch-all must therefore be declared FIRST and the specific handlers
|
|
9
|
+
# after it, or StandardError shadows every one of them: RecordNotFound
|
|
10
|
+
# rendered "Something went wrong" and wrote an ErrorLog row on every 404,
|
|
11
|
+
# fleet-wide, while handle_not_found sat unreachable. Add new specific
|
|
12
|
+
# handlers BELOW this line, never above it.
|
|
7
13
|
rescue_from StandardError, with: :handle_unexpected_error
|
|
14
|
+
rescue_from ActiveRecord::RecordNotFound, with: :handle_not_found
|
|
8
15
|
|
|
9
16
|
before_action :require_authentication
|
|
10
17
|
|
|
@@ -195,12 +202,33 @@ module Studio
|
|
|
195
202
|
raise e
|
|
196
203
|
end
|
|
197
204
|
|
|
198
|
-
# Layer 1:
|
|
205
|
+
# Layer 1: RecordNotFound — Rails' own 404, never an ErrorLog row.
|
|
206
|
+
#
|
|
207
|
+
# This handler exists for exactly ONE reason: to stop `handle_unexpected_error`
|
|
208
|
+
# from claiming a RecordNotFound and writing an ErrorLog row for it. It
|
|
209
|
+
# deliberately renders NOTHING and re-raises, which hands the exception to
|
|
210
|
+
# `ActionDispatch::PublicExceptions` — the same path an app with no rescue_from
|
|
211
|
+
# at all takes. Registering the handler and re-raising is therefore not a
|
|
212
|
+
# no-op: matching here is what keeps the catch-all's `create_error_log` from
|
|
213
|
+
# running (a raise inside a rescue_from handler propagates out; Rescuable does
|
|
214
|
+
# not re-dispatch it into another handler).
|
|
215
|
+
#
|
|
216
|
+
# Why nothing is rendered here. The method was unreachable from the initial
|
|
217
|
+
# commit until the rescue_from order was fixed (see the note in `included`),
|
|
218
|
+
# so whatever it did had never actually run — consumers have been served
|
|
219
|
+
# Rails' standard 404 all along. Anything rendered here would therefore be a
|
|
220
|
+
# NEW fleet-wide behaviour, not a restoration: an earlier attempt at this fix
|
|
221
|
+
# rendered public/404.html and silently flipped `Accept: */*` clients (curl,
|
|
222
|
+
# uptime monitors, crawlers, link previewers) from `404 text/html` to
|
|
223
|
+
# `404 application/json`, changed the JSON body shape, and dropped the
|
|
224
|
+
# DebugExceptions backtrace in development. Re-raising keeps every one of
|
|
225
|
+
# those intact, along with `public/404.<locale>.html` and any
|
|
226
|
+
# `config.exceptions_app` a host configures.
|
|
227
|
+
#
|
|
228
|
+
# The bug being fixed is the ErrorLog pollution and the "Something went wrong"
|
|
229
|
+
# copy — never the status code, the format, or the body.
|
|
199
230
|
def handle_not_found(exception)
|
|
200
|
-
|
|
201
|
-
format.html { redirect_to root_path, alert: "Not found" }
|
|
202
|
-
format.json { render json: { error: "Not found" }, status: :not_found }
|
|
203
|
-
end
|
|
231
|
+
raise exception
|
|
204
232
|
end
|
|
205
233
|
|
|
206
234
|
# Layer 1: Catch-all for unexpected errors — log + friendly response.
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
11
11
|
<%= csrf_meta_tags %>
|
|
12
12
|
<%= csp_meta_tag %>
|
|
13
|
+
<%= render "layouts/studio/smooth_load" %>
|
|
13
14
|
<link rel="icon" type="image/png" href="/favicon.png">
|
|
14
15
|
|
|
15
16
|
<%= yield :head %>
|
|
@@ -46,9 +47,10 @@
|
|
|
46
47
|
</script>
|
|
47
48
|
<script>
|
|
48
49
|
// Nav spinner — scale morph between theme toggle and loading spinner
|
|
49
|
-
// Minimum display time prevents quick flashes
|
|
50
|
+
// Minimum display time prevents quick flashes; per-app via
|
|
51
|
+
// Studio.nav_spinner_min_ms (smooth-load apps drop it to ~300).
|
|
50
52
|
window._navSpinnerShownAt = 0;
|
|
51
|
-
window._navSpinnerMinMs =
|
|
53
|
+
window._navSpinnerMinMs = <%= Studio.nav_spinner_min_ms.to_i %>;
|
|
52
54
|
window.showNavSpinner = function() {
|
|
53
55
|
window._navSpinnerShownAt = Date.now();
|
|
54
56
|
document.querySelectorAll('.nav-toggle-icon').forEach(function(e) { e.style.opacity = '0'; e.style.transform = 'scale(0) rotate(90deg)'; });
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<%# Smooth-load convention (opt-in via Studio.smooth_load) — the next page
|
|
2
|
+
materializes behind the current one, then presents itself. Turbo 8 sees the
|
|
3
|
+
view-transition meta and wraps its body swap in document.startViewTransition;
|
|
4
|
+
browsers without support get the instant swap. no-preview stops application
|
|
5
|
+
visits from painting a stale cache snapshot before the fresh response — the
|
|
6
|
+
current page holds until the new one is ready, so navigation renders exactly
|
|
7
|
+
once. Restoration visits (back/forward) still restore instantly from cache.
|
|
8
|
+
Transition styling ships in engine.css ("Smooth-load convention" block);
|
|
9
|
+
pin an app's sticky header with the .vt-pinned-header utility (exactly one
|
|
10
|
+
element per page — a duplicate view-transition-name silently disables the
|
|
11
|
+
whole transition). %>
|
|
12
|
+
<% if Studio.smooth_load %>
|
|
13
|
+
<meta name="view-transition" content="same-origin">
|
|
14
|
+
<meta name="turbo-cache-control" content="no-preview">
|
|
15
|
+
<% end %>
|
data/lib/studio/version.rb
CHANGED
data/lib/studio.rb
CHANGED
|
@@ -25,6 +25,20 @@ module Studio
|
|
|
25
25
|
mattr_accessor :theme_logos, default: []
|
|
26
26
|
mattr_accessor :sticky_table_headers, default: false
|
|
27
27
|
|
|
28
|
+
# ---- Smooth-load convention ---------------------------------------------
|
|
29
|
+
# Opt-in per app: renders the view-transition + no-preview metas so Turbo 8
|
|
30
|
+
# page swaps materialize behind the current page and present with a view
|
|
31
|
+
# transition (layouts/studio/_smooth_load). OFF by default — an app with
|
|
32
|
+
# known multi-second pages should fix those before opting in, because
|
|
33
|
+
# no-preview holds the old page until the fresh response arrives.
|
|
34
|
+
mattr_accessor :smooth_load, default: false
|
|
35
|
+
|
|
36
|
+
# Minimum ms the nav spinner stays visible once shown (_head.html.erb).
|
|
37
|
+
# Apps wrapping multi-second operations in the spinner (e.g. Solana RPC)
|
|
38
|
+
# keep the high default to avoid flicker; smooth-load apps typically drop
|
|
39
|
+
# this to ~300 so fast loads never linger on a spinner.
|
|
40
|
+
mattr_accessor :nav_spinner_min_ms, default: 2500
|
|
41
|
+
|
|
28
42
|
# ---- Authentication ------------------------------------------------------
|
|
29
43
|
# Which sign-in methods this app offers. The shared login/signup views render
|
|
30
44
|
# a button/field per enabled method (gate with Studio.auth_method?). Order is
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: studio-engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.24.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex McRitchie
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: rails
|
|
@@ -242,6 +241,7 @@ files:
|
|
|
242
241
|
- app/views/layouts/branded_mailer.html.erb
|
|
243
242
|
- app/views/layouts/studio/_flash.html.erb
|
|
244
243
|
- app/views/layouts/studio/_head.html.erb
|
|
244
|
+
- app/views/layouts/studio/_smooth_load.html.erb
|
|
245
245
|
- app/views/magic_links/confirm.html.erb
|
|
246
246
|
- app/views/navbar/show.html.erb
|
|
247
247
|
- app/views/registrations/new.html.erb
|
|
@@ -349,7 +349,6 @@ metadata:
|
|
|
349
349
|
source_code_uri: https://github.com/amcritchie/studio-engine/tree/main
|
|
350
350
|
bug_tracker_uri: https://github.com/amcritchie/studio-engine/issues
|
|
351
351
|
changelog_uri: https://github.com/amcritchie/studio-engine/blob/main/CHANGELOG.md
|
|
352
|
-
post_install_message:
|
|
353
352
|
rdoc_options: []
|
|
354
353
|
require_paths:
|
|
355
354
|
- lib
|
|
@@ -364,8 +363,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
364
363
|
- !ruby/object:Gem::Version
|
|
365
364
|
version: '0'
|
|
366
365
|
requirements: []
|
|
367
|
-
rubygems_version:
|
|
368
|
-
signing_key:
|
|
366
|
+
rubygems_version: 4.0.9
|
|
369
367
|
specification_version: 4
|
|
370
368
|
summary: Shared Rails engine providing auth, SSO, error logging, theming, and S3-backed
|
|
371
369
|
image caching
|