{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "dialog", "type": "registry:item", "title": "aesthetic Dialog", "description": "Aesthetic Dialog primitive, v0.25.0. Installs ae-button, ae-button-quiet, ae-dialog, ae-dialog-acts, ae-dialog-backdrop, ae-dialog-panel, ae-dialog-title, ae-dim without the full stylesheet. Recipes: dialog.js.", "registryDependencies": [ "https://aesthetic.mistystep.io/r/aesthetic-base.json" ], "meta": { "version": "0.25.0", "tier": "primitive", "route": "dialog", "classes": [ "ae-button", "ae-button-quiet", "ae-dialog", "ae-dialog-acts", "ae-dialog-backdrop", "ae-dialog-panel", "ae-dialog-title", "ae-dim" ], "attributes": [], "tokens": [ "--ae-surface", "--ae-wash", "--ae-ink", "--ae-ink-faint", "--ae-accent", "--ae-surface-dark", "--ae-wash-dark", "--ae-ink-dark", "--ae-ink-faint-dark", "--ae-accent-dark", "--ae-font", "--ae-font-mono", "--ae-w-regular", "--ae-ease", "--ae-quick", "--ae-ink-muted", "--ae-ink-muted-dark", "--ae-w-medium", "--ae-w-black", "--ae-radius", "--ae-space-2", "--ae-space-4", "--ae-space-6", "--ae-z-backdrop", "--ae-z-dialog", "--ae-soft" ], "recipes": ["dialog.js"] }, "files": [ { "path": "registry/dialog/aesthetic.dialog.css", "type": "registry:file", "target": "~/aesthetic.dialog.css", "content": "/* aesthetic dialog v0.25.0 — generated by scripts/build-registry.mjs. */\n@import './aesthetic.base.css';\n\n:root {\n --ae-ink-muted: #737373;\n --ae-ink-muted-dark: #8f8f8f;\n --ae-w-medium: 550;\n --ae-w-black: 800;\n --ae-radius: 0;\n --ae-space-2: 0.5em;\n --ae-space-4: 1em;\n --ae-space-6: 2em;\n --ae-z-backdrop: 40;\n --ae-z-dialog: 41;\n --ae-soft: 240ms;\n}\n\n@media (prefers-color-scheme: dark) {\n :root:not(.light):not([data-ae-mode='light']) {\n --ae-ink-muted: var(--ae-ink-muted-dark);\n }\n}\n\n.light,\n[data-ae-mode='light'] {\n color-scheme: light;\n}\n\n:root.dark,\n:root[data-ae-mode='dark'] {\n color-scheme: dark;\n --ae-ink-muted: var(--ae-ink-muted-dark);\n}\n\n@media (prefers-color-scheme: dark) {\n :root:not(.light):not([data-ae-mode='light']) dialog.ae-dialog,\n :root:not(.light):not([data-ae-mode='light']) .ae-dialog-panel {\n background: var(--ae-wash);\n box-shadow: none;\n }\n\n :root:not(.light):not([data-ae-mode='light']) dialog.ae-dialog::backdrop,\n :root:not(.light):not([data-ae-mode='light']) .ae-dialog-backdrop {\n background: rgba(18, 18, 18, 0.4);\n }\n}\n\ndialog.ae-dialog {\nborder: 0;\n border-radius: var(--ae-radius);\n background: var(--ae-surface);\n color: var(--ae-ink);\n width: min(26em, calc(100vw - 3em));\n box-sizing: border-box;\n padding: var(--ae-space-6);\n box-shadow:\n 0 1px 2px rgba(0, 0, 0, 0.05),\n 0 12px 36px rgba(0, 0, 0, 0.09);\n}\n\ndialog.ae-dialog[open] {\nanimation: ae-enter var(--ae-soft) var(--ae-ease) both;\n}\n\ndialog.ae-dialog::backdrop {\nbackground: rgba(252, 252, 252, 0.4);\n}\n\n.dark dialog.ae-dialog,\n[data-ae-mode='dark'] dialog.ae-dialog {\nbackground: var(--ae-wash);\n box-shadow: none;\n}\n\n.dark dialog.ae-dialog::backdrop,\n[data-ae-mode='dark'] dialog.ae-dialog::backdrop {\nbackground: rgba(18, 18, 18, 0.4);\n}\n\n.ae-dialog-panel {\nposition: fixed;\n top: 50%;\n left: 50%;\n translate: -50% -50%;\n z-index: var(--ae-z-dialog);\n border: 0;\n border-radius: var(--ae-radius);\n background: var(--ae-surface);\n color: var(--ae-ink);\n width: min(26em, calc(100vw - 3em));\n box-sizing: border-box;\n padding: var(--ae-space-6);\n box-shadow:\n 0 1px 2px rgba(0, 0, 0, 0.05),\n 0 12px 36px rgba(0, 0, 0, 0.09);\n outline: none;\n}\n\n.ae-dialog-panel[data-open] {\nanimation: ae-enter var(--ae-soft) var(--ae-ease) both;\n}\n\n.ae-dialog-backdrop {\nposition: fixed;\n inset: 0;\n z-index: var(--ae-z-backdrop);\n background: rgba(252, 252, 252, 0.4);\n}\n\n.dark .ae-dialog-panel,\n[data-ae-mode='dark'] .ae-dialog-panel {\nbackground: var(--ae-wash);\n box-shadow: none;\n}\n\n.dark .ae-dialog-backdrop,\n[data-ae-mode='dark'] .ae-dialog-backdrop {\nbackground: rgba(18, 18, 18, 0.4);\n}\n\n.ae-dialog-title {\nfont-weight: var(--ae-w-black);\n margin-bottom: var(--ae-space-2);\n}\n\n.ae-dialog-acts {\ndisplay: flex;\n gap: var(--ae-space-4);\n justify-content: flex-end;\n margin-top: 1.8em;\n}\n\n.ae-dim {\ncolor: var(--ae-ink-muted);\n}\n\n.ae-button {\ndisplay: inline-flex;\n align-items: center;\n justify-content: center;\n gap: var(--ae-space-2);\n background: var(--ae-ink);\n border: 1px solid var(--ae-ink);\n border-radius: var(--ae-radius);\n color: var(--ae-surface);\n font-weight: var(--ae-w-medium);\n padding: 0.55em 1.5em;\n min-height: 44px;\n cursor: pointer;\n transition:\n opacity var(--ae-quick) var(--ae-ease),\n background-color var(--ae-quick) var(--ae-ease),\n transform var(--ae-quick) var(--ae-ease);\n}\n\n.ae-button:hover {\nopacity: 0.9;\n}\n\n.ae-button:active {\ntransform: translateY(1px) scale(0.99);\n}\n\n.ae-button:disabled {\ncursor: default;\n opacity: 0.7;\n transform: none;\n}\n\n.ae-button-quiet {\nbackground: transparent;\n color: var(--ae-ink);\n}\n\n.ae-button-quiet:hover {\nbackground: var(--ae-wash);\n opacity: 1;\n}\n\n@keyframes ae-enter {\nfrom {\n opacity: 0;\n transform: translateY(6px);\n }\n}\n" }, { "path": "recipes/dialog.js", "type": "registry:file", "target": "~/aesthetic.dialog.js", "content": "/* the dialog: focus, labeling, and restoration for .\n\n The native + showModal() owns the top layer and Escape.\n This recipe fills the accessibility gaps the browser doesn't:\n — focus moves into the dialog on open (the first focusable control,\n or the dialog itself as fallback)\n — focus is trapped inside while open (Tab cycles within)\n — focus returns to the invoker on close (the button that opened it)\n — if the dialog has an aria-labelledby, it is honored; otherwise the\n .ae-dialog-title is wired as the label if neither is set\n\n Pair with: a button that calls dialog.showModal(), and both actions\n (cancel + confirm) call dialog.close().\n\n \n \n

Archive this project?

\n
\n \n \n
\n
\n\n Open it: aeDialog(document.getElementById('open'), dialogEl)\n — or let the recipe auto-wire every [data-ae-dialog] invoker. */\n(() => {\n if (window.aeDialog) return;\n const FOCUSABLE =\n 'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])';\n\n function focusables(dialog) {\n return [...dialog.querySelectorAll(FOCUSABLE)].filter(\n (el) => !el.disabled && el.offsetParent !== null,\n );\n }\n\n function label(dialog) {\n if (\n dialog.hasAttribute('aria-labelledby') ||\n dialog.hasAttribute('aria-label')\n )\n return;\n const title = dialog.querySelector('.ae-dialog-title');\n if (!title) return;\n if (!title.id)\n title.id = `ae-dlg-title-${Math.random().toString(36).slice(2, 9)}`;\n dialog.setAttribute('aria-labelledby', title.id);\n }\n\n function trap(dialog, e) {\n if (e.key !== 'Tab') return;\n const items = focusables(dialog);\n if (items.length === 0) {\n e.preventDefault();\n dialog.focus();\n return;\n }\n const first = items[0];\n const last = items[items.length - 1];\n if (e.shiftKey && document.activeElement === first) {\n e.preventDefault();\n last.focus();\n } else if (!e.shiftKey && document.activeElement === last) {\n e.preventDefault();\n first.focus();\n }\n }\n\n /* wire one invoker → dialog pair. Returns a cleanup function. */\n function aeDialog(invoker, dialog) {\n if (invoker._aeDialog) return invoker._aeDialog.cleanup;\n\n let lastFocused = null;\n label(dialog);\n if (!dialog.hasAttribute('tabindex')) dialog.tabIndex = -1;\n\n const onKey = (e) => trap(dialog, e);\n const open = () => {\n lastFocused = invoker;\n dialog.showModal();\n };\n const onClose = () => {\n if (lastFocused) lastFocused.focus();\n lastFocused = null;\n };\n\n invoker.addEventListener('click', open);\n dialog.addEventListener('keydown', onKey);\n dialog.addEventListener('close', onClose);\n\n const cleanup = () => {\n invoker.removeEventListener('click', open);\n dialog.removeEventListener('keydown', onKey);\n dialog.removeEventListener('close', onClose);\n delete invoker._aeDialog;\n };\n invoker._aeDialog = { open, cleanup };\n return cleanup;\n }\n\n /* auto-wire: */\n document.addEventListener('click', (e) => {\n const invoker = e.target.closest('[data-ae-dialog]');\n if (!invoker) return;\n const sel = invoker.getAttribute('data-ae-dialog');\n const dialog = document.querySelector(sel);\n if (!(dialog instanceof HTMLDialogElement)) return;\n if (!invoker._aeDialog) {\n aeDialog(invoker, dialog);\n // first click: the invoker's listener was added during dispatch and\n // won't fire for this event, so open the dialog directly\n invoker._aeDialog.open();\n }\n // subsequent clicks: the invoker's own listener handles opening\n });\n\n window.aeDialog = aeDialog;\n})();\n" } ] }