{"version":3,"file":"shadow.es.mjs","names":[],"sources":["../src/shadow/bodyScrollLock.ts","../src/shadow/getDeepActiveElement.ts","../src/shadow/getFocusableElements.ts","../src/shadow/focusTrapRegistry.ts","../src/shadow/generateUniqueId.ts"],"sourcesContent":["interface BodyStyleSnapshot {\n  overflow: string\n  width: string\n  height: string\n  touchAction: string\n  overscrollBehavior: string\n}\n\n// Module-scoped, reference-counted lock state (per bundle). Implemented with\n// inline body styles rather than CSS so it works even when all component CSS is\n// isolated inside a ShadowRoot, where styling document.body via CSS is\n// impossible. Multiple keys (e.g. concurrent drawer instances) share one lock.\nconst activeKeys = new Set<string>()\nlet original: BodyStyleSnapshot | null = null\n\n/**\n * Reference-counted body scroll lock for modals/drawers, keyed per instance.\n *\n * The first `lock` snapshots and overwrites `document.body` inline styles to\n * disable scrolling; the last matching `unlock` restores them. Safe to call\n * with an unknown key and in non-browser environments (no-ops).\n */\nexport const bodyScrollLock = {\n  /**\n   * Acquires the lock for `key`. The first active key applies the scroll lock.\n   * @param {string} key - Unique per-instance lock key.\n   * @returns {void} Nothing.\n   */\n  lock(key: string): void {\n    if (typeof document === 'undefined' || !document.body) return\n    if (activeKeys.has(key)) return\n\n    if (activeKeys.size === 0) {\n      const style = document.body.style\n      original = {\n        overflow: style.overflow,\n        width: style.width,\n        height: style.height,\n        touchAction: style.touchAction,\n        overscrollBehavior: style.overscrollBehavior,\n      }\n      style.width = '100vw'\n      style.height = '100vh'\n      style.overflow = 'hidden'\n      style.overscrollBehavior = 'none'\n      style.touchAction = 'none'\n    }\n\n    activeKeys.add(key)\n  },\n\n  /**\n   * Releases the lock for `key`. Restores body styles once no keys remain.\n   * @param {string} key - Unique per-instance lock key.\n   * @returns {void} Nothing.\n   */\n  unlock(key: string): void {\n    if (typeof document === 'undefined' || !document.body) return\n    if (!activeKeys.has(key)) return\n\n    activeKeys.delete(key)\n\n    if (activeKeys.size === 0 && original) {\n      const style = document.body.style\n      style.overflow = original.overflow\n      style.width = original.width\n      style.height = original.height\n      style.touchAction = original.touchAction\n      style.overscrollBehavior = original.overscrollBehavior\n      original = null\n    }\n  },\n}\n","/**\n * Resolves the deepest focused element across nested shadow roots.\n *\n * `document.activeElement` only reports the focused node in the top-level\n * document; when focus lives inside a shadow tree it returns that tree's HOST,\n * not the actual focused element. Widgets that render into a ShadowRoot almost\n * never want the raw value. Walking `activeElement.shadowRoot.activeElement`\n * recovers the real target.\n * @returns {HTMLElement | null} The innermost focused element, or null.\n */\nexport const getDeepActiveElement = (): HTMLElement | null => {\n  if (typeof document === 'undefined') return null\n\n  let active: Element | null = document.activeElement\n  while (active?.shadowRoot?.activeElement) {\n    active = active.shadowRoot.activeElement\n  }\n\n  return (active as HTMLElement | null) ?? null\n}\n","/**\n * Returns the focusable elements inside a container, in DOM order.\n *\n * Matches the common interactive selectors (links, enabled form controls, and\n * anything with a non-negative tabindex) and filters out disabled or\n * `tabindex=\"-1\"` nodes. Useful for focus trapping inside dialogs/drawers.\n * @param {HTMLElement} container - The container to search.\n * @returns {HTMLElement[]} Focusable elements in document order.\n */\nexport const getFocusableElements = (container: HTMLElement): HTMLElement[] => {\n  const selector = [\n    'a[href]',\n    'button:not([disabled])',\n    'textarea:not([disabled])',\n    'input:not([disabled])',\n    'select:not([disabled])',\n    '[tabindex]:not([tabindex=\"-1\"])',\n  ].join(',')\n\n  return Array.from(container.querySelectorAll<HTMLElement>(selector)).filter(\n    (el) => !el.hasAttribute('disabled') && el.tabIndex !== -1,\n  )\n}\n","import type { RegisteredFocusTrap } from '../types/shadow/RegisteredFocusTrap'\nimport { getDeepActiveElement } from './getDeepActiveElement'\nimport { getFocusableElements } from './getFocusableElements'\n\n// Module-scoped stack of active traps and a once-attached global Tab listener.\nconst focusTraps: RegisteredFocusTrap[] = []\nlet isTabListenerAttached = false\n\n/**\n * Stack-based focus trap registry for stacked dialogs/drawers.\n *\n * The most recently registered trap is the active one; Tab/Shift+Tab cycle\n * focus within its container, resolving the current focus across shadow roots\n * (so a widget rendered into a ShadowRoot traps correctly). A single global\n * keydown listener is attached lazily on first registration.\n */\nexport const focusTrapRegistry = {\n  /**\n   * Attaches the global Tab listener exactly once (no-op without a DOM).\n   * @returns {void} Nothing.\n   */\n  ensureTabListener(): void {\n    if (typeof document === 'undefined' || isTabListenerAttached) return\n\n    /**\n     * Traps Tab navigation inside the top-most registered container.\n     * @param {KeyboardEvent} event - The keydown event.\n     * @returns {void} Nothing.\n     */\n    const handleKeyDown = (event: KeyboardEvent): void => {\n      if (event.key !== 'Tab') return\n\n      const latest = focusTraps[focusTraps.length - 1]\n      const container = latest?.getContainer()\n      if (!container) return\n\n      const focusable = getFocusableElements(container)\n      if (focusable.length === 0) return\n\n      const active = getDeepActiveElement()\n      const currentIndex = active ? focusable.indexOf(active) : -1\n\n      const nextIndex = event.shiftKey\n        ? currentIndex <= 0\n          ? focusable.length - 1\n          : currentIndex - 1\n        : currentIndex === -1 || currentIndex >= focusable.length - 1\n          ? 0\n          : currentIndex + 1\n\n      event.preventDefault()\n      focusable[nextIndex]?.focus()\n    }\n\n    document.addEventListener('keydown', handleKeyDown)\n    isTabListenerAttached = true\n  },\n\n  /**\n   * Registers a trap as the newest (de-duplicated by id) and ensures the\n   * global Tab listener is attached.\n   * @param {RegisteredFocusTrap} trap - The trap to register.\n   * @returns {void} Nothing.\n   */\n  register(trap: RegisteredFocusTrap): void {\n    const existingIndex = focusTraps.findIndex((t) => t.id === trap.id)\n    if (existingIndex !== -1) focusTraps.splice(existingIndex, 1)\n    focusTraps.push(trap)\n    this.ensureTabListener()\n  },\n\n  /**\n   * Removes a trap by id.\n   * @param {string} id - The trap id.\n   * @returns {void} Nothing.\n   */\n  unregister(id: string): void {\n    const index = focusTraps.findIndex((t) => t.id === id)\n    if (index !== -1) focusTraps.splice(index, 1)\n  },\n\n  /**\n   * Returns the id of the current top-most trap, if any.\n   * @returns {string | null} The top-most trap id, or null.\n   */\n  getTopMostId(): string | null {\n    return focusTraps[focusTraps.length - 1]?.id ?? null\n  },\n}\n","/**\n * Generates a short, collision-resistant id safe for use as a CSS-class prefix\n * or element id.\n *\n * Prefers `crypto.randomUUID()` (first 12 hex chars) so many concurrent widget\n * instances do not share scroll-lock / focus / class-prefix keys; falls back to\n * two base36 segments when Web Crypto is unavailable (older webviews).\n * @returns {string} A unique, identifier-safe id.\n */\nexport const generateUniqueId = (): string => {\n  const webCrypto = typeof crypto !== 'undefined' ? crypto : undefined\n  if (webCrypto?.randomUUID) {\n    return webCrypto.randomUUID().replace(/-/g, '').slice(0, 12)\n  }\n\n  return (\n    Math.random().toString(36).slice(2, 8) +\n    Math.random().toString(36).slice(2, 8)\n  )\n}\n"],"mappings":"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"}