studio-engine 0.60.1 → 0.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/views/studio/modals/blocks/_close_x.html.erb +32 -0
- data/app/views/studio/modals/blocks/_rail_row.html.erb +109 -0
- data/app/views/style/_modals.html.erb +239 -0
- data/app/views/style/modals/_ds_close_x.html.erb +30 -0
- data/app/views/style/modals/_ds_onramp_hub.html.erb +71 -0
- data/app/views/style/modals/_ds_rail_row.html.erb +68 -0
- data/app/views/style/modals/_ds_wallet_topup.html.erb +69 -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/_rate_limit_general.html.erb +61 -0
- data/app/views/style/modals/_wallet_changed.html.erb +70 -0
- data/lib/studio/version.rb +1 -1
- metadata +14 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 64bf6960f2f9100da2fa8a1a38bd1ca62733d348b7393ca6bc279b2a223ad11b
|
|
4
|
+
data.tar.gz: 3b33af6cd3b2ca2f48e18b05b1cea868458c793663da2a887f2afbd3c795a4b8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 28ce9d9910b4f4eb3f96ea64bb27d415988065b3c6609b489ef737811eba2ad9bb818edddf5ab504c9e077c8104f1c15d979b1fdc61f4a540d0330f04697d42b
|
|
7
|
+
data.tar.gz: 0d303ef8c72f4927c78bba6ef585bd299d0af420e259c8f65273e13b1d01663d590b81b651d64f553be685a0a65af8d6766b8f8de7df0d7057ed5efd893955f8
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Top-right close × for a modal that centres its own title.
|
|
3
|
+
|
|
4
|
+
WHY THIS EXISTS ALONGSIDE blocks/_shell, which already offers a title row and a
|
|
5
|
+
close. The shell puts the title on the LEFT with the close beside it; the
|
|
6
|
+
funding modals centre their title and absolutely-position the close above it.
|
|
7
|
+
Both are legitimate, and retrofitting _shell onto the centred family would move
|
|
8
|
+
eight titles — a visual change wearing a refactor's clothes. So this homes the
|
|
9
|
+
mark those eight already draw, byte for byte, and leaves the layout question
|
|
10
|
+
alone.
|
|
11
|
+
|
|
12
|
+
PORTED FROM turf-monster, where this exact markup appears in EIGHT files:
|
|
13
|
+
modals/_wallet_deposit, _wallet_topup, _cdp_ramp, _onramp_hub, _buy_entry_token
|
|
14
|
+
and three under modals/auth/ (_paypal_tokens, _usdc_funding, _tokens).
|
|
15
|
+
|
|
16
|
+
THE PARENT MUST BE POSITIONED. This is `absolute`, so it anchors to the nearest
|
|
17
|
+
positioned ancestor — every callsite wraps its card in `class="relative"`. On an
|
|
18
|
+
unpositioned parent the × climbs to whatever is positioned above it, which in a
|
|
19
|
+
modal is the host's backdrop, and lands in the page corner. Nothing errors.
|
|
20
|
+
|
|
21
|
+
Locals:
|
|
22
|
+
modal_store — Alpine store name backing close(). Default "modals". The style
|
|
23
|
+
guide passes "dsModals" (its own page-scoped host).
|
|
24
|
+
label — aria-label. Default "Close".
|
|
25
|
+
%>
|
|
26
|
+
<%
|
|
27
|
+
modal_store = local_assigns.fetch(:modal_store, "modals")
|
|
28
|
+
label = local_assigns.fetch(:label, "Close")
|
|
29
|
+
%>
|
|
30
|
+
<button type="button" @click="$store.<%= modal_store %>.close()"
|
|
31
|
+
class="absolute top-0 right-0 -mt-2 -mr-2 text-secondary hover:text-heading text-xl leading-none z-10"
|
|
32
|
+
aria-label="<%= label %>">×</button>
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
A funding RAIL ROW — icon tile, title, subtitle, chevron; the whole row is the
|
|
3
|
+
button. Used where a modal offers several ways to do one thing and the choice
|
|
4
|
+
itself is the screen.
|
|
5
|
+
|
|
6
|
+
PORTED FROM turf-monster, where this markup appears TEN times across three
|
|
7
|
+
files: modals/_onramp_hub (x6), _buy_entry_token (x2) and _wallet_topup (x2).
|
|
8
|
+
All ten share the same base class string and differ only in EMPHASIS, which is
|
|
9
|
+
why that is a named local rather than a class passthrough — a passthrough would
|
|
10
|
+
have let the eleventh copy drift on day one, which is how ten happened.
|
|
11
|
+
|
|
12
|
+
THE EMPHASIS IS A RANKING, not decoration. A hub full of equally-weighted rails
|
|
13
|
+
asks the person to compare payment processors, which is not a question they can
|
|
14
|
+
answer. Exactly one rail should be :primary — the one the app wants taken —
|
|
15
|
+
and it reads louder in three places at once (a doubled border, a filled hover,
|
|
16
|
+
a coloured chevron) because one of the three alone is not visible on a 56px row.
|
|
17
|
+
|
|
18
|
+
Locals:
|
|
19
|
+
on_click (String, REQUIRED) — Alpine expression run on click.
|
|
20
|
+
title (String, REQUIRED) — the rail's name.
|
|
21
|
+
subtitle (String, optional) — the one line under it.
|
|
22
|
+
emphasis (:primary | :neutral) — default :neutral.
|
|
23
|
+
icon_label (String, optional) — short letters ("CF") or an emoji, drawn in
|
|
24
|
+
the tile. Omit for a tile with no glyph.
|
|
25
|
+
icon_bg (String, optional) — a CSS colour for the tile (e.g. "#0052FF"),
|
|
26
|
+
which is how a brand mark is carried without shipping an asset.
|
|
27
|
+
Absent = the app's primary token.
|
|
28
|
+
icon_class (String, optional) — extra classes on the tile. Default suits
|
|
29
|
+
letters ("text-white font-extrabold text-lg"); an EMOJI tile wants
|
|
30
|
+
"text-xl" instead, because the bold-white treatment renders an
|
|
31
|
+
emoji as a fuzzy white blob.
|
|
32
|
+
badge (String, optional) — a small uppercase pill beside the title
|
|
33
|
+
("New"). Dropped when absent rather than rendered empty.
|
|
34
|
+
data (Hash, optional) — data-* attributes, for the app's own test
|
|
35
|
+
hooks (turf uses data-onramp-rail / data-buy-rail / data-topup-rail).
|
|
36
|
+
state (:available | :soon) — default :available. :soon renders the
|
|
37
|
+
ANNOUNCED-BUT-NOT-WIRED rail: a non-interactive <div>, dimmed,
|
|
38
|
+
with a muted badge where the chevron goes. on_click is then
|
|
39
|
+
OPTIONAL and ignored.
|
|
40
|
+
soon_label (String, optional) — the badge text for :soon. Default "Soon".
|
|
41
|
+
|
|
42
|
+
:soon IS A THIRD SHAPE, not a disabled button, and the distinction is the
|
|
43
|
+
reason it is here. turf's onramp hub announces PayPal and Venmo before they are
|
|
44
|
+
wired, and it does so with a <div> rather than a `disabled` <button> — so there
|
|
45
|
+
is no focus stop, no pointer, and nothing for a keyboard user to land on and
|
|
46
|
+
wonder about. The chevron is replaced rather than dimmed, because a chevron
|
|
47
|
+
says "this goes somewhere" and that is the one thing this row must not say.
|
|
48
|
+
|
|
49
|
+
NOT A LINK. An :available rail is a <button> running an Alpine expression,
|
|
50
|
+
because every real callsite either swaps modals or calls a JS funding helper —
|
|
51
|
+
none navigates. A rail that must navigate should be a plain <a> and not this.
|
|
52
|
+
%>
|
|
53
|
+
<%
|
|
54
|
+
state = local_assigns.fetch(:state, :available).to_sym
|
|
55
|
+
soon = state == :soon
|
|
56
|
+
# on_click is required for a rail that DOES something, and meaningless for one
|
|
57
|
+
# that only announces itself — fetching it unconditionally would force every
|
|
58
|
+
# placeholder callsite to invent a no-op.
|
|
59
|
+
on_click = soon ? nil : local_assigns.fetch(:on_click)
|
|
60
|
+
soon_label = local_assigns.fetch(:soon_label, "Soon")
|
|
61
|
+
title = local_assigns.fetch(:title)
|
|
62
|
+
subtitle = local_assigns[:subtitle]
|
|
63
|
+
emphasis = local_assigns.fetch(:emphasis, :neutral).to_sym
|
|
64
|
+
icon_label = local_assigns[:icon_label]
|
|
65
|
+
icon_bg = local_assigns[:icon_bg]
|
|
66
|
+
icon_class = local_assigns.fetch(:icon_class, "text-white font-extrabold text-lg")
|
|
67
|
+
badge = local_assigns[:badge]
|
|
68
|
+
data_attrs = local_assigns.fetch(:data, {})
|
|
69
|
+
|
|
70
|
+
border_class = if soon
|
|
71
|
+
"border border-subtle opacity-60"
|
|
72
|
+
elsif emphasis == :primary
|
|
73
|
+
"border-2 border-primary hover:bg-surface"
|
|
74
|
+
else
|
|
75
|
+
"border border-strong hover:border-primary"
|
|
76
|
+
end
|
|
77
|
+
chevron_class = emphasis == :primary ? "text-primary" : "text-muted"
|
|
78
|
+
tile_style = icon_bg.present? ? "background-color:#{icon_bg}" : nil
|
|
79
|
+
tile_bg_class = icon_bg.present? ? nil : "bg-primary"
|
|
80
|
+
%>
|
|
81
|
+
<%= content_tag(soon ? :div : :button, type: (soon ? nil : "button"),
|
|
82
|
+
"@click": on_click,
|
|
83
|
+
class: "w-full flex items-center gap-4 p-4 rounded-xl #{border_class} bg-inset#{soon ? '' : ' transition text-left'}",
|
|
84
|
+
data: data_attrs.presence) do %>
|
|
85
|
+
<span class="shrink-0 w-10 h-10 rounded-lg flex items-center justify-center <%= icon_class %> <%= tile_bg_class %>"
|
|
86
|
+
style="<%= tile_style %>"><%= icon_label %></span>
|
|
87
|
+
<span class="flex-1 min-w-0">
|
|
88
|
+
<% if badge.present? %>
|
|
89
|
+
<span class="flex items-center gap-2">
|
|
90
|
+
<span class="text-heading font-semibold text-sm"><%= title %></span>
|
|
91
|
+
<span class="shrink-0 text-[10px] uppercase tracking-widest font-bold text-primary bg-surface border border-primary rounded-full px-2 py-0.5"><%= badge %></span>
|
|
92
|
+
</span>
|
|
93
|
+
<% else %>
|
|
94
|
+
<span class="block text-heading font-semibold text-sm"><%= title %></span>
|
|
95
|
+
<% end %>
|
|
96
|
+
<% if subtitle.present? %>
|
|
97
|
+
<span class="block text-xs text-secondary"><%= subtitle %></span>
|
|
98
|
+
<% end %>
|
|
99
|
+
</span>
|
|
100
|
+
<% if soon %>
|
|
101
|
+
<%# A badge where the chevron would be. Replacing it, not dimming it: a
|
|
102
|
+
chevron says "this goes somewhere", and that is what this row must not say. %>
|
|
103
|
+
<span class="shrink-0 text-[10px] uppercase tracking-widest font-bold text-muted bg-surface border border-subtle rounded-full px-2 py-0.5"><%= soon_label %></span>
|
|
104
|
+
<% else %>
|
|
105
|
+
<svg class="shrink-0 w-4 h-4 <%= chevron_class %>" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
|
|
106
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5"/>
|
|
107
|
+
</svg>
|
|
108
|
+
<% end %>
|
|
109
|
+
<% end %>
|
|
@@ -363,6 +363,40 @@
|
|
|
363
363
|
<div><%= render "style/modals/wallet_setup" %></div>
|
|
364
364
|
</template>
|
|
365
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
|
+
<%# --- funding chrome primitives (extract-funding-modal-chrome) --- %>
|
|
380
|
+
<template x-if="$store.dsModals.current().id === 'ds-rail-row'">
|
|
381
|
+
<div><%= render "style/modals/ds_rail_row" %></div>
|
|
382
|
+
</template>
|
|
383
|
+
<template x-if="$store.dsModals.current().id === 'ds-close-x'">
|
|
384
|
+
<div><%= render "style/modals/ds_close_x" %></div>
|
|
385
|
+
</template>
|
|
386
|
+
<%# --- the funding modals those primitives were extracted for --- %>
|
|
387
|
+
<template x-if="$store.dsModals.current().id === 'ds-wallet-topup'">
|
|
388
|
+
<div><%= render "style/modals/ds_wallet_topup" %></div>
|
|
389
|
+
</template>
|
|
390
|
+
<template x-if="$store.dsModals.current().id === 'ds-onramp-hub'">
|
|
391
|
+
<div><%= render "style/modals/ds_onramp_hub" %></div>
|
|
392
|
+
</template>
|
|
393
|
+
<template x-if="$store.dsModals.current().id === 'rate-limit-general'">
|
|
394
|
+
<div><%= render "style/modals/rate_limit_general" %></div>
|
|
395
|
+
</template>
|
|
396
|
+
<template x-if="$store.dsModals.current().id === 'wallet-changed'">
|
|
397
|
+
<div><%= render "style/modals/wallet_changed" %></div>
|
|
398
|
+
</template>
|
|
399
|
+
|
|
366
400
|
<%# --- Newsletter exit + the off-page quest celebration --- %>
|
|
367
401
|
<template x-if="$store.dsModals.current().id === 'quest-success'">
|
|
368
402
|
<div><%= render "style/modals/quest_success" %></div>
|
|
@@ -763,6 +797,24 @@
|
|
|
763
797
|
<span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
764
798
|
</div>
|
|
765
799
|
<% end %>
|
|
800
|
+
|
|
801
|
+
<%# email-change-pending — the informational handoff after an email change.
|
|
802
|
+
NOT a success card: nothing has changed yet, and the confirmation goes
|
|
803
|
+
to the CURRENT address so an unrequested change gets noticed. %>
|
|
804
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
805
|
+
label: "Email change pending",
|
|
806
|
+
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' })),
|
|
807
|
+
open_expr: "$store.dsModals.open('email-change-pending', { currentEmail: 'you@example.com', newEmail: 'new@example.com' })",
|
|
808
|
+
glow_when: ds_glow.call("email-change-pending") } do %>
|
|
809
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
810
|
+
<div class="text-2xl leading-none">📬</div>
|
|
811
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
812
|
+
<%# both addresses, which is the card's point %>
|
|
813
|
+
<span class="block h-1.5 w-28 mx-auto rounded" style="background: var(--color-text); opacity: .3"></span>
|
|
814
|
+
<span class="block h-1.5 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .3"></span>
|
|
815
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
816
|
+
</div>
|
|
817
|
+
<% end %>
|
|
766
818
|
</div>
|
|
767
819
|
</section>
|
|
768
820
|
|
|
@@ -940,6 +992,22 @@
|
|
|
940
992
|
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
941
993
|
</div>
|
|
942
994
|
<% end %>
|
|
995
|
+
|
|
996
|
+
<%# newsletter-email — the branch a wallet-only account has to take: no
|
|
997
|
+
email on file, so the quest asks for one mid-flight. %>
|
|
998
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
999
|
+
label: "Newsletter email",
|
|
1000
|
+
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')),
|
|
1001
|
+
open_expr: "$store.dsModals.open('newsletter-email')",
|
|
1002
|
+
glow_when: ds_glow.call("newsletter-email") } do %>
|
|
1003
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
1004
|
+
<div class="text-2xl leading-none">📬</div>
|
|
1005
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1006
|
+
<%# the ask itself — an email field, which no other newsletter card has %>
|
|
1007
|
+
<span class="block h-5 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
1008
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
1009
|
+
</div>
|
|
1010
|
+
<% end %>
|
|
943
1011
|
</div>
|
|
944
1012
|
</section>
|
|
945
1013
|
|
|
@@ -1189,6 +1257,53 @@
|
|
|
1189
1257
|
</div>
|
|
1190
1258
|
<% end %>
|
|
1191
1259
|
|
|
1260
|
+
<%# ds-wallet-topup — the FIRST funds modal, and a safety fork. %>
|
|
1261
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
1262
|
+
label: "Top up wallet",
|
|
1263
|
+
reference: %(the first funds modal a player meets (style/modals/_ds_wallet_topup) — turf-monster's modals/_wallet_topup, built here from blocks/_close_x and blocks/_rail_row. THE TOGGLE IS THE CARD: this is one modal with two faces, and the fork is a SAFETY one rather than a preference. A web2 viewer running with ENABLE_WEB2_USDC_ENTRY off cannot pay an entry with USDC at all, so pitching "Buy USDC" would sell them a thing that cannot do the job they came to do — for exactly that audience the primary rail becomes the entry token, and the balance line follows, because the currency it names has to be the one that can pay. Shown as ONE card with a toggle rather than two, because two would read as two products and the point is that a player never sees both. Open with $store.dsModals.open('ds-wallet-topup', { tokenFallback: false })),
|
|
1264
|
+
card_data: "opts: { tokenFallback: false }",
|
|
1265
|
+
toggles: [{ model: "opts.tokenFallback", label: "web2 kill-switch" }],
|
|
1266
|
+
open_expr: "$store.dsModals.open('ds-wallet-topup', { tokenFallback: opts.tokenFallback })",
|
|
1267
|
+
glow_when: ds_glow.call("ds-wallet-topup"),
|
|
1268
|
+
disabled: !web3_on, openable: true } do %>
|
|
1269
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
1270
|
+
<span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1271
|
+
<span class="flex items-center gap-1.5 rounded-md p-1.5" style="border: 2px solid var(--color-primary)">
|
|
1272
|
+
<span class="block h-4 w-4 rounded" style="background: var(--color-cta)"></span>
|
|
1273
|
+
<span class="block h-1.5 flex-1 rounded" style="background: var(--color-text); opacity: .3"></span>
|
|
1274
|
+
</span>
|
|
1275
|
+
<span class="block h-1.5 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1276
|
+
</div>
|
|
1277
|
+
<% end %>
|
|
1278
|
+
|
|
1279
|
+
<%# ds-onramp-hub — every rail at once, which is where ranking earns its keep. %>
|
|
1280
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
1281
|
+
label: "Add funds hub",
|
|
1282
|
+
reference: %(the full Add Funds hub (style/modals/_ds_onramp_hub) — turf-monster's modals/_onramp_hub, and the card blocks/_rail_row was extracted FOR: turf draws these six by hand today, which is six chances for the seventh to drift. Six rails is enough that "which one do I press" becomes a real question, and a hub where every rail carries equal weight answers it by asking the person to compare payment processors — not a question anyone can answer about their own life. Exactly ONE rail is :primary and the other five defer to it. PayPal and Venmo show the THIRD shape, :soon — announced before they are wired, so plain divs rather than disabled buttons, with a badge where the chevron would be, because even a dimmed chevron still promises somewhere to go. Open with $store.dsModals.open('ds-onramp-hub')),
|
|
1283
|
+
open_expr: "$store.dsModals.open('ds-onramp-hub')",
|
|
1284
|
+
glow_when: ds_glow.call("ds-onramp-hub"),
|
|
1285
|
+
disabled: !web3_on, openable: true } do %>
|
|
1286
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-3 space-y-1.5">
|
|
1287
|
+
<span class="flex items-center gap-1.5 rounded p-1" style="border: 2px solid var(--color-primary)">
|
|
1288
|
+
<span class="block h-3 w-3 rounded-sm" style="background: var(--color-cta)"></span>
|
|
1289
|
+
<span class="block h-1 flex-1 rounded" style="background: var(--color-text); opacity: .3"></span>
|
|
1290
|
+
</span>
|
|
1291
|
+
<% 3.times do %>
|
|
1292
|
+
<span class="flex items-center gap-1.5 rounded p-1 border border-subtle">
|
|
1293
|
+
<span class="block h-3 w-3 rounded-sm" style="background: var(--color-text); opacity: .25"></span>
|
|
1294
|
+
<span class="block h-1 flex-1 rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1295
|
+
</span>
|
|
1296
|
+
<% end %>
|
|
1297
|
+
<%# the two :soon rails, dimmed %>
|
|
1298
|
+
<% 2.times do %>
|
|
1299
|
+
<span class="flex items-center gap-1.5 rounded p-1 border border-subtle" style="opacity: .5">
|
|
1300
|
+
<span class="block h-3 w-3 rounded-sm" style="background: var(--color-text); opacity: .25"></span>
|
|
1301
|
+
<span class="block h-1 flex-1 rounded" style="background: var(--color-text); opacity: .12"></span>
|
|
1302
|
+
</span>
|
|
1303
|
+
<% end %>
|
|
1304
|
+
</div>
|
|
1305
|
+
<% end %>
|
|
1306
|
+
|
|
1192
1307
|
<%# Entry confirmed — the GENERIC web3 celebration (tx link + heading + drain
|
|
1193
1308
|
CTA). seedsEarned: 0 so no seeds bar even where leveling is on: this card
|
|
1194
1309
|
demos the clean success a web3-only app gets. The seeds variant lives in
|
|
@@ -1207,6 +1322,57 @@
|
|
|
1207
1322
|
<span class="block h-4 w-full rounded" style="background: var(--color-cta)"></span>
|
|
1208
1323
|
</div>
|
|
1209
1324
|
<% end %>
|
|
1325
|
+
|
|
1326
|
+
<%# network-guard — the explicit acknowledgement before a wallet request.
|
|
1327
|
+
Continue stays INERT until the box is ticked and Cancel carries equal
|
|
1328
|
+
weight: a confirmation whose safe path is harder to reach is theatre. %>
|
|
1329
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
1330
|
+
label: "Network guard",
|
|
1331
|
+
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' })),
|
|
1332
|
+
open_expr: "$store.dsModals.open('network-guard', { networkLabel: 'devnet', environmentLabel: 'QA' })",
|
|
1333
|
+
glow_when: ds_glow.call("network-guard"),
|
|
1334
|
+
disabled: !web3_on, openable: true } do %>
|
|
1335
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
1336
|
+
<div class="text-2xl leading-none">⚠️</div>
|
|
1337
|
+
<%# the two named facts, then the tick that gates Continue %>
|
|
1338
|
+
<span class="block h-1.5 w-full rounded" style="background: var(--color-text); opacity: .3"></span>
|
|
1339
|
+
<span class="block h-1.5 w-full rounded" style="background: var(--color-text); opacity: .3"></span>
|
|
1340
|
+
<span class="flex items-center gap-1.5">
|
|
1341
|
+
<span class="block h-2.5 w-2.5 rounded-sm border" style="border-color: var(--color-border-strong)"></span>
|
|
1342
|
+
<span class="block h-1.5 flex-1 rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1343
|
+
</span>
|
|
1344
|
+
<%# equal weight, deliberately %>
|
|
1345
|
+
<span class="grid grid-cols-2 gap-1.5">
|
|
1346
|
+
<span class="block h-4 rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
1347
|
+
<span class="block h-4 rounded" style="background: var(--color-cta); opacity: .5"></span>
|
|
1348
|
+
</span>
|
|
1349
|
+
</div>
|
|
1350
|
+
<% end %>
|
|
1351
|
+
|
|
1352
|
+
<%# wallet-changed — the extension switched accounts underneath the session.
|
|
1353
|
+
Both addresses are shown because recognising your own is the only way to
|
|
1354
|
+
tell a deliberate switch from an accident. %>
|
|
1355
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
1356
|
+
label: "Wallet changed",
|
|
1357
|
+
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: '…' })),
|
|
1358
|
+
open_expr: "$store.dsModals.open('wallet-changed', { currentAddress: '7xKpDemoSignedInWallet9JZ2Q', newAddress: '9mQtDemoSwitchedWalletLd4V' })",
|
|
1359
|
+
glow_when: ds_glow.call("wallet-changed"),
|
|
1360
|
+
disabled: !web3_on, openable: true } do %>
|
|
1361
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
1362
|
+
<div class="text-2xl leading-none">🔀</div>
|
|
1363
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1364
|
+
<%# the two truncated addresses — the comparison IS the card %>
|
|
1365
|
+
<span class="flex items-center justify-between gap-2">
|
|
1366
|
+
<span class="block h-1.5 w-10 rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1367
|
+
<span class="block h-1.5 w-12 rounded" style="background: var(--color-text); opacity: .35"></span>
|
|
1368
|
+
</span>
|
|
1369
|
+
<span class="flex items-center justify-between gap-2">
|
|
1370
|
+
<span class="block h-1.5 w-10 rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1371
|
+
<span class="block h-1.5 w-12 rounded" style="background: var(--color-text); opacity: .35"></span>
|
|
1372
|
+
</span>
|
|
1373
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
1374
|
+
</div>
|
|
1375
|
+
<% end %>
|
|
1210
1376
|
</div>
|
|
1211
1377
|
</section>
|
|
1212
1378
|
|
|
@@ -1339,6 +1505,25 @@
|
|
|
1339
1505
|
<span class="block h-4 w-full rounded" style="background: var(--color-cta)"></span>
|
|
1340
1506
|
</div>
|
|
1341
1507
|
<% end %>
|
|
1508
|
+
|
|
1509
|
+
<%# it-begins — the moment a contest locks. The DRAINING CTA is the whole
|
|
1510
|
+
card: it interrupts someone mid-page at a moment they did not choose,
|
|
1511
|
+
and the drain is what gives them a say in what happens next. %>
|
|
1512
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
1513
|
+
label: "It Begins",
|
|
1514
|
+
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')),
|
|
1515
|
+
open_expr: "$store.dsModals.open('it-begins')",
|
|
1516
|
+
glow_when: ds_glow.call("it-begins") } do %>
|
|
1517
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
1518
|
+
<div class="text-2xl leading-none">🏟️</div>
|
|
1519
|
+
<span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1520
|
+
<span class="block h-1.5 w-28 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
|
|
1521
|
+
<%# the CTA mid-drain — the bar empties, and leaving is the way out %>
|
|
1522
|
+
<span class="block h-5 w-full rounded overflow-hidden" style="background: color-mix(in srgb, var(--color-cta) 25%, transparent)">
|
|
1523
|
+
<span class="block h-full rounded" style="width: 55%; background: var(--color-cta)"></span>
|
|
1524
|
+
</span>
|
|
1525
|
+
</div>
|
|
1526
|
+
<% end %>
|
|
1342
1527
|
</div>
|
|
1343
1528
|
</section>
|
|
1344
1529
|
|
|
@@ -1401,6 +1586,60 @@
|
|
|
1401
1586
|
<span class="block h-1.5 w-16 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
|
|
1402
1587
|
</div>
|
|
1403
1588
|
<% end %>
|
|
1589
|
+
|
|
1590
|
+
<%# rate-limit-general — the SOFT cap. Dismissible, apologetic, and it
|
|
1591
|
+
auto-closes when the wait elapses rather than making anyone
|
|
1592
|
+
acknowledge a penalty for clicking too fast. %>
|
|
1593
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
1594
|
+
label: "Rate limited (soft)",
|
|
1595
|
+
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 })),
|
|
1596
|
+
open_expr: "$store.dsModals.open('rate-limit-general', { secondsLeft: 10 })",
|
|
1597
|
+
glow_when: ds_glow.call("rate-limit-general") } do %>
|
|
1598
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
1599
|
+
<div class="text-2xl leading-none">✋</div>
|
|
1600
|
+
<span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1601
|
+
<span class="block h-1.5 w-28 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
|
|
1602
|
+
<%# a dismiss link, not a button — the soft cap has no penalty to accept %>
|
|
1603
|
+
<span class="block h-1.5 w-12 mx-auto rounded" style="background: var(--color-text); opacity: .3"></span>
|
|
1604
|
+
</div>
|
|
1605
|
+
<% end %>
|
|
1606
|
+
|
|
1607
|
+
<%# rail-row — the funding picker's option row. Exactly ONE primary. %>
|
|
1608
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
1609
|
+
label: "Rail row",
|
|
1610
|
+
reference: %(the funding rail row (studio/modals/blocks/_rail_row) — the option row a funding hub is built from, ported out of turf-monster where the SAME markup appeared TEN times across modals/_onramp_hub, _buy_entry_token and _wallet_topup. The specimen shows three: one :primary and two :neutral, which is the arrangement a real hub uses. The emphasis is a RANKING, not decoration — a hub of equally-weighted rails asks the person to compare payment processors, which is not a question they can answer. It reads louder in three places at once (doubled border, filled hover, coloured chevron) because one alone is invisible on a 56px row, which is why it is ONE `emphasis:` local rather than three classes a callsite could half-apply. Brand colour rides in as `icon_bg`, so a rail wears a processor's mark with no asset shipped; the emoji tile passes icon_class: "text-xl", because the default bold-white treatment renders an emoji as a fuzzy white blob. Open with $store.dsModals.open('ds-rail-row')),
|
|
1611
|
+
open_expr: "$store.dsModals.open('ds-rail-row')",
|
|
1612
|
+
glow_when: ds_glow.call("ds-rail-row") } do %>
|
|
1613
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
1614
|
+
<%# the primary rail, then two neutrals — the ranking IS the thumbnail %>
|
|
1615
|
+
<span class="flex items-center gap-1.5 rounded-md p-1.5" style="border: 2px solid var(--color-primary)">
|
|
1616
|
+
<span class="block h-4 w-4 rounded" style="background: var(--color-cta)"></span>
|
|
1617
|
+
<span class="block h-1.5 flex-1 rounded" style="background: var(--color-text); opacity: .3"></span>
|
|
1618
|
+
</span>
|
|
1619
|
+
<span class="flex items-center gap-1.5 rounded-md p-1.5 border border-subtle">
|
|
1620
|
+
<span class="block h-4 w-4 rounded" style="background: var(--color-text); opacity: .25"></span>
|
|
1621
|
+
<span class="block h-1.5 flex-1 rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1622
|
+
</span>
|
|
1623
|
+
<span class="flex items-center gap-1.5 rounded-md p-1.5 border border-subtle">
|
|
1624
|
+
<span class="block h-4 w-4 rounded" style="background: var(--color-text); opacity: .25"></span>
|
|
1625
|
+
<span class="block h-1.5 flex-1 rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1626
|
+
</span>
|
|
1627
|
+
</div>
|
|
1628
|
+
<% end %>
|
|
1629
|
+
|
|
1630
|
+
<%# close-x — the mark, and the one class it depends on. %>
|
|
1631
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
1632
|
+
label: "Close ×",
|
|
1633
|
+
reference: %(the centred-title card's close mark (studio/modals/blocks/_close_x) — ported out of turf-monster where this markup appeared BYTE-IDENTICAL in eight files (modals/_wallet_deposit, _wallet_topup, _cdp_ramp, _onramp_hub, _buy_entry_token, and three under modals/auth/). It exists ALONGSIDE blocks/_shell rather than inside it: the shell puts its title on the left with the close beside it, while this family centres the title and positions the mark above it, so retrofitting the shell would move eight titles — a visual change wearing a refactor's clothes. THE PARENT MUST BE `relative`: the button is absolute, so it anchors to the nearest positioned ancestor, and without that class it climbs to the modal host's backdrop and lands in the page corner. Nothing errors and nothing in the markup reads as wrong, which is why the specimen says so out loud. Open with $store.dsModals.open('ds-close-x')),
|
|
1634
|
+
open_expr: "$store.dsModals.open('ds-close-x')",
|
|
1635
|
+
glow_when: ds_glow.call("ds-close-x") } do %>
|
|
1636
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2 relative">
|
|
1637
|
+
<span class="absolute top-1 right-2 text-secondary leading-none">×</span>
|
|
1638
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
1639
|
+
<span class="block h-1.5 w-28 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
|
|
1640
|
+
<span class="block h-4 w-full rounded" style="background: var(--color-cta)"></span>
|
|
1641
|
+
</div>
|
|
1642
|
+
<% end %>
|
|
1404
1643
|
</div>
|
|
1405
1644
|
</section>
|
|
1406
1645
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Living style guide specimen — the centred-title card and its close ×.
|
|
3
|
+
|
|
4
|
+
The mark itself is one button, so what is worth showing is the RELATIONSHIP it
|
|
5
|
+
depends on: the × is absolutely positioned, and it anchors to the nearest
|
|
6
|
+
POSITIONED ancestor. This card wraps its content in `class="relative"`, which
|
|
7
|
+
is the contract. Drop that one class and the × does not disappear — it climbs
|
|
8
|
+
to whatever is positioned above, which inside a modal is the host's backdrop,
|
|
9
|
+
and lands in the corner of the page looking like a stray glyph. Nothing errors,
|
|
10
|
+
and nothing in the markup reads as wrong.
|
|
11
|
+
|
|
12
|
+
Shown next to a centred title because that pairing is the reason this exists
|
|
13
|
+
rather than blocks/_shell, whose title sits left with the close beside it.
|
|
14
|
+
|
|
15
|
+
Single root: the outer <div> is the host's required root.
|
|
16
|
+
%>
|
|
17
|
+
<div class="relative">
|
|
18
|
+
<%= render "studio/modals/blocks/close_x", modal_store: "dsModals" %>
|
|
19
|
+
|
|
20
|
+
<div class="text-center pt-1 mb-3">
|
|
21
|
+
<h3 class="text-heading font-bold text-lg leading-tight">Top Up Your Wallet</h3>
|
|
22
|
+
</div>
|
|
23
|
+
<p class="text-xs text-body mb-4 text-center">
|
|
24
|
+
The × above sits in this card’s own corner because the card is
|
|
25
|
+
<code class="font-mono text-2xs">relative</code>. That single class is the
|
|
26
|
+
whole contract.
|
|
27
|
+
</p>
|
|
28
|
+
<button type="button" @click="$store.dsModals.close()"
|
|
29
|
+
class="btn btn-primary btn-lg w-full">Done</button>
|
|
30
|
+
</div>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Living style guide specimen — the Add Funds hub, every rail at once.
|
|
3
|
+
|
|
4
|
+
APP-SPECIFIC flow, engine chrome throughout (blocks/_close_x + six
|
|
5
|
+
blocks/_rail_row). This is the card the rail primitive was extracted FOR: turf
|
|
6
|
+
draws these six by hand today, which is six chances for the seventh to drift.
|
|
7
|
+
|
|
8
|
+
WHAT TO LOOK AT IS THE RANKING, and this is where it earns its keep. Six rails
|
|
9
|
+
is enough that "which one do I press" becomes a real question, and a hub where
|
|
10
|
+
every rail carries equal weight answers it by asking the person to compare
|
|
11
|
+
payment processors — which is not a question anyone can answer about their own
|
|
12
|
+
life. Exactly one rail is :primary. The other five defer to it.
|
|
13
|
+
|
|
14
|
+
AND THE THIRD SHAPE. PayPal and Venmo are announced before they are wired, so
|
|
15
|
+
they are :soon — not disabled buttons but plain <div>s, with a badge where the
|
|
16
|
+
chevron would be. A dimmed chevron would still promise somewhere to go.
|
|
17
|
+
|
|
18
|
+
Single root: the outer <div> is the host's required root.
|
|
19
|
+
%>
|
|
20
|
+
<div class="relative">
|
|
21
|
+
<%= render "studio/modals/blocks/close_x", modal_store: "dsModals" %>
|
|
22
|
+
|
|
23
|
+
<div class="text-center pt-1 mb-3">
|
|
24
|
+
<h3 class="text-heading font-bold text-lg leading-tight">Add Funds</h3>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="space-y-3">
|
|
28
|
+
<%= render "studio/modals/blocks/rail_row",
|
|
29
|
+
emphasis: :primary, on_click: "$store.dsModals.close()",
|
|
30
|
+
icon_label: "C", icon_bg: "#0052FF",
|
|
31
|
+
title: "Coinbase", subtitle: "Buy USDC with a card or bank",
|
|
32
|
+
data: { ds_rail: "coinbase" } %>
|
|
33
|
+
|
|
34
|
+
<%= render "studio/modals/blocks/rail_row",
|
|
35
|
+
on_click: "$store.dsModals.close()",
|
|
36
|
+
icon_label: "CF", icon_bg: "#14B8A6",
|
|
37
|
+
title: "Coinflow · $19", subtitle: "Buy 1 entry token with a card",
|
|
38
|
+
data: { ds_rail: "coinflow" } %>
|
|
39
|
+
|
|
40
|
+
<%= render "studio/modals/blocks/rail_row",
|
|
41
|
+
on_click: "$store.dsModals.close()",
|
|
42
|
+
icon_label: "A", icon_bg: "#6D28D9",
|
|
43
|
+
title: "Aeropay · $19", subtitle: "Buy 1 entry token from your bank",
|
|
44
|
+
data: { ds_rail: "aeropay" } %>
|
|
45
|
+
|
|
46
|
+
<%= render "studio/modals/blocks/rail_row",
|
|
47
|
+
state: :soon,
|
|
48
|
+
icon_label: "P", icon_bg: "#003087",
|
|
49
|
+
title: "PayPal", subtitle: "Pay with your PayPal balance",
|
|
50
|
+
data: { ds_rail: "paypal" } %>
|
|
51
|
+
|
|
52
|
+
<%= render "studio/modals/blocks/rail_row",
|
|
53
|
+
state: :soon,
|
|
54
|
+
icon_label: "V", icon_bg: "#008CFF",
|
|
55
|
+
title: "Venmo", subtitle: "Pay from your Venmo balance",
|
|
56
|
+
data: { ds_rail: "venmo" } %>
|
|
57
|
+
|
|
58
|
+
<%= render "studio/modals/blocks/rail_row",
|
|
59
|
+
on_click: "$store.dsModals.close()",
|
|
60
|
+
icon_label: "S", icon_bg: "#635BFF",
|
|
61
|
+
title: "Card · $19", subtitle: "Buy one entry token with a card",
|
|
62
|
+
data: { ds_rail: "stripe" } %>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<%# Back, not close: the hub is always reached FROM somewhere, and landing a
|
|
66
|
+
player on nothing is not the same as returning them. %>
|
|
67
|
+
<button type="button" @click="$store.dsModals.swap('ds-wallet-topup', {})"
|
|
68
|
+
class="block mx-auto mt-4 text-sm text-secondary hover:text-heading transition">
|
|
69
|
+
← Back
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Living style guide specimen — the funding rail picker.
|
|
3
|
+
|
|
4
|
+
Three rows through the ENGINE's blocks/_rail_row, arranged the way a real hub
|
|
5
|
+
arranges them: exactly ONE primary, the rest neutral. That ranking is the thing
|
|
6
|
+
to look at. A hub of equally-weighted rails asks the person to compare payment
|
|
7
|
+
processors, which is not a question they can answer; the emphasis answers it
|
|
8
|
+
for them.
|
|
9
|
+
|
|
10
|
+
Watch the three signals move together on the top row — a doubled border, a
|
|
11
|
+
filled hover, a coloured chevron. Each alone is invisible on a 56px row, which
|
|
12
|
+
is why the primitive ships them as one `emphasis:` rather than three classes a
|
|
13
|
+
callsite could half-apply.
|
|
14
|
+
|
|
15
|
+
The brand tiles carry their colour as an inline `icon_bg`, which is how a rail
|
|
16
|
+
wears a processor's mark without the engine shipping an asset for it. The last
|
|
17
|
+
row is an EMOJI tile, and it passes icon_class: "text-xl" — the default
|
|
18
|
+
bold-white treatment renders an emoji as a fuzzy white blob.
|
|
19
|
+
|
|
20
|
+
Single root: the outer <div> is the host's required root.
|
|
21
|
+
%>
|
|
22
|
+
<div class="relative">
|
|
23
|
+
<%= render "studio/modals/blocks/close_x", modal_store: "dsModals" %>
|
|
24
|
+
|
|
25
|
+
<div class="text-center pt-1 mb-3">
|
|
26
|
+
<h3 class="text-heading font-bold text-lg leading-tight">Add funds</h3>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div class="space-y-3">
|
|
30
|
+
<%= render "studio/modals/blocks/rail_row",
|
|
31
|
+
emphasis: :primary,
|
|
32
|
+
on_click: "$store.dsModals.close()",
|
|
33
|
+
icon_label: "C",
|
|
34
|
+
icon_bg: "#0052FF",
|
|
35
|
+
title: "Coinbase",
|
|
36
|
+
subtitle: "Card or bank · funds your entry",
|
|
37
|
+
data: { ds_rail: "coinbase" } %>
|
|
38
|
+
|
|
39
|
+
<%= render "studio/modals/blocks/rail_row",
|
|
40
|
+
on_click: "$store.dsModals.close()",
|
|
41
|
+
icon_label: "CF",
|
|
42
|
+
icon_bg: "#14B8A6",
|
|
43
|
+
title: "Coinflow · $19",
|
|
44
|
+
subtitle: "Buy 1 entry token with a card",
|
|
45
|
+
badge: "New",
|
|
46
|
+
data: { ds_rail: "coinflow" } %>
|
|
47
|
+
|
|
48
|
+
<%= render "studio/modals/blocks/rail_row",
|
|
49
|
+
on_click: "$store.dsModals.close()",
|
|
50
|
+
icon_label: "🎟️",
|
|
51
|
+
icon_class: "text-xl",
|
|
52
|
+
title: "Entry tokens",
|
|
53
|
+
subtitle: "1 token = 1 contest entry",
|
|
54
|
+
data: { ds_rail: "tokens" } %>
|
|
55
|
+
|
|
56
|
+
<%# The THIRD shape — a rail announced before it is wired. Not a disabled
|
|
57
|
+
button: a <div>, so there is no focus stop and nothing for a keyboard
|
|
58
|
+
user to land on and wonder about, and the badge REPLACES the chevron
|
|
59
|
+
rather than dimming it, because a chevron says "this goes somewhere". %>
|
|
60
|
+
<%= render "studio/modals/blocks/rail_row",
|
|
61
|
+
state: :soon,
|
|
62
|
+
icon_label: "P",
|
|
63
|
+
icon_bg: "#003087",
|
|
64
|
+
title: "PayPal",
|
|
65
|
+
subtitle: "Pay with your PayPal balance",
|
|
66
|
+
data: { ds_rail: "paypal" } %>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Living style guide specimen — Top Up Wallet, the FIRST funds modal a player
|
|
3
|
+
meets when they try to enter without funds.
|
|
4
|
+
|
|
5
|
+
APP-SPECIFIC flow, engine chrome throughout: blocks/_close_x for the mark and
|
|
6
|
+
blocks/_rail_row for every option. The engine owns no processors and no funding
|
|
7
|
+
policy; what turf supplies is which rails exist and which one leads.
|
|
8
|
+
|
|
9
|
+
THE TOGGLE IS THE CARD. This is one modal with two faces, and the fork is a
|
|
10
|
+
SAFETY one rather than a preference. A web2 viewer running with
|
|
11
|
+
ENABLE_WEB2_USDC_ENTRY off cannot pay an entry with USDC at all — so pitching
|
|
12
|
+
"Buy USDC" to them would sell a thing that cannot do the job they came to do.
|
|
13
|
+
For exactly that audience the primary rail becomes the entry token instead.
|
|
14
|
+
Everyone else — web3, and web2 with the flag on — sees Coinbase.
|
|
15
|
+
|
|
16
|
+
Shown as ONE card with a toggle rather than two cards, because two cards would
|
|
17
|
+
read as two products. The whole point is that a player never sees both.
|
|
18
|
+
|
|
19
|
+
Single root: the outer <div> is the host's required root.
|
|
20
|
+
%>
|
|
21
|
+
<div class="relative"
|
|
22
|
+
x-data="{
|
|
23
|
+
get props() { var c = $store.dsModals.current(); return (c && c.props) || {} },
|
|
24
|
+
get tokenFallback() { return !!this.props.tokenFallback }
|
|
25
|
+
}">
|
|
26
|
+
<%= render "studio/modals/blocks/close_x", modal_store: "dsModals" %>
|
|
27
|
+
|
|
28
|
+
<div class="text-center pt-1 mb-2">
|
|
29
|
+
<h3 class="text-heading font-bold text-lg leading-tight">Top Up Wallet</h3>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<%# The balance line follows the fork too — the currency it names has to be the
|
|
33
|
+
one that can actually pay for the entry. %>
|
|
34
|
+
<p x-show="!tokenFallback" class="text-xs text-body mb-4 text-center">
|
|
35
|
+
USDC balance: <span class="font-mono font-semibold text-heading">$12.40</span>
|
|
36
|
+
</p>
|
|
37
|
+
<p x-show="tokenFallback" x-cloak class="text-xs text-body mb-4 text-center">
|
|
38
|
+
Entry tokens: <span class="font-mono font-semibold text-heading">0</span>
|
|
39
|
+
</p>
|
|
40
|
+
|
|
41
|
+
<template x-if="!tokenFallback">
|
|
42
|
+
<%= render "studio/modals/blocks/rail_row",
|
|
43
|
+
emphasis: :primary,
|
|
44
|
+
on_click: "$store.dsModals.close()",
|
|
45
|
+
icon_label: "C",
|
|
46
|
+
icon_bg: "#0052FF",
|
|
47
|
+
title: "Buy USDC with Coinbase",
|
|
48
|
+
subtitle: "Card or bank · funds your entry",
|
|
49
|
+
data: { ds_rail: "coinbase" } %>
|
|
50
|
+
</template>
|
|
51
|
+
|
|
52
|
+
<template x-if="tokenFallback">
|
|
53
|
+
<%= render "studio/modals/blocks/rail_row",
|
|
54
|
+
emphasis: :primary,
|
|
55
|
+
on_click: "$store.dsModals.close()",
|
|
56
|
+
icon_label: "🎟️",
|
|
57
|
+
icon_class: "text-xl",
|
|
58
|
+
title: "Buy Entry Tokens",
|
|
59
|
+
subtitle: "1 token = 1 contest entry",
|
|
60
|
+
data: { ds_rail: "tokens" } %>
|
|
61
|
+
</template>
|
|
62
|
+
|
|
63
|
+
<%# The quiet way to the full hub. Deliberately a link, not a rail: it is not
|
|
64
|
+
another way to pay, it is a way to see the others. %>
|
|
65
|
+
<button type="button" @click="$store.dsModals.swap('ds-onramp-hub', {})"
|
|
66
|
+
class="block mx-auto mt-4 text-sm text-secondary hover:text-heading transition">
|
|
67
|
+
More ways to add funds →
|
|
68
|
+
</button>
|
|
69
|
+
</div>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Living style guide specimen — the email-change handoff.
|
|
3
|
+
|
|
4
|
+
APP-SPECIFIC, engine chrome only (`blocks/_card_header`). The engine sends no
|
|
5
|
+
confirmation mail and holds no pending change; turf's AccountsController#update
|
|
6
|
+
does both.
|
|
7
|
+
|
|
8
|
+
IT IS AN INFORMATIONAL HANDOFF, NOT A CELEBRATION, and the distinction is the
|
|
9
|
+
card's whole job. Nothing has happened yet: the change is HELD until the person
|
|
10
|
+
clicks a link sent to their CURRENT address, not the new one. A card that read
|
|
11
|
+
as success would tell them the opposite of the truth.
|
|
12
|
+
|
|
13
|
+
Deliberately distinct from the check-email card, which says "finish entering"
|
|
14
|
+
and carries a resend CTA wired to the contest board. This one has no resend and
|
|
15
|
+
no contest wiring — one button, which closes.
|
|
16
|
+
|
|
17
|
+
Both addresses are shown because the point of confirming to the OLD address is
|
|
18
|
+
that someone who did not make the request finds out. Seeing both is how they
|
|
19
|
+
notice.
|
|
20
|
+
|
|
21
|
+
Single root: the outer <div> is the host's required root.
|
|
22
|
+
%>
|
|
23
|
+
<div x-data="{ get props() { var c = $store.dsModals.current(); return (c && c.props) || {} } }">
|
|
24
|
+
<%= render "studio/modals/blocks/card_header",
|
|
25
|
+
icon_emoji: "📬",
|
|
26
|
+
title: "Confirm your email change" do %>
|
|
27
|
+
<p class="text-sm text-body">
|
|
28
|
+
We sent a confirmation link to
|
|
29
|
+
<span class="text-heading font-medium" x-text="props.currentEmail || 'you@example.com'"></span>
|
|
30
|
+
(your current address). Click the link in that email to confirm changing your
|
|
31
|
+
email to
|
|
32
|
+
<span class="text-heading font-medium" x-text="props.newEmail || 'new@example.com'"></span>.
|
|
33
|
+
Your email won’t change until you do — the link expires in 30 minutes.
|
|
34
|
+
</p>
|
|
35
|
+
<% end %>
|
|
36
|
+
|
|
37
|
+
<button type="button" @click="$store.dsModals.close()"
|
|
38
|
+
class="btn btn-primary btn-lg w-full">Got it</button>
|
|
39
|
+
</div>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Living style guide specimen — the moment a contest locks.
|
|
3
|
+
|
|
4
|
+
APP-SPECIFIC, engine chrome only. What fires it is turf's: a contest's lock
|
|
5
|
+
countdown reaching zero (contests/_timestamp_countdown). The engine owns no
|
|
6
|
+
contests and no lock. It lends `blocks/_cta_redirect` — the DRAINING button,
|
|
7
|
+
which is the whole reason this card is worth a specimen.
|
|
8
|
+
|
|
9
|
+
WHY THE DRAIN MATTERS, and why a reviewer should watch it rather than read it:
|
|
10
|
+
this card interrupts someone mid-page at a moment they did not choose. A
|
|
11
|
+
dismissible modal would leave them staring at a stale board; a hard redirect
|
|
12
|
+
would yank the page out from under them. The drain is the middle — it announces
|
|
13
|
+
what is about to happen and gives them the length of the bar to opt out. That
|
|
14
|
+
behaviour is a TIMER, and a timer is not visible in markup.
|
|
15
|
+
|
|
16
|
+
Shortened to 4 seconds here (production runs 8) so the drain can be watched
|
|
17
|
+
twice without waiting. In the app the destination rides in on props.lobbyUrl,
|
|
18
|
+
set by whoever opened it.
|
|
19
|
+
|
|
20
|
+
Single root: the outer <div> is the host's required root.
|
|
21
|
+
%>
|
|
22
|
+
<div class="text-center">
|
|
23
|
+
<div class="mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-full bg-primary/15 text-3xl">
|
|
24
|
+
🏟️
|
|
25
|
+
</div>
|
|
26
|
+
<h2 class="text-2xl font-extrabold text-heading">It Begins</h2>
|
|
27
|
+
<p class="mt-2 text-sm text-secondary">
|
|
28
|
+
Entries are locked — the contest is underway. Good luck.
|
|
29
|
+
</p>
|
|
30
|
+
<div class="mt-5">
|
|
31
|
+
<%# NULL destination, which is the block's documented gallery mode (see
|
|
32
|
+
blocks/_cta_redirect's header): the drain plays, the timer fires a no-op,
|
|
33
|
+
and a click falls through to closing the modal — so the specimen stays
|
|
34
|
+
alive to be opened again.
|
|
35
|
+
A truthy href here is NOT a harmless demo destination. It takes the
|
|
36
|
+
REDIRECT path, which sets `redirecting = true` and never resets it; and
|
|
37
|
+
because "#modals" is a same-document fragment, no reload ever clears the
|
|
38
|
+
component. The card ends as a permanently dead spinner: label and drain
|
|
39
|
+
bar hidden, anchor pointer-events-none and aria-disabled. %>
|
|
40
|
+
<%= render "studio/modals/blocks/cta_redirect",
|
|
41
|
+
href_key: "null",
|
|
42
|
+
label_html: "Enter the Lobby",
|
|
43
|
+
duration_seconds: 4,
|
|
44
|
+
modal_store: "dsModals" %>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Living style guide specimen — the network confirmation before a wallet request.
|
|
3
|
+
|
|
4
|
+
APP-SPECIFIC, engine chrome only (`blocks/_card_header`). The engine knows
|
|
5
|
+
nothing about networks or environments; every label here is app-supplied and
|
|
6
|
+
only ever DISPLAYED.
|
|
7
|
+
|
|
8
|
+
THE CHECKBOX IS THE POINT, and it is not friction for its own sake. A wallet
|
|
9
|
+
request signed against the wrong network is the kind of mistake that looks
|
|
10
|
+
identical to a correct one until it is irreversible — mainnet money moved on a
|
|
11
|
+
devnet assumption, or the reverse. So the card does three things a plain
|
|
12
|
+
confirm cannot: it NAMES both the network and the environment, it makes
|
|
13
|
+
Continue INERT until the person has ticked an explicit acknowledgement, and it
|
|
14
|
+
gives Cancel equal visual weight rather than burying it.
|
|
15
|
+
|
|
16
|
+
A confirmation whose safe path is harder to reach than its risky one is a
|
|
17
|
+
formality. This one is deliberately not.
|
|
18
|
+
|
|
19
|
+
Single root; x-data single-quoted throughout.
|
|
20
|
+
%>
|
|
21
|
+
<div x-data="{
|
|
22
|
+
checked: false,
|
|
23
|
+
get props() { var c = $store.dsModals.current(); return (c && c.props) || {} }
|
|
24
|
+
}">
|
|
25
|
+
<%= render "studio/modals/blocks/card_header",
|
|
26
|
+
icon_emoji: "⚠️",
|
|
27
|
+
title_key: "props.title || 'Check Network'" do %>
|
|
28
|
+
<p class="text-sm text-body"
|
|
29
|
+
x-text="props.message || 'This request will be signed on the network shown below.'"></p>
|
|
30
|
+
<% end %>
|
|
31
|
+
|
|
32
|
+
<%# Both facts, named. The app supplies them; the engine never detects either. %>
|
|
33
|
+
<div class="rounded-lg border border-subtle bg-surface-alt p-3 mb-4 text-sm">
|
|
34
|
+
<div class="flex items-center justify-between gap-3">
|
|
35
|
+
<span class="text-secondary">App network</span>
|
|
36
|
+
<span class="font-semibold text-heading" x-text="props.networkLabel || 'devnet'"></span>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="flex items-center justify-between gap-3 mt-2">
|
|
39
|
+
<span class="text-secondary">Environment</span>
|
|
40
|
+
<span class="font-semibold text-heading" x-text="props.environmentLabel || 'QA'"></span>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<label class="flex items-start gap-3 rounded-lg border border-subtle p-3 mb-4 cursor-pointer">
|
|
45
|
+
<input type="checkbox" x-model="checked" class="mt-1">
|
|
46
|
+
<span class="text-sm text-secondary">I understand this wallet request will use the network shown above.</span>
|
|
47
|
+
</label>
|
|
48
|
+
|
|
49
|
+
<%# Equal weight, deliberately. Cancel is not the quiet option here. %>
|
|
50
|
+
<div class="grid grid-cols-2 gap-3">
|
|
51
|
+
<button type="button" class="btn btn-secondary btn-lg" @click="$store.dsModals.close()">Cancel</button>
|
|
52
|
+
<button type="button" class="btn btn-primary btn-lg" :disabled="!checked"
|
|
53
|
+
@click="$store.dsModals.close()">Continue</button>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Living style guide specimen — web3 email capture.
|
|
3
|
+
|
|
4
|
+
APP-SPECIFIC, engine chrome only (`blocks/_card_header`). The engine owns no
|
|
5
|
+
newsletter and no seeds.
|
|
6
|
+
|
|
7
|
+
WHY THIS CARD EXISTS AT ALL, which is the thing worth understanding: a web3
|
|
8
|
+
account signed in with a wallet has NO EMAIL ON FILE. Every other newsletter
|
|
9
|
+
path can assume one. This is the branch that cannot, so it asks — and it asks
|
|
10
|
+
at the last possible moment, mid-quest, rather than up front where it would
|
|
11
|
+
read as a signup wall on a product that deliberately does not have one.
|
|
12
|
+
|
|
13
|
+
It hands the address BACK rather than posting it: the real card closes itself
|
|
14
|
+
and calls props.onSubmit, so the caller owns the POST. That keeps one subscribe
|
|
15
|
+
path instead of two. Here the callback resolves locally.
|
|
16
|
+
|
|
17
|
+
Single root, and everything inside the double-quoted x-data is single-quoted —
|
|
18
|
+
an inner double quote closes the attribute and mounts a silent no-op.
|
|
19
|
+
%>
|
|
20
|
+
<div x-data="{
|
|
21
|
+
email: '',
|
|
22
|
+
submitted: false,
|
|
23
|
+
get valid() { return (this.email || '').trim().indexOf('@') > 0; },
|
|
24
|
+
submit() {
|
|
25
|
+
if (!this.valid) return;
|
|
26
|
+
// Demo: the real card closes and hands the address to props.onSubmit.
|
|
27
|
+
// Showing the acknowledgement instead keeps the specimen on screen.
|
|
28
|
+
this.submitted = true;
|
|
29
|
+
}
|
|
30
|
+
}">
|
|
31
|
+
<%= render "studio/modals/blocks/card_header",
|
|
32
|
+
size: :md,
|
|
33
|
+
icon_emoji: "📬",
|
|
34
|
+
title: "One more thing" do %>
|
|
35
|
+
<p class="text-sm text-body">
|
|
36
|
+
Drop your email to join the list and claim your 25 seeds. We only use it for
|
|
37
|
+
the newsletter.
|
|
38
|
+
</p>
|
|
39
|
+
<% end %>
|
|
40
|
+
|
|
41
|
+
<template x-if="!submitted">
|
|
42
|
+
<div>
|
|
43
|
+
<input type="email" x-model="email" @keydown.enter.prevent="submit()"
|
|
44
|
+
placeholder="you@example.com" autocomplete="email"
|
|
45
|
+
class="w-full bg-inset border border-subtle rounded-lg px-3 py-2 text-sm text-heading mb-3 focus:border-primary/50 focus:outline-none">
|
|
46
|
+
<button type="button" @click="submit()" :disabled="!valid"
|
|
47
|
+
class="btn btn-primary w-full disabled:opacity-60">Join & Claim 25 Seeds</button>
|
|
48
|
+
</div>
|
|
49
|
+
</template>
|
|
50
|
+
|
|
51
|
+
<template x-if="submitted">
|
|
52
|
+
<p class="text-sm text-center text-secondary">
|
|
53
|
+
Handed <span class="text-heading font-medium" x-text="email"></span> back to the
|
|
54
|
+
caller — the app owns the POST from here.
|
|
55
|
+
</p>
|
|
56
|
+
</template>
|
|
57
|
+
</div>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Living style guide specimen — the soft rate-limit wait.
|
|
3
|
+
|
|
4
|
+
APP-SPECIFIC, engine chrome only (`blocks/_card_header`). The engine throttles
|
|
5
|
+
nothing; turf's authedFetch 429 interceptor opens this when a general-tier cap
|
|
6
|
+
trips (hammering "Mint USDC", say).
|
|
7
|
+
|
|
8
|
+
THE TONE IS THE DESIGN. This is a SOFT cap on someone who did nothing wrong —
|
|
9
|
+
they clicked too fast. So the card is dismissible, the copy is "Easy there"
|
|
10
|
+
rather than an error, and it AUTO-CLOSES when the wait elapses instead of
|
|
11
|
+
making them acknowledge a penalty. A hard-looking card here would punish
|
|
12
|
+
enthusiasm.
|
|
13
|
+
|
|
14
|
+
The countdown is seeded from the 429's own Retry-After, so the number is the
|
|
15
|
+
server's, not a guess.
|
|
16
|
+
|
|
17
|
+
TWO THINGS THIS CARD ALREADY GETS RIGHT, worth preserving if it is ever
|
|
18
|
+
rewritten — both were learned the hard way on blocks/_age_gate this week:
|
|
19
|
+
it clears its interval in a destroy() METHOD (Alpine 3 dispatches no "destroy"
|
|
20
|
+
DOM event, so an @destroy listener never fires and the timer outlives every
|
|
21
|
+
close), and it counts whole seconds rather than dividing milliseconds.
|
|
22
|
+
|
|
23
|
+
Single root; x-data is single-quoted throughout.
|
|
24
|
+
%>
|
|
25
|
+
<div x-data="{
|
|
26
|
+
secondsLeft: 0,
|
|
27
|
+
_timer: null,
|
|
28
|
+
get props() { var c = $store.dsModals.current(); return (c && c.props) || {} },
|
|
29
|
+
get waitDisplay() {
|
|
30
|
+
var s = this.secondsLeft || 0;
|
|
31
|
+
return s > 90 ? (Math.ceil(s / 60) + ' min') : (s + 's');
|
|
32
|
+
},
|
|
33
|
+
init() {
|
|
34
|
+
this.secondsLeft = this.props.secondsLeft || 20;
|
|
35
|
+
var self = this;
|
|
36
|
+
this._timer = setInterval(function () {
|
|
37
|
+
self.secondsLeft = self.secondsLeft - 1;
|
|
38
|
+
if (self.secondsLeft <= 0) {
|
|
39
|
+
self.secondsLeft = 0;
|
|
40
|
+
self.stop();
|
|
41
|
+
$store.dsModals.close();
|
|
42
|
+
}
|
|
43
|
+
}, 1000);
|
|
44
|
+
},
|
|
45
|
+
stop() { if (this._timer) { clearInterval(this._timer); this._timer = null; } },
|
|
46
|
+
destroy() { this.stop(); }
|
|
47
|
+
}">
|
|
48
|
+
<%= render "studio/modals/blocks/card_header",
|
|
49
|
+
icon_emoji: "✋",
|
|
50
|
+
title: "Easy there" do %>
|
|
51
|
+
<p class="text-sm text-body">
|
|
52
|
+
<span x-text="props.message || 'You are going a bit fast.'"></span>
|
|
53
|
+
<span class="block mt-1">
|
|
54
|
+
Try again in <span class="text-heading font-semibold" x-text="waitDisplay"></span>.
|
|
55
|
+
</span>
|
|
56
|
+
</p>
|
|
57
|
+
<% end %>
|
|
58
|
+
|
|
59
|
+
<button type="button" @click="$store.dsModals.close()"
|
|
60
|
+
class="block mx-auto mt-1 text-sm text-secondary hover:text-heading transition">Dismiss</button>
|
|
61
|
+
</div>
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Living style guide specimen — the wallet switched underneath the session.
|
|
3
|
+
|
|
4
|
+
APP-SPECIFIC, engine chrome only (`blocks/_card_header`). The switch itself is
|
|
5
|
+
the host's: turf's wallet store owns continueSwitch, and the engine owns no
|
|
6
|
+
wallet.
|
|
7
|
+
|
|
8
|
+
THE SITUATION IT NAMES: the person changed accounts inside their wallet
|
|
9
|
+
extension while signed in here. Nothing is broken and nobody did anything
|
|
10
|
+
wrong — but the session and the extension now disagree about who this is, and
|
|
11
|
+
every on-chain action from here would use an identity the page is not showing.
|
|
12
|
+
|
|
13
|
+
SO IT SHOWS BOTH ADDRESSES, truncated the way every Solana UI truncates them
|
|
14
|
+
(4 leading, 4 trailing). Recognising your own address is the only way to tell a
|
|
15
|
+
deliberate switch from an accident, and it is the one check the app cannot make
|
|
16
|
+
for them.
|
|
17
|
+
|
|
18
|
+
The busy state matters: continuing re-derives a session against the new wallet,
|
|
19
|
+
which is a round trip. Without it the button looks dead and gets double-clicked.
|
|
20
|
+
|
|
21
|
+
Single root; x-data single-quoted throughout.
|
|
22
|
+
%>
|
|
23
|
+
<div x-data="{
|
|
24
|
+
busy: false,
|
|
25
|
+
get props() { var c = $store.dsModals.current(); return (c && c.props) || {} },
|
|
26
|
+
short(addr) {
|
|
27
|
+
addr = addr || '';
|
|
28
|
+
return addr.length > 12 ? (addr.slice(0, 6) + '...' + addr.slice(-4)) : addr;
|
|
29
|
+
},
|
|
30
|
+
go() {
|
|
31
|
+
if (this.busy) return;
|
|
32
|
+
this.busy = true;
|
|
33
|
+
var s = this;
|
|
34
|
+
// Demo: the real card awaits the host's wallet store. Hold briefly so
|
|
35
|
+
// the pending state is visible rather than skipped.
|
|
36
|
+
setTimeout(function () { s.busy = false; $store.dsModals.close(); }, 600);
|
|
37
|
+
}
|
|
38
|
+
}">
|
|
39
|
+
<%= render "studio/modals/blocks/card_header",
|
|
40
|
+
icon_emoji: "🔀",
|
|
41
|
+
title: "Wallet changed" do %>
|
|
42
|
+
<p class="text-sm text-body">
|
|
43
|
+
Your wallet is now on a different account than the one you signed in with.
|
|
44
|
+
Continue with the new one, or switch back in your wallet.
|
|
45
|
+
</p>
|
|
46
|
+
<% end %>
|
|
47
|
+
|
|
48
|
+
<div class="rounded-lg border border-subtle bg-surface-alt p-3 mb-4 text-sm">
|
|
49
|
+
<div class="flex items-center justify-between gap-3">
|
|
50
|
+
<span class="text-secondary">Signed in as</span>
|
|
51
|
+
<span class="font-mono text-heading" x-text="short(props.currentAddress) || '7xKp...JZ2Q'"></span>
|
|
52
|
+
</div>
|
|
53
|
+
<div class="flex items-center justify-between gap-3 mt-2">
|
|
54
|
+
<span class="text-secondary">Wallet now</span>
|
|
55
|
+
<span class="font-mono text-heading" x-text="short(props.newAddress) || '9mQt...Ld4V'"></span>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<button type="button" @click="go()" :disabled="busy"
|
|
60
|
+
class="btn btn-primary btn-lg w-full disabled:opacity-60">
|
|
61
|
+
<span x-show="!busy">Continue with this wallet</span>
|
|
62
|
+
<span x-show="busy" class="inline-flex items-center justify-center gap-2" style="display: none;">
|
|
63
|
+
<span class="spinner" aria-hidden="true"></span>
|
|
64
|
+
Switching…
|
|
65
|
+
</span>
|
|
66
|
+
</button>
|
|
67
|
+
|
|
68
|
+
<button type="button" @click="$store.dsModals.close()"
|
|
69
|
+
class="block mx-auto mt-3 text-sm text-secondary hover:text-heading transition">Not now</button>
|
|
70
|
+
</div>
|
data/lib/studio/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: studio-engine
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.61.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Alex McRitchie
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -450,6 +450,7 @@ files:
|
|
|
450
450
|
- app/views/studio/modals/blocks/_birthday.html.erb
|
|
451
451
|
- app/views/studio/modals/blocks/_card_header.html.erb
|
|
452
452
|
- app/views/studio/modals/blocks/_change_username.html.erb
|
|
453
|
+
- app/views/studio/modals/blocks/_close_x.html.erb
|
|
453
454
|
- app/views/studio/modals/blocks/_cta_redirect.html.erb
|
|
454
455
|
- app/views/studio/modals/blocks/_digit_reel.html.erb
|
|
455
456
|
- app/views/studio/modals/blocks/_entry_confirmed.html.erb
|
|
@@ -460,6 +461,7 @@ files:
|
|
|
460
461
|
- app/views/studio/modals/blocks/_processing_card.html.erb
|
|
461
462
|
- app/views/studio/modals/blocks/_progress_countdown.html.erb
|
|
462
463
|
- app/views/studio/modals/blocks/_progress_pill.html.erb
|
|
464
|
+
- app/views/studio/modals/blocks/_rail_row.html.erb
|
|
463
465
|
- app/views/studio/modals/blocks/_seeds_bar.html.erb
|
|
464
466
|
- app/views/studio/modals/blocks/_shell.html.erb
|
|
465
467
|
- app/views/studio/modals/blocks/_solana_tx_link.html.erb
|
|
@@ -507,11 +509,21 @@ files:
|
|
|
507
509
|
- app/views/style/modals/_auth.html.erb
|
|
508
510
|
- app/views/style/modals/_birthday.html.erb
|
|
509
511
|
- app/views/style/modals/_cosign_rejected.html.erb
|
|
512
|
+
- app/views/style/modals/_ds_close_x.html.erb
|
|
513
|
+
- app/views/style/modals/_ds_onramp_hub.html.erb
|
|
514
|
+
- app/views/style/modals/_ds_rail_row.html.erb
|
|
515
|
+
- app/views/style/modals/_ds_wallet_topup.html.erb
|
|
516
|
+
- app/views/style/modals/_email_change_pending.html.erb
|
|
510
517
|
- app/views/style/modals/_entry_tokens.html.erb
|
|
518
|
+
- app/views/style/modals/_it_begins.html.erb
|
|
519
|
+
- app/views/style/modals/_network_guard.html.erb
|
|
520
|
+
- app/views/style/modals/_newsletter_email.html.erb
|
|
511
521
|
- app/views/style/modals/_onchain_tx.html.erb
|
|
512
522
|
- app/views/style/modals/_quest_success.html.erb
|
|
523
|
+
- app/views/style/modals/_rate_limit_general.html.erb
|
|
513
524
|
- app/views/style/modals/_unsubscribe_confirm.html.erb
|
|
514
525
|
- app/views/style/modals/_unsubscribe_goodbye.html.erb
|
|
526
|
+
- app/views/style/modals/_wallet_changed.html.erb
|
|
515
527
|
- app/views/style/modals/_wallet_connect.html.erb
|
|
516
528
|
- app/views/style/modals/_wallet_deposit.html.erb
|
|
517
529
|
- app/views/style/modals/_wallet_setup.html.erb
|