studio-engine 0.60.1 → 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/app/views/style/_modals.html.erb +141 -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 +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e147f36e9a7113442530805c34a0f5fe0b9fb1b37391b8e4271e89368b01236e
|
|
4
|
+
data.tar.gz: 17203c174004a90979e88259c32b5672907fe8b5b6deadb9cf240f1d4c9aab85
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e1cacfd158c940e8d78c63d97d33c6bd248112a738032230a79ac480facc90077dffa185fa4fe352567b07204e91044f88dd7ea498906fb8ea8f5bba570944be
|
|
7
|
+
data.tar.gz: e1c274a2ff0726e5ebced0ba9185157798e6c194e149ad9c846b10d215f1e282731a8838cd1ec9fa85f6d93ee94ddada47fb16bad2cf37569c181c4791310840
|
|
@@ -363,6 +363,26 @@
|
|
|
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
|
+
<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
|
+
|
|
366
386
|
<%# --- Newsletter exit + the off-page quest celebration --- %>
|
|
367
387
|
<template x-if="$store.dsModals.current().id === 'quest-success'">
|
|
368
388
|
<div><%= render "style/modals/quest_success" %></div>
|
|
@@ -763,6 +783,24 @@
|
|
|
763
783
|
<span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
764
784
|
</div>
|
|
765
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 %>
|
|
766
804
|
</div>
|
|
767
805
|
</section>
|
|
768
806
|
|
|
@@ -940,6 +978,22 @@
|
|
|
940
978
|
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
941
979
|
</div>
|
|
942
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 %>
|
|
943
997
|
</div>
|
|
944
998
|
</section>
|
|
945
999
|
|
|
@@ -1207,6 +1261,57 @@
|
|
|
1207
1261
|
<span class="block h-4 w-full rounded" style="background: var(--color-cta)"></span>
|
|
1208
1262
|
</div>
|
|
1209
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 %>
|
|
1210
1315
|
</div>
|
|
1211
1316
|
</section>
|
|
1212
1317
|
|
|
@@ -1339,6 +1444,25 @@
|
|
|
1339
1444
|
<span class="block h-4 w-full rounded" style="background: var(--color-cta)"></span>
|
|
1340
1445
|
</div>
|
|
1341
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 %>
|
|
1342
1466
|
</div>
|
|
1343
1467
|
</section>
|
|
1344
1468
|
|
|
@@ -1401,6 +1525,23 @@
|
|
|
1401
1525
|
<span class="block h-1.5 w-16 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
|
|
1402
1526
|
</div>
|
|
1403
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 %>
|
|
1404
1545
|
</div>
|
|
1405
1546
|
</section>
|
|
1406
1547
|
|
|
@@ -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.60.
|
|
4
|
+
version: 0.60.2
|
|
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
|
|
@@ -507,11 +507,17 @@ files:
|
|
|
507
507
|
- app/views/style/modals/_auth.html.erb
|
|
508
508
|
- app/views/style/modals/_birthday.html.erb
|
|
509
509
|
- app/views/style/modals/_cosign_rejected.html.erb
|
|
510
|
+
- app/views/style/modals/_email_change_pending.html.erb
|
|
510
511
|
- app/views/style/modals/_entry_tokens.html.erb
|
|
512
|
+
- app/views/style/modals/_it_begins.html.erb
|
|
513
|
+
- app/views/style/modals/_network_guard.html.erb
|
|
514
|
+
- app/views/style/modals/_newsletter_email.html.erb
|
|
511
515
|
- app/views/style/modals/_onchain_tx.html.erb
|
|
512
516
|
- app/views/style/modals/_quest_success.html.erb
|
|
517
|
+
- app/views/style/modals/_rate_limit_general.html.erb
|
|
513
518
|
- app/views/style/modals/_unsubscribe_confirm.html.erb
|
|
514
519
|
- app/views/style/modals/_unsubscribe_goodbye.html.erb
|
|
520
|
+
- app/views/style/modals/_wallet_changed.html.erb
|
|
515
521
|
- app/views/style/modals/_wallet_connect.html.erb
|
|
516
522
|
- app/views/style/modals/_wallet_deposit.html.erb
|
|
517
523
|
- app/views/style/modals/_wallet_setup.html.erb
|