studio-engine 0.127.0 → 0.129.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.
Files changed (70) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +161 -0
  3. data/README.md +9 -3
  4. data/app/assets/javascripts/studio/page_transition_reveal.js +1 -1
  5. data/app/assets/tailwind/studio_engine/engine.css +572 -37
  6. data/app/controllers/concerns/studio/error_handling.rb +17 -0
  7. data/app/controllers/studio/surveys_controller.rb +16 -3
  8. data/app/helpers/studio/loader_helper.rb +2 -2
  9. data/app/helpers/studio_icon_helper.rb +10 -0
  10. data/app/javascript/studio/agent_chat.js +88 -6
  11. data/app/javascript/studio/agent_chat_voice.js +59 -5
  12. data/app/javascript/studio/alpine_stores.js +80 -22
  13. data/app/javascript/studio/loader.js +130 -5
  14. data/app/javascript/studio/microphone_chip.js +28 -2
  15. data/app/javascript/studio/page_transitions.js +28 -19
  16. data/app/javascript/studio/survey.js +16 -3
  17. data/app/javascript/studio/theme_editor.js +4 -0
  18. data/app/javascript/studio/voice_recorder.js +18 -7
  19. data/app/models/studio/survey_response.rb +24 -17
  20. data/app/views/components/_geo_signpost.html.erb +1 -0
  21. data/app/views/components/_link_sidebar.html.erb +40 -12
  22. data/app/views/components/_sidebar_panel.html.erb +15 -5
  23. data/app/views/layouts/studio/_head.html.erb +18 -7
  24. data/app/views/layouts/studio/_smooth_load.html.erb +1 -0
  25. data/app/views/layouts/studio/component_preview.html.erb +1 -1
  26. data/app/views/registrations/new.html.erb +4 -3
  27. data/app/views/sessions/new.html.erb +4 -3
  28. data/app/views/studio/agent_chat/_chat.html.erb +2 -1
  29. data/app/views/studio/agent_chat/_turn.html.erb +3 -1
  30. data/app/views/studio/agent_chat/_voice.html.erb +28 -13
  31. data/app/views/studio/loader/_overlay.html.erb +4 -4
  32. data/app/views/style/_components.html.erb +2 -4
  33. data/app/views/style/_glass.html.erb +83 -0
  34. data/app/views/style/_host.html.erb +2 -10
  35. data/app/views/style/_modals.html.erb +38 -231
  36. data/app/views/style/_scrims.html.erb +2 -9
  37. data/app/views/style/_site_footer.html.erb +5 -13
  38. data/app/views/style/_tasks.html.erb +16 -27
  39. data/app/views/style/_theme.html.erb +56 -78
  40. data/app/views/style/_tricks.html.erb +13 -11
  41. data/app/views/style/_type_ladder.html.erb +33 -0
  42. data/app/views/style/index.html.erb +5 -1
  43. data/docs/SITE_FOOTER.md +1 -1
  44. data/lib/studio/agent_chat.rb +4 -2
  45. data/lib/studio/icons/README.md +12 -0
  46. data/lib/studio/icons/lucide/LICENSE +43 -0
  47. data/lib/studio/icons/lucide/activity.svg +15 -0
  48. data/lib/studio/icons/lucide/book-open.svg +16 -0
  49. data/lib/studio/icons/lucide/chart-column.svg +18 -0
  50. data/lib/studio/icons/lucide/diamond.svg +15 -0
  51. data/lib/studio/icons/lucide/door-stairwell.svg +19 -0
  52. data/lib/studio/icons/lucide/globe.svg +17 -0
  53. data/lib/studio/icons/lucide/hammer.svg +17 -0
  54. data/lib/studio/icons/lucide/inbox.svg +16 -0
  55. data/lib/studio/icons/lucide/key-round.svg +16 -0
  56. data/lib/studio/icons/lucide/layout-grid.svg +18 -0
  57. data/lib/studio/icons/lucide/log-out.svg +17 -0
  58. data/lib/studio/icons/lucide/message-circle.svg +15 -0
  59. data/lib/studio/icons/lucide/package.svg +18 -0
  60. data/lib/studio/icons/lucide/palette.svg +19 -0
  61. data/lib/studio/icons/lucide/plug.svg +18 -0
  62. data/lib/studio/icons/lucide/rocket.svg +18 -0
  63. data/lib/studio/icons/lucide/user.svg +16 -0
  64. data/lib/studio/icons.rb +52 -0
  65. data/lib/studio/loader.rb +7 -0
  66. data/lib/studio/sidebar_sections.rb +52 -4
  67. data/lib/studio/site_footer.rb +23 -2
  68. data/lib/studio/version.rb +1 -1
  69. data/lib/studio.rb +54 -2
  70. metadata +24 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 22ef2ab3094c82f6f447ee86455dac3e2c70b759ec3d58f2f1a887a347c4b1ad
