studio-engine 0.127.0 → 0.128.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +116 -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 +519 -37
  6. data/app/controllers/studio/surveys_controller.rb +16 -3
  7. data/app/helpers/studio/loader_helper.rb +2 -2
  8. data/app/helpers/studio_icon_helper.rb +10 -0
  9. data/app/javascript/studio/agent_chat.js +14 -3
  10. data/app/javascript/studio/agent_chat_voice.js +42 -4
  11. data/app/javascript/studio/loader.js +130 -5
  12. data/app/javascript/studio/microphone_chip.js +28 -2
  13. data/app/javascript/studio/page_transitions.js +28 -19
  14. data/app/javascript/studio/survey.js +16 -3
  15. data/app/javascript/studio/theme_editor.js +4 -0
  16. data/app/javascript/studio/voice_recorder.js +18 -7
  17. data/app/models/studio/survey_response.rb +24 -17
  18. data/app/views/components/_geo_signpost.html.erb +1 -0
  19. data/app/views/components/_link_sidebar.html.erb +40 -12
  20. data/app/views/components/_sidebar_panel.html.erb +15 -5
  21. data/app/views/layouts/studio/_head.html.erb +18 -7
  22. data/app/views/layouts/studio/_smooth_load.html.erb +1 -0
  23. data/app/views/layouts/studio/component_preview.html.erb +1 -1
  24. data/app/views/studio/agent_chat/_turn.html.erb +3 -1
  25. data/app/views/studio/agent_chat/_voice.html.erb +28 -13
  26. data/app/views/studio/loader/_overlay.html.erb +4 -4
  27. data/app/views/style/_components.html.erb +1 -1
  28. data/app/views/style/_glass.html.erb +83 -0
  29. data/app/views/style/_host.html.erb +1 -1
  30. data/app/views/style/_modals.html.erb +11 -11
  31. data/app/views/style/_scrims.html.erb +1 -1
  32. data/app/views/style/_site_footer.html.erb +1 -1
  33. data/app/views/style/_tasks.html.erb +1 -1
  34. data/app/views/style/_theme.html.erb +56 -72
  35. data/app/views/style/_tricks.html.erb +13 -11
  36. data/app/views/style/_type_ladder.html.erb +33 -0
  37. data/app/views/style/index.html.erb +5 -1
  38. data/docs/SITE_FOOTER.md +1 -1
  39. data/lib/studio/agent_chat.rb +4 -2
  40. data/lib/studio/icons/README.md +12 -0
  41. data/lib/studio/icons/lucide/LICENSE +43 -0
  42. data/lib/studio/icons/lucide/activity.svg +15 -0
  43. data/lib/studio/icons/lucide/book-open.svg +16 -0
  44. data/lib/studio/icons/lucide/chart-column.svg +18 -0
  45. data/lib/studio/icons/lucide/diamond.svg +15 -0
  46. data/lib/studio/icons/lucide/door-stairwell.svg +19 -0
  47. data/lib/studio/icons/lucide/globe.svg +17 -0
  48. data/lib/studio/icons/lucide/hammer.svg +17 -0
  49. data/lib/studio/icons/lucide/inbox.svg +16 -0
  50. data/lib/studio/icons/lucide/key-round.svg +16 -0
  51. data/lib/studio/icons/lucide/layout-grid.svg +18 -0
  52. data/lib/studio/icons/lucide/log-out.svg +17 -0
  53. data/lib/studio/icons/lucide/message-circle.svg +15 -0
  54. data/lib/studio/icons/lucide/package.svg +18 -0
  55. data/lib/studio/icons/lucide/palette.svg +19 -0
  56. data/lib/studio/icons/lucide/plug.svg +18 -0
  57. data/lib/studio/icons/lucide/rocket.svg +18 -0
  58. data/lib/studio/icons/lucide/user.svg +16 -0
  59. data/lib/studio/icons.rb +52 -0
  60. data/lib/studio/loader.rb +7 -0
  61. data/lib/studio/sidebar_sections.rb +52 -4
  62. data/lib/studio/site_footer.rb +23 -2
  63. data/lib/studio/version.rb +1 -1
  64. data/lib/studio.rb +54 -2
  65. 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: 3abb879eda738b9ebe2d7ba3aa0c7400e4e335eccdf5775c654b8810db8f96e2
4
+ data.tar.gz: 440cde0469a8d8d8905a7250da40fbd2576e3f9b5a6b9c2a68036368486759cc
5
5
  SHA512:
