studio-engine 0.16.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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +90 -0
  3. data/app/assets/tailwind/studio_engine/engine-motion.css +620 -5
  4. data/app/controllers/style_controller.rb +25 -0
  5. data/app/views/studio/modals/_crop_photo.html.erb +5 -1
  6. data/app/views/studio/modals/_host.html.erb +5 -0
  7. data/app/views/studio/modals/_image_upload.html.erb +16 -5
  8. data/app/views/studio/modals/_saving.html.erb +6 -2
  9. data/app/views/studio/modals/auth/_resend_footer.html.erb +25 -0
  10. data/app/views/studio/modals/blocks/_card_header.html.erb +83 -0
  11. data/app/views/studio/modals/blocks/_cta_redirect.html.erb +73 -0
  12. data/app/views/studio/modals/blocks/_onchain_success.html.erb +52 -0
  13. data/app/views/studio/modals/blocks/_progress_pill.html.erb +23 -0
  14. data/app/views/studio/modals/blocks/_shell.html.erb +35 -0
  15. data/app/views/studio/modals/blocks/_solana_tx_link.html.erb +42 -0
  16. data/app/views/studio/modals/shared/_age_attestation.html.erb +34 -0
  17. data/app/views/studio/modals/shared/_email_field.html.erb +70 -0
  18. data/app/views/studio/modals/templates/_action.html.erb +25 -0
  19. data/app/views/studio/modals/templates/_form.html.erb +25 -0
  20. data/app/views/studio/modals/templates/_status.html.erb +45 -0
  21. data/app/views/studio/modals/templates/_success.html.erb +23 -0
  22. data/app/views/studio/modals/templates/_wizard.html.erb +75 -0
  23. data/app/views/style/_modal_specimen.html.erb +120 -0
  24. data/app/views/style/_modals.html.erb +690 -0
  25. data/app/views/style/_specimen.html.erb +64 -0
  26. data/app/views/style/_tasks.html.erb +112 -0
  27. data/app/views/style/_theme.html.erb +222 -0
  28. data/app/views/style/_tricks.html.erb +323 -0
  29. data/app/views/style/index.html.erb +60 -0
  30. data/app/views/style/modals/_auth.html.erb +252 -0
  31. data/app/views/style/modals/_onchain_tx.html.erb +62 -0
  32. data/app/views/style/modals/_wallet_connect.html.erb +138 -0
  33. data/app/views/style/modals/_wallet_deposit.html.erb +56 -0
  34. data/lib/studio/version.rb +1 -1
  35. data/lib/studio.rb +27 -0
  36. metadata +32 -3
