{ "version": 3, "sources": ["../../assets/js/lib/scroll_lock.js", "../../assets/js/lib/overlay_registry.js", "../../assets/js/lib/turbo.js", "../../assets/js/lib/sugar_controller.js", "../../assets/js/lib/collapse.js", "../../assets/js/lib/persist.js", "../../assets/js/controllers/accordion_controller.js", "../../assets/js/lib/transition.js", "../../assets/js/lib/focus_trap.js", "../../assets/js/lib/uid.js", "../../assets/js/controllers/dialog_controller.js", "../../assets/js/controllers/alert_dialog_controller.js", "../../assets/js/lib/dates.js", "../../assets/js/controllers/calendar_controller.js", "../../assets/js/controllers/carousel_controller.js", "../../assets/js/controllers/checkbox_controller.js", "../../assets/js/controllers/collapsible_controller.js", "../../assets/js/controllers/date_picker_controller.js", "../../assets/js/controllers/drawer_controller.js", "../../assets/js/lib/anchor.js", "../../assets/js/controllers/dropdown_menu_controller.js", "../../assets/js/controllers/dropdown_menu_checkbox_item_controller.js", "../../assets/js/controllers/dropdown_menu_radio_group_controller.js", "../../assets/js/controllers/dropdown_menu_sub_controller.js", "../../assets/js/controllers/hover_card_controller.js", "../../assets/js/controllers/input_otp_controller.js", "../../assets/js/controllers/popover_controller.js", "../../assets/js/controllers/radio_group_controller.js", "../../assets/js/controllers/resizable_controller.js", "../../assets/js/controllers/scroll_area_controller.js", "../../assets/js/controllers/select_controller.js", "../../assets/js/controllers/slider_controller.js", "../../assets/js/controllers/sheet_controller.js", "../../assets/js/controllers/sonner_controller.js", "../../assets/js/controllers/sonner_trigger_controller.js", "../../assets/js/controllers/switch_controller.js", "../../assets/js/controllers/tabs_controller.js", "../../assets/js/controllers/toggle_controller.js", "../../assets/js/controllers/toggle_group_controller.js", "../../assets/js/controllers/toggle_group_item_controller.js", "../../assets/js/controllers/tooltip_controller.js", "../../assets/js/index.js"], "sourcesContent": ["// Body-scroll lock \u2014 one shared, ref-counted implementation.\n//\n// Every overlay that should freeze the page while open (Dropdown Menu,\n// Dialog, AlertDialog, Drawer, Sheet, Select) routes through this single\n// counter. ONE global count is the entire point: it is the only thing\n// that keeps two overlays whose open windows overlap \u2014 switching between\n// two dropdowns, opening a dialog from a menu item, a select layered over\n// a dropdown \u2014 in a consistent state. The first lock snapshots 's\n// overflow / padding-right and applies `overflow:hidden`; the last unlock\n// restores them. Locks in between only bump the counter.\n//\n// Both styles go on the ROOT element (), never : is the\n// scroll container, and padding on would shrink its content box\n// ~15px and drag mx-auto / calc(50% \u2026) layouts sideways on every open.\n// When the page already reserves a stable scrollbar gutter we only toggle\n// overflow and skip the padding compensation (there is no shift to\n// counter).\n//\n// State lives on `window` so that even if two copies of this module load\n// (importmap source + minified bundle) they still share one counter.\n\nexport function scrollLockState() {\n if (!window.__sugarScrollLock) {\n window.__sugarScrollLock = { count: 0, overflow: '', paddingRight: '' };\n }\n return window.__sugarScrollLock;\n}\n\n// Controllers currently holding a lock unit. Used by the Turbo render\n// reconciler to distinguish a genuine lock (a data-default-open modal that\n// re-locked while connecting on the new page) from a stranded one.\nexport const lockHolders = new Set();\n\nexport function lockBodyScroll() {\n const s = scrollLockState();\n if (s.count === 0) {\n const root = document.documentElement;\n s.overflow = root.style.overflow;\n s.paddingRight = root.style.paddingRight;\n const scrollbarWidth = window.innerWidth - root.clientWidth;\n let gutter = '';\n try { gutter = window.getComputedStyle(root).scrollbarGutter || ''; } catch (e) { /* older browsers */ }\n root.style.overflow = 'hidden';\n if (scrollbarWidth > 0 && (gutter === '' || gutter === 'auto')) {\n root.style.paddingRight = scrollbarWidth + 'px';\n }\n }\n s.count++;\n}\n\nexport function unlockBodyScroll() {\n const s = scrollLockState();\n if (s.count <= 0) return;\n s.count--;\n if (s.count === 0) {\n const root = document.documentElement;\n root.style.overflow = s.overflow || '';\n root.style.paddingRight = s.paddingRight || '';\n s.overflow = '';\n s.paddingRight = '';\n }\n}\n\n// Force the lock fully open and restore , regardless of count. Used\n// before a Turbo body swap so a navigation fired while an overlay is still\n// open can never leave the next page unscrollable.\nexport function resetBodyScroll() {\n const s = scrollLockState();\n if (s.count === 0) return;\n const root = document.documentElement;\n root.style.overflow = s.overflow || '';\n root.style.paddingRight = s.paddingRight || '';\n s.count = 0;\n s.overflow = '';\n s.paddingRight = '';\n}\n\n// Give an overlay controller idempotent, teardown-safe page-scroll locking.\n//\n// Adds a per-instance `scrollLocked` flag plus `lockScroll()` /\n// `unlockScroll()` that each no-op unless they actually change that flag,\n// so a controller contributes AT MOST one unit to the shared counter and\n// always removes exactly its own unit \u2014 no matter how many open/close\n// paths or value callbacks fire. That alone kills double-count drift.\n//\n// The disconnect wrap is the safety net: Stimulus calls disconnect() on\n// EVERY removal path \u2014 Turbo body swaps, Turbo Frame renders, a parent\n// innerHTML swap \u2014 so a controller torn out of the DOM while `open` is\n// still true can never leak its lock unit and strand the page at\n// overflow:hidden.\n//\n// Call once, from connect() or initialize(); repeated calls no-op (the\n// same controller instance is reused across disconnect/reconnect cycles).\nexport function useScrollLock(controller) {\n if (controller.__scrollLockInstalled) return;\n controller.__scrollLockInstalled = true;\n controller.scrollLocked = false;\n controller.lockScroll = function () {\n if (this.scrollLocked) return;\n this.scrollLocked = true;\n lockHolders.add(this);\n lockBodyScroll();\n };\n controller.unlockScroll = function () {\n if (!this.scrollLocked) return;\n this.scrollLocked = false;\n lockHolders.delete(this);\n unlockBodyScroll();\n };\n const originalDisconnect = controller.disconnect.bind(controller);\n controller.disconnect = function () {\n this.unlockScroll();\n originalDisconnect();\n };\n}\n", "// Registry of currently-open overlay controllers.\n//\n// Every controller with transient open state (Dialog family, Popover,\n// DropdownMenu + Sub, HoverCard, Tooltip, Select, DatePicker) adds itself\n// when it opens and removes itself when it closes or disconnects. Each\n// registrant MUST expose a synchronous `closeNow()` that skips transitions\n// and clears its own timers.\n//\n// The Turbo before-cache hook simply closes the O(open) entries here \u2014\n// no DOM walking \u2014 so snapshots never contain a stuck-open overlay.\n//\n// Persisted-open state never enters this registry (persistence applies to\n// disclosure widgets \u2014 tabs/accordion/collapsible \u2014 which are not\n// overlays), so closing everything here can't clobber user intent.\n\nexport const openOverlays = new Set();\n\nexport function registerOpenOverlay(controller) {\n openOverlays.add(controller);\n}\n\nexport function unregisterOpenOverlay(controller) {\n openOverlays.delete(controller);\n}\n", "// Turbo Drive integration \u2014 the ONLY place in the runtime that listens to\n// turbo:* events. Installed once (idempotent) by registerControllers();\n// every listener is inert when Turbo isn't on the page, so the library\n// stays fully optional-Turbo.\n//\n// This file is small on purpose:\n// Stimulus disconnects every controller in the outgoing body on any DOM\n// removal \u2014 Turbo body swaps included \u2014 so per-instance cleanup (scroll\n// lock release, anchor listener removal, focus-trap release, timers) rides\n// on disconnect() and needs no Turbo-specific code. And with no teleported\n// nodes (floating content stays inside its component root), the old\n// purge-teleports and detach-anchor-listeners sweeps have nothing left to\n// do.\n//\n// What genuinely still needs Turbo hooks:\n//\n// 1. before-cache / before-render: close open overlays SYNCHRONOUSLY.\n// Turbo serializes the body into its back/forward cache right after\n// turbo:before-cache; a dialog left open would be snapshotted with\n// data-state=\"open\" and reappear \"stuck open\" on back navigation\n// (its enter-state markup restored, but mid-transition classes and\n// aria state stale). closeNow() skips transitions and clears timers so\n// the snapshot is clean before serialization. Then force the shared\n// scroll lock back to a clean slate: closing releases each unit, but\n// resetBodyScroll() also covers a navigation fired from INSIDE an open\n// overlay where teardown ordering could otherwise strand at\n// overflow:hidden (Turbo never replaces , so a missed reset\n// freezes scroll for the rest of the session).\n//\n// 2. render / load: reconcile the scroll lock against reality. If the\n// count is still positive but no connected controller holds a lock\n// unit, the count is by definition stranded \u2014 force it clean. A\n// data-default-open modal that legitimately re-locked while connecting\n// on the new page registers itself in lockHolders and is left alone.\n// This never fights a genuine lock and self-heals every navigation\n// path.\n//\n// 3. morph (Turbo 8 page refresh): controllers derive their rendering\n// from data-*-value attributes, so a morphed value attribute re-fires\n// its *ValueChanged callback automatically. But morph can also revert\n// controller-written presentation attributes (aria-expanded, inline\n// display) back to the server-rendered markup without touching the\n// value attribute. After a morph completes, ask every ready component\n// to re-render from its current values via a broadcast event that\n// controllers opt into with:\n// data-action=\"sugar:morph@document->st-tabs#render\"\n\nimport { resetBodyScroll, scrollLockState, lockHolders } from './scroll_lock.js';\nimport { openOverlays } from './overlay_registry.js';\n\n// \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Snapshot defaults \u2014 reset Stimulus value attributes before caching.\n//\n// Stimulus stores values in DOM attributes, so a user interaction (picking\n// a tab, expanding a collapsible) mutates markup that Turbo would serialize\n// into its back/forward cache \u2014 and the restored page would re-initialize\n// from the MUTATED value, silently persisting state that was never opted\n// into persistence. The intended semantics: only explicitly persisted\n// state survives navigation; everything else re-initializes from the\n// server-rendered defaults.\n//\n// To get there, every SugarController captures its\n// server-rendered `data--*-value` attributes at connect() (via\n// captureSnapshotDefaults), and turbo:before-cache writes them back \u2014\n// except values that bindPersistedState() marked exempt, whose entire point\n// is surviving navigation. Presentation attributes (aria-*, inline display)\n// mutated by the old page may still be serialized, but on restore each\n// controller re-initializes from the reset value attributes and its initial\n// *ValueChanged repaints them.\n\nconst snapshotDefaults = new WeakMap();\n\nexport function captureSnapshotDefaults(controller) {\n const el = controller.element;\n const prefix = 'data-' + controller.identifier + '-';\n let entry = snapshotDefaults.get(el);\n if (!entry) {\n entry = { prefixes: new Set(), attrs: {}, exempt: new Set() };\n snapshotDefaults.set(el, entry);\n }\n if (entry.prefixes.has(prefix)) return; // same instance reconnecting\n entry.prefixes.add(prefix);\n for (const attr of Array.from(el.attributes)) {\n if (attr.name.startsWith(prefix) && attr.name.endsWith('-value')) {\n entry.attrs[attr.name] = attr.value;\n }\n }\n}\n\nexport function exemptFromSnapshotReset(controller, valueName) {\n const entry = snapshotDefaults.get(controller.element);\n if (!entry) return;\n const dashed = valueName\n .replace(/Value$/, '')\n .replace(/[A-Z]/g, (m) => '-' + m.toLowerCase());\n entry.exempt.add('data-' + controller.identifier + '-' + dashed + '-value');\n}\n\nfunction resetValueAttributesBeforeCache() {\n if (!document.body) return;\n for (const el of document.body.querySelectorAll('[data-controller]')) {\n const entry = snapshotDefaults.get(el);\n if (!entry) continue;\n // Remove value attributes that did not exist at connect time\u2026\n for (const attr of Array.from(el.attributes)) {\n for (const prefix of entry.prefixes) {\n if (attr.name.startsWith(prefix) && attr.name.endsWith('-value')\n && !(attr.name in entry.attrs) && !entry.exempt.has(attr.name)) {\n el.removeAttribute(attr.name);\n }\n }\n }\n // \u2026and restore the server-rendered originals.\n for (const [name, value] of Object.entries(entry.attrs)) {\n if (entry.exempt.has(name)) continue;\n if (el.getAttribute(name) !== value) el.setAttribute(name, value);\n }\n }\n}\n\nfunction beforeTurboSwap() {\n for (const controller of Array.from(openOverlays)) {\n try {\n controller.closeNow();\n } catch (e) { /* one broken overlay must not skip the others */ }\n }\n try { resetValueAttributesBeforeCache(); } catch (e) { /* must not skip the scroll reset */ }\n resetBodyScroll();\n}\n\nfunction reconcileScrollLockAfterRender() {\n const s = scrollLockState();\n if (s.count <= 0) return;\n for (const holder of lockHolders) {\n if (holder.element && holder.element.isConnected) return;\n }\n resetBodyScroll();\n}\n\nfunction afterMorph() {\n document.dispatchEvent(new CustomEvent('sugar:morph'));\n}\n\nexport function installTurboIntegration() {\n if (window.__sugarTwigTurboWired) return;\n window.__sugarTwigTurboWired = true;\n\n document.addEventListener('turbo:before-cache', beforeTurboSwap);\n document.addEventListener('turbo:before-render', beforeTurboSwap);\n document.addEventListener('turbo:render', reconcileScrollLockAfterRender);\n document.addEventListener('turbo:load', reconcileScrollLockAfterRender);\n document.addEventListener('turbo:morph', afterMorph);\n}\n", "// Shared base class for all SugarTwig controllers.\n//\n// Templates server-render their initial visual state (inline display,\n// aria-*, data-state), so there is no cloak/FOUC problem to solve. The\n// one cross-cutting duty left for connect() is the readiness marker:\n// `data-st-ready` on the controller root signals \"behavior is wired\" \u2014\n// the browser test suite waits on it before interacting.\n//\n// Subclasses that override connect() MUST call super.connect() first.\n\nimport { Controller } from '@hotwired/stimulus';\nimport { captureSnapshotDefaults } from './turbo.js';\n\nexport default class SugarController extends Controller {\n connect() {\n // Remember the server-rendered value attributes so the Turbo\n // before-cache hook can reset transient state out of snapshots\n // (see lib/turbo.js). Must run before bindPersistedState overwrites\n // a value from storage \u2014 i.e. before any subclass connect() logic.\n captureSnapshotDefaults(this);\n this.element.setAttribute('data-st-ready', '');\n }\n}\n", "// Height expand/collapse animation (used by Accordion and Collapsible\n// content).\n//\n// Uses the Web Animations API instead of inline style transitions so the\n// element's own styles are never left dirty: display flips at the animation\n// edges, overflow is clamped only while animating, and a reduced-motion\n// preference collapses the duration to zero.\n//\n// Rapid toggling is safe: each call cancels the element's in-flight\n// animation and a generation counter ensures only the latest call decides\n// the final display state.\n\nconst DURATION = 200;\n\nfunction duration() {\n try {\n if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return 0;\n } catch (e) { /* noop */ }\n return DURATION;\n}\n\nfunction cancelInFlight(el) {\n if (el.__stCollapseAnim) {\n try { el.__stCollapseAnim.cancel(); } catch (e) { /* noop */ }\n el.__stCollapseAnim = null;\n }\n}\n\nexport function expandEl(el) {\n const gen = el.__stCollapseGen = (el.__stCollapseGen || 0) + 1;\n cancelInFlight(el);\n el.style.display = '';\n const target = el.scrollHeight;\n if (duration() === 0 || target === 0) return Promise.resolve();\n el.style.overflow = 'hidden';\n const anim = el.animate(\n [{ height: '0px', opacity: 0.6 }, { height: target + 'px', opacity: 1 }],\n { duration: duration(), easing: 'ease-out' },\n );\n el.__stCollapseAnim = anim;\n return anim.finished.catch(() => {}).then(() => {\n if (el.__stCollapseGen !== gen) return;\n el.__stCollapseAnim = null;\n el.style.overflow = '';\n });\n}\n\nexport function collapseEl(el) {\n const gen = el.__stCollapseGen = (el.__stCollapseGen || 0) + 1;\n cancelInFlight(el);\n const current = el.offsetHeight;\n if (duration() === 0 || current === 0) {\n el.style.display = 'none';\n return Promise.resolve();\n }\n el.style.overflow = 'hidden';\n const anim = el.animate(\n [{ height: current + 'px', opacity: 1 }, { height: '0px', opacity: 0.6 }],\n { duration: duration(), easing: 'ease-in' },\n );\n el.__stCollapseAnim = anim;\n return anim.finished.catch(() => {}).then(() => {\n if (el.__stCollapseGen !== gen) return;\n el.__stCollapseAnim = null;\n el.style.overflow = '';\n el.style.display = 'none';\n });\n}\n\n// Synchronous collapse for Turbo before-cache teardown.\nexport function collapseElNow(el) {\n el.__stCollapseGen = (el.__stCollapseGen || 0) + 1;\n cancelInFlight(el);\n el.style.overflow = '';\n el.style.display = 'none';\n}\n", "// Persistence helper \u2014 opt-in via host id.\n//\n// Components that should survive Turbo navigations (Tabs selection,\n// Accordion expansion, Resizable panel sizes, Collapsible open state)\n// call this from connect() to bind a single Stimulus value to web storage.\n//\n// Opt-in rule: a component is persisted ONLY when its host element has an\n// `id` attribute. Anonymous components don't write anything to storage \u2014\n// preventing key collisions across pages and keeping a clean storage\n// surface for consumers.\n//\n// Storage choice: defaults to sessionStorage (per-tab, cleared on close \u2014\n// least surprising for \"remember while I navigate around\") unless\n// `opts.storage === 'local'` (genuine user preference, e.g. Resizable\n// panel sizes).\n//\n// Behavior:\n// 1. On connect, if a persisted value exists in storage, it overwrites\n// whatever the server rendered (so defaultValue/defaultOpen become a\n// fallback for the first visit only).\n// 2. The controller's `Changed` callback is wrapped so every\n// subsequent change writes back to storage.\n// 3. opts.requireOptInAttr: when true, additionally requires the host\n// element to declare `data-st-persist` \u2014 used by Collapsible where\n// persistence is rarely what the user wants.\n//\n// Keys follow the stable `sugar-twig::` scheme \u2014 a public\n// contract (asserted by the turbo-persist browser specs), so state\n// persisted by earlier releases carries over.\n//\n// Returns `true` if persistence was wired, `false` otherwise.\nimport { exemptFromSnapshotReset } from './turbo.js';\n\nexport function bindPersistedState(controller, valueName, opts) {\n if (!opts || !opts.component) return false;\n const el = controller.element;\n if (!el || !el.id) return false;\n if (opts.requireOptInAttr && !el.hasAttribute('data-st-persist')) return false;\n\n // Persisted values are the user's deliberate navigation-surviving\n // state \u2014 the Turbo before-cache reset must leave them alone.\n exemptFromSnapshotReset(controller, valueName);\n\n const storage = opts.storage === 'local' ? window.localStorage : window.sessionStorage;\n const key = 'sugar-twig:' + opts.component + ':' + el.id;\n\n try {\n const raw = storage.getItem(key);\n if (raw !== null) controller[valueName] = JSON.parse(raw);\n } catch (e) { /* noop \u2014 corrupt value, fall through to default */ }\n\n // Wrap the value-changed callback once per instance (the same\n // controller instance survives disconnect/reconnect cycles).\n if (!controller.__stPersistWrapped) controller.__stPersistWrapped = {};\n if (!controller.__stPersistWrapped[valueName]) {\n controller.__stPersistWrapped[valueName] = true;\n const callbackName = valueName + 'Changed';\n const original = controller[callbackName];\n controller[callbackName] = function (...args) {\n if (typeof original === 'function') original.apply(this, args);\n try {\n storage.setItem(key, JSON.stringify(this[valueName]));\n } catch (e) { /* noop \u2014 quota / serialization */ }\n };\n }\n\n // Tell the Turbo before-cache close walk not to mutate this value \u2014\n // its `open = false` write would otherwise be persisted, clobbering\n // the user's intent. Tracked per-value so a hypothetical component\n // that persists one value but still has a transient open elsewhere\n // works correctly.\n if (!controller.stPersistedValues) controller.stPersistedValues = {};\n controller.stPersistedValues[valueName] = true;\n\n return true;\n}\n", "import SugarController from '../lib/sugar_controller.js';\nimport { expandEl, collapseEl } from '../lib/collapse.js';\nimport { bindPersistedState } from '../lib/persist.js';\n\n// Accordion \u2014 a single `st-accordion` controller on the root owns the whole\n// widget. AccordionItems carry only data-value (no controller); triggers\n// dispatch to `#toggle` and derive their value from the closest item; each\n// item's content is found per item and expanded/collapsed (the collapse\n// helper animates height).\n//\n// `activeValue` deliberately isn't a typed Stimulus value: the persisted\n// storage contract is a bare string (or null) in single mode and an array\n// in multiple mode \u2014 shapes Stimulus's Object value type rejects (it\n// forbids null/strings). Keeping the raw shape means bindPersistedState\n// stores \"b\" for single and [\"a\",\"b\"] for multiple under\n// sugar-twig:accordion:, so state from earlier releases carries over. The manual accessor still routes through\n// activeValueChanged, so persistence wrapping and rendering work unchanged.\nexport default class AccordionController extends SugarController {\n connect() {\n super.connect();\n this.type = this.element.dataset.type || 'single';\n this.collapsible = this.element.dataset.collapsible === '1';\n\n const initial = this.element.dataset.defaultValue || '';\n this.activeValue = this.type === 'multiple'\n ? (initial ? [initial] : [])\n : (initial || null);\n\n // Persistence overwrites the default (first-visit fallback). Every\n // load-time write above and from storage paints instantly; animations\n // only kick in on user toggles, once `_ready` flips true.\n bindPersistedState(this, 'activeValue', { component: 'accordion' });\n this._ready = true;\n }\n\n get activeValue() {\n return this._active;\n }\n\n set activeValue(value) {\n const previous = this._active;\n this._active = value;\n this.activeValueChanged(value, previous);\n }\n\n toggle(event) {\n const item = event.currentTarget.closest('[data-slot=accordion-item]');\n if (!item) return;\n if (item.dataset.disabled === 'true') return;\n const value = item.dataset.value || '';\n const active = this._active;\n if (Array.isArray(active)) {\n this.activeValue = active.includes(value)\n ? active.filter((v) => v !== value)\n : active.concat([value]);\n } else if (this.collapsible) {\n this.activeValue = active === value ? null : value;\n } else {\n this.activeValue = value;\n }\n }\n\n activeValueChanged() {\n this.render();\n }\n\n isOpen(value) {\n const active = this._active;\n return Array.isArray(active) ? active.includes(value) : active === value;\n }\n\n render() {\n const animate = this._ready === true;\n for (const item of this.element.querySelectorAll('[data-slot=accordion-item]')) {\n const value = item.dataset.value || '';\n const open = value !== '' && this.isOpen(value);\n\n const trigger = item.querySelector('[data-slot=accordion-trigger]');\n if (trigger) {\n trigger.setAttribute('aria-expanded', open ? 'true' : 'false');\n if (item.dataset.disabled === 'true') {\n trigger.setAttribute('aria-disabled', 'true');\n } else {\n trigger.removeAttribute('aria-disabled');\n }\n }\n\n const content = item.querySelector('[data-slot=accordion-content]');\n if (content) {\n // Only (re-)animate an item whose open state actually changed.\n // Re-running expandEl/collapseEl on items that were already in\n // the target state makes every open item collapse-and-expand on\n // each toggle \u2014 very visible in `type=multiple` where several\n // items stay open at once. `__stOpen` tracks the last-applied\n // state per content element.\n const wasOpen = content.__stOpen === true;\n if (!animate) {\n content.style.display = open ? '' : 'none';\n } else if (open !== wasOpen) {\n if (open) expandEl(content);\n else collapseEl(content);\n }\n content.__stOpen = open;\n }\n }\n }\n}\n", "// Enter/leave class transitions.\n//\n// Elements declare their transition classes via attributes (same Tailwind\n// utility strings as before, mechanically renamed):\n//\n// data-transition-enter=\"ease-out duration-100\"\n// data-transition-enter-start=\"opacity-0 scale-95\"\n// data-transition-enter-end=\"opacity-100 scale-100\"\n// data-transition-leave=\"ease-in duration-100\"\n// data-transition-leave-start=\"opacity-100 scale-100\"\n// data-transition-leave-end=\"opacity-0 scale-95\"\n//\n// showEl()/hideEl() toggle inline `style.display` plus the transition\n// choreography: apply\n// the during+start classes, force a style flush, swap start \u2192 end on the\n// next frame, wait out the computed transition duration, then clean up.\n// Elements without transition attributes toggle instantly.\n//\n// Rapid toggling is safe: each call bumps a per-element generation counter\n// and cancels the previous run's classes/timers, so a close mid-enter\n// starts the leave from the current visual state and a stale leave can\n// never hide an element that was re-opened meanwhile.\n//\n// The `during` classes (e.g. `ease-out duration-100`) set only\n// transition-duration/timing-function, so transition-property falls back to\n// its CSS default of `all`. For an anchored overlay that is revealed at the\n// same moment the anchor mixin writes its final top/left (from the off-screen\n// -9999px parking coords the template ships), `all` would sweep top/left into\n// the transition and slide the popover in from the corner \u2014 the visible\n// \"tick\" on open. Every choreography in this design system animates only\n// opacity + a transform (zoom via `scale`, slide via `translate`), so we pin\n// transition-property to those while a transition runs, keeping position\n// (top/left) and visibility changes instant.\n//\n// Tailwind v4 compiles `scale-*`/`translate-*`/`rotate-*` to the individual\n// `scale`/`translate`/`rotate` CSS properties rather than the legacy\n// `transform` shorthand, so all four must be listed \u2014 omitting `scale` would\n// silently drop the open/close zoom, and omitting `translate` the drawer /\n// sheet slide.\nconst ANIMATED_PROPERTIES = 'opacity, transform, translate, scale, rotate';\n\nfunction classes(el, attr) {\n const raw = el.getAttribute(attr);\n return raw ? raw.split(/\\s+/).filter(Boolean) : [];\n}\n\nfunction transitionDurationMs(el) {\n const cs = window.getComputedStyle(el);\n const parse = (v) => {\n const first = String(v || '').split(',')[0].trim();\n if (!first) return 0;\n return first.endsWith('ms') ? parseFloat(first) : parseFloat(first) * 1000;\n };\n return (parse(cs.transitionDuration) || 0) + (parse(cs.transitionDelay) || 0);\n}\n\nfunction runTransition(el, stage) {\n const during = classes(el, 'data-transition-' + stage);\n const start = classes(el, 'data-transition-' + stage + '-start');\n const end = classes(el, 'data-transition-' + stage + '-end');\n\n if (el.__stTransitionCleanup) el.__stTransitionCleanup();\n\n if (!during.length && !start.length && !end.length) {\n return Promise.resolve();\n }\n\n return new Promise((resolve) => {\n let rafId = 0;\n let timerId = 0;\n const cleanup = () => {\n if (rafId) cancelAnimationFrame(rafId);\n if (timerId) clearTimeout(timerId);\n el.classList.remove(...during, ...start, ...end);\n el.style.transitionProperty = '';\n el.__stTransitionCleanup = null;\n resolve();\n };\n el.__stTransitionCleanup = cleanup;\n\n // Scope the transition to opacity+transform so a concurrent top/left\n // reposition never animates (see ANIMATED_PROPERTIES above).\n el.style.transitionProperty = ANIMATED_PROPERTIES;\n el.classList.add(...during, ...start);\n // Force a style flush NOW, while the start classes are applied and the\n // element is already displayed (showEl toggled display before calling\n // in). This commits the start state as a resolved style so the swap\n // below reads as a real property change the browser animates. Doing\n // the flush inside the rAF instead would coalesce it with the\n // display:none\u2192shown reveal, and the transition would collapse to an\n // instant jump (no fade / zoom).\n void el.offsetHeight;\n rafId = requestAnimationFrame(() => {\n rafId = 0;\n el.classList.remove(...start);\n el.classList.add(...end);\n timerId = setTimeout(cleanup, transitionDurationMs(el));\n });\n });\n}\n\nexport function enter(el) {\n return runTransition(el, 'enter');\n}\n\nexport function leave(el) {\n return runTransition(el, 'leave');\n}\n\n// Show/hide: inline display toggling around the\n// enter/leave choreography. The generation counter guarantees only the\n// latest call decides the final display state.\nexport function showEl(el) {\n el.__stShowGen = (el.__stShowGen || 0) + 1;\n el.style.display = '';\n return enter(el);\n}\n\nexport function hideEl(el) {\n const gen = el.__stShowGen = (el.__stShowGen || 0) + 1;\n return leave(el).then(() => {\n if (el.__stShowGen === gen) el.style.display = 'none';\n });\n}\n\n// Synchronous hide for Turbo before-cache teardown: skip the leave\n// transition entirely so the snapshot never captures a half-closed\n// overlay.\nexport function hideElNow(el) {\n el.__stShowGen = (el.__stShowGen || 0) + 1;\n if (el.__stTransitionCleanup) el.__stTransitionCleanup();\n el.style.display = 'none';\n}\n", "// Minimal focus trap, scoped to what the overlay components (Dialog,\n// AlertDialog, Drawer, Sheet, DropdownMenu) actually need:\n//\n// * on trap: remember the previously focused element, then focus the\n// panel's [autofocus] element, else its first focusable, else the\n// panel itself;\n// * while trapped: Tab / Shift+Tab wrap within the panel;\n// * on release: restore focus to the remembered element (when it is\n// still in the document).\n//\n// Traps stack (a dialog opened from a dropdown), and releasing out of\n// order is tolerated: each release only restores its own remembered\n// element.\n\nconst FOCUSABLE = [\n 'a[href]',\n 'button:not([disabled])',\n 'input:not([disabled]):not([type=\"hidden\"])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]:not([tabindex=\"-1\"])',\n '[contenteditable=\"true\"]',\n].join(',');\n\nfunction focusableIn(container) {\n return Array.from(container.querySelectorAll(FOCUSABLE)).filter((el) => {\n return el.offsetParent !== null || el === document.activeElement\n || window.getComputedStyle(el).position === 'fixed';\n });\n}\n\nfunction focusQuietly(el) {\n if (!el || typeof el.focus !== 'function') return;\n try { el.focus({ preventScroll: true }); } catch (e) { el.focus(); }\n}\n\nconst traps = new Map();\n\nexport function trapFocus(container) {\n if (traps.has(container)) return;\n\n const previouslyFocused = document.activeElement;\n\n const handler = (event) => {\n if (event.key !== 'Tab') return;\n const focusable = focusableIn(container);\n if (!focusable.length) {\n event.preventDefault();\n return;\n }\n const first = focusable[0];\n const last = focusable[focusable.length - 1];\n const active = document.activeElement;\n if (event.shiftKey && (active === first || !container.contains(active))) {\n event.preventDefault();\n focusQuietly(last);\n } else if (!event.shiftKey && (active === last || !container.contains(active))) {\n event.preventDefault();\n focusQuietly(first);\n }\n };\n container.addEventListener('keydown', handler);\n traps.set(container, { previouslyFocused, handler });\n\n const preferred = container.querySelector('[autofocus]');\n focusQuietly(preferred || focusableIn(container)[0] || container);\n}\n\nexport function releaseFocus(container, { restore = true } = {}) {\n const trap = traps.get(container);\n if (!trap) return;\n traps.delete(container);\n container.removeEventListener('keydown', trap.handler);\n const prev = trap.previouslyFocused;\n if (restore && prev && prev.isConnected && typeof prev.focus === 'function') {\n focusQuietly(prev);\n }\n}\n", "// Unique-id generator \u2014 a simple monotonic counter; ids only need to be unique within the\n// current document lifetime (aria-labelledby, aria-controls wiring).\n\nlet counter = 0;\n\nexport function sugarId(prefix) {\n counter += 1;\n return prefix + '-' + counter;\n}\n\n// Ensure `el` has an id (assigning a generated one when missing) and\n// return it \u2014 the common use case: point an aria-* attribute at a\n// slotted child.\nexport function ensureId(el, prefix) {\n if (!el.id) el.id = sugarId(prefix);\n return el.id;\n}\n", "import SugarController from '../lib/sugar_controller.js';\nimport { useScrollLock } from '../lib/scroll_lock.js';\nimport { showEl, hideEl, hideElNow } from '../lib/transition.js';\nimport { trapFocus, releaseFocus } from '../lib/focus_trap.js';\nimport { registerOpenOverlay, unregisterOpenOverlay } from '../lib/overlay_registry.js';\nimport { ensureId } from '../lib/uid.js';\n\n// Modal overlay base \u2014 `st-dialog` and its subclasses (`st-alert-dialog`,\n// `st-sheet`, `st-drawer`) all share this single open/close machine.\n//\n// Three descendant targets model the portal, which stays in place in the\n// DOM (never teleported to ):\n//\n// * portal \u2014 the `fixed inset-0 z-50` wrapper; display toggled wholesale,\n// and promoted to the browser's top layer while open\n// * overlay \u2014 the backdrop; enter/leave transitioned\n// * panel \u2014 the content surface; enter/leave transitioned + focus-trapped\n//\n// `openValue` is the single source of truth; `openValueChanged` is the only\n// render path. It freezes page scroll through the shared ref-counted lock\n// (installed in initialize() so the very first change \u2014 a server-rendered\n// defaultOpen \u2014 can already lock), registers with the overlay registry so\n// Turbo before-cache teardown can close it, and drives the transitions.\nexport default class DialogController extends SugarController {\n static targets = ['portal', 'overlay', 'panel'];\n static values = { open: Boolean };\n\n // Whether clicking the backdrop closes the overlay. Alert Dialog is\n // deliberately sticky (it demands an explicit choice), so it overrides\n // this to false; its overlay carries no click action either.\n dismissOnOverlayClick = true;\n\n initialize() {\n // Install before any value callback fires: the initial\n // openValueChanged (previous === undefined) runs BEFORE connect(),\n // and a defaultOpen overlay must be able to lock right there.\n useScrollLock(this);\n }\n\n connect() {\n super.connect();\n this._wireAriaLabelledby();\n }\n\n disconnect() {\n if (this.hasPanelTarget) releaseFocus(this.panelTarget);\n this._leaveTopLayer();\n unregisterOpenOverlay(this);\n // Scroll unlock is automatic \u2014 useScrollLock wraps disconnect().\n }\n\n open() {\n this.openValue = true;\n }\n\n close() {\n this.openValue = false;\n }\n\n toggle() {\n this.openValue = !this.openValue;\n }\n\n // Chrome fires a phantom `keydown` \u2014 a plain `Event` (not a\n // `KeyboardEvent`), carrying no `key` \u2014 when a native autocomplete/autofill\n // suggestion is committed. Stimulus only applies its `.esc` key filter to\n // real `KeyboardEvent`s, so that synthetic event slips past the filter and\n // would close the overlay. Re-assert the key here: a genuine Escape always\n // reports `key === 'Escape'`; the phantom reports `undefined`.\n closeOnEscape(event) {\n if (event.key !== 'Escape') return;\n if (this.openValue) this.close();\n }\n\n // Wired to `pointerdown` (not `click`) on the backdrop. Committing a\n // native autocomplete/autofill suggestion synthesizes a phantom `click` on\n // the overlay with no matching `pointerdown`; binding dismissal to\n // `pointerdown` means that phantom click can never reach here.\n closeOnOverlay() {\n if (this.dismissOnOverlayClick) this.close();\n }\n\n openValueChanged(open, previous) {\n if (open) {\n // previous === undefined \u21D2 initial server-rendered defaultOpen:\n // lock + trap + register as usual but skip the enter transition \u2014\n // there is nothing to animate from on load.\n this._show(previous === undefined);\n } else if (previous !== undefined) {\n this._hide();\n }\n }\n\n _show(immediate) {\n this.lockScroll();\n registerOpenOverlay(this);\n if (this.hasPortalTarget) {\n // Display first: the top layer paints the portal as-is, so it has\n // to be renderable before it is promoted.\n this.portalTarget.style.display = '';\n this._enterTopLayer();\n }\n this._setState('open');\n if (immediate) {\n if (this.hasOverlayTarget) this.overlayTarget.style.display = '';\n if (this.hasPanelTarget) this.panelTarget.style.display = '';\n } else {\n if (this.hasOverlayTarget) showEl(this.overlayTarget);\n if (this.hasPanelTarget) showEl(this.panelTarget);\n }\n if (this.hasPanelTarget) trapFocus(this.panelTarget);\n }\n\n _hide() {\n if (this.hasPanelTarget) releaseFocus(this.panelTarget);\n unregisterOpenOverlay(this);\n this._setState('closed');\n const settling = [];\n if (this.hasOverlayTarget) settling.push(hideEl(this.overlayTarget));\n if (this.hasPanelTarget) settling.push(hideEl(this.panelTarget));\n Promise.all(settling).then(() => {\n // A reopen may have raced the leave transition \u2014 only hide the\n // portal if we are still closed. Staying in the top layer until\n // here is what lets the leave transition play out on top.\n if (!this.openValue && this.hasPortalTarget) {\n this.portalTarget.style.display = 'none';\n this._leaveTopLayer();\n }\n });\n this.unlockScroll();\n }\n\n // Synchronous teardown for the Turbo before-cache hook: skip every\n // transition so the snapshot never captures a half-closed overlay.\n closeNow() {\n if (this.hasPanelTarget) {\n hideElNow(this.panelTarget);\n releaseFocus(this.panelTarget);\n }\n if (this.hasOverlayTarget) hideElNow(this.overlayTarget);\n if (this.hasPortalTarget) {\n this.portalTarget.style.display = 'none';\n this._leaveTopLayer();\n }\n unregisterOpenOverlay(this);\n this.unlockScroll();\n this._setState('closed');\n this.openValue = false;\n }\n\n // The portal carries `popover=\"manual\"`, so an open overlay is promoted\n // into the browser's top layer instead of relying on `z-50` alone. A\n // z-index only competes inside its own stacking context, so any\n // positioned ancestor \u2014 a `sticky` header, a `transform`ed card \u2014 traps\n // the overlay under the page chrome no matter how high we push it. The\n // top layer paints above every stacking context by definition.\n //\n // `manual` (not `auto`) is deliberate: the UA then adds no light-dismiss\n // and no Esc handling of its own, so Dialog keeps Esc and backdrop-click\n // in this controller and AlertDialog stays persistent.\n _enterTopLayer() {\n const portal = this._topLayerPortal();\n if (!portal || portal.matches(':popover-open')) return;\n try {\n portal.showPopover();\n } catch {\n // Nothing to promote (detached mid-flight) \u2014 the in-place `z-50`\n // painting still applies. Degraded, never broken.\n }\n }\n\n _leaveTopLayer() {\n const portal = this._topLayerPortal();\n if (!portal || !portal.matches(':popover-open')) return;\n try {\n portal.hidePopover();\n } catch {\n // Already dismissed by the UA (page teardown) \u2014 nothing to undo.\n }\n }\n\n // Browsers without the Popover API return null here and keep the plain\n // `z-50` behaviour, as does any custom portal markup without the\n // attribute.\n _topLayerPortal() {\n if (!this.hasPortalTarget) return null;\n const portal = this.portalTarget;\n return portal.hasAttribute('popover') && typeof portal.showPopover === 'function' ? portal : null;\n }\n\n _setState(state) {\n if (this.hasOverlayTarget) this.overlayTarget.dataset.state = state;\n if (this.hasPanelTarget) this.panelTarget.dataset.state = state;\n }\n\n // Point the labelled overlay wrapper (role dialog/alertdialog) at its\n // title \u2014 the Stimulus replacement for `:aria-labelledby=\"$id(...)\"`.\n // Sheet and Drawer wrappers carry no role, so they are left untouched.\n _wireAriaLabelledby() {\n if (!this.hasPortalTarget || !this.hasPanelTarget) return;\n const role = this.portalTarget.getAttribute('role');\n if (role !== 'dialog' && role !== 'alertdialog') return;\n const title = this.panelTarget.querySelector('[data-slot$=\"-title\"]');\n if (title) {\n this.portalTarget.setAttribute('aria-labelledby', ensureId(title, role + '-title'));\n }\n }\n}\n", "import DialogController from './dialog_controller.js';\n\n// AlertDialog \u2014 `st-alert-dialog`. Identical machinery to Dialog, but it\n// interrupts the user for a decision, so a backdrop click must NOT dismiss\n// it. The overlay template carries no click action; this flag is the\n// belt-and-braces guard.\nexport default class AlertDialogController extends DialogController {\n dismissOnOverlayClick = false;\n}\n", "// Date helpers shared by Calendar / DatePicker.\n\n// Long-format date with English ordinal suffix.\n//\n// shadcn/ui's React Date Picker renders the trigger as\n// `format(date, \"PPP\")` from date-fns, which in English produces\n// \"April 8th, 2026\" (with the ordinal \"th\"/\"st\"/\"nd\"/\"rd\"). The browser's\n// `toLocaleDateString` does not include the ordinal, so we synthesise it\n// via Intl.PluralRules whenever the resolved locale starts with `en`.\n// Every other locale falls through to the native long format\n// (\"8 de abril de 2026\", \"2026\u5E744\u67088\u65E5\").\nexport function formatLongDate(d, lang) {\n const resolved = lang || 'en';\n const primary = String(resolved).toLowerCase().split('-')[0];\n\n if (primary !== 'en') {\n return d.toLocaleDateString(resolved, {\n month: 'long', day: 'numeric', year: 'numeric',\n });\n }\n\n const dtf = new Intl.DateTimeFormat(resolved, {\n month: 'long', day: 'numeric', year: 'numeric',\n });\n const parts = dtf.formatToParts(d);\n const pr = new Intl.PluralRules(resolved, { type: 'ordinal' });\n const suffixes = { one: 'st', two: 'nd', few: 'rd', other: 'th' };\n\n return parts.map((p) => {\n if (p.type !== 'day') return p.value;\n const n = parseInt(p.value, 10);\n return n + (suffixes[pr.select(n)] || 'th');\n }).join('');\n}\n\n// Locale resolution for Calendar / DatePicker.\n//\n// Priority order, first non-empty wins:\n// 1. The component's own `data-lang` (when explicitly set).\n// 2. The closest ancestor with a non-empty `data-lang` \u2014 lets a manual\n// `
` composition drive the\n// inner calendar without repeating the prop.\n// 3. The document's `` \u2014 picks up a server-rendered app\n// locale automatically.\n// 4. Hard-coded fallback `'en'` \u2014 keeps rendering deterministic when\n// nothing else is configured.\nexport function resolveCalendarLang(el, ownLang) {\n if (ownLang) return ownLang;\n\n let node = el.parentElement && el.parentElement.closest('[data-lang]');\n while (node) {\n if (node.dataset.lang) return node.dataset.lang;\n node = node.parentElement && node.parentElement.closest('[data-lang]');\n }\n\n const htmlLang = (document.documentElement && document.documentElement.lang) || '';\n if (htmlLang) return htmlLang;\n\n return 'en';\n}\n", "import SugarController from '../lib/sugar_controller.js';\nimport { resolveCalendarLang } from '../lib/dates.js';\n\n// Calendar \u2014 `st-calendar` on the [data-slot=calendar] root. A CSP-safe,\n// self-contained month-grid\n// state machine (visible month/year, single + range selection, min/max\n// year, week-starts-on, caption dropdowns, optional time input, keyboard\n// nav across day cells).\n//\n// A single render() pass rebuilds the visible grid(s) imperatively from\n// state, cloning the `