studio-engine 0.60.0 → 0.60.1
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 +45 -0
- data/app/views/studio/{_age_verify_assets.html.erb → _birthday_assets.html.erb} +79 -13
- data/app/views/studio/_board_assets.html.erb +1 -1
- data/app/views/studio/_leveling_activity_assets.html.erb +2 -2
- data/app/views/studio/fields/_date_of_birth.html.erb +4 -3
- data/app/views/studio/modals/blocks/_age_gate.html.erb +225 -0
- data/app/views/studio/modals/blocks/_birthday.html.erb +134 -0
- data/app/views/studio/profiles/_birthday_fields.html.erb +1 -1
- data/app/views/style/_modals.html.erb +380 -114
- data/app/views/style/modals/_age_gate.html.erb +21 -0
- data/app/views/style/modals/_birthday.html.erb +59 -0
- data/app/views/style/modals/_cosign_rejected.html.erb +36 -0
- data/app/views/style/modals/_quest_success.html.erb +55 -0
- data/app/views/style/modals/_unsubscribe_confirm.html.erb +52 -0
- data/app/views/style/modals/_unsubscribe_goodbye.html.erb +30 -0
- data/app/views/style/modals/_wallet_setup.html.erb +112 -0
- data/lib/studio/version.rb +1 -1
- metadata +12 -5
- data/app/views/studio/modals/blocks/_age_verify.html.erb +0 -82
- data/app/views/style/modals/_age_verify.html.erb +0 -17
|
@@ -52,13 +52,24 @@
|
|
|
52
52
|
# crop: the two crop-photo variants (:picker vs :crop), on props.cropReady.
|
|
53
53
|
# celebrate: the input specimen (form) vs the "updated" (celebrate) specimen
|
|
54
54
|
# of the SAME leveling modal id.
|
|
55
|
-
|
|
55
|
+
# provider: PRESENCE, not a value — the two web3-step-up specimens differ
|
|
56
|
+
# only by whether a wallet BRAND is remembered, and the brand is
|
|
57
|
+
# a free-form string (phantom / solflare / backpack, or nil), so
|
|
58
|
+
# there is no fixed value to match on the way step: and state:
|
|
59
|
+
# match. true = the remembered-brand card, false = the picker
|
|
60
|
+
# fallback. Without this the pair shared one id with no
|
|
61
|
+
# discriminator, only the first card carried a glow_when, and
|
|
62
|
+
# opening the SECOND card lit the FIRST — the one bug a glow
|
|
63
|
+
# helper exists to prevent.
|
|
64
|
+
ds_glow = lambda do |id, step: nil, step_default: "credentials", crop: nil, celebrate: nil, state: nil,
|
|
65
|
+
provider: nil|
|
|
56
66
|
cur = "$store.dsModals.current()"
|
|
57
67
|
expr = "#{cur} && #{cur}.id === '#{id}'"
|
|
58
68
|
expr += " && (#{cur}.props.step || '#{step_default}') === '#{step}'" if step
|
|
59
69
|
expr += " && #{cur}.props.state === '#{state}'" if state
|
|
60
70
|
expr += (crop == :crop ? " && !!#{cur}.props.cropReady" : " && !#{cur}.props.cropReady") unless crop.nil?
|
|
61
71
|
expr += (celebrate ? " && !!#{cur}.props.celebrate" : " && !#{cur}.props.celebrate") unless celebrate.nil?
|
|
72
|
+
expr += (provider ? " && !!#{cur}.props.provider" : " && !#{cur}.props.provider") unless provider.nil?
|
|
62
73
|
expr
|
|
63
74
|
end
|
|
64
75
|
%>
|
|
@@ -279,10 +290,10 @@
|
|
|
279
290
|
defines the factory inline; no importmap change). %>
|
|
280
291
|
<%= render "studio/cropper_assets" %>
|
|
281
292
|
|
|
282
|
-
<%#
|
|
293
|
+
<%# birthdayModal factory — rendered here at page level (never inside the
|
|
283
294
|
overlay template, where a cloned script would not run) so the age-gate
|
|
284
295
|
specimen opens live. A consumer app renders this in its layout the same way. %>
|
|
285
|
-
<%= render "studio/
|
|
296
|
+
<%= render "studio/birthday_assets" %>
|
|
286
297
|
|
|
287
298
|
<%# levelingActionModal factory — page level (never inside the overlay template,
|
|
288
299
|
where a cloned script would not run) so the change-username + join-newsletter
|
|
@@ -309,7 +320,7 @@
|
|
|
309
320
|
<div><%= render "style/modals/auth" %></div>
|
|
310
321
|
</template>
|
|
311
322
|
|
|
312
|
-
<%# --- Onboarding --- %>
|
|
323
|
+
<%# --- Onboarding step (its card now lives in Profile) --- %>
|
|
313
324
|
<%# The REAL engine partial, mounted on the page-scoped host: what the
|
|
314
325
|
style page shows is exactly what a consumer renders, not a copy. %>
|
|
315
326
|
<template x-if="$store.dsModals.current().id === 'onboarding-first-name'">
|
|
@@ -318,8 +329,15 @@
|
|
|
318
329
|
</template>
|
|
319
330
|
|
|
320
331
|
<%# --- Eligibility + entry --- %>
|
|
321
|
-
|
|
322
|
-
|
|
332
|
+
<%# The birthday card and the age-gate card it hands off to. TWO ids,
|
|
333
|
+
because they are two cards: one asks, the other answers when the
|
|
334
|
+
answer is no. Registered together so the handoff round-trips on this
|
|
335
|
+
page exactly as it does in an app. %>
|
|
336
|
+
<template x-if="$store.dsModals.current().id === 'birthday'">
|
|
337
|
+
<div><%= render "style/modals/birthday" %></div>
|
|
338
|
+
</template>
|
|
339
|
+
<template x-if="$store.dsModals.current().id === 'age-gate'">
|
|
340
|
+
<div><%= render "style/modals/age_gate" %></div>
|
|
323
341
|
</template>
|
|
324
342
|
<template x-if="$store.dsModals.current().id === 'entry-tokens'">
|
|
325
343
|
<div><%= render "style/modals/entry_tokens" %></div>
|
|
@@ -336,7 +354,32 @@
|
|
|
336
354
|
resolve_expr: "$store.dsModals.close()" %></div>
|
|
337
355
|
</template>
|
|
338
356
|
|
|
357
|
+
<%# wallet-setup — registered here with the other cards this page opens.
|
|
358
|
+
Its CARD lives in the Web3 section (it is about getting a wallet); the
|
|
359
|
+
3-of-3 pill it renders is the HOST's onboarding chain position, which
|
|
360
|
+
is not this page's business. The Onboarding section that used to own
|
|
361
|
+
it was folded into Profile on 2026-08-24. %>
|
|
362
|
+
<template x-if="$store.dsModals.current().id === 'wallet-setup'">
|
|
363
|
+
<div><%= render "style/modals/wallet_setup" %></div>
|
|
364
|
+
</template>
|
|
365
|
+
|
|
366
|
+
<%# --- Newsletter exit + the off-page quest celebration --- %>
|
|
367
|
+
<template x-if="$store.dsModals.current().id === 'quest-success'">
|
|
368
|
+
<div><%= render "style/modals/quest_success" %></div>
|
|
369
|
+
</template>
|
|
370
|
+
<template x-if="$store.dsModals.current().id === 'unsubscribe-confirm'">
|
|
371
|
+
<div><%= render "style/modals/unsubscribe_confirm" %></div>
|
|
372
|
+
</template>
|
|
373
|
+
<%# Registered alongside its confirm card, because unsubscribe-confirm
|
|
374
|
+
SWAPS to it — a handoff to an unregistered id renders a blank shell. %>
|
|
375
|
+
<template x-if="$store.dsModals.current().id === 'unsubscribe-goodbye'">
|
|
376
|
+
<div><%= render "style/modals/unsubscribe_goodbye" %></div>
|
|
377
|
+
</template>
|
|
378
|
+
|
|
339
379
|
<%# --- Web3 --- %>
|
|
380
|
+
<template x-if="$store.dsModals.current().id === 'cosign-rejected'">
|
|
381
|
+
<div><%= render "style/modals/cosign_rejected" %></div>
|
|
382
|
+
</template>
|
|
340
383
|
<template x-if="$store.dsModals.current().id === 'wallet-connect'">
|
|
341
384
|
<div><%= render "style/modals/wallet_connect" %></div>
|
|
342
385
|
</template>
|
|
@@ -478,44 +521,6 @@
|
|
|
478
521
|
</div>
|
|
479
522
|
</template>
|
|
480
523
|
|
|
481
|
-
<%# ===================================================================== %>
|
|
482
|
-
<%# 0. ONBOARDING — the post-auth chain's shared step %>
|
|
483
|
-
<%# ===================================================================== %>
|
|
484
|
-
<section class="space-y-5">
|
|
485
|
-
<div class="space-y-1">
|
|
486
|
-
<h3 class="text-xl font-bold text-heading">Onboarding</h3>
|
|
487
|
-
<p class="text-muted text-sm">
|
|
488
|
-
The step every Studio app asks a brand-new account, ported from Turf
|
|
489
|
-
Monster
|
|
490
|
-
(<code class="font-mono text-2xs">studio/modals/onboarding/_first_name</code>).
|
|
491
|
-
It is <strong>one step, not a flow</strong>: the host owns the SEQUENCE
|
|
492
|
-
around it — Turf walks <strong>welcome → first name → age gate
|
|
493
|
-
→ wallet setup</strong>, while a hub app may ask nothing else — so
|
|
494
|
-
this partial never opens another modal. It reports what remains on its
|
|
495
|
-
<code class="font-mono text-2xs">done_event</code> and closes, and the host
|
|
496
|
-
decides what comes next. Every endpoint and label is a local with a
|
|
497
|
-
default, so mounting it under different routes needs no fork.
|
|
498
|
-
<strong>Both</strong> the × and the "Skip for now" link SKIP (a close
|
|
499
|
-
that silently abandons the chain is how a host ends up with a step nobody
|
|
500
|
-
can answer again), and the field focuses itself on open.
|
|
501
|
-
</p>
|
|
502
|
-
</div>
|
|
503
|
-
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
504
|
-
<%= render layout: "style/modal_specimen", locals: {
|
|
505
|
-
label: "First name",
|
|
506
|
-
reference: %(the shared onboarding first-name step (studio-engine studio/modals/onboarding/_first_name) — open with $store.dsModals.open('onboarding-first-name'). Posts to submit_path/skip_path (defaults /onboarding/first_name and /onboarding/skip_first_name) and dispatches its done_event with the steps the SERVER says remain),
|
|
507
|
-
open_expr: "$store.dsModals.open('onboarding-first-name')",
|
|
508
|
-
glow_when: ds_glow.call("onboarding-first-name")
|
|
509
|
-
} do %>
|
|
510
|
-
<p class="text-muted text-2xs">
|
|
511
|
-
Saving or skipping dispatches
|
|
512
|
-
<code class="font-mono">ds-onboarding-step-done</code> here rather than
|
|
513
|
-
the app-level event, so the specimen cannot drive a real chain.
|
|
514
|
-
</p>
|
|
515
|
-
<% end %>
|
|
516
|
-
</div>
|
|
517
|
-
</section>
|
|
518
|
-
|
|
519
524
|
<%# 1. AUTH suite %>
|
|
520
525
|
<%# ===================================================================== %>
|
|
521
526
|
<section class="space-y-5">
|
|
@@ -594,21 +599,128 @@
|
|
|
594
599
|
</section>
|
|
595
600
|
|
|
596
601
|
<%# ===================================================================== %>
|
|
597
|
-
<%# 2. PROFILE —
|
|
602
|
+
<%# 2. PROFILE — who the account is, plus the avatar modals %>
|
|
598
603
|
<%# ===================================================================== %>
|
|
604
|
+
<%# The Onboarding section folded in here on 2026-08-24. It held the post-auth
|
|
605
|
+
chain, but "onboarding" describes WHEN a card is shown, not what it is
|
|
606
|
+
about — and the cards are about the same subject this section already
|
|
607
|
+
owns: the person's name, their birthday, their picture. Grouping by
|
|
608
|
+
subject rather than by moment also stops the guide claiming a chain it
|
|
609
|
+
cannot show, now that the wallet step lives with the wallet cards. %>
|
|
599
610
|
<section class="space-y-5">
|
|
600
611
|
<div class="space-y-1">
|
|
601
612
|
<h3 class="text-xl font-bold text-heading">Profile</h3>
|
|
602
613
|
<p class="text-muted text-sm">
|
|
603
|
-
|
|
614
|
+
Who the account IS — the name it goes by, the birthday that decides
|
|
615
|
+
eligibility, and the picture. The first three cards are asked at signup
|
|
616
|
+
(a host walks them post-auth), the last three are reachable forever from
|
|
617
|
+
the profile page; all six are engine primitives.
|
|
618
|
+
</p>
|
|
619
|
+
<p class="text-muted text-sm">
|
|
620
|
+
<strong>Birthday and Age gate are one flow, deliberately split into two
|
|
621
|
+
cards.</strong> The birthday card ASKS; when the answer is too young the
|
|
622
|
+
age-gate card ANSWERS, carrying a countdown to the day they qualify, a way
|
|
623
|
+
to watch instead, and a way back to fix a mistyped year. Until 2026-08-24
|
|
624
|
+
that refusal lived inside the birthday card as red text beside a disabled
|
|
625
|
+
button — a state with nothing to press. Open <strong>Birthday</strong>
|
|
626
|
+
with <strong>Age validation</strong> on and submit any date to walk the
|
|
627
|
+
handoff; switch the toggle off for the other mode, an app with no age bar
|
|
628
|
+
that just wants the date.
|
|
629
|
+
</p>
|
|
630
|
+
<p class="text-muted text-sm">
|
|
631
|
+
The avatar modals
|
|
604
632
|
(<code class="font-mono text-2xs">studio/modals/_crop_photo</code>,
|
|
605
|
-
<code class="font-mono text-2xs">studio/modals/_image_upload</code>)
|
|
606
|
-
open live here
|
|
607
|
-
|
|
608
|
-
host.
|
|
633
|
+
<code class="font-mono text-2xs">studio/modals/_image_upload</code>)
|
|
634
|
+
open live here —
|
|
635
|
+
<code class="font-mono text-2xs">studio/cropper_assets</code> is rendered
|
|
636
|
+
above, so cropper.js loads and the modals mount on this page's host.
|
|
609
637
|
</p>
|
|
610
638
|
</div>
|
|
611
639
|
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
640
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
641
|
+
label: "First name",
|
|
642
|
+
reference: %(the shared onboarding first-name step (studio-engine studio/modals/onboarding/_first_name) — open with $store.dsModals.open('onboarding-first-name'). Posts to submit_path/skip_path (defaults /onboarding/first_name and /onboarding/skip_first_name) and dispatches its done_event with the steps the SERVER says remain),
|
|
643
|
+
open_expr: "$store.dsModals.open('onboarding-first-name')",
|
|
644
|
+
glow_when: ds_glow.call("onboarding-first-name")
|
|
645
|
+
} do %>
|
|
646
|
+
<%# Every other card in this section shows the shape of the modal it
|
|
647
|
+
opens; this one showed a paragraph of prose, so the row read as a
|
|
648
|
+
missing thumbnail rather than a deliberate difference. It is a
|
|
649
|
+
labelled text field and a submit — draw that. %>
|
|
650
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
651
|
+
<span class="block h-2 w-20 rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
652
|
+
<span class="block h-7 w-full rounded-lg border" style="border-color: var(--color-border-strong)"></span>
|
|
653
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
654
|
+
<%# the skip link that sits under the button, and is also a skip %>
|
|
655
|
+
<span class="block h-1.5 w-14 mx-auto rounded" style="background: var(--color-text); opacity: .25"></span>
|
|
656
|
+
</div>
|
|
657
|
+
<p class="text-muted text-2xs mt-3">
|
|
658
|
+
Saving or skipping dispatches
|
|
659
|
+
<code class="font-mono">ds-onboarding-step-done</code> here rather than
|
|
660
|
+
the app-level event, so the specimen cannot drive a real chain.
|
|
661
|
+
</p>
|
|
662
|
+
<% end %>
|
|
663
|
+
|
|
664
|
+
<%# BIRTHDAY — the ask. ONE card, two modes, because the difference between
|
|
665
|
+
them is a LOCAL the host sets and not a different card:
|
|
666
|
+
|
|
667
|
+
age validation ON — the app has a bar, so the card says what it is
|
|
668
|
+
and an under-age date hands off to the age gate.
|
|
669
|
+
age validation OFF — the app just wants the date. McRitchie Studio
|
|
670
|
+
has no gate and still wants a birthday to wish
|
|
671
|
+
you a happy one; the card then drops the age
|
|
672
|
+
line, the eligibility wording and the handoff.
|
|
673
|
+
|
|
674
|
+
It was two cards until 2026-08-25 ("Birthday" and "Birthday (underage)"),
|
|
675
|
+
which read as two components and hid the fact that the second was the
|
|
676
|
+
first with a prop flipped. The toggle is the honest shape — the same one
|
|
677
|
+
Profile Leveling and the Processing card use.
|
|
678
|
+
|
|
679
|
+
THE BADGE SAYS "age validation", NOT "age gate off". The old badge
|
|
680
|
+
reported this app's Studio.feature?(:age_gate) flag, which is the wrong
|
|
681
|
+
question for a card whose whole point is that the RULE is per-app:
|
|
682
|
+
turf-monster's is jurisdiction-dependent, a hub app's may not exist. %>
|
|
683
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
684
|
+
label: "Birthday",
|
|
685
|
+
reference: %(the "Birthday" DOB card (studio-engine studio/modals/blocks/_birthday, renamed from _age_verify) — an engine primitive that ASKS for a date. Toggle "Age validation" to switch the two modes a host chooses between: ON, the app supplies min_age / state and an under-age date hands off to the age-gate card; OFF, there is no bar at all and the card is a plain birthday asker (a hub app wanting the date for a greeting). The engine hardcodes no policy either way — turf-monster's minimum is jurisdiction-dependent and resolved server-side. With validation ON, submitting takes the REFUSED branch so you can walk the handoff. Open with $store.dsModals.open('birthday', { validates: true })),
|
|
686
|
+
card_data: "opts: { validates: true }",
|
|
687
|
+
toggles: [{ model: "opts.validates", label: "Age validation" }],
|
|
688
|
+
open_expr: "$store.dsModals.open('birthday', { validates: opts.validates })",
|
|
689
|
+
glow_when: ds_glow.call("birthday"),
|
|
690
|
+
openable: true } do %>
|
|
691
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2 text-center">
|
|
692
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
693
|
+
<%# month / day / year %>
|
|
694
|
+
<div class="grid grid-cols-3 gap-1">
|
|
695
|
+
<span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
696
|
+
<span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
697
|
+
<span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
698
|
+
</div>
|
|
699
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
700
|
+
</div>
|
|
701
|
+
<% end %>
|
|
702
|
+
|
|
703
|
+
<%# AGE GATE — the answer, when the answer is no. %>
|
|
704
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
705
|
+
label: "Age gate",
|
|
706
|
+
reference: %(the "Age gate" refusal card (studio-engine studio/modals/blocks/_age_gate, NEW) — where the birthday card hands off when the date is under the app's bar. Headed "Easy, Young'un" under a teddy bear rather than a red X, because a person who will be welcome later is not an error state. The body runs a LIVE countdown to the day they become eligible, built from the date the birthday card handed across — a refusal that only says "not yet" leaves someone to guess whether that is a week or a decade. Carries the two ways out: a primary CTA to WATCH the contest instead (app-supplied watch_url) and a back link that swaps to the birthday card, because a mis-picked year is a typo and a gate with no way back turns a typo into a locked door. Displays min_age / state; computes no eligibility of its own. Open with $store.dsModals.open('age-gate', { minAge: 21, state: 'CA', dobYear: ..., dobMonth: 6, dobDay: 15 })),
|
|
707
|
+
open_expr: "$store.dsModals.open('age-gate', { minAge: 21, state: 'CA', dobYear: new Date().getFullYear() - 16, dobMonth: 6, dobDay: 15 })",
|
|
708
|
+
glow_when: ds_glow.call("age-gate"),
|
|
709
|
+
openable: true } do %>
|
|
710
|
+
<%# A teddy bear, not the red X pill — the card is not an error state and
|
|
711
|
+
its thumbnail should not promise one. %>
|
|
712
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2 text-center">
|
|
713
|
+
<div class="text-2xl leading-none">🧸</div>
|
|
714
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
715
|
+
<%# the countdown line, which is the widest thing on the real card %>
|
|
716
|
+
<span class="block h-1.5 w-full rounded" style="background: var(--color-text); opacity: .3"></span>
|
|
717
|
+
<%# primary = watch instead; secondary = go fix the date %>
|
|
718
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
719
|
+
<span class="block h-1.5 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .25"></span>
|
|
720
|
+
</div>
|
|
721
|
+
<% end %>
|
|
722
|
+
|
|
723
|
+
|
|
612
724
|
<%# Image upload FIRST — the empty picker (crop-photo opened with no imageUrl).
|
|
613
725
|
cropReady: false is the defined starting sub-state; the crop factory
|
|
614
726
|
flips it true once an image is picked, moving the glow to Crop photo. %>
|
|
@@ -770,17 +882,75 @@
|
|
|
770
882
|
<span class="block h-1.5 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
771
883
|
</div>
|
|
772
884
|
<% end %>
|
|
885
|
+
|
|
886
|
+
<%# THE WAY OUT. The three cards above walk a user IN; a section that only
|
|
887
|
+
ever demonstrates opt-in is showing the happy half of a flow, not the
|
|
888
|
+
flow. These two are the exit, and they are a PAIR — the confirm card
|
|
889
|
+
swaps to the goodbye beat, so reviewing either alone misses the handoff.
|
|
890
|
+
App-specific (the engine owns no newsletter); engine chrome only. %>
|
|
891
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
892
|
+
label: "Leave the newsletter?",
|
|
893
|
+
reference: %(the unsubscribe CONFIRM card (style/modals/_unsubscribe_confirm) — turf-monster's modals/_unsubscribe_confirm, which POSTs newsletter_unsubscribe_path and swaps to the goodbye beat on success. Here the POST resolves locally so the handoff is walkable with no backend: press Unsubscribe and you land on "See you later". The destructive action wears the primary button and the never-mind out is the quiet Close link below it — a confirmation whose safe path is harder to find than its destructive one is a formality, not a confirmation. Open with $store.dsModals.open('unsubscribe-confirm')),
|
|
894
|
+
open_expr: "$store.dsModals.open('unsubscribe-confirm')",
|
|
895
|
+
glow_when: ds_glow.call("unsubscribe-confirm") } do %>
|
|
896
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
897
|
+
<div class="text-2xl leading-none">📭</div>
|
|
898
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
899
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
900
|
+
<%# the quiet out, deliberately smaller than the destructive action %>
|
|
901
|
+
<span class="block h-1.5 w-12 mx-auto rounded" style="background: var(--color-text); opacity: .25"></span>
|
|
902
|
+
</div>
|
|
903
|
+
<% end %>
|
|
904
|
+
|
|
905
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
906
|
+
label: "See you later",
|
|
907
|
+
reference: %(the unsubscribe GOODBYE beat (style/modals/_unsubscribe_goodbye) — where the confirm card lands. The smallest card in the guide, and the copy is a door left open rather than a plea: "Change your mind anytime from your account". Worth preserving if it is ever reworded. Open with $store.dsModals.open('unsubscribe-goodbye')),
|
|
908
|
+
open_expr: "$store.dsModals.open('unsubscribe-goodbye')",
|
|
909
|
+
glow_when: ds_glow.call("unsubscribe-goodbye") } do %>
|
|
910
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
911
|
+
<div class="text-2xl leading-none">✌️</div>
|
|
912
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
913
|
+
<span class="block h-1.5 w-28 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
914
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
915
|
+
</div>
|
|
916
|
+
<% end %>
|
|
917
|
+
|
|
918
|
+
<%# THE SAME BEAT, REACHED FROM SOMEWHERE ELSE. Great Username (two cards up)
|
|
919
|
+
celebrates INSIDE the modal that did the work. This one is what a host
|
|
920
|
+
shows when the quest completed somewhere with no card to advance in
|
|
921
|
+
place — turf's /account username change — so the celebration arrives on
|
|
922
|
+
its own. The toggle is the fork at its foot, which is the whole reason
|
|
923
|
+
it is one card and not two. %>
|
|
924
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
925
|
+
label: "Quest success (off-page)",
|
|
926
|
+
reference: %(the off-page quest celebration (style/modals/_quest_success) — turf-monster's modals/_quest_success, shown when a quest completes away from any quest card. Composes the engine's card_header AND blocks/_seeds_bar (a :leveling primitive: the bar is the engine's, the numbers are app-supplied). Toggle "Subscribed" to switch the fork at the foot: NOT subscribed offers the newsletter as the next quest, so the celebration doubles as the funnel; subscribed sends them to the contest where the remaining quests live. In the real card that gate is SERVER-rendered from current_user.subscribed_to_newsletter?, precisely so the CTA never flashes the wrong option at someone already in. Open with $store.dsModals.open('quest-success', { subscribed: false })),
|
|
927
|
+
card_data: "opts: { subscribed: false }",
|
|
928
|
+
toggles: [{ model: "opts.subscribed", label: "Subscribed" }],
|
|
929
|
+
open_expr: "$store.dsModals.open('quest-success', { subscribed: opts.subscribed, seedsTotal: 25 })",
|
|
930
|
+
glow_when: ds_glow.call("quest-success") } do %>
|
|
931
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
932
|
+
<div class="text-2xl leading-none">🌱</div>
|
|
933
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
934
|
+
<%# the seeds bar, which is the card's middle and the engine's %>
|
|
935
|
+
<span class="flex gap-1">
|
|
936
|
+
<span class="flex-1 h-2 rounded" style="background: var(--color-cta)"></span>
|
|
937
|
+
<span class="flex-1 h-2 rounded" style="background: var(--color-cta); opacity:.4"></span>
|
|
938
|
+
<span class="flex-1 h-2 rounded" style="background: var(--color-cta); opacity:.2"></span>
|
|
939
|
+
</span>
|
|
940
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
941
|
+
</div>
|
|
942
|
+
<% end %>
|
|
773
943
|
</div>
|
|
774
944
|
</section>
|
|
775
945
|
|
|
776
946
|
<%# ===================================================================== %>
|
|
777
|
-
<%# 2. WEB3
|
|
947
|
+
<%# 2. WEB3 — the wallet + on-chain modals, in the order a player meets them. %>
|
|
778
948
|
<%# Gated by :web3; off = disabled-but-present-yet-openable. %>
|
|
779
949
|
<%# ===================================================================== %>
|
|
780
950
|
<section class="space-y-5">
|
|
781
951
|
<div class="space-y-1">
|
|
782
952
|
<div class="flex flex-wrap items-center gap-3">
|
|
783
|
-
<h3 class="text-xl font-bold text-heading">Web3
|
|
953
|
+
<h3 class="text-xl font-bold text-heading">Web3</h3>
|
|
784
954
|
<% if web3_on %>
|
|
785
955
|
<span class="badge" style="color: var(--color-success); border-color: var(--color-success)">enabled on this app</span>
|
|
786
956
|
<% else %>
|
|
@@ -792,10 +962,14 @@
|
|
|
792
962
|
<code class="font-mono text-2xs">Studio.feature?(:web3)</code>. Web3 is
|
|
793
963
|
<strong><%= web3_on ? "on" : "off" %></strong> here, so these render
|
|
794
964
|
<%= web3_on ? "live." : "greyed and badged — but STILL openable as a preview." %>
|
|
795
|
-
The
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
965
|
+
The cards run in the order a player meets them — <strong>get a wallet,
|
|
966
|
+
prove it, spend from it</strong>: Connect wallet → Setup Wallet →
|
|
967
|
+
the two Sign Wallet states → Processing → success or error →
|
|
968
|
+
top-up → Entry confirmed. Inside that, <strong>Connect Wallet →
|
|
969
|
+
Processing on-chain transaction → On-chain success</strong> is a
|
|
970
|
+
<strong>walked flow</strong> and the live card <strong>glows</strong>,
|
|
971
|
+
following it as it advances (open Connect Wallet and pick a wallet to watch
|
|
972
|
+
it walk).
|
|
799
973
|
The <strong>Processing</strong> card is a load modal: its
|
|
800
974
|
<strong>success/error toggle</strong> chooses whether it resolves to success
|
|
801
975
|
or the error state, after the <strong>minimum-visible duration</strong>
|
|
@@ -804,7 +978,7 @@
|
|
|
804
978
|
marks inline from the engine</strong>
|
|
805
979
|
(<code class="font-mono text-2xs">studio/modals/blocks/_wallet_brand_sprite</code>),
|
|
806
980
|
so every app inherits them with no per-app icon files.
|
|
807
|
-
The two <strong>
|
|
981
|
+
The two <strong>Sign Wallet</strong> cards are a different kind of thing
|
|
808
982
|
from the rest of this section: they render
|
|
809
983
|
<code class="font-mono text-2xs">studio/modals/_web3_step_up</code> —
|
|
810
984
|
the <strong>REAL shared partial a host renders in production</strong>,
|
|
@@ -813,28 +987,136 @@
|
|
|
813
987
|
<strong>standard web3 auth button</strong>: a wallet row (brand mark, the
|
|
814
988
|
wallet’s own name, Installed badge, chevron) rather than a filled
|
|
815
989
|
CTA, glowing via <code class="font-mono text-2xs">pulse-cta</code> because
|
|
816
|
-
it is the one thing to press.
|
|
990
|
+
it is the one thing to press. Do not confuse them with <strong>Setup
|
|
991
|
+
Wallet</strong>, immediately before them: that one asks an account to
|
|
992
|
+
<strong>get</strong> a wallet, these ask an account to
|
|
993
|
+
<strong>prove</strong> the one it already has — opposite populations
|
|
994
|
+
(<code class="font-mono text-2xs">WalletSetupPolicy</code> exits at its own
|
|
995
|
+
step 1 for an account that holds a wallet), so no user ever meets both.
|
|
996
|
+
They sit next to each other anyway, because the pair is easiest to keep
|
|
997
|
+
straight when you can read them side by side.
|
|
817
998
|
</p>
|
|
818
999
|
</div>
|
|
1000
|
+
<%# ONE ordered run, read left to right as a player meets it: get a wallet,
|
|
1001
|
+
prove it, spend from it. This carried three labelled h4 runs for a day
|
|
1002
|
+
(The walk / Proving a wallet / Funding & confirmation) and they were
|
|
1003
|
+
removed on 2026-08-25 — the player order already groups the cards, and the
|
|
1004
|
+
headings were a second, competing structure over the same nine. What holds
|
|
1005
|
+
the order now is a test (style_page_test's WEB3_ORDER), not a label a
|
|
1006
|
+
reader has to trust. %>
|
|
819
1007
|
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
820
1008
|
<%# 1. Connect wallet — the walk's entry; picking a wallet swaps to Processing. %>
|
|
821
1009
|
<%= render layout: "style/modal_specimen", locals: {
|
|
822
1010
|
label: "Connect wallet",
|
|
823
|
-
reference: %(the Web3
|
|
1011
|
+
reference: %(the Web3 "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')),
|
|
824
1012
|
open_expr: "$store.dsModals.open('wallet-connect')",
|
|
825
1013
|
glow_when: ds_glow.call("wallet-connect"),
|
|
826
1014
|
disabled: !web3_on, openable: true } do %>
|
|
1015
|
+
<%# A PICKER: brand tiles with names beside them, which is what this modal
|
|
1016
|
+
actually is. Two empty outlines said "form with two inputs" and were
|
|
1017
|
+
the least informative sketch in the section — the one card whose
|
|
1018
|
+
subject is a LIST of wallets showed no wallets at all. Three rows,
|
|
1019
|
+
because the picker ships Phantom / Solflare / Backpack marks inline. %>
|
|
827
1020
|
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
828
|
-
<span class="block h-2 w-20 rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
829
|
-
|
|
830
|
-
|
|
1021
|
+
<span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1022
|
+
<% [0.9, 0.6, 0.45].each do |tint| %>
|
|
1023
|
+
<span class="flex items-center gap-1.5 h-6 w-full rounded-lg border px-1.5" style="border-color: var(--color-border-strong)">
|
|
1024
|
+
<span class="w-3.5 h-3.5 rounded shrink-0" style="background: var(--color-primary); opacity: <%= tint %>"></span>
|
|
1025
|
+
<span class="block h-1.5 w-9 rounded" style="background: var(--color-text); opacity: .35"></span>
|
|
1026
|
+
<span class="block h-1.5 w-1.5 ml-auto rounded-full" style="background: var(--color-text); opacity: .25"></span>
|
|
1027
|
+
</span>
|
|
1028
|
+
<% end %>
|
|
1029
|
+
</div>
|
|
1030
|
+
<% end %>
|
|
1031
|
+
|
|
1032
|
+
<%# WALLET SETUP — new 2026-08-24, and moved here from Onboarding the same
|
|
1033
|
+
day. It is grouped with the wallet cards rather than with the signup
|
|
1034
|
+
chain because that is what it IS: getting a wallet. The card still
|
|
1035
|
+
renders a 3-of-3 pill, which is the host's chain position and not this
|
|
1036
|
+
section's business.
|
|
1037
|
+
ONE card with a "Wallet detected" toggle rather than two cards sharing a
|
|
1038
|
+
modal id: the two Sign Wallet cards below are what that shape costs. %>
|
|
1039
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
1040
|
+
label: "Setup Wallet",
|
|
1041
|
+
reference: %(the "Set up your wallet" card (style/modals/_wallet_setup) — how an account with NO wallet gets one, and the host's third onboarding step (it renders a 3-of-3 pill, which is the host's chain position, not this section's). An APP-SPECIFIC flow: the engine owns only the chrome (shell, progress_pill, wallet_brand_sprite) and the house wallet-row idiom, while extension detection, the 90-second walkthrough video, the Detailed Guide route and the managed-wallet fallback stay in the host (turf-monster's own is ~576 lines). Toggle "Wallet detected" to swap the row between INSTALL and Installed. NOTE it is the OPPOSITE population to the two Sign Wallet cards immediately below — WalletSetupPolicy exits at its own step 1 for an account that already holds a wallet, so no user ever meets both. Open with $store.dsModals.open('wallet-setup', { detected: false })),
|
|
1042
|
+
card_data: "opts: { detected: false }",
|
|
1043
|
+
toggles: [{ model: "opts.detected", label: "Wallet detected" }],
|
|
1044
|
+
open_expr: "$store.dsModals.open('wallet-setup', { detected: opts.detected })",
|
|
1045
|
+
glow_when: ds_glow.call("wallet-setup"),
|
|
1046
|
+
disabled: !web3_on, openable: true } do %>
|
|
1047
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
1048
|
+
<span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1049
|
+
<%# the 3-of-3 pill, filled — the whole reason this card sits here %>
|
|
1050
|
+
<span class="flex gap-1">
|
|
1051
|
+
<span class="flex-1 h-1.5 rounded" style="background: var(--color-cta)"></span>
|
|
1052
|
+
<span class="flex-1 h-1.5 rounded" style="background: var(--color-cta)"></span>
|
|
1053
|
+
<span class="flex-1 h-1.5 rounded" style="background: var(--color-cta)"></span>
|
|
1054
|
+
</span>
|
|
1055
|
+
<span class="flex items-center gap-1.5 h-7 w-full rounded-lg border px-1.5" style="border-color: var(--color-border-strong)">
|
|
1056
|
+
<span class="w-4 h-4 rounded shrink-0" style="background: var(--color-primary)"></span>
|
|
1057
|
+
<span class="block h-1.5 w-8 rounded" style="background: var(--color-text); opacity: .35"></span>
|
|
1058
|
+
</span>
|
|
1059
|
+
<%# the walkthrough placeholder, the card's second half %>
|
|
1060
|
+
<span class="block w-full rounded" style="height: 1.6rem; background: var(--color-text); opacity: .10"></span>
|
|
1061
|
+
</div>
|
|
1062
|
+
<% end %>
|
|
1063
|
+
|
|
1064
|
+
<%# SIGN WALLET — the web2-session-on-a-wallet-account card. NOT part of the
|
|
1065
|
+
walk above: that walk is one on-chain transaction, this is an AUTH state
|
|
1066
|
+
that precedes any of it. Two specimens because the card has exactly two
|
|
1067
|
+
shapes, and the second is what every wallet linked before a host recorded
|
|
1068
|
+
brands will see.
|
|
1069
|
+
|
|
1070
|
+
OPPOSITE POPULATION to the "Setup Wallet" card two up in this
|
|
1071
|
+
section, and worth stating because the two look alike: that one asks
|
|
1072
|
+
"get a wallet", this one asks "prove the wallet you already have".
|
|
1073
|
+
WalletSetupPolicy exits at its own step 1 for an account that holds a
|
|
1074
|
+
wallet, so no user can ever meet both. %>
|
|
1075
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
1076
|
+
label: "Sign Wallet",
|
|
1077
|
+
reference: %(the Web3 "Sign Wallet" card (studio/modals/_web3_step_up — the REAL shared partial, not a specimen copy) — shown to an account that holds a self-custody wallet but authenticated this session with a web2 credential. One wallet row, glowing, because it is the only thing to press. Open with $store.dsModals.open('web3-step-up', { provider: 'phantom', providerLabel: 'Phantom', walletHint: '7xKp…JZ2Q' })),
|
|
1078
|
+
open_expr: "$store.dsModals.open('web3-step-up', { provider: 'phantom', providerLabel: 'Phantom', walletHint: '7xKp…JZ2Q' })",
|
|
1079
|
+
glow_when: ds_glow.call("web3-step-up", provider: true),
|
|
1080
|
+
disabled: !web3_on, openable: true } do %>
|
|
1081
|
+
<%# A FILLED row: brand tile, the wallet's name, an Installed badge. The
|
|
1082
|
+
two step-up thumbnails used to differ by a single border-dashed class
|
|
1083
|
+
on a 20px row, which is not a difference anyone can see at thumbnail
|
|
1084
|
+
size — and these two cards are the pair most likely to be mistaken for
|
|
1085
|
+
duplicates, because they share a modal id AND a title. So the sketch
|
|
1086
|
+
now shows the actual distinction: this card KNOWS which wallet. %>
|
|
1087
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
1088
|
+
<div class="text-2xl leading-none">🔐</div>
|
|
1089
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1090
|
+
<span class="flex items-center gap-1.5 h-7 w-full rounded-lg border px-1.5" style="border-color: var(--color-border-strong)">
|
|
1091
|
+
<span class="w-4 h-4 rounded shrink-0" style="background: var(--color-primary)"></span>
|
|
1092
|
+
<span class="block h-1.5 w-8 rounded" style="background: var(--color-text); opacity: .35"></span>
|
|
1093
|
+
<span class="block h-2.5 w-7 ml-auto rounded-full border" style="border-color: var(--color-primary)"></span>
|
|
1094
|
+
</span>
|
|
1095
|
+
</div>
|
|
1096
|
+
<% end %>
|
|
1097
|
+
|
|
1098
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
1099
|
+
label: "Sign Wallet (no remembered brand)",
|
|
1100
|
+
reference: %(the Web3 "Sign Wallet" card with NO remembered brand (studio/modals/_web3_step_up) — every wallet linked before a host recorded brands lands here, so it is a live population and not a defensive branch. The row falls back to the picker rather than dead-ending. Open with $store.dsModals.open('web3-step-up', {})),
|
|
1101
|
+
open_expr: "$store.dsModals.open('web3-step-up', {})",
|
|
1102
|
+
glow_when: ds_glow.call("web3-step-up", provider: false),
|
|
1103
|
+
disabled: !web3_on, openable: true } do %>
|
|
1104
|
+
<%# An EMPTY slot: dashed outline, no brand tile, a "?" where the wallet's
|
|
1105
|
+
name would be. Reads at a glance as "we do not know which wallet",
|
|
1106
|
+
which is the whole difference between this card and the one before it. %>
|
|
1107
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
1108
|
+
<div class="text-2xl leading-none">🔐</div>
|
|
1109
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1110
|
+
<span class="flex items-center justify-center h-7 w-full rounded-lg border border-dashed" style="border-color: var(--color-border-strong)">
|
|
1111
|
+
<span class="text-xs font-bold text-muted leading-none">?</span>
|
|
1112
|
+
</span>
|
|
831
1113
|
</div>
|
|
832
1114
|
<% end %>
|
|
833
1115
|
|
|
834
1116
|
<%# 2. Processing — the load modal; the success/error toggle picks the resolution. %>
|
|
835
1117
|
<%= render layout: "style/modal_specimen", locals: {
|
|
836
1118
|
label: "Processing on-chain tx",
|
|
837
|
-
reference: %(the Web3
|
|
1119
|
+
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 })),
|
|
838
1120
|
card_data: "opts: { demoError: false }",
|
|
839
1121
|
toggles: [{ model: "opts.demoError", label: "Resolve to error" }],
|
|
840
1122
|
open_expr: "$store.dsModals.open('onchain-tx', { state: 'processing', title: 'Confirming on-chain', message: 'Waiting for the wallet signature…', demoResolve: true, demoError: opts.demoError })",
|
|
@@ -849,7 +1131,7 @@
|
|
|
849
1131
|
<%# 3. On-chain success — the resolved success end-state (the walk's finish). %>
|
|
850
1132
|
<%= render layout: "style/modal_specimen", locals: {
|
|
851
1133
|
label: "On-chain success",
|
|
852
|
-
reference: %(the Web3
|
|
1134
|
+
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' })),
|
|
853
1135
|
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 })",
|
|
854
1136
|
glow_when: ds_glow.call("onchain-tx", state: "success"),
|
|
855
1137
|
disabled: !web3_on, openable: true } do %>
|
|
@@ -864,7 +1146,7 @@
|
|
|
864
1146
|
<%# On-chain error — the resolved error end-state (the toggle's other branch). %>
|
|
865
1147
|
<%= render layout: "style/modal_specimen", locals: {
|
|
866
1148
|
label: "On-chain error",
|
|
867
|
-
reference: %(the Web3
|
|
1149
|
+
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.' })),
|
|
868
1150
|
open_expr: "$store.dsModals.open('onchain-tx', { state: 'error', title: 'Could not confirm', errorMessage: 'The transaction was rejected. Give it another try.' })",
|
|
869
1151
|
glow_when: ds_glow.call("onchain-tx", state: "error"),
|
|
870
1152
|
disabled: !web3_on, openable: true } do %>
|
|
@@ -874,40 +1156,30 @@
|
|
|
874
1156
|
</div>
|
|
875
1157
|
<% end %>
|
|
876
1158
|
|
|
877
|
-
<%#
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
1159
|
+
<%# THE OTHER FAILURE, and it is a different KIND of failure from the one
|
|
1160
|
+
above. On-chain error is the chain saying no. This is the SERVER saying
|
|
1161
|
+
no — refusing to co-sign because the transaction it was handed did not
|
|
1162
|
+
match the entry it prepared. They sit together because a reviewer
|
|
1163
|
+
reading either alone would not know the other existed. %>
|
|
882
1164
|
<%= render layout: "style/modal_specimen", locals: {
|
|
883
|
-
label: "
|
|
884
|
-
reference: %(the
|
|
885
|
-
open_expr: "$store.dsModals.open('
|
|
886
|
-
glow_when: ds_glow.call("
|
|
1165
|
+
label: "Co-sign refused",
|
|
1166
|
+
reference: %(the "Transaction not signed" card (style/modals/_cosign_rejected) — turf-monster's modals/_cosign_rejected, opened when POST confirm_onchain_entry answers 422 with code tx_rejected: the server refused to co-sign because the submitted transaction did not match the entry it had prepared. APP-SPECIFIC — the engine owns no entry preparation, no co-signing and no verdict; it lends card_header and nothing else. THE COPY IS DELIBERATELY VAGUE and should stay that way: good actors never see this card (it fires on a tampered or mismatched transaction), so naming WHICH server-side check failed would help an attacker probe the validation. What the copy does commit to is the part the one honest user needs — nothing was submitted, the funds are safe. Open with $store.dsModals.open('cosign-rejected')),
|
|
1167
|
+
open_expr: "$store.dsModals.open('cosign-rejected')",
|
|
1168
|
+
glow_when: ds_glow.call("cosign-rejected"),
|
|
887
1169
|
disabled: !web3_on, openable: true } do %>
|
|
888
1170
|
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
889
|
-
<div class="text-2xl leading-none"
|
|
1171
|
+
<div class="text-2xl leading-none">🛡️</div>
|
|
890
1172
|
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
891
|
-
<span class="block h-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
<%= render layout: "style/modal_specimen", locals: {
|
|
896
|
-
label: "Web3 step-up (no remembered wallet)",
|
|
897
|
-
reference: %(the Web3 Contest "Web3 step-up" card with NO remembered brand (studio/modals/_web3_step_up) — every wallet linked before a host recorded brands lands here, so it is a live population and not a defensive branch. The row falls back to the picker rather than dead-ending. Open with $store.dsModals.open('web3-step-up', {})),
|
|
898
|
-
open_expr: "$store.dsModals.open('web3-step-up', {})",
|
|
899
|
-
disabled: !web3_on, openable: true } do %>
|
|
900
|
-
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
901
|
-
<div class="text-2xl leading-none">🔐</div>
|
|
902
|
-
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
903
|
-
<span class="block h-7 w-full rounded-lg border border-dashed" style="border-color: var(--color-border-strong)"></span>
|
|
1173
|
+
<span class="block h-1.5 w-full rounded" style="background: var(--color-text); opacity: .3"></span>
|
|
1174
|
+
<span class="block h-1.5 w-28 mx-auto rounded" style="background: var(--color-text); opacity: .3"></span>
|
|
1175
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
904
1176
|
</div>
|
|
905
1177
|
<% end %>
|
|
906
1178
|
|
|
907
1179
|
<%# Wallet deposit — standalone top-up card (not part of the walk). %>
|
|
908
1180
|
<%= render layout: "style/modal_specimen", locals: {
|
|
909
1181
|
label: "Wallet deposit",
|
|
910
|
-
reference: %(the Web3
|
|
1182
|
+
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…' })),
|
|
911
1183
|
open_expr: "$store.dsModals.open('wallet-deposit', { neededCents: 500, usdcCents: 120, usdtCents: 0, address: 'Fo1L5demoWALLETaddr9xQ2' })",
|
|
912
1184
|
disabled: !web3_on, openable: true } do %>
|
|
913
1185
|
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
@@ -923,7 +1195,7 @@
|
|
|
923
1195
|
Rewards below (gated by :leveling). %>
|
|
924
1196
|
<%= render layout: "style/modal_specimen", locals: {
|
|
925
1197
|
label: "Entry confirmed",
|
|
926
|
-
reference: %(the Web3
|
|
1198
|
+
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 })),
|
|
927
1199
|
open_expr: "$store.dsModals.open('entry-confirmed', { txSignature: '5xTrDemoSignature1234567890abcXYZ', lobbyUrl: null, seedsEarned: 0, seedsTotal: 40 })",
|
|
928
1200
|
disabled: !web3_on, openable: true } do %>
|
|
929
1201
|
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
@@ -939,7 +1211,7 @@
|
|
|
939
1211
|
</section>
|
|
940
1212
|
|
|
941
1213
|
<%# ===================================================================== %>
|
|
942
|
-
<%# 3. CONTEST ENTRY & ELIGIBILITY — moved directly under Web3
|
|
1214
|
+
<%# 3. CONTEST ENTRY & ELIGIBILITY — moved directly under Web3. The %>
|
|
943
1215
|
<%# walked entry flow (glow follows), plus the honest web2/web3 map. %>
|
|
944
1216
|
<%# ===================================================================== %>
|
|
945
1217
|
<section class="space-y-5">
|
|
@@ -953,17 +1225,20 @@
|
|
|
953
1225
|
<% end %>
|
|
954
1226
|
</div>
|
|
955
1227
|
<p class="text-muted text-sm">
|
|
956
|
-
The pre-entry gate and the full entry flow. The
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
<code class="font-mono text-2xs">
|
|
960
|
-
|
|
1228
|
+
The pre-entry gate and the full entry flow. The age bar is enforced HERE,
|
|
1229
|
+
but its two cards —
|
|
1230
|
+
<code class="font-mono text-2xs">studio/modals/blocks/_birthday</code> (the
|
|
1231
|
+
ask) and <code class="font-mono text-2xs">_age_gate</code> (the refusal)
|
|
1232
|
+
— are shown in the <strong>Profile</strong> section, because they are
|
|
1233
|
+
about who the account is. Both are engine primitives gated by
|
|
1234
|
+
<code class="font-mono text-2xs">Studio.feature?(:age_gate)</code>, and their
|
|
1235
|
+
minimum age, jurisdiction label, endpoint, and legal copy are all
|
|
961
1236
|
<strong>app-supplied</strong> (the engine hardcodes no policy). The entry flow is
|
|
962
1237
|
a <strong>walked sequence</strong> — <strong>Entry tokens → Payment
|
|
963
1238
|
processing → Entry Tokens Minted → Contest enter processing →
|
|
964
1239
|
Contest entered</strong> — and the live card <strong>glows</strong>,
|
|
965
1240
|
following the step machine (the two load steps honor the same min-duration
|
|
966
|
-
convention as Web3
|
|
1241
|
+
convention as Web3 above).
|
|
967
1242
|
</p>
|
|
968
1243
|
<%# The web2/web3 finding — a real divergence, stated honestly, not invented. %>
|
|
969
1244
|
<div class="mt-2 rounded-lg border border-subtle bg-surface p-3 text-xs text-secondary leading-snug">
|
|
@@ -986,23 +1261,14 @@
|
|
|
986
1261
|
</div>
|
|
987
1262
|
</div>
|
|
988
1263
|
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
989
|
-
<%#
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
998
|
-
<div class="grid grid-cols-3 gap-1">
|
|
999
|
-
<span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
1000
|
-
<span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
1001
|
-
<span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
1002
|
-
</div>
|
|
1003
|
-
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
1004
|
-
</div>
|
|
1005
|
-
<% end %>
|
|
1264
|
+
<%# The birthday / age-gate CARDS live in the Profile section — they are
|
|
1265
|
+
about who the account is, and grouping them by subject beats grouping
|
|
1266
|
+
them by the moment they happen to be asked. ENFORCEMENT still lives
|
|
1267
|
+
here: the bar is checked at contest entry, which is what this section is
|
|
1268
|
+
named for. The demo walk is unbroken — confirming the birthday card
|
|
1269
|
+
still advances to Entry tokens below, via the page-level 'age-verified'
|
|
1270
|
+
wiring, which the rename did NOT touch precisely because consuming apps
|
|
1271
|
+
already listen on it. %>
|
|
1006
1272
|
|
|
1007
1273
|
<%# 1. Entry tokens — the picker (walk's entry). Picking a pack advances. %>
|
|
1008
1274
|
<%= render layout: "style/modal_specimen", locals: {
|