studio-engine 0.61.0 → 0.61.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 64bf6960f2f9100da2fa8a1a38bd1ca62733d348b7393ca6bc279b2a223ad11b
4
- data.tar.gz: 3b33af6cd3b2ca2f48e18b05b1cea868458c793663da2a887f2afbd3c795a4b8
3
+ metadata.gz: e8e859fc4ea59dafad01b2aef524f8ff75bb2b5040bc05611ac17eec335c5c0a
4
+ data.tar.gz: ca4d9a0ed867017686c50d329190ef26d7488ea28349fa65996bd8b859ff4eee
5
5
  SHA512:
6
- metadata.gz: 28ce9d9910b4f4eb3f96ea64bb27d415988065b3c6609b489ef737811eba2ad9bb818edddf5ab504c9e077c8104f1c15d979b1fdc61f4a540d0330f04697d42b
7
- data.tar.gz: 0d303ef8c72f4927c78bba6ef585bd299d0af420e259c8f65273e13b1d01663d590b81b651d64f553be685a0a65af8d6766b8f8de7df0d7057ed5efd893955f8
6
+ metadata.gz: 805ad1e2f8d05504bb4ca7e782ab27fab96589dbe3acd5714502c24b0803de69631241a1a3374da1d7f92a762b1c21f6eb4122ba7c6cdafc3231844f2f03b479
7
+ data.tar.gz: 5cc030a1e38828fc0db6212f8686f6438253f00adf8a84dfa26faad576bf86fdda137b2d806e79d78a241b2b21ac706c39de6f99b694eedccfa034ec37669012
data/CHANGELOG.md CHANGED
@@ -233,6 +233,31 @@ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). This pro
233
233
 
234
234
  ### Fixed
235
235
 