4
- data.tar.gz: 92c5bc2d6c5f53914e33d7c50623a076791ef50faff385b4e63218e3773eb6a6
3
+ metadata.gz: ed5be658c4ba3547da2294995bc7c13e7181bccbd5710fd07d84c7fb0af95a2c
4
+ data.tar.gz: d48d2fbebaee5b7e33a22c16bc2f6d40b1afc161d0dacb6eb42d51a8e36cd4fe
5
5
  SHA512:
6
- metadata.gz: 1420fb65a41535cc4c4ae4a1466a83fe46d02eca34c17ed26b1ef1fdfc1cf6ca934776f70a843380410593c7932bbc23c53fe2fe6f0b992e3bf5a19206b5e7f3
7
- data.tar.gz: c88656c2c2363917fff41839b65c00ccc0f499d3c30c1a216ae783081f6712b44c200ef93500fceae04d99f6719db344ed0b213b742e104d632d9a0d063fc4d3
6
+ metadata.gz: 99e6c74a4c11320449ee3b58d89b56712067f02bc6b2228d602f4ec942b06617b77960e05d8d29dc03dda95e5c92868d4da18bd67922494fbe84abb40b6e308f
7
+ data.tar.gz: 353b2199a4a651ee65a0d00c448c8273731b9a4bebbb5c5eff97fe97ba57177ae8f1a47ab0fa6d4b9f2f99856c46a8992c62c84d4319db28116b0dd557c05adf
data/CHANGELOG.md CHANGED
@@ -4,6 +4,167 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.129.0 — 2026-11-05
8
+
9
+ ### Changed
10
+
11
+ - **The `/admin/style` copy is trimmed.** Each section keeps at most one short sentence of intro,
12
+ and the notes under specimens are cut the same way. Controls, specimens and error messages are
13
+ unchanged. The hidden copy-to-agent references on the modal cards are unchanged too. The Tricks
14
+ section is not part of this trim.
15
+
16
+ - **A bad or missing CSRF token on a JSON or `Accept: */*` request is now a 422 with no ErrorLog
17
+ row, on every controller using `Studio::ErrorHandling`.** The catch-all answered JSON 500 and
18
+ wrote a row. It now answers `422 {error, reason: "invalid_token"}` in every environment, and a
19
+ test on such a route that asserted `assert_raises(ActionController::InvalidAuthenticityToken)`
20
+ now asserts the 422. An HTML form is unchanged: the redirect home with its alert, and the row.
21
+
22
+ ### Fixed
23
+
24
+ - **A theme toggle is one crossfade.** `$store.theme.toggle()` added `theme-transition` for 300ms, and
25
+ in a host with that rule about 210 elements ran their own 200ms transition while gradients and
26
+ the page background snapped. It now flips the whole page in one 160ms view transition with no
27
+ movement. No element runs its own transition, and no named element keeps its own picture. A
28
+ browser with no view transitions, a reduced-motion reader, and a toggle during a Turbo visit
29
+ flip at once, and the visit's motion plays on. Under the crossfade the class, the store value
30
+ and localStorage change together in the transition's callback, so all three still read the old
31
+ theme for up to a frame after the call: a caller that reads them in the same tick must wait.
32
+ `toggleTheme` and `storedTheme` are no longer exported, and the store starts from the root's
33
+ class. `$store.theme.set("dark" | "light")` is the one setter, and `toggle()` stays. The
34
+ `theme-transition` rule in a host is now dead. Measured on the hub's `/deployments`, signed in,
35
+ 2100 elements, Metal GPU: the largest step between frames fell from 35-65% of the brightness
36
+ change to 11%, and element transitions started by one toggle from about 1170 to 0.
37
+ Chromium sends every pointer hit to the root during a view transition, for about 160ms. A pointer
38
+ that stops in that time can show no `:hover` until it moves again, and a second click on the toggle
39
+ is dropped. A host spec that toggles and then hovers must wait for `data-theme-flip` to clear.
40
+
41
+ - **The agent chat at phone width.** Measured at 390x844 and 320x568, light and dark:
42
+ - No blank tail. A turn's `sr-only` name escaped the log, which had no positioned ancestor,
43
+ and stretched the document to 1933px where `main` ended at 977px. The log is now
44
+ `position: relative`.
45
+ - The newest turn shows above the composer, on load and after a new turn. The page scrolls
46
+ the thread's end clear of the stuck composer; not for an empty thread, not for a reader
47
+ scrolled up in the log or the page, and not during a hold, when it waits for the button
48
+ to be let go.
49
+ - A stale page says so. A refused token reads "This page has expired" with a reload hint on
50
+ the voice line and the typed error, and a refused send keeps its words in the box.
51
+
52
+ ## 0.128.0 — 2026-11-05
53
+
54
+ ### Fixed
55
+
56
+ - **The modified-click booking spec no longer waits on a Playwright event that can stay silent.**
57
+ `e2e/booking_frame.spec.js` timed out on two `accepted` runs because Playwright 1.62.1 sometimes
58
+ never fires its `page` event for a tab Chromium did open. The spec now reads the new tab from
59
+ Chromium's target list. It still needs one new tab at the booking URL, no dialog, and the first
60
+ page unmoved. No host change: a modified click on a booking link always opened its tab.
61
+
62
+ - **Firefox runs the shared head's import map.** The head's two URL module tags (`studio/alpine`,
63
+ `studio/sticky_table_header`) sent a `Link: rel=modulepreload` response header, and Firefox then
64
+ refused the import map: every bare `studio/*` import failed and no panel opened, on every host
65
+ that renders the head. Both tags now pass `preload_links_header: false`. They keep their place in
66
+ the document, their `data-turbo-track` and their CSP treatment. Hosts: keep a module tag of your
67
+ own below the import map, and pass the same option if it loads by URL.
68
+
69
+ - **Hold to talk, before the phone test.** Studio's shared voice chat, from the review of
70
+ `engine-chat-takes-voice`:
71
+ - A tap under `TAP_MS` (300ms) on a hold button is now the recorder's `tapped` state: the
72
+ line says to hold the button, and it does not count as a silent take, so the help no longer
73
+ opens by itself. A refused microphone and two real silent takes in a row still open it; a
74
+ tap between two silent takes ends the run.
75
+ - A held button keeps its whole box through a hold at phone width, stuck or not. The status
76
+ line reserves two lines (one in a composer 36rem wide) and fixes its height for the hold,
77
+ the chip stays one line, the timer sits on the button, and the recorder's own failed and
78
+ refused lines moved to the chat's one line. The chat's chip prompt is now "Microphone: hold
79
+ to allow".
80
+ - A `site_footer_chat_path` callable that raises hides the Chat link and reports it
81
+ (`SiteFooter.reporter` on each render, `ErrorLog.capture!` once per error class).
82
+ - A turn with status `held` says it was received and won't get an automatic answer, and
83
+ never asks for a resend. The hub still reports these as `failed` until
84
+ `chat-caps-count-held-messages` ships; they keep the failed line until then.
85
+ - A later typed send clears "Sent. Spoken by ..." (`studio:agent-chat:sent`).
86
+ - A key held on the talk button no longer dismisses the refused-microphone help.
87
+ - The composer's picker scrolls within a short window, so the chip stays on screen.
88
+ - Firefox's `AbortError` reads as a busy microphone, and Esc in the open picker stops there.
89
+
90
+ ### Added
91
+
92
+ - **`/admin/style` Theme: logos first, Typography beside the editor, a preview that waits for an edit.** A
93
+ host may supply its own logo block in `app/views/style/host/_logos.html.erb`.
94
+ - **`Studio.surface_style = :glass`: opt-in translucent cards over a soft ambient page.** A host
95
+ sets it and writes `data-surface-style` on its root. It reaches `.card`, `.card-hover`,
96
+ `.empty-state` and `.bg-surface.rounded-xl.border`. Menus stay solid. Real blur is only on
97
+ `.surface-glass-blur`, because a dense board cannot afford it on every card.
98
+ - **`Studio.sidebar_style = :glass`: an opt-in floating panel for the link sidebar.** The panel
99
+ insets from the page's edge, in both themes. Where the browser supports `backdrop-filter` it
100
+ blurs the page through a 72% fill, and a nearly solid fill replaces that under the
101
+ reduced-transparency setting or with no support. The blur is read in Chromium. It is not read
102
+ in a real Safari, and a headless WebKit paints the fill with no visible blur, so the fill alone
103
+ holds resting text at 4.5:1 over the worst page for each theme. It draws a link's `icon:`, marks
104
+ the current page's row with `aria-current="page"` in the app's primary color and a ring on
105
+ keyboard focus, and turns off the separate blurred edge. It hangs from the navbar, with square
106
+ top corners, and its spacing is five tokens on the panel's shell. The default stays `:default`,
107
+ so no app moves until it opts in.
108
+ - **`studio_icon(name, size:)`: one line icon set, as inline SVG in `currentColor`.** It holds the
109
+ Lucide icons the sidebar needs, vendored under `lib/studio/icons`, with their ISC and MIT texts
110
+ beside them. An unknown name renders nothing, so a caller can fall back to an emoji.
111
+ - **Link sidebar: a section may show its header.** A section in `Studio.sidebar_sections` with
112
+ `header: true` shows its title above its rows, in the `label-upper` style. The default stays no
113
+ header, so an app changes only when it opts in. The divider stays, and there is no ADMIN chip.
114
+ The group keeps its accessible name and the word on screen is `aria-hidden`, so a screen reader
115
+ announces the title once. The header is plain text outside the rows: it takes no click, and a
116
+ row keeps its height, its centering and its no-jump description. A section with a blank title
117
+ draws no header. The engine lab shows the case at `/lab/sidebar_panels?sidebar=1&headers=1`.
118
+ - **`Studio.sidebar_geo_signpost`: a host may take the Geo row out of the link sidebar.** It
119
+ defaults to true, so no app changes. False draws no Geo row in the panel. The partial
120
+ `components/_geo_signpost` stays, so a host that moves Geo renders it on its own page.
121
+ - **One loader pace for every app.** Under 350ms no loader and no added wait; once shown, at
122
+ least 750ms (`Studio::Loader::OVERLAY_DELAY_MS`, `OVERLAY_MIN_SHOW_MS`; `min_show:` beside
123
+ `delay:` on `studio_loader_overlay`). `layouts/studio/_head` now carries the overlay in a
124
+ `<template>`, and `studio/loader` adds it to the body, so an app with no overlay of its own gets
125
+ one on this bump. `config.loader_overlay = false` opts out. A slow visit in such an app now shows
126
+ the overlay, and a visit that answers between 350ms and 1100ms lands later than it did. The wait
127
+ holds a visit's good HTML response ahead of the view transition's old picture, so the move plays
128
+ after the loader goes. Failures, downloads, frames, forms, a full-load answer and
129
+ `StudioLoader.hide()` never wait. See `docs/LOADER.md`, *The standard pace*.
130
+ - **`config.page_transition_style`,** `:slide` (the default) or `:unfold`: one app plays one move.
131
+
132
+ ### Changed
133
+
134
+ - **Page transitions draw nothing at random.** Alex McRitchie's ruling of 2026-11-04 replaces
135
+ the random draw per page change of 2026-11-03. Every page change and full load plays the app's
136
+ one move, and with no style set that move is the slide. A consumer that reads
137
+ `data-page-transition` or `Math.random` around a visit must change: `fade-*` is now `slide-*`,
138
+ and `pickStyle` and `DIRECTIONS` are gone.
139
+ - **Page transitions paint the app's page color behind the move, and pages stay opaque.** The
140
+ black backdrop and the 0.3 opacity are gone, so blank areas no longer darken, a full-width hero
141
+ has no dark strip beside it, a light theme no longer greys, and an unfold has no black wedges. The
142
+ leaving page of an unfold stays opaque too.
143
+
144
+ - **The browser lane counts its own specs.** `bin/lib/e2e_spec_census.rb` computes the spec count
145
+ from `e2e/`, as the hub does, and `bin/e2e-executed-set-check` reads it. `config/e2e_lane.yml`
146
+ loses `total_specs` and `executed`, so adding a spec moves no number and two PRs no longer
147
+ conflict on them. `bin/e2e-census-check` compares Playwright's `--list` with the census.
148
+ - **`CHANGELOG.md` merges with `merge=union`,** so parallel PRs stop conflicting on it. A branch
149
+ forked before a roll can land an entry under the rolled heading: `docs/RELEASE.md`, *Rolling
150
+ `Unreleased` into a version*, says how to read and fix that.
151
+ - **Link sidebar: the blurred edge reaches further and fades with no visible end.** The band is
152
+ 100px wide, up from 40px, with the same 6px blur. Its mask is now an eased multi-stop gradient:
153
+ whole at the panel, half at 60px, clear at 100px, flat at both ends. The old hold and straight
154
+ fade showed as a line over a photo. It stays flush with the panel, takes no clicks, and draws
155
+ nothing below `md`. The engine lab shows it over the hub's Denver photo at
156
+ `/lab/sidebar_panels?sidebar=1&busy=photo`.
157
+
158
+ ### Fixed
159
+
160
+ - **A late survey autosave can no longer change a completed response.** `record_answer` rewrote
161
+ the whole answers hash from the copy it had loaded, so a save that overlapped the submit could
162
+ replace the finished answers with a stale set, and two saves at once could lose each other's
163
+ keys. It now reads and writes under a row lock, and it refuses a completed row. The submit holds
164
+ one lock from its first answer through the completion. `on_survey_completed` still fires once,
165
+ after the commit. The stepper cancels any save still in flight when the form submits. A save that
166
+ loses the race answers 409, and the stepper shows the visitor no error. A host needs no change.
167
+
7
168
  ## 0.127.0 — 2026-11-04
