studio-engine 0.17.0 → 0.19.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +101 -0
  3. data/app/assets/tailwind/studio_engine/engine-motion.css +620 -5
  4. data/app/controllers/concerns/studio/magic_link_issuing.rb +39 -0
  5. data/app/controllers/magic_links_controller.rb +5 -14
  6. data/app/controllers/studio/local_emails_controller.rb +1 -1
  7. data/app/controllers/studio/local_reviews_controller.rb +50 -0
  8. data/app/controllers/style_controller.rb +25 -0
  9. data/app/mailers/user_mailer.rb +6 -9
  10. data/app/views/studio/modals/_crop_photo.html.erb +5 -1
  11. data/app/views/studio/modals/_host.html.erb +5 -0
  12. data/app/views/studio/modals/_image_upload.html.erb +16 -5
  13. data/app/views/studio/modals/_saving.html.erb +6 -2
  14. data/app/views/studio/modals/auth/_resend_footer.html.erb +25 -0
  15. data/app/views/studio/modals/blocks/_card_header.html.erb +83 -0
  16. data/app/views/studio/modals/blocks/_cta_redirect.html.erb +73 -0
  17. data/app/views/studio/modals/blocks/_onchain_success.html.erb +52 -0
  18. data/app/views/studio/modals/blocks/_progress_pill.html.erb +23 -0
  19. data/app/views/studio/modals/blocks/_shell.html.erb +35 -0
  20. data/app/views/studio/modals/blocks/_solana_tx_link.html.erb +42 -0
  21. data/app/views/studio/modals/shared/_age_attestation.html.erb +34 -0
  22. data/app/views/studio/modals/shared/_email_field.html.erb +70 -0
  23. data/app/views/studio/modals/templates/_action.html.erb +25 -0
  24. data/app/views/studio/modals/templates/_form.html.erb +25 -0
  25. data/app/views/studio/modals/templates/_status.html.erb +45 -0
  26. data/app/views/studio/modals/templates/_success.html.erb +23 -0
  27. data/app/views/studio/modals/templates/_wizard.html.erb +75 -0
  28. data/app/views/style/_modal_specimen.html.erb +120 -0
  29. data/app/views/style/_modals.html.erb +690 -0
  30. data/app/views/style/_specimen.html.erb +64 -0
  31. data/app/views/style/_tasks.html.erb +112 -0
  32. data/app/views/style/_theme.html.erb +222 -0
  33. data/app/views/style/_tricks.html.erb +323 -0
  34. data/app/views/style/index.html.erb +60 -0
  35. data/app/views/style/modals/_auth.html.erb +252 -0
  36. data/app/views/style/modals/_onchain_tx.html.erb +62 -0
  37. data/app/views/style/modals/_wallet_connect.html.erb +138 -0
  38. data/app/views/style/modals/_wallet_deposit.html.erb +56 -0
  39. data/lib/studio/version.rb +1 -1
  40. data/lib/studio.rb +43 -1
  41. metadata +29 -4
  42. data/app/controllers/design_system_controller.rb +0 -12
  43. data/app/views/design_system/_specimen.html.erb +0 -27
  44. data/app/views/design_system/index.html.erb +0 -232
@@ -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.17.0"
2
+ VERSION = "0.19.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
@@ -183,6 +205,18 @@ module Studio
183
205
  magic_link_store == :database && draw_link_routes
184
206
  end
185
207
 
208
+ # The floor every developer-desk tool sits on: the local email inbox
209
+ # (Studio::LocalEmailsController) and the local-review mint
210
+ # (Studio::LocalReviewsController). Both hand out sign-in material without
211
+ # authenticating anyone, so both are OFF in production and OFF for any request
212
+ # that did not come from the loopback interface. One spelling, so a tool added
213
+ # later cannot quietly ship a weaker gate. Pass request.local? in.
214
+ def self.local_tool_enabled?(request_local:)
215
+ return false if defined?(Rails) && Rails.respond_to?(:env) && Rails.env.production?
216
+
217
+ !!request_local
218
+ end
219
+
186
220
  def self.local_email_capture?
187
221
  return false if defined?(Rails) && Rails.respond_to?(:env) && Rails.env.production?
