studio-engine 0.74.7 → 0.74.8
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 +802 -631
- data/README.md +5 -2
- data/app/views/studio/modals/blocks/_close_x.html.erb +6 -4
- data/app/views/studio/modals/blocks/_rail_row.html.erb +3 -1
- data/app/views/style/_modals.html.erb +190 -380
- data/app/views/style/modals/_ds_email_field.html.erb +58 -0
- data/app/views/style/modals/_ds_resend_footer.html.erb +70 -0
- data/app/views/style/modals/_ds_stack_demo.html.erb +63 -0
- data/app/views/style/modals/_wallet_connect.html.erb +26 -12
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +4 -2
- metadata +4 -6
- data/app/views/style/modals/_auth.html.erb +0 -307
- data/app/views/style/modals/_ds_onramp_hub.html.erb +0 -71
- data/app/views/style/modals/_ds_wallet_topup.html.erb +0 -69
- data/app/views/style/modals/_entry_tokens.html.erb +0 -188
- data/app/views/style/modals/_onchain_tx.html.erb +0 -86
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
card behind ours. So this section owns a tiny, self-contained store + overlay
|
|
15
15
|
and registers all specimen content itself.
|
|
16
16
|
|
|
17
|
-
Groups: Auth (the
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
Groups: Auth (the engine-owned FRAGMENTS an app's own sign-in card is built
|
|
18
|
+
from — the engine ships no auth card), Eligibility and entry (the
|
|
19
|
+
app-supplied-policy age-gate DOB primitive, gated by Studio.feature?(:age_gate)),
|
|
20
20
|
Profile (crop / upload, opened
|
|
21
21
|
through studio/cropper_assets), Web3 (wallet-connect and web3-step-up now
|
|
22
22
|
rendered from the solana-studio gem / on-chain-tx / deposit / the generic
|
|
@@ -86,70 +86,51 @@
|
|
|
86
86
|
# alone is false, so the card renders as a NORMAL live card: no badge, no
|
|
87
87
|
# aria-disabled, and clickable whatever `openable` says. That is the worse
|
|
88
88
|
# of the two, and it is the exact scenario the BASE-app test runs in.
|
|
89
|
-
# The other
|
|
90
|
-
# unconditionally, so
|
|
89
|
+
# The ONE other web3 specimen (Entry confirmed) is engine-owned and registered
|
|
90
|
+
# unconditionally, so it keeps the plain capability gate. That population was
|
|
91
|
+
# SIX until 2026-09-09: on-chain-tx (x3), ds-wallet-topup and ds-onramp-hub
|
|
92
|
+
# were mirrors of turf-monster cards and were retired with the rest of them.
|
|
93
|
+
# Recorded here because a gate guarding one card reads like an oversight, and
|
|
94
|
+
# the next reader deserves to know it guarded six and still applies.
|
|
91
95
|
web3_card_off = !web3_on || !web3_gem
|
|
92
96
|
|
|
93
|
-
#
|
|
94
|
-
#
|
|
95
|
-
#
|
|
97
|
+
# THE AUTH METHOD-TOGGLE DEFAULTS USED TO LIVE HERE (ml_default / g_default /
|
|
98
|
+
# w_default / auth_toggles) and are gone with the card they configured. They
|
|
99
|
+
# belonged to style/modals/_auth, a MIRROR of turf-monster's sign-in modal,
|
|
100
|
+
# retired on 2026-09-09. The engine ships no auth card to configure.
|
|
96
101
|
#
|
|
97
|
-
#
|
|
98
|
-
#
|
|
99
|
-
#
|
|
100
|
-
#
|
|
101
|
-
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
104
|
-
# web3_gem above, which wraps the wallet-connect registration) or it opens an
|
|
105
|
-
# empty panel — the same failure the three specimen cards had, through a
|
|
106
|
-
# different door.
|
|
107
|
-
ml_default = Studio.auth_method?(:magic_link)
|
|
108
|
-
g_default = Studio.auth_method?(:google)
|
|
109
|
-
w_default = Studio.auth_method?(:wallet) && web3_on && web3_gem
|
|
110
|
-
|
|
111
|
-
# The Sign in card's toggles. Solana Wallet is DROPPED where the gem is
|
|
112
|
-
# absent, because that checkbox is the door this page actually opens:
|
|
113
|
-
# methodOn('wallet') reads props.methods.wallet — the toggle's own boolean —
|
|
114
|
-
# BEFORE it ever falls back to the defaults above, so a ticked box overrides
|
|
115
|
-
# w_default outright. Leaving it offered would keep the empty panel one click
|
|
116
|
-
# away in EVERY base-app configuration, including the stock auth_methods
|
|
117
|
-
# default that declares no :wallet at all. A control that cannot do its one
|
|
118
|
-
# job is worse than an absent one; the section prose says why it is gone.
|
|
119
|
-
auth_toggles = [
|
|
120
|
-
{ model: "opts.magicLink", label: "Magic Link" },
|
|
121
|
-
{ model: "opts.google", label: "Google" }
|
|
122
|
-
]
|
|
123
|
-
auth_toggles << { model: "opts.wallet", label: "Solana Wallet" } if web3_gem
|
|
124
|
-
auth_toggles << { model: "opts.terms", label: "Terms" }
|
|
102
|
+
# The rule they were built to enforce OUTLIVES them and is worth keeping: a
|
|
103
|
+
# TRIGGER must carry at least the gate its target's REGISTRATION carries, or
|
|
104
|
+
# it opens an empty panel. auth_methods declares which CREDENTIALS an app
|
|
105
|
+
# accepts and features gates PRODUCT SURFACES — both POLICY — while whether a
|
|
106
|
+
# card can RENDER AT ALL is template resolution, which is what web3_gem above
|
|
107
|
+
# answers. The three gem-backed web3 specimens are where that rule is still
|
|
108
|
+
# load-bearing on this page.
|
|
125
109
|
|
|
126
110
|
# Active-card glow-match expression builder: yields an Alpine boolean that is
|
|
127
111
|
# true when $store.dsModals.current() is THIS specimen's modal. It discriminates
|
|
128
112
|
# by sub-state so specimens that open the SAME modal id still glow the right
|
|
129
113
|
# card, and so the glow FOLLOWS a walked flow as the step machine advances:
|
|
130
|
-
# step: the auth / entry-tokens step machine (step_default is the
|
|
131
|
-
# modal's own default step — 'credentials' for auth, 'picker'
|
|
132
|
-
# for entry-tokens).
|
|
133
|
-
# state: the on-chain-tx state (processing / success / error) — lets the
|
|
134
|
-
# glow track Connect Wallet -> Processing -> On-chain success.
|
|
135
114
|
# crop: the two crop-photo variants (:picker vs :crop), on props.cropReady.
|
|
136
115
|
# celebrate: the input specimen (form) vs the "updated" (celebrate) specimen
|
|
137
116
|
# of the SAME leveling modal id.
|
|
138
117
|
# provider: PRESENCE, not a value — the two web3-step-up specimens differ
|
|
139
118
|
# only by whether a wallet BRAND is remembered, and the brand is
|
|
140
119
|
# a free-form string (phantom / solflare / backpack, or nil), so
|
|
141
|
-
# there is no fixed value to match on the way
|
|
142
|
-
# match. true = the remembered-brand card, false = the picker
|
|
120
|
+
# there is no fixed value to match on the way crop: and
|
|
121
|
+
# celebrate: match. true = the remembered-brand card, false = the picker
|
|
143
122
|
# fallback. Without this the pair shared one id with no
|
|
144
123
|
# discriminator, only the first card carried a glow_when, and
|
|
145
124
|
# opening the SECOND card lit the FIRST — the one bug a glow
|
|
146
125
|
# helper exists to prevent.
|
|
147
|
-
|
|
148
|
-
|
|
126
|
+
# step:/state: were dropped on 2026-09-09 with the walked flows that used them
|
|
127
|
+
# (auth's credentials -> magic-link-sent machine, and on-chain-tx's
|
|
128
|
+
# processing/success/error). Both were mirrors of turf-monster cards. A
|
|
129
|
+
# discriminator with no caller cannot be shown to work, so it goes rather than
|
|
130
|
+
# waits: the pattern is three lines and is in this file's history.
|
|
131
|
+
ds_glow = lambda do |id, crop: nil, celebrate: nil, provider: nil|
|
|
149
132
|
cur = "$store.dsModals.current()"
|
|
150
133
|
expr = "#{cur} && #{cur}.id === '#{id}'"
|
|
151
|
-
expr += " && (#{cur}.props.step || '#{step_default}') === '#{step}'" if step
|
|
152
|
-
expr += " && #{cur}.props.state === '#{state}'" if state
|
|
153
134
|
expr += (crop == :crop ? " && !!#{cur}.props.cropReady" : " && !#{cur}.props.cropReady") unless crop.nil?
|
|
154
135
|
expr += (celebrate ? " && !!#{cur}.props.celebrate" : " && !#{cur}.props.celebrate") unless celebrate.nil?
|
|
155
136
|
expr += (provider ? " && !!#{cur}.props.provider" : " && !#{cur}.props.provider") unless provider.nil?
|
|
@@ -158,15 +139,20 @@
|
|
|
158
139
|
%>
|
|
159
140
|
<%# DEMO flow wiring (style-page only): the age-gate primitive fires an
|
|
160
141
|
'age-verified' window event on a successful confirm — its designed handoff
|
|
161
|
-
hook, the same one a production app listens for to resume the entry
|
|
162
|
-
style page plays that "app"
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
142
|
+
hook, the same one a production app listens for to resume the entry it was
|
|
143
|
+
gating. Here the style page plays that "app" and resumes to Entry confirmed,
|
|
144
|
+
so the walk from eligibility to a finished entry runs in sequence.
|
|
145
|
+
WHAT IS BEING DEMONSTRATED IS THE EVENT, NOT THE DESTINATION. Which card an
|
|
146
|
+
app resumes to is the app's business — this page has to pick something, and
|
|
147
|
+
it picks an ENGINE-OWNED card so the demo cannot drift from a consumer's. It
|
|
148
|
+
resumed to the entry-token purchase flow until 2026-09-09; that flow was a
|
|
149
|
+
mirror of turf-monster's and was retired, which is the only reason this
|
|
150
|
+
target moved. The engine primitive is untouched; its real submit still posts
|
|
151
|
+
to the app-supplied submit_url. Kept on the section (always mounted) so it
|
|
152
|
+
fires even as the age-gate card closes. %>
|
|
167
153
|
<section id="modals" class="space-y-10" style="scroll-margin-top: calc(var(--nav-h, 0px) + 5rem)"
|
|
168
154
|
x-data="{}" @ds-modal-close.window="$store.dsModals.close()"
|
|
169
|
-
@age-verified.window="$store.dsModals.open('entry-
|
|
155
|
+
@age-verified.window="$store.dsModals.open('entry-confirmed', { txSignature: null, lobbyUrl: null, seedsEarned: 0 })">
|
|
170
156
|
<div class="space-y-1">
|
|
171
157
|
<h2 class="text-2xl font-bold text-heading">Modals</h2>
|
|
172
158
|
<p class="text-muted text-sm">
|
|
@@ -176,7 +162,7 @@
|
|
|
176
162
|
modal (Enter/Space work too). The header <strong>Copy</strong> yields an
|
|
177
163
|
agent-ready reference you can paste into a chat; option <strong>toggles</strong>
|
|
178
164
|
configure the modal before you open it; and the card that matches the open
|
|
179
|
-
modal
|
|
165
|
+
modal <strong>glows</strong>.
|
|
180
166
|
</p>
|
|
181
167
|
</div>
|
|
182
168
|
|
|
@@ -184,21 +170,17 @@
|
|
|
184
170
|
The full store API — open(id, props, opts) LIFO stack, swap() directional
|
|
185
171
|
slide, advance(patch, opts) in-modal step change, close / closeAll,
|
|
186
172
|
current(), cardClasses() driving the enter/exit animation registry, and
|
|
187
|
-
body.modal-open scroll lock. Plus the
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
the auth + wallet specimens run without an auth backend. Defined in a plain
|
|
173
|
+
body.modal-open scroll lock. Plus the walletProvider stub — required by
|
|
174
|
+
solana-studio's picker — so the wallet specimens run without a real
|
|
175
|
+
extension. Defined in a plain
|
|
191
176
|
inline script (no importmap dependency) so Alpine's defer finds it. %>
|
|
192
177
|
<script>
|
|
193
178
|
(function () {
|
|
194
179
|
// === Credential stubs — never clobber a real one an app already ships. ==
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
return { connecting: false, error: null, statusText: 'Connect Wallet',
|
|
200
|
-
walletAvailable: false, isMobile: false, connect: function () {} };
|
|
201
|
-
};
|
|
180
|
+
// postMagicLink and solanaWalletConnect were retired here on 2026-09-09
|
|
181
|
+
// with style/modals/_auth, the mirror of turf-monster's sign-in card that
|
|
182
|
+
// was their only caller. What remains is what solana-studio's own picker
|
|
183
|
+
// requires, which is why walletProvider stays and they did not.
|
|
202
184
|
// Connect stub for the wallet picker (solana-studio
|
|
203
185
|
// solana_studio/modals/_wallet_connect, configured by
|
|
204
186
|
// style/modals/_wallet_connect). Performs NO connect: it
|
|
@@ -215,7 +197,7 @@
|
|
|
215
197
|
isAvailable: function () { return false; }
|
|
216
198
|
};
|
|
217
199
|
|
|
218
|
-
// Register the page-scoped dsModals
|
|
200
|
+
// Register the page-scoped dsModals store. Named (not an
|
|
219
201
|
// inline alpine:init callback) so the dual-guard at the end can invoke it
|
|
220
202
|
// directly on a Turbo visit, when alpine:init will not fire again.
|
|
221
203
|
function registerDsModals() {
|
|
@@ -355,23 +337,14 @@
|
|
|
355
337
|
}
|
|
356
338
|
});
|
|
357
339
|
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
//
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
get title() { return this._read('title', ''); },
|
|
367
|
-
get message() { return this._read('message', ''); },
|
|
368
|
-
get txSignature() { return this._read('txSignature', null); },
|
|
369
|
-
get errorMessage() { return this._read('errorMessage', ''); },
|
|
370
|
-
get successTitle() { return this._read('successTitle', ''); },
|
|
371
|
-
get successSubtitle() { return this._read('successSubtitle', ''); },
|
|
372
|
-
get ctaLabel() { return this._read('ctaLabel', ''); },
|
|
373
|
-
get ctaHref() { return this._read('ctaHref', ''); }
|
|
374
|
-
});
|
|
340
|
+
// A SECOND STORE USED TO BE REGISTERED HERE: dsSolanaModal, a stateless
|
|
341
|
+
// proxy that re-read the current 'onchain-tx' entry's props so
|
|
342
|
+
// style/modals/_onchain_tx could keep its
|
|
343
|
+
// $store.dsSolanaModal.{state,title,txSignature,…} reads. That partial
|
|
344
|
+
// was a mirror of turf-monster's card and was retired on 2026-09-09,
|
|
345
|
+
// leaving the proxy with no reader, so it went with it. A store with no
|
|
346
|
+
// reader still registers, still looks load-bearing, and still has to be
|
|
347
|
+
// reasoned about by everyone who reads this file.
|
|
375
348
|
}
|
|
376
349
|
|
|
377
350
|
// Alpine's deferred CDN <script> fires alpine:init exactly ONCE, on the
|
|
@@ -425,13 +398,17 @@
|
|
|
425
398
|
<div class="bg-surface rounded-xl border border-subtle shadow-2xl p-6 max-w-sm w-full"
|
|
426
399
|
:class="$store.dsModals.cardClasses()">
|
|
427
400
|
|
|
428
|
-
<%# --- Auth
|
|
429
|
-
<%#
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
401
|
+
<%# --- Auth fragments --- %>
|
|
402
|
+
<%# NOT an auth card. The engine owns no sign-in modal — each app cards
|
|
403
|
+
its own on its own host section (turf-monster: style/host/_modals).
|
|
404
|
+
These two register the PIECES every app's auth card is built from,
|
|
405
|
+
which had no specimen until style/modals/_auth, a mirror of turf's
|
|
406
|
+
card, was retired on 2026-09-09 and took their only render with it. %>
|
|
407
|
+
<template x-if="$store.dsModals.current().id === 'ds-email-field'">
|
|
408
|
+
<div><%= render "style/modals/ds_email_field" %></div>
|
|
409
|
+
</template>
|
|
410
|
+
<template x-if="$store.dsModals.current().id === 'ds-resend-footer'">
|
|
411
|
+
<div><%= render "style/modals/ds_resend_footer" %></div>
|
|
435
412
|
</template>
|
|
436
413
|
|
|
437
414
|
<%# --- Onboarding step (its card now lives in Profile) --- %>
|
|
@@ -453,9 +430,6 @@
|
|
|
453
430
|
<template x-if="$store.dsModals.current().id === 'age-gate'">
|
|
454
431
|
<div><%= render "style/modals/age_gate" %></div>
|
|
455
432
|
</template>
|
|
456
|
-
<template x-if="$store.dsModals.current().id === 'entry-tokens'">
|
|
457
|
-
<div><%= render "style/modals/entry_tokens" %></div>
|
|
458
|
-
</template>
|
|
459
433
|
|
|
460
434
|
<%# --- Profile (real engine partials, opened on dsModals) --- %>
|
|
461
435
|
<template x-if="$store.dsModals.current().id === 'crop-photo'">
|
|
@@ -478,6 +452,10 @@
|
|
|
478
452
|
<template x-if="$store.dsModals.current().id === 'email-change-pending'">
|
|
479
453
|
<div><%= render "style/modals/email_change_pending" %></div>
|
|
480
454
|
</template>
|
|
455
|
+
<%# --- the stack-mechanics vehicle (see Stack behaviour, bottom of page) --- %>
|
|
456
|
+
<template x-if="$store.dsModals.current().id === 'ds-stack-demo'">
|
|
457
|
+
<div><%= render "style/modals/ds_stack_demo" %></div>
|
|
458
|
+
</template>
|
|
481
459
|
<%# --- funding chrome primitives (extract-funding-modal-chrome) --- %>
|
|
482
460
|
<template x-if="$store.dsModals.current().id === 'ds-rail-row'">
|
|
483
461
|
<div><%= render "style/modals/ds_rail_row" %></div>
|
|
@@ -485,13 +463,6 @@
|
|
|
485
463
|
<template x-if="$store.dsModals.current().id === 'ds-close-x'">
|
|
486
464
|
<div><%= render "style/modals/ds_close_x" %></div>
|
|
487
465
|
</template>
|
|
488
|
-
<%# --- the funding modals those primitives were extracted for --- %>
|
|
489
|
-
<template x-if="$store.dsModals.current().id === 'ds-wallet-topup'">
|
|
490
|
-
<div><%= render "style/modals/ds_wallet_topup" %></div>
|
|
491
|
-
</template>
|
|
492
|
-
<template x-if="$store.dsModals.current().id === 'ds-onramp-hub'">
|
|
493
|
-
<div><%= render "style/modals/ds_onramp_hub" %></div>
|
|
494
|
-
</template>
|
|
495
466
|
<template x-if="$store.dsModals.current().id === 'rate-limit-general'">
|
|
496
467
|
<div><%= render "style/modals/rate_limit_general" %></div>
|
|
497
468
|
</template>
|
|
@@ -517,9 +488,6 @@
|
|
|
517
488
|
<div><%= render "solana_studio/modals/web3_step_up", modal_store: "dsModals" %></div>
|
|
518
489
|
</template>
|
|
519
490
|
<% end %>
|
|
520
|
-
<template x-if="$store.dsModals.current().id === 'onchain-tx'">
|
|
521
|
-
<div><%= render "style/modals/onchain_tx" %></div>
|
|
522
|
-
</template>
|
|
523
491
|
<%# entry-confirmed — the engine celebration composed via _success_card's
|
|
524
492
|
yield. One registration serves the generic (web3) + seeds (leveling)
|
|
525
493
|
specimens; the seeds bar self-gates on Studio.feature?(:leveling). %>
|
|
@@ -650,72 +618,50 @@
|
|
|
650
618
|
<div class="space-y-1">
|
|
651
619
|
<h3 class="text-xl font-bold text-heading">Auth</h3>
|
|
652
620
|
<p class="text-muted text-sm">
|
|
653
|
-
The sign-in
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
621
|
+
<strong>The engine ships no sign-in card.</strong> A sign-in modal is a
|
|
622
|
+
product decision — which methods, what terms, whose legal copy
|
|
623
|
+
— so each app owns its own and cards it on <em>its</em> section of
|
|
624
|
+
this guide, against the partial that actually renders in production.
|
|
625
|
+
What the engine owns is the <strong>pieces</strong> that card is built
|
|
626
|
+
from, and they are below.
|
|
627
|
+
</p>
|
|
628
|
+
<p class="text-muted text-sm">
|
|
629
|
+
Until 2026-09-09 this section held a full auth modal
|
|
630
|
+
(<code class="font-mono text-2xs">style/modals/_auth</code>). It was a
|
|
631
|
+
<strong>mirror</strong> of turf-monster’s card, and it drifted:
|
|
632
|
+
reviewing it told you about a copy nobody shipped. It is gone, and these
|
|
633
|
+
two fragments — which it happened to be the only renderer of —
|
|
634
|
+
now stand on their own.
|
|
664
635
|
</p>
|
|
665
636
|
</div>
|
|
666
637
|
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
667
|
-
<%#
|
|
638
|
+
<%# email field — the one piece every app's auth card renders. %>
|
|
668
639
|
<%= render layout: "style/modal_specimen", locals: {
|
|
669
|
-
label: "
|
|
670
|
-
reference: %(the
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2 text-center">
|
|
676
|
-
<span class="block h-2 w-16 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
677
|
-
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
640
|
+
label: "Email field",
|
|
641
|
+
reference: %(the modal family's email input (studio/modals/shared/_email_field) — a themed <input type="email"> that either posts through its own `name` or feeds an enclosing x-model, with locals name/id/value/placeholder/required/autocomplete/autofocus/input_class/x_model/x_ref/disabled_expr/validator. THE `validator` LOCAL IS A FORK, NOT A FLAG: off, the partial is self-contained and the SERVER stays the only boundary; on, it wraps itself in x-data="emailValidator()" and paints a right-edge spinner/check/X that only works if the CONSUMER ships that factory, so passing it without one yields an Alpine scope error and a dead indicator. The specimen draws both faces side by side; this guide ships no emailValidator, which is why the second indicator stays blank. Open with $store.dsModals.open('ds-email-field')),
|
|
642
|
+
open_expr: "$store.dsModals.open('ds-email-field')",
|
|
643
|
+
glow_when: ds_glow.call("ds-email-field") } do %>
|
|
644
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
645
|
+
<span class="block h-1.5 w-12 rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
678
646
|
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
679
|
-
<span class="
|
|
647
|
+
<span class="flex items-center h-6 w-full rounded border px-1.5" style="border-color: var(--color-border-strong)">
|
|
648
|
+
<span class="block h-1.5 flex-1 rounded" style="background: var(--color-text); opacity: .12"></span>
|
|
649
|
+
<span class="block w-3 h-3 rounded-full ml-1" style="background: var(--color-success); opacity: .55"></span>
|
|
650
|
+
</span>
|
|
680
651
|
</div>
|
|
681
652
|
<% end %>
|
|
682
653
|
|
|
654
|
+
<%# resend footer — the piece both link-sent steps share. %>
|
|
683
655
|
<%= render layout: "style/modal_specimen", locals: {
|
|
684
|
-
label: "
|
|
685
|
-
reference: %(the
|
|
686
|
-
open_expr: "$store.dsModals.open('
|
|
687
|
-
glow_when: ds_glow.call("
|
|
656
|
+
label: "Resend footer",
|
|
657
|
+
reference: %(the magic-link resend footer (studio/modals/auth/_resend_footer) — the resend link, its error line and the Close beneath it, shared by an auth card's magic-link-sent and magic-link-resent steps because the two are identical, which is why it was extracted. IT RESOLVES FROM AN ENCLOSING x-data, NOT A STORE: it reads props.submitting, props.resendCooldown and props.resendError and calls resendMagicLink(), so a scope supplying none of them fails SILENTLY — nothing errors, the link simply never enables and the cooldown never counts. The specimen supplies a stand-in host so all three states are reachable, and shortens the cooldown from production's 60s to 5s because the partial does not own the duration, the host does. Open with $store.dsModals.open('ds-resend-footer')),
|
|
658
|
+
open_expr: "$store.dsModals.open('ds-resend-footer')",
|
|
659
|
+
glow_when: ds_glow.call("ds-resend-footer") } do %>
|
|
688
660
|
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
689
661
|
<div class="text-2xl leading-none">📬</div>
|
|
690
|
-
<span class="block h-
|
|
691
|
-
<span class="block h-1.5 w-16 mx-auto rounded" style="background: var(--color-
|
|
692
|
-
|
|
693
|
-
<% end %>
|
|
694
|
-
|
|
695
|
-
<%# NEW — the magic-link-resent step (the resend confirmation slide). %>
|
|
696
|
-
<%= render layout: "style/modal_specimen", locals: {
|
|
697
|
-
label: "Magic link resent",
|
|
698
|
-
reference: %(the Auth "Magic link resent" step (style/modals/_auth, step 'magic-link-resent') — open with $store.dsModals.open('auth', { step: 'magic-link-resent', sentEmail: 'you@example.com' })),
|
|
699
|
-
open_expr: "$store.dsModals.open('auth', { step: 'magic-link-resent', sentEmail: 'you@example.com' })",
|
|
700
|
-
glow_when: ds_glow.call("auth", step: "magic-link-resent") } do %>
|
|
701
|
-
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
702
|
-
<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)">
|
|
703
|
-
<svg class="w-4 h-4" style="color: var(--color-primary)" 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>
|
|
704
|
-
</div>
|
|
705
|
-
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
706
|
-
<span class="block h-1.5 w-16 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
|
|
707
|
-
</div>
|
|
708
|
-
<% end %>
|
|
709
|
-
|
|
710
|
-
<%= render layout: "style/modal_specimen", locals: {
|
|
711
|
-
label: "Redirect (drain)",
|
|
712
|
-
reference: %(the Auth "Redirect" step (style/modals/_auth, step 'redirect', drain CTA) — open with $store.dsModals.open('auth', { step: 'redirect', icon: '📍', title: 'One more step', message: 'Taking you there.', url: null })),
|
|
713
|
-
open_expr: "$store.dsModals.open('auth', { step: 'redirect', icon: '📍', title: 'One more step', message: 'Taking you there.', url: null })",
|
|
714
|
-
glow_when: ds_glow.call("auth", step: "redirect") } do %>
|
|
715
|
-
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
716
|
-
<div class="text-2xl leading-none">📍</div>
|
|
717
|
-
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
718
|
-
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
662
|
+
<span class="block h-1.5 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
|
|
663
|
+
<span class="block h-1.5 w-16 mx-auto rounded" style="background: var(--color-primary); opacity: .7"></span>
|
|
664
|
+
<span class="block h-1.5 w-10 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
719
665
|
</div>
|
|
720
666
|
<% end %>
|
|
721
667
|
</div>
|
|
@@ -1094,16 +1040,7 @@
|
|
|
1094
1040
|
<% end %>
|
|
1095
1041
|
The cards run in the order a player meets them — <strong>connect a
|
|
1096
1042
|
wallet, prove it, spend from it</strong>: Connect wallet → the two
|
|
1097
|
-
Sign Wallet states →
|
|
1098
|
-
top-up → Entry confirmed. Inside that, <strong>Connect Wallet →
|
|
1099
|
-
Processing on-chain transaction → On-chain success</strong> is a
|
|
1100
|
-
<strong>walked flow</strong> and the live card <strong>glows</strong>,
|
|
1101
|
-
following it as it advances (open Connect Wallet and pick a wallet to watch
|
|
1102
|
-
it walk).
|
|
1103
|
-
The <strong>Processing</strong> card is a load modal: its
|
|
1104
|
-
<strong>success/error toggle</strong> chooses whether it resolves to success
|
|
1105
|
-
or the error state, after the <strong>minimum-visible duration</strong>
|
|
1106
|
-
(<code class="font-mono text-2xs">studio/modals/_load_convention</code>). The
|
|
1043
|
+
Sign Wallet states → Entry confirmed. The
|
|
1107
1044
|
Connect-wallet picker ships its <strong>Phantom / Solflare / Backpack brand
|
|
1108
1045
|
marks inline from the engine</strong>
|
|
1109
1046
|
(<code class="font-mono text-2xs">studio/modals/blocks/_wallet_brand_sprite</code>),
|
|
@@ -1223,95 +1160,6 @@
|
|
|
1223
1160
|
</div>
|
|
1224
1161
|
<% end %>
|
|
1225
1162
|
|
|
1226
|
-
<%# 2. Processing — the load modal; the success/error toggle picks the resolution. %>
|
|
1227
|
-
<%= render layout: "style/modal_specimen", locals: {
|
|
1228
|
-
label: "Processing on-chain tx",
|
|
1229
|
-
reference: %(the Web3 "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 })),
|
|
1230
|
-
card_data: "opts: { demoError: false }",
|
|
1231
|
-
toggles: [{ model: "opts.demoError", label: "Resolve to error" }],
|
|
1232
|
-
open_expr: "$store.dsModals.open('onchain-tx', { state: 'processing', title: 'Confirming on-chain', message: 'Waiting for the wallet signature…', demoResolve: true, demoError: opts.demoError })",
|
|
1233
|
-
glow_when: ds_glow.call("onchain-tx", state: "processing"),
|
|
1234
|
-
disabled: !web3_on, openable: true } do %>
|
|
1235
|
-
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
1236
|
-
<div class="loading-dots mx-auto"><span></span><span></span><span></span></div>
|
|
1237
|
-
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1238
|
-
</div>
|
|
1239
|
-
<% end %>
|
|
1240
|
-
|
|
1241
|
-
<%# 3. On-chain success — the resolved success end-state (the walk's finish). %>
|
|
1242
|
-
<%= render layout: "style/modal_specimen", locals: {
|
|
1243
|
-
label: "On-chain success",
|
|
1244
|
-
reference: %(the Web3 "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' })),
|
|
1245
|
-
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 })",
|
|
1246
|
-
glow_when: ds_glow.call("onchain-tx", state: "success"),
|
|
1247
|
-
disabled: !web3_on, openable: true } do %>
|
|
1248
|
-
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
1249
|
-
<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)">
|
|
1250
|
-
<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>
|
|
1251
|
-
</div>
|
|
1252
|
-
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1253
|
-
</div>
|
|
1254
|
-
<% end %>
|
|
1255
|
-
|
|
1256
|
-
<%# On-chain error — the resolved error end-state (the toggle's other branch). %>
|
|
1257
|
-
<%= render layout: "style/modal_specimen", locals: {
|
|
1258
|
-
label: "On-chain error",
|
|
1259
|
-
reference: %(the Web3 "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.' })),
|
|
1260
|
-
open_expr: "$store.dsModals.open('onchain-tx', { state: 'error', title: 'Could not confirm', errorMessage: 'The transaction was rejected. Give it another try.' })",
|
|
1261
|
-
glow_when: ds_glow.call("onchain-tx", state: "error"),
|
|
1262
|
-
disabled: !web3_on, openable: true } do %>
|
|
1263
|
-
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
1264
|
-
<div class="text-2xl leading-none">⚠️</div>
|
|
1265
|
-
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1266
|
-
</div>
|
|
1267
|
-
<% end %>
|
|
1268
|
-
|
|
1269
|
-
<%# ds-wallet-topup — the FIRST funds modal, and a safety fork. %>
|
|
1270
|
-
<%= render layout: "style/modal_specimen", locals: {
|
|
1271
|
-
label: "Top up wallet",
|
|
1272
|
-
reference: %(the first funds modal a player meets (style/modals/_ds_wallet_topup) — turf-monster's modals/_wallet_topup, built here from blocks/_close_x and blocks/_rail_row. THE TOGGLE IS THE CARD: this is one modal with two faces, and the fork is a SAFETY one rather than a preference. A web2 viewer running with ENABLE_WEB2_USDC_ENTRY off cannot pay an entry with USDC at all, so pitching "Buy USDC" would sell them a thing that cannot do the job they came to do — for exactly that audience the primary rail becomes the entry token, and the balance line follows, because the currency it names has to be the one that can pay. Shown as ONE card with a toggle rather than two, because two would read as two products and the point is that a player never sees both. Open with $store.dsModals.open('ds-wallet-topup', { tokenFallback: false })),
|
|
1273
|
-
card_data: "opts: { tokenFallback: false }",
|
|
1274
|
-
toggles: [{ model: "opts.tokenFallback", label: "web2 kill-switch" }],
|
|
1275
|
-
open_expr: "$store.dsModals.open('ds-wallet-topup', { tokenFallback: opts.tokenFallback })",
|
|
1276
|
-
glow_when: ds_glow.call("ds-wallet-topup"),
|
|
1277
|
-
disabled: !web3_on, openable: true } do %>
|
|
1278
|
-
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
1279
|
-
<span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1280
|
-
<span class="flex items-center gap-1.5 rounded-md p-1.5" style="border: 2px solid var(--color-primary)">
|
|
1281
|
-
<span class="block h-4 w-4 rounded" style="background: var(--color-cta)"></span>
|
|
1282
|
-
<span class="block h-1.5 flex-1 rounded" style="background: var(--color-text); opacity: .3"></span>
|
|
1283
|
-
</span>
|
|
1284
|
-
<span class="block h-1.5 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1285
|
-
</div>
|
|
1286
|
-
<% end %>
|
|
1287
|
-
|
|
1288
|
-
<%# ds-onramp-hub — every rail at once, which is where ranking earns its keep. %>
|
|
1289
|
-
<%= render layout: "style/modal_specimen", locals: {
|
|
1290
|
-
label: "Add funds hub",
|
|
1291
|
-
reference: %(the full Add Funds hub (style/modals/_ds_onramp_hub) — turf-monster's modals/_onramp_hub, and the card blocks/_rail_row was extracted FOR: turf draws these six by hand today, which is six chances for the seventh to drift. Six rails is enough that "which one do I press" becomes a real question, and a hub where every rail carries equal weight answers it by asking the person to compare payment processors — not a question anyone can answer about their own life. Exactly ONE rail is :primary and the other five defer to it. PayPal and Venmo show the THIRD shape, :soon — announced before they are wired, so plain divs rather than disabled buttons, with a badge where the chevron would be, because even a dimmed chevron still promises somewhere to go. Open with $store.dsModals.open('ds-onramp-hub')),
|
|
1292
|
-
open_expr: "$store.dsModals.open('ds-onramp-hub')",
|
|
1293
|
-
glow_when: ds_glow.call("ds-onramp-hub"),
|
|
1294
|
-
disabled: !web3_on, openable: true } do %>
|
|
1295
|
-
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-3 space-y-1.5">
|
|
1296
|
-
<span class="flex items-center gap-1.5 rounded p-1" style="border: 2px solid var(--color-primary)">
|
|
1297
|
-
<span class="block h-3 w-3 rounded-sm" style="background: var(--color-cta)"></span>
|
|
1298
|
-
<span class="block h-1 flex-1 rounded" style="background: var(--color-text); opacity: .3"></span>
|
|
1299
|
-
</span>
|
|
1300
|
-
<% 3.times do %>
|
|
1301
|
-
<span class="flex items-center gap-1.5 rounded p-1 border border-subtle">
|
|
1302
|
-
<span class="block h-3 w-3 rounded-sm" style="background: var(--color-text); opacity: .25"></span>
|
|
1303
|
-
<span class="block h-1 flex-1 rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1304
|
-
</span>
|
|
1305
|
-
<% end %>
|
|
1306
|
-
<%# the two :soon rails, dimmed %>
|
|
1307
|
-
<% 2.times do %>
|
|
1308
|
-
<span class="flex items-center gap-1.5 rounded p-1 border border-subtle" style="opacity: .5">
|
|
1309
|
-
<span class="block h-3 w-3 rounded-sm" style="background: var(--color-text); opacity: .25"></span>
|
|
1310
|
-
<span class="block h-1 flex-1 rounded" style="background: var(--color-text); opacity: .12"></span>
|
|
1311
|
-
</span>
|
|
1312
|
-
<% end %>
|
|
1313
|
-
</div>
|
|
1314
|
-
<% end %>
|
|
1315
1163
|
|
|
1316
1164
|
<%# Entry confirmed — the GENERIC web3 celebration (tx link + heading + drain
|
|
1317
1165
|
CTA). seedsEarned: 0 so no seeds bar even where leveling is on: this card
|
|
@@ -1358,14 +1206,25 @@
|
|
|
1358
1206
|
about who the account is. Both are engine primitives gated by
|
|
1359
1207
|
<code class="font-mono text-2xs">Studio.feature?(:age_gate)</code>, and their
|
|
1360
1208
|
minimum age, jurisdiction label, endpoint, and legal copy are all
|
|
1361
|
-
<strong>app-supplied</strong> (the engine hardcodes no policy).
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1209
|
+
<strong>app-supplied</strong> (the engine hardcodes no policy).
|
|
1210
|
+
</p>
|
|
1211
|
+
<p class="text-muted text-sm">
|
|
1212
|
+
<strong>The entry flow itself has no specimen here, on purpose.</strong>
|
|
1213
|
+
Packs, pricing, payment rails and every on-chain write are the app’s
|
|
1214
|
+
— the engine owns none of them, and the walked purchase sequence that
|
|
1215
|
+
used to sit in this section was a <strong>mirror</strong> of turf-monster’s
|
|
1216
|
+
cards. It drifted, and it demonstrated a payment face that had stopped
|
|
1217
|
+
rendering in production. It was retired on 2026-09-09; turf-monster cards
|
|
1218
|
+
the live flow on its own section of this guide.
|
|
1219
|
+
<strong>What stays is the map below</strong>, because the shape of the flow
|
|
1220
|
+
is the part an app should not have to rediscover.
|
|
1367
1221
|
</p>
|
|
1368
|
-
<%# The web2/web3 finding — a real divergence, stated honestly, not invented.
|
|
1222
|
+
<%# The web2/web3 finding — a real divergence, stated honestly, not invented.
|
|
1223
|
+
THIS BOX IS THE SURVIVING HALF of style/modals/_entry_tokens, whose header
|
|
1224
|
+
carried the same model in a comment. When that mirror was deleted the
|
|
1225
|
+
model was folded in HERE, where it renders instead of hiding in a file
|
|
1226
|
+
nobody opens — the two sentences about what a token IS, and about who
|
|
1227
|
+
signs the mint, came across in that move. %>
|
|
1369
1228
|
<div class="mt-2 rounded-lg border border-subtle bg-surface p-3 text-xs text-secondary leading-snug">
|
|
1370
1229
|
<p class="text-heading font-semibold mb-1">Web2 vs web3 — the honest map</p>
|
|
1371
1230
|
<p class="mb-1">
|
|
@@ -1377,11 +1236,17 @@
|
|
|
1377
1236
|
<strong>mints no token</strong> — the mint → consume prelude
|
|
1378
1237
|
is web2-only.
|
|
1379
1238
|
</p>
|
|
1239
|
+
<p class="mb-1">
|
|
1240
|
+
An entry token is a <strong>program-owned prepaid entry credit</strong>
|
|
1241
|
+
recorded on-chain as its own account — <strong>not</strong> a fungible
|
|
1242
|
+
SPL token sitting in the player’s wallet, which is the wrong mental
|
|
1243
|
+
model to carry into the code. And the mint is <strong>server-signed once
|
|
1244
|
+
the payment clears</strong>, never on the buyer’s click: the two
|
|
1245
|
+
on-chain moments a web2 entry has are that mint and the later consume.
|
|
1246
|
+
</p>
|
|
1380
1247
|
<p>
|
|
1381
1248
|
Both create the <strong>same on-chain Entry PDA</strong> and resolve to the
|
|
1382
|
-
<strong>same “Entry Confirmed” card</strong>.
|
|
1383
|
-
path end-to-end; web3 skips the mint and joins at
|
|
1384
|
-
<strong>enter → entered</strong>.
|
|
1249
|
+
<strong>same “Entry Confirmed” card</strong>.
|
|
1385
1250
|
</p>
|
|
1386
1251
|
</div>
|
|
1387
1252
|
</div>
|
|
@@ -1391,79 +1256,11 @@
|
|
|
1391
1256
|
them by the moment they happen to be asked. ENFORCEMENT still lives
|
|
1392
1257
|
here: the bar is checked at contest entry, which is what this section is
|
|
1393
1258
|
named for. The demo walk is unbroken — confirming the birthday card
|
|
1394
|
-
still
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
<%# 1. Entry tokens — the picker (walk's entry). Picking a pack advances. %>
|
|
1399
|
-
<%= render layout: "style/modal_specimen", locals: {
|
|
1400
|
-
label: "Entry tokens",
|
|
1401
|
-
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' })),
|
|
1402
|
-
open_expr: "$store.dsModals.open('entry-tokens', { step: 'picker' })",
|
|
1403
|
-
glow_when: ds_glow.call("entry-tokens", step: "picker", step_default: "picker") } do %>
|
|
1404
|
-
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
1405
|
-
<span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1406
|
-
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
1407
|
-
<span class="block h-6 w-full rounded border" style="border-color: var(--color-primary)"></span>
|
|
1408
|
-
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
1409
|
-
</div>
|
|
1410
|
-
<% end %>
|
|
1259
|
+
still resumes, via the page-level 'age-verified' wiring, which no rename
|
|
1260
|
+
and no deletion has touched, precisely because consuming apps listen on
|
|
1261
|
+
it. What it resumes TO is now Entry confirmed (Web3 above): the old
|
|
1262
|
+
target was one of the retired mirrors. %>
|
|
1411
1263
|
|
|
1412
|
-
<%# 2. Payment processing — the mint-at-purchase load step (web2). %>
|
|
1413
|
-
<%= render layout: "style/modal_specimen", locals: {
|
|
1414
|
-
label: "Payment processing",
|
|
1415
|
-
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' })),
|
|
1416
|
-
open_expr: "$store.dsModals.open('entry-tokens', { step: 'confirming', mintedCount: 3, mintedBalance: '3' })",
|
|
1417
|
-
glow_when: ds_glow.call("entry-tokens", step: "confirming", step_default: "picker") } do %>
|
|
1418
|
-
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
1419
|
-
<div class="loading-dots mx-auto"><span></span><span></span><span></span></div>
|
|
1420
|
-
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1421
|
-
<span class="block h-1.5 w-16 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
|
|
1422
|
-
</div>
|
|
1423
|
-
<% end %>
|
|
1424
|
-
|
|
1425
|
-
<%# 3. Entry Tokens Minted — ready to consume at entry (hold to confirm). %>
|
|
1426
|
-
<%= render layout: "style/modal_specimen", locals: {
|
|
1427
|
-
label: "Entry Tokens Minted",
|
|
1428
|
-
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' })),
|
|
1429
|
-
open_expr: "$store.dsModals.open('entry-tokens', { step: 'minted', mintedCount: 3, mintedBalance: '3' })",
|
|
1430
|
-
glow_when: ds_glow.call("entry-tokens", step: "minted", step_default: "picker") } do %>
|
|
1431
|
-
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
1432
|
-
<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)">
|
|
1433
|
-
<span class="text-sm font-bold" style="color: var(--color-primary)">3</span>
|
|
1434
|
-
</div>
|
|
1435
|
-
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1436
|
-
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
1437
|
-
</div>
|
|
1438
|
-
<% end %>
|
|
1439
|
-
|
|
1440
|
-
<%# 4. Contest enter processing — the on-chain consume load step. %>
|
|
1441
|
-
<%= render layout: "style/modal_specimen", locals: {
|
|
1442
|
-
label: "Contest enter processing",
|
|
1443
|
-
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' })),
|
|
1444
|
-
open_expr: "$store.dsModals.open('entry-tokens', { step: 'entering' })",
|
|
1445
|
-
glow_when: ds_glow.call("entry-tokens", step: "entering", step_default: "picker") } do %>
|
|
1446
|
-
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
1447
|
-
<div class="loading-dots mx-auto"><span></span><span></span><span></span></div>
|
|
1448
|
-
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1449
|
-
<span class="block h-1.5 w-16 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
|
|
1450
|
-
</div>
|
|
1451
|
-
<% end %>
|
|
1452
|
-
|
|
1453
|
-
<%# 5. Contest entered — the shared Entry Confirmed finish (web2 + web3 converge). %>
|
|
1454
|
-
<%= render layout: "style/modal_specimen", locals: {
|
|
1455
|
-
label: "Contest entered",
|
|
1456
|
-
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: '…' })),
|
|
1457
|
-
open_expr: "$store.dsModals.open('entry-tokens', { step: 'entered', txSignature: '5xTrDemoEntrySignature1234567890abc' })",
|
|
1458
|
-
glow_when: ds_glow.call("entry-tokens", step: "entered", step_default: "picker") } do %>
|
|
1459
|
-
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
1460
|
-
<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)">
|
|
1461
|
-
<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>
|
|
1462
|
-
</div>
|
|
1463
|
-
<span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1464
|
-
<span class="block h-4 w-full rounded" style="background: var(--color-cta)"></span>
|
|
1465
|
-
</div>
|
|
1466
|
-
<% end %>
|
|
1467
1264
|
|
|
1468
1265
|
<%# it-begins — the moment a contest locks. The DRAINING CTA is the whole
|
|
1469
1266
|
card: it interrupts someone mid-page at a moment they did not choose,
|
|
@@ -1788,22 +1585,32 @@
|
|
|
1788
1585
|
<h3 class="text-xl font-bold text-heading">Stack behaviour</h3>
|
|
1789
1586
|
<p class="text-muted text-sm">
|
|
1790
1587
|
The engine modal-stack mechanics, poked at directly on
|
|
1791
|
-
<code class="font-mono text-2xs">$store.dsModals</code>:
|
|
1792
|
-
|
|
1588
|
+
<code class="font-mono text-2xs">$store.dsModals</code>:
|
|
1589
|
+
<code class="font-mono text-2xs">advance()</code>, dismissibility, the
|
|
1590
|
+
minimum-visible-duration floor, and LIFO stacking. They all run on one
|
|
1591
|
+
throwaway vehicle
|
|
1592
|
+
(<code class="font-mono text-2xs">style/modals/_ds_stack_demo</code>) built
|
|
1593
|
+
from engine blocks — <strong>the mechanics are the subject, the card
|
|
1594
|
+
is not</strong>. Until 2026-09-09 they ran on a copy of turf-monster’s
|
|
1595
|
+
on-chain card, which is how six unrelated demos came to depend on one
|
|
1596
|
+
consumer’s markup.
|
|
1793
1597
|
</p>
|
|
1794
1598
|
</div>
|
|
1795
1599
|
|
|
1796
1600
|
<div class="grid gap-4 sm:grid-cols-2">
|
|
1797
|
-
<%#
|
|
1798
|
-
|
|
1799
|
-
|
|
1601
|
+
<%# advance() — the in-flow step primitive. The static buttons open the
|
|
1602
|
+
vehicle AT a face; the timed pair transitions a LIVE card, which is the
|
|
1603
|
+
whole distinction: advance() patches props on the stack entry that is
|
|
1604
|
+
already open, so the card's x-data survives, while swap() replaces the
|
|
1605
|
+
entry and rebuilds it. Three ids could not show this — which is why the
|
|
1606
|
+
vehicle carries three faces on one. %>
|
|
1800
1607
|
<div class="card p-5 space-y-3">
|
|
1801
|
-
<h4 class="text-heading font-semibold text-sm">
|
|
1608
|
+
<h4 class="text-heading font-semibold text-sm">In-flow advance()</h4>
|
|
1802
1609
|
<p class="text-secondary text-xs">
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1610
|
+
<code class="font-mono text-2xs">advance(patch)</code> patches the OPEN
|
|
1611
|
+
entry rather than pushing a new one, so the card’s Alpine scope
|
|
1612
|
+
survives the transition. Watch the timed pair change a card that never
|
|
1613
|
+
left the stack.
|
|
1807
1614
|
</p>
|
|
1808
1615
|
<div class="flex flex-wrap gap-2">
|
|
1809
1616
|
<button type="button" class="btn btn-outline btn-sm"
|
|
@@ -1876,51 +1683,54 @@
|
|
|
1876
1683
|
THE STORE IS dsModals. turf-monster's version of this page drove roughly
|
|
1877
1684
|
half of these through its legacy solana-modal compatibility proxy, a store
|
|
1878
1685
|
this engine does not ship; every demo here goes through the page-scoped
|
|
1879
|
-
store and the
|
|
1686
|
+
store and the ds-stack-demo vehicle directly instead. The exact spelling of
|
|
1880
1687
|
that legacy store name is deliberately absent from this file, because
|
|
1881
1688
|
style_guide_modal_simulator_test scans the source for it. %>
|
|
1882
1689
|
<script>
|
|
1883
1690
|
window.dsModalDemos = (function () {
|
|
1884
|
-
var
|
|
1691
|
+
var VEHICLE = 'ds-stack-demo';
|
|
1885
1692
|
|
|
1886
1693
|
function store() { return Alpine.store('dsModals'); }
|
|
1887
1694
|
|
|
1888
|
-
// Open the
|
|
1889
|
-
//
|
|
1695
|
+
// Open the stack-mechanics vehicle. The defaults model a real pending
|
|
1696
|
+
// operation, INCLUDING dismissible: false — the demos that want an
|
|
1890
1697
|
// escapable card override it explicitly.
|
|
1891
|
-
|
|
1892
|
-
|
|
1698
|
+
//
|
|
1699
|
+
// THE VEHICLE IS DELIBERATELY GENERIC. These demos ran on the onchain-tx
|
|
1700
|
+
// specimen until 2026-09-09, which made every one of them depend on a copy
|
|
1701
|
+
// of turf-monster's card; retiring that copy would have taken the whole
|
|
1702
|
+
// section with it. style/modals/_ds_stack_demo belongs to this page and to
|
|
1703
|
+
// nothing that ships.
|
|
1704
|
+
function openDemo(props) {
|
|
1705
|
+
store().open(VEHICLE, Object.assign({
|
|
1893
1706
|
state: 'processing',
|
|
1894
1707
|
title: 'Submitting entry',
|
|
1895
|
-
message: 'Awaiting
|
|
1708
|
+
message: 'Awaiting confirmation',
|
|
1896
1709
|
dismissible: false
|
|
1897
1710
|
}, props || {}));
|
|
1898
1711
|
}
|
|
1899
1712
|
|
|
1900
1713
|
// In-flow state change on the card that is already open. advance() patches
|
|
1901
|
-
// props WITHOUT replacing the stack entry, so the
|
|
1902
|
-
// survives the transition. Guarded to a still-open
|
|
1714
|
+
// props WITHOUT replacing the stack entry, so the vehicle's x-data scope
|
|
1715
|
+
// survives the transition. Guarded to a still-open vehicle card: a timer
|
|
1903
1716
|
// that fires after the user closed it must do nothing rather than patch
|
|
1904
1717
|
// whatever card happens to be on top now.
|
|
1905
|
-
function
|
|
1718
|
+
function advanceDemo(patch) {
|
|
1906
1719
|
var cur = store().current();
|
|
1907
|
-
if (!cur || cur.id !==
|
|
1720
|
+
if (!cur || cur.id !== VEHICLE || cur._closing) return;
|
|
1908
1721
|
store().advance(patch);
|
|
1909
1722
|
}
|
|
1910
1723
|
|
|
1911
1724
|
var SUCCESS = {
|
|
1912
1725
|
state: 'success',
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
successSubtitle: 'Your entry is confirmed on-chain.',
|
|
1916
|
-
ctaLabel: 'View wallet',
|
|
1917
|
-
ctaHref: null,
|
|
1726
|
+
title: 'Entry confirmed',
|
|
1727
|
+
message: 'Resolved in place — this is the same stack entry you opened.',
|
|
1918
1728
|
dismissible: true
|
|
1919
1729
|
};
|
|
1920
1730
|
var FAILURE = {
|
|
1921
1731
|
state: 'error',
|
|
1922
1732
|
title: 'Submission failed',
|
|
1923
|
-
|
|
1733
|
+
message: 'The error face of that same entry, patched in by advance().',
|
|
1924
1734
|
dismissible: true
|
|
1925
1735
|
};
|
|
1926
1736
|
|
|
@@ -2017,34 +1827,34 @@
|
|
|
2017
1827
|
|
|
2018
1828
|
return {
|
|
2019
1829
|
// Stack behaviour
|
|
2020
|
-
processing: function () {
|
|
2021
|
-
success: function () {
|
|
2022
|
-
error: function () {
|
|
1830
|
+
processing: function () { openDemo({}); },
|
|
1831
|
+
success: function () { openDemo(SUCCESS); },
|
|
1832
|
+
error: function () { openDemo(FAILURE); },
|
|
2023
1833
|
processThenSuccess: function () {
|
|
2024
|
-
|
|
2025
|
-
setTimeout(function () {
|
|
1834
|
+
openDemo({ message: 'Awaiting RPC confirmation' });
|
|
1835
|
+
setTimeout(function () { advanceDemo(SUCCESS); }, 3000);
|
|
2026
1836
|
},
|
|
2027
1837
|
processThenError: function () {
|
|
2028
|
-
|
|
2029
|
-
setTimeout(function () {
|
|
1838
|
+
openDemo({ message: 'Awaiting RPC confirmation' });
|
|
1839
|
+
setTimeout(function () { advanceDemo(FAILURE); }, 3000);
|
|
2030
1840
|
},
|
|
2031
1841
|
dismissible: function () {
|
|
2032
|
-
|
|
1842
|
+
openDemo({
|
|
2033
1843
|
dismissible: true,
|
|
2034
1844
|
title: 'Dismissible processing',
|
|
2035
1845
|
message: 'Escape and click-outside both close this one.'
|
|
2036
1846
|
});
|
|
2037
1847
|
},
|
|
2038
1848
|
fastWithHold: function () {
|
|
2039
|
-
|
|
2040
|
-
window.StudioModals.holdAtLeast(1500).then(function () {
|
|
1849
|
+
openDemo({ title: 'Fast operation', message: 'Holding the spinner for at least 1500ms' });
|
|
1850
|
+
window.StudioModals.holdAtLeast(1500).then(function () { advanceDemo(SUCCESS); });
|
|
2041
1851
|
},
|
|
2042
1852
|
fastNoHold: function () {
|
|
2043
|
-
|
|
2044
|
-
setTimeout(function () {
|
|
1853
|
+
openDemo({ title: 'Fast operation', message: 'No hold, so this flashes past' });
|
|
1854
|
+
setTimeout(function () { advanceDemo(SUCCESS); }, 50);
|
|
2045
1855
|
},
|
|
2046
1856
|
stackTwo: function () {
|
|
2047
|
-
|
|
1857
|
+
openDemo({
|
|
2048
1858
|
dismissible: true,
|
|
2049
1859
|
title: 'Bottom of the stack',
|
|
2050
1860
|
message: 'A second card is about to push in front of this one.'
|