6
- metadata.gz: 1420fb65a41535cc4c4ae4a1466a83fe46d02eca34c17ed26b1ef1fdfc1cf6ca934776f70a843380410593c7932bbc23c53fe2fe6f0b992e3bf5a19206b5e7f3
7
- data.tar.gz: c88656c2c2363917fff41839b65c00ccc0f499d3c30c1a216ae783081f6712b44c200ef93500fceae04d99f6719db344ed0b213b742e104d632d9a0d063fc4d3
6
+ metadata.gz: e73f00423880d25f49324ed5ef4c3c152caf27f7bc144815ea4928242933e675c1e879393bdd2777d40659ddb472ddae8c23eb3ad9f0ae2f3f9e25e04e443033
7
+ data.tar.gz: '09e30076328185194bbd9773a059c1ee3a984e60e2376ff08aaf0f785ee3888c85e056dbffc38c69fa3477d03adff476beb7832bb60c974d2dc5f010a42fbc5d'
data/CHANGELOG.md CHANGED
@@ -4,6 +4,122 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.128.0 — 2026-11-05
8
+
9
+ ### Fixed
10
+
11
+ - **The modified-click booking spec no longer waits on a Playwright event that can stay silent.**
12
+ `e2e/booking_frame.spec.js` timed out on two `accepted` runs because Playwright 1.62.1 sometimes
13
+ never fires its `page` event for a tab Chromium did open. The spec now reads the new tab from
14
+ Chromium's target list. It still needs one new tab at the booking URL, no dialog, and the first
15
+ page unmoved. No host change: a modified click on a booking link always opened its tab.
16
+
17
+ - **Firefox runs the shared head's import map.** The head's two URL module tags (`studio/alpine`,
18
+ `studio/sticky_table_header`) sent a `Link: rel=modulepreload` response header, and Firefox then
19
+ refused the import map: every bare `studio/*` import failed and no panel opened, on every host
20
+ that renders the head. Both tags now pass `preload_links_header: false`. They keep their place in
21
+ the document, their `data-turbo-track` and their CSP treatment. Hosts: keep a module tag of your
22
+ own below the import map, and pass the same option if it loads by URL.
23
+
24
+ - **Hold to talk, before the phone test.** Studio's shared voice chat, from the review of
25
+ `engine-chat-takes-voice`:
26
+ - A tap under `TAP_MS` (300ms) on a hold button is now the recorder's `tapped` state: the
27
+ line says to hold the button, and it does not count as a silent take, so the help no longer
28
+ opens by itself. A refused microphone and two real silent takes in a row still open it; a
29
+ tap between two silent takes ends the run.
30
+ - A held button keeps its whole box through a hold at phone width, stuck or not. The status
31
+ line reserves two lines (one in a composer 36rem wide) and fixes its height for the hold,
32
+ the chip stays one line, the timer sits on the button, and the recorder's own failed and
33
+ refused lines moved to the chat's one line. The chat's chip prompt is now "Microphone: hold
34
+ to allow".
35
+ - A `site_footer_chat_path` callable that raises hides the Chat link and reports it
36
+ (`SiteFooter.reporter` on each render, `ErrorLog.capture!` once per error class).
37
+ - A turn with status `held` says it was received and won't get an automatic answer, and
38
+ never asks for a resend. The hub still reports these as `failed` until
39
+ `chat-caps-count-held-messages` ships; they keep the failed line until then.
40
+ - A later typed send clears "Sent. Spoken by ..." (`studio:agent-chat:sent`).
41
+ - A key held on the talk button no longer dismisses the refused-microphone help.
42
+ - The composer's picker scrolls within a short window, so the chip stays on screen.
43
+ - Firefox's `AbortError` reads as a busy microphone, and Esc in the open picker stops there.
44
+
45
+ ### Added
46
+
47
+ - **`/admin/style` Theme: logos first, Typography beside the editor, a preview that waits for an edit.** A
48
+ host may supply its own logo block in `app/views/style/host/_logos.html.erb`.
49
+ - **`Studio.surface_style = :glass`: opt-in translucent cards over a soft ambient page.** A host
50
+ sets it and writes `data-surface-style` on its root. It reaches `.card`, `.card-hover`,
51
+ `.empty-state` and `.bg-surface.rounded-xl.border`. Menus stay solid. Real blur is only on
52
+ `.surface-glass-blur`, because a dense board cannot afford it on every card.
53
+ - **`Studio.sidebar_style = :glass`: an opt-in floating panel for the link sidebar.** The panel
54
+ insets from the page's edge, in both themes. Where the browser supports `backdrop-filter` it
55
+ blurs the page through a 72% fill, and a nearly solid fill replaces that under the
56
+ reduced-transparency setting or with no support. The blur is read in Chromium. It is not read
57
+ in a real Safari, and a headless WebKit paints the fill with no visible blur, so the fill alone
58
+ holds resting text at 4.5:1 over the worst page for each theme. It draws a link's `icon:`, marks
59
+ the current page's row with `aria-current="page"` in the app's primary color and a ring on
60
+ keyboard focus, and turns off the separate blurred edge. It hangs from the navbar, with square
61
+ top corners, and its spacing is five tokens on the panel's shell. The default stays `:default`,
62
+ so no app moves until it opts in.
63
+ - **`studio_icon(name, size:)`: one line icon set, as inline SVG in `currentColor`.** It holds the
64
+ Lucide icons the sidebar needs, vendored under `lib/studio/icons`, with their ISC and MIT texts
65
+ beside them. An unknown name renders nothing, so a caller can fall back to an emoji.
66
+ - **Link sidebar: a section may show its header.** A section in `Studio.sidebar_sections` with
67
+ `header: true` shows its title above its rows, in the `label-upper` style. The default stays no
68
+ header, so an app changes only when it opts in. The divider stays, and there is no ADMIN chip.
69
+ The group keeps its accessible name and the word on screen is `aria-hidden`, so a screen reader
70
+ announces the title once. The header is plain text outside the rows: it takes no click, and a
71
+ row keeps its height, its centering and its no-jump description. A section with a blank title
72
+ draws no header. The engine lab shows the case at `/lab/sidebar_panels?sidebar=1&headers=1`.
73
+ - **`Studio.sidebar_geo_signpost`: a host may take the Geo row out of the link sidebar.** It
74
+ defaults to true, so no app changes. False draws no Geo row in the panel. The partial
75
+ `components/_geo_signpost` stays, so a host that moves Geo renders it on its own page.
76
+ - **One loader pace for every app.** Under 350ms no loader and no added wait; once shown, at
77
+ least 750ms (`Studio::Loader::OVERLAY_DELAY_MS`, `OVERLAY_MIN_SHOW_MS`; `min_show:` beside
78
+ `delay:` on `studio_loader_overlay`). `layouts/studio/_head` now carries the overlay in a
79
+ `<template>`, and `studio/loader` adds it to the body, so an app with no overlay of its own gets
80
+ one on this bump. `config.loader_overlay = false` opts out. A slow visit in such an app now shows
81
+ the overlay, and a visit that answers between 350ms and 1100ms lands later than it did. The wait
82
+ holds a visit's good HTML response ahead of the view transition's old picture, so the move plays
83
+ after the loader goes. Failures, downloads, frames, forms, a full-load answer and
84
+ `StudioLoader.hide()` never wait. See `docs/LOADER.md`, *The standard pace*.
85
+ - **`config.page_transition_style`,** `:slide` (the default) or `:unfold`: one app plays one move.
86
+
87
+ ### Changed
88
+
89
+ - **Page transitions draw nothing at random.** Alex McRitchie's ruling of 2026-11-04 replaces
90
+ the random draw per page change of 2026-11-03. Every page change and full load plays the app's
91
+ one move, and with no style set that move is the slide. A consumer that reads
92
+ `data-page-transition` or `Math.random` around a visit must change: `fade-*` is now `slide-*`,
93
+ and `pickStyle` and `DIRECTIONS` are gone.
94
+ - **Page transitions paint the app's page color behind the move, and pages stay opaque.** The
95
+ black backdrop and the 0.3 opacity are gone, so blank areas no longer darken, a full-width hero
96
+ has no dark strip beside it, a light theme no longer greys, and an unfold has no black wedges. The
97
+ leaving page of an unfold stays opaque too.
98
+
99
+ - **The browser lane counts its own specs.** `bin/lib/e2e_spec_census.rb` computes the spec count
100
+ from `e2e/`, as the hub does, and `bin/e2e-executed-set-check` reads it. `config/e2e_lane.yml`
101
+ loses `total_specs` and `executed`, so adding a spec moves no number and two PRs no longer
102
+ conflict on them. `bin/e2e-census-check` compares Playwright's `--list` with the census.
103
+ - **`CHANGELOG.md` merges with `merge=union`,** so parallel PRs stop conflicting on it. A branch
104
+ forked before a roll can land an entry under the rolled heading: `docs/RELEASE.md`, *Rolling
105
+ `Unreleased` into a version*, says how to read and fix that.
106
+ - **Link sidebar: the blurred edge reaches further and fades with no visible end.** The band is
107
+ 100px wide, up from 40px, with the same 6px blur. Its mask is now an eased multi-stop gradient:
108
+ whole at the panel, half at 60px, clear at 100px, flat at both ends. The old hold and straight
109
+ fade showed as a line over a photo. It stays flush with the panel, takes no clicks, and draws
110
+ nothing below `md`. The engine lab shows it over the hub's Denver photo at
111
+ `/lab/sidebar_panels?sidebar=1&busy=photo`.
112
+
113
+ ### Fixed
114
+
115
+ - **A late survey autosave can no longer change a completed response.** `record_answer` rewrote
116
+ the whole answers hash from the copy it had loaded, so a save that overlapped the submit could
117
+ replace the finished answers with a stale set, and two saves at once could lose each other's
118
+ keys. It now reads and writes under a row lock, and it refuses a completed row. The submit holds
119
+ one lock from its first answer through the completion. `on_survey_completed` still fires once,
120
+ after the commit. The stepper cancels any save still in flight when the form submits. A save that
121
+ loses the race answers 409, and the stepper shows the visitor no error. A host needs no change.
122
+
7
123
  ## 0.127.0 — 2026-11-04
8
124
 
9
125
  ### 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 () {