188
222
  return !!local_email_capture unless local_email_capture.nil?
@@ -277,8 +311,12 @@ module Studio
277
311
  get "auth/:provider/callback", to: "omniauth_callbacks#create"
278
312
  get "auth/failure", to: "omniauth_callbacks#failure"
279
313
 
314
+ # Developer-desk tools. Drawn outside production, and each controller
315
+ # re-checks Studio.local_tool_enabled? per request (loopback only) — the
316
+ # route being absent is the outer gate, not the only one.
280
317
  unless defined?(Rails) && Rails.env.production?
281
318
  get "_studio/local_emails", to: "studio/local_emails#index", as: :studio_local_emails
319
+ get "_studio/local_review", to: "studio/local_reviews#show", as: :studio_local_review
282
320
  end
283
321
 
284
322
  # Passwordless email (magic link). Helpers: magic_link_request_path (POST
@@ -324,7 +362,11 @@ module Studio
324
362
  patch "admin/theme", to: "theme_settings#update", as: :admin_theme_update
325
363
  post "admin/theme/regenerate", to: "theme_settings#regenerate", as: :admin_theme_regenerate
326
364
  get "admin/schema", to: "schema#index", as: :admin_schema
327
- get "admin/design_system", to: "design_system#index", as: :admin_design_system
365
+ # The living style guide. Canonical at /admin/style (StyleController#index);
366
+ # /admin/design_system redirects here but KEEPS its admin_design_system_path
367
+ # helper so a shipped host sidebar link on the old helper still resolves.
368
+ get "admin/style", to: "style#index", as: :admin_style
369
+ get "admin/design_system", to: redirect("/admin/style"), as: :admin_design_system
328
370
 
329
371
  # Admin-managed transactional-email banner images (Studio::EmailImage).
330
372
  # index lists each managed email variant + its current banner; update
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: studio-engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.0
4
+ version: 0.19.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie
@@ -178,7 +178,7 @@ files:
178
178
  - app/controllers/concerns/studio/error_handling.rb
179
179
  - app/controllers/concerns/studio/impersonation.rb
180
180
  - app/controllers/concerns/studio/link_consumption.rb
181
- - app/controllers/design_system_controller.rb
181
+ - app/controllers/concerns/studio/magic_link_issuing.rb
182
182
  - app/controllers/error_logs_controller.rb
183
183
  - app/controllers/magic_links_controller.rb
184
184
  - app/controllers/navbar_controller.rb
@@ -190,7 +190,9 @@ files:
190
190
  - app/controllers/studio/email_images_controller.rb
191
191
  - app/controllers/studio/links_controller.rb
192
192
  - app/controllers/studio/local_emails_controller.rb
193
+ - app/controllers/studio/local_reviews_controller.rb
193
194
  - app/controllers/studio/models_controller.rb
195
+ - app/controllers/style_controller.rb
194
196
  - app/controllers/theme_settings_controller.rb
195
197
  - app/helpers/studio/admin_models_table_helper.rb
196
198
  - app/helpers/studio_email_delivery_helper.rb
@@ -228,8 +230,6 @@ files:
228
230
  - app/views/components/_theme_toggle.html.erb
229
231
  - app/views/components/_theme_toggle_morph.html.erb
230
232
  - app/views/components/_user_nav.html.erb
231
- - app/views/design_system/_specimen.html.erb
232
- - app/views/design_system/index.html.erb
233
233
  - app/views/error_logs/index.html.erb
234
234
  - app/views/error_logs/show.html.erb
235
235
  - app/views/layouts/_navbar.html.erb
@@ -261,11 +261,36 @@ files:
261
261
  - app/views/studio/modals/_host.html.erb
262
262
  - app/views/studio/modals/_image_upload.html.erb
263
263
  - app/views/studio/modals/_saving.html.erb
264
+ - app/views/studio/modals/auth/_resend_footer.html.erb
265
+ - app/views/studio/modals/blocks/_card_header.html.erb
266
+ - app/views/studio/modals/blocks/_cta_redirect.html.erb
264
267
  - app/views/studio/modals/blocks/_error_card.html.erb
