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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +145 -0
- data/app/assets/javascripts/studio/canvas_confetti.js +17 -0
- data/app/assets/javascripts/studio/studio_confetti.js +93 -0
- data/app/assets/tailwind/studio_engine/engine-motion.css +47 -0
- data/app/views/layouts/studio/_head.html.erb +7 -1
- data/app/views/studio/_age_verify_assets.html.erb +120 -0
- data/app/views/studio/_leveling_activity_assets.html.erb +174 -0
- data/app/views/studio/modals/blocks/_age_verify.html.erb +106 -0
- data/app/views/studio/modals/blocks/_change_username.html.erb +46 -0
- data/app/views/studio/modals/blocks/_leveling_activity.html.erb +159 -0
- data/app/views/studio/modals/blocks/_wallet_brand_sprite.html.erb +15 -26
- data/app/views/style/_modals.html.erb +231 -2
- data/app/views/style/_tricks.html.erb +46 -0
- data/app/views/style/modals/_age_verify.html.erb +17 -0
- data/app/views/style/modals/_entry_tokens.html.erb +123 -0
- data/lib/studio/engine.rb +2 -0
- data/lib/studio/version.rb +1 -1
- metadata +10 -1
|
@@ -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 & 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
|
|
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
|
|
19
|
-
a <
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
27
|
-
|
|
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 —
|
|
34
|
-
<symbol id="se-wallet-solflare" viewBox="0 0
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
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>
|