studio-engine 0.17.0 → 0.18.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 +67 -0
- data/app/assets/tailwind/studio_engine/engine-motion.css +620 -5
- data/app/controllers/style_controller.rb +25 -0
- data/app/views/studio/modals/_crop_photo.html.erb +5 -1
- data/app/views/studio/modals/_host.html.erb +5 -0
- data/app/views/studio/modals/_image_upload.html.erb +16 -5
- data/app/views/studio/modals/_saving.html.erb +6 -2
- data/app/views/studio/modals/auth/_resend_footer.html.erb +25 -0
- data/app/views/studio/modals/blocks/_card_header.html.erb +83 -0
- data/app/views/studio/modals/blocks/_cta_redirect.html.erb +73 -0
- data/app/views/studio/modals/blocks/_onchain_success.html.erb +52 -0
- data/app/views/studio/modals/blocks/_progress_pill.html.erb +23 -0
- data/app/views/studio/modals/blocks/_shell.html.erb +35 -0
- data/app/views/studio/modals/blocks/_solana_tx_link.html.erb +42 -0
- data/app/views/studio/modals/shared/_age_attestation.html.erb +34 -0
- data/app/views/studio/modals/shared/_email_field.html.erb +70 -0
- data/app/views/studio/modals/templates/_action.html.erb +25 -0
- data/app/views/studio/modals/templates/_form.html.erb +25 -0
- data/app/views/studio/modals/templates/_status.html.erb +45 -0
- data/app/views/studio/modals/templates/_success.html.erb +23 -0
- data/app/views/studio/modals/templates/_wizard.html.erb +75 -0
- data/app/views/style/_modal_specimen.html.erb +120 -0
- data/app/views/style/_modals.html.erb +690 -0
- data/app/views/style/_specimen.html.erb +64 -0
- data/app/views/style/_tasks.html.erb +112 -0
- data/app/views/style/_theme.html.erb +222 -0
- data/app/views/style/_tricks.html.erb +323 -0
- data/app/views/style/index.html.erb +60 -0
- data/app/views/style/modals/_auth.html.erb +252 -0
- data/app/views/style/modals/_onchain_tx.html.erb +62 -0
- data/app/views/style/modals/_wallet_connect.html.erb +138 -0
- data/app/views/style/modals/_wallet_deposit.html.erb +56 -0
- data/lib/studio/version.rb +1 -1
- data/lib/studio.rb +27 -1
- metadata +32 -6
- data/app/controllers/design_system_controller.rb +0 -12
- data/app/views/design_system/_specimen.html.erb +0 -27
- data/app/views/design_system/index.html.erb +0 -232
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
<%# Modals section of the living style guide (admin/style).
|
|
2
|
+
|
|
3
|
+
The REAL Turf Monster modal system, ported into the engine and made
|
|
4
|
+
live-openable. A page-scoped host (dsModals) mirrors the shared host's full
|
|
5
|
+
API — open / swap / advance / close / current + the enter-animation
|
|
6
|
+
registry — so every specimen opens the genuine full-size modal built from
|
|
7
|
+
the engine partials, without colliding with the app-level shared host that
|
|
8
|
+
application.html.erb already mounts on $store.modals.
|
|
9
|
+
|
|
10
|
+
Why page-scoped (dsModals) and not $store.modals: both MS and TM render the
|
|
11
|
+
shared studio/modals/host once at layout level with an unconditional x-if on
|
|
12
|
+
$store.modals.current(), registering ONLY that app's own modal ids. Pushing
|
|
13
|
+
a novel specimen id onto $store.modals would make that host paint an empty
|
|
14
|
+
card behind ours. So this section owns a tiny, self-contained store + overlay
|
|
15
|
+
and registers all specimen content itself.
|
|
16
|
+
|
|
17
|
+
Groups: Auth (the sign-in step machine), Profile (crop / upload, opened
|
|
18
|
+
through studio/cropper_assets), Web3 (wallet-connect / on-chain-tx / deposit,
|
|
19
|
+
gated by Studio.feature?(:web3) — disabled-but-present-yet-openable when off),
|
|
20
|
+
System and status (the reusable card blocks), Templates (the copy-from
|
|
21
|
+
archetypes), and Rewards (level-up, gated by :leveling).
|
|
22
|
+
|
|
23
|
+
The modal motion (modal-card-* / modal-backdrop-* keyframes + studio-modal-
|
|
24
|
+
drain + body.modal-open) ships in engine-motion.css, so a consumer that
|
|
25
|
+
bundles it animates these for real. %>
|
|
26
|
+
<%
|
|
27
|
+
web3_on = Studio.feature?(:web3)
|
|
28
|
+
leveling_on = Studio.feature?(:leveling)
|
|
29
|
+
|
|
30
|
+
# Auth method-toggle defaults — mirror the app's Studio.auth_method? config;
|
|
31
|
+
# Solana Wallet also needs the web3 capability, so it defaults OFF on an app
|
|
32
|
+
# (like McRitchie Studio) that ships web3 off.
|
|
33
|
+
ml_default = Studio.auth_method?(:magic_link)
|
|
34
|
+
g_default = Studio.auth_method?(:google)
|
|
35
|
+
w_default = Studio.auth_method?(:wallet) && web3_on
|
|
36
|
+
|
|
37
|
+
# Active-card glow-match expression builder: yields an Alpine boolean that is
|
|
38
|
+
# true when $store.dsModals.current() is THIS specimen's modal. It discriminates
|
|
39
|
+
# by sub-state so two specimens that open the SAME modal id still glow the right
|
|
40
|
+
# card: `step:` for the auth step machine, and `crop:` for the two crop-photo
|
|
41
|
+
# variants (:picker vs :crop). The crop sub-state rides props.cropReady — the
|
|
42
|
+
# crop factory sets it true when the cropper mounts (on a direct image OR after
|
|
43
|
+
# an in-modal pick), so the glow moves picker -> cropper as the flow advances.
|
|
44
|
+
ds_glow = lambda do |id, step: nil, crop: nil|
|
|
45
|
+
cur = "$store.dsModals.current()"
|
|
46
|
+
expr = "#{cur} && #{cur}.id === '#{id}'"
|
|
47
|
+
expr += " && (#{cur}.props.step || 'credentials') === '#{step}'" if step
|
|
48
|
+
expr += (crop == :crop ? " && !!#{cur}.props.cropReady" : " && !#{cur}.props.cropReady") unless crop.nil?
|
|
49
|
+
expr
|
|
50
|
+
end
|
|
51
|
+
%>
|
|
52
|
+
<section id="modals" class="space-y-10" style="scroll-margin-top: calc(var(--nav-h, 0px) + 5rem)"
|
|
53
|
+
x-data="{}" @ds-modal-close.window="$store.dsModals.close()">
|
|
54
|
+
<div class="space-y-1">
|
|
55
|
+
<h2 class="text-2xl font-bold text-heading">Modals</h2>
|
|
56
|
+
<p class="text-muted text-sm">
|
|
57
|
+
The shared modal host and the reusable modal partials, ported from Turf
|
|
58
|
+
Monster into the engine. Each tile is a <strong>visual representation, not
|
|
59
|
+
to scale</strong> — <strong>click a whole card</strong> to open the real
|
|
60
|
+
modal (Enter/Space work too). The header <strong>Copy</strong> yields an
|
|
61
|
+
agent-ready reference you can paste into a chat; option <strong>toggles</strong>
|
|
62
|
+
configure the modal before you open it; and the card that matches the open
|
|
63
|
+
modal + step <strong>glows</strong>, following the step machine as it advances.
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<%# ---- Page-scoped modal host (dsModals) --------------------------------
|
|
68
|
+
The full store API — open(id, props, opts) LIFO stack, swap() directional
|
|
69
|
+
slide, advance(patch, opts) in-modal step change, close / closeAll,
|
|
70
|
+
current(), cardClasses() driving the enter/exit animation registry, and
|
|
71
|
+
body.modal-open scroll lock. Plus the dsSolanaModal proxy over the store
|
|
72
|
+
(stable show/success/error/close API for the on-chain-tx modal) and the
|
|
73
|
+
credential stubs (postMagicLink / walletProvider / solanaWalletConnect) so
|
|
74
|
+
the auth + wallet specimens run without an auth backend. Defined in a plain
|
|
75
|
+
inline script (no importmap dependency) so Alpine's defer finds it. %>
|
|
76
|
+
<script>
|
|
77
|
+
(function () {
|
|
78
|
+
// === Credential stubs — never clobber a real one an app already ships. ==
|
|
79
|
+
window.postMagicLink = window.postMagicLink || function () {
|
|
80
|
+
return Promise.resolve({ success: true });
|
|
81
|
+
};
|
|
82
|
+
window.solanaWalletConnect = window.solanaWalletConnect || function () {
|
|
83
|
+
return { connecting: false, error: null, statusText: 'Connect Wallet',
|
|
84
|
+
walletAvailable: false, isMobile: false, connect: function () {} };
|
|
85
|
+
};
|
|
86
|
+
window.walletProvider = window.walletProvider || {
|
|
87
|
+
available: function () { return [{ name: 'Phantom' }, { name: 'Solflare' }]; },
|
|
88
|
+
isMobile: function () { return false; },
|
|
89
|
+
isAvailable: function () { return false; }
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
document.addEventListener('alpine:init', function () {
|
|
93
|
+
if (Alpine.store('dsModals')) return;
|
|
94
|
+
|
|
95
|
+
// Enter/exit animation registry — keys map to a CSS class (engine-
|
|
96
|
+
// motion.css) plus its duration in ms. Open a modal with
|
|
97
|
+
// { enterAnim: 'shake' } / { exitAnim: 'slide' } to pick one; omitted
|
|
98
|
+
// uses 'pop' (the spring bounce). The directional swap()/advance()
|
|
99
|
+
// slide uses the modal-card-swap-* classes regardless of these keys.
|
|
100
|
+
var CLOSE_ANIM_MS = 220;
|
|
101
|
+
var SWAP_IN_MS = 220;
|
|
102
|
+
var Anim = {
|
|
103
|
+
enter: {
|
|
104
|
+
pop: { cls: 'modal-card-mount', ms: 320 },
|
|
105
|
+
shake: { cls: 'modal-card-shake-in', ms: 600 },
|
|
106
|
+
slide: { cls: 'modal-card-swap-in', ms: 220 }
|
|
107
|
+
},
|
|
108
|
+
exit: {
|
|
109
|
+
pop: { cls: 'modal-card-unmount', ms: 220 },
|
|
110
|
+
slide: { cls: 'modal-card-swap-out', ms: 220 }
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
function modalAnim(channel, key) {
|
|
114
|
+
var table = Anim[channel] || {};
|
|
115
|
+
return table[key] || table.pop;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
Alpine.store('dsModals', {
|
|
119
|
+
stack: [],
|
|
120
|
+
current: function () { return this.stack.length ? this.stack[this.stack.length - 1] : null; },
|
|
121
|
+
isOpen: function (id) {
|
|
122
|
+
for (var i = 0; i < this.stack.length; i++) { if (this.stack[i].id === id) return true; }
|
|
123
|
+
return false;
|
|
124
|
+
},
|
|
125
|
+
// open(id, props, opts) — opts.replace swaps the top entry with a
|
|
126
|
+
// directional slide (forward = exit right / enter left); otherwise
|
|
127
|
+
// push (stacking — closing reveals the entry underneath).
|
|
128
|
+
open: function (id, props, opts) {
|
|
129
|
+
props = props || {}; opts = opts || {};
|
|
130
|
+
var self = this;
|
|
131
|
+
if (opts.replace && this.stack.length > 0) {
|
|
132
|
+
var current = this.current();
|
|
133
|
+
if (current && !current._closing) {
|
|
134
|
+
var dir = (opts.direction === 'back') ? 'back' : 'forward';
|
|
135
|
+
current._swapDir = dir;
|
|
136
|
+
current._swappingOut = true;
|
|
137
|
+
current._closing = true;
|
|
138
|
+
setTimeout(function () {
|
|
139
|
+
var idx = self.stack.indexOf(current);
|
|
140
|
+
var newEntry = { id: id, props: props, _swappingIn: true, _swapDir: dir };
|
|
141
|
+
if (idx >= 0) { self.stack[idx] = newEntry; } else { self.stack.push(newEntry); }
|
|
142
|
+
self._sync();
|
|
143
|
+
setTimeout(function () {
|
|
144
|
+
newEntry._swappingIn = false; newEntry._swapDir = null; newEntry._settled = true;
|
|
145
|
+
}, SWAP_IN_MS);
|
|
146
|
+
}, CLOSE_ANIM_MS);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
this.stack[this.stack.length - 1] = { id: id, props: props };
|
|
150
|
+
} else {
|
|
151
|
+
this.stack.push({ id: id, props: props });
|
|
152
|
+
}
|
|
153
|
+
this._sync();
|
|
154
|
+
},
|
|
155
|
+
// swap(id, props, opts) ≡ open with { replace: true }.
|
|
156
|
+
swap: function (id, props, opts) {
|
|
157
|
+
return this.open(id, props, Object.assign({ replace: true }, opts || {}));
|
|
158
|
+
},
|
|
159
|
+
// advance(patch, opts) — in-flow step change: same directional slide as
|
|
160
|
+
// swap() but WITHOUT replacing the stack entry, so the modal's outer
|
|
161
|
+
// x-data scope survives (the auth step machine relies on this).
|
|
162
|
+
advance: function (propsPatch, opts) {
|
|
163
|
+
opts = opts || {};
|
|
164
|
+
var cur = this.current();
|
|
165
|
+
if (!cur || cur._closing || cur._swappingOut) return;
|
|
166
|
+
var self = this;
|
|
167
|
+
cur._swapDir = (opts.direction === 'back') ? 'back' : 'forward';
|
|
168
|
+
cur._swappingOut = true;
|
|
169
|
+
setTimeout(function () {
|
|
170
|
+
if (self.stack.indexOf(cur) < 0) return;
|
|
171
|
+
if (propsPatch) Object.assign(cur.props, propsPatch);
|
|
172
|
+
cur._swappingOut = false;
|
|
173
|
+
cur._swappingIn = true;
|
|
174
|
+
setTimeout(function () {
|
|
175
|
+
cur._swappingIn = false; cur._swapDir = null; cur._settled = true;
|
|
176
|
+
}, SWAP_IN_MS);
|
|
177
|
+
}, CLOSE_ANIM_MS);
|
|
178
|
+
},
|
|
179
|
+
close: function () {
|
|
180
|
+
var self = this;
|
|
181
|
+
var entry = this.current();
|
|
182
|
+
if (!entry || entry._closing) return;
|
|
183
|
+
entry._closing = true;
|
|
184
|
+
setTimeout(function () {
|
|
185
|
+
var idx = self.stack.indexOf(entry);
|
|
186
|
+
if (idx >= 0) { self.stack.splice(idx, 1); self._sync(); }
|
|
187
|
+
}, modalAnim('exit', entry.props && entry.props.exitAnim).ms);
|
|
188
|
+
},
|
|
189
|
+
closeAll: function () { this.stack = []; this._sync(); },
|
|
190
|
+
cardClasses: function () {
|
|
191
|
+
var c = this.current();
|
|
192
|
+
if (!c) return {};
|
|
193
|
+
var o = {};
|
|
194
|
+
if (!c._settled && !c._swappingIn && !c._swappingOut && !c._closing) {
|
|
195
|
+
o[modalAnim('enter', c.props && c.props.enterAnim).cls] = true;
|
|
196
|
+
}
|
|
197
|
+
if (c._closing && !c._swappingOut) {
|
|
198
|
+
o[modalAnim('exit', c.props && c.props.exitAnim).cls] = true;
|
|
199
|
+
}
|
|
200
|
+
o['modal-card-swap-in'] = !!(c._swappingIn && c._swapDir !== 'back');
|
|
201
|
+
o['modal-card-swap-out'] = !!(c._swappingOut && c._swapDir !== 'back');
|
|
202
|
+
o['modal-card-swap-in-back'] = !!(c._swappingIn && c._swapDir === 'back');
|
|
203
|
+
o['modal-card-swap-out-back'] = !!(c._swappingOut && c._swapDir === 'back');
|
|
204
|
+
return o;
|
|
205
|
+
},
|
|
206
|
+
_sync: function () {
|
|
207
|
+
if (this.stack.length) { document.body.classList.add('modal-open'); }
|
|
208
|
+
else { document.body.classList.remove('modal-open'); }
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// dsSolanaModal — stateless proxy over dsModals for the on-chain-tx
|
|
213
|
+
// modal. Reads the current 'onchain-tx' entry's props so the partial's
|
|
214
|
+
// $store.dsSolanaModal.{state,title,txSignature,...} reads stay stable.
|
|
215
|
+
Alpine.store('dsSolanaModal', {
|
|
216
|
+
_cur: function () { var c = Alpine.store('dsModals').current(); return (c && c.id === 'onchain-tx') ? c : null; },
|
|
217
|
+
_read: function (k, fb) { var c = this._cur(); return c ? (c.props[k] !== undefined ? c.props[k] : fb) : fb; },
|
|
218
|
+
close: function () { if (this._cur()) Alpine.store('dsModals').close(); },
|
|
219
|
+
get state() { return this._read('state', null); },
|
|
220
|
+
get title() { return this._read('title', ''); },
|
|
221
|
+
get message() { return this._read('message', ''); },
|
|
222
|
+
get txSignature() { return this._read('txSignature', null); },
|
|
223
|
+
get errorMessage() { return this._read('errorMessage', ''); },
|
|
224
|
+
get successTitle() { return this._read('successTitle', ''); },
|
|
225
|
+
get successSubtitle() { return this._read('successSubtitle', ''); },
|
|
226
|
+
get ctaLabel() { return this._read('ctaLabel', ''); },
|
|
227
|
+
get ctaHref() { return this._read('ctaHref', ''); }
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
})();
|
|
231
|
+
</script>
|
|
232
|
+
|
|
233
|
+
<%# Cropper.js + the cropPhotoModal factory — makes the Profile crop / upload
|
|
234
|
+
modals openable on this page (loads cropper.js via <script defer> and
|
|
235
|
+
defines the factory inline; no importmap change). %>
|
|
236
|
+
<%= render "studio/cropper_assets" %>
|
|
237
|
+
|
|
238
|
+
<%# ---- The overlay: one backdrop + card; every specimen's real content is a
|
|
239
|
+
single-root <template x-if> gated on the current modal id. Faithful to the
|
|
240
|
+
shared host — backdrop fade, scroll lock, escape + click-self dismissal
|
|
241
|
+
gated on props.dismissible (never @click.outside, so a hold modal can't
|
|
242
|
+
close itself on the button release). %>
|
|
243
|
+
<template x-if="$store.dsModals.current()">
|
|
244
|
+
<div class="fixed inset-0 z-[120] flex items-center justify-center p-4 modal-backdrop-mount"
|
|
245
|
+
:class="$store.dsModals.current()?._closing && 'modal-backdrop-unmount'"
|
|
246
|
+
style="background: rgba(0,0,0,0.6)"
|
|
247
|
+
role="dialog" aria-modal="true"
|
|
248
|
+
@keydown.escape.window="$store.dsModals.current() && $store.dsModals.current().props.dismissible !== false && $store.dsModals.close()"
|
|
249
|
+
@click.self="$store.dsModals.current() && $store.dsModals.current().props.dismissible !== false && $store.dsModals.close()">
|
|
250
|
+
<div class="bg-surface rounded-xl border border-subtle shadow-2xl p-6 max-w-sm w-full"
|
|
251
|
+
:class="$store.dsModals.cardClasses()">
|
|
252
|
+
|
|
253
|
+
<%# --- Auth suite --- %>
|
|
254
|
+
<template x-if="$store.dsModals.current().id === 'auth'">
|
|
255
|
+
<div><%= render "style/modals/auth" %></div>
|
|
256
|
+
</template>
|
|
257
|
+
|
|
258
|
+
<%# --- Profile (real engine partials, opened on dsModals) --- %>
|
|
259
|
+
<template x-if="$store.dsModals.current().id === 'crop-photo'">
|
|
260
|
+
<div><%= render "studio/modals/crop_photo", store: "dsModals" %></div>
|
|
261
|
+
</template>
|
|
262
|
+
<template x-if="$store.dsModals.current().id === 'saving'">
|
|
263
|
+
<div><%= render "studio/modals/saving", store: "dsModals" %></div>
|
|
264
|
+
</template>
|
|
265
|
+
|
|
266
|
+
<%# --- Web3 --- %>
|
|
267
|
+
<template x-if="$store.dsModals.current().id === 'wallet-connect'">
|
|
268
|
+
<div><%= render "style/modals/wallet_connect" %></div>
|
|
269
|
+
</template>
|
|
270
|
+
<template x-if="$store.dsModals.current().id === 'onchain-tx'">
|
|
271
|
+
<div><%= render "style/modals/onchain_tx" %></div>
|
|
272
|
+
</template>
|
|
273
|
+
<template x-if="$store.dsModals.current().id === 'wallet-deposit'">
|
|
274
|
+
<div><%= render "style/modals/wallet_deposit" %></div>
|
|
275
|
+
</template>
|
|
276
|
+
|
|
277
|
+
<%# --- System and status (reusable card blocks) --- %>
|
|
278
|
+
<template x-if="$store.dsModals.current().id === 'ds-processing'">
|
|
279
|
+
<div><%= render "studio/modals/blocks/processing_card",
|
|
280
|
+
title: "Confirming entry", message: "Talking to the server…" %></div>
|
|
281
|
+
</template>
|
|
282
|
+
<template x-if="$store.dsModals.current().id === 'ds-success'">
|
|
283
|
+
<div><%= render "studio/modals/blocks/success_card",
|
|
284
|
+
title: "Entry confirmed", message: "You're in — good luck!",
|
|
285
|
+
icon_color: "success", cta_label: "Continue", cta_event: "ds-modal-close" do %><% end %></div>
|
|
286
|
+
</template>
|
|
287
|
+
<template x-if="$store.dsModals.current().id === 'ds-error'">
|
|
288
|
+
<div><%= render "studio/modals/blocks/error_card",
|
|
289
|
+
title: "Couldn't connect", icon_emoji: "⚠️",
|
|
290
|
+
message: "Something went wrong. Give it another try.",
|
|
291
|
+
cta_label: "Try again", cta_event: "ds-modal-close",
|
|
292
|
+
secondary_label: "Dismiss", secondary_event: "ds-modal-close" %></div>
|
|
293
|
+
</template>
|
|
294
|
+
<template x-if="$store.dsModals.current().id === 'ds-countdown'">
|
|
295
|
+
<div x-data="{ n: 5, t: 5, timer: null }"
|
|
296
|
+
x-init="timer = setInterval(() => { if (n > 0) { n--; } else { clearInterval(timer); } }, 1000)">
|
|
297
|
+
<div class="text-center mb-4">
|
|
298
|
+
<p class="text-heading font-bold text-lg mb-1">Heading to the lobby</p>
|
|
299
|
+
<p class="text-secondary text-xs">Sit tight — this happens automatically.</p>
|
|
300
|
+
</div>
|
|
301
|
+
<%= render "studio/modals/blocks/progress_countdown", current_key: "n", total_key: "t" %>
|
|
302
|
+
<button type="button" class="btn btn-outline btn-sm w-full mt-4"
|
|
303
|
+
@click="$store.dsModals.close()">Cancel</button>
|
|
304
|
+
</div>
|
|
305
|
+
</template>
|
|
306
|
+
|
|
307
|
+
<%# --- Templates (copy-from archetypes) --- %>
|
|
308
|
+
<template x-if="$store.dsModals.current().id === 'template-wizard'">
|
|
309
|
+
<div><%= render "studio/modals/templates/wizard", modal_store: "dsModals" %></div>
|
|
310
|
+
</template>
|
|
311
|
+
<template x-if="$store.dsModals.current().id === 'template-form'">
|
|
312
|
+
<div><%= render "studio/modals/templates/form", modal_store: "dsModals" %></div>
|
|
313
|
+
</template>
|
|
314
|
+
<template x-if="$store.dsModals.current().id === 'template-action'">
|
|
315
|
+
<div><%= render "studio/modals/templates/action", modal_store: "dsModals" %></div>
|
|
316
|
+
</template>
|
|
317
|
+
<template x-if="$store.dsModals.current().id === 'template-status'">
|
|
318
|
+
<div><%= render "studio/modals/templates/status", modal_store: "dsModals" %></div>
|
|
319
|
+
</template>
|
|
320
|
+
<template x-if="$store.dsModals.current().id === 'template-success'">
|
|
321
|
+
<div><%= render "studio/modals/templates/success", modal_store: "dsModals" %></div>
|
|
322
|
+
</template>
|
|
323
|
+
|
|
324
|
+
<%# --- Rewards / leveling --- %>
|
|
325
|
+
<template x-if="$store.dsModals.current().id === 'levelup'">
|
|
326
|
+
<div><%= render "studio/modals/blocks/success_card",
|
|
327
|
+
title: "Level 10!", message: "You leveled up.", icon_emoji: "🎉",
|
|
328
|
+
large_title: true, cta_label: "Nice", cta_event: "ds-modal-close" do %>
|
|
329
|
+
<div class="flex items-center justify-center">
|
|
330
|
+
<span class="sheen sheen--loop studio-glow studio-glow--pulse inline-flex h-14 w-14 items-center justify-center rounded-full text-white text-lg font-extrabold"
|
|
331
|
+
style="background: var(--color-cta); --studio-glow-color: var(--color-success)">10</span>
|
|
332
|
+
</div>
|
|
333
|
+
<% end %></div>
|
|
334
|
+
</template>
|
|
335
|
+
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
</template>
|
|
339
|
+
|
|
340
|
+
<%# ===================================================================== %>
|
|
341
|
+
<%# 1. AUTH suite %>
|
|
342
|
+
<%# ===================================================================== %>
|
|
343
|
+
<section class="space-y-5">
|
|
344
|
+
<div class="space-y-1">
|
|
345
|
+
<h3 class="text-xl font-bold text-heading">Auth</h3>
|
|
346
|
+
<p class="text-muted text-sm">
|
|
347
|
+
The sign-in step machine
|
|
348
|
+
(<code class="font-mono text-2xs">style/modals/_auth</code>) — one modal,
|
|
349
|
+
internal steps <code class="font-mono text-2xs">credentials →
|
|
350
|
+
magic-link-sent → magic-link-resent</code>. Toggle the methods below a
|
|
351
|
+
card, then open it to watch them gate the modal. The live card
|
|
352
|
+
<strong>glows</strong>, and the glow follows the step machine as you advance.
|
|
353
|
+
</p>
|
|
354
|
+
</div>
|
|
355
|
+
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
356
|
+
<%# Sign in — method toggles + the credentials-step glow. %>
|
|
357
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
358
|
+
label: "Sign in",
|
|
359
|
+
reference: %(the Auth "Sign in" modal (studio-engine style/modals/_auth, step 'credentials') — open with $store.dsModals.open('auth', { step: 'credentials', picksRequired: 6 }); toggle methods via props.methods { magicLink, google, wallet } + props.terms),
|
|
360
|
+
card_data: "opts: { magicLink: #{ml_default}, google: #{g_default}, wallet: #{w_default}, terms: true }",
|
|
361
|
+
open_expr: "$store.dsModals.open('auth', { step: 'credentials', picksRequired: 6, methods: { magicLink: opts.magicLink, google: opts.google, wallet: opts.wallet }, terms: opts.terms })",
|
|
362
|
+
glow_when: ds_glow.call("auth", step: "credentials"),
|
|
363
|
+
toggles: [
|
|
364
|
+
{ model: "opts.magicLink", label: "Magic Link" },
|
|
365
|
+
{ model: "opts.google", label: "Google" },
|
|
366
|
+
{ model: "opts.wallet", label: "Solana Wallet" },
|
|
367
|
+
{ model: "opts.terms", label: "Terms" }
|
|
368
|
+
] } do %>
|
|
369
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2 text-center">
|
|
370
|
+
<span class="block h-2 w-16 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
371
|
+
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
372
|
+
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
373
|
+
<span class="block h-6 w-full rounded" style="background: var(--color-cta); opacity: .5"></span>
|
|
374
|
+
</div>
|
|
375
|
+
<% end %>
|
|
376
|
+
|
|
377
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
378
|
+
label: "Magic link sent",
|
|
379
|
+
reference: %(the Auth "Magic link sent" step (style/modals/_auth, step 'magic-link-sent') — open with $store.dsModals.open('auth', { step: 'magic-link-sent', sentEmail: 'you@example.com' })),
|
|
380
|
+
open_expr: "$store.dsModals.open('auth', { step: 'magic-link-sent', sentEmail: 'you@example.com' })",
|
|
381
|
+
glow_when: ds_glow.call("auth", step: "magic-link-sent") } do %>
|
|
382
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
383
|
+
<div class="text-2xl leading-none">📬</div>
|
|
384
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
385
|
+
<span class="block h-1.5 w-16 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
|
|
386
|
+
</div>
|
|
387
|
+
<% end %>
|
|
388
|
+
|
|
389
|
+
<%# NEW — the magic-link-resent step (the resend confirmation slide). %>
|
|
390
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
391
|
+
label: "Magic link resent",
|
|
392
|
+
reference: %(the Auth "Magic link resent" step (style/modals/_auth, step 'magic-link-resent') — open with $store.dsModals.open('auth', { step: 'magic-link-resent', sentEmail: 'you@example.com' })),
|
|
393
|
+
open_expr: "$store.dsModals.open('auth', { step: 'magic-link-resent', sentEmail: 'you@example.com' })",
|
|
394
|
+
glow_when: ds_glow.call("auth", step: "magic-link-resent") } do %>
|
|
395
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
396
|
+
<div class="mx-auto w-7 h-7 rounded-full flex items-center justify-center" style="background: color-mix(in srgb, var(--color-primary) 15%, transparent)">
|
|
397
|
+
<svg class="w-4 h-4" style="color: var(--color-primary)" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
|
|
398
|
+
</div>
|
|
399
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
400
|
+
<span class="block h-1.5 w-16 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
|
|
401
|
+
</div>
|
|
402
|
+
<% end %>
|
|
403
|
+
|
|
404
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
405
|
+
label: "Redirect (drain)",
|
|
406
|
+
reference: %(the Auth "Redirect" step (style/modals/_auth, step 'redirect', drain CTA) — open with $store.dsModals.open('auth', { step: 'redirect', icon: '📍', title: 'One more step', message: 'Taking you there.', url: null })),
|
|
407
|
+
open_expr: "$store.dsModals.open('auth', { step: 'redirect', icon: '📍', title: 'One more step', message: 'Taking you there.', url: null })",
|
|
408
|
+
glow_when: ds_glow.call("auth", step: "redirect") } do %>
|
|
409
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
410
|
+
<div class="text-2xl leading-none">📍</div>
|
|
411
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
412
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
413
|
+
</div>
|
|
414
|
+
<% end %>
|
|
415
|
+
</div>
|
|
416
|
+
</section>
|
|
417
|
+
|
|
418
|
+
<%# ===================================================================== %>
|
|
419
|
+
<%# 2. PROFILE — crop / upload, live via cropper_assets %>
|
|
420
|
+
<%# ===================================================================== %>
|
|
421
|
+
<section class="space-y-5">
|
|
422
|
+
<div class="space-y-1">
|
|
423
|
+
<h3 class="text-xl font-bold text-heading">Profile</h3>
|
|
424
|
+
<p class="text-muted text-sm">
|
|
425
|
+
The avatar / photo modals ship from the engine
|
|
426
|
+
(<code class="font-mono text-2xs">studio/modals/_crop_photo</code>,
|
|
427
|
+
<code class="font-mono text-2xs">studio/modals/_image_upload</code>) and
|
|
428
|
+
open live here — <code class="font-mono text-2xs">studio/cropper_assets</code>
|
|
429
|
+
is rendered above, so cropper.js loads and the modals mount on this page's
|
|
430
|
+
host.
|
|
431
|
+
</p>
|
|
432
|
+
</div>
|
|
433
|
+
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
434
|
+
<%# Image upload FIRST — the empty picker (crop-photo opened with no imageUrl).
|
|
435
|
+
cropReady: false is the defined starting sub-state; the crop factory
|
|
436
|
+
flips it true once an image is picked, moving the glow to Crop photo. %>
|
|
437
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
438
|
+
label: "Image upload",
|
|
439
|
+
reference: %(the Profile "Image upload" modal (studio-engine studio/modals/_crop_photo as the empty picker) — open with $store.dsModals.open('crop-photo', {})),
|
|
440
|
+
open_expr: "$store.dsModals.open('crop-photo', { cropReady: false })",
|
|
441
|
+
glow_when: ds_glow.call("crop-photo", crop: :picker) } do %>
|
|
442
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
443
|
+
<div class="mx-auto w-20 h-20 rounded-lg border-2 border-dashed flex items-center justify-center" style="border-color: var(--color-border-strong)">
|
|
444
|
+
<span class="text-2xl leading-none">⬆️</span>
|
|
445
|
+
</div>
|
|
446
|
+
<span class="block h-1.5 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
|
|
447
|
+
</div>
|
|
448
|
+
<% end %>
|
|
449
|
+
|
|
450
|
+
<%# Crop photo SECOND — the cropper mounted on a chosen image. cropReady:
|
|
451
|
+
true glows this card immediately on a direct open (the factory also sets
|
|
452
|
+
it when the cropper mounts). %>
|
|
453
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
454
|
+
label: "Crop photo",
|
|
455
|
+
reference: %(the Profile "Crop photo" modal (studio/modals/_crop_photo + studio/_cropper_assets) — open with $store.dsModals.open('crop-photo', { imageUrl: '/icon.png', aspectRatio: 1 })),
|
|
456
|
+
open_expr: "$store.dsModals.open('crop-photo', { imageUrl: '/icon.png', aspectRatio: 1, cropReady: true })",
|
|
457
|
+
glow_when: ds_glow.call("crop-photo", crop: :crop) } do %>
|
|
458
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
459
|
+
<div class="mx-auto w-20 h-20 rounded-lg border-2 flex items-center justify-center" style="border-color: var(--color-cta)">
|
|
460
|
+
<span class="text-muted text-2xs">Crop area</span>
|
|
461
|
+
</div>
|
|
462
|
+
<span class="block h-4 w-full rounded" style="background: var(--color-cta)"></span>
|
|
463
|
+
</div>
|
|
464
|
+
<% end %>
|
|
465
|
+
|
|
466
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
467
|
+
label: "Saving card",
|
|
468
|
+
reference: %(the Profile "Saving" card (studio/modals/_saving loading state) — open with $store.dsModals.open('saving', { title: 'Saving…' })),
|
|
469
|
+
open_expr: "$store.dsModals.open('saving', { title: 'Saving…' })",
|
|
470
|
+
glow_when: ds_glow.call("saving") } do %>
|
|
471
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
472
|
+
<div class="mx-auto w-8 h-8 rounded-full border-4 animate-spin" style="border-color: color-mix(in srgb, var(--color-cta) 30%, transparent); border-top-color: var(--color-cta)"></div>
|
|
473
|
+
<span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
474
|
+
</div>
|
|
475
|
+
<% end %>
|
|
476
|
+
</div>
|
|
477
|
+
</section>
|
|
478
|
+
|
|
479
|
+
<%# ===================================================================== %>
|
|
480
|
+
<%# 3. WEB3 — gated by Studio.feature?(:web3); off = disabled-but-present-yet-openable %>
|
|
481
|
+
<%# ===================================================================== %>
|
|
482
|
+
<section class="space-y-5">
|
|
483
|
+
<div class="space-y-1">
|
|
484
|
+
<div class="flex flex-wrap items-center gap-3">
|
|
485
|
+
<h3 class="text-xl font-bold text-heading">Web3</h3>
|
|
486
|
+
<% if web3_on %>
|
|
487
|
+
<span class="badge" style="color: var(--color-success); border-color: var(--color-success)">enabled on this app</span>
|
|
488
|
+
<% else %>
|
|
489
|
+
<span class="badge" style="color: var(--color-warning); border-color: var(--color-warning)">disabled on this app</span>
|
|
490
|
+
<% end %>
|
|
491
|
+
</div>
|
|
492
|
+
<p class="text-muted text-sm">
|
|
493
|
+
Wallet + on-chain modals, ported into the engine and gated by
|
|
494
|
+
<code class="font-mono text-2xs">Studio.feature?(:web3)</code>. Web3 is
|
|
495
|
+
<strong><%= web3_on ? "on" : "off" %></strong> here, so these render
|
|
496
|
+
<%= web3_on ? "live." : "greyed and badged — but STILL openable as a preview." %>
|
|
497
|
+
</p>
|
|
498
|
+
</div>
|
|
499
|
+
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
500
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
501
|
+
label: "Connect wallet",
|
|
502
|
+
reference: %(the Web3 "Connect wallet" picker (studio-engine style/modals/_wallet_connect) — open with $store.dsModals.open('wallet-connect')),
|
|
503
|
+
open_expr: "$store.dsModals.open('wallet-connect')",
|
|
504
|
+
disabled: !web3_on, openable: true } do %>
|
|
505
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 space-y-2">
|
|
506
|
+
<span class="block h-2 w-20 rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
507
|
+
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
508
|
+
<span class="block h-6 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
509
|
+
</div>
|
|
510
|
+
<% end %>
|
|
511
|
+
|
|
512
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
513
|
+
label: "On-chain tx · processing",
|
|
514
|
+
reference: %(the Web3 "On-chain tx" modal, processing state (style/modals/_onchain_tx via $store.dsSolanaModal) — open with $store.dsModals.open('onchain-tx', { state: 'processing', title: 'Confirming on-chain', message: 'Waiting for the wallet signature…' })),
|
|
515
|
+
open_expr: "$store.dsModals.open('onchain-tx', { state: 'processing', title: 'Confirming on-chain', message: 'Waiting for the wallet signature…' })",
|
|
516
|
+
disabled: !web3_on, openable: true } do %>
|
|
517
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
518
|
+
<div class="loading-dots mx-auto"><span></span><span></span><span></span></div>
|
|
519
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
520
|
+
</div>
|
|
521
|
+
<% end %>
|
|
522
|
+
|
|
523
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
524
|
+
label: "On-chain tx · success",
|
|
525
|
+
reference: %(the Web3 "On-chain tx" modal, generic-success state (style/modals/_onchain_tx + studio/modals/blocks/_onchain_success) — open with $store.dsModals.open('onchain-tx', { state: 'success', txSignature: '…', successTitle: 'Mint complete' })),
|
|
526
|
+
open_expr: "$store.dsModals.open('onchain-tx', { state: 'success', txSignature: '5xTrDemoSignature1234567890', successTitle: 'Mint complete', successSubtitle: 'Your on-chain action confirmed.', ctaLabel: 'View wallet', ctaHref: null })",
|
|
527
|
+
disabled: !web3_on, openable: true } do %>
|
|
528
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
529
|
+
<div class="mx-auto w-7 h-7 rounded-full flex items-center justify-center" style="background: color-mix(in srgb, var(--color-success) 15%, transparent)">
|
|
530
|
+
<svg class="w-4 h-4" style="color: var(--color-success)" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
|
|
531
|
+
</div>
|
|
532
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
533
|
+
</div>
|
|
534
|
+
<% end %>
|
|
535
|
+
|
|
536
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
537
|
+
label: "On-chain tx · error",
|
|
538
|
+
reference: %(the Web3 "On-chain tx" modal, error state (style/modals/_onchain_tx) — open with $store.dsModals.open('onchain-tx', { state: 'error', title: "Couldn't confirm", errorMessage: 'The transaction was rejected. Give it another try.' })),
|
|
539
|
+
open_expr: "$store.dsModals.open('onchain-tx', { state: 'error', title: 'Could not confirm', errorMessage: 'The transaction was rejected. Give it another try.' })",
|
|
540
|
+
disabled: !web3_on, openable: true } do %>
|
|
541
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
542
|
+
<div class="text-2xl leading-none">⚠️</div>
|
|
543
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
544
|
+
</div>
|
|
545
|
+
<% end %>
|
|
546
|
+
|
|
547
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
548
|
+
label: "Wallet deposit",
|
|
549
|
+
reference: %(the Web3 "Wallet deposit" top-up card (style/modals/_wallet_deposit) — open with $store.dsModals.open('wallet-deposit', { neededCents: 500, usdcCents: 120, usdtCents: 0, address: 'Fo1L5…' })),
|
|
550
|
+
open_expr: "$store.dsModals.open('wallet-deposit', { neededCents: 500, usdcCents: 120, usdtCents: 0, address: 'Fo1L5demoWALLETaddr9xQ2' })",
|
|
551
|
+
disabled: !web3_on, openable: true } do %>
|
|
552
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
553
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
554
|
+
<span class="block h-5 w-full rounded" style="background: var(--color-cta)"></span>
|
|
555
|
+
<span class="block h-5 w-full rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
556
|
+
</div>
|
|
557
|
+
<% end %>
|
|
558
|
+
</div>
|
|
559
|
+
</section>
|
|
560
|
+
|
|
561
|
+
<%# ===================================================================== %>
|
|
562
|
+
<%# 4. SYSTEM AND STATUS (reusable card blocks) %>
|
|
563
|
+
<%# ===================================================================== %>
|
|
564
|
+
<section class="space-y-5">
|
|
565
|
+
<div class="space-y-1">
|
|
566
|
+
<h3 class="text-xl font-bold text-heading">System & status</h3>
|
|
567
|
+
<p class="text-muted text-sm">
|
|
568
|
+
The reusable card blocks under <code class="font-mono text-2xs">studio/modals/blocks/</code> —
|
|
569
|
+
the studio-canonical processing, success, error, and countdown states.
|
|
570
|
+
</p>
|
|
571
|
+
</div>
|
|
572
|
+
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
573
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
574
|
+
label: "Processing",
|
|
575
|
+
reference: %(the "Processing" card block (studio/modals/blocks/_processing_card) — open with $store.dsModals.open('ds-processing')),
|
|
576
|
+
open_expr: "$store.dsModals.open('ds-processing')" } do %>
|
|
577
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
578
|
+
<div class="mx-auto w-6 h-6 rounded-full border-2 animate-spin" style="border-color: color-mix(in srgb, var(--color-cta) 30%, transparent); border-top-color: var(--color-cta)"></div>
|
|
579
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
580
|
+
<span class="block h-1.5 w-16 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
|
|
581
|
+
</div>
|
|
582
|
+
<% end %>
|
|
583
|
+
|
|
584
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
585
|
+
label: "Success",
|
|
586
|
+
reference: %(the "Success" card block (studio/modals/blocks/_success_card, entry-confirmed style) — open with $store.dsModals.open('ds-success')),
|
|
587
|
+
open_expr: "$store.dsModals.open('ds-success')" } do %>
|
|
588
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
589
|
+
<div class="mx-auto w-7 h-7 rounded-full flex items-center justify-center" style="background: color-mix(in srgb, var(--color-success) 15%, transparent)">
|
|
590
|
+
<svg class="w-4 h-4" style="color: var(--color-success)" fill="none" stroke="currentColor" stroke-width="3" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7"/></svg>
|
|
591
|
+
</div>
|
|
592
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
593
|
+
<span class="block h-4 w-full rounded" style="background: var(--color-cta)"></span>
|
|
594
|
+
</div>
|
|
595
|
+
<% end %>
|
|
596
|
+
|
|
597
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
598
|
+
label: "Error / retry",
|
|
599
|
+
reference: %(the "Error" card block (studio/modals/blocks/_error_card) — open with $store.dsModals.open('ds-error')),
|
|
600
|
+
open_expr: "$store.dsModals.open('ds-error')" } do %>
|
|
601
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
602
|
+
<div class="text-2xl leading-none">⚠️</div>
|
|
603
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
604
|
+
<span class="block h-4 w-20 mx-auto rounded border" style="border-color: var(--color-border-strong)"></span>
|
|
605
|
+
</div>
|
|
606
|
+
<% end %>
|
|
607
|
+
|
|
608
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
609
|
+
label: "Countdown",
|
|
610
|
+
reference: %(the "Countdown" progress block (studio/modals/blocks/_progress_countdown) — open with $store.dsModals.open('ds-countdown')),
|
|
611
|
+
open_expr: "$store.dsModals.open('ds-countdown')" } do %>
|
|
612
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
613
|
+
<span class="block h-2 w-24 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
614
|
+
<div class="w-full rounded-full overflow-hidden" style="height: 6px; background: rgba(128,128,128,0.2)">
|
|
615
|
+
<div style="height: 100%; width: 60%; border-radius: 9999px; background: var(--color-cta)"></div>
|
|
616
|
+
</div>
|
|
617
|
+
<span class="block h-1.5 w-16 mx-auto rounded" style="background: var(--color-text); opacity: .12"></span>
|
|
618
|
+
</div>
|
|
619
|
+
<% end %>
|
|
620
|
+
</div>
|
|
621
|
+
</section>
|
|
622
|
+
|
|
623
|
+
<%# ===================================================================== %>
|
|
624
|
+
<%# 5. TEMPLATES (copy-from archetypes) %>
|
|
625
|
+
<%# ===================================================================== %>
|
|
626
|
+
<section class="space-y-5">
|
|
627
|
+
<div class="space-y-1">
|
|
628
|
+
<h3 class="text-xl font-bold text-heading">Templates</h3>
|
|
629
|
+
<p class="text-muted text-sm">
|
|
630
|
+
Reusable archetypes under <code class="font-mono text-2xs">studio/modals/templates/</code> —
|
|
631
|
+
the copy-from reference cards for a new modal (wizard, form, action,
|
|
632
|
+
status, success).
|
|
633
|
+
</p>
|
|
634
|
+
</div>
|
|
635
|
+
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-5">
|
|
636
|
+
<% [
|
|
637
|
+
["template-wizard", "Wizard", "🧭", "studio/modals/templates/_wizard"],
|
|
638
|
+
["template-form", "Form", "📝", "studio/modals/templates/_form"],
|
|
639
|
+
["template-action", "Action", "❓", "studio/modals/templates/_action"],
|
|
640
|
+
["template-status", "Status", "⏳", "studio/modals/templates/_status"],
|
|
641
|
+
["template-success", "Success", "✅", "studio/modals/templates/_success"]
|
|
642
|
+
].each do |id, label, glyph, partial| %>
|
|
643
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
644
|
+
label: label,
|
|
645
|
+
reference: %(the "#{label}" modal template (#{partial}) — open with $store.dsModals.open('#{id}')),
|
|
646
|
+
open_expr: "$store.dsModals.open('#{id}')" } do %>
|
|
647
|
+
<div class="pointer-events-none w-32 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
648
|
+
<div class="text-2xl leading-none"><%= glyph %></div>
|
|
649
|
+
<span class="block h-2 w-20 mx-auto rounded" style="background: var(--color-text); opacity: .18"></span>
|
|
650
|
+
<span class="block h-4 w-full rounded" style="background: var(--color-cta); opacity: .6"></span>
|
|
651
|
+
</div>
|
|
652
|
+
<% end %>
|
|
653
|
+
<% end %>
|
|
654
|
+
</div>
|
|
655
|
+
</section>
|
|
656
|
+
|
|
657
|
+
<%# ===================================================================== %>
|
|
658
|
+
<%# 6. REWARDS / leveling (gated by Studio.feature?(:leveling)) %>
|
|
659
|
+
<%# ===================================================================== %>
|
|
660
|
+
<section class="space-y-5">
|
|
661
|
+
<div class="space-y-1">
|
|
662
|
+
<div class="flex flex-wrap items-center gap-3">
|
|
663
|
+
<h3 class="text-xl font-bold text-heading">Rewards</h3>
|
|
664
|
+
<% if leveling_on %>
|
|
665
|
+
<span class="badge" style="color: var(--color-success); border-color: var(--color-success)">enabled on this app</span>
|
|
666
|
+
<% else %>
|
|
667
|
+
<span class="badge" style="color: var(--color-warning); border-color: var(--color-warning)">disabled on this app</span>
|
|
668
|
+
<% end %>
|
|
669
|
+
</div>
|
|
670
|
+
<p class="text-muted text-sm">
|
|
671
|
+
The level-up celebration, gated by <code class="font-mono text-2xs">Studio.feature?(:leveling)</code>.
|
|
672
|
+
Leveling is <strong><%= leveling_on ? "on" : "off" %></strong> here, so this renders
|
|
673
|
+
<%= leveling_on ? "live." : "greyed and badged — but STILL openable as a preview." %>
|
|
674
|
+
</p>
|
|
675
|
+
</div>
|
|
676
|
+
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
677
|
+
<%= render layout: "style/modal_specimen", locals: {
|
|
678
|
+
label: "Level up",
|
|
679
|
+
reference: %(the Rewards "Level up" celebration (studio/modals/blocks/_success_card with the sheen + studio-glow badge) — open with $store.dsModals.open('levelup')),
|
|
680
|
+
open_expr: "$store.dsModals.open('levelup')",
|
|
681
|
+
disabled: !leveling_on, openable: true } do %>
|
|
682
|
+
<div class="pointer-events-none w-40 rounded-lg bg-surface border border-subtle shadow p-4 text-center space-y-2">
|
|
683
|
+
<div class="text-2xl leading-none">🎉</div>
|
|
684
|
+
<span class="inline-flex h-8 w-8 items-center justify-center rounded-full text-white text-sm font-extrabold" style="background: var(--color-cta)">10</span>
|
|
685
|
+
<span class="block h-4 w-full rounded" style="background: var(--color-cta)"></span>
|
|
686
|
+
</div>
|
|
687
|
+
<% end %>
|
|
688
|
+
</div>
|
|
689
|
+
</section>
|
|
690
|
+
</section>
|