@@ -0,0 +1,62 @@
1
+ <%#
2
+ On-chain transaction modal — design-system port of turf-monster's
3
+ modals/_onchain_tx, wired to the living style guide's page-scoped host. Reads
4
+ display state from $store.dsSolanaModal (a stateless proxy over $store.dsModals
5
+ that keeps the show/success/error API stable — the same façade shape the real
6
+ app ships). The proxy's methods push/pop the dsModals stack so this partial
7
+ renders when opened.
8
+
9
+ Three states — processing / success / error — cover the whole non-domain-
10
+ specific tx flow. The DS preview always uses the GENERIC success card
11
+ (studio/modals/blocks/_onchain_success); the rich contest-entry celebration
12
+ (seeds bar + level-up + lobby drain) is turf-specific and stays in the
13
+ consumer app.
14
+
15
+ Single root: content lives inside one <div> so Alpine's <template x-if> clones
16
+ it whole.
17
+ %>
18
+ <div x-data="{
19
+ init() {
20
+ this.$watch('$store.dsSolanaModal.state', function(val) {
21
+ if (val === 'success') {
22
+ setTimeout(function() {
23
+ try { if (window.fireSuccessConfetti) window.fireSuccessConfetti(); } catch (_) {}
24
+ }, 100);
25
+ }
26
+ });
27
+ }
28
+ }">
29
+
30
+ <%# === PROCESSING ====================================================== %>
31
+ <template x-if="$store.dsSolanaModal.state === 'processing'">
32
+ <%= render "studio/modals/blocks/card_header",
33
+ spinner: true,
34
+ title_key: "$store.dsSolanaModal.title",
35
+ subtitle_key: "$store.dsSolanaModal.message" %>
36
+ </template>
37
+
38
+ <%# === SUCCESS (generic) =============================================== %>
39
+ <template x-if="$store.dsSolanaModal.state === 'success'">
40
+ <%= render "studio/modals/blocks/onchain_success",
41
+ tx_signature_key: "$store.dsSolanaModal.txSignature",
42
+ title_key: "$store.dsSolanaModal.successTitle",
43
+ subtitle_key: "$store.dsSolanaModal.successSubtitle",
44
+ cta_label_key: "$store.dsSolanaModal.ctaLabel",
45
+ cta_href_key: "$store.dsSolanaModal.ctaHref",
46
+ modal_store: "dsModals" %>
47
+ </template>
48
+
49
+ <%# === ERROR =========================================================== %>
50
+ <template x-if="$store.dsSolanaModal.state === 'error'">
51
+ <div>
52
+ <%= render "studio/modals/blocks/card_header",
53
+ icon: :error,
54
+ title_key: "$store.dsSolanaModal.title",
55
+ subtitle_key: "$store.dsSolanaModal.errorMessage" %>
56
+ <button @click="$store.dsSolanaModal.close()"
57
+ class="block mx-auto mt-3 text-sm text-secondary hover:text-heading transition">
58
+ Close
59
+ </button>
60
+ </div>
61
+ </template>
62
+ </div>
@@ -0,0 +1,138 @@
1
+ <%#
2
+ "Connect Wallet" picker — design-system port of turf-monster's
3
+ modals/_wallet_connect, wired to the living style guide's page-scoped host
4
+ ($store.dsModals). Faithful reown-style picker: detected wallets on top
5
+ (click to connect), featured-but-missing wallets below (install links), a
6
+ mobile deep-link row, and the legal-age attestation for guests.
7
+
8
+ Self-contained: window.walletProvider (a page stub) supplies the detected
9
+ wallet list; pick() runs a brief connecting state then closes (no real
10
+ connect). Opened via open('wallet-connect', {...}) or swap()ed to from auth.
11
+
12
+ Single root: the outer <div> is the host's required root.
13
+ %>
14
+ <div x-data="{
15
+ get props() { var c = Alpine.store('dsModals').current(); return (c && c.props) || {}; },
16
+ wallets: [],
17
+ installs: [
18
+ { name: 'Phantom', url: 'https://phantom.app/download', icon: '/wallet-phantom.png' },
19
+ { name: 'Solflare', url: 'https://solflare.com/download', icon: '/wallet-solflare.png' },
20
+ { name: 'Backpack', url: 'https://backpack.app/downloads', icon: '/wallet-backpack.png' }
21
+ ],
22
+ connecting: false,
23
+ picking: '',
24
+ error: '',
25
+ ageAttested: false,
26
+ ageError: false,
27
+ get needsAttestation() { return !this.props.linkMode; },
28
+ attested() {
29
+ if (!this.needsAttestation) return true;
30
+ if (this.ageAttested) return true;
31
+ this.ageError = true;
32
+ return false;
33
+ },
34
+ init() {
35
+ this.ageAttested = this.props.ageAttested === true;
36
+ this.refresh();
37
+ },
38
+ refresh() {
39
+ this.wallets = (window.walletProvider && window.walletProvider.available && window.walletProvider.available()) || [];
40
+ },
41
+ get isMobile() { return !!(window.walletProvider && window.walletProvider.isMobile && window.walletProvider.isMobile()); },
42
+ hasWallet(name) {
43
+ var n = ('' + name).toLowerCase();
44
+ return (this.wallets || []).some(function(w) { return w.name && w.name.toLowerCase() === n; });
45
+ },
46
+ get missingInstalls() {
47
+ var self = this;
48
+ return this.installs.filter(function(i) { return !self.hasWallet(i.name); });
49
+ },
50
+ pick(name) {
51
+ if (this.connecting) return;
52
+ if (!this.attested()) return;
53
+ this.connecting = true; this.picking = name; this.error = '';
54
+ var self = this;
55
+ setTimeout(function () { self.connecting = false; self.picking = ''; Alpine.store('dsModals').close(); }, 1100);
56
+ },
57
+ deepLink() {
58
+ if (!this.attested()) return;
59
+ this.pick('Phantom');
60
+ },
61
+ back() {
62
+ if (this.connecting) return;
63
+ if (this.props.backTo === 'auth') {
64
+ Alpine.store('dsModals').swap('auth',
65
+ { step: 'credentials', submitting: null, formError: '' },
66
+ { direction: 'back' });
67
+ } else {
68
+ Alpine.store('dsModals').close();
69
+ }
70
+ }
71
+ }"
72
+ class="relative">
73
+
74
+ <div class="relative mb-4">
75
+ <h3 class="text-heading font-bold text-lg text-center pt-1">Connect Wallet</h3>
76
+ <button @click="$store.dsModals.close()"
77
+ class="absolute top-0 right-0 -mr-1 text-secondary hover:text-heading text-xl leading-none"
78
+ aria-label="Close">&times;</button>
79
+ </div>
80
+
81
+ <template x-if="needsAttestation">
82
+ <div class="mb-3">
83
+ <%= render "studio/modals/shared/age_attestation" %>
84
+ </div>
85
+ </template>
86
+
87
+ <div class="space-y-2">
88
+ <%# Detected wallets — click to connect. %>
89
+ <template x-for="w in wallets" :key="w.name">
90
+ <button type="button" @click="pick(w.name)" :disabled="connecting"
91
+ class="w-full flex items-center gap-3 p-3 rounded-xl bg-surface-alt border border-strong hover:bg-surface transition text-left disabled:opacity-60 disabled:cursor-wait">
92
+ <span class="w-9 h-9 rounded-lg bg-inset flex items-center justify-center text-sm font-bold text-heading" x-text="w.name.slice(0,1)"></span>
93
+ <span class="font-semibold text-heading" x-text="w.name"></span>
94
+ <span class="ml-auto flex items-center gap-2">
95
+ <span x-show="picking === w.name" class="inline-flex items-center gap-1.5 text-xs text-secondary"><span class="spinner" aria-hidden="true"></span>Connecting…</span>
96
+ <span x-show="picking !== w.name" class="badge border-primary text-primary">Installed</span>
97
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 text-muted">
98
+ <path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
99
+ </svg>
100
+ </span>
101
+ </button>
102
+ </template>
103
+
104
+ <%# Featured wallets that aren't installed — open the install page. %>
105
+ <template x-for="i in missingInstalls" :key="i.name">
106
+ <a :href="i.url" target="_blank" rel="noopener noreferrer"
107
+ class="w-full flex items-center gap-3 p-3 rounded-xl bg-surface-alt border border-strong hover:bg-surface transition no-underline">
108
+ <span class="w-9 h-9 rounded-lg bg-inset flex items-center justify-center text-sm font-bold text-muted" x-text="i.name.slice(0,1)"></span>
109
+ <span class="font-semibold text-heading" x-text="i.name"></span>
110
+ <span class="ml-auto flex items-center gap-2">
111
+ <span class="text-xs text-muted uppercase tracking-wide">Install</span>
112
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 text-muted">
113
+ <path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
114
+ </svg>
115
+ </span>
116
+ </a>
117
+ </template>
118
+
119
+ <%# Mobile (no extension) — deep-link into the Phantom app. %>
120
+ <button x-show="isMobile" type="button" @click="deepLink()"
121
+ class="w-full flex items-center gap-3 p-3 rounded-xl bg-surface-alt border border-strong hover:bg-surface transition text-left">
122
+ <span class="w-9 h-9 rounded-lg bg-inset flex items-center justify-center text-sm font-bold text-heading">P</span>
123
+ <span class="font-semibold text-heading">Open in Phantom app</span>
124
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="ml-auto w-4 h-4 text-muted">
125
+ <path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
126
+ </svg>
127
+ </button>
128
+ </div>
129
+
130
+ <template x-if="error">
131
+ <p class="text-red-400 text-sm mt-3 text-center" x-text="error"></p>
132
+ </template>
133
+
134
+ <button type="button" @click="back()" :disabled="connecting"
135
+ class="mt-4 w-full text-center text-sm text-secondary hover:text-heading disabled:opacity-50">
136
+ &larr; Back
137
+ </button>
138
+ </div>
@@ -0,0 +1,56 @@
1
+ <%#
2
+ Wallet deposit / top-up modal — design-system port of turf-monster's
3
+ modals/_wallet_deposit, wired to the living style guide's page-scoped host
4
+ ($store.dsModals). Single-card design (no step machinery): the shortfall
5
+ math, a "Pay with Card" CTA, and a copy-your-address self-fund row.
6
+
7
+ Self-contained: the production Stripe form + CDP onramp button are replaced
8
+ with a neutral demo CTA, and the wallet address reads from props (a demo
9
+ value) rather than $store.session. Opened via
10
+ open('wallet-deposit', { neededCents, usdcCents, usdtCents, address }).
11
+
12
+ Single root: the outer <div> is the host's required root.
13
+ %>
14
+ <div x-data="{ get props() { return (Alpine.store('dsModals').current() || {}).props || {} } }"
15
+ class="relative">
16
+
17
+ <button @click="$store.dsModals.close()"
18
+ class="absolute top-0 right-0 -mt-2 -mr-2 text-secondary hover:text-heading text-xl leading-none z-10"
19
+ aria-label="Close">&times;</button>
20
+
21
+ <div class="text-center pt-1 mb-3">
22
+ <h3 class="text-heading font-bold text-lg leading-tight">Top Up Your Wallet</h3>
23
+ </div>
24
+ <p class="text-xs text-secondary mb-4 text-center">
25
+ You need $<span class="font-mono" x-text="((props.neededCents || 0) / 100).toFixed(2)"></span>
26
+ to enter.
27
+ You have $<span class="font-mono" x-text="((props.usdcCents || 0) / 100).toFixed(2)"></span> USDC
28
+ + $<span class="font-mono" x-text="((props.usdtCents || 0) / 100).toFixed(2)"></span> USDT.
29
+ </p>
30
+
31
+ <div class="space-y-3">
32
+ <%# Demo pay CTA — the production modal posts to Stripe checkout in a new
33
+ tab; here it just closes (no real payment). %>
34
+ <button type="button" @click="$store.dsModals.close()" class="btn btn-primary btn-lg w-full">
35
+ Pay with Card
36
+ </button>
37
+
38
+ <%# Send-to-address — self-fund from another wallet. Address from props. %>
39
+ <div class="border-t border-strong pt-3" x-data="{ copied: false }">
40
+ <p class="text-xs text-secondary text-center mb-2">Or send USDC/USDT to your wallet:</p>
41
+ <div class="flex items-center gap-2">
42
+ <code class="flex-1 px-2 py-1 bg-surface rounded text-xs font-mono truncate"
43
+ x-text="props.address || 'Fo1L5…demo…9xQ2'"></code>
44
+ <button type="button"
45
+ @click="navigator.clipboard && navigator.clipboard.writeText(props.address || ''); copied = true; setTimeout(() => copied = false, 1500)"
46
+ class="btn btn-sm">
47
+ <span x-text="copied ? 'Copied' : 'Copy'"></span>
48
+ </button>
49
+ </div>
50
+ </div>
51
+ </div>
52
+
53
+ <p class="text-center text-xs text-muted mt-4">
54
+ Come back here once your deposit lands &mdash; we'll refresh the balance.
55
+ </p>
56
+ </div>
@@ -1,3 +1,3 @@
1
1
  module Studio
2
- VERSION = "0.16.0"
2
+ VERSION = "0.18.0"
3
3
  end
data/lib/studio.rb CHANGED
@@ -33,6 +33,20 @@ module Studio
33
33
  # contract check — see validate_user_contract!).
34
34
  mattr_accessor :auth_methods, default: %i[magic_link google wallet]
35
35
 
36
+ # ---- Capability features --------------------------------------------------
37
+ # Coarse app-level capability switches an app opts into. Distinct from
38
+ # auth_methods (which sign-in methods the login views render) — these gate
39
+ # whole product surfaces (e.g. :web3 on-chain features, :leveling XP/levels).
40
+ # Default [] means every capability is OFF; an app turns on what it ships in
41
+ # config/initializers/studio.rb:
42
+ #
43
+ # Studio.configure { |config| config.features = %i[leveling web3] }
44
+ #
45
+ # Gate a surface with Studio.feature?(:leveling). The admin/style page uses this
46
+ # to render capability-gated specimens "disabled but present" on apps with the
47
+ # feature off (e.g. McRitchie Studio, which ships neither).
48
+ mattr_accessor :features, default: []
49
+
36
50
  # Magic-link (passwordless email) tuning. token_name keys the MessageVerifier
37
51
  # purpose; bump it to invalidate every outstanding link. See MagicLink service.
