studio-engine 0.62.0 → 0.62.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: 3af6e5f7754c621df6bba0ef0972d018d48f0a08ede302530b5d5922e43328cf
4
- data.tar.gz: e54f8c9cfbbd02cf9473e7e68ac6150a3a7c8da4ab861070da662184e1ee082d
3
+ metadata.gz: 822c72023af8d108848e67535194fa5a115443b4faaeebd7d9adc931ffcc7678
4
+ data.tar.gz: a1ef68303215291461d2e07b5237506c57fc6ab83e4fd8b9edc9f18a39e19e51
5
5
  SHA512:
6
- metadata.gz: '08ae8d20d1a6e862e6985a2589221a87207d0ed562d0a6b696563b34d301cc77e6eef0bbc828074b09f104550041cbab78e529d8e553253b4b0de7dc5cb38432'
7
- data.tar.gz: 1b785c9cab657acc47e1fd8d56109d91727086bf8b2aeb98bf0ebc1938bf550c3f6233fd4daa885daeab35a4b2a77b04b8f842cb14c10b7fec7f6cdcaab745b9
6
+ metadata.gz: dac77f69b4c34943fe3bd8f8a790dbbadfd8ebbc4e80a17595af78fff7f054e9321ea92062769c474ae56b70da09d051a9d45801f0f0aff2197fedc794deaaba
7
+ data.tar.gz: 723edaf2959e13979ede94523e578ce1d612cbe9911dfd4f1340de8a77b73fa2a2a2ee2c866f3f8078a780455af3baeafd5f369fb232a90a024987ed409a84e1
@@ -30,7 +30,7 @@
30
30
  <h3 class="text-heading font-bold text-lg text-center mb-4">Crop Photo</h3>
31
31
 
32
32
  <template x-if="error">
33
- <p class="text-red-400 text-sm mb-3 text-center" x-text="error"></p>
33
+ <p role="alert" class="text-red-400 text-sm mb-3 text-center" x-text="error"></p>
34
34
  </template>
35
35
 
36
36
  <input type="file" x-ref="filePicker" @change="onFilePicked($event)" accept="image/*" class="hidden">
@@ -261,7 +261,7 @@
261
261
  </template>
262
262
 
263
263
  <template x-if="error">
264
- <p class="text-red-400 text-sm mt-3 text-center" x-text="error"></p>
264
+ <p role="alert" class="text-red-400 text-sm mt-3 text-center" x-text="error"></p>
265
265
  </template>
266
266
 
267
267
  <%# SECONDARY — always reachable, so a user whose remembered wallet is the
@@ -21,5 +21,5 @@
21
21
  <span x-text="props.submitting === 'magic-link' ? 'Resending…' : ((props.resendCooldown || 0) > 0 ? ('Resend available in ' + props.resendCooldown + 's') : 'Resend link')"></span>
22
22
  </span>
23
23
  </button>
24
- <p x-show="props.resendError" x-cloak class="text-xs text-red-400 mt-3 text-center" x-text="props.resendError"></p>
24
+ <p role="alert" x-show="props.resendError" x-cloak class="text-xs text-red-400 mt-3 text-center" x-text="props.resendError"></p>
25
25
  <button @click="$store.<%= modal_store %>.close()" class="block mx-auto mt-3 text-sm text-secondary hover:text-heading transition">Close</button>
@@ -112,7 +112,7 @@
112
112
  A refusal is not an error, and showing them in the same red paragraph is
113
113
  what let the dead end hide. %>
114
114
  <template x-if="error">
115
- <p class="text-red-400 text-sm mb-3" x-text="error"></p>
115
+ <p role="alert" class="text-red-400 text-sm mb-3" x-text="error"></p>
116
116
  </template>
117
117
 
118
118
  <%# Disabled ONLY for an incomplete date or an in-flight submit. It used to be
@@ -51,6 +51,13 @@
51
51
  seeds_total_key — Alpine expr: new seeds total. Default "props.seedsTotal".
52
52
  seeds_per_level — Integer seeds per level. Default 100.
53
53
  free_entry_label — reward copy on a level-up. Default "Free Entry Earned 🎟️".
54
+ secondary_label — optional secondary action label under the CTA (e.g.
55
+ "Dismiss"). Forwarded to _success_card, which renders a
56
+ button dispatching secondary_event.
57
+ secondary_event — window event name for that button. BOTH are required or
58
+ NEITHER renders, matching how _success_card and _error_card
59
+ already gate the pair — a label with no event is a button
60
+ that does nothing.
54
61
  %>
55
62
  <%
56
63
  modal_store = local_assigns.fetch(:modal_store, "modals")
@@ -70,6 +77,8 @@
70
77
  seeds_total_key = local_assigns.fetch(:seeds_total_key, "props.seedsTotal")
71
78
  seeds_per_level = local_assigns.fetch(:seeds_per_level, 100)
72
79
  free_entry_label = local_assigns.fetch(:free_entry_label, "Free Entry Earned 🎟️")
80
+ secondary_label = local_assigns[:secondary_label]
81
+ secondary_event = local_assigns[:secondary_event]
73
82
 
74
83
  sc_locals = {
75
84
  large_title: true,
@@ -89,6 +98,17 @@
89
98
  else
90
99
  sc_locals[:message] = subtitle
91
100
  end
101
+
102
+ # BOTH OR NEITHER, deliberately. sc_locals was a FIXED hash, so a consumer
103
+ # adopting this card in place of its own fork lost any secondary action it had
104
+ # SILENTLY — there was no error, the button simply stopped existing. That is
105
+ # what blocked turf-monster's entry-confirmed defork: its card ends in a
106
+ # "Dismiss" link, and the call site sits inside a <template x-if>, which takes
107
+ # ONE root, so the button cannot be kept outside the block either.
108
+ if secondary_label.present? && secondary_event.present?
109
+ sc_locals[:secondary_label] = secondary_label
110
+ sc_locals[:secondary_event] = secondary_event
111
+ end
92
112
  %>
93
113
  <%# props getter + clusterParam live on the root scope so _success_card's nested
94
114
  Alpine expressions (props.txSignature, props.lobbyUrl, clusterParam) resolve up
@@ -25,7 +25,11 @@
25
25
  icon_emoji = local_assigns[:icon_emoji] || "⏳" # ⏳
26
26
  cta_label = local_assigns[:cta_label] || 'Refresh'
27
27
  %>
28
- <div class="text-center py-6">
28
+ <%# role="alert" on the CARD, not on its message: the message is server-rendered
29
+ static text, so it never "changes" for a live region to notice. The modal host
30
+ renders content through <template x-if>, so this whole card ENTERS the DOM when
31
+ the failure happens — which is the event a screen reader can announce. %>
32
+ <div role="alert" class="text-center py-6">
29
33
  <div class="text-5xl mb-4 leading-none"><%= icon_emoji %></div>
30
34
  <p class="text-sm text-heading font-semibold mb-2"><%= title %></p>
31
35
 
@@ -131,7 +131,7 @@
131
131
  <% end %>
132
132
 
133
133
  <template x-if="error">
134
- <p class="text-red-400 text-sm mb-3" x-text="error"></p>
134
+ <p role="alert" class="text-red-400 text-sm mb-3" x-text="error"></p>
135
135
  </template>
136
136
 
137
137
  <form @submit.prevent="save()" class="space-y-4">
@@ -78,8 +78,22 @@
78
78
  tile_style = icon_bg.present? ? "background-color:#{icon_bg}" : nil
79
79
  tile_bg_class = icon_bg.present? ? nil : "bg-primary"
80
80
  %>
81
+ <%# on_click is marked html_safe because it is CODE, not content. content_tag
82
+ escapes through ERB::Util.unwrapped_html_escape, which no-ops on an html_safe
83
+ String, so the handler reaches the attribute exactly as the caller wrote it:
84
+ f('a'), not the entity-escaped form. A browser recovered from the escaped
85
+ version and Alpine fired either way, so this was invisible on screen; what it
86
+ broke was every consumer test that reads the markup.
87
+
88
+ IT IS STILL SAFE FOR DOUBLE QUOTES. content_tag quote-escapes an attribute
89
+ VALUE regardless of html_safe, so a double-quoted expression is entity-encoded
90
+ and still works. An earlier attempt emitted the handler through raw in literal
91
+ ERB instead, which had no such protection: it broke out of the attribute and
92
+ silently narrowed the contract to single quotes only.
93
+
94
+ Everything else here stays escaped, because everything else is content. %>
81
95
  <%= content_tag(soon ? :div : :button, type: (soon ? nil : "button"),
82
- "@click": on_click,
96
+ "@click": on_click&.html_safe,
83
97
  class: "w-full flex items-center gap-4 p-4 rounded-xl #{border_class} bg-inset#{soon ? '' : ' transition text-left'}",
84
98
  data: data_attrs.presence) do %>
85
99
  <span class="shrink-0 w-10 h-10 rounded-lg flex items-center justify-center <%= icon_class %> <%= tile_bg_class %>"
@@ -122,7 +122,7 @@
122
122
  class="input-field">
123
123
  </div>
124
124
  <template x-if="error">
125
- <p class="text-red-400 text-xs mb-3" x-text="error"></p>
125
+ <p role="alert" class="text-red-400 text-xs mb-3" x-text="error"></p>
126
126
  </template>
127
127
  <button type="submit" :disabled="submitting"
128
128
  class="btn btn-primary btn-lg w-full gap-2 disabled:cursor-wait">
@@ -28,7 +28,7 @@
28
28
  state (18+ in most states; 19+ in AL/NE; 21+ in IA/MA/VA).
29
29
  </span>
30
30
  </label>
31
- <p x-show="<%= error_model %>" x-cloak class="text-xs text-red-400 mt-1.5 ml-6">
31
+ <p role="alert" x-show="<%= error_model %>" x-cloak class="text-xs text-red-400 mt-1.5 ml-6">
32
32
  Please confirm you are of legal age to continue.
33
33
  </p>
34
34
  </div>
@@ -566,7 +566,7 @@
566
566
 
567
567
  <%# 1. AUTH suite %>
568
568
  <%# ===================================================================== %>
569
- <section class="space-y-5">
569
+ <section id="modals-auth" class="space-y-5">
570
570
  <div class="space-y-1">
571
571
  <h3 class="text-xl font-bold text-heading">Auth</h3>
572
572
  <p class="text-muted text-sm">
@@ -650,7 +650,7 @@
650
650
  owns: the person's name, their birthday, their picture. Grouping by
651
651
  subject rather than by moment also stops the guide claiming a chain it
652
652
  cannot show, now that the wallet step lives with the wallet cards. %>
653
- <section class="space-y-5">
653
+ <section id="modals-profile" class="space-y-5">
654
654
  <div class="space-y-1">
655
655
  <h3 class="text-xl font-bold text-heading">Profile</h3>
656
656
  <p class="text-muted text-sm">
@@ -838,7 +838,7 @@
838
838
  "Quest N of N" counter — the with/without fork is gone. The two "updated"
839
839
  cards open the same modal id straight at the celebrate state (props.celebrate),
840
840
  exactly like the Auth cards open a given step. %>
841
- <section class="space-y-5">
841
+ <section id="modals-profile-leveling" class="space-y-5">
842
842
  <div class="space-y-1">
843
843
  <div class="flex flex-wrap items-center gap-3">
844
844
  <h3 class="text-xl font-bold text-heading">Profile Leveling</h3>
@@ -1046,7 +1046,7 @@
1046
1046
  <%# 2. WEB3 — the wallet + on-chain modals, in the order a player meets them. %>
1047
1047
  <%# Gated by :web3; off = disabled-but-present-yet-openable. %>
1048
1048
  <%# ===================================================================== %>
1049
- <section class="space-y-5">
1049
+ <section id="modals-web3" class="space-y-5">
1050
1050
  <div class="space-y-1">
1051
1051
  <div class="flex flex-wrap items-center gap-3">
1052
1052
  <h3 class="text-xl font-bold text-heading">Web3</h3>
@@ -1447,7 +1447,7 @@
1447
1447
  <%# 3. CONTEST ENTRY & ELIGIBILITY — moved directly under Web3. The %>
1448
1448
  <%# walked entry flow (glow follows), plus the honest web2/web3 map. %>
1449
1449
  <%# ===================================================================== %>
1450
- <section class="space-y-5">
1450
+ <section id="modals-contest-entry" class="space-y-5">
1451
1451
  <div class="space-y-1">
1452
1452
  <div class="flex flex-wrap items-center gap-3">
1453
1453
  <h3 class="text-xl font-bold text-heading">Contest entry &amp; eligibility</h3>
@@ -1597,7 +1597,7 @@
1597
1597
  <%# ===================================================================== %>
1598
1598
  <%# 5. SYSTEM AND STATUS (reusable card blocks) %>
1599
1599
  <%# ===================================================================== %>
1600
- <section class="space-y-5">
1600
+ <section id="modals-system-status" class="space-y-5">
1601
1601
  <div class="space-y-1">
1602
1602
  <h3 class="text-xl font-bold text-heading">System &amp; status</h3>
1603
1603
  <p class="text-muted text-sm">
@@ -1713,7 +1713,7 @@
1713
1713
  <%# ===================================================================== %>
1714
1714
  <%# 6. TEMPLATES (copy-from archetypes) %>
1715
1715
  <%# ===================================================================== %>
1716
- <section class="space-y-5">
1716
+ <section id="modals-templates" class="space-y-5">
1717
1717
  <div class="space-y-1">
1718
1718
  <h3 class="text-xl font-bold text-heading">Templates</h3>
1719
1719
  <p class="text-muted text-sm">
@@ -1747,7 +1747,7 @@
1747
1747
  <%# ===================================================================== %>
1748
1748
  <%# 7. REWARDS / leveling (gated by Studio.feature?(:leveling)) %>
1749
1749
  <%# ===================================================================== %>
1750
- <section class="space-y-5">
1750
+ <section id="modals-rewards" class="space-y-5">
1751
1751
  <div class="space-y-1">
1752
1752
  <div class="flex flex-wrap items-center gap-3">
1753
1753
  <h3 class="text-xl font-bold text-heading">Rewards</h3>
@@ -164,7 +164,7 @@
164
164
  <span x-text="props.submitting === 'google' ? 'Waiting for Google…' : 'Google'"></span>
165
165
  </button>
166
166
  <template x-if="props.googleError">
167
- <p class="text-red-400 text-xs -mt-2 mb-3" x-text="props.googleError"></p>
167
+ <p role="alert" class="text-red-400 text-xs -mt-2 mb-3" x-text="props.googleError"></p>
168
168
  </template>
169
169
 
170
170
  <%# 2. Solana — progresses to the Connect Wallet picker modal. Gated on
@@ -200,7 +200,7 @@
200
200
  disabled_expr: "!!props.submitting" %>
201
201
  </div>
202
202
  <template x-if="props.formError">
203
- <p class="text-red-400 text-xs mb-3" x-text="props.formError"></p>
203
+ <p role="alert" class="text-red-400 text-xs mb-3" x-text="props.formError"></p>
204
204
  </template>
205
205
  <button type="submit" :disabled="!!props.submitting"
206
206
  class="btn btn-neutral btn-lg w-full gap-2 disabled:cursor-wait">
@@ -166,7 +166,7 @@
166
166
  </div>
167
167
 
168
168
  <template x-if="error">
169
- <p class="text-red-400 text-sm mt-3 text-center" x-text="error"></p>
169
+ <p role="alert" class="text-red-400 text-sm mt-3 text-center" x-text="error"></p>
170
170
  </template>
171
171
 
172
172
  <button type="button" @click="back()" :disabled="connecting"
@@ -1,3 +1,3 @@
1
1
  module Studio
2
- VERSION = "0.62.0"
2
+ VERSION = "0.62.2"
3
3
  end
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.62.0
4
+ version: 0.62.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-26 00:00:00.000000000 Z
11
+ date: 2026-08-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails