{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "shell", "type": "registry:item", "title": "aesthetic Screen", "description": "Aesthetic Screen primitive, v0.25.0. Installs ae-bar, ae-mode, ae-name, ae-nav, ae-screen, ae-stage, ae-view without the full stylesheet. Recipes: mode.js, nav.js, views.js.", "registryDependencies": [ "https://aesthetic.mistystep.io/r/aesthetic-base.json" ], "meta": { "version": "0.25.0", "tier": "primitive", "route": "shell", "classes": [ "ae-bar", "ae-mode", "ae-name", "ae-nav", "ae-screen", "ae-stage", "ae-view" ], "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-measure", "--ae-space-5", "--ae-space-6", "--ae-soft" ], "recipes": ["mode.js", "nav.js", "views.js"] }, "files": [ { "path": "registry/shell/aesthetic.shell.css", "type": "registry:file", "target": "~/aesthetic.shell.css", "content": "/* aesthetic shell 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-measure: 38rem;\n --ae-space-5: 1.5em;\n --ae-space-6: 2em;\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.ae-screen {\nheight: 100dvh;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n\n.ae-bar {\nflex: none;\n width: min(var(--ae-measure), 100% - 5rem);\n margin-inline: auto;\n padding: 1.4em 0;\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n justify-content: space-between;\n gap: 0.4em 1.5em;\n}\n\n.ae-stage {\nflex: 1;\n min-height: 0;\n width: min(var(--ae-measure), 100% - 5rem);\n margin-inline: auto;\n display: flex;\n flex-direction: column;\n overflow-y: auto;\n}\n\n.ae-stage > * {\nmargin-block: auto;\n padding-block: var(--ae-space-5);\n}\n\n.ae-view {\nanimation: ae-enter var(--ae-soft) var(--ae-ease) both;\n}\n\n.ae-nav {\nposition: relative;\n display: flex;\n gap: var(--ae-space-6);\n}\n\n.ae-nav a,\n.ae-nav button {\nbackground: transparent;\n border: 0;\n padding: 0 0 0.6em;\n margin-block: 0;\n cursor: pointer;\n color: var(--ae-ink-muted);\n font-weight: var(--ae-w-medium);\n text-decoration: none;\n transition: color var(--ae-quick) var(--ae-ease);\n}\n\n.ae-nav a:hover,\n.ae-nav button:hover {\ncolor: var(--ae-ink);\n}\n\n.ae-nav [aria-current],\n.ae-nav .is-active {\ncolor: var(--ae-ink);\n}\n\n.ae-mode {\nposition: relative;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 2.2em;\n height: 2.2em;\n background: transparent;\n border: 0;\n padding: 0;\n cursor: pointer;\n color: var(--ae-ink-muted);\n transition: color var(--ae-quick) var(--ae-ease);\n}\n\n.ae-mode::before {\ncontent: '\\200b';\n}\n\n.ae-mode:hover {\ncolor: var(--ae-ink);\n}\n\n.ae-name {\nfont-weight: var(--ae-w-black);\n letter-spacing: 0.38em;\n}\n\na.ae-name {\ntext-decoration: none;\n}\n\n@media (max-width: 40rem) {\n .ae-bar,\n .ae-stage {\n width: calc(100% - 2.4rem);\n }\n}\n\n@keyframes ae-enter {\nfrom {\n opacity: 0;\n transform: translateY(6px);\n }\n}\n" }, { "path": "recipes/mode.js", "type": "registry:file", "target": "~/aesthetic.mode.js", "content": "/* the mode: light and dark, defaulting to the system, with a toggle.\n\n The boot — read the persisted choice before first paint so the page\n never flashes the wrong mode — must be inlined in :\n\n \n\n The toggle below pins the opposite of the effective scheme. Pinning\n color-scheme keeps UA widgets (scrollbars, form controls) on the\n pinned side even when the OS disagrees. The change itself is quick\n and interruptible: a new click cuts any in-flight transition and\n applies the newest mode immediately. Unsupported view transitions\n get the same quick color ease; reduced motion is instant.\n next-themes consumers: keep attribute=\"class\" and skip this file —\n the classes match. */\n(() => {\n const root = document.documentElement;\n let activeTransition = null;\n let easingTimer = 0;\n let runId = 0;\n let targetDark = null;\n\n const isDark = () =>\n root.classList.contains('dark')\n ? true\n : root.classList.contains('light')\n ? false\n : matchMedia('(prefers-color-scheme: dark)').matches;\n\n const reducedMode = matchMedia('(prefers-reduced-motion: reduce)');\n\n const clearAnimation = () => {\n if (activeTransition && activeTransition.skipTransition) {\n activeTransition.skipTransition();\n }\n activeTransition = null;\n if (easingTimer) {\n clearTimeout(easingTimer);\n easingTimer = 0;\n }\n root.classList.remove('ae-vt-mode', 'ae-mode-easing');\n };\n\n const applyMode = (dark) => {\n root.classList.toggle('dark', dark);\n root.classList.toggle('light', !dark);\n root.style.colorScheme = dark ? 'dark' : 'light';\n try {\n localStorage.setItem('ae-mode', dark ? 'dark' : 'light');\n } catch (e) {}\n };\n\n document.querySelectorAll('.ae-mode').forEach((btn) => {\n btn.addEventListener('click', () => {\n const nextDark = !(targetDark ?? isDark());\n const id = ++runId;\n targetDark = nextDark;\n const flip = () => {\n if (id !== runId) return;\n applyMode(nextDark);\n };\n clearAnimation();\n if (reducedMode.matches) {\n flip();\n } else if (document.startViewTransition) {\n root.classList.add('ae-vt-mode');\n activeTransition = document.startViewTransition(flip);\n easingTimer = setTimeout(() => {\n if (id !== runId) return;\n root.classList.remove('ae-vt-mode');\n easingTimer = 0;\n }, 180);\n activeTransition.finished.finally(() => {\n if (id !== runId) return;\n root.classList.remove('ae-vt-mode');\n activeTransition = null;\n if (easingTimer) {\n clearTimeout(easingTimer);\n easingTimer = 0;\n }\n });\n } else {\n root.classList.add('ae-mode-easing');\n flip();\n easingTimer = setTimeout(() => {\n if (id !== runId) return;\n root.classList.remove('ae-mode-easing');\n easingTimer = 0;\n }, 180);\n }\n });\n });\n})();\n" }, { "path": "recipes/nav.js", "type": "registry:file", "target": "~/aesthetic.nav.js", "content": "/* the nav indicator: position the sliding ink underline under the\n active item on activation, resize, and font load (metrics shift\n when Geist arrives). Exposes window.aePlaceInds for page scripts\n that swap views and need the indicator re-placed after a display\n change. Works for .ae-nav and .ae-tabs alike. */\n(() => {\n if (window.aePlaceInds) return;\n const placeInd = (nav) => {\n const active = nav.querySelector(\n '[aria-current], [aria-selected=\"true\"], .is-active',\n );\n const ind = nav.querySelector('.ae-nav-ind');\n if (!active || !ind || !nav.offsetWidth) return;\n ind.style.left = active.offsetLeft + 'px';\n ind.style.width = active.offsetWidth + 'px';\n };\n const placeAllInds = () =>\n document.querySelectorAll('.ae-nav, .ae-tabs').forEach(placeInd);\n\n document.querySelectorAll('.ae-nav, .ae-tabs').forEach((nav) => {\n nav.querySelectorAll('a, button').forEach((item) => {\n item.addEventListener('click', () => {\n nav\n .querySelectorAll('a, button')\n .forEach((b) => b.classList.remove('is-active'));\n item.classList.add('is-active');\n placeInd(nav);\n });\n });\n });\n addEventListener('resize', placeAllInds);\n if (document.fonts) document.fonts.ready.then(placeAllInds);\n placeAllInds();\n\n window.aePlaceInds = placeAllInds;\n})();\n" }, { "path": "recipes/views.js", "type": "registry:file", "target": "~/aesthetic.views.js", "content": "/* view swap by hash: give each view data-route=\"name\" and link to it\n with href=\"#name\". The matching view mounts (.is-on) and replays\n its entrance; everything else unmounts. Falls back to\n data-route=\"index\" when the hash names nothing. */\n(() => {\n const views = document.querySelectorAll('[data-route]');\n if (!views.length) return;\n const show = () => {\n const route = location.hash.slice(1) || 'index';\n let hit = false;\n views.forEach((v) => {\n const on = v.dataset.route === route;\n v.classList.toggle('is-on', on);\n hit = hit || on;\n });\n if (!hit)\n views.forEach((v) =>\n v.classList.toggle('is-on', v.dataset.route === 'index'),\n );\n if (window.aePlaceInds) requestAnimationFrame(window.aePlaceInds);\n };\n addEventListener('hashchange', show);\n show();\n})();\n" } ] }