236
+ - **The age gate's back link now returns the date the person already entered.**
237
+ `blocks/_age_gate`'s "Update your Birthday" swapped back to `blocks/_birthday`
238
+ with an EMPTY props object, discarding the `dobYear`/`dobMonth`/`dobDay` parts
239
+ the birthday factory had just handed across the store — and the factory started
240
+ its three fields at `""` and never read them anyway. So the correction path came
241
+ back BLANK: a mistyped year cost all three picks, on a card whose own header
242
+ comment promised "a correction, not a restart".
243
+
244
+ Both sides of the seam moved. `back()` forwards the three date parts — and only
245
+ those three, deliberately: `minAge`, `state` and `message` describe the refusal,
246
+ and `validates` is load-bearing by its ABSENCE (the style guide specimen reads an
247
+ absent prop as validating, and a forwarded stale one would let the refusal pick
248
+ the next card's mode). `window.birthdayModal` gained an `init` that re-picks the
249
+ three selects from the props of the modal entry it mounted in, clamping a day
250
+ that the restored month cannot hold.
251
+
252
+ **The gate is not weakened.** Restoring the date restores the DATE and never the
253
+ verdict: the card comes back submittable and the app's endpoint re-decides on the
254
+ next submit exactly as it did on the first. `e2e/birthday_gate.spec.js` asserts a
255
+ restored under-age date is refused again.
256
+
257
+ **No consumer action.** A host that renders `blocks/_age_gate` and
258
+ `studio/_birthday_assets` gets this by upgrading; nothing in the call signature
259
+ changed and no host wiring moves.
260
+
236
261
  - **The Geo signpost now reaches every app whose admin chrome the engine owns.**
237
262
  The row added last release went into `components/_admin_dropdown` alone, on the
238
263
  premise that the shared dropdown reaches every app from one change. It does
@@ -29,6 +29,13 @@
29
29
  age-gate card instead. `_finish` is unchanged: the success path still flips
30
30
  session.ageVerified and dispatches `age-verified`, which is the hook consuming
31
31
  apps already listen on, so nothing downstream had to change.
32
+
33
+ AND THE HANDOFF RETURNS (2026-08-26). `_reject` sends dobYear/dobMonth/dobDay
34
+ across the store; the gate card's back() hands the same three parts back; and
35
+ `init` re-picks the three selects from them, so the correction the gate card
36
+ offers costs one scroll rather than all three fields. The date is restored,
37
+ never the VERDICT: the card comes back submittable and the app's endpoint
38
+ re-decides on the next submit exactly as it did on the first.
32
39
  %>
33
40
  <script>
34
41
  (function () {
@@ -89,6 +96,56 @@
89
96
  if (this.day && parseInt(this.day, 10) > this.dayOptions.length) this.day = "";
90
97
  this.error = "";
91
98
  },
99
+ // THE RETURN TRIP. _reject writes dobYear/dobMonth/dobDay onto the store
100
+ // on the way out to the age-gate card, and that card's back() hands the
101
+ // same three parts straight back — so a card mounted from a return trip
102
+ // finds the person's date on its own modal entry and re-picks the three
103
+ // selects. Without this the factory started at "" whatever it was handed,
104
+ // and a mistyped year cost all three fields (fixed 2026-08-26).
105
+ //
106
+ // Read off the STORE rather than taken as an opt: that is where this
107
+ // factory already meets the store (_reject writes to the same object
108
+ // through the same guard), the ERB locals in blocks/_birthday are
109
+ // server-side and cannot carry a client value anyway, and it means a
110
+ // consuming app that renders the card needs no change at all.
111
+ //
112
+ // STRINGS, because that is what the selects produce: the options render
113
+ // :value="m.n" — a number stringified by the DOM — and x-model writes
114
+ // that string back. Seeding a Number would leave the field holding a type
115
+ // the person's own pick never produces.
116
+ _returnedDob() {
117
+ var props;
118
+ try {
119
+ var entry = Alpine.store(this.store).current();
120
+ props = (entry && entry.props) || {};
121
+ } catch (e) {
122
+ return null;
123
+ }
124
+ var y = parseInt(props.dobYear, 10),
125
+ m = parseInt(props.dobMonth, 10),
126
+ d = parseInt(props.dobDay, 10);
127
+ if (!y || !m || !d) return null;
128
+ return { year: String(y), month: String(m), day: String(d) };
129
+ },
130
+ // Alpine calls init() on the data object at MOUNT, synchronously, inside
131
+ // the x-data directive and before it walks the three <select>s below
132
+ // (Alpine 3.16.1 as vendored: `a.init && T(t, a.init)`). So this is the
133
+ // fields' initial state rather than a later mutation of them, and x-model
134
+ // paints the selects from it. Measured in chromium: all three hold the
135
+ // restored values on the FIRST frame the card exists, six runs out of six
136
+ // — no deferral needed, and e2e/birthday_gate.spec.js is what watches it.
137
+ init() {
138
+ var dob = this._returnedDob();
139
+ if (!dob) return;
140
+ this.year = dob.year;
141
+ this.month = dob.month;
142
+ // Day LAST, and clamped: dayOptions is only the restored month's list
143
+ // once month and year are set, and an impossible pair from a hand-built
144
+ // props bag (Feb 30) must land empty rather than leave the select
145
+ // holding a value its own options do not contain — which would read as
146
+ // complete and submit a date nobody picked.
147
+ this.day = parseInt(dob.day, 10) <= this.dayOptions.length ? dob.day : "";
148
+ },
92
149
  // Success path: flip a session flag if present, close via the app's store,
93
150
  // and let the app resume its own flow (the entry gate listens for this).
94
151
  _finish() {
@@ -16,7 +16,14 @@
16
16
  the back link — the thing they might NEED. A wrong birthday is an ordinary
17
17
  typo (a mis-picked year is one scroll away), and a gate with no
18
18
  way back turns a typo into a locked door. swap()s back to the
19
- birthday card, so this is a correction, not a restart.
19
+ birthday card CARRYING THE DATE, so a mistyped year costs one
20
+ scroll and not all three fields. That is what makes this a
21
+ correction rather than a restart: back() forwards the dobYear /
22
+ dobMonth / dobDay parts _reject handed across the store on the
23
+ way in, and the birthday factory seeds its three selects from
24
+ them. Until 2026-08-26 it swapped with an EMPTY props object and
25
+ the card came back blank, which is the restart this paragraph
26
+ disclaimed.
20
27
 
21
28
  DELIBERATELY NOT DISMISSIBLE-BY-DEFAULT-ONLY: the host still decides via
22
29
  props.dismissible at open() time, exactly like every other card. What this
@@ -146,7 +153,26 @@
146
153
  // sibling cards that poll: modals/_web3_step_up, blocks/_cta_redirect,
147
154
  // blocks/_seeds_bar.
148
155
  destroy() { this.stop(); },
149
- back() { $store.<%= modal_store %>.swap('<%= j birthday_modal_id.to_s %>', {}); }
156
+ // THE DATE RIDES BACK. _reject put these three parts on the store on the
157
+ // way here so this card could say WHEN; handing them back is what makes
158
+ // the return trip a correction instead of a blank form. This swapped with
159
+ // an empty object until 2026-08-26, and a mistyped year cost all three
160
+ // picks.
161
+ //
162
+ // THE THREE PARTS ONLY, deliberately not the whole props bag. minAge,
163
+ // state and message describe the REFUSAL: message would land as an error
164
+ // line on a card nobody has submitted yet, and `validates` is load-bearing
165
+ // by its ABSENCE — the style guide specimen reads an absent prop as
166
+ // validating, so forwarding a stale one would let this card pick the next
167
+ // card's mode (style/modals/_birthday.html.erb).
168
+ back() {
169
+ var p = this.props;
170
+ $store.<%= modal_store %>.swap('<%= j birthday_modal_id.to_s %>', {
171
+ dobYear: p.dobYear || null,
172
+ dobMonth: p.dobMonth || null,
173
+ dobDay: p.dobDay || null
174
+ });
175
+ }
150
176
  }"
151
177
  <% if countdown %>x-init="start()"<% end %>
152
178
  class="relative">
@@ -211,7 +237,9 @@
211
237
 
212
238
  <% if birthday_modal_id.present? %>
213
239
  <%# The correction path. A mis-picked year is one scroll away from a right
214
- one, and without this the typo is indistinguishable from the verdict. %>
240
+ one, and without this the typo is indistinguishable from the verdict.
241
+ back() carries the date across, so what comes back is the same three
242
+ picks with the wrong one still selected. %>
215
243
  <button type="button" @click="back()"
216
244
  class="block mx-auto mt-3 text-sm text-secondary hover:text-heading transition underline-offset-2 hover:underline">
217
245
  <%= back_label %>
@@ -390,6 +390,15 @@
390
390
  <template x-if="$store.dsModals.current().id === 'ds-onramp-hub'">
391
391
  <div><%= render "style/modals/ds_onramp_hub" %></div>
392
392
  </template>
393
+ <template x-if="$store.dsModals.current().id === 'ds-buy-entry-token'">
394
+ <div><%= render "style/modals/ds_buy_entry_token" %></div>
395
+ </template>
396
+ <template x-if="$store.dsModals.current().id === 'ds-cdp-ramp'">
397
+ <div><%= render "style/modals/ds_cdp_ramp" %></div>
398
+ </template>
399
+ <template x-if="$store.dsModals.current().id === 'ds-newsletter-success'">
400
+ <div><%= render "style/modals/ds_newsletter_success" %></div>
401
+ </template>
393
402
  <template x-if="$store.dsModals.current().id === 'rate-limit-general'">
394
403
  <div><%= render "style/modals/rate_limit_general" %></div>
395
404
  </template>
@@ -935,6 +944,28 @@
935
944
  </div>
936
945
  <% end %>
937
946
 
947
+ <%# ds-newsletter-success — the STANDALONE celebration, and its two forks. %>
948
+ <%= render layout: "style/modal_specimen", locals: {
949
+ label: "Subscribed! (standalone)",
950
+ reference: %(the standalone newsletter celebration (style/modals/_ds_newsletter_success) — turf-monster's modals/_newsletter_success. Distinct from the card above, which is the leveling activity celebrating IN PLACE on the card the person was already on; this one arrives on its own — swapped in from the subscribe modal, or opened from the /account newsletter row — so it has to carry its own way forward. TWO FORKS, NEITHER VISIBLE IN THE MARKUP, which is why it is carded separately rather than folded in. THE SEEDS BAR IS FIRST-JOIN ONLY: a re-subscribe earns nothing, and the card HIDES the bar rather than showing a flat "0 seeds", which would read to the person as a bug about their own account rather than an accurate zero. THE CTA IS SERVER-DECIDED: with the username quest still open (current_user.next_quest == :username) the button nudges straight into it, otherwise it sends them to browse contests — showing only one of those hides the funnel this celebration doubles as. Both are toggles here so the cabinet can walk all four combinations; in the app the CTA fork is SERVER-rendered, precisely so the button never flashes the wrong option at someone whose quest is already done. Open with $store.dsModals.open('ds-newsletter-success', { firstJoin: true, questOpen: true })),
951
+ card_data: "opts: { firstJoin: true, questOpen: true }",
952
+ toggles: [{ model: "opts.firstJoin", label: "First join" },
953
+ { model: "opts.questOpen", label: "Quest open" }],
954
+ open_expr: "$store.dsModals.open('ds-newsletter-success', { firstJoin: opts.firstJoin, questOpen: opts.questOpen })",
955
+ glow_when: ds_glow.call("ds-newsletter-success") } do %>
956
+ <div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
957
+ <div class="text-2xl leading-none">🌱</div>
958
+ <span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
959
+ <%# the first-join seeds bar %>
960
+ <span class="flex gap-1">
961
+ <span class="flex-1 h-2 rounded" style="background: var(--color-cta)"></span>
962
+ <span class="flex-1 h-2 rounded" style="background: var(--color-cta); opacity:.35"></span>
963
+ <span class="flex-1 h-2 rounded" style="background: var(--color-cta); opacity:.18"></span>
964
+ </span>
965
+ <span class="block h-4 w-full rounded" style="background: var(--color-cta)"></span>
966
+ </div>
967
+ <% end %>
968
+
938
969
  <%# THE WAY OUT. The three cards above walk a user IN; a section that only
939
970
  ever demonstrates opt-in is showing the happy half of a flow, not the
940
971
  flow. These two are the exit, and they are a PAIR — the confirm card
@@ -1304,6 +1335,42 @@
1304
1335
  </div>
1305
1336
  <% end %>
1306
1337
 
1338
+ <%# ds-buy-entry-token — reached with a lineup picked and no way to pay. %>
1339
+ <%= render layout: "style/modal_specimen", locals: {
1340
+ label: "Buy entry token",
1341
+ reference: %(the entry-token purchase (style/modals/_ds_buy_entry_token) — turf-monster's modals/_buy_entry_token, built from blocks/_close_x and two blocks/_rail_row. Reached when a player has picked a lineup and has no way to pay for it, which is why the copy says the lineup is SAVED: losing their picks is the one thing they are afraid of at that moment. The ranking is a PRODUCT decision rather than a visual one — Coinflow leads and carries the "New" badge because it mints a spendable entry token directly, the shortest path from here to being in the contest, while Stripe swaps into the auth wizard's tokens picker, the older and longer path, and defers. Open with $store.dsModals.open('ds-buy-entry-token')),
1342
+ open_expr: "$store.dsModals.open('ds-buy-entry-token')",
1343
+ glow_when: ds_glow.call("ds-buy-entry-token"),
1344
+ disabled: !web3_on, openable: true } do %>
1345
+ <div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
1346
+ <span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
1347
+ <span class="flex items-center gap-1.5 rounded-md p-1.5" style="border: 2px solid var(--color-primary)">
1348
+ <span class="block h-4 w-4 rounded" style="background: var(--color-cta)"></span>
1349
+ <span class="block h-1.5 flex-1 rounded" style="background: var(--color-text); opacity: .3"></span>
1350
+ </span>
1351
+ <span class="flex items-center gap-1.5 rounded-md p-1.5 border border-subtle">
1352
+ <span class="block h-4 w-4 rounded" style="background: var(--color-text); opacity: .25"></span>
1353
+ <span class="block h-1.5 flex-1 rounded" style="background: var(--color-text); opacity: .18"></span>
1354
+ </span>
1355
+ </div>
1356
+ <% end %>
1357
+
1358
+ <%# ds-cdp-ramp — ONE of thirteen states, and the card says so. %>
1359
+ <%= render layout: "style/modal_specimen", locals: {
1360
+ label: "Coinbase ramp (preflight)",
1361
+ reference: %(the Coinbase ramp's BUY PREFLIGHT (style/modals/_ds_cdp_ramp) — turf-monster's modals/_cdp_ramp. DELIBERATELY PARTIAL: that modal is a TWO-FLOW step machine (buy and cash-out) across THIRTEEN states — preflight, opening, waiting, awaiting-cdp, send, settling and the rest — and this cards exactly one of them, the buy preflight, which is the state both funding cards hand off to and therefore the only one a player reaches without already being mid-flow. The other twelve are uncarded BY DECISION (operator, 2026-08-26), which is a different thing from an oversight — a cabinet that shows one state of a thirteen-state machine without admitting it teaches a reader that the machine is simpler than it is. /tasks/port-cdp-ramp-specimen holds the full state map if that decision reverses. Open with $store.dsModals.open('ds-cdp-ramp')),
1362
+ open_expr: "$store.dsModals.open('ds-cdp-ramp')",
1363
+ glow_when: ds_glow.call("ds-cdp-ramp"),
1364
+ disabled: !web3_on, openable: true } do %>
1365
+ <div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2 relative">
1366
+ <span class="absolute top-1 right-2 text-secondary leading-none">&times;</span>
1367
+ <span class="block h-2 w-28 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
1368
+ <span class="block h-1.5 w-full rounded" style="background: var(--color-text); opacity: .12"></span>
1369
+ <span class="block h-1.5 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
1370
+ <span class="block h-4 w-full rounded" style="background: var(--color-cta)"></span>
1371
+ </div>
1372
+ <% end %>
1373
+
1307
1374
  <%# Entry confirmed — the GENERIC web3 celebration (tx link + heading + drain
1308
1375
  CTA). seedsEarned: 0 so no seeds bar even where leveling is on: this card
1309
1376
  demos the clean success a web3-only app gets. The seeds variant lives in
@@ -23,11 +23,17 @@
23
23
 
24
24
  Why absent is not "no bar": the only card that can REACH the age gate is the
25
25
  validating one — the no-bar card carries no min_age, so the factory's _reject
26
- never fires. So every arrival with no props is a return trip from the gate,
27
- and `back()` swaps with EMPTY props by design (blocks/_age_gate.html.erb).
28
- Reading undefined as falsy sent that return trip to the NO-BAR card, where
29
- resubmitting the SAME under-age date was accepted — the guide demonstrating an
30
- age gate being bypassed. Caught in review, 2026-08-25.
26
+ never fires. So every arrival with no `validates` is a return trip from the
27
+ gate, and `back()` never sends one: it forwards the dobYear/dobMonth/dobDay
28
+ parts and NOTHING ELSE, deliberately, so the refusal cannot pick this
29
+ specimen's mode (blocks/_age_gate.html.erb). Reading undefined as falsy sent
30
+ that return trip to the NO-BAR card, where resubmitting the SAME under-age
31
+ date was accepted — the guide demonstrating an age gate being bypassed.
32
+ Caught in review, 2026-08-25.
33
+
34
+ So the default is still load-bearing, and it is load-bearing in the same way
35
+ after the gate started carrying the date back (2026-08-26): the props bag on
36
+ a return trip is no longer empty, but `validates` is still absent from it.
31
37
 
32
38
  The engine primitive is right and is NOT changed: in a real host the mode
33
39
  comes from ERB locals, not from a prop, so this is a two-mode SPECIMEN's
@@ -0,0 +1,40 @@
1
+ <%#
2
+ Living style guide specimen — Buy an Entry Token.
3
+
4
+ APP-SPECIFIC flow, engine chrome throughout (blocks/_close_x + two
5
+ blocks/_rail_row). Reached when a player has picked a lineup and has no way to
6
+ pay for it, which is why the copy says the lineup is SAVED: the one thing they
7
+ are afraid of at that moment is losing their picks.
8
+
9
+ TWO RAILS, RANKED, and the ranking is a product decision rather than a visual
10
+ one. Coinflow leads and carries the "New" badge because it mints a spendable
11
+ entry token directly — it is the shortest path from here to being in the
12
+ contest. Stripe still swaps into the auth wizard's tokens picker, which is the
13
+ older, longer path, so it defers.
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-2">
21
+ <h3 class="text-heading font-bold text-lg leading-tight">Buy an Entry Token</h3>
22
+ </div>
23
+ <p class="text-xs text-body mb-4 text-center">
24
+ 1 token = 1 contest entry. Your lineup is saved &mdash; pick how to pay.
25
+ </p>
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: "CF", icon_bg: "#14B8A6",
31
+ title: "Coinflow · $19", subtitle: "Buy 1 entry with a card · fastest",
32
+ badge: "New", data: { ds_rail: "coinflow" } %>
33
+
34
+ <%= render "studio/modals/blocks/rail_row",
35
+ on_click: "$store.dsModals.close()",
36
+ icon_label: "S", icon_bg: "#635BFF",
37
+ title: "Card · $19", subtitle: "Buy an entry token with a card",
38
+ data: { ds_rail: "stripe" } %>
39
+ </div>
40
+ </div>
@@ -0,0 +1,41 @@
1
+ <%#
2
+ Living style guide specimen — the Coinbase ramp, BUY PREFLIGHT only.
3
+
4
+ DELIBERATELY PARTIAL, and saying so is the point. turf's modals/_cdp_ramp is a
5
+ TWO-FLOW step machine — buy and cash-out — across thirteen states: preflight,
6
+ opening, waiting, awaiting-cdp, send, settling and the rest. This cards ONE of
7
+ them: the buy preflight, which is the state both funding cards hand off to and
8
+ therefore the only one a player reaches without already being mid-flow.
9
+
10
+ The other twelve are uncarded BY DECISION (operator, 2026-08-26: minimal
11
+ migration is fine here). That is a different thing from an oversight, and the
12
+ reference blurb on this card says so, because a cabinet that shows one state of
13
+ a thirteen-state machine without admitting it teaches a reader that the machine
14
+ is simpler than it is. /tasks/port-cdp-ramp-specimen holds the full state map
15
+ if the decision reverses.
16
+
17
+ APP-SPECIFIC, engine chrome: blocks/_close_x plus the centred title this whole
18
+ funding family uses.
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">Buy USDC with Coinbase</h3>
27
+ </div>
28
+
29
+ <p class="text-xs text-body mb-4 text-center">
30
+ You&rsquo;ll finish the purchase in a Coinbase tab. We&rsquo;ll update here
31
+ automatically once the USDC lands in your wallet.
32
+ </p>
33
+
34
+ <button type="button" @click="$store.dsModals.close()"
35
+ class="btn btn-primary btn-lg w-full">Continue to Coinbase</button>
36
+
37
+ <button type="button" @click="$store.dsModals.close()"
38
+ class="block mx-auto mt-3 text-sm text-secondary hover:text-heading transition">
39
+ Not now
40
+ </button>
41
+ </div>
@@ -0,0 +1,67 @@
1
+ <%#
2
+ Living style guide specimen — Subscribed!, the standalone newsletter
3
+ celebration.
4
+
5
+ APP-SPECIFIC, engine chrome (blocks/_card_header at :lg). Distinct from the
6
+ Profile Leveling "Subscribed!" beat, which is the leveling activity celebrating
7
+ IN PLACE on the card the person was already on. This one arrives on its own —
8
+ swapped in from the subscribe modal, or opened from the /account newsletter
9
+ row — so it has to carry its own way forward.
10
+
11
+ TWO FORKS, AND NEITHER IS VISIBLE TO A READER OF THE MARKUP. That is the whole
12
+ reason this card exists separately rather than being folded into the leveling
13
+ beat:
14
+
15
+ · THE SEEDS BAR IS FIRST-JOIN ONLY. A re-subscribe earns nothing, and the
16
+ card HIDES the bar rather than showing a flat "0 seeds" — which would read
17
+ to the person as a bug about their own account rather than as an accurate
18
+ zero.
19
+ · THE CTA IS SERVER-DECIDED. If the username quest is still open
20
+ (current_user.next_quest == :username) the button nudges straight into it;
21
+ otherwise it sends them off to browse contests. Showing only one of those
22
+ hides the funnel that this celebration doubles as.
23
+
24
+ Both are gated on props here so the cabinet can walk all four combinations.
25
+ In the app the CTA fork is SERVER-rendered, precisely so the button never
26
+ flashes the wrong option at someone whose quest is already done.
27
+
28
+ Single root: the outer <div> is the host's required root.
29
+ %>
30
+ <div x-data="{
31
+ get props() { var c = $store.dsModals.current(); return (c && c.props) || {} },
32
+ get firstJoin() { return !!this.props.firstJoin },
33
+ get questOpen() { return !!this.props.questOpen }
34
+ }">
35
+ <%= render "studio/modals/blocks/card_header",
36
+ size: :lg,
37
+ icon_emoji: "🌱",
38
+ title: "Subscribed!",
39
+ subtitle: "You're on the list. We'll keep the sports news and contest updates coming." %>
40
+
41
+ <%# First join only. The engine's :leveling seeds bar — the bar is the
42
+ engine's, the numbers are the app's. %>
43
+ <template x-if="firstJoin">
44
+ <div class="mb-5">
45
+ <%= render "studio/modals/blocks/seeds_bar",
46
+ seeds_earned_key: "25",
47
+ seeds_total_key: "125",
48
+ seeds_per_level: 100,
49
+ free_entry_label: "Free Entry Token 🎟️" %>
50
+ </div>
51
+ </template>
52
+
53
+ <template x-if="questOpen">
54
+ <button type="button" @click="$store.dsModals.close()"
55
+ class="btn btn-primary btn-lg w-full">Next Quest</button>
56
+ </template>
57
+
58
+ <template x-if="!questOpen">
59
+ <button type="button" @click="$store.dsModals.close()"
60
+ class="btn btn-primary btn-lg w-full">Check Out Contests</button>
61
+ </template>
62
+
63
+ <button type="button" @click="$store.dsModals.close()"
64
+ class="block mx-auto mt-3 text-sm text-secondary hover:text-heading transition">
65
+ Close
66
+ </button>
67
+ </div>
@@ -1,3 +1,3 @@
1
1
  module Studio
2
- VERSION = "0.61.0"
2
+ VERSION = "0.61.2"
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.61.0
4
+ version: 0.61.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie
@@ -509,7 +509,10 @@ files:
509
509
  - app/views/style/modals/_auth.html.erb
510
510
  - app/views/style/modals/_birthday.html.erb
511
511
  - app/views/style/modals/_cosign_rejected.html.erb
512
+ - app/views/style/modals/_ds_buy_entry_token.html.erb
513
+ - app/views/style/modals/_ds_cdp_ramp.html.erb
512
514
  - app/views/style/modals/_ds_close_x.html.erb
515
+ - app/views/style/modals/_ds_newsletter_success.html.erb
513
516
  - app/views/style/modals/_ds_onramp_hub.html.erb
514
517
  - app/views/style/modals/_ds_rail_row.html.erb
515
518
  - app/views/style/modals/_ds_wallet_topup.html.erb