studio-engine 0.60.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e147f36e9a7113442530805c34a0f5fe0b9fb1b37391b8e4271e89368b01236e
4
- data.tar.gz: 17203c174004a90979e88259c32b5672907fe8b5b6deadb9cf240f1d4c9aab85
3
+ metadata.gz: 64bf6960f2f9100da2fa8a1a38bd1ca62733d348b7393ca6bc279b2a223ad11b
4
+ data.tar.gz: 3b33af6cd3b2ca2f48e18b05b1cea868458c793663da2a887f2afbd3c795a4b8
5
5
  SHA512:
6
- metadata.gz: e1cacfd158c940e8d78c63d97d33c6bd248112a738032230a79ac480facc90077dffa185fa4fe352567b07204e91044f88dd7ea498906fb8ea8f5bba570944be
7
- data.tar.gz: e1c274a2ff0726e5ebced0ba9185157798e6c194e149ad9c846b10d215f1e282731a8838cd1ec9fa85f6d93ee94ddada47fb16bad2cf37569c181c4791310840
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 %>">&times;</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 %>
@@ -376,6 +376,20 @@
376
376
  <template x-if="$store.dsModals.current().id === 'network-guard'">
377
377
  <div><%= render "style/modals/network_guard" %></div>
378
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>
379
393
  <template x-if="$store.dsModals.current().id === 'rate-limit-general'">
380
394
  <div><%= render "style/modals/rate_limit_general" %></div>
381
395
  </template>
@@ -1243,6 +1257,53 @@
1243
1257
  </div>
1244
1258
  <% end %>
1245
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
+
1246
1307
  <%# Entry confirmed — the GENERIC web3 celebration (tx link + heading + drain
1247
1308
  CTA). seedsEarned: 0 so no seeds bar even where leveling is on: this card
1248
1309
  demos the clean success a web3-only app gets. The seeds variant lives in
@@ -1542,6 +1603,43 @@
1542
1603
  <span class="block h-1.5 w-12 mx-auto rounded" style="background: var(--color-text); opacity: .3"></span>
1543
1604
  </div>
1544
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">&times;</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 %>
1545
1643
  </div>
1546
1644
  </section>
1547
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&rsquo;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
+ &larr; 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 &rarr;
68
+ </button>
69
+ </div>
@@ -1,3 +1,3 @@
1
1
  module Studio
2
- VERSION = "0.60.2"
2
+ VERSION = "0.61.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: studio-engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.60.2
4
+ version: 0.61.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie
@@ -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,6 +509,10 @@ 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
510
516
  - app/views/style/modals/_email_change_pending.html.erb
511
517
  - app/views/style/modals/_entry_tokens.html.erb
512
518
  - app/views/style/modals/_it_begins.html.erb