studio-engine 0.25.1 → 0.26.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d87b5711dd0833ea78e20b6f3174dd57e5cc3b0311e56f962e8954a408943963
4
- data.tar.gz: 81708f5604bdebaa3f08504169e4bf1a55ba8ea9d8856f04594dbe49295d553c
3
+ metadata.gz: 8d1e665b17a360de65ba411842a7c8368e8077160cf34e4a218ebbe1857d0988
4
+ data.tar.gz: 1b9ebc66583eeacb8c65d97f4ec1eb90b0451829cf7ed263d0a5cd8895b96b88
5
5
  SHA512:
6
- metadata.gz: b3ffce4a4ba6b91444902123ee106f9f88fec2be0606d69f310ff464bfe336ca444de94f85c2c81873feb335080e2a9fc01390ff48df7f87977e645e0c3397ac
7
- data.tar.gz: 6584381c67a206525171c7195ded0999f8ce20ad896196aa8259b116e450c7db2a23ed95193dcb8281e49f57f09c5010817ce67e127bcc3d10c517ca4a67ca16
6
+ metadata.gz: 61d7d9f100144b0683bfa27a12a86ad4b0cac8cc3280c067c9f016a99edf98b0c28f73cf38e2f212d26183336f519d03f633970b15381ebcf165ba9ee143997b
7
+ data.tar.gz: 56b58a13c533dd060fd5d8313987735123e6ed1bd1f148d15ef51ea7306815f365dabf900770f0b8de84fc81b6588bf80e61545ba5a8440b49243a3db7883f52
data/CHANGELOG.md CHANGED
@@ -2,6 +2,88 @@
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.26.1 — 2026-07-28
6
+
7
+ **Fix the `/admin/style` Design System page breaking when reached via Turbo Drive
8
+ navigation (the admin sidebar "Design System" link).** Reached by a direct page
9
+ load the page was fine; reached by an in-app Turbo visit its modals would not open
10
+ on a card click and EVERY specimen glow ring lit at once. Both symptoms were one
11
+ cause: the page-scoped `dsModals` (and `dsSolanaModal`) Alpine store was registered
12
+ ONLY inside a `document.addEventListener('alpine:init', …)` handler in the page
13
+ body. Alpine loads via a deferred CDN `<script>` in the engine head and fires
14
+ `alpine:init` exactly once, on the first full-document load; Turbo Drive advance
15
+ visits swap `<body>` without reloading that head script, so `alpine:init` never
16
+ fires again — and because the registration lives only in the `/admin/style` body,
17
+ it was absent during that one `alpine:init`. The specimen cards' `x-data` still
18
+ re-initialize on the Turbo body swap (Alpine's MutationObserver), so their
19
+ `@click="$store.dsModals.open(…)"`, `:style` glow bindings, and the host's
20
+ `<template x-if="$store.dsModals.current()…">` all evaluated against an undefined
21
+ store and threw `Cannot read properties of undefined (reading 'current')` — no
22
+ modal opened, and the throwing `:style` wiped each card's static inline
23
+ `--studio-team-glow-opacity: 0`, so the CSS default of `1` relit all rings.
24
+
25
+ ### Fixed
26
+
27
+ - **`style/_modals`** — the `dsModals` + `dsSolanaModal` registration is now a
28
+ named `registerDsModals()` invoked through the same dual-guard the engine
29
+ already uses for `studio/modals/_image_upload`'s `cropPhotoModal`:
30
+ `if (window.Alpine) registerDsModals(); else document.addEventListener('alpine:init', registerDsModals);`.
31
+ On a Turbo visit Alpine has already started, so the store registers immediately
32
+ (the body script re-runs on every Turbo render) and `$store.dsModals` is defined
33
+ before the cards' bindings evaluate; on a first load Alpine has not booted yet,
34
+ so it still defers to `alpine:init`. The `if (Alpine.store('dsModals')) return;`
35
+ idempotency guard is unchanged, so a later `alpine:init` is a no-op. No consumer
36
+ change is required — `/admin/style` is an engine page.
37
+ - **Docs** — `style/_modal_specimen`'s fail-closed comment now records that the
38
+ static inline `--studio-team-glow-opacity: 0` guards the ring only while
39
+ `$store.dsModals` is DEFINED (a throwing `:style` wipes the inline value) — the
40
+ refinement of 0.24.1's "regardless of Alpine timing" claim that this bug exposed.
41
+
42
+ ### Swept
43
+
44
+ - Audited every engine `Alpine.store(...)` / `Alpine.data(...)` registration for
45
+ the same Turbo-visit fragility. `dsModals`/`dsSolanaModal` (`style/_modals`) was
46
+ the only page-scoped registration missing the guard. `studio/modals/_image_upload`
47
+ (`cropPhotoModal`) already carries it; the shared modal host (`studio/modals/_host`
48
+ → `modals`) and the theme/devMode stores (`layouts/studio/_head`) are app-wide
49
+ bootstrap rendered on the first full load, so their single `alpine:init`
50
+ registration persists across Turbo visits (verified: `$store.modals` stays
51
+ defined after a Turbo nav).
52
+
53
+ ## 0.26.0 — 2026-07-28
54
+
55
+ **The engine navbar pins itself under smooth-load, and the smooth-load CSS is
56
+ fully engine-owned.** Since 0.24 the `vt-pinned-header` pin was opt-in but the
57
+ engine's own `layouts/_navbar` never carried it, so a host that wanted the
58
+ pinned-header transition had to SHADOW the whole partial just to add one class
59
+ (acquisition-studio did exactly that — the known override-drift trap). The
60
+ navbar now self-pins when `Studio.smooth_load` is on, and the engine also ships
61
+ the `studio-header` cross-fade suppression that mcritchie-studio and
62
+ turf-monster had been carrying app-side as a bridge. **Consumer cleanup this
63
+ version unlocks:** acquisition-studio deletes its entire
64
+ `app/views/layouts/_navbar.html.erb` override; mcritchie-studio and
65
+ turf-monster delete their app-side `::view-transition-old(studio-header)` /
66
+ `::view-transition-new(studio-header) { animation: none; }` bridge blocks.
67
+
68
+ ### Changed
69
+
70
+ - **`layouts/_navbar`** — the sticky header adds `vt-pinned-header` itself when
71
+ `Studio.smooth_load` is on, non-preview branch ONLY (preview renders can
72
+ repeat per page, and a duplicate `view-transition-name` silently disables
73
+ every transition). With the flag off nothing extra renders, so a plain gem
74
+ bump changes no opted-out app.
75
+ - **`engine.css`** — new `::view-transition-old(studio-header)` /
76
+ `::view-transition-new(studio-header) { animation: none; }` rule beside the
77
+ smooth-load block: it suppresses the UA-default ~250ms plus-lighter
78
+ cross-fade between the header's two snapshots, which double-drew the wordmark
79
+ and buttons on any navigation from a scrolled page (collapsed header →
80
+ expanded at top). Lifts the identical bridge rule the hub and turf-monster
81
+ carried app-side.
82
+ - **`engine.css`** — `.turbo-progress-bar` background becomes
83
+ `var(--color-cta, #0076ff)`: the fallback (Turbo's own default blue) keeps
84
+ the bar visible in a layout that never renders the runtime theme block
85
+ (`studio_theme_css_tag`), where `--color-cta` is unset.
86
+
5
87
  ## 0.25.1 — 2026-07-28
6
88
 
7
89
  **Fix the Profile Leveling save-close regression 0.25.0 introduced.** The 0.25.0
@@ -110,6 +192,12 @@ follows the step machine), so this surfaced as a flash-of-all-glow on load.
110
192
  unchanged. The `engine-motion.css` `.studio-team-glow` default is untouched
111
193
  (the always-on Tricks demos depend on it).
112
194
 
195
+ > **Refined in 0.26.1:** "regardless of Alpine timing" holds only while
196
+ > `$store.dsModals` is defined. On a Turbo Drive visit the store went
197
+ > unregistered, the reactive `:style` threw, and Alpine wiped this inline
198
+ > default — relighting every ring. 0.26.1 dual-guards the store registration so
199
+ > it survives Turbo visits, restoring the fail-closed guarantee.
200
+
113
201
  ## 0.24.0 — 2026-07-28
114
202
 
115
203
  The **smooth-load convention**, opt-in per app. Pages materialize behind the
@@ -221,12 +221,28 @@
221
221
  view-transition-name: studio-header;
222
222
  }