268
+ - app/views/studio/modals/blocks/_onchain_success.html.erb
265
269
  - app/views/studio/modals/blocks/_processing_card.html.erb
266
270
  - app/views/studio/modals/blocks/_progress_countdown.html.erb
271
+ - app/views/studio/modals/blocks/_progress_pill.html.erb
272
+ - app/views/studio/modals/blocks/_shell.html.erb
273
+ - app/views/studio/modals/blocks/_solana_tx_link.html.erb
267
274
  - app/views/studio/modals/blocks/_success_card.html.erb
275
+ - app/views/studio/modals/shared/_age_attestation.html.erb
276
+ - app/views/studio/modals/shared/_email_field.html.erb
277
+ - app/views/studio/modals/templates/_action.html.erb
278
+ - app/views/studio/modals/templates/_form.html.erb
279
+ - app/views/studio/modals/templates/_status.html.erb
280
+ - app/views/studio/modals/templates/_success.html.erb
281
+ - app/views/studio/modals/templates/_wizard.html.erb
268
282
  - app/views/studio/models/show.html.erb
283
+ - app/views/style/_modal_specimen.html.erb
284
+ - app/views/style/_modals.html.erb
285
+ - app/views/style/_specimen.html.erb
286
+ - app/views/style/_tasks.html.erb
287
+ - app/views/style/_theme.html.erb
288
+ - app/views/style/_tricks.html.erb
289
+ - app/views/style/index.html.erb
290
+ - app/views/style/modals/_auth.html.erb
291
+ - app/views/style/modals/_onchain_tx.html.erb
292
+ - app/views/style/modals/_wallet_connect.html.erb
293
+ - app/views/style/modals/_wallet_deposit.html.erb
269
294
  - app/views/theme_settings/edit.html.erb
270
295
  - app/views/user_mailer/magic_link.html.erb
271
296
  - app/views/user_mailer/magic_link.text.erb
@@ -1,12 +0,0 @@
1
- # The living style guide — a single admin-gated page that renders the engine's
2
- # shared UI primitives (buttons, surfaces, the seven motion primitives, and the
3
- # theme role tokens) live in the host app's theme. Mirrors ThemeSettingsController:
4
- # a plain host-inherited controller, gated by `require_admin` from the already-
5
- # included Studio::ErrorHandling concern. The view is a bare content wrapper that
6
- # renders inside each host's application.html.erb, so it inherits that app's
7
- # navbar + theme automatically.
8
- class DesignSystemController < ApplicationController
9
- before_action :require_admin
10
-
11
- def index; end
12
- end
@@ -1,27 +0,0 @@
1
- <%# Specimen card for the living style guide (admin/design_system).
2
- Reusable chrome around ONE primitive:
3
- stage - the rendered primitive (yielded from the caller block)
4
- meta - its class name and a copyable usage snippet
5
- locals: klass (String, required) and usage (String, optional).
6
- Rendered via `render layout: "design_system/specimen"` so the caller's
7
- block becomes the staged primitive. Checks local_assigns (never
8
- block_given?) so a shared partial does not inherit the host layout yield. %>
9
- <article class="card overflow-hidden flex flex-col">
10
- <div class="flex flex-1 flex-wrap items-center justify-center gap-4 p-6 bg-surface-alt min-h-32">
11
- <%= yield %>
12
- </div>
13
- <div class="border-t border-subtle p-3 space-y-2">
14
- <code class="block font-mono text-2xs text-heading break-all"><%= local_assigns[:klass] %></code>
15
- <% snippet = local_assigns[:usage] %>
16
- <% if snippet.present? %>
17
- <div class="relative" x-data="{ copied: false }">
18
- <pre class="bg-inset text-body text-2xs rounded-lg px-3 py-2 pr-16 overflow-x-auto"><code x-ref="snippet"><%= snippet %></code></pre>
19
- <button type="button"
20
- class="btn btn-neutral btn-sm absolute top-2 right-2"
21
- @click="navigator.clipboard?.writeText($refs.snippet.innerText); copied = true; setTimeout(() => copied = false, 1200)">
22
- <span x-text="copied ? 'Copied' : 'Copy'">Copy</span>
23
- </button>
24
- </div>
25
- <% end %>
26
- </div>
27
- </article>