@vielzeug/refine 2.1.3 → 2.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/accordion.cjs +1 -1
- package/dist/accordion.cjs.map +1 -1
- package/dist/accordion.js +2 -2
- package/dist/accordion.js.map +1 -1
- package/dist/carousel.cjs +25 -24
- package/dist/carousel.cjs.map +1 -1
- package/dist/carousel.js +125 -135
- package/dist/carousel.js.map +1 -1
- package/dist/combobox.cjs +14 -14
- package/dist/combobox.cjs.map +1 -1
- package/dist/combobox.js +88 -98
- package/dist/combobox.js.map +1 -1
- package/dist/content/carousel/carousel.d.ts.map +1 -1
- package/dist/content/list/_list-item-reveal.cjs +1 -1
- package/dist/content/list/_list-item-reveal.cjs.map +1 -1
- package/dist/content/list/_list-item-reveal.d.ts +3 -3
- package/dist/content/list/_list-item-reveal.d.ts.map +1 -1
- package/dist/content/list/_list-item-reveal.js +22 -22
- package/dist/content/list/_list-item-reveal.js.map +1 -1
- package/dist/content/list/list-item.d.ts.map +1 -1
- package/dist/core/focus.cjs +1 -1
- package/dist/core/focus.cjs.map +1 -1
- package/dist/core/focus.d.ts.map +1 -1
- package/dist/core/focus.js +21 -20
- package/dist/core/focus.js.map +1 -1
- package/dist/core/index.d.ts +2 -3
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/nav.cjs +1 -1
- package/dist/core/nav.cjs.map +1 -1
- package/dist/core/nav.d.ts +3 -62
- package/dist/core/nav.d.ts.map +1 -1
- package/dist/core/nav.js +41 -124
- package/dist/core/nav.js.map +1 -1
- package/dist/core/option-list.cjs +1 -1
- package/dist/core/option-list.cjs.map +1 -1
- package/dist/core/option-list.d.ts +5 -5
- package/dist/core/option-list.d.ts.map +1 -1
- package/dist/core/option-list.js +14 -13
- package/dist/core/option-list.js.map +1 -1
- package/dist/core/overlay.cjs +1 -1
- package/dist/core/overlay.cjs.map +1 -1
- package/dist/core/overlay.d.ts +0 -8
- package/dist/core/overlay.d.ts.map +1 -1
- package/dist/core/overlay.js +1 -3
- package/dist/core/overlay.js.map +1 -1
- package/dist/custom-elements.json +5636 -5683
- package/dist/drawer.cjs +6 -6
- package/dist/drawer.cjs.map +1 -1
- package/dist/drawer.js +65 -75
- package/dist/drawer.js.map +1 -1
- package/dist/feedback/skeleton/skeleton.css.cjs +1 -1
- package/dist/feedback/skeleton/skeleton.css.cjs.map +1 -1
- package/dist/feedback/skeleton/skeleton.css.js +1 -1
- package/dist/feedback/skeleton/skeleton.css.js.map +1 -1
- package/dist/feedback/skeleton/skeleton.d.ts.map +1 -1
- package/dist/feedback/toast/toast.d.ts.map +1 -1
- package/dist/grid.cjs +2 -2
- package/dist/grid.cjs.map +1 -1
- package/dist/grid.js +42 -41
- package/dist/grid.js.map +1 -1
- package/dist/input.cjs.map +1 -1
- package/dist/input.js.map +1 -1
- package/dist/inputs/input/input.css.cjs +1 -1
- package/dist/inputs/input/input.css.cjs.map +1 -1
- package/dist/inputs/input/input.css.js +1 -1
- package/dist/inputs/input/input.css.js.map +1 -1
- package/dist/inputs/input/input.d.ts +2 -0
- package/dist/inputs/input/input.d.ts.map +1 -1
- package/dist/inputs/select/select.d.ts +2 -2
- package/dist/inputs/select/select.d.ts.map +1 -1
- package/dist/layout/grid/grid.d.ts.map +1 -1
- package/dist/layout/navbar/navbar.d.ts.map +1 -1
- package/dist/layout/sidebar/sidebar.d.ts.map +1 -1
- package/dist/list-item.cjs +2 -6
- package/dist/list-item.cjs.map +1 -1
- package/dist/list-item.js +40 -39
- package/dist/list-item.js.map +1 -1
- package/dist/list.cjs +1 -1
- package/dist/list.cjs.map +1 -1
- package/dist/list.js +2 -2
- package/dist/list.js.map +1 -1
- package/dist/marquee.cjs +1 -1
- package/dist/marquee.cjs.map +1 -1
- package/dist/marquee.js +3 -1
- package/dist/marquee.js.map +1 -1
- package/dist/menu.cjs +5 -5
- package/dist/menu.cjs.map +1 -1
- package/dist/menu.js +59 -59
- package/dist/menu.js.map +1 -1
- package/dist/navbar.cjs +9 -9
- package/dist/navbar.cjs.map +1 -1
- package/dist/navbar.js +124 -116
- package/dist/navbar.js.map +1 -1
- package/dist/otp-input.cjs +1 -1
- package/dist/otp-input.cjs.map +1 -1
- package/dist/otp-input.js +2 -2
- package/dist/otp-input.js.map +1 -1
- package/dist/overlay/drawer/drawer.d.ts.map +1 -1
- package/dist/overlay/menu/menu.d.ts.map +1 -1
- package/dist/radio-group.cjs +1 -1
- package/dist/radio-group.cjs.map +1 -1
- package/dist/radio-group.js +2 -3
- package/dist/radio-group.js.map +1 -1
- package/dist/radio.cjs +1 -1
- package/dist/radio.cjs.map +1 -1
- package/dist/radio.js +12 -11
- package/dist/radio.js.map +1 -1
- package/dist/rating.cjs +1 -1
- package/dist/rating.cjs.map +1 -1
- package/dist/rating.js +1 -1
- package/dist/rating.js.map +1 -1
- package/dist/refine.cjs +188 -195
- package/dist/refine.cjs.map +1 -1
- package/dist/refine.iife.js +212 -219
- package/dist/refine.iife.js.map +1 -1
- package/dist/refine.js +392 -399
- package/dist/refine.js.map +1 -1
- package/dist/select.cjs +1 -1
- package/dist/select.cjs.map +1 -1
- package/dist/select.js +1 -0
- package/dist/select.js.map +1 -1
- package/dist/sidebar.cjs +16 -16
- package/dist/sidebar.cjs.map +1 -1
- package/dist/sidebar.js +106 -91
- package/dist/sidebar.js.map +1 -1
- package/dist/skeleton.cjs +3 -3
- package/dist/skeleton.cjs.map +1 -1
- package/dist/skeleton.js +8 -7
- package/dist/skeleton.js.map +1 -1
- package/dist/stepper.cjs +1 -1
- package/dist/stepper.cjs.map +1 -1
- package/dist/stepper.js +3 -3
- package/dist/stepper.js.map +1 -1
- package/dist/styles/fouc.css +22 -0
- package/dist/styles/preflight.css +2 -10
- package/dist/tabs.cjs +1 -1
- package/dist/tabs.cjs.map +1 -1
- package/dist/tabs.js +4 -5
- package/dist/tabs.js.map +1 -1
- package/dist/toast.cjs +13 -17
- package/dist/toast.cjs.map +1 -1
- package/dist/toast.js +86 -85
- package/dist/toast.js.map +1 -1
- package/package.json +18 -11
- package/dist/core/swipe.cjs +0 -2
- package/dist/core/swipe.cjs.map +0 -1
- package/dist/core/swipe.d.ts +0 -40
- package/dist/core/swipe.d.ts.map +0 -1
- package/dist/core/swipe.js +0 -63
- package/dist/core/swipe.js.map +0 -1
- package/dist/core/typeahead.cjs +0 -2
- package/dist/core/typeahead.cjs.map +0 -1
- package/dist/core/typeahead.d.ts +0 -53
- package/dist/core/typeahead.d.ts.map +0 -1
- package/dist/core/typeahead.js +0 -23
- package/dist/core/typeahead.js.map +0 -1
package/dist/navbar.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"navbar.js","names":[],"sources":["../src/layout/navbar/navbar.ts"],"sourcesContent":["import {\n bind,\n createContext,\n define,\n getHost,\n html,\n inject,\n onMounted,\n prop,\n provide,\n resizeObserver,\n useEmit,\n useSlots,\n} from '@vielzeug/ore';\nimport { computed, type Readable, signal, watch } from '@vielzeug/ripple';\n\nimport type { ElevationLevel, RoundedSize, ThemeColor, VisualVariant } from '../../types';\n\nimport '../../content/icon/icon';\nimport {\n coarsePointerMixin,\n colorThemeMixin,\n elevationMixin,\n frostVariantMixin,\n reducedMotionMixin,\n roundedVariantMixin,\n} from '../../styles';\nimport { computeSafeRel } from '../../utils';\nimport navbarStyles from './navbar.css?inline';\n\nconst listen = <E extends Event = Event>(\n el: EventTarget | null | undefined,\n name: string,\n handler: (e: E) => void,\n options?: AddEventListenerOptions,\n): (() => void) => {\n if (!el) return () => {};\n\n el.addEventListener(name, handler as EventListener, options);\n\n return () => el.removeEventListener(name, handler as EventListener, options);\n};\n\ntype NavbarMode = 'floating' | 'sticky';\n\nconst hasElementContent = (el: Element): boolean => {\n if (!(el instanceof HTMLElement)) return true;\n\n if (el.tagName.includes('-')) return true;\n\n if (el.childElementCount > 0) return true;\n\n return (el.textContent ?? '').trim().length > 0;\n};\n\nconst parseMaxWidthPx = (query: string | undefined): number | undefined => {\n const value = String(query ?? '').trim();\n\n if (!value) return undefined;\n\n const match = /max-width\\s*:\\s*([0-9]+(?:\\.[0-9]+)?)px/i.exec(value);\n\n if (!match) return undefined;\n\n const parsed = Number.parseFloat(match[1]);\n\n return Number.isFinite(parsed) ? parsed : undefined;\n};\n\nconst resolveContainerElement = (el: HTMLElement): HTMLElement | null => {\n let container = el.parentElement;\n\n while (container?.tagName.toLowerCase() === 'ore-grid-item') {\n container = container.parentElement;\n }\n\n return container;\n};\n\nconst readContainerWidth = (el: HTMLElement): number => {\n const parentWidth = resolveContainerElement(el)?.clientWidth ?? 0;\n\n if (parentWidth > 0) return parentWidth;\n\n return el.offsetWidth;\n};\n\nconst findScrollContainer = (start: HTMLElement): HTMLElement | null => {\n let current: HTMLElement | null = start.parentElement;\n\n while (current) {\n const styles = window.getComputedStyle(current);\n const overflowY = styles.overflowY;\n\n if (overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay') {\n return current;\n }\n\n current = current.parentElement;\n }\n\n return null;\n};\n\n/** Context provided by `ore-navbar` to `ore-navbar-item` children. */\nexport type NavbarContext = {\n closeMobileMenu: () => void;\n isMobile: Readable<boolean>;\n mobileMenuOpen: Readable<boolean>;\n};\n\n/** Injection key for navbar context. */\nexport const NAVBAR_CTX = createContext<NavbarContext>('NavbarContext');\n\n/** Navbar component events */\nexport type OreNavbarEvents = {\n 'mobile-menu-change': { open: boolean };\n};\n\n/** Navbar component element interface */\nexport type NavbarElement = HTMLElement &\n OreNavbarProps & {\n closeMobileMenu(): void;\n openMobileMenu(): void;\n toggleMobileMenu(): void;\n };\n\ntype MobileSidebarElement = HTMLElement & {\n closeMobile?: () => void;\n openMobile?: () => void;\n toggleMobile?: () => void;\n};\n\n/** Navbar component properties */\nexport type OreNavbarProps = {\n /** CSS media query used for mobile layout switching */\n breakpoint?: string;\n /** Theme color */\n color?: ThemeColor;\n /** Evaluate mobile breakpoint against container width only when possible. */\n 'container-breakpoints'?: boolean;\n /** Shadow elevation level (0-5) */\n elevation?: ElevationLevel;\n /** Start as detached floating navbar */\n floating?: boolean;\n /** Accessible nav landmark label */\n label?: string;\n /** CSS selector for an external sidebar toggled by the mobile button */\n 'mobile-sidebar'?: string;\n /** Border radius size */\n rounded?: RoundedSize;\n /** Scroll threshold in px used by floating+sticky transition */\n 'scroll-threshold'?: number;\n /** Stick to top of viewport */\n sticky?: boolean;\n /** Visual style variant */\n variant?: Exclude<VisualVariant, 'ghost' | 'text'>;\n};\n\n/** Navbar item properties */\nexport type OreNavbarItemProps = {\n /** Whether this item represents the current page */\n active?: boolean;\n /** Whether this item is disabled */\n disabled?: boolean;\n /** Link target URL. Renders an anchor when provided. */\n href?: string;\n /**\n * Icon-only mode — hides the (empty) label region and switches to compact, symmetric\n * padding instead of the icon+label layout's wider horizontal padding. Set this whenever\n * the default slot is intentionally left empty (name conveyed via `aria-label` instead),\n * otherwise the empty label still reserves its inter-element gap and the item ends up\n * with lopsided padding around the icon.\n */\n 'icon-only'?: boolean;\n /** Relationship of linked URL (anchor-only) */\n rel?: string;\n /** Browsing context for link (anchor-only) */\n target?: string;\n};\n\n/**\n * `ore-navbar` — Responsive navigation with sticky/floating modes and a mobile overflow panel.\n *\n * @element ore-navbar\n * @element ore-navbar-item - Navigation link or button placed in the default or mobile-menu slot\n *\n * @attr {string} label - Accessible nav landmark label\n * @attr {boolean} sticky - Makes navbar sticky at top\n * @attr {boolean} floating - Makes navbar detached and floating\n * @attr {number} scroll-threshold - Scroll threshold for floating+sticky transition\n * @attr {string} breakpoint - CSS media query used for mobile mode\n * @attr {boolean} container-breakpoints - Evaluates parseable max-width breakpoints against container width\n * @attr {string} variant - Visual variant: 'flat' | 'solid' | 'bordered' | 'outline' | 'frost' | 'glass'\n * @attr {string} color - Theme color: 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error'\n * @attr {string} rounded - Border radius: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'full'\n * @attr {string} elevation - Shadow elevation: '0' | '1' | '2' | '3' | '4' | '5'\n *\n * @fires mobile-menu-change - Emitted when mobile menu open state changes. detail: { open: boolean }\n *\n * @slot logo - Brand/logo content rendered at the leading edge\n * @slot start - Content rendered in the left/start region\n * @slot - Center navigation content rendered between the start and end regions\n * @slot end - Content rendered in the right/end region\n * @slot mobile-menu - Controls and links rendered inside the mobile overflow panel\n *\n * @cssprop --navbar-height - Navbar bar height\n * @cssprop --navbar-width - Max width of the navbar content\n * @cssprop --navbar-offset - Horizontal offset for floating/sticky positioning\n * @cssprop --navbar-bg - Navbar background color\n * @cssprop --navbar-color - Navbar text color\n * @cssprop --navbar-border-color - Navbar border color\n * @cssprop --navbar-shadow - Navbar box shadow\n * @cssprop --navbar-radius - Border radius (floating/sticky modes)\n * @cssprop --navbar-backdrop-filter - Backdrop filter for blur effect\n * @cssprop --navbar-item-color - Nav item text color\n * @cssprop --navbar-item-hover-color - Nav item hover text color\n * @cssprop --navbar-item-hover-bg - Nav item hover background\n * @cssprop --navbar-item-active-color - Nav item active/current text color\n * @cssprop --navbar-item-active-bg - Nav item active/current background\n * @part nav - Outer navigation landmark\n * @part bar - Main navbar row container\n * @part logo - Logo slot container\n * @part start - Leading action/content region\n * @part center - Center content region\n * @part end - Trailing action/content region\n * @part mobile-toggle - Mobile menu toggle button\n * @part mobile-menu - Mobile overflow panel\n * @part item-icon - Leading icon inside navbar items\n * @part item-label - Label text inside navbar items\n * @part item-end - Trailing content inside navbar items\n * @part item - Clickable navbar item root\n * @example\n * ```html\n * <ore-navbar sticky elevation=\"1\">\n * <a href=\"/\" slot=\"logo\"><strong>MyApp</strong></a>\n * <ore-navbar-item href=\"/dashboard\">Dashboard</ore-navbar-item>\n * <ore-navbar-item href=\"/docs\">Docs</ore-navbar-item>\n * <ore-button slot=\"end\" variant=\"solid\" color=\"primary\" size=\"sm\">Sign in</ore-button>\n * </ore-navbar>\n * ```\n */\nexport const NAVBAR_TAG = 'ore-navbar' as const;\ndefine<OreNavbarProps>(NAVBAR_TAG, {\n props: {\n breakpoint: prop.string('(max-width: 768px)'),\n color: prop.string<ThemeColor>(),\n 'container-breakpoints': prop.bool(false),\n elevation: prop.number<ElevationLevel>(),\n floating: prop.bool(false),\n label: prop.string('Main navigation'),\n 'mobile-sidebar': prop.string(),\n rounded: prop.string<RoundedSize>(),\n 'scroll-threshold': prop.number(80),\n sticky: prop.bool(false),\n variant: prop.string<'flat' | 'solid' | 'bordered' | 'outline' | 'frost'>(),\n },\n setup(props) {\n const el = getHost();\n const emit = useEmit<OreNavbarEvents>();\n const slots = useSlots();\n\n const hasLogo = slots.has('logo');\n const mobileMenuElements = slots.elements('mobile-menu');\n const hasMobileMenu = signal(false);\n const mobileSidebarTarget = signal<MobileSidebarElement | null>(null);\n const isExternalMobileMode = signal(false);\n const isExternalMobileOpen = signal(false);\n const isMobile = signal(false);\n const isMobileMenuOpen = signal(false);\n const isModeTransitioning = signal(false);\n const isScrolled = signal(false);\n const scrollBase = signal(0);\n const mediaMatches = signal(false);\n const sizeMatches = signal(false);\n const maxWidthPx = signal<number | undefined>(parseMaxWidthPx(props.breakpoint.value));\n const isPreviewMode = signal(false);\n\n const getExternalSidebar = () => {\n const selector = String(props['mobile-sidebar'].value ?? '').trim();\n\n if (!selector) return null;\n\n const root = el.getRootNode();\n const scopedTarget =\n root instanceof ShadowRoot || root instanceof Document\n ? (root.querySelector(selector) as MobileSidebarElement | null)\n : null;\n\n return scopedTarget ?? (document.querySelector(selector) as MobileSidebarElement | null);\n };\n\n provide(NAVBAR_CTX, {\n // Wrapped, not passed directly — `closeMobileMenu` (below) isn't defined yet at this point\n // in `setup()`; by the time `ore-navbar-item` actually calls this (a later click), it is.\n closeMobileMenu: () => closeMobileMenu(),\n isMobile: computed(() => isMobile.value) as Readable<boolean>,\n mobileMenuOpen: computed(() => isMobileMenuOpen.value) as Readable<boolean>,\n });\n\n const computeMode = (): NavbarMode | undefined => {\n if (props.floating.value && props.sticky.value) {\n return isScrolled.value ? 'sticky' : 'floating';\n }\n\n if (props.floating.value) return 'floating';\n\n if (props.sticky.value) return 'sticky';\n\n return undefined;\n };\n\n const setMobileMenu = (next: boolean) => {\n const open = Boolean(next);\n\n if (!hasMobileMenu.value) {\n const target = mobileSidebarTarget.value ?? getExternalSidebar();\n\n if (target) {\n if (open) {\n target.openMobile?.();\n } else {\n target.closeMobile?.();\n }\n\n return;\n }\n }\n\n if (open && !hasMobileMenu.value) return;\n\n if (isMobileMenuOpen.value === open) return;\n\n isMobileMenuOpen.value = open;\n emit('mobile-menu-change', { open });\n };\n\n const closeMobileMenu = () => setMobileMenu(false);\n const openMobileMenu = () => setMobileMenu(true);\n const toggleMobileMenu = () => {\n if (!hasMobileMenu.value) {\n const target = mobileSidebarTarget.value ?? getExternalSidebar();\n\n if (target) {\n if (typeof target.toggleMobile === 'function') {\n target.toggleMobile();\n } else if (typeof target.openMobile === 'function' && typeof target.closeMobile === 'function') {\n const isOpen = target.hasAttribute('data-mobile-open');\n\n if (isOpen) target.closeMobile();\n else target.openMobile();\n }\n\n return;\n }\n }\n\n setMobileMenu(!isMobileMenuOpen.value);\n };\n\n const syncMobileMode = () => {\n const next =\n (mobileSidebarTarget.value && !hasMobileMenu.value ? isExternalMobileMode.value : false) ||\n mediaMatches.value ||\n sizeMatches.value;\n\n isMobile.value = next;\n\n if (!next) closeMobileMenu();\n };\n\n const readScrollOffset = (scrollContainer: HTMLElement | null): number => {\n if (scrollContainer) {\n return scrollContainer.scrollTop || 0;\n }\n\n return window.scrollY || document.documentElement.scrollTop || 0;\n };\n\n const resetScrollBase = (scrollContainer: HTMLElement | null) => {\n scrollBase.value = readScrollOffset(scrollContainer);\n };\n\n const updateScrolled = (scrollContainer: HTMLElement | null = null) => {\n if (!(props.floating.value && props.sticky.value)) {\n if (isScrolled.value) isScrolled.value = false;\n\n return;\n }\n\n const threshold = Math.max(0, Number(props['scroll-threshold'].value) || 0);\n const currentOffset = readScrollOffset(scrollContainer);\n const delta = currentOffset - scrollBase.value;\n\n isScrolled.value = delta > threshold;\n };\n\n bind({\n attr: {\n 'data-mobile': () => (isMobile.value ? true : undefined),\n 'data-mobile-open': () => (isMobile.value && isMobileMenuOpen.value ? true : undefined),\n 'data-mode': () => computeMode() ?? null,\n 'data-mode-transitioning': () => (isModeTransitioning.value ? true : undefined),\n 'data-preview-mode': () => (isPreviewMode.value ? true : undefined),\n 'data-scrolled': () => (props.floating.value && props.sticky.value && isScrolled.value ? true : undefined),\n },\n });\n\n onMounted(() => {\n const navbarEl = el as NavbarElement;\n const scrollContainer = findScrollContainer(el);\n\n navbarEl.closeMobileMenu = closeMobileMenu;\n navbarEl.openMobileMenu = openMobileMenu;\n navbarEl.toggleMobileMenu = toggleMobileMenu;\n\n const stopScroll = listen(\n window,\n 'scroll',\n () => {\n updateScrolled(scrollContainer);\n },\n { passive: true },\n );\n const stopContainerScroll = scrollContainer\n ? listen(\n scrollContainer,\n 'scroll',\n () => {\n updateScrolled(scrollContainer);\n },\n { passive: true },\n )\n : undefined;\n const stopEscape = listen(el, 'keydown', (event: KeyboardEvent) => {\n if (event.key !== 'Escape' || !isMobileMenuOpen.value) return;\n\n closeMobileMenu();\n });\n\n let mediaCleanup: (() => void) | undefined;\n let mobileSidebarCleanup: (() => void) | undefined;\n let modeTransitionTimeout: ReturnType<typeof setTimeout> | undefined;\n let previousMode: NavbarMode | undefined;\n\n const resolveMobileSidebarTarget = () => {\n mobileSidebarCleanup?.();\n mobileSidebarCleanup = undefined;\n mobileSidebarTarget.value = null;\n isExternalMobileMode.value = false;\n isExternalMobileOpen.value = false;\n\n if (!String(props['mobile-sidebar'].value ?? '').trim()) return;\n\n const target = getExternalSidebar();\n\n if (!target) {\n const root = el.getRootNode();\n const discoveryRoot = root instanceof Document || root instanceof ShadowRoot ? root : document;\n const discoveryObserver = new MutationObserver(() => {\n if (getExternalSidebar()) resolveMobileSidebarTarget();\n });\n\n discoveryObserver.observe(discoveryRoot, { childList: true, subtree: true });\n mobileSidebarCleanup = () => discoveryObserver.disconnect();\n\n return;\n }\n\n const syncTargetState = (event?: CustomEvent<{ open: boolean }>) => {\n const hasBottomNav = target.hasAttribute('data-bottom-nav');\n const hasMobileOpen = target.hasAttribute('data-mobile-open');\n\n isExternalMobileMode.value = hasBottomNav;\n isExternalMobileOpen.value = event?.detail ? event.detail.open : hasMobileOpen;\n\n // If the sidebar is reporting it's open, it must be in bottom-nav mode\n // (either natural or forced). We honor this to avoid race conditions\n // where attributes haven't reflected yet.\n if (isExternalMobileOpen.value && !isExternalMobileMode.value) {\n isExternalMobileMode.value = true;\n }\n\n syncMobileMode();\n };\n\n const targetObserver = new MutationObserver(() => {\n syncTargetState();\n });\n\n syncTargetState();\n targetObserver.observe(target, {\n attributeFilter: ['data-bottom-nav', 'data-mobile-open'],\n attributes: true,\n });\n target.addEventListener('mobile-open-change', syncTargetState as EventListener);\n mobileSidebarCleanup = () => {\n targetObserver.disconnect();\n target.removeEventListener('mobile-open-change', syncTargetState as EventListener);\n };\n mobileSidebarTarget.value = target;\n };\n\n const triggerModeTransition = () => {\n isModeTransitioning.value = true;\n\n if (modeTransitionTimeout != null) clearTimeout(modeTransitionTimeout);\n\n modeTransitionTimeout = setTimeout(() => {\n isModeTransitioning.value = false;\n modeTransitionTimeout = undefined;\n }, 220);\n };\n\n const stopModeWatch = watch(\n computed(() => props.floating.value && props.sticky.value),\n (enabled) => {\n if (!enabled) {\n if (isScrolled.value) isScrolled.value = false;\n\n if (isModeTransitioning.value) isModeTransitioning.value = false;\n\n previousMode = undefined;\n\n if (modeTransitionTimeout != null) {\n clearTimeout(modeTransitionTimeout);\n modeTransitionTimeout = undefined;\n }\n\n return;\n }\n\n resetScrollBase(scrollContainer);\n updateScrolled(scrollContainer);\n\n const currentMode = computeMode();\n\n previousMode = currentMode;\n },\n { immediate: true },\n );\n\n const stopMobileSidebarWatch = watch(\n props['mobile-sidebar'],\n () => {\n resolveMobileSidebarTarget();\n },\n { immediate: true },\n );\n\n const stopModeTransitionWatch = watch(\n computed(() => computeMode()),\n (nextMode) => {\n const comboEnabled = props.floating.value && props.sticky.value;\n\n if (!comboEnabled) {\n previousMode = nextMode ?? undefined;\n\n return;\n }\n\n const next = nextMode ?? undefined;\n\n if (previousMode && next && previousMode !== next) {\n triggerModeTransition();\n }\n\n previousMode = next;\n },\n { immediate: true },\n );\n\n const stopMobileMenuSlotWatch = watch(\n hasMobileMenu,\n (hasContent) => {\n if (!hasContent && isMobileMenuOpen.value) {\n closeMobileMenu();\n }\n },\n { immediate: true },\n );\n const syncMobileMenuContent = () => {\n hasMobileMenu.value = mobileMenuElements.value.some(hasElementContent);\n };\n const stopMobileMenuElementsWatch = watch(mobileMenuElements, syncMobileMenuContent, { immediate: true });\n const mobileMenuContentObserver = new MutationObserver(() => {\n syncMobileMenuContent();\n });\n\n // Parser-created demo content can populate an already-assigned slot wrapper after this\n // custom element connects; `slotchange` alone does not observe that nested content.\n mobileMenuContentObserver.observe(el, { childList: true, subtree: true });\n\n watch(\n props.breakpoint,\n (query) => {\n mediaCleanup?.();\n mediaCleanup = undefined;\n\n const mediaQuery = String(query ?? '').trim();\n\n maxWidthPx.value = parseMaxWidthPx(mediaQuery);\n mediaMatches.value = false;\n\n const width = readContainerWidth(el);\n\n sizeMatches.value = width > 0 && maxWidthPx.value != null ? width <= maxWidthPx.value : false;\n syncMobileMode();\n\n if (props['container-breakpoints'].value && maxWidthPx.value != null) {\n return;\n }\n\n if (!mediaQuery) {\n return;\n }\n\n if (typeof window.matchMedia !== 'function') {\n return;\n }\n\n const mql = window.matchMedia(mediaQuery);\n const syncMedia = (next: boolean) => {\n mediaMatches.value = next;\n syncMobileMode();\n };\n const onChange = (event: MediaQueryListEvent) => {\n syncMedia(event.matches);\n };\n\n syncMedia(mql.matches);\n mql.addEventListener('change', onChange);\n\n mediaCleanup = () => {\n mql.removeEventListener('change', onChange);\n };\n },\n { immediate: true },\n );\n\n const stopResizeEffect =\n typeof ResizeObserver === 'function'\n ? (() => {\n const hostSize = resizeObserver(el);\n const wrapperEl = el.parentElement;\n const containerEl = resolveContainerElement(el);\n const wrapperSize = wrapperEl ? resizeObserver(wrapperEl) : undefined;\n const parentSize = containerEl && containerEl !== wrapperEl ? resizeObserver(containerEl) : undefined;\n\n return watch(\n computed(() => [\n hostSize.value.width,\n hostSize.value.height,\n wrapperSize?.value.width,\n wrapperSize?.value.height,\n parentSize?.value.width,\n parentSize?.value.height,\n ]),\n () => {\n const width = readContainerWidth(el);\n const parentWidth = resolveContainerElement(el)?.clientWidth ?? 0;\n\n sizeMatches.value = width > 0 && maxWidthPx.value != null ? width <= maxWidthPx.value : false;\n isPreviewMode.value = parentWidth > 0 && parentWidth < window.innerWidth;\n syncMobileMode();\n },\n );\n })()\n : undefined;\n\n return () => {\n stopEscape?.();\n stopScroll?.();\n stopContainerScroll?.();\n stopModeWatch?.dispose();\n stopModeTransitionWatch?.dispose();\n stopMobileMenuSlotWatch?.dispose();\n stopMobileMenuElementsWatch?.dispose();\n stopMobileSidebarWatch?.dispose();\n mobileMenuContentObserver.disconnect();\n mediaCleanup?.();\n mobileSidebarCleanup?.();\n stopResizeEffect?.dispose();\n\n if (modeTransitionTimeout != null) {\n clearTimeout(modeTransitionTimeout);\n modeTransitionTimeout = undefined;\n }\n };\n });\n\n return html`\n <nav part=\"nav\" aria-label=\"${props.label}\">\n <div class=\"navbar\" part=\"bar\">\n <div class=\"navbar-logo\" part=\"logo\" ?hidden=${() => !hasLogo.value}>\n <slot name=\"logo\"></slot>\n </div>\n\n <div class=\"navbar-start\" part=\"start\">\n <slot name=\"start\"></slot>\n </div>\n\n <div class=\"navbar-center\" part=\"center\">\n <slot></slot>\n </div>\n\n <div class=\"navbar-end\" part=\"end\">\n <slot name=\"end\"></slot>\n </div>\n\n <button\n class=\"mobile-toggle\"\n part=\"mobile-toggle\"\n type=\"button\"\n aria-label=\"${() =>\n hasMobileMenu.value\n ? isMobileMenuOpen.value\n ? 'Close navigation menu'\n : 'Open navigation menu'\n : isExternalMobileOpen.value\n ? 'Close navigation menu'\n : 'Open navigation menu'}\"\n aria-controls=\"mobile-menu-panel\"\n aria-expanded=\"${() => String(hasMobileMenu.value ? isMobileMenuOpen.value : isExternalMobileOpen.value)}\"\n ?hidden=${() => !isMobile.value || (!hasMobileMenu.value && !mobileSidebarTarget.value)}\n @click=\"${toggleMobileMenu}\">\n <ore-icon\n name=\"${() => ((hasMobileMenu.value ? isMobileMenuOpen.value : isExternalMobileOpen.value) ? 'x' : 'menu')}\"\n size=\"18\"\n stroke-width=\"2.5\"\n aria-hidden=\"true\"></ore-icon>\n </button>\n </div>\n\n <div\n id=\"mobile-menu-panel\"\n class=\"mobile-menu-panel\"\n part=\"mobile-menu\"\n role=\"navigation\"\n aria-label=\"${() => `${props.label.value} mobile menu`}\"\n ?hidden=${() => !isMobile.value || !isMobileMenuOpen.value || !hasMobileMenu.value}>\n <slot name=\"mobile-menu\"></slot>\n </div>\n </nav>\n `;\n },\n styles: [\n colorThemeMixin,\n elevationMixin,\n roundedVariantMixin,\n coarsePointerMixin,\n reducedMotionMixin,\n frostVariantMixin('.navbar'),\n navbarStyles,\n ],\n});\n\n/**\n * `ore-navbar-item` — Navigation item for navbar slots.\n *\n * Renders as an anchor when `href` is provided and item is not disabled.\n *\n * @element ore-navbar-item\n *\n * @attr {boolean} active - Marks item as the current active route\n * @attr {boolean} disabled - Disables interaction\n * @attr {string} href - Link URL; renders as `<a>` when set\n * @attr {boolean} icon-only - Compact, symmetric padding for items with no visible label\n * @attr {string} rel - Anchor `rel` attribute (links only)\n * @attr {string} target - Anchor `target` attribute (links only)\n *\n * @slot - Item label\n * @slot icon - Optional icon before the label\n *\n * @part item - Root clickable element (anchor or button).\n * @part item-icon - Leading icon container.\n * @part item-label - Label text container.\n * @part item-end - Trailing content container.\n *\n * @example\n * ```html\n * <ore-navbar-item href=\"/home\">Home</ore-navbar-item>\n * <ore-navbar-item href=\"/docs\" active>Docs</ore-navbar-item>\n * <ore-navbar-item href=\"/about\" target=\"_blank\" rel=\"noopener\">About</ore-navbar-item>\n * <ore-navbar-item disabled>Disabled</ore-navbar-item>\n * <ore-navbar-item icon-only aria-label=\"Search\"><ore-icon slot=\"icon\" name=\"search\"></ore-icon></ore-navbar-item>\n * ```\n */\nexport const NAVBAR_ITEM_TAG = 'ore-navbar-item' as const;\ndefine<OreNavbarItemProps>(NAVBAR_ITEM_TAG, {\n props: {\n active: prop.bool(false),\n disabled: prop.bool(false),\n href: prop.string(),\n 'icon-only': prop.bool(false),\n rel: prop.string(),\n target: prop.string(),\n },\n setup(props) {\n const slots = useSlots();\n\n const hasIcon = () => slots.has('icon').value;\n const hasEnd = () => slots.has('end').value;\n const navbarCtx = inject(NAVBAR_CTX);\n\n bind({\n attr: {\n 'navbar-mobile': () => (navbarCtx?.isMobile.value ? true : undefined),\n 'navbar-mobile-open': () => (navbarCtx?.mobileMenuOpen.value ? true : undefined),\n },\n });\n\n const isLink = () => Boolean(props.href.value) && !props.disabled.value;\n\n // Prevent reverse tabnapping: auto-inject noopener + noreferrer for _blank links.\n const effectiveRel = computed(() => computeSafeRel(props.rel.value, props.target.value));\n\n // Closes the mobile overflow panel when one of its items is activated — otherwise picking a\n // destination (or firing any other action placed in the `mobile-menu` slot) leaves the panel\n // open over the page until the user separately taps the toggle again. Only matters while\n // actually in mobile mode; on desktop `navbarCtx.mobileMenuOpen` is never true to begin with,\n // so `closeMobileMenu()` (idempotent — see `setMobileMenu`'s equality check) is a no-op there.\n const closeMobileMenuIfOpen = (): void => {\n if (navbarCtx?.mobileMenuOpen.value) navbarCtx.closeMobileMenu();\n };\n\n const renderItemContent = () => html`\n <span class=\"item-icon\" part=\"item-icon\" ?hidden=${() => !hasIcon()} aria-hidden=\"true\">\n <slot name=\"icon\"></slot>\n </span>\n <span class=\"item-label\" part=\"item-label\"><slot></slot></span>\n <span class=\"item-end\" part=\"item-end\" ?hidden=${() => !hasEnd()}>\n <slot name=\"end\"></slot>\n </span>\n `;\n\n return html`\n ${() => {\n if (isLink()) {\n return html`\n <a\n class=\"item\"\n part=\"item\"\n href=\"${props.href}\"\n rel=\"${effectiveRel}\"\n target=\"${props.target}\"\n aria-current=\"${() => (props.active.value ? 'page' : null)}\"\n @click=${closeMobileMenuIfOpen}>\n ${renderItemContent()}\n </a>\n `;\n }\n\n if (props.disabled.value) {\n return html`\n <div class=\"item\" part=\"item\" tabindex=\"-1\" aria-disabled=\"true\">${renderItemContent()}</div>\n `;\n }\n\n return html`\n <button class=\"item\" part=\"item\" type=\"button\" @click=${closeMobileMenuIfOpen}>${renderItemContent()}</button>\n `;\n }}\n `;\n },\n styles: [coarsePointerMixin, navbarStyles],\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"navbar.js","names":[],"sources":["../src/layout/navbar/navbar.ts"],"sourcesContent":["import {\n bind,\n createContext,\n define,\n getHost,\n html,\n inject,\n onCleanup,\n onMounted,\n prop,\n provide,\n useEmit,\n useSlots,\n} from '@vielzeug/ore';\nimport { computed, type Readable, signal, untrack, watch } from '@vielzeug/ripple';\nimport { createElementSize, createMediaQuery, SentinelUnavailableError } from '@vielzeug/sentinel';\n\nimport type { ElevationLevel, RoundedSize, ThemeColor, VisualVariant } from '../../types';\n\nimport '../../content/icon/icon';\nimport {\n coarsePointerMixin,\n colorThemeMixin,\n elevationMixin,\n frostVariantMixin,\n reducedMotionMixin,\n roundedVariantMixin,\n} from '../../styles';\nimport { computeSafeRel } from '../../utils';\nimport navbarStyles from './navbar.css?inline';\n\nconst listen = <E extends Event = Event>(\n el: EventTarget | null | undefined,\n name: string,\n handler: (e: E) => void,\n options?: AddEventListenerOptions,\n): (() => void) => {\n if (!el) return () => {};\n\n el.addEventListener(name, handler as EventListener, options);\n\n return () => el.removeEventListener(name, handler as EventListener, options);\n};\n\ntype NavbarMode = 'floating' | 'sticky';\n\nconst hasElementContent = (el: Element): boolean => {\n if (!(el instanceof HTMLElement)) return true;\n\n if (el.tagName.includes('-')) return true;\n\n if (el.childElementCount > 0) return true;\n\n return (el.textContent ?? '').trim().length > 0;\n};\n\nconst parseMaxWidthPx = (query: string | undefined): number | undefined => {\n const value = String(query ?? '').trim();\n\n if (!value) return undefined;\n\n const match = /max-width\\s*:\\s*([0-9]+(?:\\.[0-9]+)?)px/i.exec(value);\n\n if (!match) return undefined;\n\n const parsed = Number.parseFloat(match[1]);\n\n return Number.isFinite(parsed) ? parsed : undefined;\n};\n\nconst resolveContainerElement = (el: HTMLElement): HTMLElement | null => {\n let container = el.parentElement;\n\n while (container?.tagName.toLowerCase() === 'ore-grid-item') {\n container = container.parentElement;\n }\n\n return container;\n};\n\nconst readContainerWidth = (el: HTMLElement): number => {\n const parentWidth = resolveContainerElement(el)?.clientWidth ?? 0;\n\n if (parentWidth > 0) return parentWidth;\n\n return el.offsetWidth;\n};\n\nconst findScrollContainer = (start: HTMLElement): HTMLElement | null => {\n let current: HTMLElement | null = start.parentElement;\n\n while (current) {\n const styles = window.getComputedStyle(current);\n const overflowY = styles.overflowY;\n\n if (overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay') {\n return current;\n }\n\n current = current.parentElement;\n }\n\n return null;\n};\n\n/** Context provided by `ore-navbar` to `ore-navbar-item` children. */\nexport type NavbarContext = {\n closeMobileMenu: () => void;\n isMobile: Readable<boolean>;\n mobileMenuOpen: Readable<boolean>;\n};\n\n/** Injection key for navbar context. */\nexport const NAVBAR_CTX = createContext<NavbarContext>('NavbarContext');\n\n/** Navbar component events */\nexport type OreNavbarEvents = {\n 'mobile-menu-change': { open: boolean };\n};\n\n/** Navbar component element interface */\nexport type NavbarElement = HTMLElement &\n OreNavbarProps & {\n closeMobileMenu(): void;\n openMobileMenu(): void;\n toggleMobileMenu(): void;\n };\n\ntype MobileSidebarElement = HTMLElement & {\n closeMobile?: () => void;\n openMobile?: () => void;\n toggleMobile?: () => void;\n};\n\n/** Navbar component properties */\nexport type OreNavbarProps = {\n /** CSS media query used for mobile layout switching */\n breakpoint?: string;\n /** Theme color */\n color?: ThemeColor;\n /** Evaluate mobile breakpoint against container width only when possible. */\n 'container-breakpoints'?: boolean;\n /** Shadow elevation level (0-5) */\n elevation?: ElevationLevel;\n /** Start as detached floating navbar */\n floating?: boolean;\n /** Accessible nav landmark label */\n label?: string;\n /** CSS selector for an external sidebar toggled by the mobile button */\n 'mobile-sidebar'?: string;\n /** Border radius size */\n rounded?: RoundedSize;\n /** Scroll threshold in px used by floating+sticky transition */\n 'scroll-threshold'?: number;\n /** Stick to top of viewport */\n sticky?: boolean;\n /** Visual style variant */\n variant?: Exclude<VisualVariant, 'ghost' | 'text'>;\n};\n\n/** Navbar item properties */\nexport type OreNavbarItemProps = {\n /** Whether this item represents the current page */\n active?: boolean;\n /** Whether this item is disabled */\n disabled?: boolean;\n /** Link target URL. Renders an anchor when provided. */\n href?: string;\n /**\n * Icon-only mode — hides the (empty) label region and switches to compact, symmetric\n * padding instead of the icon+label layout's wider horizontal padding. Set this whenever\n * the default slot is intentionally left empty (name conveyed via `aria-label` instead),\n * otherwise the empty label still reserves its inter-element gap and the item ends up\n * with lopsided padding around the icon.\n */\n 'icon-only'?: boolean;\n /** Relationship of linked URL (anchor-only) */\n rel?: string;\n /** Browsing context for link (anchor-only) */\n target?: string;\n};\n\n/**\n * `ore-navbar` — Responsive navigation with sticky/floating modes and a mobile overflow panel.\n *\n * @element ore-navbar\n * @element ore-navbar-item - Navigation link or button placed in the default or mobile-menu slot\n *\n * @attr {string} label - Accessible nav landmark label\n * @attr {boolean} sticky - Makes navbar sticky at top\n * @attr {boolean} floating - Makes navbar detached and floating\n * @attr {number} scroll-threshold - Scroll threshold for floating+sticky transition\n * @attr {string} breakpoint - CSS media query used for mobile mode\n * @attr {boolean} container-breakpoints - Evaluates parseable max-width breakpoints against container width\n * @attr {string} variant - Visual variant: 'flat' | 'solid' | 'bordered' | 'outline' | 'frost' | 'glass'\n * @attr {string} color - Theme color: 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error'\n * @attr {string} rounded - Border radius: 'none' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | '3xl' | 'full'\n * @attr {string} elevation - Shadow elevation: '0' | '1' | '2' | '3' | '4' | '5'\n *\n * @fires mobile-menu-change - Emitted when mobile menu open state changes. detail: { open: boolean }\n *\n * @slot logo - Brand/logo content rendered at the leading edge\n * @slot start - Content rendered in the left/start region\n * @slot - Center navigation content rendered between the start and end regions\n * @slot end - Content rendered in the right/end region\n * @slot mobile-menu - Controls and links rendered inside the mobile overflow panel\n *\n * @cssprop --navbar-height - Navbar bar height\n * @cssprop --navbar-width - Max width of the navbar content\n * @cssprop --navbar-offset - Horizontal offset for floating/sticky positioning\n * @cssprop --navbar-bg - Navbar background color\n * @cssprop --navbar-color - Navbar text color\n * @cssprop --navbar-border-color - Navbar border color\n * @cssprop --navbar-shadow - Navbar box shadow\n * @cssprop --navbar-radius - Border radius (floating/sticky modes)\n * @cssprop --navbar-backdrop-filter - Backdrop filter for blur effect\n * @cssprop --navbar-item-color - Nav item text color\n * @cssprop --navbar-item-hover-color - Nav item hover text color\n * @cssprop --navbar-item-hover-bg - Nav item hover background\n * @cssprop --navbar-item-active-color - Nav item active/current text color\n * @cssprop --navbar-item-active-bg - Nav item active/current background\n * @part nav - Outer navigation landmark\n * @part bar - Main navbar row container\n * @part logo - Logo slot container\n * @part start - Leading action/content region\n * @part center - Center content region\n * @part end - Trailing action/content region\n * @part mobile-toggle - Mobile menu toggle button\n * @part mobile-menu - Mobile overflow panel\n * @part item-icon - Leading icon inside navbar items\n * @part item-label - Label text inside navbar items\n * @part item-end - Trailing content inside navbar items\n * @part item - Clickable navbar item root\n * @example\n * ```html\n * <ore-navbar sticky elevation=\"1\">\n * <a href=\"/\" slot=\"logo\"><strong>MyApp</strong></a>\n * <ore-navbar-item href=\"/dashboard\">Dashboard</ore-navbar-item>\n * <ore-navbar-item href=\"/docs\">Docs</ore-navbar-item>\n * <ore-button slot=\"end\" variant=\"solid\" color=\"primary\" size=\"sm\">Sign in</ore-button>\n * </ore-navbar>\n * ```\n */\nexport const NAVBAR_TAG = 'ore-navbar' as const;\ndefine<OreNavbarProps>(NAVBAR_TAG, {\n props: {\n breakpoint: prop.string('(max-width: 768px)'),\n color: prop.string<ThemeColor>(),\n 'container-breakpoints': prop.bool(false),\n elevation: prop.number<ElevationLevel>(),\n floating: prop.bool(false),\n label: prop.string('Main navigation'),\n 'mobile-sidebar': prop.string(),\n rounded: prop.string<RoundedSize>(),\n 'scroll-threshold': prop.number(80),\n sticky: prop.bool(false),\n variant: prop.string<'flat' | 'solid' | 'bordered' | 'outline' | 'frost'>(),\n },\n setup(props) {\n const el = getHost();\n const emit = useEmit<OreNavbarEvents>();\n const slots = useSlots();\n\n const hasLogo = slots.has('logo');\n const mobileMenuElements = slots.elements('mobile-menu');\n const hasMobileMenu = signal(false);\n const mobileSidebarTarget = signal<MobileSidebarElement | null>(null);\n const isExternalMobileMode = signal(false);\n const isExternalMobileOpen = signal(false);\n const isMobile = signal(false);\n const isMobileMenuOpen = signal(false);\n const isModeTransitioning = signal(false);\n const isScrolled = signal(false);\n const scrollBase = signal(0);\n const mediaMatches = signal(false);\n const sizeMatches = signal(false);\n const maxWidthPx = signal<number | undefined>(parseMaxWidthPx(props.breakpoint.value));\n const isPreviewMode = signal(false);\n\n const getExternalSidebar = () => {\n const selector = String(props['mobile-sidebar'].value ?? '').trim();\n\n if (!selector) return null;\n\n const root = el.getRootNode();\n const scopedTarget =\n root instanceof ShadowRoot || root instanceof Document\n ? (root.querySelector(selector) as MobileSidebarElement | null)\n : null;\n\n return scopedTarget ?? (document.querySelector(selector) as MobileSidebarElement | null);\n };\n\n provide(NAVBAR_CTX, {\n // Wrapped, not passed directly — `closeMobileMenu` (below) isn't defined yet at this point\n // in `setup()`; by the time `ore-navbar-item` actually calls this (a later click), it is.\n closeMobileMenu: () => closeMobileMenu(),\n isMobile: computed(() => isMobile.value) as Readable<boolean>,\n mobileMenuOpen: computed(() => isMobileMenuOpen.value) as Readable<boolean>,\n });\n\n const computeMode = (): NavbarMode | undefined => {\n if (props.floating.value && props.sticky.value) {\n return isScrolled.value ? 'sticky' : 'floating';\n }\n\n if (props.floating.value) return 'floating';\n\n if (props.sticky.value) return 'sticky';\n\n return undefined;\n };\n\n const setMobileMenu = (next: boolean) => {\n const open = Boolean(next);\n\n if (!hasMobileMenu.value) {\n const target = mobileSidebarTarget.value ?? getExternalSidebar();\n\n if (target) {\n if (open) {\n target.openMobile?.();\n } else {\n target.closeMobile?.();\n }\n\n return;\n }\n }\n\n if (open && !hasMobileMenu.value) return;\n\n if (isMobileMenuOpen.value === open) return;\n\n isMobileMenuOpen.value = open;\n emit('mobile-menu-change', { open });\n };\n\n const closeMobileMenu = () => setMobileMenu(false);\n const openMobileMenu = () => setMobileMenu(true);\n const toggleMobileMenu = () => {\n if (!hasMobileMenu.value) {\n const target = mobileSidebarTarget.value ?? getExternalSidebar();\n\n if (target) {\n if (typeof target.toggleMobile === 'function') {\n target.toggleMobile();\n } else if (typeof target.openMobile === 'function' && typeof target.closeMobile === 'function') {\n const isOpen = target.hasAttribute('data-mobile-open');\n\n if (isOpen) target.closeMobile();\n else target.openMobile();\n }\n\n return;\n }\n }\n\n setMobileMenu(!isMobileMenuOpen.value);\n };\n\n const syncMobileMode = () => {\n const next =\n (mobileSidebarTarget.value && !hasMobileMenu.value ? isExternalMobileMode.value : false) ||\n mediaMatches.value ||\n sizeMatches.value;\n\n isMobile.value = next;\n\n if (!next) closeMobileMenu();\n };\n\n const readScrollOffset = (scrollContainer: HTMLElement | null): number => {\n if (scrollContainer) {\n return scrollContainer.scrollTop || 0;\n }\n\n return window.scrollY || document.documentElement.scrollTop || 0;\n };\n\n const resetScrollBase = (scrollContainer: HTMLElement | null) => {\n scrollBase.value = readScrollOffset(scrollContainer);\n };\n\n const updateScrolled = (scrollContainer: HTMLElement | null = null) => {\n if (!(props.floating.value && props.sticky.value)) {\n if (isScrolled.value) isScrolled.value = false;\n\n return;\n }\n\n const threshold = Math.max(0, Number(props['scroll-threshold'].value) || 0);\n const currentOffset = readScrollOffset(scrollContainer);\n const delta = currentOffset - scrollBase.value;\n\n isScrolled.value = delta > threshold;\n };\n\n bind({\n attr: {\n 'data-mobile': () => (isMobile.value ? true : undefined),\n 'data-mobile-open': () => (isMobile.value && isMobileMenuOpen.value ? true : undefined),\n 'data-mode': () => computeMode() ?? null,\n 'data-mode-transitioning': () => (isModeTransitioning.value ? true : undefined),\n 'data-preview-mode': () => (isPreviewMode.value ? true : undefined),\n 'data-scrolled': () => (props.floating.value && props.sticky.value && isScrolled.value ? true : undefined),\n },\n });\n\n onMounted(() => {\n const navbarEl = el as NavbarElement;\n const scrollContainer = findScrollContainer(el);\n\n navbarEl.closeMobileMenu = closeMobileMenu;\n navbarEl.openMobileMenu = openMobileMenu;\n navbarEl.toggleMobileMenu = toggleMobileMenu;\n\n const stopScroll = listen(\n window,\n 'scroll',\n () => {\n updateScrolled(scrollContainer);\n },\n { passive: true },\n );\n const stopContainerScroll = scrollContainer\n ? listen(\n scrollContainer,\n 'scroll',\n () => {\n updateScrolled(scrollContainer);\n },\n { passive: true },\n )\n : undefined;\n const stopEscape = listen(el, 'keydown', (event: KeyboardEvent) => {\n if (event.key !== 'Escape' || !isMobileMenuOpen.value) return;\n\n closeMobileMenu();\n });\n\n let mediaCleanup: (() => void) | undefined;\n let mobileSidebarCleanup: (() => void) | undefined;\n let modeTransitionTimeout: ReturnType<typeof setTimeout> | undefined;\n let previousMode: NavbarMode | undefined;\n\n const resolveMobileSidebarTarget = () => {\n mobileSidebarCleanup?.();\n mobileSidebarCleanup = undefined;\n mobileSidebarTarget.value = null;\n isExternalMobileMode.value = false;\n isExternalMobileOpen.value = false;\n\n if (!String(props['mobile-sidebar'].value ?? '').trim()) return;\n\n const target = getExternalSidebar();\n\n if (!target) {\n const root = el.getRootNode();\n const discoveryRoot = root instanceof Document || root instanceof ShadowRoot ? root : document;\n const discoveryObserver = new MutationObserver(() => {\n if (getExternalSidebar()) resolveMobileSidebarTarget();\n });\n\n discoveryObserver.observe(discoveryRoot, { childList: true, subtree: true });\n mobileSidebarCleanup = () => discoveryObserver.disconnect();\n\n return;\n }\n\n const syncTargetState = (event?: CustomEvent<{ open: boolean }>) => {\n const hasBottomNav = target.hasAttribute('data-bottom-nav');\n const hasMobileOpen = target.hasAttribute('data-mobile-open');\n\n isExternalMobileMode.value = hasBottomNav;\n isExternalMobileOpen.value = event?.detail ? event.detail.open : hasMobileOpen;\n\n // If the sidebar is reporting it's open, it must be in bottom-nav mode\n // (either natural or forced). We honor this to avoid race conditions\n // where attributes haven't reflected yet.\n if (isExternalMobileOpen.value && !isExternalMobileMode.value) {\n isExternalMobileMode.value = true;\n }\n\n syncMobileMode();\n };\n\n const targetObserver = new MutationObserver(() => {\n syncTargetState();\n });\n\n syncTargetState();\n targetObserver.observe(target, {\n attributeFilter: ['data-bottom-nav', 'data-mobile-open'],\n attributes: true,\n });\n target.addEventListener('mobile-open-change', syncTargetState as EventListener);\n mobileSidebarCleanup = () => {\n targetObserver.disconnect();\n target.removeEventListener('mobile-open-change', syncTargetState as EventListener);\n };\n mobileSidebarTarget.value = target;\n };\n\n const triggerModeTransition = () => {\n isModeTransitioning.value = true;\n\n if (modeTransitionTimeout != null) clearTimeout(modeTransitionTimeout);\n\n modeTransitionTimeout = setTimeout(() => {\n isModeTransitioning.value = false;\n modeTransitionTimeout = undefined;\n }, 220);\n };\n\n const stopModeWatch = watch(\n computed(() => props.floating.value && props.sticky.value),\n (enabled) => {\n if (!enabled) {\n if (isScrolled.value) isScrolled.value = false;\n\n if (isModeTransitioning.value) isModeTransitioning.value = false;\n\n previousMode = undefined;\n\n if (modeTransitionTimeout != null) {\n clearTimeout(modeTransitionTimeout);\n modeTransitionTimeout = undefined;\n }\n\n return;\n }\n\n resetScrollBase(scrollContainer);\n updateScrolled(scrollContainer);\n\n const currentMode = computeMode();\n\n previousMode = currentMode;\n },\n { immediate: true },\n );\n\n const stopMobileSidebarWatch = watch(\n props['mobile-sidebar'],\n () => {\n resolveMobileSidebarTarget();\n },\n { immediate: true },\n );\n\n const stopModeTransitionWatch = watch(\n computed(() => computeMode()),\n (nextMode) => {\n const comboEnabled = props.floating.value && props.sticky.value;\n\n if (!comboEnabled) {\n previousMode = nextMode ?? undefined;\n\n return;\n }\n\n const next = nextMode ?? undefined;\n\n if (previousMode && next && previousMode !== next) {\n triggerModeTransition();\n }\n\n previousMode = next;\n },\n { immediate: true },\n );\n\n const stopMobileMenuSlotWatch = watch(\n hasMobileMenu,\n (hasContent) => {\n if (!hasContent && isMobileMenuOpen.value) {\n closeMobileMenu();\n }\n },\n { immediate: true },\n );\n const syncMobileMenuContent = () => {\n hasMobileMenu.value = mobileMenuElements.value.some(hasElementContent);\n };\n const stopMobileMenuElementsWatch = watch(mobileMenuElements, syncMobileMenuContent, { immediate: true });\n const mobileMenuContentObserver = new MutationObserver(() => {\n syncMobileMenuContent();\n });\n\n // Parser-created demo content can populate an already-assigned slot wrapper after this\n // custom element connects; `slotchange` alone does not observe that nested content.\n mobileMenuContentObserver.observe(el, { childList: true, subtree: true });\n\n watch(\n props.breakpoint,\n (query) => {\n // Untrack the callback body: the watch already tracks `props.breakpoint` via\n // its internal `read()`. Without untrack, signals read inside (e.g.\n // `mediaMatches` via `syncMobileMode()`) become dependencies of this watch\n // effect. When the sentinel updates `mediaMatches` on a viewport change, the\n // effect re-runs — `EffectNode.run()` disposes the previous scope (killing the\n // mediaHandle and its MediaQueryList listener) but the callback is NOT invoked\n // (source unchanged), so no new handle is created. The navbar is then deaf to\n // further MQ changes and stays stuck in mobile mode after resizing back to\n // desktop.\n untrack(() => {\n mediaCleanup?.();\n mediaCleanup = undefined;\n\n const mediaQuery = String(query ?? '').trim();\n\n maxWidthPx.value = parseMaxWidthPx(mediaQuery);\n mediaMatches.value = false;\n\n const width = readContainerWidth(el);\n\n sizeMatches.value = width > 0 && maxWidthPx.value != null ? width <= maxWidthPx.value : false;\n syncMobileMode();\n\n if (props['container-breakpoints'].value && maxWidthPx.value != null) {\n return;\n }\n\n if (!mediaQuery) {\n return;\n }\n\n try {\n const mediaHandle = createMediaQuery(mediaQuery);\n const syncMedia = (state: typeof mediaHandle.value) => {\n if (state) {\n mediaMatches.value = state.matches;\n syncMobileMode();\n }\n };\n\n syncMedia(mediaHandle.value);\n\n const mediaCleanupFn = watch(mediaHandle, syncMedia);\n mediaCleanup = () => {\n mediaCleanupFn.dispose();\n mediaHandle.dispose();\n };\n } catch (error) {\n if (!(error instanceof SentinelUnavailableError)) throw error;\n }\n });\n },\n { immediate: true },\n );\n\n const stopResizeEffect =\n typeof ResizeObserver === 'function'\n ? (() => {\n const hostSize = createElementSize(el);\n onCleanup(() => hostSize.dispose());\n\n const wrapperEl = el.parentElement;\n const containerEl = resolveContainerElement(el);\n const wrapperSize = wrapperEl ? createElementSize(wrapperEl) : undefined;\n const parentSize = containerEl && containerEl !== wrapperEl ? createElementSize(containerEl) : undefined;\n\n if (wrapperSize) {\n onCleanup(() => wrapperSize.dispose());\n }\n if (parentSize) {\n onCleanup(() => parentSize.dispose());\n }\n\n return watch(\n computed(() => [\n hostSize.value?.width,\n hostSize.value?.height,\n wrapperSize?.value?.width,\n wrapperSize?.value?.height,\n parentSize?.value?.width,\n parentSize?.value?.height,\n ]),\n () => {\n const width = readContainerWidth(el);\n const parentWidth = resolveContainerElement(el)?.clientWidth ?? 0;\n\n sizeMatches.value = width > 0 && maxWidthPx.value != null ? width <= maxWidthPx.value : false;\n isPreviewMode.value = parentWidth > 0 && parentWidth < window.innerWidth;\n syncMobileMode();\n },\n );\n })()\n : undefined;\n\n return () => {\n stopEscape?.();\n stopScroll?.();\n stopContainerScroll?.();\n stopModeWatch?.dispose();\n stopModeTransitionWatch?.dispose();\n stopMobileMenuSlotWatch?.dispose();\n stopMobileMenuElementsWatch?.dispose();\n stopMobileSidebarWatch?.dispose();\n mobileMenuContentObserver.disconnect();\n mediaCleanup?.();\n mobileSidebarCleanup?.();\n stopResizeEffect?.dispose();\n\n if (modeTransitionTimeout != null) {\n clearTimeout(modeTransitionTimeout);\n modeTransitionTimeout = undefined;\n }\n };\n });\n\n return html`\n <nav part=\"nav\" aria-label=\"${props.label}\">\n <div class=\"navbar\" part=\"bar\">\n <div class=\"navbar-logo\" part=\"logo\" ?hidden=${() => !hasLogo.value}>\n <slot name=\"logo\"></slot>\n </div>\n\n <div class=\"navbar-start\" part=\"start\">\n <slot name=\"start\"></slot>\n </div>\n\n <div class=\"navbar-center\" part=\"center\">\n <slot></slot>\n </div>\n\n <div class=\"navbar-end\" part=\"end\">\n <slot name=\"end\"></slot>\n </div>\n\n <button\n class=\"mobile-toggle\"\n part=\"mobile-toggle\"\n type=\"button\"\n aria-label=\"${() =>\n hasMobileMenu.value\n ? isMobileMenuOpen.value\n ? 'Close navigation menu'\n : 'Open navigation menu'\n : isExternalMobileOpen.value\n ? 'Close navigation menu'\n : 'Open navigation menu'}\"\n aria-controls=\"mobile-menu-panel\"\n aria-expanded=\"${() => String(hasMobileMenu.value ? isMobileMenuOpen.value : isExternalMobileOpen.value)}\"\n ?hidden=${() => !isMobile.value || (!hasMobileMenu.value && !mobileSidebarTarget.value)}\n @click=\"${toggleMobileMenu}\">\n <ore-icon\n name=\"${() => ((hasMobileMenu.value ? isMobileMenuOpen.value : isExternalMobileOpen.value) ? 'x' : 'menu')}\"\n size=\"18\"\n stroke-width=\"2.5\"\n aria-hidden=\"true\"></ore-icon>\n </button>\n </div>\n\n <div\n id=\"mobile-menu-panel\"\n class=\"mobile-menu-panel\"\n part=\"mobile-menu\"\n role=\"navigation\"\n aria-label=\"${() => `${props.label.value} mobile menu`}\"\n ?hidden=${() => !isMobile.value || !isMobileMenuOpen.value || !hasMobileMenu.value}>\n <slot name=\"mobile-menu\"></slot>\n </div>\n </nav>\n `;\n },\n styles: [\n colorThemeMixin,\n elevationMixin,\n roundedVariantMixin,\n coarsePointerMixin,\n reducedMotionMixin,\n frostVariantMixin('.navbar'),\n navbarStyles,\n ],\n});\n\n/**\n * `ore-navbar-item` — Navigation item for navbar slots.\n *\n * Renders as an anchor when `href` is provided and item is not disabled.\n *\n * @element ore-navbar-item\n *\n * @attr {boolean} active - Marks item as the current active route\n * @attr {boolean} disabled - Disables interaction\n * @attr {string} href - Link URL; renders as `<a>` when set\n * @attr {boolean} icon-only - Compact, symmetric padding for items with no visible label\n * @attr {string} rel - Anchor `rel` attribute (links only)\n * @attr {string} target - Anchor `target` attribute (links only)\n *\n * @slot - Item label\n * @slot icon - Optional icon before the label\n *\n * @part item - Root clickable element (anchor or button).\n * @part item-icon - Leading icon container.\n * @part item-label - Label text container.\n * @part item-end - Trailing content container.\n *\n * @example\n * ```html\n * <ore-navbar-item href=\"/home\">Home</ore-navbar-item>\n * <ore-navbar-item href=\"/docs\" active>Docs</ore-navbar-item>\n * <ore-navbar-item href=\"/about\" target=\"_blank\" rel=\"noopener\">About</ore-navbar-item>\n * <ore-navbar-item disabled>Disabled</ore-navbar-item>\n * <ore-navbar-item icon-only aria-label=\"Search\"><ore-icon slot=\"icon\" name=\"search\"></ore-icon></ore-navbar-item>\n * ```\n */\nexport const NAVBAR_ITEM_TAG = 'ore-navbar-item' as const;\ndefine<OreNavbarItemProps>(NAVBAR_ITEM_TAG, {\n props: {\n active: prop.bool(false),\n disabled: prop.bool(false),\n href: prop.string(),\n 'icon-only': prop.bool(false),\n rel: prop.string(),\n target: prop.string(),\n },\n setup(props) {\n const slots = useSlots();\n\n const hasIcon = () => slots.has('icon').value;\n const hasEnd = () => slots.has('end').value;\n const navbarCtx = inject(NAVBAR_CTX);\n\n bind({\n attr: {\n 'navbar-mobile': () => (navbarCtx?.isMobile.value ? true : undefined),\n 'navbar-mobile-open': () => (navbarCtx?.mobileMenuOpen.value ? true : undefined),\n },\n });\n\n const isLink = () => Boolean(props.href.value) && !props.disabled.value;\n\n // Prevent reverse tabnapping: auto-inject noopener + noreferrer for _blank links.\n const effectiveRel = computed(() => computeSafeRel(props.rel.value, props.target.value));\n\n // Closes the mobile overflow panel when one of its items is activated — otherwise picking a\n // destination (or firing any other action placed in the `mobile-menu` slot) leaves the panel\n // open over the page until the user separately taps the toggle again. Only matters while\n // actually in mobile mode; on desktop `navbarCtx.mobileMenuOpen` is never true to begin with,\n // so `closeMobileMenu()` (idempotent — see `setMobileMenu`'s equality check) is a no-op there.\n const closeMobileMenuIfOpen = (): void => {\n if (navbarCtx?.mobileMenuOpen.value) navbarCtx.closeMobileMenu();\n };\n\n const renderItemContent = () => html`\n <span class=\"item-icon\" part=\"item-icon\" ?hidden=${() => !hasIcon()} aria-hidden=\"true\">\n <slot name=\"icon\"></slot>\n </span>\n <span class=\"item-label\" part=\"item-label\"><slot></slot></span>\n <span class=\"item-end\" part=\"item-end\" ?hidden=${() => !hasEnd()}>\n <slot name=\"end\"></slot>\n </span>\n `;\n\n return html`\n ${() => {\n if (isLink()) {\n return html`\n <a\n class=\"item\"\n part=\"item\"\n href=\"${props.href}\"\n rel=\"${effectiveRel}\"\n target=\"${props.target}\"\n aria-current=\"${() => (props.active.value ? 'page' : null)}\"\n @click=${closeMobileMenuIfOpen}>\n ${renderItemContent()}\n </a>\n `;\n }\n\n if (props.disabled.value) {\n return html`\n <div class=\"item\" part=\"item\" tabindex=\"-1\" aria-disabled=\"true\">${renderItemContent()}</div>\n `;\n }\n\n return html`\n <button class=\"item\" part=\"item\" type=\"button\" @click=${closeMobileMenuIfOpen}>${renderItemContent()}</button>\n `;\n }}\n `;\n },\n styles: [coarsePointerMixin, navbarStyles],\n});\n"],"mappings":"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"}
|
package/dist/otp-input.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./styles/mixins/field-variants.css.cjs"),t=require("./styles/mixins/shape.css.cjs"),n=require("./styles/mixins/theme.css.cjs"),r=require("./core/ore.cjs"),i=require("./core/nav.cjs"),a=require("./shared/prop-bundles.cjs"),o=require("./inputs/shared/native-field-event.cjs"),s=require("./inputs/otp-input/otp-input.css.cjs");let c=require("@vielzeug/ore"),l=require("@vielzeug/ripple");var u=`ore-otp-input`;(0,c.define)(u,{formAssociated:!0,props:{...a.themableBundle,...a.sizableBundle,...a.disablableBundle,label:c.prop.string(`One-time password`),length:c.prop.number(6),masked:c.prop.bool(!1),name:c.prop.string(),separator:c.prop.string(),type:c.prop.oneOf([`numeric`,`alphanumeric`],`numeric`),value:c.prop.string(),variant:c.prop.string()},setup(e){let t=(0,c.getHost)(),n=(0,c.useEmit)(),a=(0,l.computed)(()=>Number(e.length.value)||6),s=(0,l.computed)(()=>!!e.disabled.value),u=(0,l.computed)(()=>Array.from({length:a.value},(e,t)=>t)),d=(0,l.signal)(String(e.value.value||``)),f=()=>String(e.value.value||``);(0,c.useField)({disabled:s,toFormValue:e=>e,value:d}),(0,c.bind)({attr:{size:e.size,value:()=>d.value||null,variant:e.variant}});function p(){return[...t.shadowRoot?.querySelectorAll(`input.cell`)??[]]}let m=i.createListControl({getItems:()=>p(),keys:{next:[`ArrowRight`],prev:[`ArrowLeft`]},loop:!1,onNavigate:(e
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./styles/mixins/field-variants.css.cjs"),t=require("./styles/mixins/shape.css.cjs"),n=require("./styles/mixins/theme.css.cjs"),r=require("./core/ore.cjs"),i=require("./core/nav.cjs"),a=require("./shared/prop-bundles.cjs"),o=require("./inputs/shared/native-field-event.cjs"),s=require("./inputs/otp-input/otp-input.css.cjs");let c=require("@vielzeug/ore"),l=require("@vielzeug/ripple");var u=`ore-otp-input`;(0,c.define)(u,{formAssociated:!0,props:{...a.themableBundle,...a.sizableBundle,...a.disablableBundle,label:c.prop.string(`One-time password`),length:c.prop.number(6),masked:c.prop.bool(!1),name:c.prop.string(),separator:c.prop.string(),type:c.prop.oneOf([`numeric`,`alphanumeric`],`numeric`),value:c.prop.string(),variant:c.prop.string()},setup(e){let t=(0,c.getHost)(),n=(0,c.useEmit)(),a=(0,l.computed)(()=>Number(e.length.value)||6),s=(0,l.computed)(()=>!!e.disabled.value),u=(0,l.computed)(()=>Array.from({length:a.value},(e,t)=>t)),d=(0,l.signal)(String(e.value.value||``)),f=()=>String(e.value.value||``);(0,c.useField)({disabled:s,toFormValue:e=>e,value:d}),(0,c.bind)({attr:{size:e.size,value:()=>d.value||null,variant:e.variant}});function p(){return[...t.shadowRoot?.querySelectorAll(`input.cell`)??[]]}let m=i.createListControl({getItems:()=>p(),keys:{next:[`ArrowRight`],prev:[`ArrowLeft`]},loop:!1,onNavigate:({item:e})=>{e.focus()},signal:r.lifecycleSignal(c.onCleanup)});function h(){return p().map(e=>e.value).join(``)}function g(e){let t=p(),n=String(e||``).slice(0,t.length);for(let e of t)e.value=``;n.split(``).forEach((e,n)=>{t[n]&&(t[n].value=e)}),d.value=h()}o.defineFieldValue(t,()=>d.value,e=>{g(e)});function _(e,r){let i=p(),a=h(),s=r??(a.length===i.length&&a.split(``).every(Boolean));d.value=a,o.setFieldValue(t,a),o.dispatchNativeFieldEvent(t,`input`),o.dispatchNativeFieldEvent(t,`change`),s&&n(`complete`,{originalEvent:e,value:a})}function v(t){return e.type.value===`numeric`?/^\d$/.test(t):/^[a-z\d]$/i.test(t)}function y(e,t){let n=e.target,r=n.value;if(r.length>1&&(r=r.slice(-1)),r&&!v(r)){n.value=``;return}n.value=r;let i=p();_(e),r&&t<i.length-1&&(i[t+1].focus(),i[t+1].select())}function b(e,t){let n=e.target,r=p();if(m.set(t),e.key===`Backspace`){if(n.value)n.value=``;else if(t>0){let e=r[m.navigate(`prev`)];e&&(e.select(),e.value=``)}_(e,!1),e.preventDefault()}else m.handleKeydown(e)}function x(e){e.preventDefault();let t=(e.clipboardData?.getData(`text`)??``).split(``).filter(e=>v(e)).slice(0,a.value),n=p();t.forEach((e,t)=>{n[t]&&(n[t].value=e)}),_(e),n[Math.min(t.length,n.length-1)]?.focus()}(0,l.watch)(e.value,e=>{g(String(e||``))}),(0,l.watch)(e.length,()=>{requestAnimationFrame(()=>g(f()))});let S=(0,l.computed)(()=>{let t=Number(e.length.value)||6;return e.separator.value==null?-1:Math.floor(t/2)});return(0,c.onMounted)(()=>{g(f())}),c.html`
|
|
2
2
|
<div class="otp-group" part="group" role="group" aria-label="${e.label}">
|
|
3
3
|
${()=>u.value.map(t=>c.html`
|
|
4
4
|
${()=>S.value>0&&t===S.value?c.html`
|
package/dist/otp-input.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"otp-input.cjs","names":[],"sources":["../src/inputs/otp-input/otp-input.ts"],"sourcesContent":["import { bind, define, getHost, html, onCleanup, onMounted, prop, useEmit, useField } from '@vielzeug/ore';\nimport { computed, signal, watch } from '@vielzeug/ripple';\nimport { createListControl, lifecycleSignal } from '../../core';\nimport { disablableBundle, sizableBundle, themableBundle } from '../../shared';\nimport { colorThemeMixin, fieldVariantMixin, forcedColorsFocusMixin, sizeVariantMixin } from '../../styles';\nimport type { ComponentSize, ThemeColor, VisualVariant } from '../../types';\nimport { defineFieldValue, dispatchNativeFieldEvent, setFieldValue } from '../shared/native-field-event';\nimport componentStyles from './otp-input.css?inline';\n\nexport type OreOtpInputEvents = {\n change: Event;\n complete: { originalEvent?: Event; value: string };\n input: Event;\n};\n\n/** OTP Input props */\nexport type OreOtpInputProps = {\n /** Theme color */\n color?: ThemeColor;\n /** Make inputs disabled */\n disabled?: boolean;\n /** Accessible label */\n label?: string;\n /** Number of input cells */\n length?: number;\n /** Mask input (show dots instead of characters) */\n masked?: boolean;\n /** Form field name */\n name?: string;\n /** Show a separator in the middle (e.g. \"–\") */\n separator?: string;\n /** Component size */\n size?: ComponentSize;\n /** Input type: 'numeric' (digits only) or 'alphanumeric' */\n type?: 'numeric' | 'alphanumeric';\n /** Current value */\n value?: string;\n /** Visual variant */\n variant?: Exclude<VisualVariant, 'text' | 'frost'>;\n};\n\n/**\n * A segmented OTP (One-Time Password) input with N individual cells.\n * Auto-advances focus on input, auto-backs on Backspace, handles paste.\n *\n * @element ore-otp-input\n *\n * @attr {number} length - Number of cells (default: 6)\n * @attr {string} type - 'numeric' (default) | 'alphanumeric'\n * @attr {string} value - Current code value\n * @attr {boolean} disabled - Disable all cells\n * @attr {boolean} masked - Show as password\n * @attr {string} size - 'sm' | 'md' | 'lg'\n * @attr {string} color - Theme color: 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error'\n * @attr {string} name - Form field name\n * @attr {string} label - Group aria-label\n * @attr {string} separator - Optional separator character shown in the middle\n *\n * @fires input - Emitted whenever a cell changes.\n * @fires change - Emitted whenever a cell changes.\n * @fires complete - Emitted when all cells are filled. detail: { value: string, originalEvent?: Event }\n *\n * @slot label - Custom label above the OTP input\n * @slot helper - Custom helper text below the OTP input\n * @slot error - Custom error content below the OTP input\n *\n * @cssprop --otp-cell-size - Width and height of each cell\n * @cssprop --otp-cell-gap - Gap between cells\n * @cssprop --otp-cell-font-size - Font size inside cells\n * @cssprop --otp-cell-radius - Border radius of cells\n * @cssprop --otp-cell-bg - Cell background color\n * @cssprop --otp-cell-border-color - Default border color\n * @cssprop --otp-cell-focus-border - Focused border/caret color\n * @cssprop --otp-cell-hover-bg - Cell background on hover (flat/ghost variants)\n * @cssprop --otp-cell-hover-border-color - Cell border on hover (flat/bordered variants)\n * @cssprop --otp-cell-focus-bg - Cell background when focused (flat variant)\n * @cssprop --otp-cell-focus-border-color - Cell border when focused (flat variant)\n *\n * @part group - Group container.\n * @part cell - Shadow part for the `cell` element.\n * @example\n * ```html\n * <ore-otp-input length=\"6\" color=\"primary\"></ore-otp-input>\n * ```\n */\nexport const OTP_INPUT_TAG = 'ore-otp-input' as const;\ndefine<OreOtpInputProps>(OTP_INPUT_TAG, {\n formAssociated: true,\n props: {\n ...themableBundle,\n ...sizableBundle,\n ...disablableBundle,\n label: prop.string('One-time password'),\n length: prop.number(6),\n masked: prop.bool(false),\n name: prop.string(),\n separator: prop.string(),\n type: prop.oneOf(['numeric', 'alphanumeric'] as const, 'numeric'),\n value: prop.string(),\n variant: prop.string<Exclude<VisualVariant, 'text' | 'frost'>>(),\n },\n setup(props) {\n const el = getHost();\n const emit = useEmit<OreOtpInputEvents>();\n\n const lengthValue = computed(() => Number(props.length.value) || 6);\n const isDisabled = computed(() => Boolean(props.disabled.value));\n const cells = computed(() => Array.from({ length: lengthValue.value }, (_, i) => i));\n const otpValue = signal(String(props.value.value || ''));\n const normalizedPropValue = () => String(props.value.value || '');\n\n // Form association: the joined cell value is the single form value for `name`,\n // since individual per-cell <input> elements inside the shadow root cannot\n // participate in an ancestor <form>'s submission on their own.\n useField<string>({ disabled: isDisabled, toFormValue: (v) => v, value: otpValue });\n\n bind({\n attr: {\n size: props.size,\n value: () => otpValue.value || null,\n variant: props.variant,\n },\n });\n\n function getInputs(): HTMLInputElement[] {\n return [...(el.shadowRoot?.querySelectorAll<HTMLInputElement>('input.cell') ?? [])];\n }\n\n const listControl = createListControl({\n getItems: () => getInputs(),\n keys: { next: ['ArrowRight'], prev: ['ArrowLeft'] },\n loop: false,\n onNavigate: (_action, index) => {\n const inputs = getInputs();\n\n inputs[index]?.focus();\n },\n signal: lifecycleSignal(onCleanup),\n });\n\n function getValue(): string {\n return getInputs()\n .map((i) => i.value)\n .join('');\n }\n function syncInputsFromValue(raw: string) {\n const allInputs = getInputs();\n const normalized = String(raw || '').slice(0, allInputs.length);\n\n for (const input of allInputs) input.value = '';\n\n normalized.split('').forEach((c, i) => {\n if (allInputs[i]) allInputs[i].value = c;\n });\n\n otpValue.value = getValue();\n }\n defineFieldValue(\n el,\n () => otpValue.value,\n (value) => {\n syncInputsFromValue(value);\n },\n );\n function emitOtpState(originalEvent?: Event, forceComplete?: boolean): void {\n const allInputs = getInputs();\n const full = getValue();\n const complete = forceComplete ?? (full.length === allInputs.length && full.split('').every(Boolean));\n\n otpValue.value = full;\n setFieldValue(el, full);\n dispatchNativeFieldEvent(el, 'input');\n dispatchNativeFieldEvent(el, 'change');\n\n if (complete) emit('complete', { originalEvent, value: full });\n }\n function isAllowed(char: string): boolean {\n if (props.type.value === 'numeric') return /^\\d$/.test(char);\n\n return /^[a-z\\d]$/i.test(char);\n }\n function handleInput(e: Event, index: number) {\n const input = e.target as HTMLInputElement;\n let val = input.value;\n\n // Keep only last character if multiple were typed somehow\n if (val.length > 1) val = val.slice(-1);\n\n // Validate character\n if (val && !isAllowed(val)) {\n input.value = '';\n\n return;\n }\n\n input.value = val;\n\n const allInputs = getInputs();\n\n emitOtpState(e);\n\n // Auto-advance\n if (val && index < allInputs.length - 1) {\n allInputs[index + 1].focus();\n allInputs[index + 1].select();\n }\n }\n function handleKeydown(e: KeyboardEvent, index: number) {\n const input = e.target as HTMLInputElement;\n const allInputs = getInputs();\n\n listControl.set(index);\n\n if (e.key === 'Backspace') {\n if (input.value) {\n input.value = '';\n } else if (index > 0) {\n const prevIndex = listControl.navigate('prev');\n const prevInput = allInputs[prevIndex];\n\n if (prevInput) {\n prevInput.select();\n prevInput.value = '';\n }\n }\n\n emitOtpState(e, false);\n e.preventDefault();\n } else {\n listControl.handleKeydown(e);\n }\n }\n function handlePaste(e: ClipboardEvent) {\n e.preventDefault();\n\n const pasted = e.clipboardData?.getData('text') ?? '';\n const chars = pasted\n .split('')\n .filter((c) => isAllowed(c))\n .slice(0, lengthValue.value);\n const allInputs = getInputs();\n\n chars.forEach((char, i) => {\n if (allInputs[i]) allInputs[i].value = char;\n });\n\n emitOtpState(e);\n\n // Focus the cell after last pasted char\n const focusIdx = Math.min(chars.length, allInputs.length - 1);\n\n allInputs[focusIdx]?.focus();\n }\n\n watch(props.value, (value) => {\n syncInputsFromValue(String(value || ''));\n });\n\n watch(props.length, () => {\n // Cells re-render when length changes; sync after DOM updates.\n requestAnimationFrame(() => syncInputsFromValue(normalizedPropValue()));\n });\n\n const separatorIdx = computed(() => {\n const len = Number(props.length.value) || 6;\n\n return props.separator.value != null ? Math.floor(len / 2) : -1;\n });\n\n onMounted(() => {\n // Populate cells from value prop on mount\n syncInputsFromValue(normalizedPropValue());\n });\n\n return html`\n <div class=\"otp-group\" part=\"group\" role=\"group\" aria-label=\"${props.label}\">\n ${() =>\n cells.value.map(\n (i) => html`\n ${() =>\n separatorIdx.value > 0 && i === separatorIdx.value\n ? html`\n <span class=\"separator\" aria-hidden=\"true\">${() => props.separator.value || '-'}</span>\n `\n : ''}\n <input\n class=\"cell\"\n part=\"cell\"\n type=\"${() => (props.masked.value ? 'password' : 'text')}\"\n inputmode=\"${() => (props.type.value === 'numeric' ? 'numeric' : 'text')}\"\n maxlength=\"1\"\n autocomplete=\"${() => (i === 0 ? 'one-time-code' : 'off')}\"\n aria-label=\"${() => `Digit ${i + 1} of ${lengthValue.value}`}\"\n disabled=\"${() => (isDisabled.value ? true : null)}\"\n @input=\"${(e: Event) => handleInput(e, i)}\"\n @keydown=\"${(e: KeyboardEvent) => handleKeydown(e, i)}\"\n @paste=\"${(e: ClipboardEvent) => (i === 0 ? handlePaste(e) : e.preventDefault())}\"\n @focus=\"${(e: FocusEvent) => (e.target as HTMLInputElement).select()}\" />\n `,\n )}\n </div>\n `;\n },\n styles: [\n colorThemeMixin,\n sizeVariantMixin({}),\n forcedColorsFocusMixin('.cell'),\n componentStyles,\n // Must come after `componentStyles` — see `ore-input`'s identical ordering note for why.\n fieldVariantMixin({ accentVar: '--_cell-focus-border', container: '.cell', tokenPrefix: 'otp-cell' }),\n ],\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"otp-input.cjs","names":[],"sources":["../src/inputs/otp-input/otp-input.ts"],"sourcesContent":["import { bind, define, getHost, html, onCleanup, onMounted, prop, useEmit, useField } from '@vielzeug/ore';\nimport { computed, signal, watch } from '@vielzeug/ripple';\nimport { createListControl, lifecycleSignal } from '../../core';\nimport { disablableBundle, sizableBundle, themableBundle } from '../../shared';\nimport { colorThemeMixin, fieldVariantMixin, forcedColorsFocusMixin, sizeVariantMixin } from '../../styles';\nimport type { ComponentSize, ThemeColor, VisualVariant } from '../../types';\nimport { defineFieldValue, dispatchNativeFieldEvent, setFieldValue } from '../shared/native-field-event';\nimport componentStyles from './otp-input.css?inline';\n\nexport type OreOtpInputEvents = {\n change: Event;\n complete: { originalEvent?: Event; value: string };\n input: Event;\n};\n\n/** OTP Input props */\nexport type OreOtpInputProps = {\n /** Theme color */\n color?: ThemeColor;\n /** Make inputs disabled */\n disabled?: boolean;\n /** Accessible label */\n label?: string;\n /** Number of input cells */\n length?: number;\n /** Mask input (show dots instead of characters) */\n masked?: boolean;\n /** Form field name */\n name?: string;\n /** Show a separator in the middle (e.g. \"–\") */\n separator?: string;\n /** Component size */\n size?: ComponentSize;\n /** Input type: 'numeric' (digits only) or 'alphanumeric' */\n type?: 'numeric' | 'alphanumeric';\n /** Current value */\n value?: string;\n /** Visual variant */\n variant?: Exclude<VisualVariant, 'text' | 'frost'>;\n};\n\n/**\n * A segmented OTP (One-Time Password) input with N individual cells.\n * Auto-advances focus on input, auto-backs on Backspace, handles paste.\n *\n * @element ore-otp-input\n *\n * @attr {number} length - Number of cells (default: 6)\n * @attr {string} type - 'numeric' (default) | 'alphanumeric'\n * @attr {string} value - Current code value\n * @attr {boolean} disabled - Disable all cells\n * @attr {boolean} masked - Show as password\n * @attr {string} size - 'sm' | 'md' | 'lg'\n * @attr {string} color - Theme color: 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error'\n * @attr {string} name - Form field name\n * @attr {string} label - Group aria-label\n * @attr {string} separator - Optional separator character shown in the middle\n *\n * @fires input - Emitted whenever a cell changes.\n * @fires change - Emitted whenever a cell changes.\n * @fires complete - Emitted when all cells are filled. detail: { value: string, originalEvent?: Event }\n *\n * @slot label - Custom label above the OTP input\n * @slot helper - Custom helper text below the OTP input\n * @slot error - Custom error content below the OTP input\n *\n * @cssprop --otp-cell-size - Width and height of each cell\n * @cssprop --otp-cell-gap - Gap between cells\n * @cssprop --otp-cell-font-size - Font size inside cells\n * @cssprop --otp-cell-radius - Border radius of cells\n * @cssprop --otp-cell-bg - Cell background color\n * @cssprop --otp-cell-border-color - Default border color\n * @cssprop --otp-cell-focus-border - Focused border/caret color\n * @cssprop --otp-cell-hover-bg - Cell background on hover (flat/ghost variants)\n * @cssprop --otp-cell-hover-border-color - Cell border on hover (flat/bordered variants)\n * @cssprop --otp-cell-focus-bg - Cell background when focused (flat variant)\n * @cssprop --otp-cell-focus-border-color - Cell border when focused (flat variant)\n *\n * @part group - Group container.\n * @part cell - Shadow part for the `cell` element.\n * @example\n * ```html\n * <ore-otp-input length=\"6\" color=\"primary\"></ore-otp-input>\n * ```\n */\nexport const OTP_INPUT_TAG = 'ore-otp-input' as const;\ndefine<OreOtpInputProps>(OTP_INPUT_TAG, {\n formAssociated: true,\n props: {\n ...themableBundle,\n ...sizableBundle,\n ...disablableBundle,\n label: prop.string('One-time password'),\n length: prop.number(6),\n masked: prop.bool(false),\n name: prop.string(),\n separator: prop.string(),\n type: prop.oneOf(['numeric', 'alphanumeric'] as const, 'numeric'),\n value: prop.string(),\n variant: prop.string<Exclude<VisualVariant, 'text' | 'frost'>>(),\n },\n setup(props) {\n const el = getHost();\n const emit = useEmit<OreOtpInputEvents>();\n\n const lengthValue = computed(() => Number(props.length.value) || 6);\n const isDisabled = computed(() => Boolean(props.disabled.value));\n const cells = computed(() => Array.from({ length: lengthValue.value }, (_, i) => i));\n const otpValue = signal(String(props.value.value || ''));\n const normalizedPropValue = () => String(props.value.value || '');\n\n // Form association: the joined cell value is the single form value for `name`,\n // since individual per-cell <input> elements inside the shadow root cannot\n // participate in an ancestor <form>'s submission on their own.\n useField<string>({ disabled: isDisabled, toFormValue: (v) => v, value: otpValue });\n\n bind({\n attr: {\n size: props.size,\n value: () => otpValue.value || null,\n variant: props.variant,\n },\n });\n\n function getInputs(): HTMLInputElement[] {\n return [...(el.shadowRoot?.querySelectorAll<HTMLInputElement>('input.cell') ?? [])];\n }\n\n const listControl = createListControl({\n getItems: () => getInputs(),\n keys: { next: ['ArrowRight'], prev: ['ArrowLeft'] },\n loop: false,\n onNavigate: ({ item }) => {\n item.focus();\n },\n signal: lifecycleSignal(onCleanup),\n });\n\n function getValue(): string {\n return getInputs()\n .map((i) => i.value)\n .join('');\n }\n function syncInputsFromValue(raw: string) {\n const allInputs = getInputs();\n const normalized = String(raw || '').slice(0, allInputs.length);\n\n for (const input of allInputs) input.value = '';\n\n normalized.split('').forEach((c, i) => {\n if (allInputs[i]) allInputs[i].value = c;\n });\n\n otpValue.value = getValue();\n }\n defineFieldValue(\n el,\n () => otpValue.value,\n (value) => {\n syncInputsFromValue(value);\n },\n );\n function emitOtpState(originalEvent?: Event, forceComplete?: boolean): void {\n const allInputs = getInputs();\n const full = getValue();\n const complete = forceComplete ?? (full.length === allInputs.length && full.split('').every(Boolean));\n\n otpValue.value = full;\n setFieldValue(el, full);\n dispatchNativeFieldEvent(el, 'input');\n dispatchNativeFieldEvent(el, 'change');\n\n if (complete) emit('complete', { originalEvent, value: full });\n }\n function isAllowed(char: string): boolean {\n if (props.type.value === 'numeric') return /^\\d$/.test(char);\n\n return /^[a-z\\d]$/i.test(char);\n }\n function handleInput(e: Event, index: number) {\n const input = e.target as HTMLInputElement;\n let val = input.value;\n\n // Keep only last character if multiple were typed somehow\n if (val.length > 1) val = val.slice(-1);\n\n // Validate character\n if (val && !isAllowed(val)) {\n input.value = '';\n\n return;\n }\n\n input.value = val;\n\n const allInputs = getInputs();\n\n emitOtpState(e);\n\n // Auto-advance\n if (val && index < allInputs.length - 1) {\n allInputs[index + 1].focus();\n allInputs[index + 1].select();\n }\n }\n function handleKeydown(e: KeyboardEvent, index: number) {\n const input = e.target as HTMLInputElement;\n const allInputs = getInputs();\n\n listControl.set(index);\n\n if (e.key === 'Backspace') {\n if (input.value) {\n input.value = '';\n } else if (index > 0) {\n const prevIndex = listControl.navigate('prev');\n const prevInput = allInputs[prevIndex];\n\n if (prevInput) {\n prevInput.select();\n prevInput.value = '';\n }\n }\n\n emitOtpState(e, false);\n e.preventDefault();\n } else {\n listControl.handleKeydown(e);\n }\n }\n function handlePaste(e: ClipboardEvent) {\n e.preventDefault();\n\n const pasted = e.clipboardData?.getData('text') ?? '';\n const chars = pasted\n .split('')\n .filter((c) => isAllowed(c))\n .slice(0, lengthValue.value);\n const allInputs = getInputs();\n\n chars.forEach((char, i) => {\n if (allInputs[i]) allInputs[i].value = char;\n });\n\n emitOtpState(e);\n\n // Focus the cell after last pasted char\n const focusIdx = Math.min(chars.length, allInputs.length - 1);\n\n allInputs[focusIdx]?.focus();\n }\n\n watch(props.value, (value) => {\n syncInputsFromValue(String(value || ''));\n });\n\n watch(props.length, () => {\n // Cells re-render when length changes; sync after DOM updates.\n requestAnimationFrame(() => syncInputsFromValue(normalizedPropValue()));\n });\n\n const separatorIdx = computed(() => {\n const len = Number(props.length.value) || 6;\n\n return props.separator.value != null ? Math.floor(len / 2) : -1;\n });\n\n onMounted(() => {\n // Populate cells from value prop on mount\n syncInputsFromValue(normalizedPropValue());\n });\n\n return html`\n <div class=\"otp-group\" part=\"group\" role=\"group\" aria-label=\"${props.label}\">\n ${() =>\n cells.value.map(\n (i) => html`\n ${() =>\n separatorIdx.value > 0 && i === separatorIdx.value\n ? html`\n <span class=\"separator\" aria-hidden=\"true\">${() => props.separator.value || '-'}</span>\n `\n : ''}\n <input\n class=\"cell\"\n part=\"cell\"\n type=\"${() => (props.masked.value ? 'password' : 'text')}\"\n inputmode=\"${() => (props.type.value === 'numeric' ? 'numeric' : 'text')}\"\n maxlength=\"1\"\n autocomplete=\"${() => (i === 0 ? 'one-time-code' : 'off')}\"\n aria-label=\"${() => `Digit ${i + 1} of ${lengthValue.value}`}\"\n disabled=\"${() => (isDisabled.value ? true : null)}\"\n @input=\"${(e: Event) => handleInput(e, i)}\"\n @keydown=\"${(e: KeyboardEvent) => handleKeydown(e, i)}\"\n @paste=\"${(e: ClipboardEvent) => (i === 0 ? handlePaste(e) : e.preventDefault())}\"\n @focus=\"${(e: FocusEvent) => (e.target as HTMLInputElement).select()}\" />\n `,\n )}\n </div>\n `;\n },\n styles: [\n colorThemeMixin,\n sizeVariantMixin({}),\n forcedColorsFocusMixin('.cell'),\n componentStyles,\n // Must come after `componentStyles` — see `ore-input`'s identical ordering note for why.\n fieldVariantMixin({ accentVar: '--_cell-focus-border', container: '.cell', tokenPrefix: 'otp-cell' }),\n ],\n});\n"],"mappings":"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"}
|
package/dist/otp-input.js
CHANGED
package/dist/otp-input.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"otp-input.js","names":[],"sources":["../src/inputs/otp-input/otp-input.ts"],"sourcesContent":["import { bind, define, getHost, html, onCleanup, onMounted, prop, useEmit, useField } from '@vielzeug/ore';\nimport { computed, signal, watch } from '@vielzeug/ripple';\nimport { createListControl, lifecycleSignal } from '../../core';\nimport { disablableBundle, sizableBundle, themableBundle } from '../../shared';\nimport { colorThemeMixin, fieldVariantMixin, forcedColorsFocusMixin, sizeVariantMixin } from '../../styles';\nimport type { ComponentSize, ThemeColor, VisualVariant } from '../../types';\nimport { defineFieldValue, dispatchNativeFieldEvent, setFieldValue } from '../shared/native-field-event';\nimport componentStyles from './otp-input.css?inline';\n\nexport type OreOtpInputEvents = {\n change: Event;\n complete: { originalEvent?: Event; value: string };\n input: Event;\n};\n\n/** OTP Input props */\nexport type OreOtpInputProps = {\n /** Theme color */\n color?: ThemeColor;\n /** Make inputs disabled */\n disabled?: boolean;\n /** Accessible label */\n label?: string;\n /** Number of input cells */\n length?: number;\n /** Mask input (show dots instead of characters) */\n masked?: boolean;\n /** Form field name */\n name?: string;\n /** Show a separator in the middle (e.g. \"–\") */\n separator?: string;\n /** Component size */\n size?: ComponentSize;\n /** Input type: 'numeric' (digits only) or 'alphanumeric' */\n type?: 'numeric' | 'alphanumeric';\n /** Current value */\n value?: string;\n /** Visual variant */\n variant?: Exclude<VisualVariant, 'text' | 'frost'>;\n};\n\n/**\n * A segmented OTP (One-Time Password) input with N individual cells.\n * Auto-advances focus on input, auto-backs on Backspace, handles paste.\n *\n * @element ore-otp-input\n *\n * @attr {number} length - Number of cells (default: 6)\n * @attr {string} type - 'numeric' (default) | 'alphanumeric'\n * @attr {string} value - Current code value\n * @attr {boolean} disabled - Disable all cells\n * @attr {boolean} masked - Show as password\n * @attr {string} size - 'sm' | 'md' | 'lg'\n * @attr {string} color - Theme color: 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error'\n * @attr {string} name - Form field name\n * @attr {string} label - Group aria-label\n * @attr {string} separator - Optional separator character shown in the middle\n *\n * @fires input - Emitted whenever a cell changes.\n * @fires change - Emitted whenever a cell changes.\n * @fires complete - Emitted when all cells are filled. detail: { value: string, originalEvent?: Event }\n *\n * @slot label - Custom label above the OTP input\n * @slot helper - Custom helper text below the OTP input\n * @slot error - Custom error content below the OTP input\n *\n * @cssprop --otp-cell-size - Width and height of each cell\n * @cssprop --otp-cell-gap - Gap between cells\n * @cssprop --otp-cell-font-size - Font size inside cells\n * @cssprop --otp-cell-radius - Border radius of cells\n * @cssprop --otp-cell-bg - Cell background color\n * @cssprop --otp-cell-border-color - Default border color\n * @cssprop --otp-cell-focus-border - Focused border/caret color\n * @cssprop --otp-cell-hover-bg - Cell background on hover (flat/ghost variants)\n * @cssprop --otp-cell-hover-border-color - Cell border on hover (flat/bordered variants)\n * @cssprop --otp-cell-focus-bg - Cell background when focused (flat variant)\n * @cssprop --otp-cell-focus-border-color - Cell border when focused (flat variant)\n *\n * @part group - Group container.\n * @part cell - Shadow part for the `cell` element.\n * @example\n * ```html\n * <ore-otp-input length=\"6\" color=\"primary\"></ore-otp-input>\n * ```\n */\nexport const OTP_INPUT_TAG = 'ore-otp-input' as const;\ndefine<OreOtpInputProps>(OTP_INPUT_TAG, {\n formAssociated: true,\n props: {\n ...themableBundle,\n ...sizableBundle,\n ...disablableBundle,\n label: prop.string('One-time password'),\n length: prop.number(6),\n masked: prop.bool(false),\n name: prop.string(),\n separator: prop.string(),\n type: prop.oneOf(['numeric', 'alphanumeric'] as const, 'numeric'),\n value: prop.string(),\n variant: prop.string<Exclude<VisualVariant, 'text' | 'frost'>>(),\n },\n setup(props) {\n const el = getHost();\n const emit = useEmit<OreOtpInputEvents>();\n\n const lengthValue = computed(() => Number(props.length.value) || 6);\n const isDisabled = computed(() => Boolean(props.disabled.value));\n const cells = computed(() => Array.from({ length: lengthValue.value }, (_, i) => i));\n const otpValue = signal(String(props.value.value || ''));\n const normalizedPropValue = () => String(props.value.value || '');\n\n // Form association: the joined cell value is the single form value for `name`,\n // since individual per-cell <input> elements inside the shadow root cannot\n // participate in an ancestor <form>'s submission on their own.\n useField<string>({ disabled: isDisabled, toFormValue: (v) => v, value: otpValue });\n\n bind({\n attr: {\n size: props.size,\n value: () => otpValue.value || null,\n variant: props.variant,\n },\n });\n\n function getInputs(): HTMLInputElement[] {\n return [...(el.shadowRoot?.querySelectorAll<HTMLInputElement>('input.cell') ?? [])];\n }\n\n const listControl = createListControl({\n getItems: () => getInputs(),\n keys: { next: ['ArrowRight'], prev: ['ArrowLeft'] },\n loop: false,\n onNavigate: (_action, index) => {\n const inputs = getInputs();\n\n inputs[index]?.focus();\n },\n signal: lifecycleSignal(onCleanup),\n });\n\n function getValue(): string {\n return getInputs()\n .map((i) => i.value)\n .join('');\n }\n function syncInputsFromValue(raw: string) {\n const allInputs = getInputs();\n const normalized = String(raw || '').slice(0, allInputs.length);\n\n for (const input of allInputs) input.value = '';\n\n normalized.split('').forEach((c, i) => {\n if (allInputs[i]) allInputs[i].value = c;\n });\n\n otpValue.value = getValue();\n }\n defineFieldValue(\n el,\n () => otpValue.value,\n (value) => {\n syncInputsFromValue(value);\n },\n );\n function emitOtpState(originalEvent?: Event, forceComplete?: boolean): void {\n const allInputs = getInputs();\n const full = getValue();\n const complete = forceComplete ?? (full.length === allInputs.length && full.split('').every(Boolean));\n\n otpValue.value = full;\n setFieldValue(el, full);\n dispatchNativeFieldEvent(el, 'input');\n dispatchNativeFieldEvent(el, 'change');\n\n if (complete) emit('complete', { originalEvent, value: full });\n }\n function isAllowed(char: string): boolean {\n if (props.type.value === 'numeric') return /^\\d$/.test(char);\n\n return /^[a-z\\d]$/i.test(char);\n }\n function handleInput(e: Event, index: number) {\n const input = e.target as HTMLInputElement;\n let val = input.value;\n\n // Keep only last character if multiple were typed somehow\n if (val.length > 1) val = val.slice(-1);\n\n // Validate character\n if (val && !isAllowed(val)) {\n input.value = '';\n\n return;\n }\n\n input.value = val;\n\n const allInputs = getInputs();\n\n emitOtpState(e);\n\n // Auto-advance\n if (val && index < allInputs.length - 1) {\n allInputs[index + 1].focus();\n allInputs[index + 1].select();\n }\n }\n function handleKeydown(e: KeyboardEvent, index: number) {\n const input = e.target as HTMLInputElement;\n const allInputs = getInputs();\n\n listControl.set(index);\n\n if (e.key === 'Backspace') {\n if (input.value) {\n input.value = '';\n } else if (index > 0) {\n const prevIndex = listControl.navigate('prev');\n const prevInput = allInputs[prevIndex];\n\n if (prevInput) {\n prevInput.select();\n prevInput.value = '';\n }\n }\n\n emitOtpState(e, false);\n e.preventDefault();\n } else {\n listControl.handleKeydown(e);\n }\n }\n function handlePaste(e: ClipboardEvent) {\n e.preventDefault();\n\n const pasted = e.clipboardData?.getData('text') ?? '';\n const chars = pasted\n .split('')\n .filter((c) => isAllowed(c))\n .slice(0, lengthValue.value);\n const allInputs = getInputs();\n\n chars.forEach((char, i) => {\n if (allInputs[i]) allInputs[i].value = char;\n });\n\n emitOtpState(e);\n\n // Focus the cell after last pasted char\n const focusIdx = Math.min(chars.length, allInputs.length - 1);\n\n allInputs[focusIdx]?.focus();\n }\n\n watch(props.value, (value) => {\n syncInputsFromValue(String(value || ''));\n });\n\n watch(props.length, () => {\n // Cells re-render when length changes; sync after DOM updates.\n requestAnimationFrame(() => syncInputsFromValue(normalizedPropValue()));\n });\n\n const separatorIdx = computed(() => {\n const len = Number(props.length.value) || 6;\n\n return props.separator.value != null ? Math.floor(len / 2) : -1;\n });\n\n onMounted(() => {\n // Populate cells from value prop on mount\n syncInputsFromValue(normalizedPropValue());\n });\n\n return html`\n <div class=\"otp-group\" part=\"group\" role=\"group\" aria-label=\"${props.label}\">\n ${() =>\n cells.value.map(\n (i) => html`\n ${() =>\n separatorIdx.value > 0 && i === separatorIdx.value\n ? html`\n <span class=\"separator\" aria-hidden=\"true\">${() => props.separator.value || '-'}</span>\n `\n : ''}\n <input\n class=\"cell\"\n part=\"cell\"\n type=\"${() => (props.masked.value ? 'password' : 'text')}\"\n inputmode=\"${() => (props.type.value === 'numeric' ? 'numeric' : 'text')}\"\n maxlength=\"1\"\n autocomplete=\"${() => (i === 0 ? 'one-time-code' : 'off')}\"\n aria-label=\"${() => `Digit ${i + 1} of ${lengthValue.value}`}\"\n disabled=\"${() => (isDisabled.value ? true : null)}\"\n @input=\"${(e: Event) => handleInput(e, i)}\"\n @keydown=\"${(e: KeyboardEvent) => handleKeydown(e, i)}\"\n @paste=\"${(e: ClipboardEvent) => (i === 0 ? handlePaste(e) : e.preventDefault())}\"\n @focus=\"${(e: FocusEvent) => (e.target as HTMLInputElement).select()}\" />\n `,\n )}\n </div>\n `;\n },\n styles: [\n colorThemeMixin,\n sizeVariantMixin({}),\n forcedColorsFocusMixin('.cell'),\n componentStyles,\n // Must come after `componentStyles` — see `ore-input`'s identical ordering note for why.\n fieldVariantMixin({ accentVar: '--_cell-focus-border', container: '.cell', tokenPrefix: 'otp-cell' }),\n ],\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"otp-input.js","names":[],"sources":["../src/inputs/otp-input/otp-input.ts"],"sourcesContent":["import { bind, define, getHost, html, onCleanup, onMounted, prop, useEmit, useField } from '@vielzeug/ore';\nimport { computed, signal, watch } from '@vielzeug/ripple';\nimport { createListControl, lifecycleSignal } from '../../core';\nimport { disablableBundle, sizableBundle, themableBundle } from '../../shared';\nimport { colorThemeMixin, fieldVariantMixin, forcedColorsFocusMixin, sizeVariantMixin } from '../../styles';\nimport type { ComponentSize, ThemeColor, VisualVariant } from '../../types';\nimport { defineFieldValue, dispatchNativeFieldEvent, setFieldValue } from '../shared/native-field-event';\nimport componentStyles from './otp-input.css?inline';\n\nexport type OreOtpInputEvents = {\n change: Event;\n complete: { originalEvent?: Event; value: string };\n input: Event;\n};\n\n/** OTP Input props */\nexport type OreOtpInputProps = {\n /** Theme color */\n color?: ThemeColor;\n /** Make inputs disabled */\n disabled?: boolean;\n /** Accessible label */\n label?: string;\n /** Number of input cells */\n length?: number;\n /** Mask input (show dots instead of characters) */\n masked?: boolean;\n /** Form field name */\n name?: string;\n /** Show a separator in the middle (e.g. \"–\") */\n separator?: string;\n /** Component size */\n size?: ComponentSize;\n /** Input type: 'numeric' (digits only) or 'alphanumeric' */\n type?: 'numeric' | 'alphanumeric';\n /** Current value */\n value?: string;\n /** Visual variant */\n variant?: Exclude<VisualVariant, 'text' | 'frost'>;\n};\n\n/**\n * A segmented OTP (One-Time Password) input with N individual cells.\n * Auto-advances focus on input, auto-backs on Backspace, handles paste.\n *\n * @element ore-otp-input\n *\n * @attr {number} length - Number of cells (default: 6)\n * @attr {string} type - 'numeric' (default) | 'alphanumeric'\n * @attr {string} value - Current code value\n * @attr {boolean} disabled - Disable all cells\n * @attr {boolean} masked - Show as password\n * @attr {string} size - 'sm' | 'md' | 'lg'\n * @attr {string} color - Theme color: 'primary' | 'secondary' | 'info' | 'success' | 'warning' | 'error'\n * @attr {string} name - Form field name\n * @attr {string} label - Group aria-label\n * @attr {string} separator - Optional separator character shown in the middle\n *\n * @fires input - Emitted whenever a cell changes.\n * @fires change - Emitted whenever a cell changes.\n * @fires complete - Emitted when all cells are filled. detail: { value: string, originalEvent?: Event }\n *\n * @slot label - Custom label above the OTP input\n * @slot helper - Custom helper text below the OTP input\n * @slot error - Custom error content below the OTP input\n *\n * @cssprop --otp-cell-size - Width and height of each cell\n * @cssprop --otp-cell-gap - Gap between cells\n * @cssprop --otp-cell-font-size - Font size inside cells\n * @cssprop --otp-cell-radius - Border radius of cells\n * @cssprop --otp-cell-bg - Cell background color\n * @cssprop --otp-cell-border-color - Default border color\n * @cssprop --otp-cell-focus-border - Focused border/caret color\n * @cssprop --otp-cell-hover-bg - Cell background on hover (flat/ghost variants)\n * @cssprop --otp-cell-hover-border-color - Cell border on hover (flat/bordered variants)\n * @cssprop --otp-cell-focus-bg - Cell background when focused (flat variant)\n * @cssprop --otp-cell-focus-border-color - Cell border when focused (flat variant)\n *\n * @part group - Group container.\n * @part cell - Shadow part for the `cell` element.\n * @example\n * ```html\n * <ore-otp-input length=\"6\" color=\"primary\"></ore-otp-input>\n * ```\n */\nexport const OTP_INPUT_TAG = 'ore-otp-input' as const;\ndefine<OreOtpInputProps>(OTP_INPUT_TAG, {\n formAssociated: true,\n props: {\n ...themableBundle,\n ...sizableBundle,\n ...disablableBundle,\n label: prop.string('One-time password'),\n length: prop.number(6),\n masked: prop.bool(false),\n name: prop.string(),\n separator: prop.string(),\n type: prop.oneOf(['numeric', 'alphanumeric'] as const, 'numeric'),\n value: prop.string(),\n variant: prop.string<Exclude<VisualVariant, 'text' | 'frost'>>(),\n },\n setup(props) {\n const el = getHost();\n const emit = useEmit<OreOtpInputEvents>();\n\n const lengthValue = computed(() => Number(props.length.value) || 6);\n const isDisabled = computed(() => Boolean(props.disabled.value));\n const cells = computed(() => Array.from({ length: lengthValue.value }, (_, i) => i));\n const otpValue = signal(String(props.value.value || ''));\n const normalizedPropValue = () => String(props.value.value || '');\n\n // Form association: the joined cell value is the single form value for `name`,\n // since individual per-cell <input> elements inside the shadow root cannot\n // participate in an ancestor <form>'s submission on their own.\n useField<string>({ disabled: isDisabled, toFormValue: (v) => v, value: otpValue });\n\n bind({\n attr: {\n size: props.size,\n value: () => otpValue.value || null,\n variant: props.variant,\n },\n });\n\n function getInputs(): HTMLInputElement[] {\n return [...(el.shadowRoot?.querySelectorAll<HTMLInputElement>('input.cell') ?? [])];\n }\n\n const listControl = createListControl({\n getItems: () => getInputs(),\n keys: { next: ['ArrowRight'], prev: ['ArrowLeft'] },\n loop: false,\n onNavigate: ({ item }) => {\n item.focus();\n },\n signal: lifecycleSignal(onCleanup),\n });\n\n function getValue(): string {\n return getInputs()\n .map((i) => i.value)\n .join('');\n }\n function syncInputsFromValue(raw: string) {\n const allInputs = getInputs();\n const normalized = String(raw || '').slice(0, allInputs.length);\n\n for (const input of allInputs) input.value = '';\n\n normalized.split('').forEach((c, i) => {\n if (allInputs[i]) allInputs[i].value = c;\n });\n\n otpValue.value = getValue();\n }\n defineFieldValue(\n el,\n () => otpValue.value,\n (value) => {\n syncInputsFromValue(value);\n },\n );\n function emitOtpState(originalEvent?: Event, forceComplete?: boolean): void {\n const allInputs = getInputs();\n const full = getValue();\n const complete = forceComplete ?? (full.length === allInputs.length && full.split('').every(Boolean));\n\n otpValue.value = full;\n setFieldValue(el, full);\n dispatchNativeFieldEvent(el, 'input');\n dispatchNativeFieldEvent(el, 'change');\n\n if (complete) emit('complete', { originalEvent, value: full });\n }\n function isAllowed(char: string): boolean {\n if (props.type.value === 'numeric') return /^\\d$/.test(char);\n\n return /^[a-z\\d]$/i.test(char);\n }\n function handleInput(e: Event, index: number) {\n const input = e.target as HTMLInputElement;\n let val = input.value;\n\n // Keep only last character if multiple were typed somehow\n if (val.length > 1) val = val.slice(-1);\n\n // Validate character\n if (val && !isAllowed(val)) {\n input.value = '';\n\n return;\n }\n\n input.value = val;\n\n const allInputs = getInputs();\n\n emitOtpState(e);\n\n // Auto-advance\n if (val && index < allInputs.length - 1) {\n allInputs[index + 1].focus();\n allInputs[index + 1].select();\n }\n }\n function handleKeydown(e: KeyboardEvent, index: number) {\n const input = e.target as HTMLInputElement;\n const allInputs = getInputs();\n\n listControl.set(index);\n\n if (e.key === 'Backspace') {\n if (input.value) {\n input.value = '';\n } else if (index > 0) {\n const prevIndex = listControl.navigate('prev');\n const prevInput = allInputs[prevIndex];\n\n if (prevInput) {\n prevInput.select();\n prevInput.value = '';\n }\n }\n\n emitOtpState(e, false);\n e.preventDefault();\n } else {\n listControl.handleKeydown(e);\n }\n }\n function handlePaste(e: ClipboardEvent) {\n e.preventDefault();\n\n const pasted = e.clipboardData?.getData('text') ?? '';\n const chars = pasted\n .split('')\n .filter((c) => isAllowed(c))\n .slice(0, lengthValue.value);\n const allInputs = getInputs();\n\n chars.forEach((char, i) => {\n if (allInputs[i]) allInputs[i].value = char;\n });\n\n emitOtpState(e);\n\n // Focus the cell after last pasted char\n const focusIdx = Math.min(chars.length, allInputs.length - 1);\n\n allInputs[focusIdx]?.focus();\n }\n\n watch(props.value, (value) => {\n syncInputsFromValue(String(value || ''));\n });\n\n watch(props.length, () => {\n // Cells re-render when length changes; sync after DOM updates.\n requestAnimationFrame(() => syncInputsFromValue(normalizedPropValue()));\n });\n\n const separatorIdx = computed(() => {\n const len = Number(props.length.value) || 6;\n\n return props.separator.value != null ? Math.floor(len / 2) : -1;\n });\n\n onMounted(() => {\n // Populate cells from value prop on mount\n syncInputsFromValue(normalizedPropValue());\n });\n\n return html`\n <div class=\"otp-group\" part=\"group\" role=\"group\" aria-label=\"${props.label}\">\n ${() =>\n cells.value.map(\n (i) => html`\n ${() =>\n separatorIdx.value > 0 && i === separatorIdx.value\n ? html`\n <span class=\"separator\" aria-hidden=\"true\">${() => props.separator.value || '-'}</span>\n `\n : ''}\n <input\n class=\"cell\"\n part=\"cell\"\n type=\"${() => (props.masked.value ? 'password' : 'text')}\"\n inputmode=\"${() => (props.type.value === 'numeric' ? 'numeric' : 'text')}\"\n maxlength=\"1\"\n autocomplete=\"${() => (i === 0 ? 'one-time-code' : 'off')}\"\n aria-label=\"${() => `Digit ${i + 1} of ${lengthValue.value}`}\"\n disabled=\"${() => (isDisabled.value ? true : null)}\"\n @input=\"${(e: Event) => handleInput(e, i)}\"\n @keydown=\"${(e: KeyboardEvent) => handleKeydown(e, i)}\"\n @paste=\"${(e: ClipboardEvent) => (i === 0 ? handlePaste(e) : e.preventDefault())}\"\n @focus=\"${(e: FocusEvent) => (e.target as HTMLInputElement).select()}\" />\n `,\n )}\n </div>\n `;\n },\n styles: [\n colorThemeMixin,\n sizeVariantMixin({}),\n forcedColorsFocusMixin('.cell'),\n componentStyles,\n // Must come after `componentStyles` — see `ore-input`'s identical ordering note for why.\n fieldVariantMixin({ accentVar: '--_cell-focus-border', container: '.cell', tokenPrefix: 'otp-cell' }),\n ],\n});\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drawer.d.ts","sourceRoot":"","sources":["../../../src/overlay/drawer/drawer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"drawer.d.ts","sourceRoot":"","sources":["../../../src/overlay/drawer/drawer.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAC1D,OAAO,yBAAyB,CAAC;AAKjC,KAAK,eAAe,GAAG,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,QAAQ,CAAC;AAC3D,KAAK,UAAU,GAAG,IAAI,GAAG,IAAI,GAAG,MAAM,CAAC;AACvC,KAAK,cAAc,GAAG,QAAQ,GAAG,MAAM,GAAG,aAAa,CAAC;AACxD,KAAK,yBAAyB,GAAG,SAAS,GAAG,OAAO,CAAC;AAiCrD,sEAAsE;AACtE,MAAM,WAAW,aAAc,SAAQ,WAAW,EAAE,IAAI,CAAC,cAAc,EAAE,OAAO,CAAC;IAC/E,iEAAiE;IACjE,IAAI,IAAI,IAAI,CAAC;IACb,sEAAsE;IACtE,IAAI,IAAI,IAAI,CAAC;CACd;AAED,kCAAkC;AAElC,MAAM,MAAM,eAAe,GAAG;IAC5B,aAAa,EAAE,uBAAuB,CAAC;CACxC,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,oEAAoE;IACpE,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,mEAAmE;IACnE,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,8DAA8D;IAC9D,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,4DAA4D;IAC5D,uBAAuB,CAAC,EAAE,yBAAyB,CAAC;IACpD;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4BAA4B;IAC5B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,0EAA0E;IAC1E,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,2CAA2C;IAC3C,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,iCAAiC;IACjC,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAEH,eAAO,MAAM,UAAU,EAAG,YAAqB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"menu.d.ts","sourceRoot":"","sources":["../../../src/overlay/menu/menu.ts"],"names":[],"mappings":"AAgBA,OAAO,EAOL,KAAK,uBAAuB,
|
|
1
|
+
{"version":3,"file":"menu.d.ts","sourceRoot":"","sources":["../../../src/overlay/menu/menu.ts"],"names":[],"mappings":"AAgBA,OAAO,EAOL,KAAK,uBAAuB,EAE7B,MAAM,YAAY,CAAC;AAGpB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAOjD,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,OAAO,CAAC;AAEnD,MAAM,MAAM,aAAa,GAAG;IAC1B,aAAa,EAAE,uBAAuB,CAAC;IACvC,MAAM,EAAE,gBAAgB,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,mEAAmE;IACnE,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,6BAA6B;IAC7B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,QAAQ,GAAG,cAAc,GAAG,YAAY,GAAG,KAAK,GAAG,WAAW,GAAG,SAAS,CAAC;IACvF,IAAI,CAAC,EAAE,aAAa,CAAC;CACtB,CAAC;AAMF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,eAAO,MAAM,aAAa,EAAG,eAAwB,CAAC;AAwDtD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,aAAa,EAAG,oBAA6B,CAAC;AAgB3D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,eAAO,MAAM,QAAQ,EAAG,UAAmB,CAAC"}
|
package/dist/radio-group.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./styles/mixins/states.css.cjs"),t=require("./core/choice-field.cjs"),n=require("./core/light-dom.cjs"),r=require("./core/ore.cjs"),i=require("./core/nav.cjs"),a=require("./shared/prop-bundles.cjs"),o=require("./inputs/radio-group/radio-group.css.cjs");let s=require("@vielzeug/ore");var c=(0,s.createContext)(`OreRadioGroup`),l=`ore-radio-group`;(0,s.define)(l,{formAssociated:!0,props:{...a.themableBundle,...a.sizableBundle,...a.disablableBundle,error:s.prop.string(),helper:s.prop.string(),label:s.prop.string(),name:s.prop.string(),orientation:s.prop.oneOf([`horizontal`,`vertical`],`vertical`),required:s.prop.bool(!1),value:{...s.prop.string(),reflect:!1}},setup(e){let a=(0,s.getHost)(),o=(0,s.useEmit)(),l=(0,s.useSlots)(),u=s.watchEffect,d=t.createChoiceField({disabled:e.disabled,error:e.error,helper:e.helper,label:e.label,prefix:`radio-group`,required:e.required,signal:r.lifecycleSignal(s.onCleanup),value:e.value});d.attachFormField((0,s.useField)({disabled:d.disabled,onReset:d.reset,toFormValue:e=>e,validationMessage:d.validationMessage,validity:d.validity,value:d.formValue}));let f=d.selectedValue,p=e.disabled;(0,s.bind)({attr:{size:e.size,value:()=>f.value||null}});let m=()=>n.getLightChildrenByTag(a,`ore-radio`),h=()=>p.value?[]:m().filter(e=>!e.hasAttribute(`disabled`)),g=e=>n.getChoiceLabel(m(),e),_=(e,t)=>{d.setValues(e?[e]:[]);let n=e?[g(e)]:[];o(`change`,{labels:n,originalEvent:t,values:e?[e]:[]}),d.triggerValidation(`blur`)};(0,s.provide)(c,{color:e.color,disabled:p,name:e.name,select:_,size:e.size,value:f}),u(()=>{l.elements().value,f.value;let t=m();for(let n of t){let t=n.getAttribute(`value`)??``;n.toggleAttribute(`checked`,t===f.value),e.name.value?n.setAttribute(`name`,e.name.value):n.removeAttribute(`name`),e.color.value?n.setAttribute(`color`,e.color.value):n.removeAttribute(`color`),e.size.value?n.setAttribute(`size`,e.size.value):n.removeAttribute(`size`),n.toggleAttribute(`disabled`,p.value)}}),u(()=>{l.elements().value;let e=m(),t=!1;for(let n of e){let e=n.getAttribute(`value`)===f.value;n.setAttribute(`tabindex`,e&&!p.value?`0`:`-1`),e&&!p.value&&(t=!0)}if(!t&&e.length>0){let t=e.find(e=>!e.hasAttribute(`disabled`));t&&t.setAttribute(`tabindex`,`0`)}});let v=i.createListControl({getItems:h,keys:{next:[`ArrowDown`,`ArrowRight`],prev:[`ArrowUp`,`ArrowLeft`]},loop:!0,onNavigate:(e,t
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./styles/mixins/states.css.cjs"),t=require("./core/choice-field.cjs"),n=require("./core/light-dom.cjs"),r=require("./core/ore.cjs"),i=require("./core/nav.cjs"),a=require("./shared/prop-bundles.cjs"),o=require("./inputs/radio-group/radio-group.css.cjs");let s=require("@vielzeug/ore");var c=(0,s.createContext)(`OreRadioGroup`),l=`ore-radio-group`;(0,s.define)(l,{formAssociated:!0,props:{...a.themableBundle,...a.sizableBundle,...a.disablableBundle,error:s.prop.string(),helper:s.prop.string(),label:s.prop.string(),name:s.prop.string(),orientation:s.prop.oneOf([`horizontal`,`vertical`],`vertical`),required:s.prop.bool(!1),value:{...s.prop.string(),reflect:!1}},setup(e){let a=(0,s.getHost)(),o=(0,s.useEmit)(),l=(0,s.useSlots)(),u=s.watchEffect,d=t.createChoiceField({disabled:e.disabled,error:e.error,helper:e.helper,label:e.label,prefix:`radio-group`,required:e.required,signal:r.lifecycleSignal(s.onCleanup),value:e.value});d.attachFormField((0,s.useField)({disabled:d.disabled,onReset:d.reset,toFormValue:e=>e,validationMessage:d.validationMessage,validity:d.validity,value:d.formValue}));let f=d.selectedValue,p=e.disabled;(0,s.bind)({attr:{size:e.size,value:()=>f.value||null}});let m=()=>n.getLightChildrenByTag(a,`ore-radio`),h=()=>p.value?[]:m().filter(e=>!e.hasAttribute(`disabled`)),g=e=>n.getChoiceLabel(m(),e),_=(e,t)=>{d.setValues(e?[e]:[]);let n=e?[g(e)]:[];o(`change`,{labels:n,originalEvent:t,values:e?[e]:[]}),d.triggerValidation(`blur`)};(0,s.provide)(c,{color:e.color,disabled:p,name:e.name,select:_,size:e.size,value:f}),u(()=>{l.elements().value,f.value;let t=m();for(let n of t){let t=n.getAttribute(`value`)??``;n.toggleAttribute(`checked`,t===f.value),e.name.value?n.setAttribute(`name`,e.name.value):n.removeAttribute(`name`),e.color.value?n.setAttribute(`color`,e.color.value):n.removeAttribute(`color`),e.size.value?n.setAttribute(`size`,e.size.value):n.removeAttribute(`size`),n.toggleAttribute(`disabled`,p.value)}}),u(()=>{l.elements().value;let e=m(),t=!1;for(let n of e){let e=n.getAttribute(`value`)===f.value;n.setAttribute(`tabindex`,e&&!p.value?`0`:`-1`),e&&!p.value&&(t=!0)}if(!t&&e.length>0){let t=e.find(e=>!e.hasAttribute(`disabled`));t&&t.setAttribute(`tabindex`,`0`)}});let v=i.createListControl({getItems:h,keys:{next:[`ArrowDown`,`ArrowRight`],prev:[`ArrowUp`,`ArrowLeft`]},loop:!0,onNavigate:({event:e,item:t})=>{t.focus(),t.tagName===`ORE-RADIO`&&_(t.getAttribute(`value`)??``,e)},signal:r.lifecycleSignal(s.onCleanup)});(0,s.bind)({on:{change:e=>{e.target!==a&&(e.stopPropagation(),_(e.target.getAttribute(`value`)??``,e))},keydown:e=>{let t=h();if(!t.length)return;let n=t.indexOf(document.activeElement);n!==-1&&(v.set(n),v.handleKeydown(e))}}});let y=(0,s.createStableId)(`radio-group-legend`);return s.html`
|
|
2
2
|
<fieldset
|
|
3
3
|
role="radiogroup"
|
|
4
4
|
aria-required="${()=>String(!!e.required.value)}"
|
package/dist/radio-group.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"radio-group.cjs","names":[],"sources":["../src/inputs/radio-group/radio-group.ts"],"sourcesContent":["import {\n bind,\n createContext,\n createStableId,\n define,\n getHost,\n html,\n onCleanup,\n prop,\n provide,\n useEmit,\n useField,\n useSlots,\n watchEffect,\n when,\n} from '@vielzeug/ore';\nimport type { Readable } from '@vielzeug/ripple';\nimport {\n type ChoiceChangeDetail,\n createChoiceField,\n createListControl,\n getChoiceLabel,\n getLightChildrenByTag,\n lifecycleSignal,\n} from '../../core';\nimport { disablableBundle, sizableBundle, themableBundle } from '../../shared';\nimport { disabledStateMixin } from '../../styles';\nimport type { ComponentSize, ThemeColor } from '../../types';\nimport componentStyles from './radio-group.css?inline';\n\n/** Radio group component properties */\nexport type OreRadioGroupProps = {\n /** Theme color tint */\n color?: ThemeColor;\n /** Disabled state */\n disabled?: boolean;\n /** Error message text */\n error?: string;\n /** Helper text displayed below the items */\n helper?: string;\n /** Group label text */\n label?: string;\n /** Form field name */\n name?: string;\n /** Layout orientation */\n orientation?: 'horizontal' | 'vertical';\n /** Required field */\n required?: boolean;\n /** Items size preset */\n size?: ComponentSize;\n /** Initial selected value */\n value?: string;\n};\n\nexport type RadioGroupContext = {\n color: Readable<ThemeColor | undefined>;\n disabled: Readable<boolean>;\n name: Readable<string | undefined>;\n select: (value: string, originalEvent?: Event) => void;\n size: Readable<ComponentSize | undefined>;\n value: Readable<string | undefined>;\n};\n\nexport const RADIO_GROUP_CTX = createContext<RadioGroupContext | undefined>('OreRadioGroup');\n\n/** Events emitted by the radio-group component */\nexport type OreRadioGroupEvents = {\n /** Emitted when the selection changes */\n change: ChoiceChangeDetail;\n};\n\n/**\n * A group of radio buttons that allows users to select a single option from a set.\n * Supports keyboard navigation (arrows) and automatic value management.\n *\n * @element ore-radio-group\n *\n * @attr {string} value - Selected value\n * @attr {string} name - Form field name\n * @attr {string} label - Group label\n * @attr {string} orientation - Layout: 'vertical' | 'horizontal'\n * @attr {boolean} required - Required field\n *\n * @fires change - Emitted when a radio is selected. detail: { values: string[], labels: string[], originalEvent?: Event }\n *\n * @slot - Place `ore-radio` elements here\n *\n * @cssprop --radio-group-direction - Flex direction of the items list ('row' | 'column')\n * @cssprop --radio-group-gap - Gap between radio items\n * @part items - Items container.\n * @example\n * ```html\n * <ore-radio-group name=\"plan\" label=\"Choose a plan\" value=\"free\" required>\n * <ore-radio value=\"free\">Free</ore-radio>\n * <ore-radio value=\"pro\">Pro</ore-radio>\n * <ore-radio value=\"enterprise\">Enterprise</ore-radio>\n * </ore-radio-group>\n * <ore-radio-group name=\"direction\" orientation=\"horizontal\" color=\"primary\">\n * <ore-radio value=\"left\">Left</ore-radio>\n * <ore-radio value=\"right\">Right</ore-radio>\n * </ore-radio-group>\n * ```\n */\nexport const RADIO_GROUP_TAG = 'ore-radio-group' as const;\ndefine<OreRadioGroupProps>(RADIO_GROUP_TAG, {\n formAssociated: true,\n props: {\n ...themableBundle,\n ...sizableBundle,\n ...disablableBundle,\n error: prop.string(),\n helper: prop.string(),\n label: prop.string(),\n name: prop.string(),\n orientation: prop.oneOf(['horizontal', 'vertical'] as const, 'vertical'),\n required: prop.bool(false),\n // Not auto-reflected (`reflect: false`) — the derived, interaction-updated selection\n // (`selectedValue`) is the single writer for this attribute, via `bind()` below; letting\n // `prop.string()`'s own default reflection also write the raw incoming value would leave\n // two effects racing to set the same attribute from different sources.\n value: { ...prop.string(), reflect: false },\n },\n setup(props) {\n const el = getHost();\n const emit = useEmit<OreRadioGroupEvents>();\n const slots = useSlots();\n const watch = watchEffect;\n\n const choice = createChoiceField({\n disabled: props.disabled,\n error: props.error,\n helper: props.helper,\n label: props.label,\n prefix: 'radio-group',\n required: props.required,\n signal: lifecycleSignal(onCleanup),\n value: props.value,\n });\n\n choice.attachFormField(\n useField<string>({\n disabled: choice.disabled,\n onReset: choice.reset,\n toFormValue: (v) => v,\n validationMessage: choice.validationMessage,\n validity: choice.validity,\n value: choice.formValue,\n }),\n );\n\n const selectedValue = choice.selectedValue;\n const isDisabled = props.disabled;\n\n bind({\n attr: {\n size: props.size,\n value: () => selectedValue.value || null,\n },\n });\n\n const getSlottedRadios = (): HTMLElement[] => getLightChildrenByTag(el, 'ore-radio');\n\n const getEnabledRadios = (): HTMLElement[] =>\n isDisabled.value ? [] : getSlottedRadios().filter((radio) => !radio.hasAttribute('disabled'));\n\n const getLabelForValue = (value: string): string => getChoiceLabel(getSlottedRadios(), value);\n\n const selectRadio = (val: string, originalEvent?: Event): void => {\n choice.setValues(val ? [val] : []);\n\n const labels = val ? [getLabelForValue(val)] : [];\n const values = val ? [val] : [];\n\n emit('change', { labels, originalEvent, values });\n choice.triggerValidation('blur');\n };\n\n provide(RADIO_GROUP_CTX, {\n color: props.color as Readable<ThemeColor | undefined>,\n disabled: isDisabled,\n name: props.name,\n select: selectRadio,\n size: props.size as Readable<ComponentSize | undefined>,\n value: selectedValue,\n });\n\n // Sync name/color/size/disabled/checked onto slotted ore-radio children.\n watch(() => {\n void slots.elements().value;\n void selectedValue.value;\n\n const radios = getSlottedRadios();\n\n for (const radio of radios) {\n const val = radio.getAttribute('value') ?? '';\n\n radio.toggleAttribute('checked', val === selectedValue.value);\n\n if (props.name.value) radio.setAttribute('name', props.name.value);\n else radio.removeAttribute('name');\n\n if (props.color.value) radio.setAttribute('color', props.color.value);\n else radio.removeAttribute('color');\n\n if (props.size.value) radio.setAttribute('size', props.size.value);\n else radio.removeAttribute('size');\n\n radio.toggleAttribute('disabled', isDisabled.value);\n }\n });\n\n // Roving tabindex: only the selected (or first) radio is tabbable.\n watch(() => {\n void slots.elements().value;\n\n const radios = getSlottedRadios();\n let hasFocusable = false;\n\n for (const radio of radios) {\n const isSelected = radio.getAttribute('value') === selectedValue.value;\n\n radio.setAttribute('tabindex', isSelected && !isDisabled.value ? '0' : '-1');\n\n if (isSelected && !isDisabled.value) hasFocusable = true;\n }\n\n if (!hasFocusable && radios.length > 0) {\n const first = radios.find((r) => !r.hasAttribute('disabled'));\n\n if (first) first.setAttribute('tabindex', '0');\n }\n });\n\n const listControl = createListControl<HTMLElement>({\n getItems: getEnabledRadios,\n keys: { next: ['ArrowDown', 'ArrowRight'], prev: ['ArrowUp', 'ArrowLeft'] },\n loop: true,\n onNavigate: (_action, index, event) => {\n const radio = getEnabledRadios()[index];\n\n if (!radio) return;\n\n radio.focus();\n\n if (radio.tagName === 'ORE-RADIO') {\n selectRadio(radio.getAttribute('value') ?? '', event);\n }\n },\n signal: lifecycleSignal(onCleanup),\n });\n\n bind({\n on: {\n change: (e: Event) => {\n if (e.target === el) return;\n\n e.stopPropagation();\n selectRadio((e.target as HTMLElement).getAttribute('value') ?? '', e);\n },\n keydown: (e: KeyboardEvent) => {\n const radios = getEnabledRadios();\n\n if (!radios.length) return;\n\n const focused = radios.indexOf(document.activeElement as HTMLElement);\n\n if (focused === -1) return;\n\n listControl.set(focused);\n listControl.handleKeydown(e);\n },\n },\n });\n\n const legendId = createStableId('radio-group-legend');\n\n return html`\n <fieldset\n role=\"radiogroup\"\n aria-required=\"${() => String(Boolean(props.required.value))}\"\n aria-invalid=\"${choice.ariaInvalid}\"\n aria-errormessage=\"${choice.ariaErrorMessage}\"\n aria-describedby=\"${choice.ariaDescribedBy}\">\n <legend id=\"${legendId}\" ?hidden=${() => !props.label.value}>\n ${props.label}${when(\n () => Boolean(props.required.value),\n () => html`\n <span aria-hidden=\"true\">*</span>\n `,\n )}\n </legend>\n <div class=\"radio-group-items\" part=\"items\">\n <slot></slot>\n </div>\n <div\n class=\"helper-text\"\n part=\"helper\"\n id=\"${choice.assistiveId}\"\n role=\"${() => (choice.errorText.value ? 'alert' : null)}\"\n aria-live=\"polite\"\n ?hidden=\"${() => !choice.errorText.value && !choice.helperText.value}\">\n ${() => choice.errorText.value || choice.helperText.value}\n </div>\n </fieldset>\n `;\n },\n styles: [disabledStateMixin, componentStyles],\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"radio-group.cjs","names":[],"sources":["../src/inputs/radio-group/radio-group.ts"],"sourcesContent":["import {\n bind,\n createContext,\n createStableId,\n define,\n getHost,\n html,\n onCleanup,\n prop,\n provide,\n useEmit,\n useField,\n useSlots,\n watchEffect,\n when,\n} from '@vielzeug/ore';\nimport type { Readable } from '@vielzeug/ripple';\nimport {\n type ChoiceChangeDetail,\n createChoiceField,\n createListControl,\n getChoiceLabel,\n getLightChildrenByTag,\n lifecycleSignal,\n} from '../../core';\nimport { disablableBundle, sizableBundle, themableBundle } from '../../shared';\nimport { disabledStateMixin } from '../../styles';\nimport type { ComponentSize, ThemeColor } from '../../types';\nimport componentStyles from './radio-group.css?inline';\n\n/** Radio group component properties */\nexport type OreRadioGroupProps = {\n /** Theme color tint */\n color?: ThemeColor;\n /** Disabled state */\n disabled?: boolean;\n /** Error message text */\n error?: string;\n /** Helper text displayed below the items */\n helper?: string;\n /** Group label text */\n label?: string;\n /** Form field name */\n name?: string;\n /** Layout orientation */\n orientation?: 'horizontal' | 'vertical';\n /** Required field */\n required?: boolean;\n /** Items size preset */\n size?: ComponentSize;\n /** Initial selected value */\n value?: string;\n};\n\nexport type RadioGroupContext = {\n color: Readable<ThemeColor | undefined>;\n disabled: Readable<boolean>;\n name: Readable<string | undefined>;\n select: (value: string, originalEvent?: Event) => void;\n size: Readable<ComponentSize | undefined>;\n value: Readable<string | undefined>;\n};\n\nexport const RADIO_GROUP_CTX = createContext<RadioGroupContext | undefined>('OreRadioGroup');\n\n/** Events emitted by the radio-group component */\nexport type OreRadioGroupEvents = {\n /** Emitted when the selection changes */\n change: ChoiceChangeDetail;\n};\n\n/**\n * A group of radio buttons that allows users to select a single option from a set.\n * Supports keyboard navigation (arrows) and automatic value management.\n *\n * @element ore-radio-group\n *\n * @attr {string} value - Selected value\n * @attr {string} name - Form field name\n * @attr {string} label - Group label\n * @attr {string} orientation - Layout: 'vertical' | 'horizontal'\n * @attr {boolean} required - Required field\n *\n * @fires change - Emitted when a radio is selected. detail: { values: string[], labels: string[], originalEvent?: Event }\n *\n * @slot - Place `ore-radio` elements here\n *\n * @cssprop --radio-group-direction - Flex direction of the items list ('row' | 'column')\n * @cssprop --radio-group-gap - Gap between radio items\n * @part items - Items container.\n * @example\n * ```html\n * <ore-radio-group name=\"plan\" label=\"Choose a plan\" value=\"free\" required>\n * <ore-radio value=\"free\">Free</ore-radio>\n * <ore-radio value=\"pro\">Pro</ore-radio>\n * <ore-radio value=\"enterprise\">Enterprise</ore-radio>\n * </ore-radio-group>\n * <ore-radio-group name=\"direction\" orientation=\"horizontal\" color=\"primary\">\n * <ore-radio value=\"left\">Left</ore-radio>\n * <ore-radio value=\"right\">Right</ore-radio>\n * </ore-radio-group>\n * ```\n */\nexport const RADIO_GROUP_TAG = 'ore-radio-group' as const;\ndefine<OreRadioGroupProps>(RADIO_GROUP_TAG, {\n formAssociated: true,\n props: {\n ...themableBundle,\n ...sizableBundle,\n ...disablableBundle,\n error: prop.string(),\n helper: prop.string(),\n label: prop.string(),\n name: prop.string(),\n orientation: prop.oneOf(['horizontal', 'vertical'] as const, 'vertical'),\n required: prop.bool(false),\n // Not auto-reflected (`reflect: false`) — the derived, interaction-updated selection\n // (`selectedValue`) is the single writer for this attribute, via `bind()` below; letting\n // `prop.string()`'s own default reflection also write the raw incoming value would leave\n // two effects racing to set the same attribute from different sources.\n value: { ...prop.string(), reflect: false },\n },\n setup(props) {\n const el = getHost();\n const emit = useEmit<OreRadioGroupEvents>();\n const slots = useSlots();\n const watch = watchEffect;\n\n const choice = createChoiceField({\n disabled: props.disabled,\n error: props.error,\n helper: props.helper,\n label: props.label,\n prefix: 'radio-group',\n required: props.required,\n signal: lifecycleSignal(onCleanup),\n value: props.value,\n });\n\n choice.attachFormField(\n useField<string>({\n disabled: choice.disabled,\n onReset: choice.reset,\n toFormValue: (v) => v,\n validationMessage: choice.validationMessage,\n validity: choice.validity,\n value: choice.formValue,\n }),\n );\n\n const selectedValue = choice.selectedValue;\n const isDisabled = props.disabled;\n\n bind({\n attr: {\n size: props.size,\n value: () => selectedValue.value || null,\n },\n });\n\n const getSlottedRadios = (): HTMLElement[] => getLightChildrenByTag(el, 'ore-radio');\n\n const getEnabledRadios = (): HTMLElement[] =>\n isDisabled.value ? [] : getSlottedRadios().filter((radio) => !radio.hasAttribute('disabled'));\n\n const getLabelForValue = (value: string): string => getChoiceLabel(getSlottedRadios(), value);\n\n const selectRadio = (val: string, originalEvent?: Event): void => {\n choice.setValues(val ? [val] : []);\n\n const labels = val ? [getLabelForValue(val)] : [];\n const values = val ? [val] : [];\n\n emit('change', { labels, originalEvent, values });\n choice.triggerValidation('blur');\n };\n\n provide(RADIO_GROUP_CTX, {\n color: props.color as Readable<ThemeColor | undefined>,\n disabled: isDisabled,\n name: props.name,\n select: selectRadio,\n size: props.size as Readable<ComponentSize | undefined>,\n value: selectedValue,\n });\n\n // Sync name/color/size/disabled/checked onto slotted ore-radio children.\n watch(() => {\n void slots.elements().value;\n void selectedValue.value;\n\n const radios = getSlottedRadios();\n\n for (const radio of radios) {\n const val = radio.getAttribute('value') ?? '';\n\n radio.toggleAttribute('checked', val === selectedValue.value);\n\n if (props.name.value) radio.setAttribute('name', props.name.value);\n else radio.removeAttribute('name');\n\n if (props.color.value) radio.setAttribute('color', props.color.value);\n else radio.removeAttribute('color');\n\n if (props.size.value) radio.setAttribute('size', props.size.value);\n else radio.removeAttribute('size');\n\n radio.toggleAttribute('disabled', isDisabled.value);\n }\n });\n\n // Roving tabindex: only the selected (or first) radio is tabbable.\n watch(() => {\n void slots.elements().value;\n\n const radios = getSlottedRadios();\n let hasFocusable = false;\n\n for (const radio of radios) {\n const isSelected = radio.getAttribute('value') === selectedValue.value;\n\n radio.setAttribute('tabindex', isSelected && !isDisabled.value ? '0' : '-1');\n\n if (isSelected && !isDisabled.value) hasFocusable = true;\n }\n\n if (!hasFocusable && radios.length > 0) {\n const first = radios.find((r) => !r.hasAttribute('disabled'));\n\n if (first) first.setAttribute('tabindex', '0');\n }\n });\n\n const listControl = createListControl<HTMLElement>({\n getItems: getEnabledRadios,\n keys: { next: ['ArrowDown', 'ArrowRight'], prev: ['ArrowUp', 'ArrowLeft'] },\n loop: true,\n onNavigate: ({ event, item }) => {\n item.focus();\n\n if (item.tagName === 'ORE-RADIO') {\n selectRadio(item.getAttribute('value') ?? '', event);\n }\n },\n signal: lifecycleSignal(onCleanup),\n });\n\n bind({\n on: {\n change: (e: Event) => {\n if (e.target === el) return;\n\n e.stopPropagation();\n selectRadio((e.target as HTMLElement).getAttribute('value') ?? '', e);\n },\n keydown: (e: KeyboardEvent) => {\n const radios = getEnabledRadios();\n\n if (!radios.length) return;\n\n const focused = radios.indexOf(document.activeElement as HTMLElement);\n\n if (focused === -1) return;\n\n listControl.set(focused);\n listControl.handleKeydown(e);\n },\n },\n });\n\n const legendId = createStableId('radio-group-legend');\n\n return html`\n <fieldset\n role=\"radiogroup\"\n aria-required=\"${() => String(Boolean(props.required.value))}\"\n aria-invalid=\"${choice.ariaInvalid}\"\n aria-errormessage=\"${choice.ariaErrorMessage}\"\n aria-describedby=\"${choice.ariaDescribedBy}\">\n <legend id=\"${legendId}\" ?hidden=${() => !props.label.value}>\n ${props.label}${when(\n () => Boolean(props.required.value),\n () => html`\n <span aria-hidden=\"true\">*</span>\n `,\n )}\n </legend>\n <div class=\"radio-group-items\" part=\"items\">\n <slot></slot>\n </div>\n <div\n class=\"helper-text\"\n part=\"helper\"\n id=\"${choice.assistiveId}\"\n role=\"${() => (choice.errorText.value ? 'alert' : null)}\"\n aria-live=\"polite\"\n ?hidden=\"${() => !choice.errorText.value && !choice.helperText.value}\">\n ${() => choice.errorText.value || choice.helperText.value}\n </div>\n </fieldset>\n `;\n },\n styles: [disabledStateMixin, componentStyles],\n});\n"],"mappings":"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"}
|
package/dist/radio-group.js
CHANGED
|
@@ -91,9 +91,8 @@ p(T, {
|
|
|
91
91
|
prev: ["ArrowUp", "ArrowLeft"]
|
|
92
92
|
},
|
|
93
93
|
loop: !0,
|
|
94
|
-
onNavigate: (e, t
|
|
95
|
-
|
|
96
|
-
r && (r.focus(), r.tagName === "ORE-RADIO" && O(r.getAttribute("value") ?? "", n));
|
|
94
|
+
onNavigate: ({ event: e, item: t }) => {
|
|
95
|
+
t.focus(), t.tagName === "ORE-RADIO" && O(t.getAttribute("value") ?? "", e);
|
|
97
96
|
},
|
|
98
97
|
signal: i(g)
|
|
99
98
|
});
|
package/dist/radio-group.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"radio-group.js","names":[],"sources":["../src/inputs/radio-group/radio-group.ts"],"sourcesContent":["import {\n bind,\n createContext,\n createStableId,\n define,\n getHost,\n html,\n onCleanup,\n prop,\n provide,\n useEmit,\n useField,\n useSlots,\n watchEffect,\n when,\n} from '@vielzeug/ore';\nimport type { Readable } from '@vielzeug/ripple';\nimport {\n type ChoiceChangeDetail,\n createChoiceField,\n createListControl,\n getChoiceLabel,\n getLightChildrenByTag,\n lifecycleSignal,\n} from '../../core';\nimport { disablableBundle, sizableBundle, themableBundle } from '../../shared';\nimport { disabledStateMixin } from '../../styles';\nimport type { ComponentSize, ThemeColor } from '../../types';\nimport componentStyles from './radio-group.css?inline';\n\n/** Radio group component properties */\nexport type OreRadioGroupProps = {\n /** Theme color tint */\n color?: ThemeColor;\n /** Disabled state */\n disabled?: boolean;\n /** Error message text */\n error?: string;\n /** Helper text displayed below the items */\n helper?: string;\n /** Group label text */\n label?: string;\n /** Form field name */\n name?: string;\n /** Layout orientation */\n orientation?: 'horizontal' | 'vertical';\n /** Required field */\n required?: boolean;\n /** Items size preset */\n size?: ComponentSize;\n /** Initial selected value */\n value?: string;\n};\n\nexport type RadioGroupContext = {\n color: Readable<ThemeColor | undefined>;\n disabled: Readable<boolean>;\n name: Readable<string | undefined>;\n select: (value: string, originalEvent?: Event) => void;\n size: Readable<ComponentSize | undefined>;\n value: Readable<string | undefined>;\n};\n\nexport const RADIO_GROUP_CTX = createContext<RadioGroupContext | undefined>('OreRadioGroup');\n\n/** Events emitted by the radio-group component */\nexport type OreRadioGroupEvents = {\n /** Emitted when the selection changes */\n change: ChoiceChangeDetail;\n};\n\n/**\n * A group of radio buttons that allows users to select a single option from a set.\n * Supports keyboard navigation (arrows) and automatic value management.\n *\n * @element ore-radio-group\n *\n * @attr {string} value - Selected value\n * @attr {string} name - Form field name\n * @attr {string} label - Group label\n * @attr {string} orientation - Layout: 'vertical' | 'horizontal'\n * @attr {boolean} required - Required field\n *\n * @fires change - Emitted when a radio is selected. detail: { values: string[], labels: string[], originalEvent?: Event }\n *\n * @slot - Place `ore-radio` elements here\n *\n * @cssprop --radio-group-direction - Flex direction of the items list ('row' | 'column')\n * @cssprop --radio-group-gap - Gap between radio items\n * @part items - Items container.\n * @example\n * ```html\n * <ore-radio-group name=\"plan\" label=\"Choose a plan\" value=\"free\" required>\n * <ore-radio value=\"free\">Free</ore-radio>\n * <ore-radio value=\"pro\">Pro</ore-radio>\n * <ore-radio value=\"enterprise\">Enterprise</ore-radio>\n * </ore-radio-group>\n * <ore-radio-group name=\"direction\" orientation=\"horizontal\" color=\"primary\">\n * <ore-radio value=\"left\">Left</ore-radio>\n * <ore-radio value=\"right\">Right</ore-radio>\n * </ore-radio-group>\n * ```\n */\nexport const RADIO_GROUP_TAG = 'ore-radio-group' as const;\ndefine<OreRadioGroupProps>(RADIO_GROUP_TAG, {\n formAssociated: true,\n props: {\n ...themableBundle,\n ...sizableBundle,\n ...disablableBundle,\n error: prop.string(),\n helper: prop.string(),\n label: prop.string(),\n name: prop.string(),\n orientation: prop.oneOf(['horizontal', 'vertical'] as const, 'vertical'),\n required: prop.bool(false),\n // Not auto-reflected (`reflect: false`) — the derived, interaction-updated selection\n // (`selectedValue`) is the single writer for this attribute, via `bind()` below; letting\n // `prop.string()`'s own default reflection also write the raw incoming value would leave\n // two effects racing to set the same attribute from different sources.\n value: { ...prop.string(), reflect: false },\n },\n setup(props) {\n const el = getHost();\n const emit = useEmit<OreRadioGroupEvents>();\n const slots = useSlots();\n const watch = watchEffect;\n\n const choice = createChoiceField({\n disabled: props.disabled,\n error: props.error,\n helper: props.helper,\n label: props.label,\n prefix: 'radio-group',\n required: props.required,\n signal: lifecycleSignal(onCleanup),\n value: props.value,\n });\n\n choice.attachFormField(\n useField<string>({\n disabled: choice.disabled,\n onReset: choice.reset,\n toFormValue: (v) => v,\n validationMessage: choice.validationMessage,\n validity: choice.validity,\n value: choice.formValue,\n }),\n );\n\n const selectedValue = choice.selectedValue;\n const isDisabled = props.disabled;\n\n bind({\n attr: {\n size: props.size,\n value: () => selectedValue.value || null,\n },\n });\n\n const getSlottedRadios = (): HTMLElement[] => getLightChildrenByTag(el, 'ore-radio');\n\n const getEnabledRadios = (): HTMLElement[] =>\n isDisabled.value ? [] : getSlottedRadios().filter((radio) => !radio.hasAttribute('disabled'));\n\n const getLabelForValue = (value: string): string => getChoiceLabel(getSlottedRadios(), value);\n\n const selectRadio = (val: string, originalEvent?: Event): void => {\n choice.setValues(val ? [val] : []);\n\n const labels = val ? [getLabelForValue(val)] : [];\n const values = val ? [val] : [];\n\n emit('change', { labels, originalEvent, values });\n choice.triggerValidation('blur');\n };\n\n provide(RADIO_GROUP_CTX, {\n color: props.color as Readable<ThemeColor | undefined>,\n disabled: isDisabled,\n name: props.name,\n select: selectRadio,\n size: props.size as Readable<ComponentSize | undefined>,\n value: selectedValue,\n });\n\n // Sync name/color/size/disabled/checked onto slotted ore-radio children.\n watch(() => {\n void slots.elements().value;\n void selectedValue.value;\n\n const radios = getSlottedRadios();\n\n for (const radio of radios) {\n const val = radio.getAttribute('value') ?? '';\n\n radio.toggleAttribute('checked', val === selectedValue.value);\n\n if (props.name.value) radio.setAttribute('name', props.name.value);\n else radio.removeAttribute('name');\n\n if (props.color.value) radio.setAttribute('color', props.color.value);\n else radio.removeAttribute('color');\n\n if (props.size.value) radio.setAttribute('size', props.size.value);\n else radio.removeAttribute('size');\n\n radio.toggleAttribute('disabled', isDisabled.value);\n }\n });\n\n // Roving tabindex: only the selected (or first) radio is tabbable.\n watch(() => {\n void slots.elements().value;\n\n const radios = getSlottedRadios();\n let hasFocusable = false;\n\n for (const radio of radios) {\n const isSelected = radio.getAttribute('value') === selectedValue.value;\n\n radio.setAttribute('tabindex', isSelected && !isDisabled.value ? '0' : '-1');\n\n if (isSelected && !isDisabled.value) hasFocusable = true;\n }\n\n if (!hasFocusable && radios.length > 0) {\n const first = radios.find((r) => !r.hasAttribute('disabled'));\n\n if (first) first.setAttribute('tabindex', '0');\n }\n });\n\n const listControl = createListControl<HTMLElement>({\n getItems: getEnabledRadios,\n keys: { next: ['ArrowDown', 'ArrowRight'], prev: ['ArrowUp', 'ArrowLeft'] },\n loop: true,\n onNavigate: (_action, index, event) => {\n const radio = getEnabledRadios()[index];\n\n if (!radio) return;\n\n radio.focus();\n\n if (radio.tagName === 'ORE-RADIO') {\n selectRadio(radio.getAttribute('value') ?? '', event);\n }\n },\n signal: lifecycleSignal(onCleanup),\n });\n\n bind({\n on: {\n change: (e: Event) => {\n if (e.target === el) return;\n\n e.stopPropagation();\n selectRadio((e.target as HTMLElement).getAttribute('value') ?? '', e);\n },\n keydown: (e: KeyboardEvent) => {\n const radios = getEnabledRadios();\n\n if (!radios.length) return;\n\n const focused = radios.indexOf(document.activeElement as HTMLElement);\n\n if (focused === -1) return;\n\n listControl.set(focused);\n listControl.handleKeydown(e);\n },\n },\n });\n\n const legendId = createStableId('radio-group-legend');\n\n return html`\n <fieldset\n role=\"radiogroup\"\n aria-required=\"${() => String(Boolean(props.required.value))}\"\n aria-invalid=\"${choice.ariaInvalid}\"\n aria-errormessage=\"${choice.ariaErrorMessage}\"\n aria-describedby=\"${choice.ariaDescribedBy}\">\n <legend id=\"${legendId}\" ?hidden=${() => !props.label.value}>\n ${props.label}${when(\n () => Boolean(props.required.value),\n () => html`\n <span aria-hidden=\"true\">*</span>\n `,\n )}\n </legend>\n <div class=\"radio-group-items\" part=\"items\">\n <slot></slot>\n </div>\n <div\n class=\"helper-text\"\n part=\"helper\"\n id=\"${choice.assistiveId}\"\n role=\"${() => (choice.errorText.value ? 'alert' : null)}\"\n aria-live=\"polite\"\n ?hidden=\"${() => !choice.errorText.value && !choice.helperText.value}\">\n ${() => choice.errorText.value || choice.helperText.value}\n </div>\n </fieldset>\n `;\n },\n styles: [disabledStateMixin, componentStyles],\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"radio-group.js","names":[],"sources":["../src/inputs/radio-group/radio-group.ts"],"sourcesContent":["import {\n bind,\n createContext,\n createStableId,\n define,\n getHost,\n html,\n onCleanup,\n prop,\n provide,\n useEmit,\n useField,\n useSlots,\n watchEffect,\n when,\n} from '@vielzeug/ore';\nimport type { Readable } from '@vielzeug/ripple';\nimport {\n type ChoiceChangeDetail,\n createChoiceField,\n createListControl,\n getChoiceLabel,\n getLightChildrenByTag,\n lifecycleSignal,\n} from '../../core';\nimport { disablableBundle, sizableBundle, themableBundle } from '../../shared';\nimport { disabledStateMixin } from '../../styles';\nimport type { ComponentSize, ThemeColor } from '../../types';\nimport componentStyles from './radio-group.css?inline';\n\n/** Radio group component properties */\nexport type OreRadioGroupProps = {\n /** Theme color tint */\n color?: ThemeColor;\n /** Disabled state */\n disabled?: boolean;\n /** Error message text */\n error?: string;\n /** Helper text displayed below the items */\n helper?: string;\n /** Group label text */\n label?: string;\n /** Form field name */\n name?: string;\n /** Layout orientation */\n orientation?: 'horizontal' | 'vertical';\n /** Required field */\n required?: boolean;\n /** Items size preset */\n size?: ComponentSize;\n /** Initial selected value */\n value?: string;\n};\n\nexport type RadioGroupContext = {\n color: Readable<ThemeColor | undefined>;\n disabled: Readable<boolean>;\n name: Readable<string | undefined>;\n select: (value: string, originalEvent?: Event) => void;\n size: Readable<ComponentSize | undefined>;\n value: Readable<string | undefined>;\n};\n\nexport const RADIO_GROUP_CTX = createContext<RadioGroupContext | undefined>('OreRadioGroup');\n\n/** Events emitted by the radio-group component */\nexport type OreRadioGroupEvents = {\n /** Emitted when the selection changes */\n change: ChoiceChangeDetail;\n};\n\n/**\n * A group of radio buttons that allows users to select a single option from a set.\n * Supports keyboard navigation (arrows) and automatic value management.\n *\n * @element ore-radio-group\n *\n * @attr {string} value - Selected value\n * @attr {string} name - Form field name\n * @attr {string} label - Group label\n * @attr {string} orientation - Layout: 'vertical' | 'horizontal'\n * @attr {boolean} required - Required field\n *\n * @fires change - Emitted when a radio is selected. detail: { values: string[], labels: string[], originalEvent?: Event }\n *\n * @slot - Place `ore-radio` elements here\n *\n * @cssprop --radio-group-direction - Flex direction of the items list ('row' | 'column')\n * @cssprop --radio-group-gap - Gap between radio items\n * @part items - Items container.\n * @example\n * ```html\n * <ore-radio-group name=\"plan\" label=\"Choose a plan\" value=\"free\" required>\n * <ore-radio value=\"free\">Free</ore-radio>\n * <ore-radio value=\"pro\">Pro</ore-radio>\n * <ore-radio value=\"enterprise\">Enterprise</ore-radio>\n * </ore-radio-group>\n * <ore-radio-group name=\"direction\" orientation=\"horizontal\" color=\"primary\">\n * <ore-radio value=\"left\">Left</ore-radio>\n * <ore-radio value=\"right\">Right</ore-radio>\n * </ore-radio-group>\n * ```\n */\nexport const RADIO_GROUP_TAG = 'ore-radio-group' as const;\ndefine<OreRadioGroupProps>(RADIO_GROUP_TAG, {\n formAssociated: true,\n props: {\n ...themableBundle,\n ...sizableBundle,\n ...disablableBundle,\n error: prop.string(),\n helper: prop.string(),\n label: prop.string(),\n name: prop.string(),\n orientation: prop.oneOf(['horizontal', 'vertical'] as const, 'vertical'),\n required: prop.bool(false),\n // Not auto-reflected (`reflect: false`) — the derived, interaction-updated selection\n // (`selectedValue`) is the single writer for this attribute, via `bind()` below; letting\n // `prop.string()`'s own default reflection also write the raw incoming value would leave\n // two effects racing to set the same attribute from different sources.\n value: { ...prop.string(), reflect: false },\n },\n setup(props) {\n const el = getHost();\n const emit = useEmit<OreRadioGroupEvents>();\n const slots = useSlots();\n const watch = watchEffect;\n\n const choice = createChoiceField({\n disabled: props.disabled,\n error: props.error,\n helper: props.helper,\n label: props.label,\n prefix: 'radio-group',\n required: props.required,\n signal: lifecycleSignal(onCleanup),\n value: props.value,\n });\n\n choice.attachFormField(\n useField<string>({\n disabled: choice.disabled,\n onReset: choice.reset,\n toFormValue: (v) => v,\n validationMessage: choice.validationMessage,\n validity: choice.validity,\n value: choice.formValue,\n }),\n );\n\n const selectedValue = choice.selectedValue;\n const isDisabled = props.disabled;\n\n bind({\n attr: {\n size: props.size,\n value: () => selectedValue.value || null,\n },\n });\n\n const getSlottedRadios = (): HTMLElement[] => getLightChildrenByTag(el, 'ore-radio');\n\n const getEnabledRadios = (): HTMLElement[] =>\n isDisabled.value ? [] : getSlottedRadios().filter((radio) => !radio.hasAttribute('disabled'));\n\n const getLabelForValue = (value: string): string => getChoiceLabel(getSlottedRadios(), value);\n\n const selectRadio = (val: string, originalEvent?: Event): void => {\n choice.setValues(val ? [val] : []);\n\n const labels = val ? [getLabelForValue(val)] : [];\n const values = val ? [val] : [];\n\n emit('change', { labels, originalEvent, values });\n choice.triggerValidation('blur');\n };\n\n provide(RADIO_GROUP_CTX, {\n color: props.color as Readable<ThemeColor | undefined>,\n disabled: isDisabled,\n name: props.name,\n select: selectRadio,\n size: props.size as Readable<ComponentSize | undefined>,\n value: selectedValue,\n });\n\n // Sync name/color/size/disabled/checked onto slotted ore-radio children.\n watch(() => {\n void slots.elements().value;\n void selectedValue.value;\n\n const radios = getSlottedRadios();\n\n for (const radio of radios) {\n const val = radio.getAttribute('value') ?? '';\n\n radio.toggleAttribute('checked', val === selectedValue.value);\n\n if (props.name.value) radio.setAttribute('name', props.name.value);\n else radio.removeAttribute('name');\n\n if (props.color.value) radio.setAttribute('color', props.color.value);\n else radio.removeAttribute('color');\n\n if (props.size.value) radio.setAttribute('size', props.size.value);\n else radio.removeAttribute('size');\n\n radio.toggleAttribute('disabled', isDisabled.value);\n }\n });\n\n // Roving tabindex: only the selected (or first) radio is tabbable.\n watch(() => {\n void slots.elements().value;\n\n const radios = getSlottedRadios();\n let hasFocusable = false;\n\n for (const radio of radios) {\n const isSelected = radio.getAttribute('value') === selectedValue.value;\n\n radio.setAttribute('tabindex', isSelected && !isDisabled.value ? '0' : '-1');\n\n if (isSelected && !isDisabled.value) hasFocusable = true;\n }\n\n if (!hasFocusable && radios.length > 0) {\n const first = radios.find((r) => !r.hasAttribute('disabled'));\n\n if (first) first.setAttribute('tabindex', '0');\n }\n });\n\n const listControl = createListControl<HTMLElement>({\n getItems: getEnabledRadios,\n keys: { next: ['ArrowDown', 'ArrowRight'], prev: ['ArrowUp', 'ArrowLeft'] },\n loop: true,\n onNavigate: ({ event, item }) => {\n item.focus();\n\n if (item.tagName === 'ORE-RADIO') {\n selectRadio(item.getAttribute('value') ?? '', event);\n }\n },\n signal: lifecycleSignal(onCleanup),\n });\n\n bind({\n on: {\n change: (e: Event) => {\n if (e.target === el) return;\n\n e.stopPropagation();\n selectRadio((e.target as HTMLElement).getAttribute('value') ?? '', e);\n },\n keydown: (e: KeyboardEvent) => {\n const radios = getEnabledRadios();\n\n if (!radios.length) return;\n\n const focused = radios.indexOf(document.activeElement as HTMLElement);\n\n if (focused === -1) return;\n\n listControl.set(focused);\n listControl.handleKeydown(e);\n },\n },\n });\n\n const legendId = createStableId('radio-group-legend');\n\n return html`\n <fieldset\n role=\"radiogroup\"\n aria-required=\"${() => String(Boolean(props.required.value))}\"\n aria-invalid=\"${choice.ariaInvalid}\"\n aria-errormessage=\"${choice.ariaErrorMessage}\"\n aria-describedby=\"${choice.ariaDescribedBy}\">\n <legend id=\"${legendId}\" ?hidden=${() => !props.label.value}>\n ${props.label}${when(\n () => Boolean(props.required.value),\n () => html`\n <span aria-hidden=\"true\">*</span>\n `,\n )}\n </legend>\n <div class=\"radio-group-items\" part=\"items\">\n <slot></slot>\n </div>\n <div\n class=\"helper-text\"\n part=\"helper\"\n id=\"${choice.assistiveId}\"\n role=\"${() => (choice.errorText.value ? 'alert' : null)}\"\n aria-live=\"polite\"\n ?hidden=\"${() => !choice.errorText.value && !choice.helperText.value}\">\n ${() => choice.errorText.value || choice.helperText.value}\n </div>\n </fieldset>\n `;\n },\n styles: [disabledStateMixin, componentStyles],\n});\n"],"mappings":"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"}
|