223
223
 
224
+ /* Suppress the UA default on the pinned header's own group: without this the
225
+ browser runs its ~250ms plus-lighter cross-fade between the header's old and
226
+ new snapshots, and on any navigation from a scrolled page (collapsed header
227
+ → expanded at top) the wordmark and buttons draw TWICE, offset by the height
228
+ delta, until the blend settles. animation: none shows the incoming header
229
+ immediately while the group box still morphs its height. Engine-owned as of
230
+ 0.26.0 — this lifts the identical bridge rule mcritchie-studio and
231
+ turf-monster carried app-side; consumers delete their copies when adopting
232
+ this version. */
233
+ ::view-transition-old(studio-header),
234
+ ::view-transition-new(studio-header) {
235
+ animation: none;
236
+ }
237
+
224
238
  /* Turbo's progress bar is the slow-load feedback under no-preview (the old
225
239
  page holds until the fresh response). Theme it so every app's bar matches
226
- its brand instead of Turbo's default. */
240
+ its brand instead of Turbo's default. The fallback (#0076ff, Turbo's own
241
+ default blue) keeps the bar visible in a layout that never renders the
242
+ runtime theme block (studio_theme_css_tag), where --color-cta is unset. */
227
243
  .turbo-progress-bar {
228
244
  height: 3px;
229
- background: var(--color-cta);
245
+ background: var(--color-cta, #0076ff);
230
246
  }
231
247
 
232
248
  /* Reduced motion: keep the single-render swap, drop the choreography. Also
@@ -8,6 +8,14 @@
8
8
  %>
9
9
  <% show_user = local_assigns.fetch(:show_logged_in, logged_in?) %>
10
10
  <% is_preview = local_assigns.fetch(:preview, false) %>
11
+ <%# Smooth-load self-pin (engine 0.26): when the app opts into Studio.smooth_load,
12
+ the sticky header names itself as the shared view-transition group
13
+ (vt-pinned-header) so page content transitions beneath a navbar that stays
14
+ put — hosts no longer shadow this partial just to add the class. Non-preview
15
+ branch ONLY: preview renders can repeat per page, and a duplicate
16
+ view-transition-name silently disables every transition. With the flag off,
17
+ nothing extra renders. %>
18
+ <% pin_header = Studio.smooth_load && !is_preview %>
11
19
  <% balance_html = local_assigns.fetch(:balance_html, nil) %>
12
20
  <% extra_icons_html = local_assigns.fetch(:extra_icons_html, nil) %>
13
21
  <% show_logout_link = local_assigns.fetch(:show_logout_link, false) %>
@@ -19,7 +27,7 @@
19
27
  %>
20
28
 
21
29
  <header x-data="{ scrolled: false }" <%= '@scroll.window="scrolled = scrolled ? (window.scrollY > 5) : (window.scrollY > 60)"'.html_safe unless is_preview %>
22
- class="<%= is_preview ? 'bg-page' : 'sticky top-0 z-50 bg-page transition-shadow duration-300' %>"
30
+ class="<%= is_preview ? 'bg-page' : "#{'vt-pinned-header ' if pin_header}sticky top-0 z-50 bg-page transition-shadow duration-300" %>"
23
31
  :class="scrolled && 'shadow-lg border-b border-subtle is-scrolled'">
24
32
  <style>
25
33
  .user-nav-col { width: 14rem; }
@@ -59,7 +59,14 @@
59
59
  default (--studio-team-glow-opacity of 1) so the ring is OFF at first
60
60
  paint, BEFORE Alpine hydrates the reactive :style below. Without it every
61
61
  glow card shows its ring until Alpine boots (the fail-open flash). The
62
- :style then drives 0 to 0.95 with the 0.4s cross-fade. Keep BOTH. %>
62
+ :style then drives 0 to 0.95 with the 0.4s cross-fade. Keep BOTH.
63
+ CAVEAT: this fail-closed default holds only while $store.dsModals is
64
+ DEFINED. glow_when dereferences the store; if the store is missing the
65
+ reactive :style THROWS, and Alpine's style bind wipes this inline value
66
+ on the way, so the CSS default of 1 relights every ring. That is exactly
67
+ the Turbo-nav store gap fixed in 0.26.1 (style/_modals dual-guards the
68
+ dsModals registration so it survives Turbo visits, not just first load).
69
+ So the guarantee is: static inline 0 PLUS a registered dsModals. %>
63
70
  <% if glow_when %>style="--studio-team-glow-opacity: 0" :style="{ '--studio-team-glow-opacity': (<%= glow_when %>) ? '0.95' : '0' }"<% end %>>
64
71
  <article
65
72
  class="card overflow-hidden flex flex-col h-full<%= " opacity-95" if disabled %><%= " cursor-pointer" if clickable %>"
@@ -107,7 +107,10 @@
107
107
  isAvailable: function () { return false; }
108
108
  };
109
109
 
110
- document.addEventListener('alpine:init', function () {
110
+ // Register the page-scoped dsModals + dsSolanaModal stores. Named (not an
111
+ // inline alpine:init callback) so the dual-guard at the end can invoke it
112
+ // directly on a Turbo visit, when alpine:init will not fire again.
113
+ function registerDsModals() {
111
114
  if (Alpine.store('dsModals')) return;
112
115
 
113
116
  // Enter/exit animation registry — keys map to a CSS class (engine-
@@ -244,7 +247,19 @@
244
247
  get ctaLabel() { return this._read('ctaLabel', ''); },
245
248
  get ctaHref() { return this._read('ctaHref', ''); }
246
249
  });
247
- });
250
+ }
251
+
252
+ // Alpine's deferred CDN <script> fires alpine:init exactly ONCE, on the
253
+ // first full-document load. Turbo Drive advance visits swap the document
254
+ // body without reloading that head script, so alpine:init never fires
255
+ // again — and
256
+ // because this dsModals registration lives ONLY in the /admin/style body,
257
+ // it was absent during that one alpine:init. Registering here on the
258
+ // Turbo-visit path (Alpine already started) keeps $store.dsModals defined,
259
+ // so the specimen cards' @click / :style / <template x-if> don't throw.
260
+ // Mirrors studio/modals/_image_upload's cropPhotoModal guard.
261
+ if (window.Alpine) { registerDsModals(); }
262
+ else { document.addEventListener('alpine:init', registerDsModals); }
248
263
  })();
249
264
  </script>
250
265
 
@@ -1,3 +1,3 @@
1
1
  module Studio
2
- VERSION = "0.25.1"
2
+ VERSION = "0.26.1"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: studio-engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.25.1
4
+ version: 0.26.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-28 00:00:00.000000000 Z
11
+ date: 2026-07-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails