studio-engine 0.20.0 → 0.22.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.
@@ -0,0 +1,174 @@
1
+ <%#
2
+ levelingActionModal factory — the Alpine data behind the engine leveling-activity
3
+ modals (studio/modals/blocks/_leveling_activity and its _change_username
4
+ specialization). Ships here, at PAGE level, on purpose: the modal partial mounts
5
+ inside a modal host's single-root template, and a script cloned out of a template
6
+ never runs. So a consumer renders THIS once in its layout (like the other shared
7
+ Alpine factories), and the modal partial anywhere below just references
8
+ window.levelingActionModal. Mirrors how studio/_age_verify_assets homes the
9
+ ageVerifyModal factory.
10
+
11
+ CRITICAL BOUNDARY — this factory is UI ONLY. It POSTs to an app-supplied
12
+ submitUrl and reacts to a NEUTRAL JSON contract; it holds ZERO on-chain logic,
13
+ no wallet taxonomy, no signing. Any second step some apps need (e.g. a wallet
14
+ approval) is delegated OPAQUELY: the engine hands the app-returned "challenge"
15
+ blob to an app-supplied window[finalizeHook] and POSTs back whatever opaque
16
+ "proof" the app returns. The engine never inspects, parses, or decodes either
17
+ blob, so the whole wallet dance stays in the app.
18
+
19
+ Wire contract (submitUrl + finalizeUrl responses are JSON):
20
+ POST submitUrl { value } -> one of:
21
+ { status: "saved", seeds_earned?, seeds_total? } done
22
+ { status: "needs_step", challenge, token } app step required
23
+ { status: "error", message } show message
24
+ when needs_step and a finalizeHook is wired:
25
+ proof = await window[finalizeHook](challenge, { token, onProgress })
26
+ POST finalizeUrl { token, proof } -> { status: "saved" | "error", ... }
27
+
28
+ opts: { hasInput, initialValue, minLength, submitUrl, finalizeUrl, finalizeHook,
29
+ savedEvent, store, leveling, demo, seedsPerLevel }.
30
+ hasInput true renders + validates a single-line input; false is a
31
+ no-field activity (a plain "Complete" action).
32
+ initialValue seed value for the input (a plain string; never a model).
33
+ minLength client-only min length for the input (0 = none).
34
+ submitUrl app endpoint the action POSTs to (the app owns persistence).
35
+ finalizeUrl app endpoint for the opaque second step (omit if none).
36
+ finalizeHook name of a window async fn the app supplies for the step (omit
37
+ if none); it owns EVERY app-side detail of that step.
38
+ savedEvent DOM event dispatched on success, carrying the app payload, so
39
+ the app can run its own follow-on. Default "studio:activity-saved".
40
+ store Alpine modal store name backing close(). Default "modals".
41
+ leveling true shows the seeds celebration on success (a UI flourish).
42
+ demo style-guide preview: resolve locally, skip the network POST.
43
+ seedsPerLevel seeds per level for the celebration bar. Default 100.
44
+ %>
45
+ <script>
46
+ (function () {
47
+ if (window.levelingActionModal) return;
48
+ window.levelingActionModal = function (opts) {
49
+ opts = opts || {};
50
+ return {
51
+ hasInput: !!opts.hasInput,
52
+ value: opts.initialValue || "",
53
+ original: opts.initialValue || "",
54
+ minLength: parseInt(opts.minLength, 10) || 0,
55
+ submitUrl: opts.submitUrl || "",
56
+ finalizeUrl: opts.finalizeUrl || "",
57
+ finalizeHook: opts.finalizeHook || "",
58
+ savedEvent: opts.savedEvent || "studio:activity-saved",
59
+ store: opts.store || "modals",
60
+ leveling: !!opts.leveling,
61
+ demo: !!opts.demo,
62
+ seedsPerLevel: parseInt(opts.seedsPerLevel, 10) || 100,
63
+ saving: false,
64
+ error: "",
65
+ saved: false,
66
+ celebrate: false,
67
+ progressLabel: "",
68
+ seedsEarned: 0,
69
+ seedsTotal: 0,
70
+
71
+ // Actionable while there is a real change to save (input activities) or
72
+ // until the one-shot action has fired (no-field activities).
73
+ get changed() {
74
+ if (!this.hasInput) return !this.saved;
75
+ var v = (this.value || "").trim();
76
+ if (v.length < this.minLength) return false;
77
+ return v !== (this.original || "").trim();
78
+ },
79
+
80
+ _dispatch: function (payload) {
81
+ try {
82
+ window.dispatchEvent(new CustomEvent(this.savedEvent, { detail: payload || {} }));
83
+ } catch (e) {}
84
+ },
85
+
86
+ _finishSaved: function (payload) {
87
+ payload = payload || {};
88
+ this.saving = false;
89
+ this.saved = true;
90
+ this.progressLabel = "";
91
+ if (this.hasInput) this.original = (this.value || "").trim();
92
+ this._dispatch(payload);
93
+ if (this.leveling) {
94
+ this.seedsEarned = parseInt(payload.seeds_earned, 10) || 0;
95
+ this.seedsTotal = parseInt(payload.seeds_total, 10) || 0;
96
+ this.celebrate = true;
97
+ }
98
+ },
99
+
100
+ _post: function (url, body) {
101
+ var csrf = (document.querySelector('meta[name="csrf-token"]') || {}).content || "";
102
+ var fetcher = window.authedFetch || window.fetch;
103
+ return fetcher(url, {
104
+ method: "POST",
105
+ headers: { "Content-Type": "application/json", "X-CSRF-Token": csrf, "Accept": "application/json" },
106
+ body: JSON.stringify(body || {})
107
+ }).then(function (r) {
108
+ return r.json().then(function (j) { return { ok: r.ok, body: j || {} }; });
109
+ });
110
+ },
111
+
112
+ save: function () {
113
+ if (this.saving || !this.changed) return;
114
+ this.saving = true;
115
+ this.error = "";
116
+ var self = this;
117
+ if (this.demo) {
118
+ // Style-guide preview: no backend. Resolve to a level-crossing payload
119
+ // so the leveling celebration demonstrates its full flourish.
120
+ setTimeout(function () {
121
+ self._finishSaved({ seeds_earned: 30, seeds_total: self.seedsPerLevel });
122
+ }, 600);
123
+ return;
124
+ }
125
+ var body = this.hasInput ? { value: (this.value || "").trim() } : {};
126
+ this._post(this.submitUrl, body)
127
+ .then(function (res) { return self._handle(res); })
128
+ .catch(function (e) {
129
+ self.error = (e && e.message) || "Something went wrong. Please try again.";
130
+ self.saving = false;
131
+ });
132
+ },
133
+
134
+ _handle: function (res) {
135
+ var body = (res && res.body) || {};
136
+ var status = body.status || (res && res.ok ? "saved" : "error");
137
+ if (status === "saved") { this._finishSaved(body); return; }
138
+ if (status === "needs_step") { return this._step(body); }
139
+ this.error = body.message || "Something went wrong. Please try again.";
140
+ this.saving = false;
141
+ },
142
+
143
+ // The opaque second step. The engine hands the app's "challenge" blob to
144
+ // the app-supplied hook and POSTs back the app's "proof" blob — it inspects
145
+ // neither, so every app-side detail of the step stays in the app.
146
+ _step: function (body) {
147
+ var hook = this.finalizeHook && window[this.finalizeHook];
148
+ var self = this;
149
+ if (typeof hook !== "function") {
150
+ this.error = "This step isn't available.";
151
+ this.saving = false;
152
+ return;
153
+ }
154
+ return Promise.resolve(
155
+ hook(body.challenge, {
156
+ token: body.token,
157
+ onProgress: function (label) { self.progressLabel = label || ""; }
158
+ })
159
+ ).then(function (proof) {
160
+ return self._post(self.finalizeUrl, { token: body.token, proof: proof });
161
+ }).then(function (res) {
162
+ var rb = (res && res.body) || {};
163
+ var status = rb.status || (res && res.ok ? "saved" : "error");
164
+ if (status === "saved") { self._finishSaved(rb); }
165
+ else { self.error = rb.message || "Couldn't complete the step."; self.saving = false; }
166
+ }).catch(function (e) {
167
+ self.error = (e && e.message) || "Couldn't complete the step.";
168
+ self.saving = false;
169
+ });
170
+ }
171
+ };
172
+ };
173
+ })();
174
+ </script>
@@ -0,0 +1,106 @@
1
+ <%#
2
+ Age-gate DOB modal — engine primitive. Collects a real date of birth
3
+ (Month / Day / Year), shows a live "too young" hint, and submits. Homed in the
4
+ engine as the heavier sibling of studio/modals/shared/_age_attestation (the
5
+ lighter one-checkbox legal attestation). RUN ONE OR THE OTHER, not both: the
6
+ attestation is a signup-time "I confirm I'm of legal age" checkbox; this is an
7
+ entry-time DOB gate that a server can recompute and stamp.
8
+
9
+ CRITICAL — the engine hardcodes NO legal policy. It renders the modal UI, the
10
+ DOB fields, and the submit; everything legal is app-supplied:
11
+
12
+ min_age (Integer, REQUIRED) — the app's minimum age. There is NO engine
13
+ default (18 is itself a policy value); the app resolves it from
14
+ its own policy against its own server-detected jurisdiction.
15
+ submit_url (String, REQUIRED) — the app endpoint the DOB POSTs to. The
16
+ app owns the authoritative recompute + DOB persistence; the modal
17
+ only shows the response.
18
+ state (String, optional) — a PASSIVE jurisdiction label for the copy
19
+ (e.g. "CA"). Server-detected upstream; the modal never detects
20
+ geography and never offers an editable state field (a spoofable
21
+ client state must not be able to lower the bar). Blank => the
22
+ jurisdiction clause is dropped.
23
+ title (String, optional) — modal title. Default "Verify your age".
24
+ intro (String, optional) — the lead sentence after the age line.
25
+ fine_print (String, optional) — the app's legal / jurisdiction copy (e.g.
26
+ its per-state age table). The engine ships only a neutral,
27
+ policy-free default; the app passes its real copy here.
28
+ modal_store (String, optional) — Alpine store backing close(). Default "modals".
29
+ demo (Boolean, optional) — style-guide preview: resolve locally, no POST.
30
+
31
+ Requires window.ageVerifyModal — render studio/_age_verify_assets once at
32
+ layout level (the factory can't ship inside this template; a cloned script
33
+ never runs). Single root element (modal-host template mount rule).
34
+ %>
35
+ <%
36
+ min_age = local_assigns.fetch(:min_age)
37
+ submit_url = local_assigns.fetch(:submit_url)
38
+ state = local_assigns[:state].to_s.strip
39
+ title = local_assigns.fetch(:title, "Verify your age")
40
+ intro = local_assigns.fetch(:intro, "Enter your date of birth to confirm you're eligible — we only ask once.")
41
+ fine_print = local_assigns.fetch(:fine_print, "We use your date of birth only to confirm eligibility.")
42
+ modal_store = local_assigns.fetch(:modal_store, "modals")
43
+ demo = local_assigns.fetch(:demo, false)
44
+ %>
45
+ <div x-data="ageVerifyModal({ minAge: <%= min_age.to_i %>, state: '<%= j state %>', url: '<%= j submit_url %>', store: '<%= j modal_store %>', demo: <%= demo ? 'true' : 'false' %> })">
46
+ <%= render layout: "studio/modals/blocks/shell", locals: { title: title, modal_store: modal_store } do %>
47
+ <p class="text-sm text-muted mb-4">
48
+ You must be <span class="text-heading font-semibold"><%= min_age %>+</span><%= state.present? ? " in #{state}" : "" %>.
49
+ <%= intro %>
50
+ </p>
51
+
52
+ <%# Month / Day / Year selectors %>
53
+ <div class="grid grid-cols-3 gap-2 mb-4">
54
+ <div>
55
+ <label class="label-upper block mb-1">Month</label>
56
+ <select x-model="month" @change="onMonthChange()" class="input-field !px-3 !py-2.5 text-sm appearance-none cursor-pointer">
57
+ <option value="" disabled>Month</option>
58
+ <template x-for="m in months" :key="m.n">
59
+ <option :value="m.n" x-text="m.label"></option>
60
+ </template>
61
+ </select>
62
+ </div>
63
+ <div>
64
+ <label class="label-upper block mb-1">Day</label>
65
+ <select x-model="day" @change="error = ''" class="input-field !px-3 !py-2.5 text-sm appearance-none cursor-pointer">
66
+ <option value="" disabled>Day</option>
67
+ <template x-for="d in dayOptions" :key="d">
68
+ <option :value="d" x-text="d"></option>
69
+ </template>
70
+ </select>
71
+ </div>
72
+ <div>
73
+ <label class="label-upper block mb-1">Year</label>
74
+ <select x-model="year" @change="onMonthChange()" class="input-field !px-3 !py-2.5 text-sm appearance-none cursor-pointer">
75
+ <option value="" disabled>Year</option>
76
+ <template x-for="y in years" :key="y">
77
+ <option :value="y" x-text="y"></option>
78
+ </template>
79
+ </select>
80
+ </div>
81
+ </div>
82
+
83
+ <%# Live too-young hint — shown once a full date is picked and it fails the
84
+ app-supplied bar, so the user sees the problem before submitting. %>
85
+ <template x-if="complete && computedAge !== null && minAge && computedAge < minAge">
86
+ <p class="text-red-400 text-sm mb-3" x-text="'You must be ' + minAge + '+' + (stateCode ? ' in ' + stateCode : '') + '.'"></p>
87
+ </template>
88
+
89
+ <%# Server error (invalid date, or the authoritative too-young check). %>
90
+ <template x-if="error">
91
+ <p class="text-red-400 text-sm mb-3" x-text="error"></p>
92
+ </template>
93
+
94
+ <button type="button" @click="submit()"
95
+ class="btn btn-primary btn-lg w-full"
96
+ :disabled="!complete || submitting || (computedAge !== null && minAge && computedAge < minAge)">
97
+ <span x-show="!submitting">Confirm &amp; Continue</span>
98
+ <span x-show="submitting" class="inline-flex items-center justify-center gap-2" style="display: none;">
99
+ <span class="cta-spinner" aria-hidden="true"></span>
100
+ Verifying…
101
+ </span>
102
+ </button>
103
+
104
+ <p class="text-[11px] text-muted text-center mt-3 leading-snug"><%= fine_print %></p>
105
+ <% end %>
106
+ </div>
@@ -0,0 +1,46 @@
1
+ <%#
2
+ Change-username modal — a named engine primitive, and a thin specialization of
3
+ studio/modals/blocks/_leveling_activity with username defaults. TWO modes via
4
+ the :leveling flag (quest pill + seeds celebration when on; plain input + Save +
5
+ Saved when off).
6
+
7
+ CRITICAL BOUNDARY — UI ONLY. The engine renders the input, the Save button, the
8
+ saved state, and the optional leveling chrome. The actual username SAVE is an
9
+ APP-SUPPLIED callback: the modal POSTs to submit_url and, if the app returns a
10
+ second step, delegates it opaquely to an app-supplied finalize_hook (the app's
11
+ managed-wallet write). No wallet, signing, keys, or on-chain writes live here.
12
+
13
+ Locals (beyond _leveling_activity's):
14
+ current_username (String, REQUIRED) — seed value for the input (a plain
15
+ string; never current_user — no model coupling in the engine).
16
+ submit_url (String, REQUIRED) — app endpoint the username POSTs to.
17
+ description (String) — app-authored lead copy. Pass the wallet-aware line
18
+ here (e.g. "Your username is stored on-chain. Changes are
19
+ saved via your managed wallet") — the engine default stays
20
+ neutral so no on-chain assumption is baked in.
21
+
22
+ Everything else (finalize_url, finalize_hook, saved_event, quest_label,
23
+ leveling, modal_store, demo, celebration copy) passes straight through.
24
+ %>
25
+ <%
26
+ current_username = local_assigns.fetch(:current_username)
27
+
28
+ username_locals = local_assigns.except(:current_username).reverse_merge(
29
+ title: "Change Username",
30
+ description: "Pick a name that's right for you.",
31
+ input: true,
32
+ initial_value: current_username.to_s,
33
+ min_length: 3,
34
+ max_length: 30,
35
+ pattern: "[a-zA-Z0-9_-]+",
36
+ pattern_title: "3-30 characters — letters, numbers, hyphens, and underscores",
37
+ placeholder: "username",
38
+ cta_label: "Save",
39
+ saved_label: "Saved",
40
+ saved_event: "studio:username-saved",
41
+ icon_emoji: "🌱",
42
+ celebrate_title: "Great Username",
43
+ celebrate_subtitle: "Nice — you earned seeds for updating your profile. Keep completing quests to earn a Free Entry."
44
+ )
45
+ %>
46
+ <%= render "studio/modals/blocks/leveling_activity", username_locals %>
@@ -0,0 +1,159 @@
1
+ <%#
2
+ Leveling-activity modal — the engine primitive behind TM's quest/leveling
3
+ activities (change username, join newsletter, and the rest). ONE primitive,
4
+ TWO modes, decided by the app's :leveling capability:
5
+
6
+ leveling ON — the full quest framing: a "Quest N of N" pill, and on success
7
+ the seeds celebration (progress bar + level-up + Free Entry).
8
+ leveling OFF — the plain action modal: just the action + Save, and a Saved
9
+ state on success. No quest pill, no seeds. (This is the
10
+ McRitchie-Studio shape: MS has no leveling, TM does.)
11
+
12
+ Same primitive; the flag decides. The leveling chrome (quest pill, seeds bar) is
13
+ engine-owned UI — it is NOT on-chain.
14
+
15
+ CRITICAL BOUNDARY — UI ONLY. There is NO wallet, signing, key, or on-chain write
16
+ anywhere in this primitive. The actual save is an APP-SUPPLIED callback: the
17
+ modal POSTs to submit_url and reacts to a neutral JSON contract; any wallet step
18
+ is delegated opaquely to an app-supplied finalize_hook (see the factory,
19
+ studio/_leveling_activity_assets). TM wires the managed-wallet write; MS wires a
20
+ trivial endpoint. Requires window.levelingActionModal — render
21
+ studio/leveling_activity_assets once at layout level.
22
+
23
+ Locals:
24
+ submit_url (String, REQUIRED) — app endpoint the action POSTs to.
25
+ title (String) — modal title. Default "Complete activity".
26
+ description (String) — the app-authored lead copy. App-owned so wallet-aware
27
+ prose ("saved via your managed wallet") lives app-side, never here.
28
+ input (Boolean) — render a single-line input. Default false.
29
+ input_type (String) — the input type (e.g. "text", "email"). Default "text".
30
+ initial_value (String) — seed value for the input (a plain string).
31
+ placeholder / min_length / max_length / pattern / pattern_title — input attrs.
32
+ cta_label (String) — action button label. Default "Complete".
33
+ saved_label (String) — label once saved. Default "Done".
34
+ saving_label (String) — label while saving. Default "Saving…".
35
+ finalize_url (String) — endpoint for the opaque second step (omit if none).
36
+ finalize_hook (String) — window async fn name for the step (omit if none).
37
+ saved_event (String) — DOM event dispatched on success. Default
38
+ "studio:activity-saved".
39
+ quest_label (String) — the "Quest N of N" pill text (leveling only).
40
+ icon_emoji (String) — celebration glyph. Default "🌱".
41
+ celebrate_title (String) — celebration headline. Default "Activity complete".
42
+ celebrate_subtitle (String) — celebration subtext.
43
+ free_entry_label (String) — level-up reward line. Default "Free Entry Earned 🎟️".
44
+ seeds_per_level (Integer) — seeds per level. Default 100.
45
+ leveling (Boolean) — override the :leveling gate. Defaults to
46
+ Studio.feature?(:leveling); the style guide passes both to show
47
+ each mode.
48
+ modal_store (String) — Alpine store name backing close(). Default "modals".
49
+ demo (Boolean) — style-guide preview: resolve locally, no POST.
50
+
51
+ Single root element (the modal host mounts this through its own template x-if).
52
+ %>
53
+ <%
54
+ submit_url = local_assigns.fetch(:submit_url)
55
+ title = local_assigns.fetch(:title, "Complete activity")
56
+ description = local_assigns.fetch(:description, nil)
57
+ input = local_assigns.fetch(:input, false)
58
+ input_type = local_assigns.fetch(:input_type, "text")
59
+ initial_value = local_assigns.fetch(:initial_value, "")
60
+ placeholder = local_assigns.fetch(:placeholder, "")
61
+ min_length = local_assigns.fetch(:min_length, 0)
62
+ max_length = local_assigns.fetch(:max_length, nil)
63
+ pattern = local_assigns.fetch(:pattern, nil)
64
+ pattern_title = local_assigns.fetch(:pattern_title, nil)
65
+ cta_label = local_assigns.fetch(:cta_label, "Complete")
66
+ saved_label = local_assigns.fetch(:saved_label, "Done")
67
+ saving_label = local_assigns.fetch(:saving_label, "Saving…")
68
+ finalize_url = local_assigns.fetch(:finalize_url, "")
69
+ finalize_hook = local_assigns.fetch(:finalize_hook, "")
70
+ saved_event = local_assigns.fetch(:saved_event, "studio:activity-saved")
71
+ quest_label = local_assigns.fetch(:quest_label, nil)
72
+ icon_emoji = local_assigns.fetch(:icon_emoji, "🌱")
73
+ celebrate_title = local_assigns.fetch(:celebrate_title, "Activity complete")
74
+ celebrate_subtitle = local_assigns.fetch(:celebrate_subtitle, "Nice — you earned seeds. Keep completing quests to earn a Free Entry.")
75
+ free_entry_label = local_assigns.fetch(:free_entry_label, "Free Entry Earned 🎟️")
76
+ seeds_per_level = local_assigns.fetch(:seeds_per_level, 100)
77
+ leveling = local_assigns.fetch(:leveling, Studio.feature?(:leveling))
78
+ modal_store = local_assigns.fetch(:modal_store, "modals")
79
+ demo = local_assigns.fetch(:demo, false)
80
+ %>
81
+ <div x-data="levelingActionModal({
82
+ hasInput: <%= input ? 'true' : 'false' %>,
83
+ initialValue: '<%= j initial_value.to_s %>',
84
+ minLength: <%= min_length.to_i %>,
85
+ submitUrl: '<%= j submit_url %>',
86
+ finalizeUrl: '<%= j finalize_url %>',
87
+ finalizeHook: '<%= j finalize_hook %>',
88
+ savedEvent: '<%= j saved_event %>',
89
+ store: '<%= j modal_store %>',
90
+ leveling: <%= leveling ? 'true' : 'false' %>,
91
+ demo: <%= demo ? 'true' : 'false' %>,
92
+ seedsPerLevel: <%= seeds_per_level.to_i %>
93
+ })">
94
+
95
+ <%# FORM VIEW — the action. Single root inside the template. %>
96
+ <template x-if="!celebrate">
97
+ <div>
98
+ <%= render layout: "studio/modals/blocks/shell", locals: { title: title, modal_store: modal_store } do %>
99
+ <% if leveling && quest_label.present? %>
100
+ <div class="mb-3">
101
+ <span class="inline-block text-[10px] font-semibold uppercase tracking-wide text-primary bg-primary/10 px-2 py-0.5 rounded-full whitespace-nowrap"><%= quest_label %></span>
102
+ </div>
103
+ <% end %>
104
+
105
+ <% if description.present? %>
106
+ <p class="text-sm text-muted mb-4"><%= description %></p>
107
+ <% end %>
108
+
109
+ <template x-if="error">
110
+ <p class="text-red-400 text-sm mb-3" x-text="error"></p>
111
+ </template>
112
+
113
+ <form @submit.prevent="save()" class="space-y-4">
114
+ <% if input %>
115
+ <input type="<%= input_type %>" x-model="value" class="input-field w-full"
116
+ placeholder="<%= placeholder %>"
117
+ <%= "minlength=\"#{min_length}\"".html_safe if min_length.to_i.positive? %>
118
+ <%= "maxlength=\"#{max_length}\"".html_safe if max_length.present? %>
119
+ <%= "pattern=\"#{pattern}\"".html_safe if pattern.present? %>
120
+ <%= "title=\"#{pattern_title}\"".html_safe if pattern_title.present? %>
121
+ autofocus>
122
+ <% end %>
123
+ <button type="submit" class="btn btn-primary btn-lg w-full" :disabled="saving || !changed">
124
+ <span x-show="!saving" x-text="changed ? '<%= j cta_label %>' : '<%= j saved_label %>'"><%= cta_label %></span>
125
+ <span x-show="saving" class="inline-flex items-center justify-center gap-2" style="display: none;">
126
+ <span class="cta-spinner" aria-hidden="true"></span>
127
+ <span x-text="progressLabel || '<%= j saving_label %>'"><%= saving_label %></span>
128
+ </span>
129
+ </button>
130
+ </form>
131
+ <% end %>
132
+ </div>
133
+ </template>
134
+
135
+ <%# CELEBRATION VIEW — leveling ONLY. Rendered at ERB time only when leveling is
136
+ on, so a leveling-off modal ships NO seeds chrome at all (not merely
137
+ unmounted). Mounts once celebrate flips true so the seeds bar animates from
138
+ the freshly-saved payload. Single root. %>
139
+ <% if leveling %>
140
+ <template x-if="celebrate">
141
+ <div>
142
+ <%= render "studio/modals/blocks/card_header",
143
+ size: :lg, icon_emoji: icon_emoji,
144
+ title: celebrate_title, subtitle: celebrate_subtitle %>
145
+
146
+ <div class="mb-5">
147
+ <%= render "studio/modals/blocks/seeds_bar",
148
+ seeds_earned_key: "seedsEarned", seeds_total_key: "seedsTotal",
149
+ seeds_per_level: seeds_per_level, free_entry_label: free_entry_label %>
150
+ </div>
151
+
152
+ <button type="button" @click="$store.<%= modal_store %>.close()"
153
+ class="block mx-auto mt-3 text-sm text-secondary hover:text-heading transition">
154
+ Close
155
+ </button>
156
+ </div>
157
+ </template>
158
+ <% end %>
159
+ </div>
@@ -1,5 +1,5 @@
1
1
  <%#
