studio-engine 0.97.0 → 0.98.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 (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +89 -0
  3. data/app/javascript/studio/alpine_scopes.js +66 -0
  4. data/app/javascript/studio/alpine_shims.js +4 -0
  5. data/app/javascript/studio/application.js +4 -1
  6. data/app/javascript/studio/birthday.js +351 -0
  7. data/app/javascript/studio/confirm_interstitial.js +44 -0
  8. data/app/javascript/studio/controllers/identity_mini_controller.js +32 -0
  9. data/app/javascript/studio/controllers/style_modals_controller.js +41 -0
  10. data/app/javascript/studio/cropper.js +281 -0
  11. data/app/javascript/studio/identity_mini.js +74 -0
  12. data/app/javascript/studio/image_upload.js +189 -0
  13. data/app/javascript/studio/profile_form.js +88 -0
  14. data/app/javascript/studio/stimulus.js +4 -0
  15. data/app/javascript/studio/style_guide.js +13 -0
  16. data/app/javascript/studio/style_modals.js +214 -0
  17. data/app/views/components/_avatar_cropper.html.erb +3 -38
  18. data/app/views/layouts/studio/_head.html.erb +12 -2
  19. data/app/views/studio/_alpine_scopes_guard.html.erb +178 -0
  20. data/app/views/studio/_birthday_assets.html.erb +6 -240
  21. data/app/views/studio/_confirm_interstitial.html.erb +31 -18
  22. data/app/views/studio/_cropper_assets.html.erb +9 -8
  23. data/app/views/studio/modals/_crop_photo.html.erb +1 -1
  24. data/app/views/studio/modals/_image_upload.html.erb +8 -321
  25. data/app/views/studio/modals/blocks/_birthday.html.erb +2 -3
  26. data/app/views/studio/profiles/_birthday_fields.html.erb +0 -1
  27. data/app/views/studio/profiles/_identity_mini.html.erb +5 -62
  28. data/app/views/studio/profiles/_plain_save.html.erb +11 -0
  29. data/app/views/studio/profiles/edit.html.erb +1 -6
  30. data/app/views/style/_modals.html.erb +36 -420
  31. data/lib/studio/version.rb +1 -1
  32. metadata +14 -3
  33. data/app/views/studio/profiles/_birthday_picker_script.html.erb +0 -146
  34. data/app/views/studio/profiles/_form_script.html.erb +0 -65
@@ -151,6 +151,7 @@
151
151
  to the app-supplied submit_url. Kept on the section (always mounted) so it
152
152
  fires even as the age-gate card closes. %>
153
153
  <section id="modals" class="space-y-10" style="scroll-margin-top: calc(var(--nav-h, 0px) + 5rem)"
154
+ data-studio-controller="style-modals"
154
155
  x-data="{}" @ds-modal-close.window="$store.dsModals.close()"
155
156
  @age-verified.window="$store.dsModals.open('entry-confirmed', { txSignature: null, lobbyUrl: null, seedsEarned: 0 })">
156
157
  <div class="space-y-1">
@@ -166,199 +167,21 @@
166
167
  </p>
167
168
  </div>
168
169
 
169
- <%# ---- Page-scoped modal host (dsModals) --------------------------------
170
- The full store API — open(id, props, opts) LIFO stack, swap() directional
171
- slide, advance(patch, opts) in-modal step change, close / closeAll,
172
- current(), cardClasses() driving the enter/exit animation registry, and
173
- body.modal-open scroll lock. Plus the walletProvider stub — required by
174
- solana-studio's picker — so the wallet specimens run without a real
175
- extension. Defined in a plain
176
- inline script (no importmap dependency) so Alpine's defer finds it. %>
177
- <script>
178
- (function () {
179
- // === Credential stubs — never clobber a real one an app already ships. ==
180
- // postMagicLink and solanaWalletConnect were retired here on 2026-09-09
181
- // with style/modals/_auth, the mirror of turf-monster's sign-in card that
182
- // was their only caller. What remains is what solana-studio's own picker
183
- // requires, which is why walletProvider stays and they did not.
184
- // Connect stub for the wallet picker (solana-studio
185
- // solana_studio/modals/_wallet_connect, configured by
186
- // style/modals/_wallet_connect). Performs NO connect: it
187
- // resolves success after a beat so the picker's connecting state is
188
- // visible and its onConnected hook can continue the demo walk.
189
- window.dsWalletConnectDemo = window.dsWalletConnectDemo || function () {
190
- return new Promise(function (resolve) {
191
- setTimeout(function () { resolve({ success: true }); }, 1100);
192
- });
193
- };
194
- window.walletProvider = window.walletProvider || {
195
- available: function () { return [{ name: 'Phantom' }, { name: 'Solflare' }]; },
196
- isMobile: function () { return false; },
197
- isAvailable: function () { return false; }
198
- };
199
-
200
- // Register the page-scoped dsModals store. Named (not an
201
- // inline alpine:init callback) so the dual-guard at the end can invoke it
202
- // directly on a Turbo visit, when alpine:init will not fire again.
203
- function registerDsModals() {
204
- if (Alpine.store('dsModals')) return;
205
-
206
- // Enter/exit animation registry — keys map to a CSS class (engine-
207
- // motion.css) plus its duration in ms. Open a modal with
208
- // { enterAnim: 'shake' } / { exitAnim: 'slide' } to pick one; omitted
209
- // uses 'pop' (the spring bounce). The directional swap()/advance()
210
- // slide uses the modal-card-swap-* classes regardless of these keys.
211
- var CLOSE_ANIM_MS = 220;
212
- var SWAP_IN_MS = 220;
213
-
214
- // THE LIVE REGISTRY IS window.ModalAnimations, NOT THIS TABLE. This is a
215
- // fallback only, for the case where studio/modal_host, which publishes
216
- // the registry on every page, failed to load.
217
- //
218
- // It used to be a hard-coded local copy, and that made the Enter/Leave
219
- // simulator below a LIAR: the simulator builds its controls from the live
220
- // registry, so a newly registered animation grew a button here while the
221
- // store silently resolved the unknown key back to 'pop'. The control said
222
- // "shake", the card popped, and nothing reported the gap. Reading the
223
- // registry at CALL time is what makes a registered animation actually
224
- // playable on this page.
225
- var ANIM_FALLBACK = {
226
- enter: {
227
- pop: { cls: 'modal-card-mount', ms: 320 },
228
- shake: { cls: 'modal-card-shake-in', ms: 600 },
229
- slide: { cls: 'modal-card-swap-in', ms: 220 }
230
- },
231
- exit: {
232
- pop: { cls: 'modal-card-unmount', ms: 220 },
233
- slide: { cls: 'modal-card-swap-out', ms: 220 }
234
- }
235
- };
236
- // Same late-binding guard, and for the same reason, as the host's own
237
- // modalAnim: never return undefined. A miss would throw on .ms in close()
238
- // and strand the modal open, so an unknown key falls back to 'pop'.
239
- function modalAnim(channel, key) {
240
- var table = (window.ModalAnimations && window.ModalAnimations[channel]) ||
241
- ANIM_FALLBACK[channel];
242
- return table[key] || table.pop || ANIM_FALLBACK[channel].pop;
243
- }
244
-
245
- Alpine.store('dsModals', {
246
- stack: [],
247
- current: function () { return this.stack.length ? this.stack[this.stack.length - 1] : null; },
248
- isOpen: function (id) {
249
- for (var i = 0; i < this.stack.length; i++) { if (this.stack[i].id === id) return true; }
250
- return false;
251
- },
252
- // open(id, props, opts) — opts.replace swaps the top entry with a
253
- // directional slide (forward = exit right / enter left); otherwise
254
- // push (stacking — closing reveals the entry underneath).
255
- open: function (id, props, opts) {
256
- props = props || {}; opts = opts || {};
257
- var self = this;
258
- if (opts.replace && this.stack.length > 0) {
259
- var current = this.current();
260
- if (current && !current._closing) {
261
- var dir = (opts.direction === 'back') ? 'back' : 'forward';
262
- current._swapDir = dir;
263
- current._swappingOut = true;
264
- current._closing = true;
265
- setTimeout(function () {
266
- var idx = self.stack.indexOf(current);
267
- var newEntry = { id: id, props: props, _swappingIn: true, _swapDir: dir };
268
- if (idx >= 0) { self.stack[idx] = newEntry; } else { self.stack.push(newEntry); }
269
- self._sync();
270
- setTimeout(function () {
271
- newEntry._swappingIn = false; newEntry._swapDir = null; newEntry._settled = true;
272
- }, SWAP_IN_MS);
273
- }, CLOSE_ANIM_MS);
274
- return;
275
- }
276
- this.stack[this.stack.length - 1] = { id: id, props: props };
277
- } else {
278
- this.stack.push({ id: id, props: props });
279
- }
280
- this._sync();
281
- },
282
- // swap(id, props, opts) ≡ open with { replace: true }.
283
- swap: function (id, props, opts) {
284
- return this.open(id, props, Object.assign({ replace: true }, opts || {}));
285
- },
286
- // advance(patch, opts) — in-flow step change: same directional slide as
287
- // swap() but WITHOUT replacing the stack entry, so the modal's outer
288
- // x-data scope survives (the auth step machine relies on this).
289
- advance: function (propsPatch, opts) {
290
- opts = opts || {};
291
- var cur = this.current();
292
- if (!cur || cur._closing || cur._swappingOut) return;
293
- var self = this;
294
- cur._swapDir = (opts.direction === 'back') ? 'back' : 'forward';
295
- cur._swappingOut = true;
296
- setTimeout(function () {
297
- if (self.stack.indexOf(cur) < 0) return;
298
- if (propsPatch) Object.assign(cur.props, propsPatch);
299
- cur._swappingOut = false;
300
- cur._swappingIn = true;
301
- setTimeout(function () {
302
- cur._swappingIn = false; cur._swapDir = null; cur._settled = true;
303
- }, SWAP_IN_MS);
304
- }, CLOSE_ANIM_MS);
305
- },
306
- close: function () {
307
- var self = this;
308
- var entry = this.current();
309
- if (!entry || entry._closing) return;
310
- entry._closing = true;
311
- setTimeout(function () {
312
- var idx = self.stack.indexOf(entry);
313
- if (idx >= 0) { self.stack.splice(idx, 1); self._sync(); }
314
- }, modalAnim('exit', entry.props && entry.props.exitAnim).ms);
315
- },
316
- closeAll: function () { this.stack = []; this._sync(); },
317
- cardClasses: function () {
318
- var c = this.current();
319
- if (!c) return {};
320
- var o = {};
321
- if (!c._settled && !c._swappingIn && !c._swappingOut && !c._closing) {
322
- o[modalAnim('enter', c.props && c.props.enterAnim).cls] = true;
323
- }
324
- if (c._closing && !c._swappingOut) {
325
- o[modalAnim('exit', c.props && c.props.exitAnim).cls] = true;
326
- }
327
- o['modal-card-swap-in'] = !!(c._swappingIn && c._swapDir !== 'back');
328
- o['modal-card-swap-out'] = !!(c._swappingOut && c._swapDir !== 'back');
329
- o['modal-card-swap-in-back'] = !!(c._swappingIn && c._swapDir === 'back');
330
- o['modal-card-swap-out-back'] = !!(c._swappingOut && c._swapDir === 'back');
331
- return o;
332
- },
333
- _sync: function () {
334
- if (this.stack.length) { document.body.classList.add('modal-open'); }
335
- else { document.body.classList.remove('modal-open'); }
336
- }
337
- });
338
-
339
- // A SECOND STORE USED TO BE REGISTERED HERE: dsSolanaModal, a stateless
340
- // proxy that re-read the current 'onchain-tx' entry's props so
341
- // style/modals/_onchain_tx could keep its
342
- // $store.dsSolanaModal.{state,title,txSignature,…} reads. That partial
343
- // was a mirror of turf-monster's card and was retired on 2026-09-09,
344
- // leaving the proxy with no reader, so it went with it. A store with no
345
- // reader still registers, still looks load-bearing, and still has to be
346
- // reasoned about by everyone who reads this file.
347
- }
348
-
349
- // Alpine's deferred CDN <script> fires alpine:init exactly ONCE, on the
350
- // first full-document load. Turbo Drive advance visits swap the document
351
- // body without reloading that head script, so alpine:init never fires
352
- // again — and
353
- // because this dsModals registration lives ONLY in the /admin/style body,
354
- // it was absent during that one alpine:init. Registering here on the
355
- // Turbo-visit path (Alpine already started) keeps $store.dsModals defined,
356
- // so the specimen cards' @click / :style / <template x-if> don't throw.
357
- // Mirrors studio/modals/_image_upload's cropPhotoModal guard.
358
- if (window.Alpine) { registerDsModals(); }
359
- else { document.addEventListener('alpine:init', registerDsModals); }
360
- })();
361
- </script>
170
+ <%# ---- The section's own modal host (dsModals) --------------------------
171
+ $store.dsModals is the engine's modal stack under the guide's own name:
172
+ the overlay below declares itself a modal host, and studio/modal_host
173
+ registers the store from that declaration before Alpine reads it, on a
174
+ full load and on a Turbo visit alike. So the store the specimens open on
175
+ is the store an app runs: open / swap / advance / close / closeAll,
176
+ current(), cardClasses() reading the live animation registry, and the
177
+ body.modal-open scroll lock.
178
+
179
+ The demo drivers, the enter/leave simulator and the walletProvider stub
180
+ solana-studio's picker needs are studio/style_modals, bound to the
181
+ section by the style-modals controller. studio/style_guide registers that
182
+ controller; the tag below is the only page that loads it. %>
183
+ <%= tag.script('import "studio/style_guide"'.html_safe, type: "module",
184
+ nonce: (content_security_policy_nonce if respond_to?(:content_security_policy_nonce))) %>
362
185
 
363
186
  <%# The minimum-visible-duration convention (window.StudioModals.holdAtLeast +
364
187
  MIN_LOAD_MS) is studio/modal_host's, published on every page, so the demo
@@ -366,34 +189,27 @@
366
189
  resolveAt = max(min_duration, actual) and auto-resolve after min_duration
367
190
  in demo mode. %>
368
191
 
369
- <%# Cropper.js + the cropPhotoModal factory — makes the Profile crop / upload
370
- modals openable on this page (loads cropper.js via <script defer> and
371
- defines the factory inline; no importmap change). %>
192
+ <%# Cropper.js, so the Profile crop and upload modals open live on this page.
193
+ The cropPhotoModal, birthdayModal and levelingActionModal factories the
194
+ specimens mount are on every page (studio/alpine_scopes, studio/alpine_shims). %>
372
195
  <%= render "studio/cropper_assets" %>
373
196
 
374
- <%# birthdayModal factory — rendered here at page level (never inside the
375
- overlay template, where a cloned script would not run) so the age-gate
376
- specimen opens live. A consumer app renders this in its layout the same way. %>
377
- <%= render "studio/birthday_assets" %>
378
-
379
- <%# levelingActionModal factory — page level (never inside the overlay template,
380
- where a cloned script would not run) so the change-username + join-newsletter
381
- specimens open live. A consumer app renders this in its layout the same way. %>
382
- <%= render "studio/leveling_activity_assets" %>
383
-
384
197
  <%# ---- The overlay: one backdrop + card; every specimen's real content is a
385
198
  single-root <template x-if> gated on the current modal id. Faithful to the
386
199
  shared host — backdrop fade, scroll lock, escape + click-self dismissal
387
200
  gated on props.dismissible (never @click.outside, so a hold modal can't
388
201
  close itself on the button release). %>
389
- <template x-if="$store.dsModals.current()">
202
+ <template x-if="$store.dsModals.current()"
203
+ data-studio-controller="modal-host"
204
+ data-modal-host-store-value="dsModals">
390
205
  <div class="fixed inset-0 z-[var(--z-modal)] flex items-center justify-center p-4 modal-backdrop-mount"
391
206
  :class="$store.dsModals.current()?._closing && 'modal-backdrop-unmount'"
392
207
  style="background: rgba(0,0,0,0.6)"
393
208
  role="dialog" aria-modal="true"
394
209
  @keydown.escape.window="$store.dsModals.current() && $store.dsModals.current().props.dismissible !== false && $store.dsModals.close()"
395
210
  @click.self="$store.dsModals.current() && $store.dsModals.current().props.dismissible !== false && $store.dsModals.close()">
396
- <div class="bg-surface rounded-xl border border-subtle shadow-2xl p-6 max-w-sm w-full"
211
+ <%# No max-w-* here: cardClasses() names exactly one, by modal id. %>
212
+ <div class="bg-surface rounded-xl border border-subtle shadow-2xl p-6 w-full"
397
213
  :class="$store.dsModals.cardClasses()">
