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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e37d3038ae5d72bbd0829247f562aa744960458dc9e54faa73c38fc1a2adcb83
4
- data.tar.gz: a74cb5455f9977009b3499981dec577bb7df9782a5ab2f08a2bda92fe9cbe9c5
3
+ metadata.gz: 2aed8dd0e8d45e41602035462f0b60d01414ac3ea2f41484cbff9d4a40384cb8
4
+ data.tar.gz: e7aa1bfef279150c47cf3bd709de81b5b174f9027252a177a58a19e4609cf85c
5
5
  SHA512:
6
- metadata.gz: 45ecd0d11aef5443dc5342013e39124b52ecf67b46261b096e45a57b088a22335e5ce7c1b58f894c0878888cd1dc087b886f76e7ed15875ec08759222a52ae74
7
- data.tar.gz: 2e1b9c6dd268214317af8a93aa27bf49e1aa2ebff4e487010a81fd5f332ee261a0d1011fb636cbd9577dd3300c23ba0b1edc6e40e7b56366d83c7314d40a0636
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
- rescue_from ActiveRecord::RecordNotFound, with: :handle_not_found
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: Catch-all for RecordNotFound — 404 redirect, no logging.
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
- respond_to do |format|
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 = 2500;
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 %>
@@ -1,3 +1,3 @@
1
1
  module Studio
2
- VERSION = "0.23.0"
2
+ VERSION = "0.24.0"
3
3
  end
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.23.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: 2026-07-28 00:00:00.000000000 Z
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: 3.5.22
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