studio-engine 0.23.0 → 0.25.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 +121 -0
- data/README.md +22 -0
- data/app/assets/tailwind/studio_engine/engine.css +59 -0
- data/app/controllers/concerns/studio/error_handling.rb +34 -6
- data/app/views/layouts/studio/_head.html.erb +4 -2
- data/app/views/layouts/studio/_smooth_load.html.erb +15 -0
- data/app/views/studio/_leveling_activity_assets.html.erb +90 -12
- data/app/views/studio/modals/blocks/_change_username.html.erb +1 -1
- data/app/views/studio/modals/blocks/_leveling_activity.html.erb +93 -43
- data/app/views/style/_modal_specimen.html.erb +6 -1
- data/app/views/style/_modals.html.erb +157 -128
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +14 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ecc09d648e749381eed240dbdf6290e6899bfff776a08a8cf76e8cf6fde5da35
|
|
4
|
+
data.tar.gz: bfa162a9a1078281ce421fcc9dcb4b3741fc30ab8bc9147b8eedbd7cee9a825b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d1cc7ce640975c429a0f85ea08cdc66037506efa681995128ad4a3ab76bf0894d1e60bf4738c3d52bc85a2625ea6ee21b60a420b3eef98af56008f0de33f92d5
|
|
7
|
+
data.tar.gz: fd4465c53f2591ff8031fcebee0ce81dfa7acc1113a934b98983a761ad54a2a9a6d4dff8c8b6b1c41b7c8947166f36d69dbf4da182c7e8713b4eb986c79ee6f3
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,127 @@
|
|
|
2
2
|
|
|
3
3
|
The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html) — `MAJOR.MINOR.PATCH`. Consumer Rails apps install the released RubyGems package with `gem "studio-engine", "~> 0.6"`; bumping the gem version and updating consumer lockfiles is a release.
|
|
4
4
|
|
|
5
|
+
## 0.25.0 — 2026-07-28
|
|
6
|
+
|
|
7
|
+
**Profile Leveling** — the `/admin/style` "Leveling activities" section is rebuilt
|
|
8
|
+
into a single toggle-driven "Profile Leveling" flow, and the with-leveling /
|
|
9
|
+
without-leveling FORK is killed. Previously each activity shipped as TWO modal
|
|
10
|
+
ids (`change-username` + `change-username-plain`, `quest-activity` +
|
|
11
|
+
`quest-activity-plain`), because the primitive read `leveling` from a Ruby local
|
|
12
|
+
at render time. Now the primitive reads `leveling` at RUNTIME from the modal
|
|
13
|
+
store's `props.leveling`, so ONE id per activity flips the Turf Monster shape
|
|
14
|
+
(the seeds celebration + Free Entry Token progress) ↔ the McRitchie Studio shape
|
|
15
|
+
(plain input + Save, then a simple confirmation) live as its per-card leveling
|
|
16
|
+
toggle moves.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- **`studio/modals/blocks/_leveling_activity`** — three views now, all ALWAYS
|
|
21
|
+
rendered and gated by the reactive `leveling` getter instead of dropped at ERB
|
|
22
|
+
time: the **form** (`!celebrate`), the **seeds celebration**
|
|
23
|
+
(`celebrate && leveling`, TM), and a **plain confirmation**
|
|
24
|
+
(`celebrate && !leveling`, MS). The `leveling` local becomes the render-time
|
|
25
|
+
fallback only. **The "Quest N of N" counter pill is removed** — the profile
|
|
26
|
+
modals do not carry one. **The "Level N" pill is also dropped** from the
|
|
27
|
+
celebration (no consumer used it); the seeds bar stays. All existing seams
|
|
28
|
+
are preserved unchanged (`submit_url`, `finalize_hook`, `saved_event`,
|
|
29
|
+
`modal_store`, `demo`, seeds/level props), so Turf Monster's live usage and the
|
|
30
|
+
opaque save-callback contract are unaffected (TM ships no `props.leveling` and
|
|
31
|
+
keeps its `:leveling`-capability default).
|
|
32
|
+
- **`studio/_leveling_activity_assets`** (factory) — `leveling` is a runtime getter
|
|
33
|
+
reading `props.leveling` off the modal store (falling back to `_levelingDefault`);
|
|
34
|
+
an `init` hook opens the modal directly at the updated state via `props.celebrate`
|
|
35
|
+
(the "updated" cards open the same id pre-advanced, like the Auth step cards);
|
|
36
|
+
`_finishSaved` always advances to the updated state (the view gate picks
|
|
37
|
+
celebration vs plain confirmation) AND mirrors `celebrate` onto the live store
|
|
38
|
+
`props.celebrate`, so the active-card **glow follows the flow** — it transfers
|
|
39
|
+
from the input card to the updated card as the modal advances, exactly like the
|
|
40
|
+
Auth glow follows `props.step`. In **demo** mode the app-facing saved event is
|
|
41
|
+
suppressed, so a host app's follow-on handler (e.g. Turf Monster's
|
|
42
|
+
`quest-success` on `studio:username-saved`) can't stack a second modal over the
|
|
43
|
+
demo's own celebrate view.
|
|
44
|
+
- **`/admin/style`** — "Leveling activities" → **"Profile Leveling"**: **each
|
|
45
|
+
specimen card carries its OWN `Leveling` toggle** (like the Auth card's method
|
|
46
|
+
checkboxes), flipping THAT card between the Turf Monster and McRitchie Studio
|
|
47
|
+
shape at open — so you can preview Change Username leveling-on beside Great
|
|
48
|
+
Username leveling-off. The section walks **four** cards — **Change Username →
|
|
49
|
+
Great Username**, then **Join the Newsletter → Subscribed!** — each "updated"
|
|
50
|
+
card opening its modal straight at the success state, mirroring Turf Monster's
|
|
51
|
+
real copy, seeds, and Free Entry Token progress. No quest counter, no Level pill.
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
- **`_leveling_activity` locals** (all optional, additive): `consent_label` (a
|
|
56
|
+
consent checkbox that gates the action — the newsletter join), `confirm_subtitle`
|
|
57
|
+
(subtext on the MS-shape plain confirmation), `next_label` / `next_open` (a
|
|
58
|
+
"Next Quest" button that walks to the next activity), and `demo_seeds_earned` /
|
|
59
|
+
`demo_seeds_total` (style-guide-only seed payload tuning).
|
|
60
|
+
|
|
61
|
+
## 0.24.1 — 2026-07-28
|
|
62
|
+
|
|
63
|
+
Bug fix — the **`/admin/style` specimen glow failed OPEN**. Every glow-capable
|
|
64
|
+
specimen card (`style/_modal_specimen`) carries the `.studio-team-glow` class
|
|
65
|
+
statically, and the "only the active card glows" behavior rode entirely on a
|
|
66
|
+
reactive Alpine `:style` overriding the primitive's CSS default of
|
|
67
|
+
`--studio-team-glow-opacity: 1` (visible) down to `0`. Before Alpine hydrates —
|
|
68
|
+
or if it never loads — all 13 reactive-glow cards (Auth, Eligibility, Profile,
|
|
69
|
+
Leveling) painted their ring at once. The steady state was correct (the glow
|
|
70
|
+
follows the step machine), so this surfaced as a flash-of-all-glow on load.
|
|
71
|
+
|
|
72
|
+
### Fixed
|
|
73
|
+
|
|
74
|
+
- **`style/_modal_specimen`** now renders a **static inline
|
|
75
|
+
`style="--studio-team-glow-opacity: 0"`** on the glow wrapper beside the
|
|
76
|
+
reactive `:style`, so the ring is OFF at first paint (fail-closed) regardless
|
|
77
|
+
of Alpine timing. The reactive binding still drives `0 ↔ 0.95` with the 0.4s
|
|
78
|
+
cross-fade, so the active-card glow and its slide between step cards are
|
|
79
|
+
unchanged. The `engine-motion.css` `.studio-team-glow` default is untouched
|
|
80
|
+
(the always-on Tricks demos depend on it).
|
|
81
|
+
|
|
82
|
+
## 0.24.0 — 2026-07-28
|
|
83
|
+
|
|
84
|
+
The **smooth-load convention**, opt-in per app. Pages materialize behind the
|
|
85
|
+
current one and present themselves with a view transition — navigation renders
|
|
86
|
+
exactly once, with no stale-preview flash. Modifies an existing primitive:
|
|
87
|
+
`layouts/studio/_head` gains the convention partial and a configurable nav
|
|
88
|
+
spinner minimum.
|
|
89
|
+
|
|
90
|
+
**One ungated visual change on the bump, opt-in or not:** `.turbo-progress-bar`
|
|
91
|
+
in `engine.css` restyles every consumer's Turbo progress bar from Turbo's
|
|
92
|
+
default blue to the app's theme CTA color (3px, `var(--color-cta)` — defined in
|
|
93
|
+
both themes by the theme resolver, and the rule wins Turbo's injected-first
|
|
94
|
+
stylesheet cascade). Everything else is inert until an app sets
|
|
95
|
+
`Studio.smooth_load = true`.
|
|
96
|
+
|
|
97
|
+
### Added
|
|
98
|
+
|
|
99
|
+
- **Smooth-load convention (opt-in)** — `Studio.smooth_load` (default OFF)
|
|
100
|
+
renders `layouts/studio/_smooth_load` from `_head`: the `view-transition`
|
|
101
|
+
meta (Turbo 8 wraps page swaps in `document.startViewTransition`) plus
|
|
102
|
+
`turbo-cache-control: no-preview`, so the next page materializes behind the
|
|
103
|
+
current one and presents itself — navigation renders exactly once. An app
|
|
104
|
+
with known multi-second pages should fix those before opting in (no-preview
|
|
105
|
+
holds the old page until the fresh response arrives).
|
|
106
|
+
- **`Studio.nav_spinner_min_ms`** — the nav spinner's minimum display time,
|
|
107
|
+
previously hardcoded to 2500 in `_head`. Default unchanged (2500); smooth-load
|
|
108
|
+
apps typically drop it to ~300 so fast loads never linger on a spinner.
|
|
109
|
+
- **Smooth-load CSS in `engine.css`** — root fade-out / rise-in view-transition
|
|
110
|
+
keyframes (inert until the metas render), the `.vt-pinned-header` opt-in
|
|
111
|
+
utility (exactly one per page — a duplicate `view-transition-name` silently
|
|
112
|
+
disables the transition), the theme-colored `.turbo-progress-bar` noted
|
|
113
|
+
above, and a `prefers-reduced-motion` kill switch app e2e suites can lean on
|
|
114
|
+
(`reducedMotion: "reduce"`).
|
|
115
|
+
|
|
116
|
+
### Fixed
|
|
117
|
+
|
|
118
|
+
- **`rescue_from` order in `Studio::ErrorHandling`** — `RecordNotFound` now
|
|
119
|
+
resolves to `handle_not_found` (Rescuable matches last-registered first, so
|
|
120
|
+
the catch-all must register first). A missing record renders a real 404 from
|
|
121
|
+
`public/404.html` and **creates no ErrorLog row**; previously it was shadowed
|
|
122
|
+
by the catch-all, logged as an unexpected error, and soft-404'd (302 to root)
|
|
123
|
+
in production HTML. Per-host behavior changes only as each app bumps to this
|
|
124
|
+
release.
|
|
125
|
+
|
|
5
126
|
## 0.23.0 — 2026-07-28
|
|
6
127
|
|
|
7
128
|
Phase D, slice 1 — the **board primitive**. The three near-identical McRitchie
|
data/README.md
CHANGED
|
@@ -42,6 +42,16 @@ Studio.configure do |config|
|
|
|
42
42
|
)
|
|
43
43
|
config.theme_primary = "#4BAF50" # Override default violet
|
|
44
44
|
config.theme_logos = ["logo.svg"]
|
|
45
|
+
|
|
46
|
+
# Smooth-load convention (default OFF). Renders the view-transition +
|
|
47
|
+
# no-preview metas: Turbo page swaps materialize behind the current page and
|
|
48
|
+
# present with a view transition, exactly one render per navigation. Fix any
|
|
49
|
+
# multi-second pages BEFORE opting in — no-preview holds the old page until
|
|
50
|
+
# the fresh response arrives.
|
|
51
|
+
config.smooth_load = true
|
|
52
|
+
# Nav spinner minimum display (default 2500). Smooth-load apps typically
|
|
53
|
+
# drop to ~300; keep the high floor if multi-second ops ride the spinner.
|
|
54
|
+
config.nav_spinner_min_ms = 300
|
|
45
55
|
end
|
|
46
56
|
```
|
|
47
57
|
|
|
@@ -114,6 +124,18 @@ privileges while impersonating.
|
|
|
114
124
|
|
|
115
125
|
## UI Primitives
|
|
116
126
|
|
|
127
|
+
### Smooth-load header pin — `.vt-pinned-header`
|
|
128
|
+
|
|
129
|
+
When `Studio.smooth_load` is on, put `vt-pinned-header` on the app's sticky
|
|
130
|
+
header: it gets its own named view-transition group, so page content
|
|
131
|
+
transitions beneath a navbar that stays put (or smoothly morphs heights).
|
|
132
|
+
**Exactly one element per page** — a duplicate `view-transition-name` makes
|
|
133
|
+
the browser silently skip the whole transition, with no error and no animation.
|
|
134
|
+
|
|
135
|
+
```erb
|
|
136
|
+
<header class="sticky top-0 vt-pinned-header ...">
|
|
137
|
+
```
|
|
138
|
+
|
|
117
139
|
Render `components/emoji_swap` inside a link or button with the `group` class to
|
|
118
140
|
slide between two emoji on hover and keyboard focus. The CSS ships through
|
|
119
141
|
`studio_theme_css_tag`, including a reduced-motion fade fallback.
|
|
@@ -180,3 +180,62 @@
|
|
|
180
180
|
@utility btn-lg {
|
|
181
181
|
@apply px-8 py-3 text-base rounded-xl shadow-sm;
|
|
182
182
|
}
|
|
183
|
+
|
|
184
|
+
/* -- Smooth-load convention ------------------------------------------------
|
|
185
|
+
Paired with layouts/studio/_smooth_load.html.erb (the Studio.smooth_load
|
|
186
|
+
metas that opt Turbo into document.startViewTransition). These rules are
|
|
187
|
+
inert until an app renders those metas, so shipping them in the auto-bundled
|
|
188
|
+
layer costs nothing. The outgoing page fades while the incoming one rises in
|
|
189
|
+
underneath — quick enough to read as polish, not as a wait. */
|
|
190
|
+
::view-transition-old(root) {
|
|
191
|
+
animation: vt-page-out 160ms cubic-bezier(0.4, 0, 1, 1) both;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
::view-transition-new(root) {
|
|
195
|
+
animation: vt-page-in 220ms cubic-bezier(0, 0, 0.2, 1) both;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
@keyframes vt-page-out {
|
|
199
|
+
to {
|
|
200
|
+
opacity: 0;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
@keyframes vt-page-in {
|
|
205
|
+
from {
|
|
206
|
+
opacity: 0;
|
|
207
|
+
transform: translateY(8px);
|
|
208
|
+
}
|
|
209
|
+
to {
|
|
210
|
+
opacity: 1;
|
|
211
|
+
transform: none;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* Opt-in header pin: put this on the app's sticky header so it gets its own
|
|
216
|
+
named transition group, lifted out of the root snapshot — page content
|
|
217
|
+
transitions beneath a navbar that stays put (or smoothly morphs heights).
|
|
218
|
+
EXACTLY ONE element per page: a duplicate view-transition-name makes the
|
|
219
|
+
browser silently skip the whole transition. */
|
|
220
|
+
@utility vt-pinned-header {
|
|
221
|
+
view-transition-name: studio-header;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/* Turbo's progress bar is the slow-load feedback under no-preview (the old
|
|
225
|
+
page holds until the fresh response). Theme it so every app's bar matches
|
|
226
|
+
its brand instead of Turbo's default. */
|
|
227
|
+
.turbo-progress-bar {
|
|
228
|
+
height: 3px;
|
|
229
|
+
background: var(--color-cta);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/* Reduced motion: keep the single-render swap, drop the choreography. Also
|
|
233
|
+
what the apps' e2e suites lean on (reducedMotion: "reduce") for stable
|
|
234
|
+
clicks. */
|
|
235
|
+
@media (prefers-reduced-motion: reduce) {
|
|
236
|
+
::view-transition-group(*),
|
|
237
|
+
::view-transition-old(*),
|
|
238
|
+
::view-transition-new(*) {
|
|
239
|
+
animation: none !important;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -3,8 +3,15 @@ module Studio
|
|
|
3
3
|
extend ActiveSupport::Concern
|
|
4
4
|
|
|
5
5
|
included do
|
|
6
|
-
|
|
6
|
+
# ORDER IS LOAD-BEARING — ActiveSupport::Rescuable resolves handlers with
|
|
7
|
+
# `reverse_each`, so the LAST matching rescue_from registered wins. The
|
|
8
|
+
# catch-all must therefore be declared FIRST and the specific handlers
|
|
9
|
+
# after it, or StandardError shadows every one of them: RecordNotFound
|
|
10
|
+
# rendered "Something went wrong" and wrote an ErrorLog row on every 404,
|
|
11
|
+
# fleet-wide, while handle_not_found sat unreachable. Add new specific
|
|
12
|
+
# handlers BELOW this line, never above it.
|
|
7
13
|
rescue_from StandardError, with: :handle_unexpected_error
|
|
14
|
+
rescue_from ActiveRecord::RecordNotFound, with: :handle_not_found
|
|
8
15
|
|
|
9
16
|
before_action :require_authentication
|
|
10
17
|
|
|
@@ -195,12 +202,33 @@ module Studio
|
|
|
195
202
|
raise e
|
|
196
203
|
end
|
|
197
204
|
|
|
198
|
-
# Layer 1:
|
|
205
|
+
# Layer 1: RecordNotFound — Rails' own 404, never an ErrorLog row.
|
|
206
|
+
#
|
|
207
|
+
# This handler exists for exactly ONE reason: to stop `handle_unexpected_error`
|
|
208
|
+
# from claiming a RecordNotFound and writing an ErrorLog row for it. It
|
|
209
|
+
# deliberately renders NOTHING and re-raises, which hands the exception to
|
|
210
|
+
# `ActionDispatch::PublicExceptions` — the same path an app with no rescue_from
|
|
211
|
+
# at all takes. Registering the handler and re-raising is therefore not a
|
|
212
|
+
# no-op: matching here is what keeps the catch-all's `create_error_log` from
|
|
213
|
+
# running (a raise inside a rescue_from handler propagates out; Rescuable does
|
|
214
|
+
# not re-dispatch it into another handler).
|
|
215
|
+
#
|
|
216
|
+
# Why nothing is rendered here. The method was unreachable from the initial
|
|
217
|
+
# commit until the rescue_from order was fixed (see the note in `included`),
|
|
218
|
+
# so whatever it did had never actually run — consumers have been served
|
|
219
|
+
# Rails' standard 404 all along. Anything rendered here would therefore be a
|
|
220
|
+
# NEW fleet-wide behaviour, not a restoration: an earlier attempt at this fix
|
|
221
|
+
# rendered public/404.html and silently flipped `Accept: */*` clients (curl,
|
|
222
|
+
# uptime monitors, crawlers, link previewers) from `404 text/html` to
|
|
223
|
+
# `404 application/json`, changed the JSON body shape, and dropped the
|
|
224
|
+
# DebugExceptions backtrace in development. Re-raising keeps every one of
|
|
225
|
+
# those intact, along with `public/404.<locale>.html` and any
|
|
226
|
+
# `config.exceptions_app` a host configures.
|
|
227
|
+
#
|
|
228
|
+
# The bug being fixed is the ErrorLog pollution and the "Something went wrong"
|
|
229
|
+
# copy — never the status code, the format, or the body.
|
|
199
230
|
def handle_not_found(exception)
|
|
200
|
-
|
|
201
|
-
format.html { redirect_to root_path, alert: "Not found" }
|
|
202
|
-
format.json { render json: { error: "Not found" }, status: :not_found }
|
|
203
|
-
end
|
|
231
|
+
raise exception
|
|
204
232
|
end
|
|
205
233
|
|
|
206
234
|
# Layer 1: Catch-all for unexpected errors — log + friendly response.
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
11
11
|
<%= csrf_meta_tags %>
|
|
12
12
|
<%= csp_meta_tag %>
|
|
13
|
+
<%= render "layouts/studio/smooth_load" %>
|
|
13
14
|
<link rel="icon" type="image/png" href="/favicon.png">
|
|
14
15
|
|
|
15
16
|
<%= yield :head %>
|
|
@@ -46,9 +47,10 @@
|
|
|
46
47
|
</script>
|
|
47
48
|
<script>
|
|
48
49
|
// Nav spinner — scale morph between theme toggle and loading spinner
|
|
49
|
-
// Minimum display time prevents quick flashes
|
|
50
|
+
// Minimum display time prevents quick flashes; per-app via
|
|
51
|
+
// Studio.nav_spinner_min_ms (smooth-load apps drop it to ~300).
|
|
50
52
|
window._navSpinnerShownAt = 0;
|
|
51
|
-
window._navSpinnerMinMs =
|
|
53
|
+
window._navSpinnerMinMs = <%= Studio.nav_spinner_min_ms.to_i %>;
|
|
52
54
|
window.showNavSpinner = function() {
|
|
53
55
|
window._navSpinnerShownAt = Date.now();
|
|
54
56
|
document.querySelectorAll('.nav-toggle-icon').forEach(function(e) { e.style.opacity = '0'; e.style.transform = 'scale(0) rotate(90deg)'; });
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<%# Smooth-load convention (opt-in via Studio.smooth_load) — the next page
|
|
2
|
+
materializes behind the current one, then presents itself. Turbo 8 sees the
|
|
3
|
+
view-transition meta and wraps its body swap in document.startViewTransition;
|
|
4
|
+
browsers without support get the instant swap. no-preview stops application
|
|
5
|
+
visits from painting a stale cache snapshot before the fresh response — the
|
|
6
|
+
current page holds until the new one is ready, so navigation renders exactly
|
|
7
|
+
once. Restoration visits (back/forward) still restore instantly from cache.
|
|
8
|
+
Transition styling ships in engine.css ("Smooth-load convention" block);
|
|
9
|
+
pin an app's sticky header with the .vt-pinned-header utility (exactly one
|
|
10
|
+
element per page — a duplicate view-transition-name silently disables the
|
|
11
|
+
whole transition). %>
|
|
12
|
+
<% if Studio.smooth_load %>
|
|
13
|
+
<meta name="view-transition" content="same-origin">
|
|
14
|
+
<meta name="turbo-cache-control" content="no-preview">
|
|
15
|
+
<% end %>
|
|
@@ -38,9 +38,20 @@
|
|
|
38
38
|
savedEvent DOM event dispatched on success, carrying the app payload, so
|
|
39
39
|
the app can run its own follow-on. Default "studio:activity-saved".
|
|
40
40
|
store Alpine modal store name backing close(). Default "modals".
|
|
41
|
-
leveling
|
|
42
|
-
|
|
41
|
+
leveling the render-time DEFAULT for the seeds celebration. The live
|
|
42
|
+
value is read at RUNTIME from the modal store's current
|
|
43
|
+
props.leveling (this.leveling getter), so one modal id flips
|
|
44
|
+
TM (celebration) <-> MS (plain save) as a toggle moves; when
|
|
45
|
+
props omits it (TM's live path) the default stands.
|
|
46
|
+
hasConsent true renders a consent checkbox that gates the action until
|
|
47
|
+
ticked (the newsletter join).
|
|
48
|
+
demo style-guide preview: resolve locally, skip the network POST, and
|
|
49
|
+
suppress the app-facing saved event (so a host app's follow-on
|
|
50
|
+
handler can't stack a second modal over the demo's own state).
|
|
43
51
|
seedsPerLevel seeds per level for the celebration bar. Default 100.
|
|
52
|
+
demoSeedsEarned / demoSeedsTotal demo-only seed payload (defaults to a
|
|
53
|
+
level-crossing 30 / seedsPerLevel) so each activity's
|
|
54
|
+
celebration can showcase a chosen state.
|
|
44
55
|
%>
|
|
45
56
|
<script>
|
|
46
57
|
(function () {
|
|
@@ -57,9 +68,17 @@
|
|
|
57
68
|
finalizeHook: opts.finalizeHook || "",
|
|
58
69
|
savedEvent: opts.savedEvent || "studio:activity-saved",
|
|
59
70
|
store: opts.store || "modals",
|
|
60
|
-
leveling
|
|
71
|
+
// leveling is a RUNTIME getter (below) reading props.leveling off the modal
|
|
72
|
+
// store, so ONE modal id flips TM <-> MS shape live as a section toggle
|
|
73
|
+
// moves. _levelingDefault is the render-time fallback (TM ships no
|
|
74
|
+
// props.leveling and relies on its :leveling capability default).
|
|
75
|
+
_levelingDefault: !!opts.leveling,
|
|
76
|
+
hasConsent: !!opts.hasConsent,
|
|
77
|
+
consent: false,
|
|
61
78
|
demo: !!opts.demo,
|
|
62
79
|
seedsPerLevel: parseInt(opts.seedsPerLevel, 10) || 100,
|
|
80
|
+
demoSeedsEarned: parseInt(opts.demoSeedsEarned, 10),
|
|
81
|
+
demoSeedsTotal: parseInt(opts.demoSeedsTotal, 10),
|
|
63
82
|
saving: false,
|
|
64
83
|
error: "",
|
|
65
84
|
saved: false,
|
|
@@ -68,9 +87,42 @@
|
|
|
68
87
|
seedsEarned: 0,
|
|
69
88
|
seedsTotal: 0,
|
|
70
89
|
|
|
90
|
+
// Open directly AT the updated/celebrate state — the specimen's "updated"
|
|
91
|
+
// cards (Great Username / Subscribed!) open the same modal id pre-advanced,
|
|
92
|
+
// exactly like the Auth cards open a given step. props.celebrate mounts the
|
|
93
|
+
// modal already saved, with the seed payload the celebration animates from.
|
|
94
|
+
// leveling (a getter) then decides at runtime whether that state renders the
|
|
95
|
+
// seeds celebration (TM) or the plain confirmation (MS).
|
|
96
|
+
init: function () {
|
|
97
|
+
var s = (window.Alpine && Alpine.store) ? Alpine.store(this.store) : null;
|
|
98
|
+
var cur = (s && typeof s.current === "function") ? s.current() : null;
|
|
99
|
+
var p = (cur && cur.props) || {};
|
|
100
|
+
if (p.celebrate) {
|
|
101
|
+
this.seedsEarned = parseInt(p.seedsEarned, 10) || 0;
|
|
102
|
+
this.seedsTotal = parseInt(p.seedsTotal, 10) || 0;
|
|
103
|
+
this.saved = true;
|
|
104
|
+
this.celebrate = true;
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
// leveling — resolved at RUNTIME from the modal store's current props, so a
|
|
109
|
+
// single modal id flips TM (seeds/level/quest) <-> MS (plain save) live as
|
|
110
|
+
// the section's leveling toggle moves. Falls back to the render-time default
|
|
111
|
+
// when props does not set it (TM's live path), so existing callers are
|
|
112
|
+
// unaffected.
|
|
113
|
+
get leveling() {
|
|
114
|
+
var s = (window.Alpine && Alpine.store) ? Alpine.store(this.store) : null;
|
|
115
|
+
var cur = (s && typeof s.current === "function") ? s.current() : null;
|
|
116
|
+
var p = cur && cur.props;
|
|
117
|
+
if (p && typeof p.leveling === "boolean") return p.leveling;
|
|
118
|
+
return this._levelingDefault;
|
|
119
|
+
},
|
|
120
|
+
|
|
71
121
|
// Actionable while there is a real change to save (input activities) or
|
|
72
|
-
// until the one-shot action has fired (no-field activities).
|
|
122
|
+
// until the one-shot action has fired (no-field activities). A consent
|
|
123
|
+
// checkbox (the newsletter join) additionally gates the action until ticked.
|
|
73
124
|
get changed() {
|
|
125
|
+
if (this.hasConsent && !this.consent) return false;
|
|
74
126
|
if (!this.hasInput) return !this.saved;
|
|
75
127
|
var v = (this.value || "").trim();
|
|
76
128
|
if (v.length < this.minLength) return false;
|
|
@@ -78,6 +130,13 @@
|
|
|
78
130
|
},
|
|
79
131
|
|
|
80
132
|
_dispatch: function (payload) {
|
|
133
|
+
// In demo mode (the /admin/style preview) do NOT fire the app-facing
|
|
134
|
+
// saved event: the host app may listen for it and open its OWN follow-on
|
|
135
|
+
// modal (e.g. Turf Monster swaps to 'quest-success' on
|
|
136
|
+
// 'studio:username-saved'), which would stack a SECOND modal over the
|
|
137
|
+
// demo's own celebrate view — the reported duplicate. The demo resolves
|
|
138
|
+
// locally and is self-contained, so it must not drive real app follow-ons.
|
|
139
|
+
if (this.demo) return;
|
|
81
140
|
try {
|
|
82
141
|
window.dispatchEvent(new CustomEvent(this.savedEvent, { detail: payload || {} }));
|
|
83
142
|
} catch (e) {}
|
|
@@ -90,11 +149,26 @@
|
|
|
90
149
|
this.progressLabel = "";
|
|
91
150
|
if (this.hasInput) this.original = (this.value || "").trim();
|
|
92
151
|
this._dispatch(payload);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
152
|
+
// Always advance to the updated state; the view gate (celebrate &&
|
|
153
|
+
// leveling vs celebrate && !leveling) picks the seeds celebration (TM)
|
|
154
|
+
// or the plain confirmation (MS). Seeds are set either way — only the
|
|
155
|
+
// leveling view reads them.
|
|
156
|
+
this.seedsEarned = parseInt(payload.seeds_earned, 10) || 0;
|
|
157
|
+
this.seedsTotal = parseInt(payload.seeds_total, 10) || 0;
|
|
158
|
+
this.celebrate = true;
|
|
159
|
+
// Reflect the advance onto the store's LIVE current props so the specimen
|
|
160
|
+
// glow (which reads props.celebrate) transfers from the input card to the
|
|
161
|
+
// updated card — exactly like the Auth glow follows props.step as its
|
|
162
|
+
// machine advances. Without this, the glow stays on the input card.
|
|
163
|
+
this._syncCelebrate(true);
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
// Mirror the component's celebrate onto the current modal store entry's
|
|
167
|
+
// props, the reactive source the specimen glow_when reads.
|
|
168
|
+
_syncCelebrate: function (value) {
|
|
169
|
+
var s = (window.Alpine && Alpine.store) ? Alpine.store(this.store) : null;
|
|
170
|
+
var cur = (s && typeof s.current === "function") ? s.current() : null;
|
|
171
|
+
if (cur && cur.props) cur.props.celebrate = !!value;
|
|
98
172
|
},
|
|
99
173
|
|
|
100
174
|
_post: function (url, body) {
|
|
@@ -115,10 +189,14 @@
|
|
|
115
189
|
this.error = "";
|
|
116
190
|
var self = this;
|
|
117
191
|
if (this.demo) {
|
|
118
|
-
// Style-guide preview: no backend. Resolve to a
|
|
119
|
-
//
|
|
192
|
+
// Style-guide preview: no backend. Resolve to a per-activity seed
|
|
193
|
+
// payload (defaulting to a level-crossing one) so the celebration
|
|
194
|
+
// demonstrates its flourish — Username stays at Level 1 (+25), the
|
|
195
|
+
// Newsletter crosses a level to reveal the Free Entry Token line.
|
|
196
|
+
var de = isNaN(self.demoSeedsEarned) ? 30 : self.demoSeedsEarned;
|
|
197
|
+
var dt = isNaN(self.demoSeedsTotal) ? self.seedsPerLevel : self.demoSeedsTotal;
|
|
120
198
|
setTimeout(function () {
|
|
121
|
-
self._finishSaved({ seeds_earned:
|
|
199
|
+
self._finishSaved({ seeds_earned: de, seeds_total: dt });
|
|
122
200
|
}, 600);
|
|
123
201
|
return;
|
|
124
202
|
}
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
saved_event: "studio:username-saved",
|
|
41
41
|
icon_emoji: "🌱",
|
|
42
42
|
celebrate_title: "Great Username",
|
|
43
|
-
celebrate_subtitle: "Nice — you earned seeds
|
|
43
|
+
celebrate_subtitle: "Nice — you earned 25 seeds by completing a quest. Complete more quests to earn a Free Entry Token."
|
|
44
44
|
)
|
|
45
45
|
%>
|
|
46
46
|
<%= render "studio/modals/blocks/leveling_activity", username_locals %>
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
<%#
|
|
2
|
-
Leveling-activity modal — the engine primitive behind TM's
|
|
2
|
+
Leveling-activity modal — the engine primitive behind TM's profile-leveling
|
|
3
3
|
activities (change username, join newsletter, and the rest). ONE primitive,
|
|
4
|
-
TWO modes
|
|
4
|
+
three views (form / seeds celebration / plain confirm), TWO modes decided by the
|
|
5
|
+
app's :leveling capability:
|
|
5
6
|
|
|
6
|
-
leveling ON — the
|
|
7
|
-
the seeds
|
|
8
|
-
leveling OFF — the plain action
|
|
9
|
-
|
|
10
|
-
McRitchie-Studio shape: MS has no leveling, TM does.)
|
|
7
|
+
leveling ON — the action, then on success the seeds celebration: a "Level N"
|
|
8
|
+
pill + the seeds progress bar (level-up + Free Entry Token).
|
|
9
|
+
leveling OFF — the plain action, then a simple confirmation on success (a
|
|
10
|
+
"Saved" / "Subscribed" card). No Level pill, no seeds. (This is
|
|
11
|
+
the McRitchie-Studio shape: MS has no leveling, TM does.)
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
There is NO "Quest N of N" counter — the profile modals do not carry one.
|
|
14
|
+
|
|
15
|
+
Same primitive; the flag decides — and it decides at RUNTIME. The leveling chrome
|
|
16
|
+
(Level pill, seeds celebration) is ALWAYS rendered and gated by the reactive
|
|
17
|
+
leveling getter (which reads props.leveling off the modal store, falling back to
|
|
18
|
+
this local), so ONE modal id flips TM <-> MS live as a toggle moves — no more
|
|
19
|
+
`-plain` duplicate ids. The leveling chrome is engine-owned UI — it is NOT
|
|
20
|
+
on-chain.
|
|
14
21
|
|
|
15
22
|
CRITICAL BOUNDARY — UI ONLY. There is NO wallet, signing, key, or on-chain write
|
|
16
23
|
anywhere in this primitive. The actual save is an APP-SUPPLIED callback: the
|
|
@@ -36,17 +43,27 @@
|
|
|
36
43
|
finalize_hook (String) — window async fn name for the step (omit if none).
|
|
37
44
|
saved_event (String) — DOM event dispatched on success. Default
|
|
38
45
|
"studio:activity-saved".
|
|
39
|
-
quest_label (String) — the "Quest N of N" pill text (leveling only).
|
|
40
46
|
icon_emoji (String) — celebration glyph. Default "🌱".
|
|
41
47
|
celebrate_title (String) — celebration headline. Default "Activity complete".
|
|
42
48
|
celebrate_subtitle (String) — celebration subtext.
|
|
43
49
|
free_entry_label (String) — level-up reward line. Default "Free Entry Earned 🎟️".
|
|
44
50
|
seeds_per_level (Integer) — seeds per level. Default 100.
|
|
45
|
-
leveling (Boolean) —
|
|
46
|
-
Studio.feature?(:leveling)
|
|
47
|
-
|
|
51
|
+
leveling (Boolean) — the RENDER-TIME DEFAULT for the :leveling gate
|
|
52
|
+
(defaults to Studio.feature?(:leveling)). The LIVE value is read
|
|
53
|
+
at runtime from the modal store's props.leveling, so a single
|
|
54
|
+
modal id flips TM <-> MS as a toggle moves.
|
|
55
|
+
consent_label (String) — render a consent checkbox that gates the action until
|
|
56
|
+
ticked (the newsletter join). Omit for no checkbox.
|
|
57
|
+
confirm_subtitle (String) — subtext on the MS-shape (leveling off) plain
|
|
58
|
+
confirmation. Omit = none.
|
|
59
|
+
next_label / next_open — an optional "Next Quest" button on the celebration;
|
|
60
|
+
next_open is a developer-authored Alpine expression (e.g. swap to
|
|
61
|
+
the next activity). Both required to render the button.
|
|
62
|
+
demo_seeds_earned / demo_seeds_total — style-guide-only seed payload for the
|
|
63
|
+
demo save (defaults to a level-crossing 30 / seeds_per_level).
|
|
48
64
|
modal_store (String) — Alpine store name backing close(). Default "modals".
|
|
49
|
-
demo (Boolean) — style-guide preview: resolve locally, no POST
|
|
65
|
+
demo (Boolean) — style-guide preview: resolve locally, no POST, and
|
|
66
|
+
no app-facing saved event (no host follow-on can stack a modal).
|
|
50
67
|
|
|
51
68
|
Single root element (the modal host mounts this through its own template x-if).
|
|
52
69
|
%>
|
|
@@ -68,7 +85,6 @@
|
|
|
68
85
|
finalize_url = local_assigns.fetch(:finalize_url, "")
|
|
69
86
|
finalize_hook = local_assigns.fetch(:finalize_hook, "")
|
|
70
87
|
saved_event = local_assigns.fetch(:saved_event, "studio:activity-saved")
|
|
71
|
-
quest_label = local_assigns.fetch(:quest_label, nil)
|
|
72
88
|
icon_emoji = local_assigns.fetch(:icon_emoji, "🌱")
|
|
73
89
|
celebrate_title = local_assigns.fetch(:celebrate_title, "Activity complete")
|
|
74
90
|
celebrate_subtitle = local_assigns.fetch(:celebrate_subtitle, "Nice — you earned seeds. Keep completing quests to earn a Free Entry.")
|
|
@@ -77,6 +93,17 @@
|
|
|
77
93
|
leveling = local_assigns.fetch(:leveling, Studio.feature?(:leveling))
|
|
78
94
|
modal_store = local_assigns.fetch(:modal_store, "modals")
|
|
79
95
|
demo = local_assigns.fetch(:demo, false)
|
|
96
|
+
# consent_label — render a consent checkbox that gates the action (newsletter
|
|
97
|
+
# join). next_label / next_open — an optional "Next Quest" button in the
|
|
98
|
+
# celebration (next_open is a developer-authored Alpine expression). demo_seeds_*
|
|
99
|
+
# — style-guide-only tuning.
|
|
100
|
+
consent_label = local_assigns.fetch(:consent_label, nil)
|
|
101
|
+
next_label = local_assigns.fetch(:next_label, nil)
|
|
102
|
+
next_open = local_assigns[:next_open].presence&.to_s&.html_safe
|
|
103
|
+
# confirm_subtitle — the MS-shape (leveling off) plain-confirmation subtext.
|
|
104
|
+
confirm_subtitle = local_assigns.fetch(:confirm_subtitle, nil)
|
|
105
|
+
demo_seeds_earned = local_assigns.fetch(:demo_seeds_earned, nil)
|
|
106
|
+
demo_seeds_total = local_assigns.fetch(:demo_seeds_total, nil)
|
|
80
107
|
%>
|
|
81
108
|
<div x-data="levelingActionModal({
|
|
82
109
|
hasInput: <%= input ? 'true' : 'false' %>,
|
|
@@ -88,20 +115,17 @@
|
|
|
88
115
|
savedEvent: '<%= j saved_event %>',
|
|
89
116
|
store: '<%= j modal_store %>',
|
|
90
117
|
leveling: <%= leveling ? 'true' : 'false' %>,
|
|
118
|
+
hasConsent: <%= consent_label.present? ? 'true' : 'false' %>,
|
|
91
119
|
demo: <%= demo ? 'true' : 'false' %>,
|
|
92
|
-
seedsPerLevel: <%= seeds_per_level.to_i
|
|
120
|
+
seedsPerLevel: <%= seeds_per_level.to_i %>,
|
|
121
|
+
demoSeedsEarned: <%= demo_seeds_earned.present? ? demo_seeds_earned.to_i : 'null' %>,
|
|
122
|
+
demoSeedsTotal: <%= demo_seeds_total.present? ? demo_seeds_total.to_i : 'null' %>
|
|
93
123
|
})">
|
|
94
124
|
|
|
95
125
|
<%# FORM VIEW — the action. Single root inside the template. %>
|
|
96
126
|
<template x-if="!celebrate">
|
|
97
127
|
<div>
|
|
98
128
|
<%= render layout: "studio/modals/blocks/shell", locals: { title: title, modal_store: modal_store } do %>
|
|
99
|
-
<% if leveling && quest_label.present? %>
|
|
100
|
-
<div class="mb-3">
|
|
101
|
-
<span class="inline-block text-[10px] font-semibold uppercase tracking-wide text-primary bg-primary/10 px-2 py-0.5 rounded-full whitespace-nowrap"><%= quest_label %></span>
|
|
102
|
-
</div>
|
|
103
|
-
<% end %>
|
|
104
|
-
|
|
105
129
|
<% if description.present? %>
|
|
106
130
|
<p class="text-sm text-muted mb-4"><%= description %></p>
|
|
107
131
|
<% end %>
|
|
@@ -120,6 +144,12 @@
|
|
|
120
144
|
<%= "title=\"#{pattern_title}\"".html_safe if pattern_title.present? %>
|
|
121
145
|
autofocus>
|
|
122
146
|
<% end %>
|
|
147
|
+
<% if consent_label.present? %>
|
|
148
|
+
<label class="flex items-center gap-2 text-xs text-secondary select-none cursor-pointer">
|
|
149
|
+
<input type="checkbox" x-model="consent" class="rounded border-subtle text-primary cursor-pointer">
|
|
150
|
+
<span><%= consent_label %></span>
|
|
151
|
+
</label>
|
|
152
|
+
<% end %>
|
|
123
153
|
<button type="submit" class="btn btn-primary btn-lg w-full" :disabled="saving || !changed">
|
|
124
154
|
<span x-show="!saving" x-text="changed ? '<%= j cta_label %>' : '<%= j saved_label %>'"><%= cta_label %></span>
|
|
125
155
|
<span x-show="saving" class="inline-flex items-center justify-center gap-2" style="display: none;">
|
|
@@ -132,28 +162,48 @@
|
|
|
132
162
|
</div>
|
|
133
163
|
</template>
|
|
134
164
|
|
|
135
|
-
<%# CELEBRATION VIEW — leveling
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
165
|
+
<%# CELEBRATION VIEW — leveling-only chrome. ALWAYS rendered now (so a runtime
|
|
166
|
+
leveling toggle can reveal it); the x-if gates on BOTH celebrate AND the
|
|
167
|
+
reactive leveling getter, so an MS-shape modal (leveling off) shows the plain
|
|
168
|
+
saved state and never this. Mounts once celebrate flips true so the seeds bar
|
|
169
|
+
animates from the freshly-saved payload. Single root. %>
|
|
170
|
+
<template x-if="celebrate && leveling">
|
|
171
|
+
<div>
|
|
172
|
+
<%= render "studio/modals/blocks/card_header",
|
|
173
|
+
size: :lg, icon_emoji: icon_emoji,
|
|
174
|
+
title: celebrate_title, subtitle: celebrate_subtitle %>
|
|
145
175
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
176
|
+
<div class="mb-5">
|
|
177
|
+
<%= render "studio/modals/blocks/seeds_bar",
|
|
178
|
+
seeds_earned_key: "seedsEarned", seeds_total_key: "seedsTotal",
|
|
179
|
+
seeds_per_level: seeds_per_level, free_entry_label: free_entry_label %>
|
|
180
|
+
</div>
|
|
151
181
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
182
|
+
<% if next_label.present? && next_open.present? %>
|
|
183
|
+
<button type="button" @click="<%= next_open %>" class="btn btn-primary btn-lg w-full">
|
|
184
|
+
<%= next_label %>
|
|
155
185
|
</button>
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
186
|
+
<% end %>
|
|
187
|
+
<button type="button" @click="$store.<%= modal_store %>.close()"
|
|
188
|
+
class="block mx-auto mt-3 text-sm text-secondary hover:text-heading transition">
|
|
189
|
+
Close
|
|
190
|
+
</button>
|
|
191
|
+
</div>
|
|
192
|
+
</template>
|
|
193
|
+
|
|
194
|
+
<%# PLAIN CONFIRM VIEW — the MS shape (leveling off) after an update: a simple
|
|
195
|
+
confirmation, NO seeds / Level / celebration chrome. Gated celebrate &&
|
|
196
|
+
!leveling, so the single leveling toggle flips this <-> the seeds celebration
|
|
197
|
+
above, live. Single root. %>
|
|
198
|
+
<template x-if="celebrate && !leveling">
|
|
199
|
+
<div>
|
|
200
|
+
<%= render "studio/modals/blocks/card_header",
|
|
201
|
+
size: :lg, icon_color: "success",
|
|
202
|
+
title: saved_label, subtitle: confirm_subtitle %>
|
|
203
|
+
<button type="button" @click="$store.<%= modal_store %>.close()"
|
|
204
|
+
class="btn btn-primary btn-lg w-full">
|
|
205
|
+
Done
|
|
206
|
+
</button>
|
|
207
|
+
</div>
|
|
208
|
+
</template>
|
|
159
209
|
</div>
|
|
@@ -55,7 +55,12 @@
|
|
|
55
55
|
height via the grid's default align-items: stretch). %>
|
|
56
56
|
<div class="relative h-full<%= " studio-team-glow rounded-xl" if glow_when %>"
|
|
57
57
|
x-data="<%= card_x_data %>"
|
|
58
|
-
|
|
58
|
+
<%# Fail-closed default: a STATIC inline 0 out-specifies the primitive's CSS
|
|
59
|
+
default (--studio-team-glow-opacity of 1) so the ring is OFF at first
|
|
60
|
+
paint, BEFORE Alpine hydrates the reactive :style below. Without it every
|
|
61
|
+
glow card shows its ring until Alpine boots (the fail-open flash). The
|
|
62
|
+
:style then drives 0 to 0.95 with the 0.4s cross-fade. Keep BOTH. %>
|
|
63
|
+
<% if glow_when %>style="--studio-team-glow-opacity: 0" :style="{ '--studio-team-glow-opacity': (<%= glow_when %>) ? '0.95' : '0' }"<% end %>>
|
|
59
64
|
<article
|
|
60
65
|
class="card overflow-hidden flex flex-col h-full<%= " opacity-95" if disabled %><%= " cursor-pointer" if clickable %>"
|
|
61
66
|
<%= 'aria-disabled="true"'.html_safe if disabled %>
|
|
@@ -47,11 +47,14 @@
|
|
|
47
47
|
# variants (:picker vs :crop). The crop sub-state rides props.cropReady — the
|
|
48
48
|
# crop factory sets it true when the cropper mounts (on a direct image OR after
|
|
49
49
|
# an in-modal pick), so the glow moves picker -> cropper as the flow advances.
|
|
50
|
-
ds_glow = lambda do |id, step: nil, crop: nil|
|
|
50
|
+
ds_glow = lambda do |id, step: nil, crop: nil, celebrate: nil|
|
|
51
51
|
cur = "$store.dsModals.current()"
|
|
52
52
|
expr = "#{cur} && #{cur}.id === '#{id}'"
|
|
53
53
|
expr += " && (#{cur}.props.step || 'credentials') === '#{step}'" if step
|
|
54
54
|
expr += (crop == :crop ? " && !!#{cur}.props.cropReady" : " && !#{cur}.props.cropReady") unless crop.nil?
|
|
55
|
+
# celebrate discriminates the input specimen (form) from the "updated" specimen
|
|
56
|
+
# (celebrate) of the SAME leveling modal id, mirroring the step/crop sub-states.
|
|
57
|
+
expr += (celebrate ? " && !!#{cur}.props.celebrate" : " && !#{cur}.props.celebrate") unless celebrate.nil?
|
|
55
58
|
expr
|
|
56
59
|
end
|
|
57
60
|
%>
|
|
@@ -256,7 +259,7 @@
|
|
|
256
259
|
<%= render "studio/age_verify_assets" %>
|
|
257
260
|
|
|
258
261
|
<%# levelingActionModal factory — page level (never inside the overlay template,
|
|
259
|
-
where a cloned script would not run) so the change-username +
|
|
262
|
+
where a cloned script would not run) so the change-username + join-newsletter
|
|
260
263
|
specimens open live. A consumer app renders this in its layout the same way. %>
|
|
261
264
|
<%= render "studio/leveling_activity_assets" %>
|
|
262
265
|
|
|
@@ -381,47 +384,46 @@
|
|
|
381
384
|
subtitle: "You leveled up! A Free Entry lands in your account soon. Keep leveling for more." %></div>
|
|
382
385
|
</template>
|
|
383
386
|
|
|
384
|
-
<%# --- Leveling
|
|
385
|
-
<%#
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
387
|
+
<%# --- Profile Leveling: change username + join newsletter --- %>
|
|
388
|
+
<%# ONE id per activity — opened at the form OR (with props.celebrate) at the
|
|
389
|
+
"updated" state, exactly like the Auth cards open a given step. The
|
|
390
|
+
primitive reads props.leveling at RUNTIME, so each card's own Leveling
|
|
391
|
+
toggle flips TM (the seeds celebration) <-> MS (plain confirmation) at
|
|
392
|
+
open — no more `-plain` twins, and no "Quest N of N" counter. All
|
|
393
|
+
demo:true (the save resolves locally); the on-chain-accurate copy is an
|
|
394
|
+
app-authored description (the engine default stays neutral). The two
|
|
395
|
+
activities walk in sequence via the celebration's "Next Quest"
|
|
396
|
+
(Username -> Newsletter -> close). %>
|
|
390
397
|
<template x-if="$store.dsModals.current().id === 'change-username'">
|
|
391
398
|
<div><%= render "studio/modals/blocks/change_username",
|
|
392
399
|
modal_store: "dsModals", demo: true, leveling: true,
|
|
393
400
|
current_username: "turfmonster",
|
|
394
401
|
submit_url: "#demo-change-username",
|
|
395
402
|
description: "Your username is stored on-chain. Changes are saved via your managed wallet.",
|
|
396
|
-
|
|
403
|
+
cta_label: "Save", saved_label: "Saved",
|
|
404
|
+
demo_seeds_earned: 25, demo_seeds_total: 25,
|
|
405
|
+
free_entry_label: "Free Entry Token 🎟️",
|
|
406
|
+
confirm_subtitle: "Your username has been updated.",
|
|
407
|
+
next_label: "Next Quest",
|
|
408
|
+
next_open: "$store.dsModals.swap('join-newsletter', { demo: true, leveling: true })" %></div>
|
|
397
409
|
</template>
|
|
398
|
-
<template x-if="$store.dsModals.current().id === '
|
|
399
|
-
<div><%= render "studio/modals/blocks/change_username",
|
|
400
|
-
modal_store: "dsModals", demo: true, leveling: false,
|
|
401
|
-
current_username: "mcritchie",
|
|
402
|
-
submit_url: "#demo-change-username",
|
|
403
|
-
description: "Choose the name shown across your account." %></div>
|
|
404
|
-
</template>
|
|
405
|
-
<template x-if="$store.dsModals.current().id === 'quest-activity'">
|
|
410
|
+
<template x-if="$store.dsModals.current().id === 'join-newsletter'">
|
|
406
411
|
<div><%= render "studio/modals/blocks/leveling_activity",
|
|
407
412
|
modal_store: "dsModals", demo: true, leveling: true,
|
|
408
|
-
submit_url: "#demo-
|
|
413
|
+
submit_url: "#demo-newsletter",
|
|
409
414
|
title: "Join the Newsletter",
|
|
410
|
-
description: "Sports news and contest updates —
|
|
411
|
-
input:
|
|
415
|
+
description: "Sports news and contest updates — and 25 seeds for joining.",
|
|
416
|
+
input: false,
|
|
417
|
+
consent_label: "Email me sports news and contest updates.",
|
|
412
418
|
cta_label: "Subscribe", saved_label: "Subscribed", saving_label: "Subscribing…",
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
celebrate_subtitle: "
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
title: "Join the Newsletter",
|
|
422
|
-
description: "Sports news and contest updates.",
|
|
423
|
-
input: true, input_type: "email", placeholder: "you@example.com",
|
|
424
|
-
cta_label: "Subscribe", saved_label: "Subscribed", saving_label: "Subscribing…" %></div>
|
|
419
|
+
icon_emoji: "🌱",
|
|
420
|
+
celebrate_title: "Subscribed!",
|
|
421
|
+
celebrate_subtitle: "You're on the list. We'll keep the sports news and contest updates coming.",
|
|
422
|
+
demo_seeds_earned: 25, demo_seeds_total: 100,
|
|
423
|
+
free_entry_label: "Free Entry Token 🎟️",
|
|
424
|
+
confirm_subtitle: "You're on the list.",
|
|
425
|
+
next_label: "Next Quest",
|
|
426
|
+
next_open: "$store.dsModals.close()" %></div>
|
|
425
427
|
</template>
|
|
426
428
|
|
|
427
429
|
</div>
|
|
@@ -630,7 +632,126 @@
|
|
|
630
632
|
</section>
|
|
631
633
|
|
|
632
634
|
<%# ===================================================================== %>
|
|
633
|
-
<%# 3.
|
|
635
|
+
<%# 3. PROFILE LEVELING — change username + join newsletter (per-card toggles) %>
|
|
636
|
+
<%# ===================================================================== %>
|
|
637
|
+
<%# Each specimen card carries its OWN Leveling toggle (like the Auth "Sign in"
|
|
638
|
+
card's method checkboxes) — the card's opts.leveling flows into its open() as
|
|
639
|
+
props.leveling, so the card opens at the TM shape (checked, the default) or the
|
|
640
|
+
MS shape (unchecked). Independent per card: preview Change Username leveling-on
|
|
641
|
+
beside Great Username leveling-off. ONE primitive, ONE id per activity, no
|
|
642
|
+
"Quest N of N" counter — the with/without fork is gone. The two "updated"
|
|
643
|
+
cards open the same modal id straight at the celebrate state (props.celebrate),
|
|
644
|
+
exactly like the Auth cards open a given step. %>
|
|
645
|
+
<section class="space-y-5">
|
|
646
|
+
<div class="space-y-1">
|
|
647
|
+
<div class="flex flex-wrap items-center gap-3">
|
|
648
|
+
<h3 class="text-xl font-bold text-heading">Profile Leveling</h3>
|
|
649
|
+
<% if leveling_on %>
|
|
650
|
+
<span class="badge" style="color: var(--color-success); border-color: var(--color-success)">leveling on</span>
|
|
651
|
+
<% else %>
|
|
652
|
+
<span class="badge" style="color: var(--color-warning); border-color: var(--color-warning)">leveling off</span>
|
|
653
|
+
<% end %>
|
|
654
|
+
</div>
|
|
655
|
+
<p class="text-muted text-sm">
|
|
656
|
+
The profile leveling flow, ported from Turf Monster as ONE primitive
|
|
657
|
+
(<code class="font-mono text-2xs">studio/modals/blocks/_leveling_activity</code>,
|
|
658
|
+
with <code class="font-mono text-2xs">_change_username</code> as its username
|
|
659
|
+
specialization). Walk all four states — <strong>Change Username → Great
|
|
660
|
+
Username</strong>, then <strong>Join the Newsletter → Subscribed!</strong>
|
|
661
|
+
— each "updated" card opens its modal straight at the success state, just like
|
|
662
|
+
the Auth cards open a given step. Each card carries its <strong>own Leveling
|
|
663
|
+
toggle</strong> (like the Auth card's method checkboxes): <strong>checked</strong>
|
|
664
|
+
is the Turf Monster shape (the seeds bar + Free Entry Token progress),
|
|
665
|
+
<strong>unchecked</strong> is the McRitchie Studio shape (plain input + Save,
|
|
666
|
+
then a simple confirmation). Flip a card's toggle, then open it — so you can
|
|
667
|
+
preview Change Username leveling-on beside Great Username leveling-off. Same
|
|
668
|
+
modal id either way — the primitive reads
|
|
669
|
+
<code class="font-mono text-2xs">leveling</code> at runtime, so there is no
|
|
670
|
+
with/without fork and <strong>no "Quest N of N" counter</strong>. The save is an
|
|
671
|
+
<strong>app-supplied callback</strong>
|
|
672
|
+
(<code class="font-mono text-2xs">submit_url</code> + an opaque
|
|
673
|
+
<code class="font-mono text-2xs">finalize_hook</code> for a wallet step) —
|
|
674
|
+
<strong>no wallet, signing, or on-chain write lives in the engine</strong>.
|
|
675
|
+
(These demos resolve locally.)
|
|
676
|
+
</p>
|
|
677
|
+
</div>
|
|
678
|
+
<%# The walk, left to right: Change Username -> Great Username -> Join Newsletter
|
|
679
|
+
-> Subscribed. The two "updated" cards open the same id at props.celebrate. %>
|
|
680
|
+
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
681
|
+
<%# 1. Change Username — the input (form) state. %>
|
|
682
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
683
|
+
label: "Change Username",
|
|
684
|
+
reference: %(the Profile Leveling "Change Username" activity (studio-engine studio/modals/blocks/_change_username) — the input state. Reads leveling at runtime; the on-chain save is an app-supplied callback (submit_url + an opaque finalize_hook), the engine is UI only. Open with $store.dsModals.open('change-username', { demo: true, leveling: true })),
|
|
685
|
+
card_data: "opts: { leveling: true }",
|
|
686
|
+
toggles: [{ model: "opts.leveling", label: "Leveling" }],
|
|
687
|
+
open_expr: "$store.dsModals.open('change-username', { demo: true, leveling: opts.leveling })",
|
|
688
|
+
glow_when: ds_glow.call("change-username", celebrate: false) } do %>
|
|
689
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
690
|
+
<span class="block h-2 w-16 rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
691
|
+
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
692
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
693
|
+
</div>
|
|
694
|
+
<% end %>
|
|
695
|
+
|
|
696
|
+
<%# 2. Great Username — the updated (celebrate) state, opened directly. %>
|
|
697
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
698
|
+
label: "Great Username",
|
|
699
|
+
reference: %(the Profile Leveling "Great Username" updated state (studio/modals/blocks/_change_username opened at props.celebrate) — leveling ON shows the seeds celebration (plus 25, Free Entry Token) + Next Quest; leveling OFF shows the plain "Saved" confirmation. Toggle this card's Leveling checkbox to switch, then open. Open with $store.dsModals.open('change-username', { demo: true, leveling: true, celebrate: true, seedsEarned: 25, seedsTotal: 25 })),
|
|
700
|
+
card_data: "opts: { leveling: true }",
|
|
701
|
+
toggles: [{ model: "opts.leveling", label: "Leveling" }],
|
|
702
|
+
open_expr: "$store.dsModals.open('change-username', { demo: true, leveling: opts.leveling, celebrate: true, seedsEarned: 25, seedsTotal: 25 })",
|
|
703
|
+
glow_when: ds_glow.call("change-username", celebrate: true) } do %>
|
|
704
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
705
|
+
<div class="text-2xl leading-none">🌱</div>
|
|
706
|
+
<div x-show="opts.leveling" class="flex gap-1">
|
|
707
|
+
<span class="flex-1 h-2 rounded" style="background: var(--color-cta)"></span>
|
|
708
|
+
<span class="flex-1 h-2 rounded" style="background: var(--color-cta); opacity:.5"></span>
|
|
709
|
+
<span class="flex-1 h-2 rounded" style="background: var(--color-text); opacity:.12"></span>
|
|
710
|
+
</div>
|
|
711
|
+
<span class="block h-1.5 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
712
|
+
</div>
|
|
713
|
+
<% end %>
|
|
714
|
+
|
|
715
|
+
<%# 3. Join the Newsletter — the input state (consent checkbox + Subscribe). %>
|
|
716
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
717
|
+
label: "Join Newsletter",
|
|
718
|
+
reference: %(the Profile Leveling "Join the Newsletter" activity (studio-engine studio/modals/blocks/_leveling_activity) — the input state: a consent checkbox + Subscribe. Reads leveling at runtime. Open with $store.dsModals.open('join-newsletter', { demo: true, leveling: true })),
|
|
719
|
+
card_data: "opts: { leveling: true }",
|
|
720
|
+
toggles: [{ model: "opts.leveling", label: "Leveling" }],
|
|
721
|
+
open_expr: "$store.dsModals.open('join-newsletter', { demo: true, leveling: opts.leveling })",
|
|
722
|
+
glow_when: ds_glow.call("join-newsletter", celebrate: false) } do %>
|
|
723
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
724
|
+
<span class="text-lg leading-none">📬</span>
|
|
725
|
+
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
726
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
727
|
+
</div>
|
|
728
|
+
<% end %>
|
|
729
|
+
|
|
730
|
+
<%# 4. Subscribed! — the updated (celebrate) state, opened directly. %>
|
|
731
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
732
|
+
label: "Subscribed!",
|
|
733
|
+
reference: %(the Profile Leveling "Subscribed!" updated state (studio/modals/blocks/_leveling_activity opened at props.celebrate) — leveling ON shows the seeds celebration (Free Entry Token) + Next Quest; leveling OFF shows the plain "Subscribed" confirmation. Toggle this card's Leveling checkbox to switch, then open. Open with $store.dsModals.open('join-newsletter', { demo: true, leveling: true, celebrate: true, seedsEarned: 25, seedsTotal: 100 })),
|
|
734
|
+
card_data: "opts: { leveling: true }",
|
|
735
|
+
toggles: [{ model: "opts.leveling", label: "Leveling" }],
|
|
736
|
+
open_expr: "$store.dsModals.open('join-newsletter', { demo: true, leveling: opts.leveling, celebrate: true, seedsEarned: 25, seedsTotal: 100 })",
|
|
737
|
+
glow_when: ds_glow.call("join-newsletter", celebrate: true) } do %>
|
|
738
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
739
|
+
<div class="mx-auto w-7 h-7 rounded-full flex items-center justify-center" style="background: color-mix(in srgb, var(--color-success) 15%, transparent)">
|
|
740
|
+
<svg class="w-4 h-4" style="color: var(--color-success)" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
|
|
741
|
+
</div>
|
|
742
|
+
<div x-show="opts.leveling" class="flex gap-1">
|
|
743
|
+
<span class="flex-1 h-2 rounded" style="background: var(--color-cta)"></span>
|
|
744
|
+
<span class="flex-1 h-2 rounded" style="background: var(--color-cta); opacity:.6"></span>
|
|
745
|
+
<span class="flex-1 h-2 rounded" style="background: var(--color-cta); opacity:.3"></span>
|
|
746
|
+
</div>
|
|
747
|
+
<span class="block h-1.5 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
748
|
+
</div>
|
|
749
|
+
<% end %>
|
|
750
|
+
</div>
|
|
751
|
+
</section>
|
|
752
|
+
|
|
753
|
+
<%# ===================================================================== %>
|
|
754
|
+
<%# 4. WEB3 — gated by Studio.feature?(:web3); off = disabled-but-present-yet-openable %>
|
|
634
755
|
<%# ===================================================================== %>
|
|
635
756
|
<section class="space-y-5">
|
|
636
757
|
<div class="space-y-1">
|
|
@@ -735,7 +856,7 @@
|
|
|
735
856
|
</section>
|
|
736
857
|
|
|
737
858
|
<%# ===================================================================== %>
|
|
738
|
-
<%#
|
|
859
|
+
<%# 5. SYSTEM AND STATUS (reusable card blocks) %>
|
|
739
860
|
<%# ===================================================================== %>
|
|
740
861
|
<section class="space-y-5">
|
|
741
862
|
<div class="space-y-1">
|
|
@@ -797,7 +918,7 @@
|
|
|
797
918
|
</section>
|
|
798
919
|
|
|
799
920
|
<%# ===================================================================== %>
|
|
800
|
-
<%#
|
|
921
|
+
<%# 6. TEMPLATES (copy-from archetypes) %>
|
|
801
922
|
<%# ===================================================================== %>
|
|
802
923
|
<section class="space-y-5">
|
|
803
924
|
<div class="space-y-1">
|
|
@@ -831,7 +952,7 @@
|
|
|
831
952
|
</section>
|
|
832
953
|
|
|
833
954
|
<%# ===================================================================== %>
|
|
834
|
-
<%#
|
|
955
|
+
<%# 7. REWARDS / leveling (gated by Studio.feature?(:leveling)) %>
|
|
835
956
|
<%# ===================================================================== %>
|
|
836
957
|
<section class="space-y-5">
|
|
837
958
|
<div class="space-y-1">
|
|
@@ -908,96 +1029,4 @@
|
|
|
908
1029
|
</div>
|
|
909
1030
|
</section>
|
|
910
1031
|
|
|
911
|
-
<%# ===================================================================== %>
|
|
912
|
-
<%# 7. LEVELING ACTIVITIES — change username + quest activities %>
|
|
913
|
-
<%# ===================================================================== %>
|
|
914
|
-
<section class="space-y-5">
|
|
915
|
-
<div class="space-y-1">
|
|
916
|
-
<div class="flex flex-wrap items-center gap-3">
|
|
917
|
-
<h3 class="text-xl font-bold text-heading">Leveling activities</h3>
|
|
918
|
-
<% if leveling_on %>
|
|
919
|
-
<span class="badge" style="color: var(--color-success); border-color: var(--color-success)">leveling on</span>
|
|
920
|
-
<% else %>
|
|
921
|
-
<span class="badge" style="color: var(--color-warning); border-color: var(--color-warning)">leveling off</span>
|
|
922
|
-
<% end %>
|
|
923
|
-
</div>
|
|
924
|
-
<p class="text-muted text-sm">
|
|
925
|
-
The quest/leveling action modals
|
|
926
|
-
(<code class="font-mono text-2xs">studio/modals/blocks/_change_username</code>,
|
|
927
|
-
<code class="font-mono text-2xs">studio/modals/blocks/_leveling_activity</code>),
|
|
928
|
-
migrated from Turf Monster. ONE primitive, <strong>two modes</strong>, decided
|
|
929
|
-
by <code class="font-mono text-2xs">Studio.feature?(:leveling)</code>:
|
|
930
|
-
<strong>leveling on</strong> adds the quest pill + the seeds level-up
|
|
931
|
-
celebration; <strong>leveling off</strong> is the plain action modal (just
|
|
932
|
-
the input + Save + a Saved state — the McRitchie Studio shape). Each modal is
|
|
933
|
-
shown <strong>both ways</strong> below (the specimens pass an explicit
|
|
934
|
-
<code class="font-mono text-2xs">leveling:</code> so both render regardless of
|
|
935
|
-
this app's flag). The actual save is an <strong>app-supplied callback</strong>:
|
|
936
|
-
the modal POSTs to <code class="font-mono text-2xs">submit_url</code> and, for
|
|
937
|
-
an app that needs a wallet step, delegates it opaquely to an app-supplied
|
|
938
|
-
<code class="font-mono text-2xs">finalize_hook</code> — <strong>no wallet,
|
|
939
|
-
signing, or on-chain write lives in the engine</strong>. (These demos resolve
|
|
940
|
-
the save locally.)
|
|
941
|
-
</p>
|
|
942
|
-
</div>
|
|
943
|
-
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
944
|
-
<%# Change username — leveling ON: quest pill + seeds celebration on save. %>
|
|
945
|
-
<%= render layout: "style/modal_specimen", locals: {
|
|
946
|
-
label: "Change username (leveling)",
|
|
947
|
-
reference: %(the "Change username" modal WITH leveling (studio-engine studio/modals/blocks/_change_username, leveling: true) — quest pill + seeds level-up celebration on save. The on-chain save is an app-supplied callback (submit_url + an opaque finalize_hook); the engine is UI only. Open with $store.dsModals.open('change-username')),
|
|
948
|
-
open_expr: "$store.dsModals.open('change-username')",
|
|
949
|
-
glow_when: ds_glow.call("change-username") } do %>
|
|
950
|
-
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
951
|
-
<div class="flex items-center justify-between gap-2">
|
|
952
|
-
<span class="block h-2 w-16 rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
953
|
-
<span class="text-[8px] font-semibold uppercase rounded-full px-1.5 py-0.5" style="color: var(--color-primary); background: color-mix(in srgb, var(--color-primary) 12%, transparent)">Quest 1/4</span>
|
|
954
|
-
</div>
|
|
955
|
-
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
956
|
-
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
957
|
-
</div>
|
|
958
|
-
<% end %>
|
|
959
|
-
|
|
960
|
-
<%# Change username — leveling OFF: the plain MS-style action + Save. %>
|
|
961
|
-
<%= render layout: "style/modal_specimen", locals: {
|
|
962
|
-
label: "Change username (plain)",
|
|
963
|
-
reference: %(the "Change username" modal WITHOUT leveling (studio-engine studio/modals/blocks/_change_username, leveling: false) — the plain McRitchie Studio shape: input + Save, then a Saved state. No quest pill, no seeds. Same primitive, flag off. Open with $store.dsModals.open('change-username-plain')),
|
|
964
|
-
open_expr: "$store.dsModals.open('change-username-plain')",
|
|
965
|
-
glow_when: ds_glow.call("change-username-plain") } do %>
|
|
966
|
-
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
967
|
-
<span class="block h-2 w-16 rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
968
|
-
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
969
|
-
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
970
|
-
</div>
|
|
971
|
-
<% end %>
|
|
972
|
-
|
|
973
|
-
<%# Quest activity — leveling ON: generic quest (newsletter join) + seeds. %>
|
|
974
|
-
<%= render layout: "style/modal_specimen", locals: {
|
|
975
|
-
label: "Quest activity (leveling)",
|
|
976
|
-
reference: %(a generic quest activity WITH leveling (studio-engine studio/modals/blocks/_leveling_activity, leveling: true) — here a "Join the Newsletter" quest: quest pill + seeds celebration on complete. Covers join-newsletter / send-message and the rest as ONE parameterized primitive. Open with $store.dsModals.open('quest-activity')),
|
|
977
|
-
open_expr: "$store.dsModals.open('quest-activity')",
|
|
978
|
-
glow_when: ds_glow.call("quest-activity") } do %>
|
|
979
|
-
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
980
|
-
<div class="flex items-center justify-between gap-2">
|
|
981
|
-
<span class="text-lg leading-none">📬</span>
|
|
982
|
-
<span class="text-[8px] font-semibold uppercase rounded-full px-1.5 py-0.5" style="color: var(--color-primary); background: color-mix(in srgb, var(--color-primary) 12%, transparent)">Quest 2/4</span>
|
|
983
|
-
</div>
|
|
984
|
-
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
985
|
-
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
986
|
-
</div>
|
|
987
|
-
<% end %>
|
|
988
|
-
|
|
989
|
-
<%# Quest activity — leveling OFF: the plain action (no quest, no seeds). %>
|
|
990
|
-
<%= render layout: "style/modal_specimen", locals: {
|
|
991
|
-
label: "Quest activity (plain)",
|
|
992
|
-
reference: %(the SAME generic activity WITHOUT leveling (studio-engine studio/modals/blocks/_leveling_activity, leveling: false) — a plain "Join the Newsletter" action: input + Subscribe, then Subscribed. No quest pill, no seeds. Open with $store.dsModals.open('quest-activity-plain')),
|
|
993
|
-
open_expr: "$store.dsModals.open('quest-activity-plain')",
|
|
994
|
-
glow_when: ds_glow.call("quest-activity-plain") } do %>
|
|
995
|
-
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
996
|
-
<span class="text-lg leading-none">📬</span>
|
|
997
|
-
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
998
|
-
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
999
|
-
</div>
|
|
1000
|
-
<% end %>
|
|
1001
|
-
</div>
|
|
1002
|
-
</section>
|
|
1003
1032
|
</section>
|
data/lib/studio/version.rb
CHANGED
data/lib/studio.rb
CHANGED
|
@@ -25,6 +25,20 @@ module Studio
|
|
|
25
25
|
mattr_accessor :theme_logos, default: []
|
|
26
26
|
mattr_accessor :sticky_table_headers, default: false
|
|
27
27
|
|
|
28
|
+
# ---- Smooth-load convention ---------------------------------------------
|
|
29
|
+
# Opt-in per app: renders the view-transition + no-preview metas so Turbo 8
|
|
30
|
+
# page swaps materialize behind the current page and present with a view
|
|
31
|
+
# transition (layouts/studio/_smooth_load). OFF by default — an app with
|
|
32
|
+
# known multi-second pages should fix those before opting in, because
|
|
33
|
+
# no-preview holds the old page until the fresh response arrives.
|
|
34
|
+
mattr_accessor :smooth_load, default: false
|
|
35
|
+
|
|
36
|
+
# Minimum ms the nav spinner stays visible once shown (_head.html.erb).
|
|
37
|
+
# Apps wrapping multi-second operations in the spinner (e.g. Solana RPC)
|
|
38
|
+
# keep the high default to avoid flicker; smooth-load apps typically drop
|
|
39
|
+
# this to ~300 so fast loads never linger on a spinner.
|
|
40
|
+
mattr_accessor :nav_spinner_min_ms, default: 2500
|
|
41
|
+
|
|
28
42
|
# ---- Authentication ------------------------------------------------------
|
|
29
43
|
# Which sign-in methods this app offers. The shared login/signup views render
|
|
30
44
|
# a button/field per enabled method (gate with Studio.auth_method?). Order is
|
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.25.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex McRitchie
|
|
@@ -242,6 +242,7 @@ files:
|
|
|
242
242
|
- app/views/layouts/branded_mailer.html.erb
|
|
243
243
|
- app/views/layouts/studio/_flash.html.erb
|
|
244
244
|
- app/views/layouts/studio/_head.html.erb
|
|
245
|
+
- app/views/layouts/studio/_smooth_load.html.erb
|
|
245
246
|
- app/views/magic_links/confirm.html.erb
|
|
246
247
|
- app/views/navbar/show.html.erb
|
|
247
248
|
- app/views/registrations/new.html.erb
|