8
169
 
9
170
  ### 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, 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).
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. `config.page_transition_style` (`:slide`, the default, or `:unfold`) names the one move an app plays; nothing is drawn at random. The direction 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).
@@ -171,6 +171,7 @@ Studio.configure do |config|
171
171
  # the fresh response arrives.
172
172
  config.smooth_load = true
173
173
  # config.page_transition_direction = :left # the app's page move; also turns smooth-load on
174
+ # config.page_transition_style = :unfold # default :slide
174
175
  # Nav spinner minimum display (default 2500). Smooth-load apps typically
175
176
  # drop to ~300; keep the high floor if multi-second ops ride the spinner.
176
177
  config.nav_spinner_min_ms = 300
@@ -782,6 +783,9 @@ than assuming drop-in equivalence:
782
783
  the engine's: Theme, Modals, Tricks, Tasks. An app grows a **fifth section, its
783
784
  own**, by defining one partial:
784
785
 
786
+ A host may also define `app/views/style/host/_logos.html.erb`, which replaces the Theme section's
787
+ default logo grid.
788
+
785
789
  ```erb
786
790
  <%# app/views/style/host/_modals.html.erb %>
787
791
  <div class="card p-4 space-y-3">
@@ -823,7 +827,9 @@ of `app/views/style/_host.html.erb`.
823
827
  Two seams, both empty by default, so the navbar is unchanged until an app opts in.
824
828
 
825
829
  - `sidebar_sections` fills the slide-out link sidebar (the cog trigger in the icon
826
- rail). Rules: `lib/studio/sidebar_sections.rb`.
830
+ rail). Rules: `lib/studio/sidebar_sections.rb`. `Studio.sidebar_style = :glass` floats it
831
+ as an inset translucent panel with line icons (`studio_icon`, `lib/studio/icons/README.md`);
832
+ see `docs/NAVBAR_SETUP.md`.
827
833
  - `navbar_links` fills the navbar's own link slots: the desktop bar beside the
828
834
  logo and the phone row under it. Rules: `lib/studio/navbar_links.rb`.
829
835
 
@@ -1095,7 +1101,7 @@ config.navbar_account_sidebar = { flag: "gearOpen", controls: %w[gear-sidebar ge
1095
1101
  `components/_sidebar_panel` takes three opt-in locals; unset, the panel renders
1096
1102
  as before:
1097
1103
 
1098
- - `edge: true` draws a soft blurred band along the panel's inner edge from `md`
1104
+ - `edge: true` draws a soft blurred band, 100px wide, along the panel's inner edge from `md`
1099
1105
  up, through a `data-sidebar-edge` hook and engine.css. It takes no clicks. The
1100
1106
  link sidebar passes it. It softens the page beside the panel, so leave it off
1101
1107
  a panel kept open beside live content.
@@ -5,7 +5,7 @@
5
5
  // CLASSIC AND SYNCHRONOUS ON PURPOSE. pagereveal fires before the first render, and a listener
6
6
  // bound after that misses it for good. A module is deferred, so it can be too late.
7
7
  //
8
- // It holds no table of styles. The leaving page drew the style with studio/page_transitions
8
+ // It holds no table of styles. The leaving page read the style with studio/page_transitions
9
9
  // at pageswap and left it here. No style means the leaving page did not plan this one (its
10
10
  // module had not run, or it was the same path), so the load is plain, not the browser's fade.
11
11
  (function () {