2
- Wallet brand-icon sprite — inline SVG marks for the wallets the Connect-Wallet
2
+ Wallet brand-icon sprite — inline brand marks for the wallets the Connect-Wallet
3
3
  picker knows about (Phantom, Solflare, Backpack), OWNED BY THE ENGINE so every
4
4
  consumer inherits crisp brand icons with ZERO per-app asset files. Before this,
5
5
  the picker referenced app-served PNGs (/wallet-phantom.png, ...), so a fresh app
@@ -15,37 +15,26 @@
15
15
  the name and returns it only for a known brand, else null → letter fallback).
16
16
 
17
17
  The sprite is visually hidden (zero-box, absolute) and aria-hidden; only the
18
- <use> references paint. Each <symbol> carries its own viewBox + brand fills, so
19
- a <use> scales into whatever box the outer <svg> sizes. Marks are geometric
20
- interpretations of the official brand icons (Phantom purple ghost, Solflare
21
- yellow S, Backpack red pack); the letter tile stays the fallback for any wallet
22
- not in this set.
18
+ <use> references paint. Each <symbol> carries the OFFICIAL brand asset embedded
19
+ inline as a base64 PNG data-URI <image> byte-identical to the real logos each
20
+ wallet ships (matching Turf Monster's public/wallet-*.png), so no hand-drawn
21
+ approximation drifts from the brand. The letter tile stays the fallback for any
22
+ wallet not in this set.
23
23
  %>
24
24
  <svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false"
25
25
  style="position:absolute;width:0;height:0;overflow:hidden" data-wallet-brand-sprite>
26
- <%# Phantom — light-purple tile, cream ghost with two purple eye holes (the
27
- official ghost path; fill-rule evenodd cuts the eyes so the tile shows). %>
28
- <symbol id="se-wallet-phantom" viewBox="0 0 128 128">
29
- <rect width="128" height="128" rx="28" fill="#AB9FF2"/>
30
- <path fill="#FFFDF8" fill-rule="evenodd" d="M110.584 64.9142H99.142C99.142 41.7651 80.173 23 56.7724 23C33.6612 23 14.8716 41.3057 14.4118 64.0583C13.936 87.577 36.2456 108 60.0197 108H63.0263C83.9754 108 112.069 91.6259 116.459 71.7107C117.27 68.033 114.351 64.9142 110.584 64.9142ZM39.7689 65.9482C39.7689 69.0453 37.2384 71.5758 34.1413 71.5758C31.0442 71.5758 28.5137 69.0453 28.5137 65.9482V56.8554C28.5137 53.7583 31.0442 51.2278 34.1413 51.2278C37.2384 51.2278 39.7689 53.7583 39.7689 56.8554V65.9482ZM59.3312 65.9482C59.3312 69.0453 56.8007 71.5758 53.7036 71.5758C50.6065 71.5758 48.076 69.0453 48.076 65.9482V56.8554C48.076 53.7583 50.6065 51.2278 53.7036 51.2278C56.8007 51.2278 59.3312 53.7583 59.3312 56.8554V65.9482Z"/>
26
+ <%# Phantom — official brand mark (light-purple tile, cream ghost). %>
27
+ <symbol id="se-wallet-phantom" viewBox="0 0 460 460">
28
+ <image width="460" height="460" href="data:image/png;base64,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"/>
31
29
  </symbol>