398
214
 
399
215
  <%# --- Auth fragments --- %>
@@ -1565,9 +1381,9 @@
1565
1381
  class="block mt-1 text-sm bg-inset border border-subtle rounded px-2 py-1 text-heading"></select>
1566
1382
  </label>
1567
1383
  <button type="button" class="btn btn-primary btn-sm"
1568
- onclick="dsModalDemos.animOpenSelected()">Open</button>
1384
+ data-studio-action="click->style-modals#demo" data-style-modals-name-param="animOpenSelected">Open</button>
1569
1385
  <button type="button" class="btn btn-outline btn-sm"
1570
- onclick="dsModalDemos.animRoundTripSelected()">Open, then auto-close</button>
1386
+ data-studio-action="click->style-modals#demo" data-style-modals-name-param="animRoundTripSelected">Open, then auto-close</button>
1571
1387
  <button type="button" class="btn btn-outline btn-sm"
1572
1388
  @click="$store.dsModals.close()">Close</button>
1573
1389
  </div>
@@ -1612,15 +1428,15 @@
1612
1428
  </p>
1613
1429
  <div class="flex flex-wrap gap-2">
1614
1430
  <button type="button" class="btn btn-outline btn-sm"
1615
- onclick="dsModalDemos.processing()">Processing</button>
1431
+ data-studio-action="click->style-modals#demo" data-style-modals-name-param="processing">Processing</button>
1616
1432
  <button type="button" class="btn btn-outline btn-sm"
1617
- onclick="dsModalDemos.success()">Success</button>
1433
+ data-studio-action="click->style-modals#demo" data-style-modals-name-param="success">Success</button>
1618
1434
  <button type="button" class="btn btn-outline btn-sm"
1619
- onclick="dsModalDemos.error()">Error</button>
1435
+ data-studio-action="click->style-modals#demo" data-style-modals-name-param="error">Error</button>
1620
1436
  <button type="button" class="btn btn-primary btn-sm"
1621
- onclick="dsModalDemos.processThenSuccess()">Processing to success (3s)</button>
1437
+ data-studio-action="click->style-modals#demo" data-style-modals-name-param="processThenSuccess">Processing to success (3s)</button>
1622
1438
  <button type="button" class="btn btn-primary btn-sm"
1623
- onclick="dsModalDemos.processThenError()">Processing to error (3s)</button>
1439
+ data-studio-action="click->style-modals#demo" data-style-modals-name-param="processThenError">Processing to error (3s)</button>
1624
1440
  </div>
1625
1441
  </div>
1626
1442
 
@@ -1634,9 +1450,9 @@
1634
1450
  </p>
1635
1451
  <div class="flex flex-wrap gap-2">
1636
1452
  <button type="button" class="btn btn-outline btn-sm"
1637
- onclick="dsModalDemos.processing()">Try Escape (locked)</button>
1453
+ data-studio-action="click->style-modals#demo" data-style-modals-name-param="processing">Try Escape (locked)</button>
1638
1454
  <button type="button" class="btn btn-outline btn-sm"
1639
- onclick="dsModalDemos.dismissible()">Dismissible processing</button>
1455
+ data-studio-action="click->style-modals#demo" data-style-modals-name-param="dismissible">Dismissible processing</button>
1640
1456
  </div>
1641
1457
  </div>
1642
1458
 
@@ -1649,9 +1465,9 @@
1649
1465
  </p>
1650
1466
  <div class="flex flex-wrap gap-2">
1651
1467
  <button type="button" class="btn btn-primary btn-sm"
1652
- onclick="dsModalDemos.fastWithHold()">Fast op, hold 1500ms</button>
1468
+ data-studio-action="click->style-modals#demo" data-style-modals-name-param="fastWithHold">Fast op, hold 1500ms</button>
1653
1469
  <button type="button" class="btn btn-outline btn-sm"
1654
- onclick="dsModalDemos.fastNoHold()">Fast op, no hold (flashes)</button>
1470
+ data-studio-action="click->style-modals#demo" data-style-modals-name-param="fastNoHold">Fast op, no hold (flashes)</button>
1655
1471
  </div>
1656
1472
  </div>
1657
1473
 
@@ -1664,7 +1480,7 @@
1664
1480
  </p>
1665
1481
  <div class="flex flex-wrap gap-2">
1666
1482
  <button type="button" class="btn btn-primary btn-sm"
1667
- onclick="dsModalDemos.stackTwo()">Open two</button>
1483
+ data-studio-action="click->style-modals#demo" data-style-modals-name-param="stackTwo">Open two</button>
1668
1484
  <button type="button" class="btn btn-outline btn-sm"
1669
1485
  @click="$store.dsModals.closeAll()">closeAll()</button>
1670
1486
  </div>
@@ -1672,204 +1488,4 @@
1672
1488
  </div>
1673
1489
  </section>
1674
1490
 
