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 +4 -4
- data/CHANGELOG.md +31 -0
- data/README.md +1 -1
- data/app/assets/javascripts/studio/page_transition_reveal.js +75 -0
- data/app/assets/stylesheets/studio/page_transition_full_load.css +10 -0
- data/app/assets/tailwind/studio_engine/engine.css +7 -1
- data/app/javascript/studio/page_transitions.js +89 -7
- data/app/views/components/_link_sidebar.html.erb +1 -1
- data/app/views/layouts/studio/_head.html.erb +11 -2
- data/lib/studio/engine.rb +5 -2
- data/lib/studio/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 13bb9cfe1f6491a4dd2a199c16f734f466edad8fb38beae903033305e8fd91da
|
|
4
|
+
data.tar.gz: 20d1fec190a9bfe2391e4e4121f2fa11dc35729abd54a8df47e0a19346611715
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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, () => {
|
|
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
|
-
|
|
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)
|
|
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)
|
|
93
|
-
# under app/javascript,
|
|
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
|
]
|
data/lib/studio/version.rb
CHANGED
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.
|
|
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
|