32
30
 
33
- <%# Solflare — bright-yellow tile, bold black S crossed by a diagonal slash. %>
34
- <symbol id="se-wallet-solflare" viewBox="0 0 100 100">
35
- <rect width="100" height="100" rx="24" fill="#FFEC1F"/>
36
- <path d="M67 37c-3-7-11-11-20-11-10 0-17 5-17 13 0 7 5 11 16 14l8 2c9 2 13 5 13 11 0 8-8 13-18 13-9 0-16-3-19-10"
37
- fill="none" stroke="#131313" stroke-width="12" stroke-linecap="round" stroke-linejoin="round"/>
38
- <path d="M27 76 73 24" fill="none" stroke="#131313" stroke-width="8" stroke-linecap="round"/>
31
+ <%# Solflare — official brand mark (yellow tile, black blackletter S). %>
32
+ <symbol id="se-wallet-solflare" viewBox="0 0 361 361">
33
+ <image width="361" height="361" href="data:image/png;base64,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"/>
39
34
  </symbol>
40
35
 
41
- <%# Backpack — red tile, white pack: handle arch, rounded body with a red buckle
42
- ring, and a rounded lower pocket band. %>
43
- <symbol id="se-wallet-backpack" viewBox="0 0 100 100">
44
- <rect width="100" height="100" rx="24" fill="#E33E3F"/>
45
- <path d="M39 32v-3a11 11 0 0 1 22 0v3" fill="none" stroke="#fff" stroke-width="6" stroke-linecap="round"/>
46
- <rect x="23" y="31" width="54" height="31" rx="15" fill="#fff"/>
47
- <circle cx="50" cy="46" r="8" fill="#E33E3F"/>
48
- <circle cx="50" cy="46" r="3.5" fill="#fff"/>
49
- <rect x="23" y="66" width="54" height="13" rx="6.5" fill="#fff"/>
36
+ <%# Backpack — official brand mark (red tile, white pack). %>
37
+ <symbol id="se-wallet-backpack" viewBox="0 0 128 128">
38
+ <image width="128" height="128" href="data:image/png;base64,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"/>
50
39
  </symbol>
51
40
  </svg>