studio-engine 0.124.0 → 0.125.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: b7f68b7fc8c9d951dfa0f8069db58d4d819a9e5c785a35c5319c00d1a7450fab
4
- data.tar.gz: 3674ddc7b3c29a7f58e06ee8c928bf63223a52388a191f819a90919986355e79
3
+ metadata.gz: 13bb9cfe1f6491a4dd2a199c16f734f466edad8fb38beae903033305e8fd91da
4
+ data.tar.gz: 20d1fec190a9bfe2391e4e4121f2fa11dc35729abd54a8df47e0a19346611715
5
5
  SHA512:
6
- metadata.gz: 1b8755f0f24f02d33aaf1f91bac610325e988a5cf9633ec74b31227b14f9f8582293ff0bd225b38efdc62e468b433dabb66fa14590e785355073f70d339add32
7
- data.tar.gz: '083f8e499dedaa8efb934df433a3cd99c196828d7625e77e0cd4cd3f5003d5ab57a26990eee8c031a803d4b80fb7777ce05133cb4fda5bd616a38b448a288dc6'
6
+ metadata.gz: 3687dd8d85fffecbb4c1ab84177bcdb9dee0bfd0aa87dac7dbad61a81a4921d920f9c2f1cda8cfc3bff378c4a12d3528ab015e14085b447204c745e69c84155e
7
+ data.tar.gz: 2d135f12456ce99328d5422df362a83271cd9075b9f618c80b0cf3053edccb3a383164a0c23fdc6cced5e5e4514467e90b637d5beb976f052b2b742cfd7a8b18
data/CHANGELOG.md CHANGED
@@ -4,6 +4,37 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.125.0 — 2026-11-04
8
+
9
+ ### Added
10
+
11
+ - **Page transitions across full page loads.** With `Studio.page_transition_direction` set,
12
+ a same-origin full load plays the app's direction through the browser's cross-document
13
+ view transition: the same pair of styles, keyframes and timing as a Turbo visit, with the
14
+ navbar and banners held still. This reaches strict-policy pages, which must stay their
15
+ own documents. New files, linked only with a direction: `studio/page_transition_full_load.css`
16
+ and the classic `studio/page_transition_reveal.js`, which carries the request's nonce. A
17
+ reload, a same-path load, reduced motion and a browser without support load plainly. A full
18
+ load has no morph, and it cannot wait for images, so the 500ms image cap does not apply.
19
+ With no direction, a host renders no new tag.
20
+ [`docs/PAGE_TRANSITIONS.md`](docs/PAGE_TRANSITIONS.md).
21
+
22
+ ### Fixed
23
+
24
+ - **A Turbo visit answered by `turbo-visit-control: reload` no longer plays the engine's fade**
25
+ before the full load. Turbo opens its view transition before it reads the meta, so that
26
+ transition now draws nothing.
27
+ - **The navbar no longer draws twice during a page transition** when the old header is taller
28
+ than the new one, as on Back to a scrolled page. This changes Turbo visits too: only the new
29
+ navbar draws.
30
+
31
+ - Link sidebar: a one-line row centers its icon on its label. It sat 3px low. A row with a
32
+ description keeps its icon at the top. The navbar baseline is regenerated for it.
33
+ - Page transitions: an advance visit to the same path keeps a shared `data-vt-morph` name, so
34
+ a launch panel still morphs into the chat at its own address. A refresh stays silent.
35
+ - Page transitions: a GET that redirects moves the page once. It played the move twice, because
36
+ Turbo renders the response and then visits the final address with that same response.
37
+
7
38
  ## 0.124.0 — 2026-11-04
8
39
 
9
40
  ### Added
data/README.md CHANGED
@@ -41,7 +41,7 @@ resolved.
41
41
  - **Geo**: `Studio::GeoDetection` places every visitor (IP → country + subdivision, session-cached), `Studio::GeoSetting` stores the operator's blocked countries and regions, `require_geo_allowed` locks whichever surfaces an app chooses, and the shared badge + `/admin/geo` manager ship with it. See [`docs/GEO.md`](docs/GEO.md).
42
42
  - **Site identity and link previews**: `Studio::SiteIdentity` holds the app's title, description and image, edited at `/admin/link_preview` beside a live unfurl card and read anywhere through `Studio.site_identity`. Every page unfurls with it unless it calls `link_preview image:, title:, description:`, and `Studio::LinkPreviewBots` serves preview fetchers a slim page under iMessage's 1 MiB limit. Adopt with `bin/rails g studio:site_identity`. See [`docs/LINK_PREVIEW.md`](docs/LINK_PREVIEW.md).
