studio-engine 0.25.1 → 0.27.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 +137 -0
- data/app/assets/tailwind/studio_engine/engine.css +18 -2
- data/app/views/layouts/_navbar.html.erb +9 -1
- data/app/views/studio/modals/_host.html.erb +8 -18
- data/app/views/studio/modals/_load_convention.html.erb +55 -0
- data/app/views/studio/modals/_saving.html.erb +10 -3
- data/app/views/studio/modals/blocks/_processing_card.html.erb +20 -6
- data/app/views/style/_modal_specimen.html.erb +8 -1
- data/app/views/style/_modals.html.erb +218 -90
- data/app/views/style/modals/_entry_tokens.html.erb +97 -32
- data/app/views/style/modals/_onchain_tx.html.erb +24 -0
- data/app/views/style/modals/_wallet_connect.html.erb +12 -1
- data/lib/studio/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3fefcb19f5cf631e4e1515bf0b113fb33167cb1173f039a0777038aac5173d57
|
|
4
|
+
data.tar.gz: f203c29195b71e945a055bcfa60ad58f0d1aea16fe2ce60e84de4de1993d62ec
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d790f97aede359baf45915bbde14fef29cea6779a0866159a3b5f1dc5a396d24951d7ba3d796e4523e0dc17489319375929e8b3e2741ca9b7c569dd5f9dac91e
|
|
7
|
+
data.tar.gz: 25a54c2e44ad2e29608afe72fcf6b7dc8028fe28c47bd92e582e041b282f7f4e2b70ba5eb3468f4d8edb083f3d1fe185c340c135678b9a0a0556bfbab601a98b
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,137 @@
|
|
|
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.27.0 — 2026-07-28
|
|
6
|
+
|
|
7
|
+
**Two coordinated modal flows on the `/admin/style` Design System page — a walked
|
|
8
|
+
"Web3 Contest" wallet → on-chain flow and an extended "Contest entry & eligibility"
|
|
9
|
+
flow — plus a reusable minimum-visible-duration convention for load modals.** All
|
|
10
|
+
specimens are DEMO chrome (`demo: true`, no real signing); the engine gains only
|
|
11
|
+
view/chrome primitives and keeps custodial keys, wallet/multisig signing,
|
|
12
|
+
`Solana::Config`, and every on-chain write app-side.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **Minimum-visible-duration convention** — `studio/modals/_load_convention`
|
|
17
|
+
ships `window.StudioModals.holdAtLeast(minMs)` + a standard
|
|
18
|
+
`MIN_LOAD_MS` (1400) as a SINGLE definition, rendered by both the shared modal
|
|
19
|
+
host (`studio/modals/_host`, which drops its inline copy) and the style page.
|
|
20
|
+
A load spinner holds for at least `min_duration`, longer if the real async runs
|
|
21
|
+
longer — `resolveAt = max(min_duration, actual_completion)`; in demo mode
|
|
22
|
+
`min_duration` doubles as the auto-resolve timer. `_processing_card` gains
|
|
23
|
+
`min_duration` + `resolve_expr` locals so a demo load card self-advances.
|
|
24
|
+
- **Web3 Contest** section (renamed from "Web3") — a walked flow with
|
|
25
|
+
glow-follows-the-flow continuity: Connect Wallet → Processing on-chain
|
|
26
|
+
transaction → On-chain success. The Processing card is a load modal with a demo
|
|
27
|
+
success/error toggle (unchecked resolves to success, checked to error), both
|
|
28
|
+
resolved states built. Picking a wallet swaps to Processing, which auto-resolves
|
|
29
|
+
after the min-load duration.
|
|
30
|
+
- **Contest entry & eligibility** section (renamed from "Eligibility & entry",
|
|
31
|
+
moved directly under Web3 Contest) — the entry flow extended to
|
|
32
|
+
Entry tokens → Payment processing → Entry Tokens Minted → Contest enter
|
|
33
|
+
processing → Contest entered, with the glow following the step machine. Keeps
|
|
34
|
+
the Age gate + Entry tokens cards. The section states the honest web2/web3 map:
|
|
35
|
+
the token mint is web2-only (web3 funds USDC on-chain directly, no token); the
|
|
36
|
+
paths diverge at the funding front-end and the entry-submit endpoint, then
|
|
37
|
+
converge on the same on-chain Entry PDA and the same "Entry Confirmed" card.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- **`studio/modals/_host`** — the `holdAtLeast` helper now lives in the shared
|
|
42
|
+
`_load_convention` partial (same API, byte-identical behavior) so the host and
|
|
43
|
+
the DS page share one definition instead of drifting copies.
|
|
44
|
+
- **`style/modals/_entry_tokens`** — the confirming step uses the min-load
|
|
45
|
+
convention instead of a hardcoded timeout, and the step machine gains
|
|
46
|
+
`entering` + `entered` so hold-to-confirm walks through the on-chain consume to
|
|
47
|
+
the shared Entry Confirmed finish.
|
|
48
|
+
- **`style/modals/_onchain_tx`** — a processing modal opened with `demoResolve`
|
|
49
|
+
auto-resolves to success (or error when `demoError`) after the min-load
|
|
50
|
+
duration, honoring the convention.
|
|
51
|
+
- The Modals-section glow helper discriminates the on-chain-tx `state` and a
|
|
52
|
+
configurable step default, so the glow tracks a walked flow across modal ids.
|
|
53
|
+
|
|
54
|
+
## 0.26.1 — 2026-07-28
|
|
55
|
+
|
|
56
|
+
**Fix the `/admin/style` Design System page breaking when reached via Turbo Drive
|
|
57
|
+
navigation (the admin sidebar "Design System" link).** Reached by a direct page
|
|
58
|
+
load the page was fine; reached by an in-app Turbo visit its modals would not open
|
|
59
|
+
on a card click and EVERY specimen glow ring lit at once. Both symptoms were one
|
|
60
|
+
cause: the page-scoped `dsModals` (and `dsSolanaModal`) Alpine store was registered
|
|
61
|
+
ONLY inside a `document.addEventListener('alpine:init', …)` handler in the page
|
|
62
|
+
body. Alpine loads via a deferred CDN `<script>` in the engine head and fires
|
|
63
|
+
`alpine:init` exactly once, on the first full-document load; Turbo Drive advance
|
|
64
|
+
visits swap `<body>` without reloading that head script, so `alpine:init` never
|
|
65
|
+
fires again — and because the registration lives only in the `/admin/style` body,
|
|
66
|
+
it was absent during that one `alpine:init`. The specimen cards' `x-data` still
|
|
67
|
+
re-initialize on the Turbo body swap (Alpine's MutationObserver), so their
|
|
68
|
+
`@click="$store.dsModals.open(…)"`, `:style` glow bindings, and the host's
|
|
69
|
+
`<template x-if="$store.dsModals.current()…">` all evaluated against an undefined
|
|
70
|
+
store and threw `Cannot read properties of undefined (reading 'current')` — no
|
|
71
|
+
modal opened, and the throwing `:style` wiped each card's static inline
|
|
72
|
+
`--studio-team-glow-opacity: 0`, so the CSS default of `1` relit all rings.
|
|
73
|
+
|
|
74
|
+
### Fixed
|
|
75
|
+
|
|
76
|
+
- **`style/_modals`** — the `dsModals` + `dsSolanaModal` registration is now a
|
|
77
|
+
named `registerDsModals()` invoked through the same dual-guard the engine
|
|
78
|
+
already uses for `studio/modals/_image_upload`'s `cropPhotoModal`:
|
|
79
|
+
`if (window.Alpine) registerDsModals(); else document.addEventListener('alpine:init', registerDsModals);`.
|
|
80
|
+
On a Turbo visit Alpine has already started, so the store registers immediately
|
|
81
|
+
(the body script re-runs on every Turbo render) and `$store.dsModals` is defined
|
|
82
|
+
before the cards' bindings evaluate; on a first load Alpine has not booted yet,
|
|
83
|
+
so it still defers to `alpine:init`. The `if (Alpine.store('dsModals')) return;`
|
|
84
|
+
idempotency guard is unchanged, so a later `alpine:init` is a no-op. No consumer
|
|
85
|
+
change is required — `/admin/style` is an engine page.
|
|
86
|
+
- **Docs** — `style/_modal_specimen`'s fail-closed comment now records that the
|
|
87
|
+
static inline `--studio-team-glow-opacity: 0` guards the ring only while
|
|
88
|
+
`$store.dsModals` is DEFINED (a throwing `:style` wipes the inline value) — the
|
|
89
|
+
refinement of 0.24.1's "regardless of Alpine timing" claim that this bug exposed.
|
|
90
|
+
|
|
91
|
+
### Swept
|
|
92
|
+
|
|
93
|
+
- Audited every engine `Alpine.store(...)` / `Alpine.data(...)` registration for
|
|
94
|
+
the same Turbo-visit fragility. `dsModals`/`dsSolanaModal` (`style/_modals`) was
|
|
95
|
+
the only page-scoped registration missing the guard. `studio/modals/_image_upload`
|
|
96
|
+
(`cropPhotoModal`) already carries it; the shared modal host (`studio/modals/_host`
|
|
97
|
+
→ `modals`) and the theme/devMode stores (`layouts/studio/_head`) are app-wide
|
|
98
|
+
bootstrap rendered on the first full load, so their single `alpine:init`
|
|
99
|
+
registration persists across Turbo visits (verified: `$store.modals` stays
|
|
100
|
+
defined after a Turbo nav).
|
|
101
|
+
|
|
102
|
+
## 0.26.0 — 2026-07-28
|
|
103
|
+
|
|
104
|
+
**The engine navbar pins itself under smooth-load, and the smooth-load CSS is
|
|
105
|
+
fully engine-owned.** Since 0.24 the `vt-pinned-header` pin was opt-in but the
|
|
106
|
+
engine's own `layouts/_navbar` never carried it, so a host that wanted the
|
|
107
|
+
pinned-header transition had to SHADOW the whole partial just to add one class
|
|
108
|
+
(acquisition-studio did exactly that — the known override-drift trap). The
|
|
109
|
+
navbar now self-pins when `Studio.smooth_load` is on, and the engine also ships
|
|
110
|
+
the `studio-header` cross-fade suppression that mcritchie-studio and
|
|
111
|
+
turf-monster had been carrying app-side as a bridge. **Consumer cleanup this
|
|
112
|
+
version unlocks:** acquisition-studio deletes its entire
|
|
113
|
+
`app/views/layouts/_navbar.html.erb` override; mcritchie-studio and
|
|
114
|
+
turf-monster delete their app-side `::view-transition-old(studio-header)` /
|
|
115
|
+
`::view-transition-new(studio-header) { animation: none; }` bridge blocks.
|
|
116
|
+
|
|
117
|
+
### Changed
|
|
118
|
+
|
|
119
|
+
- **`layouts/_navbar`** — the sticky header adds `vt-pinned-header` itself when
|
|
120
|
+
`Studio.smooth_load` is on, non-preview branch ONLY (preview renders can
|
|
121
|
+
repeat per page, and a duplicate `view-transition-name` silently disables
|
|
122
|
+
every transition). With the flag off nothing extra renders, so a plain gem
|
|
123
|
+
bump changes no opted-out app.
|
|
124
|
+
- **`engine.css`** — new `::view-transition-old(studio-header)` /
|
|
125
|
+
`::view-transition-new(studio-header) { animation: none; }` rule beside the
|
|
126
|
+
smooth-load block: it suppresses the UA-default ~250ms plus-lighter
|
|
127
|
+
cross-fade between the header's two snapshots, which double-drew the wordmark
|
|
128
|
+
and buttons on any navigation from a scrolled page (collapsed header →
|
|
129
|
+
expanded at top). Lifts the identical bridge rule the hub and turf-monster
|
|
130
|
+
carried app-side.
|
|
131
|
+
- **`engine.css`** — `.turbo-progress-bar` background becomes
|
|
132
|
+
`var(--color-cta, #0076ff)`: the fallback (Turbo's own default blue) keeps
|
|
133
|
+
the bar visible in a layout that never renders the runtime theme block
|
|
134
|
+
(`studio_theme_css_tag`), where `--color-cta` is unset.
|
|
135
|
+
|
|
5
136
|
## 0.25.1 — 2026-07-28
|
|
6
137
|
|
|
7
138
|
**Fix the Profile Leveling save-close regression 0.25.0 introduced.** The 0.25.0
|
|
@@ -110,6 +241,12 @@ follows the step machine), so this surfaced as a flash-of-all-glow on load.
|
|
|
110
241
|
unchanged. The `engine-motion.css` `.studio-team-glow` default is untouched
|
|
111
242
|
(the always-on Tricks demos depend on it).
|
|
112
243
|
|
|
244
|
+
> **Refined in 0.26.1:** "regardless of Alpine timing" holds only while
|
|
245
|
+
> `$store.dsModals` is defined. On a Turbo Drive visit the store went
|
|
246
|
+
> unregistered, the reactive `:style` threw, and Alpine wiped this inline
|
|
247
|
+
> default — relighting every ring. 0.26.1 dual-guards the store registration so
|
|
248
|
+
> it survives Turbo visits, restoring the fail-closed guarantee.
|
|
249
|
+
|
|
113
250
|
## 0.24.0 — 2026-07-28
|
|
114
251
|
|
|
115
252
|
The **smooth-load convention**, opt-in per app. Pages materialize behind the
|
|
@@ -221,12 +221,28 @@
|
|
|
221
221
|
view-transition-name: studio-header;
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
/* Suppress the UA default on the pinned header's own group: without this the
|
|
225
|
+
browser runs its ~250ms plus-lighter cross-fade between the header's old and
|
|
226
|
+
new snapshots, and on any navigation from a scrolled page (collapsed header
|
|
227
|
+
→ expanded at top) the wordmark and buttons draw TWICE, offset by the height
|
|
228
|
+
delta, until the blend settles. animation: none shows the incoming header
|
|
229
|
+
immediately while the group box still morphs its height. Engine-owned as of
|
|
230
|
+
0.26.0 — this lifts the identical bridge rule mcritchie-studio and
|
|
231
|
+
turf-monster carried app-side; consumers delete their copies when adopting
|
|
232
|
+
this version. */
|
|
233
|
+
::view-transition-old(studio-header),
|
|
234
|
+
::view-transition-new(studio-header) {
|
|
235
|
+
animation: none;
|
|
236
|
+
}
|
|
237
|
+
|
|
224
238
|
/* Turbo's progress bar is the slow-load feedback under no-preview (the old
|
|
225
239
|
page holds until the fresh response). Theme it so every app's bar matches
|
|
226
|
-
its brand instead of Turbo's default.
|
|
240
|
+
its brand instead of Turbo's default. The fallback (#0076ff, Turbo's own
|
|
241
|
+
default blue) keeps the bar visible in a layout that never renders the
|
|
242
|
+
runtime theme block (studio_theme_css_tag), where --color-cta is unset. */
|
|
227
243
|
.turbo-progress-bar {
|
|
228
244
|
height: 3px;
|
|
229
|
-
background: var(--color-cta);
|
|
245
|
+
background: var(--color-cta, #0076ff);
|
|
230
246
|
}
|
|
231
247
|
|
|
232
248
|
/* Reduced motion: keep the single-render swap, drop the choreography. Also
|
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
%>
|
|
9
9
|
<% show_user = local_assigns.fetch(:show_logged_in, logged_in?) %>
|
|
10
10
|
<% is_preview = local_assigns.fetch(:preview, false) %>
|
|
11
|
+
<%# Smooth-load self-pin (engine 0.26): when the app opts into Studio.smooth_load,
|
|
12
|
+
the sticky header names itself as the shared view-transition group
|
|
13
|
+
(vt-pinned-header) so page content transitions beneath a navbar that stays
|
|
14
|
+
put — hosts no longer shadow this partial just to add the class. Non-preview
|
|
15
|
+
branch ONLY: preview renders can repeat per page, and a duplicate
|
|
16
|
+
view-transition-name silently disables every transition. With the flag off,
|
|
17
|
+
nothing extra renders. %>
|
|
18
|
+
<% pin_header = Studio.smooth_load && !is_preview %>
|
|
11
19
|
<% balance_html = local_assigns.fetch(:balance_html, nil) %>
|
|
12
20
|
<% extra_icons_html = local_assigns.fetch(:extra_icons_html, nil) %>
|
|
13
21
|
<% show_logout_link = local_assigns.fetch(:show_logout_link, false) %>
|
|
@@ -19,7 +27,7 @@
|
|
|
19
27
|
%>
|
|
20
28
|
|
|
21
29
|
<header x-data="{ scrolled: false }" <%= '@scroll.window="scrolled = scrolled ? (window.scrollY > 5) : (window.scrollY > 60)"'.html_safe unless is_preview %>
|
|
22
|
-
class="<%= is_preview ? 'bg-page' : 'sticky top-0 z-50 bg-page transition-shadow duration-300
|
|
30
|
+
class="<%= is_preview ? 'bg-page' : "#{'vt-pinned-header ' if pin_header}sticky top-0 z-50 bg-page transition-shadow duration-300" %>"
|
|
23
31
|
:class="scrolled && 'shadow-lg border-b border-subtle is-scrolled'">
|
|
24
32
|
<style>
|
|
25
33
|
.user-nav-col { width: 14rem; }
|
|
@@ -153,24 +153,6 @@
|
|
|
153
153
|
|
|
154
154
|
<script>
|
|
155
155
|
(function() {
|
|
156
|
-
// === StudioModals helpers ===========================================
|
|
157
|
-
//
|
|
158
|
-
// holdAtLeast(ms) — pair with the _processing_card block. Stamp the
|
|
159
|
-
// moment a loading view becomes visible, then await the minimum
|
|
160
|
-
// before swapping to success so the spinner doesn't flash past the
|
|
161
|
-
// user when the operation finishes fast.
|
|
162
|
-
window.StudioModals = window.StudioModals || {};
|
|
163
|
-
window.StudioModals.holdAtLeast = function(minMs) {
|
|
164
|
-
var startedAt = Date.now();
|
|
165
|
-
return {
|
|
166
|
-
then: function(callback) {
|
|
167
|
-
var remaining = Math.max(0, minMs - (Date.now() - startedAt));
|
|
168
|
-
if (remaining === 0) { callback(); return; }
|
|
169
|
-
setTimeout(callback, remaining);
|
|
170
|
-
}
|
|
171
|
-
};
|
|
172
|
-
};
|
|
173
|
-
|
|
174
156
|
// === Modal animation registry ======================================
|
|
175
157
|
//
|
|
176
158
|
// The single place to define + tune named entrance / exit animations
|
|
@@ -490,6 +472,14 @@
|
|
|
490
472
|
})();
|
|
491
473
|
</script>
|
|
492
474
|
|
|
475
|
+
<%# The minimum-visible-duration convention (window.StudioModals.holdAtLeast +
|
|
476
|
+
MIN_LOAD_MS) — the SINGLE source, shared with the /admin/style demo page.
|
|
477
|
+
Pair it with the _processing_card block so a load spinner never flashes past
|
|
478
|
+
the user. Rendered AFTER the store <script> above so the store stays the
|
|
479
|
+
host's first <script> (a Node behavior harness extracts that first script).
|
|
480
|
+
See studio/modals/_load_convention for the full contract. %>
|
|
481
|
+
<%= render "studio/modals/load_convention" %>
|
|
482
|
+
|
|
493
483
|
<template x-if="$store.modals.current()">
|
|
494
484
|
<%# Backdrop + card use CSS keyframe animations (NOT Alpine
|
|
495
485
|
x-transition), all shipped in the inline style block above. On
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Minimum-visible-duration convention for load / processing modals.
|
|
3
|
+
|
|
4
|
+
A load modal shows its spinner for AT LEAST a minimum duration so it never
|
|
5
|
+
flashes past the user (registering "did anything happen?" instead of
|
|
6
|
+
"processing → done"), but LONGER whenever the real async takes longer:
|
|
7
|
+
|
|
8
|
+
resolveAt = max(min_duration, actual_completion)
|
|
9
|
+
|
|
10
|
+
This ships the convention on window.StudioModals, defined ONCE here and
|
|
11
|
+
rendered by BOTH the shared modal host (studio/modals/_host) and the living
|
|
12
|
+
style guide (style/_modals) — so every consumer app and the /admin/style demo
|
|
13
|
+
page share a single definition:
|
|
14
|
+
|
|
15
|
+
MIN_LOAD_MS — the standard default minimum (1400 ms). A caller may pass its
|
|
16
|
+
own, but reach for this so every load modal feels consistent.
|
|
17
|
+
|
|
18
|
+
holdAtLeast(minMs) — returns a thenable whose .then(cb) fires cb at
|
|
19
|
+
max(minMs, elapsed-since-this-call). Two usages:
|
|
20
|
+
|
|
21
|
+
REAL async — stamp the moment the loading view appears, then
|
|
22
|
+
resolve in the async completion, so a slow op is never cut
|
|
23
|
+
short and a fast one still honors the spinner:
|
|
24
|
+
var hold = StudioModals.holdAtLeast(StudioModals.MIN_LOAD_MS);
|
|
25
|
+
doWork().then(function () { hold.then(showSuccess); });
|
|
26
|
+
|
|
27
|
+
DEMO (no real work) — call .then immediately and minMs
|
|
28
|
+
DOUBLES as the auto-resolve timer: the demo advances after
|
|
29
|
+
min_duration exactly:
|
|
30
|
+
StudioModals.holdAtLeast(StudioModals.MIN_LOAD_MS).then(advance);
|
|
31
|
+
|
|
32
|
+
Idempotent (|| guards): never clobber a definition an app already shipped, and
|
|
33
|
+
safe to render more than once on a page.
|
|
34
|
+
%>
|
|
35
|
+
<script>
|
|
36
|
+
(function () {
|
|
37
|
+
window.StudioModals = window.StudioModals || {};
|
|
38
|
+
// Standard minimum-visible duration for a load spinner, in ms. ~1200–1500
|
|
39
|
+
// reads as "processing → done"; below ~1s the spinner flashes and reads as a
|
|
40
|
+
// glitch. Callers may override per-modal, but default to this for consistency.
|
|
41
|
+
window.StudioModals.MIN_LOAD_MS = window.StudioModals.MIN_LOAD_MS || 1400;
|
|
42
|
+
// holdAtLeast(minMs).then(cb) — cb fires at max(minMs, elapsed-since-call).
|
|
43
|
+
// Stamp the moment a loading view becomes visible; the anti-flicker floor.
|
|
44
|
+
window.StudioModals.holdAtLeast = window.StudioModals.holdAtLeast || function (minMs) {
|
|
45
|
+
var startedAt = Date.now();
|
|
46
|
+
return {
|
|
47
|
+
then: function (callback) {
|
|
48
|
+
var remaining = Math.max(0, minMs - (Date.now() - startedAt));
|
|
49
|
+
if (remaining === 0) { callback(); return; }
|
|
50
|
+
setTimeout(callback, remaining);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
})();
|
|
55
|
+
</script>
|
|
@@ -2,10 +2,17 @@
|
|
|
2
2
|
a { title } prop while a Turbo form uploads. Register it in your modal-host
|
|
3
3
|
block alongside crop-photo. Single root for the host's <template x-if>.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
Locals:
|
|
6
|
+
store — Alpine store name backing the title read. Default "modals".
|
|
7
|
+
The living style guide passes store: "dsModals".
|
|
8
|
+
resolve_expr — optional demo auto-resolve (min-load convention). The real
|
|
9
|
+
Turbo submit drives the spinner and closes on completion, so
|
|
10
|
+
apps omit this; the /admin/style demo passes an expression
|
|
11
|
+
(e.g. close) so the specimen self-terminates after min_duration
|
|
12
|
+
instead of spinning forever. %>
|
|
7
13
|
<% saving_store = local_assigns.fetch(:store, "modals") %>
|
|
8
14
|
<div>
|
|
9
15
|
<%= render "studio/modals/blocks/processing_card",
|
|
10
|
-
title_key: "$store.#{saving_store}.current()?.props?.title || 'Saving…'"
|
|
16
|
+
title_key: "$store.#{saving_store}.current()?.props?.title || 'Saving…'",
|
|
17
|
+
resolve_expr: local_assigns[:resolve_expr] %>
|
|
11
18
|
</div>
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
<%#
|
|
2
2
|
Reusable processing / loading card. Spinner + title + optional message.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
|
|
4
|
+
Minimum-visible-duration convention (studio/modals/_load_convention):
|
|
5
|
+
a load spinner must show for AT LEAST a floor, but longer if the real async
|
|
6
|
+
runs longer — resolveAt = max(min_duration, actual_completion) — so it never
|
|
7
|
+
flashes past the user ("did anything happen?") yet never cuts a slow op short.
|
|
8
|
+
Two ways to honor it:
|
|
9
|
+
• REAL async — the caller stamps window.StudioModals.holdAtLeast(min_duration)
|
|
10
|
+
when this card appears, then resolves in its completion callback.
|
|
11
|
+
• DEMO / auto-resolve — pass `resolve_expr`: this card self-schedules that
|
|
12
|
+
Alpine expression after `min_duration` (min_duration doubles as the demo's
|
|
13
|
+
auto-resolve timer), via the same holdAtLeast floor.
|
|
8
14
|
|
|
9
15
|
Locals — pass static strings OR `_key` variants for Alpine-driven
|
|
10
16
|
text that updates as state mutates (e.g. live store reads):
|
|
@@ -15,6 +21,10 @@
|
|
|
15
21
|
size: spinner size — 'sm', 'md' (default), 'lg'
|
|
16
22
|
color: color token for the spinner — 'primary' (default),
|
|
17
23
|
'success', 'warning'
|
|
24
|
+
min_duration: minimum visible ms (default 1400, mirrors
|
|
25
|
+
StudioModals.MIN_LOAD_MS). The convention's floor.
|
|
26
|
+
resolve_expr: Alpine expression run after min_duration to advance the flow
|
|
27
|
+
(demo auto-resolve). Omit for a real spinner the caller drives.
|
|
18
28
|
|
|
19
29
|
One of `title` or `title_key` is required.
|
|
20
30
|
%>
|
|
@@ -27,8 +37,12 @@
|
|
|
27
37
|
'lg' => 'w-16 h-16 border-4'
|
|
28
38
|
}
|
|
29
39
|
spinner_class = sizes[size] || sizes['md']
|
|
40
|
+
# min_duration mirrors StudioModals.MIN_LOAD_MS (the JS default) so the Ruby and
|
|
41
|
+
# JS sides of the convention agree on the same floor.
|
|
42
|
+
min_duration = local_assigns[:min_duration] || 1400
|
|
43
|
+
resolve_expr = local_assigns[:resolve_expr]
|
|
30
44
|
%>
|
|
31
|
-
<div class="text-center py-6"
|
|
45
|
+
<div class="text-center py-6"<%= " x-data=\"{}\" x-init=\"window.StudioModals.holdAtLeast(#{min_duration}).then(() => { #{resolve_expr} })\"".html_safe if resolve_expr %>>
|
|
32
46
|
<div class="mx-auto <%= spinner_class %> rounded-full border-<%= color %>/30 border-t-<%= color %> animate-spin mb-5"></div>
|
|
33
47
|
<% if local_assigns[:title_key] %>
|
|
34
48
|
<p class="text-base font-bold text-heading mb-1" x-text="<%= title_key %>"></p>
|
|
@@ -59,7 +59,14 @@
|
|
|
59
59
|
default (--studio-team-glow-opacity of 1) so the ring is OFF at first
|
|
60
60
|
paint, BEFORE Alpine hydrates the reactive :style below. Without it every
|
|
61
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.
|
|
62
|
+
:style then drives 0 to 0.95 with the 0.4s cross-fade. Keep BOTH.
|
|
63
|
+
CAVEAT: this fail-closed default holds only while $store.dsModals is
|
|
64
|
+
DEFINED. glow_when dereferences the store; if the store is missing the
|
|
65
|
+
reactive :style THROWS, and Alpine's style bind wipes this inline value
|
|
66
|
+
on the way, so the CSS default of 1 relights every ring. That is exactly
|
|
67
|
+
the Turbo-nav store gap fixed in 0.26.1 (style/_modals dual-guards the
|
|
68
|
+
dsModals registration so it survives Turbo visits, not just first load).
|
|
69
|
+
So the guarantee is: static inline 0 PLUS a registered dsModals. %>
|
|
63
70
|
<% if glow_when %>style="--studio-team-glow-opacity: 0" :style="{ '--studio-team-glow-opacity': (<%= glow_when %>) ? '0.95' : '0' }"<% end %>>
|
|
64
71
|
<article
|
|
65
72
|
class="card overflow-hidden flex flex-col h-full<%= " opacity-95" if disabled %><%= " cursor-pointer" if clickable %>"
|
|
@@ -42,18 +42,22 @@
|
|
|
42
42
|
|
|
43
43
|
# Active-card glow-match expression builder: yields an Alpine boolean that is
|
|
44
44
|
# true when $store.dsModals.current() is THIS specimen's modal. It discriminates
|
|
45
|
-
# by sub-state so
|
|
46
|
-
# card
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
|
|
45
|
+
# by sub-state so specimens that open the SAME modal id still glow the right
|
|
46
|
+
# card, and so the glow FOLLOWS a walked flow as the step machine advances:
|
|
47
|
+
# step: the auth / entry-tokens step machine (step_default is the
|
|
48
|
+
# modal's own default step — 'credentials' for auth, 'picker'
|
|
49
|
+
# for entry-tokens).
|
|
50
|
+
# state: the on-chain-tx state (processing / success / error) — lets the
|
|
51
|
+
# glow track Connect Wallet -> Processing -> On-chain success.
|
|
52
|
+
# crop: the two crop-photo variants (:picker vs :crop), on props.cropReady.
|
|
53
|
+
# celebrate: the input specimen (form) vs the "updated" (celebrate) specimen
|
|
54
|
+
# of the SAME leveling modal id.
|
|
55
|
+
ds_glow = lambda do |id, step: nil, step_default: "credentials", crop: nil, celebrate: nil, state: nil|
|
|
51
56
|
cur = "$store.dsModals.current()"
|
|
52
57
|
expr = "#{cur} && #{cur}.id === '#{id}'"
|
|
53
|
-
expr += " && (#{cur}.props.step || '
|
|
58
|
+
expr += " && (#{cur}.props.step || '#{step_default}') === '#{step}'" if step
|
|
59
|
+
expr += " && #{cur}.props.state === '#{state}'" if state
|
|
54
60
|
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
61
|
expr += (celebrate ? " && !!#{cur}.props.celebrate" : " && !#{cur}.props.celebrate") unless celebrate.nil?
|
|
58
62
|
expr
|
|
59
63
|
end
|
|
@@ -107,7 +111,10 @@
|
|
|
107
111
|
isAvailable: function () { return false; }
|
|
108
112
|
};
|
|
109
113
|
|
|
110
|
-
|
|
114
|
+
// Register the page-scoped dsModals + dsSolanaModal stores. Named (not an
|
|
115
|
+
// inline alpine:init callback) so the dual-guard at the end can invoke it
|
|
116
|
+
// directly on a Turbo visit, when alpine:init will not fire again.
|
|
117
|
+
function registerDsModals() {
|
|
111
118
|
if (Alpine.store('dsModals')) return;
|
|
112
119
|
|
|
113
120
|
// Enter/exit animation registry — keys map to a CSS class (engine-
|
|
@@ -244,10 +251,29 @@
|
|
|
244
251
|
get ctaLabel() { return this._read('ctaLabel', ''); },
|
|
245
252
|
get ctaHref() { return this._read('ctaHref', ''); }
|
|
246
253
|
});
|
|
247
|
-
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Alpine's deferred CDN <script> fires alpine:init exactly ONCE, on the
|
|
257
|
+
// first full-document load. Turbo Drive advance visits swap the document
|
|
258
|
+
// body without reloading that head script, so alpine:init never fires
|
|
259
|
+
// again — and
|
|
260
|
+
// because this dsModals registration lives ONLY in the /admin/style body,
|
|
261
|
+
// it was absent during that one alpine:init. Registering here on the
|
|
262
|
+
// Turbo-visit path (Alpine already started) keeps $store.dsModals defined,
|
|
263
|
+
// so the specimen cards' @click / :style / <template x-if> don't throw.
|
|
264
|
+
// Mirrors studio/modals/_image_upload's cropPhotoModal guard.
|
|
265
|
+
if (window.Alpine) { registerDsModals(); }
|
|
266
|
+
else { document.addEventListener('alpine:init', registerDsModals); }
|
|
248
267
|
})();
|
|
249
268
|
</script>
|
|
250
269
|
|
|
270
|
+
<%# The minimum-visible-duration convention (window.StudioModals.holdAtLeast +
|
|
271
|
+
MIN_LOAD_MS) — the SAME single definition the shared modal host renders, so
|
|
272
|
+
the demo load modals here (payment/entry processing, on-chain tx, saving)
|
|
273
|
+
honor resolveAt = max(min_duration, actual) and auto-resolve after
|
|
274
|
+
min_duration in demo mode. A consumer app already has it via the host. %>
|
|
275
|
+
<%= render "studio/modals/load_convention" %>
|
|
276
|
+
|
|
251
277
|
<%# Cropper.js + the cropPhotoModal factory — makes the Profile crop / upload
|
|
252
278
|
modals openable on this page (loads cropper.js via <script defer> and
|
|
253
279
|
defines the factory inline; no importmap change). %>
|
|
@@ -295,8 +321,11 @@
|
|
|
295
321
|
<template x-if="$store.dsModals.current().id === 'crop-photo'">
|
|
296
322
|
<div><%= render "studio/modals/crop_photo", store: "dsModals" %></div>
|
|
297
323
|
</template>
|
|
324
|
+
<%# saving — the demo passes resolve_expr so the spinner self-terminates
|
|
325
|
+
after min_duration (a real app's Turbo submit drives + closes it). %>
|
|
298
326
|
<template x-if="$store.dsModals.current().id === 'saving'">
|
|
299
|
-
<div><%= render "studio/modals/saving", store: "dsModals"
|
|
327
|
+
<div><%= render "studio/modals/saving", store: "dsModals",
|
|
328
|
+
resolve_expr: "$store.dsModals.close()" %></div>
|
|
300
329
|
</template>
|
|
301
330
|
|
|
302
331
|
<%# --- Web3 --- %>
|
|
@@ -320,9 +349,12 @@
|
|
|
320
349
|
</template>
|
|
321
350
|
|
|
322
351
|
<%# --- System and status (reusable card blocks) --- %>
|
|
352
|
+
<%# ds-processing demos the min-duration convention on the canonical block:
|
|
353
|
+
resolve_expr auto-swaps to the success card after min_duration. %>
|
|
323
354
|
<template x-if="$store.dsModals.current().id === 'ds-processing'">
|
|
324
355
|
<div><%= render "studio/modals/blocks/processing_card",
|
|
325
|
-
title: "Confirming entry", message: "Talking to the server…"
|
|
356
|
+
title: "Confirming entry", message: "Talking to the server…",
|
|
357
|
+
resolve_expr: "$store.dsModals.swap('ds-success')" %></div>
|
|
326
358
|
</template>
|
|
327
359
|
<template x-if="$store.dsModals.current().id === 'ds-success'">
|
|
328
360
|
<div><%= render "studio/modals/blocks/success_card",
|
|
@@ -508,68 +540,6 @@
|
|
|
508
540
|
</div>
|
|
509
541
|
</section>
|
|
510
542
|
|
|
511
|
-
<%# ===================================================================== %>
|
|
512
|
-
<%# 1b. ELIGIBILITY + ENTRY %>
|
|
513
|
-
<%# ===================================================================== %>
|
|
514
|
-
<section class="space-y-5">
|
|
515
|
-
<div class="space-y-1">
|
|
516
|
-
<div class="flex flex-wrap items-center gap-3">
|
|
517
|
-
<h3 class="text-xl font-bold text-heading">Eligibility & entry</h3>
|
|
518
|
-
<% if age_gate_on %>
|
|
519
|
-
<span class="badge" style="color: var(--color-success); border-color: var(--color-success)">age gate on</span>
|
|
520
|
-
<% else %>
|
|
521
|
-
<span class="badge" style="color: var(--color-warning); border-color: var(--color-warning)">age gate off</span>
|
|
522
|
-
<% end %>
|
|
523
|
-
</div>
|
|
524
|
-
<p class="text-muted text-sm">
|
|
525
|
-
The pre-entry gate and the entry-purchase flow. The <strong>age gate</strong>
|
|
526
|
-
(<code class="font-mono text-2xs">studio/modals/blocks/_age_verify</code>) is a
|
|
527
|
-
NEW engine primitive gated by
|
|
528
|
-
<code class="font-mono text-2xs">Studio.feature?(:age_gate)</code> — a DOB
|
|
529
|
-
modal whose minimum age, jurisdiction label, endpoint, and legal copy are all
|
|
530
|
-
<strong>app-supplied</strong> (the engine hardcodes no policy). It is the heavier
|
|
531
|
-
sibling of the <code class="font-mono text-2xs">shared/_age_attestation</code>
|
|
532
|
-
checkbox inside Sign in above — <strong>run one or the other</strong>. The
|
|
533
|
-
<strong>entry-tokens</strong> flow is <strong>app-specific</strong> (payments
|
|
534
|
-
product): documented here composing engine chrome, but the packs, rails, and
|
|
535
|
-
the on-chain mint stay app-owned.
|
|
536
|
-
</p>
|
|
537
|
-
</div>
|
|
538
|
-
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
539
|
-
<%# Age gate — engine primitive, gated on :age_gate (disabled-but-present). %>
|
|
540
|
-
<%= render layout: "style/modal_specimen", locals: {
|
|
541
|
-
label: "Age gate (DOB)",
|
|
542
|
-
reference: %(the "Age gate" DOB modal (studio-engine studio/modals/blocks/_age_verify) — a NEW engine primitive; the app supplies min_age / state / submit_url / fine_print and the engine hardcodes no legal policy. In this demo, Confirm & Continue advances to the Entry tokens purchase (page-level dsModals wiring on the primitive's 'age-verified' hook; the primitive's real submit posts to submit_url). Open with $store.dsModals.open('age-verify')),
|
|
543
|
-
open_expr: "$store.dsModals.open('age-verify')",
|
|
544
|
-
glow_when: ds_glow.call("age-verify"),
|
|
545
|
-
disabled: !age_gate_on, openable: true, disabled_label: "age gate off" } do %>
|
|
546
|
-
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2 text-center">
|
|
547
|
-
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
548
|
-
<div class="grid grid-cols-3 gap-1">
|
|
549
|
-
<span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
550
|
-
<span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
551
|
-
<span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
552
|
-
</div>
|
|
553
|
-
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
554
|
-
</div>
|
|
555
|
-
<% end %>
|
|
556
|
-
|
|
557
|
-
<%# Entry tokens — app-specific flow, documented (not capability-gated). %>
|
|
558
|
-
<%= render layout: "style/modal_specimen", locals: {
|
|
559
|
-
label: "Entry tokens",
|
|
560
|
-
reference: %(the "Entry tokens" purchase flow (app-specific — documented in the engine style guide composing studio/modals/blocks chrome). The packs, pricing, rails, and on-chain mint stay app-owned. Open with $store.dsModals.open('entry-tokens', { step: 'picker' })),
|
|
561
|
-
open_expr: "$store.dsModals.open('entry-tokens', { step: 'picker' })",
|
|
562
|
-
glow_when: ds_glow.call("entry-tokens") } do %>
|
|
563
|
-
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
564
|
-
<span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
565
|
-
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
566
|
-
<span class="block h-6 w-full rounded border" style="border-color: var(--color-primary)"></span>
|
|
567
|
-
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
568
|
-
</div>
|
|
569
|
-
<% end %>
|
|
570
|
-
</div>
|
|
571
|
-
</section>
|
|
572
|
-
|
|
573
543
|
<%# ===================================================================== %>
|
|
574
544
|
<%# 2. PROFILE — crop / upload, live via cropper_assets %>
|
|
575
545
|
<%# ===================================================================== %>
|
|
@@ -751,12 +721,13 @@
|
|
|
751
721
|
</section>
|
|
752
722
|
|
|
753
723
|
<%# ===================================================================== %>
|
|
754
|
-
<%#
|
|
724
|
+
<%# 2. WEB3 CONTEST — the walked wallet -> on-chain flow (glow follows it). %>
|
|
725
|
+
<%# Gated by :web3; off = disabled-but-present-yet-openable. %>
|
|
755
726
|
<%# ===================================================================== %>
|
|
756
727
|
<section class="space-y-5">
|
|
757
728
|
<div class="space-y-1">
|
|
758
729
|
<div class="flex flex-wrap items-center gap-3">
|
|
759
|
-
<h3 class="text-xl font-bold text-heading">Web3</h3>
|
|
730
|
+
<h3 class="text-xl font-bold text-heading">Web3 Contest</h3>
|
|
760
731
|
<% if web3_on %>
|
|
761
732
|
<span class="badge" style="color: var(--color-success); border-color: var(--color-success)">enabled on this app</span>
|
|
762
733
|
<% else %>
|
|
@@ -764,21 +735,31 @@
|
|
|
764
735
|
<% end %>
|
|
765
736
|
</div>
|
|
766
737
|
<p class="text-muted text-sm">
|
|
767
|
-
|
|
738
|
+
The wallet + on-chain modals, ported into the engine and gated by
|
|
768
739
|
<code class="font-mono text-2xs">Studio.feature?(:web3)</code>. Web3 is
|
|
769
740
|
<strong><%= web3_on ? "on" : "off" %></strong> here, so these render
|
|
770
741
|
<%= web3_on ? "live." : "greyed and badged — but STILL openable as a preview." %>
|
|
771
|
-
The
|
|
772
|
-
|
|
742
|
+
The first three cards are a <strong>walked flow</strong> —
|
|
743
|
+
<strong>Connect Wallet → Processing on-chain transaction → On-chain
|
|
744
|
+
success</strong> — and the live card <strong>glows</strong>, following the
|
|
745
|
+
flow as it advances (open Connect Wallet and pick a wallet to watch it walk).
|
|
746
|
+
The <strong>Processing</strong> card is a load modal: its
|
|
747
|
+
<strong>success/error toggle</strong> chooses whether it resolves to success
|
|
748
|
+
or the error state, after the <strong>minimum-visible duration</strong>
|
|
749
|
+
(<code class="font-mono text-2xs">studio/modals/_load_convention</code>). The
|
|
750
|
+
Connect-wallet picker ships its <strong>Phantom / Solflare / Backpack brand
|
|
751
|
+
marks inline from the engine</strong>
|
|
773
752
|
(<code class="font-mono text-2xs">studio/modals/blocks/_wallet_brand_sprite</code>),
|
|
774
753
|
so every app inherits them with no per-app icon files.
|
|
775
754
|
</p>
|
|
776
755
|
</div>
|
|
777
756
|
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
757
|
+
<%# 1. Connect wallet — the walk's entry; picking a wallet swaps to Processing. %>
|
|
778
758
|
<%= render layout: "style/modal_specimen", locals: {
|
|
779
759
|
label: "Connect wallet",
|
|
780
|
-
reference: %(the Web3 "Connect wallet" picker (studio-engine style/modals/_wallet_connect) —
|
|
760
|
+
reference: %(the Web3 Contest "Connect wallet" picker (studio-engine style/modals/_wallet_connect) — the walk's entry: picking a wallet runs a brief connecting state then swaps to the on-chain Processing modal. Open with $store.dsModals.open('wallet-connect')),
|
|
781
761
|
open_expr: "$store.dsModals.open('wallet-connect')",
|
|
762
|
+
glow_when: ds_glow.call("wallet-connect"),
|
|
782
763
|
disabled: !web3_on, openable: true } do %>
|
|
783
764
|
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
784
765
|
<span class="block h-2 w-20 rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
@@ -787,10 +768,14 @@
|
|
|
787
768
|
</div>
|
|
788
769
|
<% end %>
|
|
789
770
|
|
|
771
|
+
<%# 2. Processing — the load modal; the success/error toggle picks the resolution. %>
|
|
790
772
|
<%= render layout: "style/modal_specimen", locals: {
|
|
791
|
-
label: "
|
|
792
|
-
reference: %(the Web3 "
|
|
793
|
-
|
|
773
|
+
label: "Processing on-chain tx",
|
|
774
|
+
reference: %(the Web3 Contest "Processing" on-chain load modal (style/modals/_onchain_tx via $store.dsSolanaModal) — holds the spinner at least MIN_LOAD_MS, then auto-resolves to success (or the error state when the toggle is set). Open with $store.dsModals.open('onchain-tx', { state: 'processing', demoResolve: true, demoError: false })),
|
|
775
|
+
card_data: "opts: { demoError: false }",
|
|
776
|
+
toggles: [{ model: "opts.demoError", label: "Resolve to error" }],
|
|
777
|
+
open_expr: "$store.dsModals.open('onchain-tx', { state: 'processing', title: 'Confirming on-chain', message: 'Waiting for the wallet signature…', demoResolve: true, demoError: opts.demoError })",
|
|
778
|
+
glow_when: ds_glow.call("onchain-tx", state: "processing"),
|
|
794
779
|
disabled: !web3_on, openable: true } do %>
|
|
795
780
|
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
796
781
|
<div class="loading-dots mx-auto"><span></span><span></span><span></span></div>
|
|
@@ -798,10 +783,12 @@
|
|
|
798
783
|
</div>
|
|
799
784
|
<% end %>
|
|
800
785
|
|
|
786
|
+
<%# 3. On-chain success — the resolved success end-state (the walk's finish). %>
|
|
801
787
|
<%= render layout: "style/modal_specimen", locals: {
|
|
802
|
-
label: "On-chain
|
|
803
|
-
reference: %(the Web3 "On-chain
|
|
788
|
+
label: "On-chain success",
|
|
789
|
+
reference: %(the Web3 Contest "On-chain success" state (style/modals/_onchain_tx + studio/modals/blocks/_onchain_success) — the resolved success card the walk lands on. Open with $store.dsModals.open('onchain-tx', { state: 'success', txSignature: '…', successTitle: 'Mint complete' })),
|
|
804
790
|
open_expr: "$store.dsModals.open('onchain-tx', { state: 'success', txSignature: '5xTrDemoSignature1234567890', successTitle: 'Mint complete', successSubtitle: 'Your on-chain action confirmed.', ctaLabel: 'View wallet', ctaHref: null })",
|
|
791
|
+
glow_when: ds_glow.call("onchain-tx", state: "success"),
|
|
805
792
|
disabled: !web3_on, openable: true } do %>
|
|
806
793
|
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
807
794
|
<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)">
|
|
@@ -811,10 +798,12 @@
|
|
|
811
798
|
</div>
|
|
812
799
|
<% end %>
|
|
813
800
|
|
|
801
|
+
<%# On-chain error — the resolved error end-state (the toggle's other branch). %>
|
|
814
802
|
<%= render layout: "style/modal_specimen", locals: {
|
|
815
|
-
label: "On-chain
|
|
816
|
-
reference: %(the Web3 "On-chain
|
|
803
|
+
label: "On-chain error",
|
|
804
|
+
reference: %(the Web3 Contest "On-chain error" state (style/modals/_onchain_tx) — the resolved error card the Processing toggle resolves to. Open with $store.dsModals.open('onchain-tx', { state: 'error', title: "Couldn't confirm", errorMessage: 'The transaction was rejected. Give it another try.' })),
|
|
817
805
|
open_expr: "$store.dsModals.open('onchain-tx', { state: 'error', title: 'Could not confirm', errorMessage: 'The transaction was rejected. Give it another try.' })",
|
|
806
|
+
glow_when: ds_glow.call("onchain-tx", state: "error"),
|
|
818
807
|
disabled: !web3_on, openable: true } do %>
|
|
819
808
|
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
820
809
|
<div class="text-2xl leading-none">⚠️</div>
|
|
@@ -822,9 +811,10 @@
|
|
|
822
811
|
</div>
|
|
823
812
|
<% end %>
|
|
824
813
|
|
|
814
|
+
<%# Wallet deposit — standalone top-up card (not part of the walk). %>
|
|
825
815
|
<%= render layout: "style/modal_specimen", locals: {
|
|
826
816
|
label: "Wallet deposit",
|
|
827
|
-
reference: %(the Web3 "Wallet deposit" top-up card (style/modals/_wallet_deposit) — open with $store.dsModals.open('wallet-deposit', { neededCents: 500, usdcCents: 120, usdtCents: 0, address: 'Fo1L5…' })),
|
|
817
|
+
reference: %(the Web3 Contest "Wallet deposit" top-up card (style/modals/_wallet_deposit) — open with $store.dsModals.open('wallet-deposit', { neededCents: 500, usdcCents: 120, usdtCents: 0, address: 'Fo1L5…' })),
|
|
828
818
|
open_expr: "$store.dsModals.open('wallet-deposit', { neededCents: 500, usdcCents: 120, usdtCents: 0, address: 'Fo1L5demoWALLETaddr9xQ2' })",
|
|
829
819
|
disabled: !web3_on, openable: true } do %>
|
|
830
820
|
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
@@ -840,7 +830,7 @@
|
|
|
840
830
|
Rewards below (gated by :leveling). %>
|
|
841
831
|
<%= render layout: "style/modal_specimen", locals: {
|
|
842
832
|
label: "Entry confirmed",
|
|
843
|
-
reference: %(the Web3 "Entry confirmed" celebration (studio/modals/blocks/_entry_confirmed, composed via _success_card's yield — branded tx link + heading + drain CTA; the seeds bar is gated off by :leveling and stays hidden here) — open with $store.dsModals.open('entry-confirmed', { txSignature: '…', lobbyUrl: null, seedsEarned: 0 })),
|
|
833
|
+
reference: %(the Web3 Contest "Entry confirmed" celebration (studio/modals/blocks/_entry_confirmed, composed via _success_card's yield — branded tx link + heading + drain CTA; the seeds bar is gated off by :leveling and stays hidden here) — open with $store.dsModals.open('entry-confirmed', { txSignature: '…', lobbyUrl: null, seedsEarned: 0 })),
|
|
844
834
|
open_expr: "$store.dsModals.open('entry-confirmed', { txSignature: '5xTrDemoSignature1234567890abcXYZ', lobbyUrl: null, seedsEarned: 0, seedsTotal: 40 })",
|
|
845
835
|
disabled: !web3_on, openable: true } do %>
|
|
846
836
|
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
@@ -855,6 +845,144 @@
|
|
|
855
845
|
</div>
|
|
856
846
|
</section>
|
|
857
847
|
|
|
848
|
+
<%# ===================================================================== %>
|
|
849
|
+
<%# 3. CONTEST ENTRY & ELIGIBILITY — moved directly under Web3 Contest. The %>
|
|
850
|
+
<%# walked entry flow (glow follows), plus the honest web2/web3 map. %>
|
|
851
|
+
<%# ===================================================================== %>
|
|
852
|
+
<section class="space-y-5">
|
|
853
|
+
<div class="space-y-1">
|
|
854
|
+
<div class="flex flex-wrap items-center gap-3">
|
|
855
|
+
<h3 class="text-xl font-bold text-heading">Contest entry & eligibility</h3>
|
|
856
|
+
<% if age_gate_on %>
|
|
857
|
+
<span class="badge" style="color: var(--color-success); border-color: var(--color-success)">age gate on</span>
|
|
858
|
+
<% else %>
|
|
859
|
+
<span class="badge" style="color: var(--color-warning); border-color: var(--color-warning)">age gate off</span>
|
|
860
|
+
<% end %>
|
|
861
|
+
</div>
|
|
862
|
+
<p class="text-muted text-sm">
|
|
863
|
+
The pre-entry gate and the full entry flow. The <strong>age gate</strong>
|
|
864
|
+
(<code class="font-mono text-2xs">studio/modals/blocks/_age_verify</code>) is an
|
|
865
|
+
engine primitive gated by
|
|
866
|
+
<code class="font-mono text-2xs">Studio.feature?(:age_gate)</code> — a DOB
|
|
867
|
+
modal whose minimum age, jurisdiction label, endpoint, and legal copy are all
|
|
868
|
+
<strong>app-supplied</strong> (the engine hardcodes no policy). The entry flow is
|
|
869
|
+
a <strong>walked sequence</strong> — <strong>Entry tokens → Payment
|
|
870
|
+
processing → Entry Tokens Minted → Contest enter processing →
|
|
871
|
+
Contest entered</strong> — and the live card <strong>glows</strong>,
|
|
872
|
+
following the step machine (the two load steps honor the same min-duration
|
|
873
|
+
convention as Web3 Contest above).
|
|
874
|
+
</p>
|
|
875
|
+
<%# The web2/web3 finding — a real divergence, stated honestly, not invented. %>
|
|
876
|
+
<div class="mt-2 rounded-lg border border-subtle bg-surface p-3 text-xs text-secondary leading-snug">
|
|
877
|
+
<p class="text-heading font-semibold mb-1">Web2 vs web3 — the honest map</p>
|
|
878
|
+
<p class="mb-1">
|
|
879
|
+
The two funding paths <strong>diverge at the front-end and the entry-submit
|
|
880
|
+
endpoint, then converge</strong>. <strong>Web2</strong> (card / Stripe /
|
|
881
|
+
Coinflow / PayPal) mints one prepaid entry token per credit on-chain after
|
|
882
|
+
payment clears, then <strong>consumes</strong> one token to enter.
|
|
883
|
+
<strong>Web3</strong> (wallet / USDC on-chain) funds USDC directly and
|
|
884
|
+
<strong>mints no token</strong> — the mint → consume prelude
|
|
885
|
+
is web2-only.
|
|
886
|
+
</p>
|
|
887
|
+
<p>
|
|
888
|
+
Both create the <strong>same on-chain Entry PDA</strong> and resolve to the
|
|
889
|
+
<strong>same “Entry Confirmed” card</strong>. The walk below demos the web2
|
|
890
|
+
path end-to-end; web3 skips the mint and joins at
|
|
891
|
+
<strong>enter → entered</strong>.
|
|
892
|
+
</p>
|
|
893
|
+
</div>
|
|
894
|
+
</div>
|
|
895
|
+
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
896
|
+
<%# Age gate — engine primitive, gated on :age_gate (disabled-but-present). %>
|
|
897
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
898
|
+
label: "Age gate (DOB)",
|
|
899
|
+
reference: %(the "Age gate" DOB modal (studio-engine studio/modals/blocks/_age_verify) — an engine primitive; the app supplies min_age / state / submit_url / fine_print and the engine hardcodes no legal policy. In this demo, Confirm & Continue advances to the Entry tokens purchase (page-level dsModals wiring on the primitive's 'age-verified' hook; the primitive's real submit posts to submit_url). Open with $store.dsModals.open('age-verify')),
|
|
900
|
+
open_expr: "$store.dsModals.open('age-verify')",
|
|
901
|
+
glow_when: ds_glow.call("age-verify"),
|
|
902
|
+
disabled: !age_gate_on, openable: true, disabled_label: "age gate off" } do %>
|
|
903
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2 text-center">
|
|
904
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
905
|
+
<div class="grid grid-cols-3 gap-1">
|
|
906
|
+
<span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
907
|
+
<span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
908
|
+
<span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
909
|
+
</div>
|
|
910
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
911
|
+
</div>
|
|
912
|
+
<% end %>
|
|
913
|
+
|
|
914
|
+
<%# 1. Entry tokens — the picker (walk's entry). Picking a pack advances. %>
|
|
915
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
916
|
+
label: "Entry tokens",
|
|
917
|
+
reference: %(the "Entry tokens" picker (app-specific — documented composing studio/modals/blocks chrome). The packs, pricing, rails, and on-chain mint stay app-owned. Picking a pack walks Payment processing -> Minted -> enter -> entered. Open with $store.dsModals.open('entry-tokens', { step: 'picker' })),
|
|
918
|
+
open_expr: "$store.dsModals.open('entry-tokens', { step: 'picker' })",
|
|
919
|
+
glow_when: ds_glow.call("entry-tokens", step: "picker", step_default: "picker") } do %>
|
|
920
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
921
|
+
<span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
922
|
+
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
923
|
+
<span class="block h-6 w-full rounded border" style="border-color: var(--color-primary)"></span>
|
|
924
|
+
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
925
|
+
</div>
|
|
926
|
+
<% end %>
|
|
927
|
+
|
|
928
|
+
<%# 2. Payment processing — the mint-at-purchase load step (web2). %>
|
|
929
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
930
|
+
label: "Payment processing",
|
|
931
|
+
reference: %(the "Payment processing" step (entry-tokens step 'confirming') — "Confirming your purchase…" over "Payment cleared — a server job mints one prepaid entry credit per token on-chain." A load modal; honors the min-duration convention then auto-advances to Entry Tokens Minted. Open with $store.dsModals.open('entry-tokens', { step: 'confirming', mintedCount: 3, mintedBalance: '3' })),
|
|
932
|
+
open_expr: "$store.dsModals.open('entry-tokens', { step: 'confirming', mintedCount: 3, mintedBalance: '3' })",
|
|
933
|
+
glow_when: ds_glow.call("entry-tokens", step: "confirming", step_default: "picker") } do %>
|
|
934
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
935
|
+
<div class="loading-dots mx-auto"><span></span><span></span><span></span></div>
|
|
936
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
937
|
+
<span class="block h-1.5 w-16 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
|
|
938
|
+
</div>
|
|
939
|
+
<% end %>
|
|
940
|
+
|
|
941
|
+
<%# 3. Entry Tokens Minted — ready to consume at entry (hold to confirm). %>
|
|
942
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
943
|
+
label: "Entry Tokens Minted",
|
|
944
|
+
reference: %(the "Entry Tokens Minted" step (entry-tokens step 'minted') — "You now have N available." Holding to confirm consumes one token on-chain to create the contest entry (advances to Contest enter processing). Open with $store.dsModals.open('entry-tokens', { step: 'minted', mintedCount: 3, mintedBalance: '3' })),
|
|
945
|
+
open_expr: "$store.dsModals.open('entry-tokens', { step: 'minted', mintedCount: 3, mintedBalance: '3' })",
|
|
946
|
+
glow_when: ds_glow.call("entry-tokens", step: "minted", step_default: "picker") } do %>
|
|
947
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
948
|
+
<div class="mx-auto w-7 h-7 rounded-full flex items-center justify-center" style="background: color-mix(in srgb, var(--color-primary) 15%, transparent)">
|
|
949
|
+
<span class="text-sm font-bold" style="color: var(--color-primary)">3</span>
|
|
950
|
+
</div>
|
|
951
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
952
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
953
|
+
</div>
|
|
954
|
+
<% end %>
|
|
955
|
+
|
|
956
|
+
<%# 4. Contest enter processing — the on-chain consume load step. %>
|
|
957
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
958
|
+
label: "Contest enter processing",
|
|
959
|
+
reference: %(the "Contest enter processing" step (entry-tokens step 'entering') — "Confirming your entry…" over "Consuming one token on-chain to create your contest entry." A load modal; honors the min-duration convention then auto-advances to Contest entered. Open with $store.dsModals.open('entry-tokens', { step: 'entering' })),
|
|
960
|
+
open_expr: "$store.dsModals.open('entry-tokens', { step: 'entering' })",
|
|
961
|
+
glow_when: ds_glow.call("entry-tokens", step: "entering", step_default: "picker") } do %>
|
|
962
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
963
|
+
<div class="loading-dots mx-auto"><span></span><span></span><span></span></div>
|
|
964
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
965
|
+
<span class="block h-1.5 w-16 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
|
|
966
|
+
</div>
|
|
967
|
+
<% end %>
|
|
968
|
+
|
|
969
|
+
<%# 5. Contest entered — the shared Entry Confirmed finish (web2 + web3 converge). %>
|
|
970
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
971
|
+
label: "Contest entered",
|
|
972
|
+
reference: %(the "Contest entered" success (entry-tokens step 'entered') — "Good Luck / Entry Confirmed" with the branded on-chain tx link and a Contest Lobby CTA. The SAME finish both web2 and web3 entries converge on. Open with $store.dsModals.open('entry-tokens', { step: 'entered', txSignature: '…' })),
|
|
973
|
+
open_expr: "$store.dsModals.open('entry-tokens', { step: 'entered', txSignature: '5xTrDemoEntrySignature1234567890abc' })",
|
|
974
|
+
glow_when: ds_glow.call("entry-tokens", step: "entered", step_default: "picker") } do %>
|
|
975
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
976
|
+
<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)">
|
|
977
|
+
<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>
|
|
978
|
+
</div>
|
|
979
|
+
<span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
980
|
+
<span class="block h-4 w-full rounded" style="background: var(--color-cta)"></span>
|
|
981
|
+
</div>
|
|
982
|
+
<% end %>
|
|
983
|
+
</div>
|
|
984
|
+
</section>
|
|
985
|
+
|
|
858
986
|
<%# ===================================================================== %>
|
|
859
987
|
<%# 5. SYSTEM AND STATUS (reusable card blocks) %>
|
|
860
988
|
<%# ===================================================================== %>
|
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
<%#
|
|
2
|
-
Living style guide specimen — the entry
|
|
3
|
-
APP-SPECIFIC flow (Turf Monster's "Get Entry Tokens");
|
|
4
|
-
does NOT own the packs, pricing,
|
|
5
|
-
engine CHROME (studio/modals/blocks/
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
Living style guide specimen — the contest entry & eligibility flow. This
|
|
3
|
+
documents an APP-SPECIFIC flow (Turf Monster's "Get Entry Tokens" → enter);
|
|
4
|
+
the engine deliberately does NOT own the packs, pricing, payment rails, or any
|
|
5
|
+
on-chain write. The specimen composes only engine CHROME (studio/modals/blocks/
|
|
6
|
+
shell, card_header, progress_pill, solana_tx_link) and represents the packs and
|
|
7
|
+
the on-chain moments as an illustrative demo — the app owns the money + signing.
|
|
8
8
|
|
|
9
9
|
Opened on the page-scoped dsModals host with an internal step machine on
|
|
10
|
-
props.step
|
|
10
|
+
props.step, the full web2 walk:
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
picker -> confirming -> minted -> entering -> entered
|
|
13
|
+
|
|
14
|
+
Single-root templates throughout. The two load steps (confirming, entering)
|
|
15
|
+
honor the minimum-visible-duration convention (studio/modals/_load_convention):
|
|
16
|
+
the spinner holds at least MIN_LOAD_MS, then auto-advances (demo mode) —
|
|
17
|
+
resolveAt = max(min_duration, actual_completion).
|
|
18
|
+
|
|
19
|
+
On-chain model (documented, not owned here). An entry token is a PROGRAM-OWNED
|
|
20
|
+
prepaid entry credit recorded on-chain (a per-token account), NOT a fungible SPL
|
|
21
|
+
token in the user's wallet. WEB2 (card / Stripe / Coinflow / PayPal) has two
|
|
22
|
+
on-chain moments: (1) MINT at purchase, server-signed after the payment clears —
|
|
23
|
+
not on the buyer's click; (2) CONSUME at entry, when the hold-to-confirm gesture
|
|
24
|
+
spends one token to create the contest entry. WEB3 (wallet / USDC on-chain)
|
|
25
|
+
funds USDC directly and mints NO token — so the mint→consume prelude is
|
|
26
|
+
web2-only. Both paths converge at the SAME on-chain Entry PDA and the SAME
|
|
27
|
+
"Entry Confirmed" card.
|
|
18
28
|
%>
|
|
19
29
|
<%
|
|
20
30
|
# Illustrative packs only — a real app reads these from its own config
|
|
@@ -29,10 +39,20 @@
|
|
|
29
39
|
get props() { var c = Alpine.store('dsModals').current(); return (c && c.props) || {}; },
|
|
30
40
|
buy(count, balance) {
|
|
31
41
|
Alpine.store('dsModals').advance({ step: 'confirming', mintedCount: count, mintedBalance: balance });
|
|
32
|
-
|
|
42
|
+
},
|
|
43
|
+
confirmEntry() {
|
|
44
|
+
Alpine.store('dsModals').advance({ step: 'entering' });
|
|
45
|
+
},
|
|
46
|
+
// Demo auto-resolve honoring the min-load convention: hold the spinner at
|
|
47
|
+
// least MIN_LOAD_MS, then advance — but only while this modal is still on
|
|
48
|
+
// the given step (guards against an early close / manual advance).
|
|
49
|
+
autoAdvance(fromStep, patch) {
|
|
50
|
+
window.StudioModals.holdAtLeast(window.StudioModals.MIN_LOAD_MS).then(function () {
|
|
33
51
|
var c = Alpine.store('dsModals').current();
|
|
34
|
-
if (c && c.id === 'entry-tokens'
|
|
35
|
-
|
|
52
|
+
if (c && c.id === 'entry-tokens' && c.props.step === fromStep) {
|
|
53
|
+
Alpine.store('dsModals').advance(patch);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
36
56
|
}
|
|
37
57
|
}" class="relative">
|
|
38
58
|
|
|
@@ -68,36 +88,52 @@
|
|
|
68
88
|
<% end %>
|
|
69
89
|
</div>
|
|
70
90
|
|
|
71
|
-
<%# Rails —
|
|
72
|
-
The USDC-direct path is intentionally NOT a pack here (it's a separate
|
|
73
|
-
rail that transfers USDC and mints no token). %>
|
|
91
|
+
<%# Rails — which funding path mints tokens vs funds an entry directly. %>
|
|
74
92
|
<div class="mt-4 flex flex-wrap items-center justify-center gap-x-4 gap-y-1 text-xs text-muted">
|
|
75
93
|
<span>Pay by card</span>
|
|
76
94
|
<span aria-hidden="true">·</span>
|
|
77
95
|
<span>PayPal / Venmo</span>
|
|
96
|
+
<span aria-hidden="true">·</span>
|
|
97
|
+
<span>Coinflow</span>
|
|
78
98
|
</div>
|
|
79
|
-
<p class="text-center text-2xs text-muted mt-1">
|
|
99
|
+
<p class="text-center text-2xs text-muted mt-1">
|
|
100
|
+
These are the <strong>web2</strong> rails — they mint prepaid entry tokens.
|
|
101
|
+
The <strong>web3</strong> path funds USDC on-chain directly and mints no token.
|
|
102
|
+
</p>
|
|
80
103
|
|
|
81
|
-
<%# The seam callout — what the engine owns vs
|
|
104
|
+
<%# The seam callout — what the engine owns vs the app, plus the honest
|
|
105
|
+
web2/web3 map (a real divergence, not invented). %>
|
|
82
106
|
<div class="mt-4 rounded-lg border border-subtle bg-surface p-3 text-2xs text-secondary leading-snug">
|
|
83
107
|
<p class="text-heading font-semibold mb-1">App-specific flow, engine chrome</p>
|
|
108
|
+
<p class="mb-2">
|
|
109
|
+
The engine supplies only the shell, header, progress pill, and tx link. The
|
|
110
|
+
packs, pricing, payment rails, and every on-chain write are
|
|
111
|
+
<strong>app-owned</strong> — this specimen represents them as a demo. On-chain,
|
|
112
|
+
an entry token is a <strong>prepaid entry credit recorded on-chain</strong>
|
|
113
|
+
(a per-token account), not a wallet-held token.
|
|
114
|
+
</p>
|
|
115
|
+
<p class="text-heading font-semibold mb-1">Web2 vs web3 — where they diverge and converge</p>
|
|
84
116
|
<p class="mb-1">
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
117
|
+
<strong>Web2</strong> (card / Stripe / Coinflow / PayPal): a server job mints
|
|
118
|
+
one prepaid entry credit per token on-chain once payment clears, then
|
|
119
|
+
hold-to-confirm <strong>consumes</strong> one token to create the entry.
|
|
120
|
+
</p>
|
|
121
|
+
<p class="mb-1">
|
|
122
|
+
<strong>Web3</strong> (wallet / USDC on-chain): funds USDC directly and
|
|
123
|
+
<strong>mints no token</strong> — the mint → consume prelude is web2-only.
|
|
88
124
|
</p>
|
|
89
125
|
<p>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
126
|
+
Both paths converge on the <strong>same on-chain Entry PDA</strong> and the
|
|
127
|
+
<strong>same “Entry Confirmed” card</strong> — divergent funding front-end,
|
|
128
|
+
one shared entry → entered finish.
|
|
93
129
|
</p>
|
|
94
130
|
</div>
|
|
95
131
|
</div>
|
|
96
132
|
</template>
|
|
97
133
|
|
|
98
|
-
<%# ===== CONFIRMING (mint-at-purchase) ===== %>
|
|
134
|
+
<%# ===== CONFIRMING (payment processing — mint-at-purchase) ===== %>
|
|
99
135
|
<template x-if="props.step === 'confirming'">
|
|
100
|
-
<div x-init="
|
|
136
|
+
<div x-init="autoAdvance('confirming', { step: 'minted' })">
|
|
101
137
|
<%= render "studio/modals/blocks/card_header",
|
|
102
138
|
spinner: true,
|
|
103
139
|
title: "Confirming your purchase…",
|
|
@@ -114,10 +150,39 @@
|
|
|
114
150
|
You now have <span class="font-bold text-primary font-mono" x-text="props.mintedBalance"></span> available.
|
|
115
151
|
<% end %>
|
|
116
152
|
<p class="text-xs text-secondary text-center mb-4 leading-snug">
|
|
117
|
-
|
|
118
|
-
|
|
153
|
+
Holding to confirm now <strong>consumes one token on-chain</strong> to create
|
|
154
|
+
your contest entry.
|
|
119
155
|
</p>
|
|
120
|
-
<button @click="
|
|
156
|
+
<button @click="confirmEntry()" class="btn btn-primary btn-lg w-full">Hold to Confirm Entry</button>
|
|
157
|
+
<button @click="$store.dsModals.close()"
|
|
158
|
+
class="block mx-auto mt-3 text-xs text-secondary hover:text-heading underline underline-offset-2">
|
|
159
|
+
I’ll confirm later
|
|
160
|
+
</button>
|
|
161
|
+
</div>
|
|
162
|
+
</template>
|
|
163
|
+
|
|
164
|
+
<%# ===== ENTERING (contest enter processing — the on-chain consume) ===== %>
|
|
165
|
+
<template x-if="props.step === 'entering'">
|
|
166
|
+
<div x-init="autoAdvance('entering', { step: 'entered', txSignature: '5xTrDemoEntrySignature1234567890abc' })">
|
|
167
|
+
<%= render "studio/modals/blocks/card_header",
|
|
168
|
+
spinner: true,
|
|
169
|
+
title: "Confirming your entry…",
|
|
170
|
+
subtitle: "Consuming one token on-chain to create your contest entry." %>
|
|
171
|
+
</div>
|
|
172
|
+
</template>
|
|
173
|
+
|
|
174
|
+
<%# ===== ENTERED (contest entered — the shared Entry Confirmed finish) ===== %>
|
|
175
|
+
<template x-if="props.step === 'entered'">
|
|
176
|
+
<div>
|
|
177
|
+
<%= render "studio/modals/blocks/card_header",
|
|
178
|
+
size: :lg, icon_color: 'primary',
|
|
179
|
+
title: "Good Luck", subtitle: "Entry Confirmed" %>
|
|
180
|
+
<%= render "studio/modals/blocks/solana_tx_link", tx_signature_key: "props.txSignature" %>
|
|
181
|
+
<button @click="$store.dsModals.close()" class="btn btn-primary btn-lg w-full">Contest Lobby ⚔️</button>
|
|
182
|
+
<button @click="$store.dsModals.close()"
|
|
183
|
+
class="block mx-auto mt-3 text-sm text-secondary hover:text-heading transition">
|
|
184
|
+
Dismiss
|
|
185
|
+
</button>
|
|
121
186
|
</div>
|
|
122
187
|
</template>
|
|
123
188
|
</div>
|
|
@@ -24,6 +24,30 @@
|
|
|
24
24
|
}, 100);
|
|
25
25
|
}
|
|
26
26
|
});
|
|
27
|
+
// Demo auto-resolve (min-load convention): a processing modal opened with
|
|
28
|
+
// demoResolve holds its spinner at least MIN_LOAD_MS, then advances to
|
|
29
|
+
// success — or to error when demoError is set (the Processing specimen's
|
|
30
|
+
// success/error toggle). Guarded to the still-open, still-processing modal.
|
|
31
|
+
var entry = Alpine.store('dsModals').current();
|
|
32
|
+
if (!entry || entry.id !== 'onchain-tx' || !entry.props ||
|
|
33
|
+
entry.props.state !== 'processing' || !entry.props.demoResolve) return;
|
|
34
|
+
var minMs = entry.props.minDuration ||
|
|
35
|
+
(window.StudioModals && window.StudioModals.MIN_LOAD_MS) || 1400;
|
|
36
|
+
window.StudioModals.holdAtLeast(minMs).then(function () {
|
|
37
|
+
var cur = Alpine.store('dsModals').current();
|
|
38
|
+
if (!cur || cur.id !== 'onchain-tx' || cur.props.state !== 'processing') return;
|
|
39
|
+
if (entry.props.demoError) {
|
|
40
|
+
Alpine.store('dsModals').advance({ state: 'error',
|
|
41
|
+
title: 'Could not confirm',
|
|
42
|
+
errorMessage: 'The transaction was rejected. Give it another try.' });
|
|
43
|
+
} else {
|
|
44
|
+
Alpine.store('dsModals').advance({ state: 'success',
|
|
45
|
+
txSignature: '5xTrDemoSignature1234567890abcXYZ',
|
|
46
|
+
successTitle: 'Mint complete',
|
|
47
|
+
successSubtitle: 'Your on-chain action confirmed.',
|
|
48
|
+
ctaLabel: 'View wallet', ctaHref: null });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
27
51
|
}
|
|
28
52
|
}">
|
|
29
53
|
|
|
@@ -60,7 +60,18 @@
|
|
|
60
60
|
if (!this.attested()) return;
|
|
61
61
|
this.connecting = true; this.picking = name; this.error = '';
|
|
62
62
|
var self = this;
|
|
63
|
-
|
|
63
|
+
// DEMO walk continuity: a picked wallet runs a brief connecting state,
|
|
64
|
+
// then swaps to the on-chain tx modal in its processing state, which
|
|
65
|
+
// auto-resolves to on-chain success (demoResolve). So the glow follows
|
|
66
|
+
// Connect Wallet -> Processing -> On-chain success. (No real connect.)
|
|
67
|
+
setTimeout(function () {
|
|
68
|
+
self.connecting = false; self.picking = '';
|
|
69
|
+
Alpine.store('dsModals').swap('onchain-tx', {
|
|
70
|
+
state: 'processing', demoResolve: true, demoError: false,
|
|
71
|
+
title: 'Confirming on-chain',
|
|
72
|
+
message: 'Waiting for the wallet signature…'
|
|
73
|
+
});
|
|
74
|
+
}, 1100);
|
|
64
75
|
},
|
|
65
76
|
deepLink() {
|
|
66
77
|
if (!this.attested()) return;
|
data/lib/studio/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: studio-engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.27.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex McRitchie
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -273,6 +273,7 @@ files:
|
|
|
273
273
|
- app/views/studio/modals/_crop_photo.html.erb
|
|
274
274
|
- app/views/studio/modals/_host.html.erb
|
|
275
275
|
- app/views/studio/modals/_image_upload.html.erb
|
|
276
|
+
- app/views/studio/modals/_load_convention.html.erb
|
|
276
277
|
- app/views/studio/modals/_saving.html.erb
|
|
277
278
|
- app/views/studio/modals/auth/_resend_footer.html.erb
|
|
278
279
|
- app/views/studio/modals/blocks/_age_verify.html.erb
|