studio-engine 0.60.0 → 0.60.2
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 +521 -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/_email_change_pending.html.erb +39 -0
- data/app/views/style/modals/_it_begins.html.erb +46 -0
- data/app/views/style/modals/_network_guard.html.erb +55 -0
- data/app/views/style/modals/_newsletter_email.html.erb +57 -0
- data/app/views/style/modals/_quest_success.html.erb +55 -0
- data/app/views/style/modals/_rate_limit_general.html.erb +61 -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_changed.html.erb +70 -0
- data/app/views/style/modals/_wallet_setup.html.erb +112 -0
- data/lib/studio/version.rb +1 -1
- metadata +18 -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,52 @@
|
|
|
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
|
+
<%# --- Batch 3: the modals that had no card on EITHER page --- %>
|
|
367
|
+
<template x-if="$store.dsModals.current().id === 'it-begins'">
|
|
368
|
+
<div><%= render "style/modals/it_begins" %></div>
|
|
369
|
+
</template>
|
|
370
|
+
<template x-if="$store.dsModals.current().id === 'newsletter-email'">
|
|
371
|
+
<div><%= render "style/modals/newsletter_email" %></div>
|
|
372
|
+
</template>
|
|
373
|
+
<template x-if="$store.dsModals.current().id === 'email-change-pending'">
|
|
374
|
+
<div><%= render "style/modals/email_change_pending" %></div>
|
|
375
|
+
</template>
|
|
376
|
+
<template x-if="$store.dsModals.current().id === 'network-guard'">
|
|
377
|
+
<div><%= render "style/modals/network_guard" %></div>
|
|
378
|
+
</template>
|
|
379
|
+
<template x-if="$store.dsModals.current().id === 'rate-limit-general'">
|
|
380
|
+
<div><%= render "style/modals/rate_limit_general" %></div>
|
|
381
|
+
</template>
|
|
382
|
+
<template x-if="$store.dsModals.current().id === 'wallet-changed'">
|
|
383
|
+
<div><%= render "style/modals/wallet_changed" %></div>
|
|
384
|
+
</template>
|
|
385
|
+
|
|
386
|
+
<%# --- Newsletter exit + the off-page quest celebration --- %>
|
|
387
|
+
<template x-if="$store.dsModals.current().id === 'quest-success'">
|
|
388
|
+
<div><%= render "style/modals/quest_success" %></div>
|
|
389
|
+
</template>
|
|
390
|
+
<template x-if="$store.dsModals.current().id === 'unsubscribe-confirm'">
|
|
391
|
+
<div><%= render "style/modals/unsubscribe_confirm" %></div>
|
|
392
|
+
</template>
|
|
393
|
+
<%# Registered alongside its confirm card, because unsubscribe-confirm
|
|
394
|
+
SWAPS to it — a handoff to an unregistered id renders a blank shell. %>
|
|
395
|
+
<template x-if="$store.dsModals.current().id === 'unsubscribe-goodbye'">
|
|
396
|
+
<div><%= render "style/modals/unsubscribe_goodbye" %></div>
|
|
397
|
+
</template>
|
|
398
|
+
|
|
339
399
|
<%# --- Web3 --- %>
|
|
400
|
+
<template x-if="$store.dsModals.current().id === 'cosign-rejected'">
|
|
401
|
+
<div><%= render "style/modals/cosign_rejected" %></div>
|
|
402
|
+
</template>
|
|
340
403
|
<template x-if="$store.dsModals.current().id === 'wallet-connect'">
|
|
341
404
|
<div><%= render "style/modals/wallet_connect" %></div>
|
|
342
405
|
</template>
|
|
@@ -478,44 +541,6 @@
|
|
|
478
541
|
</div>
|
|
479
542
|
</template>
|
|
480
543
|
|
|
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
544
|
<%# 1. AUTH suite %>
|
|
520
545
|
<%# ===================================================================== %>
|
|
521
546
|
<section class="space-y-5">
|
|
@@ -594,21 +619,128 @@
|
|
|
594
619
|
</section>
|
|
595
620
|
|
|
596
621
|
<%# ===================================================================== %>
|
|
597
|
-
<%# 2. PROFILE —
|
|
622
|
+
<%# 2. PROFILE — who the account is, plus the avatar modals %>
|
|
598
623
|
<%# ===================================================================== %>
|
|
624
|
+
<%# The Onboarding section folded in here on 2026-08-24. It held the post-auth
|
|
625
|
+
chain, but "onboarding" describes WHEN a card is shown, not what it is
|
|
626
|
+
about — and the cards are about the same subject this section already
|
|
627
|
+
owns: the person's name, their birthday, their picture. Grouping by
|
|
628
|
+
subject rather than by moment also stops the guide claiming a chain it
|
|
629
|
+
cannot show, now that the wallet step lives with the wallet cards. %>
|
|
599
630
|
<section class="space-y-5">
|
|
600
631
|
<div class="space-y-1">
|
|
601
632
|
<h3 class="text-xl font-bold text-heading">Profile</h3>
|
|
602
633
|
<p class="text-muted text-sm">
|
|
603
|
-
|
|
634
|
+
Who the account IS — the name it goes by, the birthday that decides
|
|
635
|
+
eligibility, and the picture. The first three cards are asked at signup
|
|
636
|
+
(a host walks them post-auth), the last three are reachable forever from
|
|
637
|
+
the profile page; all six are engine primitives.
|
|
638
|
+
</p>
|
|
639
|
+
<p class="text-muted text-sm">
|
|
640
|
+
<strong>Birthday and Age gate are one flow, deliberately split into two
|
|
641
|
+
cards.</strong> The birthday card ASKS; when the answer is too young the
|
|
642
|
+
age-gate card ANSWERS, carrying a countdown to the day they qualify, a way
|
|
643
|
+
to watch instead, and a way back to fix a mistyped year. Until 2026-08-24
|
|
644
|
+
that refusal lived inside the birthday card as red text beside a disabled
|
|
645
|
+
button — a state with nothing to press. Open <strong>Birthday</strong>
|
|
646
|
+
with <strong>Age validation</strong> on and submit any date to walk the
|
|
647
|
+
handoff; switch the toggle off for the other mode, an app with no age bar
|
|
648
|
+
that just wants the date.
|
|
649
|
+
</p>
|
|
650
|
+
<p class="text-muted text-sm">
|
|
651
|
+
The avatar modals
|
|
604
652
|
(<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.
|
|
653
|
+
<code class="font-mono text-2xs">studio/modals/_image_upload</code>)
|
|
654
|
+
open live here —
|
|
655
|
+
<code class="font-mono text-2xs">studio/cropper_assets</code> is rendered
|
|
656
|
+
above, so cropper.js loads and the modals mount on this page's host.
|
|
609
657
|
</p>
|
|
610
658
|
</div>
|
|
611
659
|
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
660
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
661
|
+
label: "First name",
|
|
662
|
+
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),
|
|
663
|
+
open_expr: "$store.dsModals.open('onboarding-first-name')",
|
|
664
|
+
glow_when: ds_glow.call("onboarding-first-name")
|
|
665
|
+
} do %>
|
|
666
|
+
<%# Every other card in this section shows the shape of the modal it
|
|
667
|
+
opens; this one showed a paragraph of prose, so the row read as a
|
|
668
|
+
missing thumbnail rather than a deliberate difference. It is a
|
|
669
|
+
labelled text field and a submit — draw that. %>
|
|
670
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
671
|
+
<span class="block h-2 w-20 rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
672
|
+
<span class="block h-7 w-full rounded-lg border" style="border-color: var(--color-border-strong)"></span>
|
|
673
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
674
|
+
<%# the skip link that sits under the button, and is also a skip %>
|
|
675
|
+
<span class="block h-1.5 w-14 mx-auto rounded" style="background: var(--color-text); opacity: .25"></span>
|
|
676
|
+
</div>
|
|
677
|
+
<p class="text-muted text-2xs mt-3">
|
|
678
|
+
Saving or skipping dispatches
|
|
679
|
+
<code class="font-mono">ds-onboarding-step-done</code> here rather than
|
|
680
|
+
the app-level event, so the specimen cannot drive a real chain.
|
|
681
|
+
</p>
|
|
682
|
+
<% end %>
|
|
683
|
+
|
|
684
|
+
<%# BIRTHDAY — the ask. ONE card, two modes, because the difference between
|
|
685
|
+
them is a LOCAL the host sets and not a different card:
|
|
686
|
+
|
|
687
|
+
age validation ON — the app has a bar, so the card says what it is
|
|
688
|
+
and an under-age date hands off to the age gate.
|
|
689
|
+
age validation OFF — the app just wants the date. McRitchie Studio
|
|
690
|
+
has no gate and still wants a birthday to wish
|
|
691
|
+
you a happy one; the card then drops the age
|
|
692
|
+
line, the eligibility wording and the handoff.
|
|
693
|
+
|
|
694
|
+
It was two cards until 2026-08-25 ("Birthday" and "Birthday (underage)"),
|
|
695
|
+
which read as two components and hid the fact that the second was the
|
|
696
|
+
first with a prop flipped. The toggle is the honest shape — the same one
|
|
697
|
+
Profile Leveling and the Processing card use.
|
|
698
|
+
|
|
699
|
+
THE BADGE SAYS "age validation", NOT "age gate off". The old badge
|
|
700
|
+
reported this app's Studio.feature?(:age_gate) flag, which is the wrong
|
|
701
|
+
question for a card whose whole point is that the RULE is per-app:
|
|
702
|
+
turf-monster's is jurisdiction-dependent, a hub app's may not exist. %>
|
|
703
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
704
|
+
label: "Birthday",
|
|
705
|
+
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 })),
|
|
706
|
+
card_data: "opts: { validates: true }",
|
|
707
|
+
toggles: [{ model: "opts.validates", label: "Age validation" }],
|
|
708
|
+
open_expr: "$store.dsModals.open('birthday', { validates: opts.validates })",
|
|
709
|
+
glow_when: ds_glow.call("birthday"),
|
|
710
|
+
openable: true } do %>
|
|
711
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2 text-center">
|
|
712
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
713
|
+
<%# month / day / year %>
|
|
714
|
+
<div class="grid grid-cols-3 gap-1">
|
|
715
|
+
<span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
716
|
+
<span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
717
|
+
<span class="h-6 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
718
|
+
</div>
|
|
719
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
720
|
+
</div>
|
|
721
|
+
<% end %>
|
|
722
|
+
|
|
723
|
+
<%# AGE GATE — the answer, when the answer is no. %>
|
|
724
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
725
|
+
label: "Age gate",
|
|
726
|
+
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 })),
|
|
727
|
+
open_expr: "$store.dsModals.open('age-gate', { minAge: 21, state: 'CA', dobYear: new Date().getFullYear() - 16, dobMonth: 6, dobDay: 15 })",
|
|
728
|
+
glow_when: ds_glow.call("age-gate"),
|
|
729
|
+
openable: true } do %>
|
|
730
|
+
<%# A teddy bear, not the red X pill — the card is not an error state and
|
|
731
|
+
its thumbnail should not promise one. %>
|
|
732
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2 text-center">
|
|
733
|
+
<div class="text-2xl leading-none">🧸</div>
|
|
734
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
735
|
+
<%# the countdown line, which is the widest thing on the real card %>
|
|
736
|
+
<span class="block h-1.5 w-full rounded" style="background: var(--color-text); opacity: .3"></span>
|
|
737
|
+
<%# primary = watch instead; secondary = go fix the date %>
|
|
738
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
739
|
+
<span class="block h-1.5 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .25"></span>
|
|
740
|
+
</div>
|
|
741
|
+
<% end %>
|
|
742
|
+
|
|
743
|
+
|
|
612
744
|
<%# Image upload FIRST — the empty picker (crop-photo opened with no imageUrl).
|
|
613
745
|
cropReady: false is the defined starting sub-state; the crop factory
|
|
614
746
|
flips it true once an image is picked, moving the glow to Crop photo. %>
|
|
@@ -651,6 +783,24 @@
|
|
|
651
783
|
<span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
652
784
|
</div>
|
|
653
785
|
<% end %>
|
|
786
|
+
|
|
787
|
+
<%# email-change-pending — the informational handoff after an email change.
|
|
788
|
+
NOT a success card: nothing has changed yet, and the confirmation goes
|
|
789
|
+
to the CURRENT address so an unrequested change gets noticed. %>
|
|
790
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
791
|
+
label: "Email change pending",
|
|
792
|
+
reference: %(the Profile "Confirm your email change" handoff (style/modals/_email_change_pending) — turf-monster's modals/_email_change_pending. Composes the engine's blocks/_card_header; the pending change and the confirmation mail are the app's (AccountsController#update). It is an INFORMATIONAL handoff, not a celebration — nothing has changed yet, and a card that read as success would tell the person the opposite of the truth. The link goes to the CURRENT address precisely so someone who did NOT request the change finds out, which is why both addresses are shown. Distinct from the check-email card: no resend, no contest wiring, one button that closes. Open with $store.dsModals.open('email-change-pending', { currentEmail: 'you@example.com', newEmail: 'new@example.com' })),
|
|
793
|
+
open_expr: "$store.dsModals.open('email-change-pending', { currentEmail: 'you@example.com', newEmail: 'new@example.com' })",
|
|
794
|
+
glow_when: ds_glow.call("email-change-pending") } do %>
|
|
795
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
796
|
+
<div class="text-2xl leading-none">📬</div>
|
|
797
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
798
|
+
<%# both addresses, which is the card's point %>
|
|
799
|
+
<span class="block h-1.5 w-28 mx-auto rounded" style="background: var(--color-text); opacity: .3"></span>
|
|
800
|
+
<span class="block h-1.5 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .3"></span>
|
|
801
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
802
|
+
</div>
|
|
803
|
+
<% end %>
|
|
654
804
|
</div>
|
|
655
805
|
</section>
|
|
656
806
|
|
|
@@ -770,17 +920,91 @@
|
|
|
770
920
|
<span class="block h-1.5 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
771
921
|
</div>
|
|
772
922
|
<% end %>
|
|
923
|
+
|
|
924
|
+
<%# THE WAY OUT. The three cards above walk a user IN; a section that only
|
|
925
|
+
ever demonstrates opt-in is showing the happy half of a flow, not the
|
|
926
|
+
flow. These two are the exit, and they are a PAIR — the confirm card
|
|
927
|
+
swaps to the goodbye beat, so reviewing either alone misses the handoff.
|
|
928
|
+
App-specific (the engine owns no newsletter); engine chrome only. %>
|
|
929
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
930
|
+
label: "Leave the newsletter?",
|
|
931
|
+
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')),
|
|
932
|
+
open_expr: "$store.dsModals.open('unsubscribe-confirm')",
|
|
933
|
+
glow_when: ds_glow.call("unsubscribe-confirm") } do %>
|
|
934
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
935
|
+
<div class="text-2xl leading-none">📭</div>
|
|
936
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
937
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
938
|
+
<%# the quiet out, deliberately smaller than the destructive action %>
|
|
939
|
+
<span class="block h-1.5 w-12 mx-auto rounded" style="background: var(--color-text); opacity: .25"></span>
|
|
940
|
+
</div>
|
|
941
|
+
<% end %>
|
|
942
|
+
|
|
943
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
944
|
+
label: "See you later",
|
|
945
|
+
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')),
|
|
946
|
+
open_expr: "$store.dsModals.open('unsubscribe-goodbye')",
|
|
947
|
+
glow_when: ds_glow.call("unsubscribe-goodbye") } do %>
|
|
948
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
949
|
+
<div class="text-2xl leading-none">✌️</div>
|
|
950
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
951
|
+
<span class="block h-1.5 w-28 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
952
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
953
|
+
</div>
|
|
954
|
+
<% end %>
|
|
955
|
+
|
|
956
|
+
<%# THE SAME BEAT, REACHED FROM SOMEWHERE ELSE. Great Username (two cards up)
|
|
957
|
+
celebrates INSIDE the modal that did the work. This one is what a host
|
|
958
|
+
shows when the quest completed somewhere with no card to advance in
|
|
959
|
+
place — turf's /account username change — so the celebration arrives on
|
|
960
|
+
its own. The toggle is the fork at its foot, which is the whole reason
|
|
961
|
+
it is one card and not two. %>
|
|
962
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
963
|
+
label: "Quest success (off-page)",
|
|
964
|
+
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 })),
|
|
965
|
+
card_data: "opts: { subscribed: false }",
|
|
966
|
+
toggles: [{ model: "opts.subscribed", label: "Subscribed" }],
|
|
967
|
+
open_expr: "$store.dsModals.open('quest-success', { subscribed: opts.subscribed, seedsTotal: 25 })",
|
|
968
|
+
glow_when: ds_glow.call("quest-success") } do %>
|
|
969
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
970
|
+
<div class="text-2xl leading-none">🌱</div>
|
|
971
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
972
|
+
<%# the seeds bar, which is the card's middle and the engine's %>
|
|
973
|
+
<span class="flex gap-1">
|
|
974
|
+
<span class="flex-1 h-2 rounded" style="background: var(--color-cta)"></span>
|
|
975
|
+
<span class="flex-1 h-2 rounded" style="background: var(--color-cta); opacity:.4"></span>
|
|
976
|
+
<span class="flex-1 h-2 rounded" style="background: var(--color-cta); opacity:.2"></span>
|
|
977
|
+
</span>
|
|
978
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
979
|
+
</div>
|
|
980
|
+
<% end %>
|
|
981
|
+
|
|
982
|
+
<%# newsletter-email — the branch a wallet-only account has to take: no
|
|
983
|
+
email on file, so the quest asks for one mid-flight. %>
|
|
984
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
985
|
+
label: "Newsletter email",
|
|
986
|
+
reference: %(the web3 newsletter email capture (style/modals/_newsletter_email) — turf-monster's modals/_newsletter_email. Composes the engine's blocks/_card_header; the newsletter and the seeds are the app's. It exists because an account signed in with a WALLET has no email on file — every other newsletter path can assume one, and this is the branch that cannot. It asks mid-quest rather than up front, where it would read as a signup wall on a product that deliberately has none. The real card hands the address BACK to props.onSubmit and closes, so the app keeps ONE subscribe path instead of two; the specimen resolves locally so it stays on screen. Open with $store.dsModals.open('newsletter-email')),
|
|
987
|
+
open_expr: "$store.dsModals.open('newsletter-email')",
|
|
988
|
+
glow_when: ds_glow.call("newsletter-email") } do %>
|
|
989
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
990
|
+
<div class="text-2xl leading-none">📬</div>
|
|
991
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
992
|
+
<%# the ask itself — an email field, which no other newsletter card has %>
|
|
993
|
+
<span class="block h-5 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
994
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
995
|
+
</div>
|
|
996
|
+
<% end %>
|
|
773
997
|
</div>
|
|
774
998
|
</section>
|
|
775
999
|
|
|
776
1000
|
<%# ===================================================================== %>
|
|
777
|
-
<%# 2. WEB3
|
|
1001
|
+
<%# 2. WEB3 — the wallet + on-chain modals, in the order a player meets them. %>
|
|
778
1002
|
<%# Gated by :web3; off = disabled-but-present-yet-openable. %>
|
|
779
1003
|
<%# ===================================================================== %>
|
|
780
1004
|
<section class="space-y-5">
|
|
781
1005
|
<div class="space-y-1">
|
|
782
1006
|
<div class="flex flex-wrap items-center gap-3">
|
|
783
|
-
<h3 class="text-xl font-bold text-heading">Web3
|
|
1007
|
+
<h3 class="text-xl font-bold text-heading">Web3</h3>
|
|
784
1008
|
<% if web3_on %>
|
|
785
1009
|
<span class="badge" style="color: var(--color-success); border-color: var(--color-success)">enabled on this app</span>
|
|
786
1010
|
<% else %>
|
|
@@ -792,10 +1016,14 @@
|
|
|
792
1016
|
<code class="font-mono text-2xs">Studio.feature?(:web3)</code>. Web3 is
|
|
793
1017
|
<strong><%= web3_on ? "on" : "off" %></strong> here, so these render
|
|
794
1018
|
<%= web3_on ? "live." : "greyed and badged — but STILL openable as a preview." %>
|
|
795
|
-
The
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
1019
|
+
The cards run in the order a player meets them — <strong>get a wallet,
|
|
1020
|
+
prove it, spend from it</strong>: Connect wallet → Setup Wallet →
|
|
1021
|
+
the two Sign Wallet states → Processing → success or error →
|
|
1022
|
+
top-up → Entry confirmed. Inside that, <strong>Connect Wallet →
|
|
1023
|
+
Processing on-chain transaction → On-chain success</strong> is a
|
|
1024
|
+
<strong>walked flow</strong> and the live card <strong>glows</strong>,
|
|
1025
|
+
following it as it advances (open Connect Wallet and pick a wallet to watch
|
|
1026
|
+
it walk).
|
|
799
1027
|
The <strong>Processing</strong> card is a load modal: its
|
|
800
1028
|
<strong>success/error toggle</strong> chooses whether it resolves to success
|
|
801
1029
|
or the error state, after the <strong>minimum-visible duration</strong>
|
|
@@ -804,7 +1032,7 @@
|
|
|
804
1032
|
marks inline from the engine</strong>
|
|
805
1033
|
(<code class="font-mono text-2xs">studio/modals/blocks/_wallet_brand_sprite</code>),
|
|
806
1034
|
so every app inherits them with no per-app icon files.
|
|
807
|
-
The two <strong>
|
|
1035
|
+
The two <strong>Sign Wallet</strong> cards are a different kind of thing
|
|
808
1036
|
from the rest of this section: they render
|
|
809
1037
|
<code class="font-mono text-2xs">studio/modals/_web3_step_up</code> —
|
|
810
1038
|
the <strong>REAL shared partial a host renders in production</strong>,
|
|
@@ -813,28 +1041,136 @@
|
|
|
813
1041
|
<strong>standard web3 auth button</strong>: a wallet row (brand mark, the
|
|
814
1042
|
wallet’s own name, Installed badge, chevron) rather than a filled
|
|
815
1043
|
CTA, glowing via <code class="font-mono text-2xs">pulse-cta</code> because
|
|
816
|
-
it is the one thing to press.
|
|
1044
|
+
it is the one thing to press. Do not confuse them with <strong>Setup
|
|
1045
|
+
Wallet</strong>, immediately before them: that one asks an account to
|
|
1046
|
+
<strong>get</strong> a wallet, these ask an account to
|
|
1047
|
+
<strong>prove</strong> the one it already has — opposite populations
|
|
1048
|
+
(<code class="font-mono text-2xs">WalletSetupPolicy</code> exits at its own
|
|
1049
|
+
step 1 for an account that holds a wallet), so no user ever meets both.
|
|
1050
|
+
They sit next to each other anyway, because the pair is easiest to keep
|
|
1051
|
+
straight when you can read them side by side.
|
|
817
1052
|
</p>
|
|
818
1053
|
</div>
|
|
1054
|
+
<%# ONE ordered run, read left to right as a player meets it: get a wallet,
|
|
1055
|
+
prove it, spend from it. This carried three labelled h4 runs for a day
|
|
1056
|
+
(The walk / Proving a wallet / Funding & confirmation) and they were
|
|
1057
|
+
removed on 2026-08-25 — the player order already groups the cards, and the
|
|
1058
|
+
headings were a second, competing structure over the same nine. What holds
|
|
1059
|
+
the order now is a test (style_page_test's WEB3_ORDER), not a label a
|
|
1060
|
+
reader has to trust. %>
|
|
819
1061
|
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
820
1062
|
<%# 1. Connect wallet — the walk's entry; picking a wallet swaps to Processing. %>
|
|
821
1063
|
<%= render layout: "style/modal_specimen", locals: {
|
|
822
1064
|
label: "Connect wallet",
|
|
823
|
-
reference: %(the Web3
|
|
1065
|
+
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
1066
|
open_expr: "$store.dsModals.open('wallet-connect')",
|
|
825
1067
|
glow_when: ds_glow.call("wallet-connect"),
|
|
826
1068
|
disabled: !web3_on, openable: true } do %>
|
|
1069
|
+
<%# A PICKER: brand tiles with names beside them, which is what this modal
|
|
1070
|
+
actually is. Two empty outlines said "form with two inputs" and were
|
|
1071
|
+
the least informative sketch in the section — the one card whose
|
|
1072
|
+
subject is a LIST of wallets showed no wallets at all. Three rows,
|
|
1073
|
+
because the picker ships Phantom / Solflare / Backpack marks inline. %>
|
|
827
1074
|
<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
|
-
|
|
1075
|
+
<span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1076
|
+
<% [0.9, 0.6, 0.45].each do |tint| %>
|
|
1077
|
+
<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)">
|
|
1078
|
+
<span class="w-3.5 h-3.5 rounded shrink-0" style="background: var(--color-primary); opacity: <%= tint %>"></span>
|
|
1079
|
+
<span class="block h-1.5 w-9 rounded" style="background: var(--color-text); opacity: .35"></span>
|
|
1080
|
+
<span class="block h-1.5 w-1.5 ml-auto rounded-full" style="background: var(--color-text); opacity: .25"></span>
|
|
1081
|
+
</span>
|
|
1082
|
+
<% end %>
|
|
1083
|
+
</div>
|
|
1084
|
+
<% end %>
|
|
1085
|
+
|
|
1086
|
+
<%# WALLET SETUP — new 2026-08-24, and moved here from Onboarding the same
|
|
1087
|
+
day. It is grouped with the wallet cards rather than with the signup
|
|
1088
|
+
chain because that is what it IS: getting a wallet. The card still
|
|
1089
|
+
renders a 3-of-3 pill, which is the host's chain position and not this
|
|
1090
|
+
section's business.
|
|
1091
|
+
ONE card with a "Wallet detected" toggle rather than two cards sharing a
|
|
1092
|
+
modal id: the two Sign Wallet cards below are what that shape costs. %>
|
|
1093
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
1094
|
+
label: "Setup Wallet",
|
|
1095
|
+
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 })),
|
|
1096
|
+
card_data: "opts: { detected: false }",
|
|
1097
|
+
toggles: [{ model: "opts.detected", label: "Wallet detected" }],
|
|
1098
|
+
open_expr: "$store.dsModals.open('wallet-setup', { detected: opts.detected })",
|
|
1099
|
+
glow_when: ds_glow.call("wallet-setup"),
|
|
1100
|
+
disabled: !web3_on, openable: true } do %>
|
|
1101
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
1102
|
+
<span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1103
|
+
<%# the 3-of-3 pill, filled — the whole reason this card sits here %>
|
|
1104
|
+
<span class="flex gap-1">
|
|
1105
|
+
<span class="flex-1 h-1.5 rounded" style="background: var(--color-cta)"></span>
|
|
1106
|
+
<span class="flex-1 h-1.5 rounded" style="background: var(--color-cta)"></span>
|
|
1107
|
+
<span class="flex-1 h-1.5 rounded" style="background: var(--color-cta)"></span>
|
|
1108
|
+
</span>
|
|
1109
|
+
<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)">
|
|
1110
|
+
<span class="w-4 h-4 rounded shrink-0" style="background: var(--color-primary)"></span>
|
|
1111
|
+
<span class="block h-1.5 w-8 rounded" style="background: var(--color-text); opacity: .35"></span>
|
|
1112
|
+
</span>
|
|
1113
|
+
<%# the walkthrough placeholder, the card's second half %>
|
|
1114
|
+
<span class="block w-full rounded" style="height: 1.6rem; background: var(--color-text); opacity: .10"></span>
|
|
1115
|
+
</div>
|
|
1116
|
+
<% end %>
|
|
1117
|
+
|
|
1118
|
+
<%# SIGN WALLET — the web2-session-on-a-wallet-account card. NOT part of the
|
|
1119
|
+
walk above: that walk is one on-chain transaction, this is an AUTH state
|
|
1120
|
+
that precedes any of it. Two specimens because the card has exactly two
|
|
1121
|
+
shapes, and the second is what every wallet linked before a host recorded
|
|
1122
|
+
brands will see.
|
|
1123
|
+
|
|
1124
|
+
OPPOSITE POPULATION to the "Setup Wallet" card two up in this
|
|
1125
|
+
section, and worth stating because the two look alike: that one asks
|
|
1126
|
+
"get a wallet", this one asks "prove the wallet you already have".
|
|
1127
|
+
WalletSetupPolicy exits at its own step 1 for an account that holds a
|
|
1128
|
+
wallet, so no user can ever meet both. %>
|
|
1129
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
1130
|
+
label: "Sign Wallet",
|
|
1131
|
+
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' })),
|
|
1132
|
+
open_expr: "$store.dsModals.open('web3-step-up', { provider: 'phantom', providerLabel: 'Phantom', walletHint: '7xKp…JZ2Q' })",
|
|
1133
|
+
glow_when: ds_glow.call("web3-step-up", provider: true),
|
|
1134
|
+
disabled: !web3_on, openable: true } do %>
|
|
1135
|
+
<%# A FILLED row: brand tile, the wallet's name, an Installed badge. The
|
|
1136
|
+
two step-up thumbnails used to differ by a single border-dashed class
|
|
1137
|
+
on a 20px row, which is not a difference anyone can see at thumbnail
|
|
1138
|
+
size — and these two cards are the pair most likely to be mistaken for
|
|
1139
|
+
duplicates, because they share a modal id AND a title. So the sketch
|
|
1140
|
+
now shows the actual distinction: this card KNOWS which wallet. %>
|
|
1141
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
1142
|
+
<div class="text-2xl leading-none">🔐</div>
|
|
1143
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1144
|
+
<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)">
|
|
1145
|
+
<span class="w-4 h-4 rounded shrink-0" style="background: var(--color-primary)"></span>
|
|
1146
|
+
<span class="block h-1.5 w-8 rounded" style="background: var(--color-text); opacity: .35"></span>
|
|
1147
|
+
<span class="block h-2.5 w-7 ml-auto rounded-full border" style="border-color: var(--color-primary)"></span>
|
|
1148
|
+
</span>
|
|
1149
|
+
</div>
|
|
1150
|
+
<% end %>
|
|
1151
|
+
|
|
1152
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
1153
|
+
label: "Sign Wallet (no remembered brand)",
|
|
1154
|
+
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', {})),
|
|
1155
|
+
open_expr: "$store.dsModals.open('web3-step-up', {})",
|
|
1156
|
+
glow_when: ds_glow.call("web3-step-up", provider: false),
|
|
1157
|
+
disabled: !web3_on, openable: true } do %>
|
|
1158
|
+
<%# An EMPTY slot: dashed outline, no brand tile, a "?" where the wallet's
|
|
1159
|
+
name would be. Reads at a glance as "we do not know which wallet",
|
|
1160
|
+
which is the whole difference between this card and the one before it. %>
|
|
1161
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
1162
|
+
<div class="text-2xl leading-none">🔐</div>
|
|
1163
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1164
|
+
<span class="flex items-center justify-center h-7 w-full rounded-lg border border-dashed" style="border-color: var(--color-border-strong)">
|
|
1165
|
+
<span class="text-xs font-bold text-muted leading-none">?</span>
|
|
1166
|
+
</span>
|
|
831
1167
|
</div>
|
|
832
1168
|
<% end %>
|
|
833
1169
|
|
|
834
1170
|
<%# 2. Processing — the load modal; the success/error toggle picks the resolution. %>
|
|
835
1171
|
<%= render layout: "style/modal_specimen", locals: {
|
|
836
1172
|
label: "Processing on-chain tx",
|
|
837
|
-
reference: %(the Web3
|
|
1173
|
+
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
1174
|
card_data: "opts: { demoError: false }",
|
|
839
1175
|
toggles: [{ model: "opts.demoError", label: "Resolve to error" }],
|
|
840
1176
|
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 +1185,7 @@
|
|
|
849
1185
|
<%# 3. On-chain success — the resolved success end-state (the walk's finish). %>
|
|
850
1186
|
<%= render layout: "style/modal_specimen", locals: {
|
|
851
1187
|
label: "On-chain success",
|
|
852
|
-
reference: %(the Web3
|
|
1188
|
+
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
1189
|
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
1190
|
glow_when: ds_glow.call("onchain-tx", state: "success"),
|
|
855
1191
|
disabled: !web3_on, openable: true } do %>
|
|
@@ -864,7 +1200,7 @@
|
|
|
864
1200
|
<%# On-chain error — the resolved error end-state (the toggle's other branch). %>
|
|
865
1201
|
<%= render layout: "style/modal_specimen", locals: {
|
|
866
1202
|
label: "On-chain error",
|
|
867
|
-
reference: %(the Web3
|
|
1203
|
+
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
1204
|
open_expr: "$store.dsModals.open('onchain-tx', { state: 'error', title: 'Could not confirm', errorMessage: 'The transaction was rejected. Give it another try.' })",
|
|
869
1205
|
glow_when: ds_glow.call("onchain-tx", state: "error"),
|
|
870
1206
|
disabled: !web3_on, openable: true } do %>
|
|
@@ -874,40 +1210,30 @@
|
|
|
874
1210
|
</div>
|
|
875
1211
|
<% end %>
|
|
876
1212
|
|
|
877
|
-
<%#
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
<%= render layout: "style/modal_specimen", locals: {
|
|
883
|
-
label: "Web3 step-up (remembered wallet)",
|
|
884
|
-
reference: %(the Web3 Contest "Web3 step-up" 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' })),
|
|
885
|
-
open_expr: "$store.dsModals.open('web3-step-up', { provider: 'phantom', providerLabel: 'Phantom', walletHint: '7xKp…JZ2Q' })",
|
|
886
|
-
glow_when: ds_glow.call("web3-step-up"),
|
|
887
|
-
disabled: !web3_on, openable: true } do %>
|
|
888
|
-
<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">🔐</div>
|
|
890
|
-
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
891
|
-
<span class="block h-7 w-full rounded-lg border" style="border-color: var(--color-border-strong)"></span>
|
|
892
|
-
</div>
|
|
893
|
-
<% end %>
|
|
894
|
-
|
|
1213
|
+
<%# THE OTHER FAILURE, and it is a different KIND of failure from the one
|
|
1214
|
+
above. On-chain error is the chain saying no. This is the SERVER saying
|
|
1215
|
+
no — refusing to co-sign because the transaction it was handed did not
|
|
1216
|
+
match the entry it prepared. They sit together because a reviewer
|
|
1217
|
+
reading either alone would not know the other existed. %>
|
|
895
1218
|
<%= render layout: "style/modal_specimen", locals: {
|
|
896
|
-
label: "
|
|
897
|
-
reference: %(the
|
|
898
|
-
open_expr: "$store.dsModals.open('
|
|
1219
|
+
label: "Co-sign refused",
|
|
1220
|
+
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')),
|
|
1221
|
+
open_expr: "$store.dsModals.open('cosign-rejected')",
|
|
1222
|
+
glow_when: ds_glow.call("cosign-rejected"),
|
|
899
1223
|
disabled: !web3_on, openable: true } do %>
|
|
900
1224
|
<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"
|
|
1225
|
+
<div class="text-2xl leading-none">🛡️</div>
|
|
902
1226
|
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
903
|
-
<span class="block h-
|
|
1227
|
+
<span class="block h-1.5 w-full rounded" style="background: var(--color-text); opacity: .3"></span>
|
|
1228
|
+
<span class="block h-1.5 w-28 mx-auto rounded" style="background: var(--color-text); opacity: .3"></span>
|
|
1229
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
904
1230
|
</div>
|
|
905
1231
|
<% end %>
|
|
906
1232
|
|
|
907
1233
|
<%# Wallet deposit — standalone top-up card (not part of the walk). %>
|
|
908
1234
|
<%= render layout: "style/modal_specimen", locals: {
|
|
909
1235
|
label: "Wallet deposit",
|
|
910
|
-
reference: %(the Web3
|
|
1236
|
+
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
1237
|
open_expr: "$store.dsModals.open('wallet-deposit', { neededCents: 500, usdcCents: 120, usdtCents: 0, address: 'Fo1L5demoWALLETaddr9xQ2' })",
|
|
912
1238
|
disabled: !web3_on, openable: true } do %>
|
|
913
1239
|
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
@@ -923,7 +1249,7 @@
|
|
|
923
1249
|
Rewards below (gated by :leveling). %>
|
|
924
1250
|
<%= render layout: "style/modal_specimen", locals: {
|
|
925
1251
|
label: "Entry confirmed",
|
|
926
|
-
reference: %(the Web3
|
|
1252
|
+
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
1253
|
open_expr: "$store.dsModals.open('entry-confirmed', { txSignature: '5xTrDemoSignature1234567890abcXYZ', lobbyUrl: null, seedsEarned: 0, seedsTotal: 40 })",
|
|
928
1254
|
disabled: !web3_on, openable: true } do %>
|
|
929
1255
|
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
@@ -935,11 +1261,62 @@
|
|
|
935
1261
|
<span class="block h-4 w-full rounded" style="background: var(--color-cta)"></span>
|
|
936
1262
|
</div>
|
|
937
1263
|
<% end %>
|
|
1264
|
+
|
|
1265
|
+
<%# network-guard — the explicit acknowledgement before a wallet request.
|
|
1266
|
+
Continue stays INERT until the box is ticked and Cancel carries equal
|
|
1267
|
+
weight: a confirmation whose safe path is harder to reach is theatre. %>
|
|
1268
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
1269
|
+
label: "Network guard",
|
|
1270
|
+
reference: %(the Web3 network confirmation (style/modals/_network_guard) — turf-monster's modals/_network_guard, shown before a wallet request when the app wants the network acknowledged. Composes the engine's blocks/_card_header and nothing else: the engine detects no network and no environment, so every label here is app-supplied and only ever DISPLAYED. A request signed against the wrong network looks identical to a correct one until it is irreversible, so the card NAMES both facts, keeps Continue inert until an explicit tick, and gives Cancel equal visual weight rather than burying it. Open with $store.dsModals.open('network-guard', { networkLabel: 'devnet', environmentLabel: 'QA' })),
|
|
1271
|
+
open_expr: "$store.dsModals.open('network-guard', { networkLabel: 'devnet', environmentLabel: 'QA' })",
|
|
1272
|
+
glow_when: ds_glow.call("network-guard"),
|
|
1273
|
+
disabled: !web3_on, openable: true } do %>
|
|
1274
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
1275
|
+
<div class="text-2xl leading-none">⚠️</div>
|
|
1276
|
+
<%# the two named facts, then the tick that gates Continue %>
|
|
1277
|
+
<span class="block h-1.5 w-full rounded" style="background: var(--color-text); opacity: .3"></span>
|
|
1278
|
+
<span class="block h-1.5 w-full rounded" style="background: var(--color-text); opacity: .3"></span>
|
|
1279
|
+
<span class="flex items-center gap-1.5">
|
|
1280
|
+
<span class="block h-2.5 w-2.5 rounded-sm border" style="border-color: var(--color-border-strong)"></span>
|
|
1281
|
+
<span class="block h-1.5 flex-1 rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1282
|
+
</span>
|
|
1283
|
+
<%# equal weight, deliberately %>
|
|
1284
|
+
<span class="grid grid-cols-2 gap-1.5">
|
|
1285
|
+
<span class="block h-4 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
1286
|
+
<span class="block h-4 rounded" style="background: var(--color-cta); opacity: .5"></span>
|
|
1287
|
+
</span>
|
|
1288
|
+
</div>
|
|
1289
|
+
<% end %>
|
|
1290
|
+
|
|
1291
|
+
<%# wallet-changed — the extension switched accounts underneath the session.
|
|
1292
|
+
Both addresses are shown because recognising your own is the only way to
|
|
1293
|
+
tell a deliberate switch from an accident. %>
|
|
1294
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
1295
|
+
label: "Wallet changed",
|
|
1296
|
+
reference: %(the Web3 "Wallet changed" reconciliation (style/modals/_wallet_changed) — turf-monster's modals/_wallet_changed, fired when the person switches accounts inside their wallet extension while signed in. Composes the engine's blocks/_card_header; the switch itself belongs to turf's wallet store (continueSwitch) and the engine owns no wallet. Nothing is broken — but the session and the extension now disagree about who this is, and every on-chain action from here would use an identity the page is not showing. So it prints BOTH addresses, truncated the Solana way, because recognising your own is the one check the app cannot make for you. Continuing re-derives a session against the new wallet, which is a round trip, so the button carries a busy state rather than looking dead. Open with $store.dsModals.open('wallet-changed', { currentAddress: '…', newAddress: '…' })),
|
|
1297
|
+
open_expr: "$store.dsModals.open('wallet-changed', { currentAddress: '7xKpDemoSignedInWallet9JZ2Q', newAddress: '9mQtDemoSwitchedWalletLd4V' })",
|
|
1298
|
+
glow_when: ds_glow.call("wallet-changed"),
|
|
1299
|
+
disabled: !web3_on, openable: true } do %>
|
|
1300
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
1301
|
+
<div class="text-2xl leading-none">🔀</div>
|
|
1302
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1303
|
+
<%# the two truncated addresses — the comparison IS the card %>
|
|
1304
|
+
<span class="flex items-center justify-between gap-2">
|
|
1305
|
+
<span class="block h-1.5 w-10 rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1306
|
+
<span class="block h-1.5 w-12 rounded" style="background: var(--color-text); opacity: .35"></span>
|
|
1307
|
+
</span>
|
|
1308
|
+
<span class="flex items-center justify-between gap-2">
|
|
1309
|
+
<span class="block h-1.5 w-10 rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1310
|
+
<span class="block h-1.5 w-12 rounded" style="background: var(--color-text); opacity: .35"></span>
|
|
1311
|
+
</span>
|
|
1312
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
1313
|
+
</div>
|
|
1314
|
+
<% end %>
|
|
938
1315
|
</div>
|
|
939
1316
|
</section>
|
|
940
1317
|
|
|
941
1318
|
<%# ===================================================================== %>
|
|
942
|
-
<%# 3. CONTEST ENTRY & ELIGIBILITY — moved directly under Web3
|
|
1319
|
+
<%# 3. CONTEST ENTRY & ELIGIBILITY — moved directly under Web3. The %>
|
|
943
1320
|
<%# walked entry flow (glow follows), plus the honest web2/web3 map. %>
|
|
944
1321
|
<%# ===================================================================== %>
|
|
945
1322
|
<section class="space-y-5">
|
|
@@ -953,17 +1330,20 @@
|
|
|
953
1330
|
<% end %>
|
|
954
1331
|
</div>
|
|
955
1332
|
<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
|
-
|
|
1333
|
+
The pre-entry gate and the full entry flow. The age bar is enforced HERE,
|
|
1334
|
+
but its two cards —
|
|
1335
|
+
<code class="font-mono text-2xs">studio/modals/blocks/_birthday</code> (the
|
|
1336
|
+
ask) and <code class="font-mono text-2xs">_age_gate</code> (the refusal)
|
|
1337
|
+
— are shown in the <strong>Profile</strong> section, because they are
|
|
1338
|
+
about who the account is. Both are engine primitives gated by
|
|
1339
|
+
<code class="font-mono text-2xs">Studio.feature?(:age_gate)</code>, and their
|
|
1340
|
+
minimum age, jurisdiction label, endpoint, and legal copy are all
|
|
961
1341
|
<strong>app-supplied</strong> (the engine hardcodes no policy). The entry flow is
|
|
962
1342
|
a <strong>walked sequence</strong> — <strong>Entry tokens → Payment
|
|
963
1343
|
processing → Entry Tokens Minted → Contest enter processing →
|
|
964
1344
|
Contest entered</strong> — and the live card <strong>glows</strong>,
|
|
965
1345
|
following the step machine (the two load steps honor the same min-duration
|
|
966
|
-
convention as Web3
|
|
1346
|
+
convention as Web3 above).
|
|
967
1347
|
</p>
|
|
968
1348
|
<%# The web2/web3 finding — a real divergence, stated honestly, not invented. %>
|
|
969
1349
|
<div class="mt-2 rounded-lg border border-subtle bg-surface p-3 text-xs text-secondary leading-snug">
|
|
@@ -986,23 +1366,14 @@
|
|
|
986
1366
|
</div>
|
|
987
1367
|
</div>
|
|
988
1368
|
<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 %>
|
|
1369
|
+
<%# The birthday / age-gate CARDS live in the Profile section — they are
|
|
1370
|
+
about who the account is, and grouping them by subject beats grouping
|
|
1371
|
+
them by the moment they happen to be asked. ENFORCEMENT still lives
|
|
1372
|
+
here: the bar is checked at contest entry, which is what this section is
|
|
1373
|
+
named for. The demo walk is unbroken — confirming the birthday card
|
|
1374
|
+
still advances to Entry tokens below, via the page-level 'age-verified'
|
|
1375
|
+
wiring, which the rename did NOT touch precisely because consuming apps
|
|
1376
|
+
already listen on it. %>
|
|
1006
1377
|
|
|
1007
1378
|
<%# 1. Entry tokens — the picker (walk's entry). Picking a pack advances. %>
|
|
1008
1379
|
<%= render layout: "style/modal_specimen", locals: {
|
|
@@ -1073,6 +1444,25 @@
|
|
|
1073
1444
|
<span class="block h-4 w-full rounded" style="background: var(--color-cta)"></span>
|
|
1074
1445
|
</div>
|
|
1075
1446
|
<% end %>
|
|
1447
|
+
|
|
1448
|
+
<%# it-begins — the moment a contest locks. The DRAINING CTA is the whole
|
|
1449
|
+
card: it interrupts someone mid-page at a moment they did not choose,
|
|
1450
|
+
and the drain is what gives them a say in what happens next. %>
|
|
1451
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
1452
|
+
label: "It Begins",
|
|
1453
|
+
reference: %(the contest-lock announcement (style/modals/_it_begins) — turf-monster's modals/_it_begins, fired when a contest's lock countdown reaches zero. APP-SPECIFIC: the engine owns no contests and no lock. It lends blocks/_cta_redirect — the DRAINING button, which is the whole reason this card is worth a specimen and the one part you have to WATCH rather than read, because a timer is not visible in markup. This card interrupts someone mid-page at a moment they did not choose: a dismissible modal would leave them on a stale board, a hard redirect would yank the page away, and the drain is the middle — it announces what is about to happen and gives them the length of the bar to opt out. Shortened to 4 seconds here (production runs 8) so the drain can be watched twice; it redirects NOWHERE — the block's null-destination gallery mode, so the drain completes and the card stays openable instead of ending as a stuck spinner. Open with $store.dsModals.open('it-begins')),
|
|
1454
|
+
open_expr: "$store.dsModals.open('it-begins')",
|
|
1455
|
+
glow_when: ds_glow.call("it-begins") } do %>
|
|
1456
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
1457
|
+
<div class="text-2xl leading-none">🏟️</div>
|
|
1458
|
+
<span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1459
|
+
<span class="block h-1.5 w-28 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
|
|
1460
|
+
<%# the CTA mid-drain — the bar empties, and leaving is the way out %>
|
|
1461
|
+
<span class="block h-5 w-full rounded overflow-hidden" style="background: color-mix(in srgb, var(--color-cta) 25%, transparent)">
|
|
1462
|
+
<span class="block h-full rounded" style="width: 55%; background: var(--color-cta)"></span>
|
|
1463
|
+
</span>
|
|
1464
|
+
</div>
|
|
1465
|
+
<% end %>
|
|
1076
1466
|
</div>
|
|
1077
1467
|
</section>
|
|
1078
1468
|
|
|
@@ -1135,6 +1525,23 @@
|
|
|
1135
1525
|
<span class="block h-1.5 w-16 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
|
|
1136
1526
|
</div>
|
|
1137
1527
|
<% end %>
|
|
1528
|
+
|
|
1529
|
+
<%# rate-limit-general — the SOFT cap. Dismissible, apologetic, and it
|
|
1530
|
+
auto-closes when the wait elapses rather than making anyone
|
|
1531
|
+
acknowledge a penalty for clicking too fast. %>
|
|
1532
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
1533
|
+
label: "Rate limited (soft)",
|
|
1534
|
+
reference: %(the soft rate-limit wait (style/modals/_rate_limit_general) — turf-monster's modals/_rate_limit_general, opened by the authedFetch 429 interceptor when a general-tier cap trips. Composes the engine's blocks/_card_header; the engine throttles nothing. THE TONE IS THE DESIGN: this is a soft cap on someone who did nothing wrong, so the card is dismissible, the copy is "Easy there" rather than an error, and it AUTO-CLOSES when the wait elapses instead of demanding an acknowledgement. The countdown is seeded from the 429's own Retry-After, so the number is the server's rather than a guess. It also clears its interval in a destroy() METHOD — Alpine 3 dispatches no "destroy" DOM event, so an @destroy listener never fires and the timer outlives every close. Shortened to 10 seconds here so the auto-close can be watched. Open with $store.dsModals.open('rate-limit-general', { secondsLeft: 10 })),
|
|
1535
|
+
open_expr: "$store.dsModals.open('rate-limit-general', { secondsLeft: 10 })",
|
|
1536
|
+
glow_when: ds_glow.call("rate-limit-general") } do %>
|
|
1537
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
1538
|
+
<div class="text-2xl leading-none">✋</div>
|
|
1539
|
+
<span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1540
|
+
<span class="block h-1.5 w-28 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
|
|
1541
|
+
<%# a dismiss link, not a button — the soft cap has no penalty to accept %>
|
|
1542
|
+
<span class="block h-1.5 w-12 mx-auto rounded" style="background: var(--color-text); opacity: .3"></span>
|
|
1543
|
+
</div>
|
|
1544
|
+
<% end %>
|
|
1138
1545
|
</div>
|
|
1139
1546
|
</section>
|
|
1140
1547
|
|