1675
- <%# ---- Demo drivers for the two sections above -------------------------
1676
- Deliberately NOT an Alpine component: these are multi-step sequences with
1677
- timers, and an x-data scope would be cloned per specimen card. One global
1678
- namespace, defined after the markup it drives so the control containers
1679
- exist when the registry build runs.
1680
-
1681
- THE STORE IS dsModals. turf-monster's version of this page drove roughly
1682
- half of these through its legacy solana-modal compatibility proxy, a store
1683
- this engine does not ship; every demo here goes through the page-scoped
1684
- store and the ds-stack-demo vehicle directly instead. The exact spelling of
1685
- that legacy store name is deliberately absent from this file, because
1686
- style_guide_modal_simulator_test scans the source for it. %>
1687
- <script>
1688
- window.dsModalDemos = (function () {
1689
- var VEHICLE = 'ds-stack-demo';
1690
-
1691
- function store() { return Alpine.store('dsModals'); }
1692
-
1693
- // Open the stack-mechanics vehicle. The defaults model a real pending
1694
- // operation, INCLUDING dismissible: false — the demos that want an
1695
- // escapable card override it explicitly.
1696
- //
1697
- // THE VEHICLE IS DELIBERATELY GENERIC. These demos ran on the onchain-tx
1698
- // specimen until 2026-09-09, which made every one of them depend on a copy
1699
- // of turf-monster's card; retiring that copy would have taken the whole
1700
- // section with it. style/modals/_ds_stack_demo belongs to this page and to
1701
- // nothing that ships.
1702
- function openDemo(props) {
1703
- store().open(VEHICLE, Object.assign({
1704
- state: 'processing',
1705
- title: 'Submitting entry',
1706
- message: 'Awaiting confirmation',
1707
- dismissible: false
1708
- }, props || {}));
1709
- }
1710
-
1711
- // In-flow state change on the card that is already open. advance() patches
1712
- // props WITHOUT replacing the stack entry, so the vehicle's x-data scope
1713
- // survives the transition. Guarded to a still-open vehicle card: a timer
1714
- // that fires after the user closed it must do nothing rather than patch
1715
- // whatever card happens to be on top now.
1716
- function advanceDemo(patch) {
1717
- var cur = store().current();
1718
- if (!cur || cur.id !== VEHICLE || cur._closing) return;
1719
- store().advance(patch);
1720
- }
1721
-
1722
- var SUCCESS = {
1723
- state: 'success',
1724
- title: 'Entry confirmed',
1725
- message: 'Resolved in place — this is the same stack entry you opened.',
1726
- dismissible: true
1727
- };
1728
- var FAILURE = {
1729
- state: 'error',
1730
- title: 'Submission failed',
1731
- message: 'The error face of that same entry, patched in by advance().',
1732
- dismissible: true
1733
- };
1734
-
1735
- // === Enter / leave simulator =====================================
1736
- // The demo vehicle is the always-registered email-change-pending card, so
1737
- // only the motion varies between runs.
1738
- var ANIM_DEMO_ID = 'email-change-pending';
1739
- var ANIM_DEMO_PROPS = { currentEmail: 'you@example.com', newEmail: 'new@example.com' };
1740
-
1741
- function animOpen(enterAnim, exitAnim) {
1742
- store().open(ANIM_DEMO_ID, Object.assign({}, ANIM_DEMO_PROPS, {
1743
- enterAnim: enterAnim, exitAnim: exitAnim
1744
- }));
1745
- }
1746
-
1747
- // Open, let the entrance settle, then close so the chosen EXIT keyframe is
1748
- // what you watch. The hold is the entrance's own registry duration plus a
1749
- // beat, read from the live registry rather than assumed.
1750
- function animRoundTrip(enterAnim, exitAnim) {
1751
- animOpen(enterAnim, exitAnim);
1752
- var reg = window.ModalAnimations || { enter: {}, exit: {} };
1753
- var enterMs = ((reg.enter[enterAnim] || {}).ms) || 320;
1754
- setTimeout(function () { store().close(); }, enterMs + 700);
1755
- }
1756
-
1757
- function el(id) { return document.getElementById(id); }
1758
-
1759
- // Build the quick buttons and the dropdown options FROM THE LIVE REGISTRY.
1760
- // This is the property the section exists to demonstrate: a newly
1761
- // registered animation key grows a control here with no edit to this file.
1762
- // Idempotent — it clears each container first, so re-running on a Turbo
1763
- // visit cannot double the controls.
1764
- function buildAnimControls() {
1765
- var reg = window.ModalAnimations || { enter: {}, exit: {} };
1766
-
1767
- var label = function (key) {
1768
- return key.charAt(0).toUpperCase() + key.slice(1) + (key === 'pop' ? ' (default)' : '');
1769
- };
1770
- var addOption = function (sel, key) {
1771
- if (!sel) return;
1772
- var o = document.createElement('option');
1773
- o.value = key;
1774
- o.textContent = label(key);
1775
- if (key === 'pop') o.selected = true;
1776
- sel.appendChild(o);
1777
- };
1778
- var addButton = function (container, key, onClick, suffix) {
1779
- if (!container) return;
1780
- var b = document.createElement('button');
1781
- b.type = 'button';
1782
- b.className = 'btn btn-outline btn-sm';
1783
- b.textContent = label(key) + (suffix || '');
1784
- b.onclick = onClick;
1785
- container.appendChild(b);
1786
- };
1787
-
1788
- var enterBtns = el('modal-anim-enter-buttons');
1789
- var exitBtns = el('modal-anim-exit-buttons');
1790
- var enterSel = el('modal-anim-enter-select');
1791
- var exitSel = el('modal-anim-exit-select');
1792
-
1793
- [enterBtns, exitBtns, enterSel, exitSel].forEach(function (node) {
1794
- if (node) node.innerHTML = '';
1795
- });
1796
-
1797
- Object.keys(reg.enter || {}).forEach(function (key) {
1798
- addButton(enterBtns, key, function () { animOpen(key, 'pop'); }, ' ↗');
1799
- addOption(enterSel, key);
1800
- });
1801
- Object.keys(reg.exit || {}).forEach(function (key) {
1802
- addButton(exitBtns, key, function () { animRoundTrip('pop', key); });
1803
- addOption(exitSel, key);
1804
- });
1805
- }
1806
-
1807
- // WHEN TO BUILD. window.ModalAnimations is published by studio/modal_host,
1808
- // a module, which runs after the document has parsed and before
1809
- // DOMContentLoaded, so the build waits for the rest of the load either way:
1810
- // full load — DOMContentLoaded, the module has run by then;
1811
- // Turbo visit — DOMContentLoaded already fired and never fires again, so
1812
- // fall through to a macrotask; the registry is already up.
1813
- // turbo:load covers the remaining visits; the build is idempotent, so the
1814
- // overlap between these paths is redundant work and nothing worse.
1815
- if (document.readyState === 'loading') {
1816
- document.addEventListener('DOMContentLoaded', buildAnimControls);
1817
- } else {
1818
- setTimeout(buildAnimControls, 0);
1819
- }
1820
- if (!window.__dsAnimControlsBound) {
1821
- window.__dsAnimControlsBound = true;
1822
- document.addEventListener('turbo:load', buildAnimControls);
1823
- }
1824
-
1825
- return {
1826
- // Stack behaviour
1827
- processing: function () { openDemo({}); },
1828
- success: function () { openDemo(SUCCESS); },
1829
- error: function () { openDemo(FAILURE); },
1830
- processThenSuccess: function () {
1831
- openDemo({ message: 'Awaiting RPC confirmation' });
1832
- setTimeout(function () { advanceDemo(SUCCESS); }, 3000);
1833
- },
1834
- processThenError: function () {
1835
- openDemo({ message: 'Awaiting RPC confirmation' });
1836
- setTimeout(function () { advanceDemo(FAILURE); }, 3000);
1837
- },
1838
- dismissible: function () {
1839
- openDemo({
1840
- dismissible: true,
1841
- title: 'Dismissible processing',
1842
- message: 'Escape and click-outside both close this one.'
1843
- });
1844
- },
1845
- fastWithHold: function () {
1846
- openDemo({ title: 'Fast operation', message: 'Holding the spinner for at least 1500ms' });
1847
- window.StudioModals.holdAtLeast(1500).then(function () { advanceDemo(SUCCESS); });
1848
- },
1849
- fastNoHold: function () {
1850
- openDemo({ title: 'Fast operation', message: 'No hold, so this flashes past' });
1851
- setTimeout(function () { advanceDemo(SUCCESS); }, 50);
1852
- },
1853
- stackTwo: function () {
1854
- openDemo({
1855
- dismissible: true,
1856
- title: 'Bottom of the stack',
1857
- message: 'A second card is about to push in front of this one.'
1858
- });
1859
- setTimeout(function () {
1860
- store().open(ANIM_DEMO_ID, ANIM_DEMO_PROPS);
1861
- }, 700);
1862
- },
1863
-
1864
- // Enter / leave simulator
1865
- animOpenSelected: function () {
1866
- animOpen(el('modal-anim-enter-select').value, el('modal-anim-exit-select').value);
1867
- },
1868
- animRoundTripSelected: function () {
1869
- animRoundTrip(el('modal-anim-enter-select').value, el('modal-anim-exit-select').value);
1870
- }
1871
- };
1872
- })();
1873
- </script>
1874
-
1875
1491
  </section>