43
43
  - **Branded loader**: `studio_loader`, `studio_loader_pending` and `studio_loader_overlay` draw the app's own icon in its theme colours for page loads, long waits and pending buttons, in the style `config.loader` names (Breathe by default). See [`docs/LOADER.md`](docs/LOADER.md).
44
- - **App-to-app page transitions**: `config.page_transition_direction` (`:left`, `:right`, `:top`, `:bottom`) moves an app's pages one way on every page change, with the navbar and banners held still. It turns smooth-load on. See [`docs/PAGE_TRANSITIONS.md`](docs/PAGE_TRANSITIONS.md).
44
+ - **App-to-app page transitions**: `config.page_transition_direction` (`:left`, `:right`, `:top`, `:bottom`) moves an app's pages one way on every page change, full page loads included, with the navbar and banners held still. It turns smooth-load on. See [`docs/PAGE_TRANSITIONS.md`](docs/PAGE_TRANSITIONS.md).
45
45
  - **Public user page**: `/u/:username` shows a user's avatar and username (nothing else), and unfurls with their avatar, falling back to the site image. Link any username to it with `link_to_user_profile(user)` or `studio_user_profile_path(user)`. Opt in with `config.draw_public_user_routes = true`. See [`docs/PUBLIC_USER_PAGE.md`](docs/PUBLIC_USER_PAGE.md).
