Skip to content

v0.4.5

@amcritchie amcritchie tagged this 23 May 06:50
Adds a shared modal layer in the same shape as v0.4.0's toast system —
consumers render `studio/modals/host` once, open through
`Alpine.store('modals')`, and compose `studio/modals/blocks/_*` for
the inside-the-card content. Backward compatible; pure additive.

The host partial bundles together:
  • body.modal-open scroll-lock CSS
  • bfcache + Turbo Drive snapshot cleanup
  • Alpine.store('modals') registration — stack API:
      open(id, props, opts), close(), closeAll(), isOpen(id), current()
      opts.replace flips top-of-stack for flicker-free wizard transitions
  • window.StudioModals.holdAtLeast(ms) — { then(cb) } that delays
    the callback by the remaining ms so loading states don't flash
    past the user when the operation finishes fast (mirrors the
    _navSpinnerMinMs pattern in _head.html.erb)
  • Host markup — z-[120] backdrop, fade-and-scale transitions,
    escape / click-outside, role="dialog" aria-modal

Consumers wire content partials by id through a block:

    <%= render "studio/modals/host" do %>
      <template x-if="$store.modals.current().id === 'auth'">
        <%= render "modals/auth" %>
      </template>
    <% end %>

Reusable content blocks (`studio/modals/blocks/_*`) — composable
inside any modal content partial:
  _success_card        — icon (check or any emoji) + title + message +
                         optional Solana tx explorer link + primary
                         CTA (href OR dispatch event) + secondary CTA +
                         self-driven auto-redirect countdown (bar +
                         "Xs…" text + window.location.href at zero) +
                         opt-in confetti via window.fireSuccessConfetti
  _error_card          — emoji icon + title + message (static OR Alpine
                         expression key) + retry CTA (reload OR event)
  _processing_card     — spinner (sm/md/lg × primary/success/warning) +
                         title + message, pairs with holdAtLeast()
  _progress_countdown  — standalone bar + text, reads from Alpine
                         expressions so caller drives state

Architecture note in the CHANGELOG: modal CONTENT partials stay in
each consumer app (product-specific flows). Modal HOST and BLOCKS
live in the engine (universal vocabulary). Same split as toasts.

KNOWN LANDMINE captured in the CHANGELOG: a modal content partial
mounted via Alpine's <template x-if> MUST have a single root element.
Sibling <style> blocks or trailing whitespace get silently dropped —
yields an empty modal shell. Bake the style inside the partial's
outer wrapping <div>.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Assets 2
Loading