studio-engine 0.26.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 +49 -0
- 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/_modals.html.erb +201 -88
- 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 +2 -1
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,55 @@
|
|
|
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
|
+
|
|
5
54
|
## 0.26.1 — 2026-07-28
|
|
6
55
|
|
|
7
56
|
**Fix the `/admin/style` Design System page breaking when reached via Turbo Drive
|
|
@@ -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>
|
|
@@ -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
|
|
@@ -263,6 +267,13 @@
|
|
|
263
267
|
})();
|
|
264
268
|
</script>
|
|
265
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
|
+
|
|
266
277
|
<%# Cropper.js + the cropPhotoModal factory — makes the Profile crop / upload
|
|
267
278
|
modals openable on this page (loads cropper.js via <script defer> and
|
|
268
279
|
defines the factory inline; no importmap change). %>
|
|
@@ -310,8 +321,11 @@
|
|
|
310
321
|
<template x-if="$store.dsModals.current().id === 'crop-photo'">
|
|
311
322
|
<div><%= render "studio/modals/crop_photo", store: "dsModals" %></div>
|
|
312
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). %>
|
|
313
326
|
<template x-if="$store.dsModals.current().id === 'saving'">
|
|
314
|
-
<div><%= render "studio/modals/saving", store: "dsModals"
|
|
327
|
+
<div><%= render "studio/modals/saving", store: "dsModals",
|
|
328
|
+
resolve_expr: "$store.dsModals.close()" %></div>
|
|
315
329
|
</template>
|
|
316
330
|
|
|
317
331
|
<%# --- Web3 --- %>
|
|
@@ -335,9 +349,12 @@
|
|
|
335
349
|
</template>
|
|
336
350
|
|
|
337
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. %>
|
|
338
354
|
<template x-if="$store.dsModals.current().id === 'ds-processing'">
|
|
339
355
|
<div><%= render "studio/modals/blocks/processing_card",
|
|
340
|
-
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>
|
|
341
358
|
</template>
|
|
342
359
|
<template x-if="$store.dsModals.current().id === 'ds-success'">
|
|
343
360
|
<div><%= render "studio/modals/blocks/success_card",
|
|
@@ -523,68 +540,6 @@
|
|
|
523
540
|
</div>
|
|
524
541
|
</section>
|
|
525
542
|
|
|
526
|
-
<%# ===================================================================== %>
|
|
527
|
-
<%# 1b. ELIGIBILITY + ENTRY %>
|
|
528
|
-
<%# ===================================================================== %>
|
|
529
|
-
<section class="space-y-5">
|
|
530
|
-
<div class="space-y-1">
|
|
531
|
-
<div class="flex flex-wrap items-center gap-3">
|
|
532
|
-
<h3 class="text-xl font-bold text-heading">Eligibility & entry</h3>
|
|
533
|
-
<% if age_gate_on %>
|
|
534
|
-
<span class="badge" style="color: var(--color-success); border-color: var(--color-success)">age gate on</span>
|
|
535
|
-
<% else %>
|
|
536
|
-
<span class="badge" style="color: var(--color-warning); border-color: var(--color-warning)">age gate off</span>
|
|
537
|
-
<% end %>
|
|
538
|
-
</div>
|
|
539
|
-
<p class="text-muted text-sm">
|
|
540
|
-
The pre-entry gate and the entry-purchase flow. The <strong>age gate</strong>
|
|
541
|
-
(<code class="font-mono text-2xs">studio/modals/blocks/_age_verify</code>) is a
|
|
542
|
-
NEW engine primitive gated by
|
|
543
|
-
<code class="font-mono text-2xs">Studio.feature?(:age_gate)</code> — a DOB
|
|
544
|
-
modal whose minimum age, jurisdiction label, endpoint, and legal copy are all
|
|
545
|
-
<strong>app-supplied</strong> (the engine hardcodes no policy). It is the heavier
|
|
546
|
-
sibling of the <code class="font-mono text-2xs">shared/_age_attestation</code>
|
|
547
|
-
checkbox inside Sign in above — <strong>run one or the other</strong>. The
|
|
548
|
-
<strong>entry-tokens</strong> flow is <strong>app-specific</strong> (payments
|
|
549
|
-
product): documented here composing engine chrome, but the packs, rails, and
|
|
550
|
-
the on-chain mint stay app-owned.
|
|
551
|
-
</p>
|
|
552
|
-
</div>
|
|
553
|
-
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
554
|
-
<%# Age gate — engine primitive, gated on :age_gate (disabled-but-present). %>
|
|
555
|
-
<%= render layout: "style/modal_specimen", locals: {
|
|
556
|
-
label: "Age gate (DOB)",
|
|
557
|
-
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')),
|
|
558
|
-
open_expr: "$store.dsModals.open('age-verify')",
|
|
559
|
-
glow_when: ds_glow.call("age-verify"),
|
|
560
|
-
disabled: !age_gate_on, openable: true, disabled_label: "age gate off" } do %>
|
|
561
|
-
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2 text-center">
|
|
562
|
-
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
563
|
-
<div class="grid grid-cols-3 gap-1">
|
|
564
|
-
<span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
565
|
-
<span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
566
|
-
<span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
567
|
-
</div>
|
|
568
|
-
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
569
|
-
</div>
|
|
570
|
-
<% end %>
|
|
571
|
-
|
|
572
|
-
<%# Entry tokens — app-specific flow, documented (not capability-gated). %>
|
|
573
|
-
<%= render layout: "style/modal_specimen", locals: {
|
|
574
|
-
label: "Entry tokens",
|
|
575
|
-
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' })),
|
|
576
|
-
open_expr: "$store.dsModals.open('entry-tokens', { step: 'picker' })",
|
|
577
|
-
glow_when: ds_glow.call("entry-tokens") } do %>
|
|
578
|
-
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
579
|
-
<span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
580
|
-
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
581
|
-
<span class="block h-6 w-full rounded border" style="border-color: var(--color-primary)"></span>
|
|
582
|
-
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
583
|
-
</div>
|
|
584
|
-
<% end %>
|
|
585
|
-
</div>
|
|
586
|
-
</section>
|
|
587
|
-
|
|
588
543
|
<%# ===================================================================== %>
|
|
589
544
|
<%# 2. PROFILE — crop / upload, live via cropper_assets %>
|
|
590
545
|
<%# ===================================================================== %>
|
|
@@ -766,12 +721,13 @@
|
|
|
766
721
|
</section>
|
|
767
722
|
|
|
768
723
|
<%# ===================================================================== %>
|
|
769
|
-
<%#
|
|
724
|
+
<%# 2. WEB3 CONTEST — the walked wallet -> on-chain flow (glow follows it). %>
|
|
725
|
+
<%# Gated by :web3; off = disabled-but-present-yet-openable. %>
|
|
770
726
|
<%# ===================================================================== %>
|
|
771
727
|
<section class="space-y-5">
|
|
772
728
|
<div class="space-y-1">
|
|
773
729
|
<div class="flex flex-wrap items-center gap-3">
|
|
774
|
-
<h3 class="text-xl font-bold text-heading">Web3</h3>
|
|
730
|
+
<h3 class="text-xl font-bold text-heading">Web3 Contest</h3>
|
|
775
731
|
<% if web3_on %>
|
|
776
732
|
<span class="badge" style="color: var(--color-success); border-color: var(--color-success)">enabled on this app</span>
|
|
777
733
|
<% else %>
|
|
@@ -779,21 +735,31 @@
|
|
|
779
735
|
<% end %>
|
|
780
736
|
</div>
|
|
781
737
|
<p class="text-muted text-sm">
|
|
782
|
-
|
|
738
|
+
The wallet + on-chain modals, ported into the engine and gated by
|
|
783
739
|
<code class="font-mono text-2xs">Studio.feature?(:web3)</code>. Web3 is
|
|
784
740
|
<strong><%= web3_on ? "on" : "off" %></strong> here, so these render
|
|
785
741
|
<%= web3_on ? "live." : "greyed and badged — but STILL openable as a preview." %>
|
|
786
|
-
The
|
|
787
|
-
|
|
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>
|
|
788
752
|
(<code class="font-mono text-2xs">studio/modals/blocks/_wallet_brand_sprite</code>),
|
|
789
753
|
so every app inherits them with no per-app icon files.
|
|
790
754
|
</p>
|
|
791
755
|
</div>
|
|
792
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. %>
|
|
793
758
|
<%= render layout: "style/modal_specimen", locals: {
|
|
794
759
|
label: "Connect wallet",
|
|
795
|
-
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')),
|
|
796
761
|
open_expr: "$store.dsModals.open('wallet-connect')",
|
|
762
|
+
glow_when: ds_glow.call("wallet-connect"),
|
|
797
763
|
disabled: !web3_on, openable: true } do %>
|
|
798
764
|
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
799
765
|
<span class="block h-2 w-20 rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
@@ -802,10 +768,14 @@
|
|
|
802
768
|
</div>
|
|
803
769
|
<% end %>
|
|
804
770
|
|
|
771
|
+
<%# 2. Processing — the load modal; the success/error toggle picks the resolution. %>
|
|
805
772
|
<%= render layout: "style/modal_specimen", locals: {
|
|
806
|
-
label: "
|
|
807
|
-
reference: %(the Web3 "
|
|
808
|
-
|
|
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"),
|
|
809
779
|
disabled: !web3_on, openable: true } do %>
|
|
810
780
|
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
811
781
|
<div class="loading-dots mx-auto"><span></span><span></span><span></span></div>
|
|
@@ -813,10 +783,12 @@
|
|
|
813
783
|
</div>
|
|
814
784
|
<% end %>
|
|
815
785
|
|
|
786
|
+
<%# 3. On-chain success — the resolved success end-state (the walk's finish). %>
|
|
816
787
|
<%= render layout: "style/modal_specimen", locals: {
|
|
817
|
-
label: "On-chain
|
|
818
|
-
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' })),
|
|
819
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"),
|
|
820
792
|
disabled: !web3_on, openable: true } do %>
|
|
821
793
|
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
822
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)">
|
|
@@ -826,10 +798,12 @@
|
|
|
826
798
|
</div>
|
|
827
799
|
<% end %>
|
|
828
800
|
|
|
801
|
+
<%# On-chain error — the resolved error end-state (the toggle's other branch). %>
|
|
829
802
|
<%= render layout: "style/modal_specimen", locals: {
|
|
830
|
-
label: "On-chain
|
|
831
|
-
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.' })),
|
|
832
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"),
|
|
833
807
|
disabled: !web3_on, openable: true } do %>
|
|
834
808
|
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
835
809
|
<div class="text-2xl leading-none">⚠️</div>
|
|
@@ -837,9 +811,10 @@
|
|
|
837
811
|
</div>
|
|
838
812
|
<% end %>
|
|
839
813
|
|
|
814
|
+
<%# Wallet deposit — standalone top-up card (not part of the walk). %>
|
|
840
815
|
<%= render layout: "style/modal_specimen", locals: {
|
|
841
816
|
label: "Wallet deposit",
|
|
842
|
-
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…' })),
|
|
843
818
|
open_expr: "$store.dsModals.open('wallet-deposit', { neededCents: 500, usdcCents: 120, usdtCents: 0, address: 'Fo1L5demoWALLETaddr9xQ2' })",
|
|
844
819
|
disabled: !web3_on, openable: true } do %>
|
|
845
820
|
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
@@ -855,7 +830,7 @@
|
|
|
855
830
|
Rewards below (gated by :leveling). %>
|
|
856
831
|
<%= render layout: "style/modal_specimen", locals: {
|
|
857
832
|
label: "Entry confirmed",
|
|
858
|
-
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 })),
|
|
859
834
|
open_expr: "$store.dsModals.open('entry-confirmed', { txSignature: '5xTrDemoSignature1234567890abcXYZ', lobbyUrl: null, seedsEarned: 0, seedsTotal: 40 })",
|
|
860
835
|
disabled: !web3_on, openable: true } do %>
|
|
861
836
|
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
@@ -870,6 +845,144 @@
|
|
|
870
845
|
</div>
|
|
871
846
|
</section>
|
|
872
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
|
+
|
|
873
986
|
<%# ===================================================================== %>
|
|
874
987
|
<%# 5. SYSTEM AND STATUS (reusable card blocks) %>
|
|
875
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,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.27.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex McRitchie
|
|
@@ -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
|