studio-engine 0.129.0 → 0.131.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 +192 -0
- data/README.md +48 -6
- data/app/assets/tailwind/studio_engine/engine-motion.css +322 -56
- data/app/assets/tailwind/studio_engine/engine.css +737 -19
- data/app/assets/tailwind/studio_engine/theme.css +4 -0
- data/app/controllers/concerns/studio/error_handling.rb +53 -15
- data/app/controllers/concerns/studio/magic_link_issuing.rb +4 -0
- data/app/controllers/magic_links_controller.rb +25 -2
- data/app/controllers/registrations_controller.rb +58 -22
- data/app/controllers/style_controller.rb +6 -0
- data/app/helpers/studio/agent_chat_helper.rb +14 -1
- data/app/helpers/studio/crumbs_helper.rb +34 -0
- data/app/helpers/studio/fizz_helper.rb +5 -0
- data/app/helpers/studio/loader_helper.rb +79 -2
- data/app/javascript/studio/agent_chat.js +294 -6
- data/app/javascript/studio/carousel.js +99 -0
- data/app/javascript/studio/controllers/carousel_controller.js +138 -0
- data/app/javascript/studio/controllers/infinite_scroll_controller.js +17 -0
- data/app/javascript/studio/controllers/pull_to_refresh_controller.js +18 -0
- data/app/javascript/studio/controllers/style_section_nav_controller.js +4 -4
- data/app/javascript/studio/controllers/talk_to_agent_controller.js +20 -0
- data/app/javascript/studio/infinite_scroll.js +142 -0
- data/app/javascript/studio/loader.js +6 -1
- data/app/javascript/studio/page_transitions.js +65 -8
- data/app/javascript/studio/pinned_stack.js +112 -4
- data/app/javascript/studio/pull_to_refresh.js +147 -0
- data/app/javascript/studio/stimulus.js +12 -1
- data/app/javascript/studio/talk_to_agent.js +116 -0
- data/app/views/components/_geo_signpost.html.erb +2 -2
- data/app/views/components/_link_sidebar.html.erb +2 -2
- data/app/views/components/_link_sidebar_trigger.html.erb +1 -1
- data/app/views/components/_sidebar_panel.html.erb +1 -1
- data/app/views/error_logs/index.html.erb +2 -8
- data/app/views/studio/_carousel.html.erb +97 -0
- data/app/views/studio/_fizz.html.erb +26 -0
- data/app/views/studio/_fizz_layer.html.erb +4 -1
- data/app/views/studio/_hold_button.html.erb +1 -2
- data/app/views/studio/_page_header.html.erb +55 -13
- data/app/views/studio/_section_header.html.erb +23 -0
- data/app/views/studio/agent_chat/_chat.html.erb +7 -4
- data/app/views/studio/agent_chat/_talk_modal.html.erb +58 -0
- data/app/views/studio/agent_chat/_turn.html.erb +1 -1
- data/app/views/studio/loader/_infinite_scroll.html.erb +42 -0
- data/app/views/studio/loader/_loader.html.erb +20 -3
- data/app/views/studio/loader/_pull_to_refresh.html.erb +20 -0
- data/app/views/studio/loader/_skeleton.html.erb +37 -0
- data/app/views/style/_components.html.erb +3 -5
- data/app/views/style/_host.html.erb +3 -6
- data/app/views/style/_loaders.html.erb +118 -0
- data/app/views/style/_loaders_feed_page.html.erb +16 -0
- data/app/views/style/_modal_specimen.html.erb +5 -5
- data/app/views/style/_modals.html.erb +15 -5
- data/app/views/style/_site_footer.html.erb +1 -1
- data/app/views/style/_specimen.html.erb +21 -10
- data/app/views/style/_tasks.html.erb +3 -5
- data/app/views/style/_theme.html.erb +2 -2
- data/app/views/style/_tricks.html.erb +218 -217
- data/app/views/style/index.html.erb +21 -11
- data/app/views/style/loaders_feed.html.erb +7 -0
- data/lib/studio/crumbs.rb +150 -0
- data/lib/studio/loader.rb +30 -2
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +3 -0
- metadata +22 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f090aa3b8a00d7e7a62e7395fdc02fff3c0fdf4063350a492e1e835932dc5b26
|
|
4
|
+
data.tar.gz: 6d2ae853a4bb64281147c1a67721d93e1a8c38b5d3fb43e6897df85c7be964c7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7cc84fc8fad2d0b18b53e41b96e8d11bc878d8cdad732bc4a02b3581466fdafb927f3fbec54984e0ca04c839c85417008bef31f6efe21b347ebe4fce7ad7672b
|
|
7
|
+
data.tar.gz: 2b005e75062ef195f1178074f97efc738d798e69ee9dccbfb58cd88f1155760f4cbc1712da6bbd8de624673fce6d76a2f0982a0b8b3a58d615add5fc2f626c87
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,198 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.131.0 — 2026-11-05
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **`.studio-border-glow--clear`.** The border glow on a clear button: no fill, only the
|
|
12
|
+
moving rainbow ring and its halo, with the heading ink as the label (16:1 light, 18:1 dark,
|
|
13
|
+
measured). `<button class="btn studio-border-glow studio-border-glow--clear">`. Under
|
|
14
|
+
reduced motion the ring and halo of every `.studio-border-glow` stand still.
|
|
15
|
+
- **The loader family: the eight ways a page says wait, in one place.** From the
|
|
16
|
+
reel Alex McRitchie shared (task `engine-loader-family`). `studio_loader` takes
|
|
17
|
+
three plain styles that draw no icon: `style: :spinner`, `style: :dots` and
|
|
18
|
+
`style: :bar` (a progressbar; `percent:` is its value, and with none it sweeps).
|
|
19
|
+
They wrap the canonical `.spinner`, `.loading-dots` and `.progress-meter` rules,
|
|
20
|
+
so there is one set of rules; an app's own `config.loader` still refuses them.
|
|
21
|
+
New: `studio_skeleton` (`:lines`, `:avatar`, `:card`, `:media`; aria-busy, shapes
|
|
22
|
+
hidden from assistive tech, a shimmer that stops under reduced motion),
|
|
23
|
+
`studio_infinite_scroll` (the next page loads in a Turbo frame as the list's end
|
|
24
|
+
nears, "Load more" stays visible, and a polite status line counts what came) and
|
|
25
|
+
`studio_pull_to_refresh` (phones with touch only). `/admin/style` has a Loaders
|
|
26
|
+
section with all eight, and a live infinite scroll from an admin-only demo feed
|
|
27
|
+
(`/admin/style/loaders_feed`). Busy buttons can show their busy words with
|
|
28
|
+
`data-loader-busy`. Docs: `docs/LOADER.md`, "The family".
|
|
29
|
+
- **Agent replies show a small, safe Markdown subset.** In `studio/agent_chat`, an agent
|
|
30
|
+
turn's `**bold**`, `*italics*`, `` `code` ``, http(s) links, `- ` and `1. ` lists and
|
|
31
|
+
line breaks render as such, and a leading `> ` is dropped. The nodes are built with
|
|
32
|
+
`createElement` and text nodes, never HTML; any other markup, and any link that is not
|
|
33
|
+
http or https, shows as its own characters. A visitor's words stay plain text. Detail:
|
|
34
|
+
`docs/AGENT_CHAT.md`, "The page's script".
|
|
35
|
+
- **A spotlight style for the carousel: `studio/_carousel`, `style: :spotlight`.** The
|
|
36
|
+
same carousel, arrows, dots and keys, but each slide snaps to the centre. The centred
|
|
37
|
+
slide is full size and colour, with an optional `caption:` line under it (a callable on
|
|
38
|
+
the item); its neighbours are smaller (0.75), faded and grey, and the next ones peek at
|
|
39
|
+
the edges. An arrow or key moves one slide, and every slide gets a dot. Where the browser
|
|
40
|
+
has scroll-driven animations (`animation-timeline: view()`), each slide's own view
|
|
41
|
+
timeline drives the look, so it follows the finger; elsewhere the controller marks the
|
|
42
|
+
centred slide `data-current` and CSS transitions it. Reduced motion scales and greys
|
|
43
|
+
nothing: the neighbours are only dimmed. An unknown `style:` raises. Specimen on
|
|
44
|
+
`/admin/style`, Tricks.
|
|
45
|
+
- **Talk to an agent in a modal: `studio/agent_chat/_talk_modal`.** Render it once on a page,
|
|
46
|
+
then mark any link with `studio_talk_to_agent_link(label, href:, soul:, topic:, opener:)`. A
|
|
47
|
+
click opens the shared agent chat in a card on the page-scoped modal host and sends the
|
|
48
|
+
opener as the visitor's first message once the thread loads, so the agent is already
|
|
49
|
+
answering while the card opens. A new answer types itself in. It is the same chat and the
|
|
50
|
+
same hub thread as `studio/agent_chat/chat`, not a second one. The opener goes once per soul
|
|
51
|
+
and topic per tab. A modified click, a page with no modal, or a viewer
|
|
52
|
+
`agent_chat_allowed` refuses follows the link's `href`. The chat takes two new section
|
|
53
|
+
attributes, `data-agent-chat-opener` (with `-opener-key`) and `data-agent-chat-reveal`, and
|
|
54
|
+
the partial takes `compact: true`. Detail: `docs/AGENT_CHAT.md`, "Talk to an agent in a
|
|
55
|
+
modal".
|
|
56
|
+
- **A crumb trail on the page header, from one parent map.** `studio/_page_header` takes
|
|
57
|
+
`crumbs:` (the page's ancestors, root first) and `crumb_label:`. The trail sits above the
|
|
58
|
+
title: muted ancestor links, the page itself bold, a chevron between. Below 40rem it gives
|
|
59
|
+
way to one "‹ Parent" link. An app names each parent page once in `Studio.crumbs.draw`
|
|
60
|
+
(`page :deployments, "Deployments", :deployments_path, parent: :admin`), and a view calls
|
|
61
|
+
`studio_crumbs(:key)` or `studio_crumbs(parent: :key)`. The row also takes `links:`, the
|
|
62
|
+
page's other links on a line at its foot that sticks with it, and renders `actions_html`
|
|
63
|
+
after the tabs when it has both. Detail: `docs/PAGE_HEADER.md`.
|
|
64
|
+
- **The gliding pill.** The page header's open tab is one pill, anchored by CSS anchor
|
|
65
|
+
positioning to the tab with `aria-current`, so it glides to the next tab when the scroll spy
|
|
66
|
+
or a click moves `aria-current`. No script measures it. Reduced motion jumps. A browser
|
|
67
|
+
without anchor positioning keeps each tab's own fill.
|
|
68
|
+
|
|
69
|
+
- **A carousel standard for the apps: `studio/_carousel`.** Render it as a layout, with
|
|
70
|
+
`label:` and `items:`, and the block draws each slide, so a slide is any block content.
|
|
71
|
+
The track is a native scroll-snap row: a swipe, a trackpad and a focused link move it with
|
|
72
|
+
no script. A wide row shows `per_view:` slides (default 3), a medium row up to two and a
|
|
73
|
+
phone one, each with a sliver of the next; the widths come from container queries on the
|
|
74
|
+
region, not the window. The lazy `carousel` controller adds the edge fade, arrows that
|
|
75
|
+
set `aria-disabled` at an end, dots that hide past the last stop, the arrow keys with Home
|
|
76
|
+
and End, and a polite status line after a press. It follows the APG carousel pattern:
|
|
77
|
+
a labelled region with `aria-roledescription="carousel"`, and each slide a labelled
|
|
78
|
+
group (`slide_label:` names it from its item). It honours `prefers-reduced-motion`, and
|
|
79
|
+
its arrows take the card fill, so they are glass under the glass surfaces. Until the
|
|
80
|
+
controller connects, the controls stay hidden and the row keeps a thin scrollbar. New
|
|
81
|
+
classes: `carousel`, `carousel-track`, `carousel-slide`, `carousel-controls`,
|
|
82
|
+
`carousel-dots`, `carousel-dot`, `carousel-arrows` and `carousel-button`. `/admin/style`
|
|
83
|
+
shows it under Tricks.
|
|
84
|
+
|
|
85
|
+
### Changed
|
|
86
|
+
|
|
87
|
+
- **Back plays the app's page move the other way.** Browser Back (a Turbo restore that Turbo
|
|
88
|
+
marks `back`, or a full load that traverses to an earlier entry) and a click on a link with
|
|
89
|
+
`data-page-transition-back` (every ancestor crumb and the phone's parent link) play the
|
|
90
|
+
reverse direction in the same style: a `:left` slide goes back as `slide-right`, an
|
|
91
|
+
`:unfold` with `:right` as `unfold-right`. Forward keeps the app's own direction. Detail:
|
|
92
|
+
`docs/PAGE_TRANSITIONS.md`, "Back".
|
|
93
|
+
- **The page header's title is the type scale's h2 from 40rem** (30px, weight 800), Alex
|
|
94
|
+
McRitchie's "H2 header"; a phone keeps 1.5x the base. It is still the page's one `h1`.
|
|
95
|
+
|
|
96
|
+
### Fixed
|
|
97
|
+
|
|
98
|
+
- **Review follow-ups on the sidebar, the carousel and the page header** (/tasks/engine-ui-polish-follow-ups).
|
|
99
|
+
The link sidebar's focus ring (its rows, close button, Log out, trigger and geo rows) is
|
|
100
|
+
`ring-primary-ink`, a new ring colour from the theme's primary ink: measured 5.4 to 8.9:1
|
|
101
|
+
beside every surface in dark, light and glass on five palettes, from 1.3 to 2.0:1 for
|
|
102
|
+
`ring-primary/40`. A carousel arrow or key that goes nowhere scrolls and announces nothing;
|
|
103
|
+
keys inside a slider, listbox, combobox, tab list, other arrow-key widget or nested carousel
|
|
104
|
+
stay theirs; and the focusable track is a group named "<label> slides". The page header row
|
|
105
|
+
is now always a `<div>`: with tabs, the tabs are their own `<nav>` named by `label`, beside
|
|
106
|
+
the crumb and links navs, so no landmark nests in another (a host test that found the row by
|
|
107
|
+
`nav[aria-label=…]` now finds the tabs). The `/admin/style` specimen adds no landmark.
|
|
108
|
+
The current link under the title is bold as well as coloured, and the pill's track sets
|
|
109
|
+
`anchor-scope`. `studio_crumbs` with a key the map lacks still raises in development and
|
|
110
|
+
test; elsewhere it logs to ErrorLog and draws no trail, not a 500. The dead redirect guard
|
|
111
|
+
in `studio/page_transitions` is gone.
|
|
112
|
+
- **`/admin/style`: Tricks is second, and nothing in the header is cut off.** The sections and
|
|
113
|
+
their pills run Theme, Tricks, Loaders, Modals, Tasks, Components (a host's own modals still
|
|
114
|
+
follow Modals). The tab scroller gives its pills 4px of room on every side, so their shadow, lit
|
|
115
|
+
edge and focus ring show, and the last pill's right edge too; phones still scroll sideways.
|
|
116
|
+
Each specimen card has one Copy (the snippet's, or the class name's when there is no
|
|
117
|
+
snippet), and the code wraps clear of it.
|
|
118
|
+
- **The talk modal's opener is never dropped.** A visitor who sent a message before the
|
|
119
|
+
thread first loaded lost the opener: `studio/agent_chat` marked it sent, then the
|
|
120
|
+
post returned early because the visitor's send was in flight. Now a send made
|
|
121
|
+
before the load posts the opener first, then the visitor's words; a send made while
|
|
122
|
+
the opener is in flight waits for it; and the tab remembers the opener only as its
|
|
123
|
+
POST starts, forgetting it if the POST fails. It is never sent twice.
|
|
124
|
+
- **`e2e/talk_to_agent.spec.js`'s modified-click spec no longer flakes.** It waited for
|
|
125
|
+
the new tab a Ctrl+click opens, which on CI's headless Linux Chromium stayed at
|
|
126
|
+
`about:blank` (it read "blank", and a wait for its URL timed out), so it measured the
|
|
127
|
+
browser rather than the modal. It now asserts what the modal owes a modified click:
|
|
128
|
+
the click is not prevented (the browser follows the `href`) and no card opens.
|
|
129
|
+
- **A signup that cannot send its email no longer fails silently.** `RegistrationsController#create`
|
|
130
|
+
ended in a `rescue StandardError` that logged nothing and rendered `registrations/new`. On a
|
|
131
|
+
passwordless app `@user` was never set, so a failed link mint or mail raised `NoMethodError` on
|
|
132
|
+
nil in that view, and no ErrorLog row named the first failure. Now a passwordless failure is
|
|
133
|
+
logged through `rescue_and_log` and redirects to `/signup` with one friendly alert for every
|
|
134
|
+
address, so it reveals nothing about which emails have accounts. The account is still created
|
|
135
|
+
only when the link is used. A password signup re-renders the form on a validation error and
|
|
136
|
+
writes no ErrorLog row. A POST with no `user` params gets the form too, not a
|
|
137
|
+
`ParameterMissing`. Any other raise is logged once and goes to the catch-all. The rescue no
|
|
138
|
+
longer names the unsaved user as its target: ErrorLog's `belongs_to` autosaved it, which ran the
|
|
139
|
+
failing save again inside the logger. That second raise skipped the logged mark, so the
|
|
140
|
+
catch-all wrote the same failure twice.
|
|
141
|
+
- **`rescue_and_log` writes one ErrorLog row per failure, also for an unsaved target.** It
|
|
142
|
+
linked `target:` with ErrorLog's `belongs_to`, which saves a new record. With an unsaved
|
|
143
|
+
record, the logger ran the failing save a second time. That raise skipped the logged mark,
|
|
144
|
+
so the catch-all wrote the same failure again, and a save that could pass created the
|
|
145
|
+
record. Now an unsaved target or parent is named on the row by its slug (else
|
|
146
|
+
`unsaved User`) and not linked. This covers the SSO, wallet and link sign-ups and the theme save. When the
|
|
147
|
+
ErrorLog write itself fails, in `rescue_and_log` or in the catch-all, `Rails.logger.error`
|
|
148
|
+
records both errors. `rescue_and_log` still raises the original error, and the catch-all
|
|
149
|
+
still gives its friendly reply. Before, the logger's error replaced both.
|
|
150
|
+
- **`POST /magic_link` answers a failed mint or mail the same way for every address.** The
|
|
151
|
+
raise went to the catch-all: "Something went wrong" on the home page, or a JSON 500. Now
|
|
152
|
+
the failure is logged once and the visitor goes back to `/login` with one alert. JSON gets
|
|
153
|
+
`503` with `{ success: false, error: "delivery_failed", message: … }`: the input was
|
|
154
|
+
fine and a retry can work, so it is not a 4xx. The body does not carry the internal
|
|
155
|
+
message, and the action never looks an account up, so it reveals nothing about which
|
|
156
|
+
emails have accounts. `Studio::MagicLinkIssuing::DELIVERY_FAILED` holds the words.
|
|
157
|
+
|
|
158
|
+
## 0.130.0 — 2026-11-05
|
|
159
|
+
|
|
160
|
+
### Added
|
|
161
|
+
|
|
162
|
+
- **Section headers that stack under the page header: `studio/_section_header`.** Render it
|
|
163
|
+
as the first child of a `<section>`. The title sticks directly under the page row, takes a
|
|
164
|
+
bar (solid in light and dark, glass under glass), and the next section's header takes its
|
|
165
|
+
place, by CSS sticky and no scroll script. `/admin/style` uses it on every section,
|
|
166
|
+
including a host's own. It joins the pinned stack as one layer, `section`: a new
|
|
167
|
+
`data-pin-section`, and `--pin-section-top`, `-h` and `-bottom`. Only the stuck header
|
|
168
|
+
counts, so `--pin-stack-bottom` and a table's sticky header sit under it. New classes:
|
|
169
|
+
`section-header`, `section-target` and `section-subtarget`, which
|
|
170
|
+
replace a hand-written `scroll-margin-top`. The publisher now reads the layers once more after
|
|
171
|
+
a settle and after a layer's top moves, so a layer under a layer does not keep a stale line.
|
|
172
|
+
Detail: `docs/PAGE_HEADER.md`.
|
|
173
|
+
### Changed
|
|
174
|
+
|
|
175
|
+
- **A lit button's gradient centers on its own colour.** Every lit variant steps lightness 0.06 up
|
|
176
|
+
and 0.06 down from its fill, with chroma and hue unchanged, so the painted midpoint is the token.
|
|
177
|
+
The lit primary now centers on `--color-cta`, the violet a `text-primary` word paints, instead of
|
|
178
|
+
sitting darker and more saturated. White on the default primary is about 3.1:1 at the midpoint;
|
|
179
|
+
Alex McRitchie accepted that so the button and the brand word read as one purple. The semantic
|
|
180
|
+
fills still hold 4.5:1 there. `/lab/hero_button` shows a `text-primary` word beside the button.
|
|
181
|
+
|
|
182
|
+
- **Tricks starts with the buttons, then what you can add to them.** A new Button tricks group
|
|
183
|
+
sits directly under Buttons: rotating edge glow, border glow, sheen, pulse, fizz, hold to confirm,
|
|
184
|
+
confetti burst and cannons. Each sits on a real `.btn` or fires from one, with its code to copy.
|
|
185
|
+
The other groups keep their order, with one short sentence each at most.
|
|
186
|
+
- **`.studio-team-glow` is now `.edge-rotate-glow`, with `--edge-rotate-glow-*` knobs.** The old class
|
|
187
|
+
and knobs are a deprecated alias and paint the same, so no host has to change.
|
|
188
|
+
- **Fizz works around any button.** `render layout: "studio/fizz", locals: { seed: "save" }` wraps a
|
|
189
|
+
button in a `.fizz-stack`. The hold button's markup and selectors are unchanged. The only tie was
|
|
190
|
+
the `.hold-stack` selector.
|
|
191
|
+
- **Trial: a dark, full-size lit button's fill drops from 0.94 to 0.85 alpha** (`--lit-fill-alpha`).
|
|
192
|
+
|
|
193
|
+
### Fixed
|
|
194
|
+
|
|
195
|
+
- **The button tricks keep a lit button lit.** A lit button's `::after` closed the hole in the glow's
|
|
196
|
+
bloom, which washed its whole face. The sheen clipped the top highlight, and the pulse dropped the
|
|
197
|
+
lit shadow. `.studio-border-glow` on a button no longer ends its halo on a hard box edge.
|
|
198
|
+
|
|
7
199
|
## 0.129.0 — 2026-11-05
|
|
8
200
|
|
|
9
201
|
### Changed
|
data/README.md
CHANGED
|
@@ -31,7 +31,7 @@ resolved.
|
|
|
31
31
|
## What It Provides
|
|
32
32
|
|
|
33
33
|
- **Authentication**: Passwordless magic-link auth, optional password auth, Google OAuth via OmniAuth (ships ready: strategy, default `User.from_omniauth` and a setup modal until the OAuth client is set; docs/GOOGLE_AUTH_SETUP.md), Solana wallet sign-in, and optional one-way SSO patterns
|
|
34
|
-
- **Error handling**: `Studio::ErrorHandling` concern with `rescue_and_log`, `ErrorLog` model with `capture!`, error log viewer at `/error_logs`
|
|
34
|
+
- **Error handling**: `Studio::ErrorHandling` concern with `rescue_and_log`, `ErrorLog` model with `capture!`, error log viewer at `/error_logs`. `rescue_and_log` writes one row per failure: an unsaved `target:` or `parent:` is named on the row by its slug (else `unsaved User`), never linked, so the logger never saves it. If the row itself cannot be written, the original error still raises and `Rails.logger.error` records both.
|
|
35
35
|
- **Session drift**: every page carries a session stamp and loads `window.StudioSession`, which notices when another tab signs in or out, the session expires, or the server revokes it; `session:changed` / `session:mismatch` events, cross-tab sync, an identity-source plug-in interface, and `expectChange` holds for deliberate switches. The rehydrate endpoint (`GET /session/state`) is opt-in. See [`docs/SESSION_DRIFT.md`](docs/SESSION_DRIFT.md).
|
|
36
36
|
- **Theme system**: Dynamic CSS custom properties generated from 7 role colors (primary, dark, light, success, accent, warning, danger). Dark/light mode toggle. Admin theme editor at `/admin/theme`.
|
|
37
37
|
- **UI primitives**: Shared component partials and CSS primitives such as `components/emoji_swap` for nav/sidebar emoji hover transitions.
|
|
@@ -184,7 +184,9 @@ form only when `:password` is enabled and the User has `authenticate`
|
|
|
184
184
|
sign-in-link form when `:magic_link` is enabled, and the Google button only with
|
|
185
185
|
`:google`. A passwordless signup stores nothing from the form: it mails a link,
|
|
186
186
|
and using the link creates the account. So `registration_params`' `:name` shows a
|
|
187
|
-
Name field only on a password app.
|
|
187
|
+
Name field only on a password app. If the link cannot be minted or mailed, the
|
|
188
|
+
failure is logged to ErrorLog and the visitor goes back to `/signup` with one
|
|
189
|
+
alert, the same for every address.
|
|
188
190
|
|
|
189
191
|
`POST /login`, the password exchange, is drawn only when `auth_methods` includes
|
|
190
192
|
`:password`. A passwordless app answers it with 404 for every address, so it
|
|
@@ -242,7 +244,7 @@ Rails.application.routes.draw do
|
|
|
242
244
|
end
|
|
243
245
|
```
|
|
244
246
|
|
|
245
|
-
This draws the enabled auth routes (`/login`, `/signup`, `/logout`, `POST /magic_link` to request a link, `GET`/`POST /l/:token` for the link itself, Solana routes), OAuth callbacks, optional SSO routes, `/error_logs`, and `/admin/theme`. Set `Studio.draw_geo_routes = true` to add the geo manager (`/admin/geo`) and its public probe (`/geo/check`) — off by default because turf-monster owns those helper names until its adoption lands. Magic-link emails point at the inert `GET /l/:token` confirmation page; the single-use token is burned only by the CSRF-protected `POST` to `link_consume_path`.
|
|
247
|
+
This draws the enabled auth routes (`/login`, `/signup`, `/logout`, `POST /magic_link` to request a link, `GET`/`POST /l/:token` for the link itself, Solana routes), OAuth callbacks, optional SSO routes, `/error_logs`, and `/admin/theme`. Set `Studio.draw_geo_routes = true` to add the geo manager (`/admin/geo`) and its public probe (`/geo/check`) — off by default because turf-monster owns those helper names until its adoption lands. If `POST /magic_link` cannot mint or mail the link, it logs the failure and answers the same for every address: the login page with one alert, or a `503` with `{ success: false, error: "delivery_failed", message: … }` for JSON. Magic-link emails point at the inert `GET /l/:token` confirmation page; the single-use token is burned only by the CSRF-protected `POST` to `link_consume_path`.
|
|
246
248
|
|
|
247
249
|
Set `Studio.draw_session_routes = true` to add the session-drift rehydrate endpoint (`GET /session/state`) — off by default because it inherits the host's filters, which an app should check first ([`docs/SESSION_DRIFT.md`](docs/SESSION_DRIFT.md)).
|
|
248
250
|
|
|
@@ -252,7 +254,7 @@ Set `Studio.draw_survey_routes = true` to add the public survey pages (`/surveys
|
|
|
252
254
|
|
|
253
255
|
**Magic links need the `studio_links` table.** Install it with `bin/rails studio_engine:install:migrations && bin/rails db:migrate` (install all of them) before enabling `:magic_link` — never by hand-copying the migration, which collides with the task's own copy on `class CreateStudioLinks`. Without the table, the first sign-in raises `Studio::Link::MissingTable`.
|
|
254
256
|
|
|
255
|
-
Set `Studio.agent_chat_souls = %w[alex]` to add the shared agent chat's proxy (`POST /agent_chat/messages`, `GET /agent_chat/turns`), which forwards to the McRitchie Studio hub with the app's `AGENT_CHAT_TOKEN`; then `render "studio/agent_chat/chat", soul: "alex", name: "Alex", portrait: ...` on any page, with `voice: true` for hold to talk in the composer ([`docs/AGENT_CHAT.md`](docs/AGENT_CHAT.md)).
|
|
257
|
+
Set `Studio.agent_chat_souls = %w[alex]` to add the shared agent chat's proxy (`POST /agent_chat/messages`, `GET /agent_chat/turns`), which forwards to the McRitchie Studio hub with the app's `AGENT_CHAT_TOKEN`; then `render "studio/agent_chat/chat", soul: "alex", name: "Alex", portrait: ...` on any page, with `voice: true` for hold to talk in the composer ([`docs/AGENT_CHAT.md`](docs/AGENT_CHAT.md)). To open that chat in a modal from any button, render `studio/agent_chat/talk_modal` once and mark the buttons with `studio_talk_to_agent_link` ([`docs/AGENT_CHAT.md`](docs/AGENT_CHAT.md), "Talk to an agent in a modal").
|
|
256
258
|
|
|
257
259
|
In non-production local requests, this also draws `/_studio/local_emails`, a local email inbox for agent/worktree proof flows. Set `LOCAL_EMAIL_CAPTURE=1` or run with `AGENT_WORKTREE=1` to record outbox rows without sending real email.
|
|
258
260
|
|
|
@@ -290,15 +292,55 @@ actions; the engine supplies only the banner.
|
|
|
290
292
|
|
|
291
293
|
### The page header — `studio/_page_header`
|
|
292
294
|
|
|
293
|
-
An admin page's title: a
|
|
294
|
-
|
|
295
|
+
An admin page's title: a crumb trail from the app's one parent map (`Studio.crumbs`,
|
|
296
|
+
`studio_crumbs`) above an h2-size title, tabs with a gliding pill and one action on the right,
|
|
297
|
+
and the page's other links on a line under it. A plain header at rest, a bar once stuck under
|
|
298
|
+
the navbar, in the pinned stack so a table's sticky header sits under it
|
|
295
299
|
([`docs/PAGE_HEADER.md`](docs/PAGE_HEADER.md)).
|
|
296
300
|
|
|
301
|
+
```erb
|
|
302
|
+
<%= render "studio/page_header", title: "Deployments", crumbs: studio_crumbs(:deployments),
|
|
303
|
+
label: "Views", tabs: [{ label: "Board", href: deployments_path, current: "page" }],
|
|
304
|
+
actions_html: link_to("New Task", new_task_path, class: "btn btn-sm btn-primary"),
|
|
305
|
+
links: [{ label: "Epics", href: epics_path }] %>
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### The section header — `studio/_section_header`
|
|
309
|
+
|
|
310
|
+
A section's title that sticks under the page row until the next section's takes its place,
|
|
311
|
+
with a bar once stuck ([`docs/PAGE_HEADER.md`](docs/PAGE_HEADER.md#the-section-header)).
|
|
312
|
+
|
|
297
313
|
```erb
|
|
298
314
|
<%= render "studio/page_header", title: "Models", meta: "v1.2.3", label: "Models",
|
|
299
315
|
tabs: [{ label: "Users", href: users_path, current: "page" }, { label: "Teams", href: teams_path }] %>
|
|
300
316
|
```
|
|
301
317
|
|
|
318
|
+
### The carousel — `studio/_carousel`
|
|
319
|
+
|
|
320
|
+
The apps' standard for a set too long to grid: a swipeable scroll-snap row with
|
|
321
|
+
arrows, dots and arrow keys, a peek of the next slide, and an edge fade. Render it
|
|
322
|
+
as a layout; the block draws each slide. `per_view:` is the wide row's count
|
|
323
|
+
(default 3). `/admin/style` shows it under Tricks.
|
|
324
|
+
|
|
325
|
+
```erb
|
|
326
|
+
<%= render layout: "studio/carousel", locals: { label: "Portfolio", items: @apps, per_view: 3 } do |app| %>
|
|
327
|
+
<%= render "apps/card", app: app %>
|
|
328
|
+
<% end %>
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
`style: :spotlight` snaps each slide to the centre: the centred slide is full size and
|
|
332
|
+
colour, with its `caption:` line (a callable on the item) under it, and its neighbours
|
|
333
|
+
are smaller, faded and grey. The arrows and keys move one slide. Where the browser has
|
|
334
|
+
scroll-driven animations the look follows the scroll; elsewhere the controller marks the
|
|
335
|
+
centred slide (`data-current`) and CSS transitions it. Reduced motion only dims.
|
|
336
|
+
|
|
337
|
+
```erb
|
|
338
|
+
<%= render layout: "studio/carousel",
|
|
339
|
+
locals: { label: "Team", items: @people, style: :spotlight, caption: ->(person) { person.title } } do |person| %>
|
|
340
|
+
<%= render "people/card", person: person %>
|
|
341
|
+
<% end %>
|
|
342
|
+
```
|
|
343
|
+
|
|
302
344
|
### The "at" time stamp — `at_time_tag`
|
|
303
345
|
|
|
304
346
|
Stamps WHEN something happened, on the reader's own clock: `at 3:53p`, gaining a
|