38
52
  mattr_accessor :magic_link_ttl, default: 15.minutes
@@ -152,6 +166,14 @@ module Studio
152
166
  auth_methods.include?(method.to_sym)
153
167
  end
154
168
 
169
+ # True when the given capability feature is enabled for this app. Mirrors
170
+ # auth_method? — apps opt in via config.features in their initializer (see the
171
+ # Studio.features accessor). Tolerates String or Symbol entries and any
172
+ # Enumerable (Array or Set), so `feature?("web3")` and `feature?(:web3)` agree.
173
+ def self.feature?(name)
174
+ features.any? { |f| f.to_sym == name.to_sym }
175
+ end
176
+
155
177
  # True when the engine login should render a PASSWORD field/form: passwords are enabled
156
178
  # (`:password` in auth_methods) AND the host User model actually supports them (responds to
157
179
  # `authenticate` — i.e. `has_secure_password`). Both are required, and the second is the
@@ -324,6 +346,11 @@ module Studio
324
346
  patch "admin/theme", to: "theme_settings#update", as: :admin_theme_update
325
347
  post "admin/theme/regenerate", to: "theme_settings#regenerate", as: :admin_theme_regenerate
326
348
  get "admin/schema", to: "schema#index", as: :admin_schema
349
+ # The living style guide. Canonical at /admin/style (StyleController#index);
350
+ # /admin/design_system redirects here but KEEPS its admin_design_system_path
351
+ # helper so a shipped host sidebar link on the old helper still resolves.
352
+ get "admin/style", to: "style#index", as: :admin_style
353
+ get "admin/design_system", to: redirect("/admin/style"), as: :admin_design_system
327
354
 