@@ -1,3 +1,3 @@
1
1
  module Studio
2
- VERSION = "0.97.0"
2
+ VERSION = "0.98.0"
3
3
  end
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.97.0
4
+ version: 0.98.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie
@@ -409,23 +409,29 @@ files:
409
409
  - app/helpers/studio_navbar_helper.rb
410
410
  - app/helpers/studio_sidebar_helper.rb
411
411
  - app/helpers/studio_theme_helper.rb
412
+ - app/javascript/studio/alpine_scopes.js
412
413
  - app/javascript/studio/alpine_shims.js
413
414
  - app/javascript/studio/alpine_stores.js
414
415
  - app/javascript/studio/application.js
416
+ - app/javascript/studio/birthday.js
415
417
  - app/javascript/studio/board.js
416
418
  - app/javascript/studio/booking.js
419
+ - app/javascript/studio/confirm_interstitial.js
417
420
  - app/javascript/studio/controllers/board_controller.js
418
421
  - app/javascript/studio/controllers/booking_controller.js
419
422
  - app/javascript/studio/controllers/email_banner_scale_controller.js
420
423
  - app/javascript/studio/controllers/footer_map_controller.js
421
424
  - app/javascript/studio/controllers/geo_settings_controller.js
422
425
  - app/javascript/studio/controllers/hold_button_controller.js