46
46
  - **Transactional emails**: `Studio::EmailCatalog` — every email an app sends, its type, a live preview, and its banner — plus the shared `/admin/emails` page. Every app inherits the standard emails and their artwork on day one, and can register its own workflows and upload its own banners. See [Transactional emails](#transactional-emails).
47
47
  - **Site footer**: `<%= studio_site_footer %>` in the layout renders the public site's footer. It is on by default: with nothing configured it prints the app's name, logo and the legal links the app has routes for, and no address. `config.site_footer` declares the app's own facts (brand, social profiles, link columns, an address with a live map, legal links), every part optional; `config.site_footer_address` adds a location in one setting; `config.site_footer = false` turns the footer off; `config.site_footer_chat_path` adds a standard Chat link to the Contact column. Leaflet is vendored and served by the engine. See [`docs/SITE_FOOTER.md`](docs/SITE_FOOTER.md).
@@ -0,0 +1,75 @@
1
+ // studio/page_transition_reveal: the arriving half of a full-load page transition
2
+ // (docs/PAGE_TRANSITIONS.md, "Full page loads"). Loaded only with
3
+ // Studio.page_transition_direction set.
4
+ //
5
+ // CLASSIC AND SYNCHRONOUS ON PURPOSE. pagereveal fires before the first render, and a listener
6
+ // bound after that misses it for good. A module is deferred, so it can be too late.
7
+ //
8
+ // It holds no table of styles. The leaving page drew the style with studio/page_transitions
9
+ // at pageswap and left it here. No style means the leaving page did not plan this one (its
10
+ // module had not run, or it was the same path), so the load is plain, not the browser's fade.
11
+ (function () {
12
+ var STYLE_KEY = "studio-page-transition-style"
13
+ var HOLD_CAP_MS = 3000
14
+
15
+ // Holds the first render to the end of parsing, on an arrival only (nothing has this id). A
16
+ // render of half a body would give the new picture no navbar or stack to hold still. The
17
+ // browser sets no limit on this hold, so the timer does: a slow body paints at the cap. The
18
+ // cap sits inside the browser's own limit on the transition, so the motion still plays.
19
+ function holdForTheWholePage() {
20
+ try {
21
+ if (!window.sessionStorage.getItem(STYLE_KEY)) return
22
+ } catch (_error) {
23
+ return
24
+ }
25
+ var link = document.createElement("link")
26
+ link.rel = "expect"
27
+ link.href = "#studio-page-transition-end-of-parse"
28
+ link.setAttribute("blocking", "render")
29
+ document.head.appendChild(link)
30
+ window.setTimeout(function () { link.remove() }, HOLD_CAP_MS)
31
+ }
32
+
33
+ function takeStyle() {
34
+ try {
35
+ var style = window.sessionStorage.getItem(STYLE_KEY)
36
+ window.sessionStorage.removeItem(STYLE_KEY)
37
+ return style
38
+ } catch (_error) {
39
+ return null
40
+ }
41
+ }
42
+
43
+ holdForTheWholePage()
44
+
45
+ window.addEventListener("pagereveal", function (event) {
46
+ var root = document.documentElement
47
+ // A page back from the back/forward cache still wears what it had on when it left.
48
+ delete root.dataset.pageTransition
49
+ delete root.dataset.pageTransitionVisit
50
+
51
+ var style = takeStyle()
52
+ var transition = event.viewTransition
53
+ if (!transition) return
54
+ var direction = document.querySelector('meta[name="page-transition-direction"]')
55
+ if (!style || !direction) {
56
+ transition.skipTransition()
57
+ return
58
+ }
59
+
60
+ // engine.css reads these three. The new page's picture comes after this listener, so the
61
+ // held stack and every morph name are settled in time.
62
+ root.dataset.pageTransitionDirection = direction.content
63
+ root.dataset.pageTransition = style
64
+ root.dataset.pageTransitionVisit = ""
65
+ var stack = document.querySelector("[data-vt-held], .studio-bar-stack")
66
+ if (stack) stack.setAttribute("data-vt-held", "")
67
+
68
+ var done = function () {
69
+ if (root.dataset.pageTransition !== style) return
70
+ delete root.dataset.pageTransition
71
+ delete root.dataset.pageTransitionVisit
72
+ }
73
+ transition.finished.then(done, done)
74
+ })
75
+ })()
@@ -0,0 +1,10 @@
1
+ /* Opts a document in to the browser's cross-document view transition. A separate file,
2
+ because this at-rule takes no selector: in the engine's bundle it would act on every
3
+ host. layouts/studio/_head links it only with Studio.page_transition_direction set.
4
+ Both documents must carry it. Reduced motion gets no transition at all.
5
+ docs/PAGE_TRANSITIONS.md, "Full page loads". */
6
+ @media (prefers-reduced-motion: no-preference) {
7
+ @view-transition {
8
+ navigation: auto;
9
+ }
10
+ }
@@ -674,7 +674,7 @@ html[data-page-transition]::view-transition-new(root) {
674
674
  backface-visibility: hidden;
675
675
  }
676
676
 
677
- /* A same-page visit (a refresh, a poll) draws nothing. */
677
+ /* A same-page visit (a refresh, a poll, the second half of a redirect) draws nothing. */
678
678
  html[data-page-transition="still"]::view-transition-old(root),
679
679
  html[data-page-transition="still"]::view-transition-new(root) {
680
680
  animation: none;
@@ -695,6 +695,12 @@ html[data-page-transition-direction][data-page-transition-visit] [data-vt-held]
695
695
  animation: none;
696
696
  }
697
697
 
698
+ /* A taller old navbar would show under the new one for the whole motion, then pop off.
699
+ Only the new navbar draws. Where the two match, the new one covers the same pixels. */
700
+ html[data-page-transition]:not([data-page-transition="still"])::view-transition-old(studio-header) {
701
+ opacity: 0;
702
+ }
703
+
698
704
  /* A named element moves with the page, unless both pages share its name. The
699
705
  script decides that before the old page's picture, and only then is the name let
700
706
  through, so the morph stands. data-vt-morph carries the name it marks. */
@@ -7,6 +7,10 @@
7
7
  // settled ahead of the visit: the held banner stack and the morph names are decided here on
8
8
  // turbo:visit and from the response, never first at before-render.
9
9
  //
10
+ // A full load is the other path to the same look. The new document draws the motion, so the old
11
+ // one only plans it: at pageswap it names what it holds, draws the style here, and leaves it for
12
+ // studio/page_transition_reveal, the classic script that applies it at pagereveal.
13
+ //
10
14
  // The effects are from the Codrops Page Transitions demo; the keyframes were written fresh.
11
15
 
12
16
  export const DIRECTIONS = Object.freeze({
@@ -16,9 +20,12 @@ export const DIRECTIONS = Object.freeze({
16
20
  bottom: Object.freeze(["fade-bottom", "unfold-top"]),
17
21
  })
18
22
 
19
- // A same-page visit (a refresh, a poll): the transition still runs, and draws nothing.
23
+ // A same-page visit (a refresh, a poll, the second half of a redirect): the transition still
24
+ // runs, and draws nothing.
20
25
  export const STILL = "still"
21
26
  export const READY_CAP_MS = 500
27
+ // Read by app/assets/javascripts/studio/page_transition_reveal.js too.
28
+ export const FULL_LOAD_STYLE_KEY = "studio-page-transition-style"
22
29
  const MAX_READY_IMAGES = 12
23
30
  const REDUCED_MOTION = "(prefers-reduced-motion: reduce)"
24
31
 
@@ -29,13 +36,37 @@ export function pickStyle(direction, random = Math.random) {
29
36
  }
30
37
 
31
38
  // The style for one visit, STILL, or null to leave the swap alone (the engine's fade plays,
32
- // and a morph between two pages stands).
33
- export function planVisit({ direction, from, to, transitions, sharedName }, random = Math.random) {
39
+ // and a morph between two pages stands). An advance visit to the same path can bring a new
40
+ // page, as a launch panel does when it gives way to the chat at its own address, so a name
41
+ // the two share still morphs. A refresh replaces, and stays silent.
42
+ export function planVisit({ direction, from, to, transitions, sharedName, advance }, random = Math.random) {
34
43
  if (!transitions || !from || !to || !Object.hasOwn(DIRECTIONS, direction)) return null
35
- if (from === to) return STILL
44
+ if (from === to) return advance && sharedName ? null : STILL
36
45
  return sharedName ? null : pickStyle(direction, random)
37
46
  }
38
47
 
48
+ // The style a full load should play, or null for a plain load: planVisit's own rule, so the
49
+ // two paths cannot differ. No morph, since the old page cannot read the new page's names
50
+ // before its picture, and a browser with no activation entry has nothing to compare.
51
+ export function planFullLoad({ direction, from, activation }, random = Math.random) {
52
+ const url = activation && activation.entry && activation.entry.url
53
+ if (!url) return null
54
+ const to = new URL(url).pathname
55
+ const style = planVisit({ direction, from, to, transitions: true, sharedName: false }, random)
56
+ return style === STILL ? null : style
57
+ }
58
+
59
+ // Hands the style to the next document. Storage can refuse, and then the load is plain.
60
+ export function leaveStyle(win, style) {
61
+ try {
62
+ if (style) win.sessionStorage.setItem(FULL_LOAD_STYLE_KEY, style)
63
+ else win.sessionStorage.removeItem(FULL_LOAD_STYLE_KEY)
64
+ return !!style
65
+ } catch {
66
+ return false
67
+ }
68
+ }
69
+
39
70
  // A slow image must not hold a click: past the cap the swap goes ahead.
40
71
  export function withCap(promise, ms, timer = setTimeout) {
41
72
  return Promise.race([promise, new Promise((done) => timer(done, ms))])
@@ -109,12 +140,14 @@ export function installPageTransitions(doc) {
109
140
  const known = new Map()
110
141
  let from = win.location.pathname
111
142
  let target = null
143
+ let advance = false
144
+ let rendered = false
112
145
 
113
146
  const nameTheMorph = (shared) => {
114
147
  if (shared) root.dataset.pageTransitionMorph = ""
115
148
  else delete root.dataset.pageTransitionMorph
116
149
  }
117
- const predict = (names) => nameTheMorph(target !== from && shareName(morphNames(doc), names))
150
+ const predict = (names) => nameTheMorph((target !== from || advance) && shareName(morphNames(doc), names))
118
151
 
119
152
  root.dataset.pageTransitionDirection = readDirection(doc)
120
153
  markHeld(doc)
@@ -122,7 +155,17 @@ export function installPageTransitions(doc) {
122
155
 
123
156
  doc.addEventListener("turbo:visit", (event) => {
124
157
  root.dataset.pageTransitionVisit = ""
158
+ // Turbo opens its view transition before it reads turbo-visit-control. A response it will
159
+ // not render gets no turbo:before-render, so this stands, and that transition draws nothing.
160
+ // The full load after it plays the one motion. Left alone while a motion is still running.
161
+ if (!root.dataset.pageTransition) root.dataset.pageTransition = STILL
125
162
  target = new URL(event.detail.url).pathname
163
+ // A GET that redirects: Turbo renders the response, then replaces to the final address
164
+ // with that same response, all before turbo:load. The page has landed, so nothing moves.
165
+ const action = event.detail.action
166
+ if (rendered && action === "replace") from = target
167
+ rendered = false
168
+ advance = action === "advance"
126
169
  predict(known.get(target))
127
170
  })
128
171
 
@@ -154,6 +197,7 @@ export function installPageTransitions(doc) {
154
197
  to: target,
155
198
  transitions: !!doc.startViewTransition && !win.matchMedia(REDUCED_MOTION).matches,
156
199
  sharedName,
200
+ advance,
157
201
  })
158
202
  nameTheMorph(sharedName && style !== STILL)
159
203
  if (style) root.dataset.pageTransition = style
@@ -161,11 +205,20 @@ export function installPageTransitions(doc) {
161
205
  if (style && style !== STILL) holdRender(event, () => readyForSwap(doc, newBody))
162
206
  }, { capture: true })
163
207
 
164
- // Turbo fires this after the transition has finished.
208
+ // A refused form renders with no visit and fires no turbo:load, so only a render inside a
209
+ // visit counts. Otherwise the flag would outlive that render and eat the next page's move.
210
+ doc.addEventListener("turbo:render", () => { rendered = !!target })
211
+
212
+ // Turbo fires this after the transition has finished. With no visit, it is this document's
213
+ // own first load, and a full load's motion can still be playing: its script cleans up.
165
214
  doc.addEventListener("turbo:load", () => {
215
+ const visited = target !== null
166
216
  from = win.location.pathname
167
217
  target = null
218
+ advance = false
219
+ rendered = false
168
220
  known.set(from, morphNames(doc))
221
+ if (!visited) return
169
222
  delete root.dataset.pageTransition
170
223
  delete root.dataset.pageTransitionVisit
171
224
  nameTheMorph(false)
@@ -173,8 +226,37 @@ export function installPageTransitions(doc) {
173
226
 
174
227
  // A visit that never renders: the stack must not keep its name at rest.
175
228
  for (const name of ["turbo:fetch-request-error", "turbo:reload"]) {
176
- doc.addEventListener(name, () => { delete root.dataset.pageTransitionVisit })
229
+ doc.addEventListener(name, () => {
230
+ delete root.dataset.pageTransitionVisit
231
+ if (name !== "turbo:reload") delete root.dataset.pageTransition
232
+ })
177
233
  }
234
+
235
+ // The last moment on the old document. The browser takes its picture after this.
236
+ win.addEventListener("pageswap", (event) => {
237
+ const transition = event.viewTransition
238
+ const style = transition && planFullLoad({ direction: readDirection(doc), from, activation: event.activation })
239
+ const left = leaveStyle(win, style)
240
+ if (!transition) return
241
+ if (!left) {
242
+ transition.skipTransition()
243
+ return
244
+ }
245
+ markHeld(doc)
246
+ root.dataset.pageTransitionVisit = ""
247
+ nameTheMorph(false)
248
+ // The loader settles at pagehide, which comes after the picture.
249
+ if (win.StudioLoader) win.StudioLoader.hide()
250
+ })
251
+
252
+ // Back from the back/forward cache: a visit that left by a full load is over.
253
+ win.addEventListener("pageshow", (event) => {
254
+ if (!event.persisted) return
255
+ from = win.location.pathname
256
+ target = null
257
+ advance = false
258
+ rendered = false
259
+ })
178
260
  return true
179
261
  }
180
262
 
@@ -37,7 +37,7 @@
37
37
  <% section[:links].each do |link| %>
38
38
  <% target = link[:target].presence %>
39
39
  <%= link_to link[:href],
40
- class: "group flex items-start gap-3 px-4 py-3 text-sm text-body hover:text-heading hover:bg-surface-alt focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-inset transition",
40
+ class: "group flex #{link[:desc].present? ? "items-start" : "items-center"} gap-3 px-4 py-3 text-sm text-body hover:text-heading hover:bg-surface-alt focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-inset transition",
41
41
  target: target,
42
42
  rel: target == "_blank" ? "noopener" : nil,
43
43
  "@click": close_action do %>
@@ -30,7 +30,7 @@
30
30
  <%# The session-drift stamp + browser store. Renders nothing unless the controller
31
31
  includes Studio::ErrorHandling. See studio/_session_stamp.
32
32
 
33
- THE ONE ENGINE SCRIPT THAT STAYS CLASSIC, AND SYNCHRONOUS. Host code
33
+ AN ENGINE SCRIPT THAT STAYS CLASSIC, AND SYNCHRONOUS. Host code
34
34
  registers identity sources with window.StudioSession as its own modules
35
35
  EVALUATE (turf-monster registers its identity source from a module at
36
36
  import; CHANGELOG, "Held"), and the host's entry point is the module tag
@@ -39,6 +39,14 @@
39
39
  silently be null. It becomes a module when those consumers wait for it. %>
40
40
  <%= render "studio/session_stamp" %>
41
41
  <%= render "layouts/studio/smooth_load" %>
42
+ <%# A FULL PAGE LOAD PLAYS THE SAME TRANSITION, only with the setting on: an app without it
43
+ gets neither tag. The sheet carries the at-rule that takes no selector, so it cannot sit in
44
+ the engine's bundle. The script is classic and synchronous, the second one here, because
45
+ pagereveal fires before the first render, ahead of every module. docs/PAGE_TRANSITIONS.md. %>
46
+ <% if @studio_page_transition_direction || Studio.page_transition_direction %>
47
+ <%= stylesheet_link_tag "studio/page_transition_full_load" %>
48
+ <%= javascript_include_tag "studio/page_transition_reveal", nonce: studio_script_nonce %>
49
+ <% end %>
42
50
  <%# The favicon's URL carries its content version (Studio::PublicFileVersion),
43
51
  so a redrawn favicon is a new URL no browser has cached. %>
44
52
  <link rel="icon" type="image/png" href="<%= Studio::PublicFileVersion.path("/favicon.png") %>">
@@ -197,7 +205,8 @@
197
205
  take every bare specifier on the page down with it. The ones below that load by
198
206
  URL (type="module" with a src) are external files, not inline scripts: they
199
207
  carry no body and need no nonce under a policy that allows 'self'. The session
200
- store (studio/_session_stamp, above) is the one classic script, and says why.
208
+ store (studio/_session_stamp, above) and the full-load script above are the two
209
+ classic scripts, and each says why.
201
210
  A MODULE LOADED BY URL MUST IMPORT NO BARE SPECIFIER. javascript_include_tag
202
211
  also sends a `Link: rel=modulepreload` response header, which the browser
203
212
  acts on before it parses this map, so a bare import in that module can
data/lib/studio/engine.rb CHANGED
@@ -89,8 +89,9 @@ module Studio
89
89
  # loaded as an ES module (Alpine by the head's type="module" tag,
90
90
  # canvas-confetti and SortableJS through their importmap pins, Leaflet by
91
91
  # studio/footer_map's import()), and studio/session.js, the one classic
92
- # script (layouts/studio/_head says why). The engine's own modules are
93
- # under app/javascript, enumerated below.
92
+ # script (layouts/studio/_head says why), with studio/page_transition_reveal.js,
93
+ # the other classic one. The engine's own modules are under app/javascript,
94
+ # enumerated below.
94
95
  app.config.assets.precompile += %w[
95
96
  studio/sticky_table_header.css
96
97
  studio/alpine.js
@@ -99,6 +100,8 @@ module Studio
99
100
  studio/leaflet.js
100
101
  studio/leaflet.css
101
102
  studio/session.js
103
+ studio/page_transition_full_load.css
104
+ studio/page_transition_reveal.js
102
105
  studio/montserrat-latin.woff2
103
106
  studio/montserrat-latin-ext.woff2
104
107
  ]
@@ -1,3 +1,3 @@
1
1
  module Studio
2
- VERSION = "0.124.0"
2
+ VERSION = "0.125.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: studio-engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.124.0
4
+ version: 0.125.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie
@@ -424,9 +424,11 @@ files:
424
424
  - app/assets/javascripts/studio/alpine.js
425
425
  - app/assets/javascripts/studio/canvas_confetti.js
426
426
  - app/assets/javascripts/studio/leaflet.js
427
+ - app/assets/javascripts/studio/page_transition_reveal.js
427
428
  - app/assets/javascripts/studio/session.js
428
429
  - app/assets/javascripts/studio/sortable.js
429
430
  - app/assets/stylesheets/studio/leaflet.css
431
+ - app/assets/stylesheets/studio/page_transition_full_load.css
430
432
  - app/assets/stylesheets/studio/sticky_table_header.css
431
433
  - app/assets/tailwind/studio_engine/engine-motion.css
432
434
  - app/assets/tailwind/studio_engine/engine.css