328
355
  # Admin-managed transactional-email banner images (Studio::EmailImage).
329
356
  # index lists each managed email variant + its current banner; update
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: studio-engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.0
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-07-27 00:00:00.000000000 Z
11
12
  dependencies:
12
13
  - !ruby/object:Gem::Dependency
13
14
  name: rails
@@ -190,6 +191,7 @@ files:
190
191
  - app/controllers/studio/links_controller.rb
191
192
  - app/controllers/studio/local_emails_controller.rb
192
193
  - app/controllers/studio/models_controller.rb
194
+ - app/controllers/style_controller.rb
193
195
  - app/controllers/theme_settings_controller.rb
194
196
  - app/helpers/studio/admin_models_table_helper.rb
195
197
  - app/helpers/studio_email_delivery_helper.rb
@@ -258,11 +260,36 @@ files:
258
260
  - app/views/studio/modals/_host.html.erb
259
261
  - app/views/studio/modals/_image_upload.html.erb
260
262
  - app/views/studio/modals/_saving.html.erb
263
+ - app/views/studio/modals/auth/_resend_footer.html.erb
264
+ - app/views/studio/modals/blocks/_card_header.html.erb
265
+ - app/views/studio/modals/blocks/_cta_redirect.html.erb
261
266
  - app/views/studio/modals/blocks/_error_card.html.erb
267
+ - app/views/studio/modals/blocks/_onchain_success.html.erb
262
268
  - app/views/studio/modals/blocks/_processing_card.html.erb
263
269
  - app/views/studio/modals/blocks/_progress_countdown.html.erb
270
+ - app/views/studio/modals/blocks/_progress_pill.html.erb
271
+ - app/views/studio/modals/blocks/_shell.html.erb
272
+ - app/views/studio/modals/blocks/_solana_tx_link.html.erb
264
273
  - app/views/studio/modals/blocks/_success_card.html.erb
274
+ - app/views/studio/modals/shared/_age_attestation.html.erb
275
+ - app/views/studio/modals/shared/_email_field.html.erb
276
+ - app/views/studio/modals/templates/_action.html.erb
277
+ - app/views/studio/modals/templates/_form.html.erb
278
+ - app/views/studio/modals/templates/_status.html.erb
279
+ - app/views/studio/modals/templates/_success.html.erb
280
+ - app/views/studio/modals/templates/_wizard.html.erb
265
281
  - app/views/studio/models/show.html.erb
282
+ - app/views/style/_modal_specimen.html.erb
283
+ - app/views/style/_modals.html.erb
284
+ - app/views/style/_specimen.html.erb
285
+ - app/views/style/_tasks.html.erb
286
+ - app/views/style/_theme.html.erb
287
+ - app/views/style/_tricks.html.erb
288
+ - app/views/style/index.html.erb
289
+ - app/views/style/modals/_auth.html.erb
290
+ - app/views/style/modals/_onchain_tx.html.erb
291
+ - app/views/style/modals/_wallet_connect.html.erb
292
+ - app/views/style/modals/_wallet_deposit.html.erb
266
293
  - app/views/theme_settings/edit.html.erb
267
294
  - app/views/user_mailer/magic_link.html.erb
268
295
  - app/views/user_mailer/magic_link.text.erb
@@ -298,6 +325,7 @@ metadata:
298
325
  source_code_uri: https://github.com/amcritchie/studio-engine/tree/main
299
326
  bug_tracker_uri: https://github.com/amcritchie/studio-engine/issues
300
327
  changelog_uri: https://github.com/amcritchie/studio-engine/blob/main/CHANGELOG.md
328
+ post_install_message:
301
329
  rdoc_options: []
302
330
  require_paths:
303
331
  - lib
@@ -312,7 +340,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
312
340
  - !ruby/object:Gem::Version
313
341
  version: '0'
314
342
  requirements: []
315
- rubygems_version: 4.0.9
343
+ rubygems_version: 3.5.22
344
+ signing_key:
316
345
  specification_version: 4
317
346
  summary: Shared Rails engine providing auth, SSO, error logging, theming, and S3-backed
318
347
  image caching