426
+ - app/javascript/studio/controllers/identity_mini_controller.js
423
427
  - app/javascript/studio/controllers/link_preview_card_controller.js
424
428
  - app/javascript/studio/controllers/link_sidebar_controller.js
425
429
  - app/javascript/studio/controllers/modal_host_controller.js
426
430
  - app/javascript/studio/controllers/nav_collapse_controller.js
431
+ - app/javascript/studio/controllers/style_modals_controller.js
427
432
  - app/javascript/studio/controllers/survey_controller.js
428
433
  - app/javascript/studio/controllers/toast_controller.js
434
+ - app/javascript/studio/cropper.js
429
435
  - app/javascript/studio/email_banner.js
430
436
  - app/javascript/studio/email_banner_scale.js
431
437
  - app/javascript/studio/footer_map.js
@@ -433,6 +439,8 @@ files:
433
439
  - app/javascript/studio/head_chrome.js
434
440
  - app/javascript/studio/hold_button.js
435
441
  - app/javascript/studio/hold_button_hooks.js
442
+ - app/javascript/studio/identity_mini.js
443
+ - app/javascript/studio/image_upload.js
436
444
  - app/javascript/studio/lazy_controllers.js
437
445
  - app/javascript/studio/leveling_activity.js
438
446
  - app/javascript/studio/link_preview_card.js
@@ -441,7 +449,10 @@ files:
441
449
  - app/javascript/studio/modal_host.js
442
450
  - app/javascript/studio/nav_collapse.js
443
451
  - app/javascript/studio/pinned_stack.js
452
+ - app/javascript/studio/profile_form.js
444
453
  - app/javascript/studio/stimulus.js
454
+ - app/javascript/studio/style_guide.js
455
+ - app/javascript/studio/style_modals.js
445
456
  - app/javascript/studio/survey.js
446
457
  - app/javascript/studio/theme_editor.js
447
458
  - app/javascript/studio/toast.js
@@ -507,6 +518,7 @@ files:
507
518
  - app/views/sessions/_sso_continue.html.erb
508
519
  - app/views/sessions/new.html.erb
509
520
  - app/views/solana_sessions/phantom_callback.html.erb
521
+ - app/views/studio/_alpine_scopes_guard.html.erb
510
522
  - app/views/studio/_at_time_script.html.erb
511
523
  - app/views/studio/_birthday_assets.html.erb
512
524
  - app/views/studio/_board_assets.html.erb
@@ -601,10 +613,8 @@ files:
601
613
  - app/views/studio/profile_mailer/email_change_notification.html.erb
602
614
  - app/views/studio/profile_mailer/email_change_notification.text.erb
603
615
  - app/views/studio/profiles/_birthday_fields.html.erb
604
- - app/views/studio/profiles/_birthday_picker_script.html.erb
605
616
  - app/views/studio/profiles/_editable_identity.html.erb
606
617
  - app/views/studio/profiles/_email_fields.html.erb
607
- - app/views/studio/profiles/_form_script.html.erb
608
618
  - app/views/studio/profiles/_google_section.html.erb
609
619
  - app/views/studio/profiles/_identity.html.erb
610
620
  - app/views/studio/profiles/_identity_body.html.erb
@@ -614,6 +624,7 @@ files:
614
624
  - app/views/studio/profiles/_newsletter_modals.html.erb
615
625
  - app/views/studio/profiles/_newsletter_section.html.erb
616
626
  - app/views/studio/profiles/_pencil_icon.html.erb
627
+ - app/views/studio/profiles/_plain_save.html.erb
617
628
  - app/views/studio/profiles/_save_controls.html.erb
618
629
  - app/views/studio/profiles/edit.html.erb
619
630
  - app/views/studio/profiles/show.html.erb