rei-kit 4.2.0 → 4.4.0

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.
@@ -1 +1 @@
1
- {"version":3,"file":"focus-target-BtcoJdmz.js","names":[],"sources":["../src/utils/sheet-root.ts","../src/composables/use-visual-viewport.ts","../src/composables/use-bound-value.ts","../src/utils/inert.ts","../src/components/BaseSheet.vue","../src/components/BaseSheet.vue","../src/components/BaseCheckbox.vue","../src/components/BaseCheckbox.vue","../src/utils/direction.ts","../src/utils/focus-target.ts"],"sourcesContent":["/** The node every sheet teleports into. */\nexport const SHEET_ROOT_ID = 'sheet-root'\n\n/**\n * Returns the sheet root, creating it if the page has none.\n *\n * `BaseSheet` teleports out of the app's tree so the app behind it can be\n * marked `inert` — a sheet inside the element it is disabling would disable\n * itself. That means it needs a mount point outside `#app`.\n *\n * It used to need the consuming app to put `<div id=\"sheet-root\">` in its\n * `index.html`, and nothing said so. A page without it got a sheet that opened,\n * blocked the page, and rendered nothing: Vue warns to the console about a\n * missing teleport target and carries on, so the build is green, the types are\n * fine, and the screen is wrong. The kit's own showcase had exactly that bug,\n * which is how it was found.\n *\n * So the node is made on demand. An app that already declares one keeps it —\n * this only fills a gap, it never replaces.\n */\nexport function ensureSheetRoot(): string {\n const selector = `#${SHEET_ROOT_ID}`\n\n // Server-rendered: there is no document, and Teleport is skipped anyway.\n if (typeof document === 'undefined') return selector\n if (document.getElementById(SHEET_ROOT_ID)) return selector\n\n const root = document.createElement('div')\n root.id = SHEET_ROOT_ID\n document.body.appendChild(root)\n\n return selector\n}\n","import { onScopeDispose, readonly, ref } from 'vue'\n\n/** The visible area, once the on-screen keyboard has taken its share. */\nexport interface VisualViewportRect {\n height: number\n offsetTop: number\n}\n\n/**\n * Tracks the visual viewport.\n *\n * Chrome and Android browsers honour `interactive-widget=resizes-content`, so\n * the layout viewport already shrinks for the keyboard there. Safari on iOS\n * does not implement it: it shrinks only the *visual* viewport, leaving a sheet\n * sized in `dvh` sitting partly underneath the keyboard.\n *\n * `null` means the API is unavailable, which callers should read as \"trust the\n * layout viewport\" rather than as zero. A server has no viewport at all, so it\n * gets that same `null` — this runs during `setup`, and a component using it\n * has to survive being rendered there.\n *\n * @example\n * ```ts\n * const viewport = useVisualViewport()\n * // :style=\"viewport ? { height: `${viewport.height}px` } : undefined\"\n * ```\n */\nexport function useVisualViewport() {\n const rect = ref<VisualViewportRect | null>(null)\n\n const viewport = typeof window === 'undefined' ? undefined : window.visualViewport\n if (!viewport) return readonly(rect)\n\n function read() {\n if (!viewport) return\n\n rect.value = { height: viewport.height, offsetTop: viewport.offsetTop }\n }\n\n read()\n\n // `scroll` matters as much as `resize`: iOS shifts the visual viewport up to\n // keep the focused field visible, without changing its height.\n viewport.addEventListener('resize', read)\n viewport.addEventListener('scroll', read)\n\n onScopeDispose(() => {\n viewport.removeEventListener('resize', read)\n viewport.removeEventListener('scroll', read)\n })\n\n return readonly(rect)\n}\n","import { computed, ref, watch } from 'vue'\nimport type { Ref, WritableComputedRef } from 'vue'\n\n/**\n * A `v-model` that accepts `undefined` and emits only what a field produces.\n *\n * `defineModel` could not be typed both ways. Without a default it declares\n * that it may emit `undefined`, so under vue-tsc's strictTemplates a\n * `ref('')` cannot be bound to it. With a default it drops `undefined` from\n * what it accepts, so under `exactOptionalPropertyTypes` a form library's\n * `string | undefined` cannot be. Hibi binds exactly that, and 2.4.0 as first\n * written broke it — the consumer check caught it before the tag.\n *\n * So the prop and the event are declared by hand — the prop as\n * `T | undefined`, the event as `T` — and this is the rest of what\n * `defineModel` did: follow the prop, and keep a value of its own when\n * nothing is bound. Vue applies `.trim` and `.number` to the event itself, so\n * modifiers behave as they did.\n *\n * Internal: not exported from the package.\n */\nexport function useBoundValue<T>(\n read: () => T | undefined,\n write: (value: T) => void,\n): WritableComputedRef<T | undefined> {\n const local = ref(read()) as Ref<T | undefined>\n\n watch(read, (next) => {\n local.value = next\n })\n\n return computed({\n get: () => local.value,\n set: (next) => {\n local.value = next\n write(next as T)\n },\n })\n}\n","/**\n * How many open layers hold each element inert. An element set inert by the\n * app itself is never in here, so releasing never undoes the app's own choice.\n */\nconst holds = new Map<Element, number>()\n\n/**\n * Takes everything on the page except the branch holding `keep` out of tab\n * order and pointer events, and returns the function that gives it back.\n *\n * `inert` is a real focus trap without keydown bookkeeping, and the only one\n * that also stops a screen reader's virtual cursor. It used to be applied to\n * `document.getElementById('app')` — the id Vite's template happens to use —\n * so an app mounted on `#root` got a dialog whose background was still\n * reachable with Tab, and nothing said so. Working from `<body>`'s children\n * instead holds whatever the app is mounted on, and anything else teleported\n * there too.\n *\n * Counted per element, so a sheet opened over a sheet, or a tour over either,\n * does not hand the page back when the inner one closes.\n */\nexport function inertOutside(keep: Element): () => void {\n if (typeof document === 'undefined') return () => {}\n\n const taken: Element[] = []\n\n for (const element of Array.from(document.body.children)) {\n if (element.contains(keep)) continue\n\n const count = holds.get(element)\n if (count === undefined) {\n if (element.hasAttribute('inert')) continue\n element.setAttribute('inert', '')\n }\n holds.set(element, (count ?? 0) + 1)\n taken.push(element)\n }\n\n let released = false\n\n return () => {\n if (released) return\n released = true\n\n for (const element of taken) {\n const count = (holds.get(element) ?? 1) - 1\n if (count > 0) {\n holds.set(element, count)\n continue\n }\n holds.delete(element)\n element.removeAttribute('inert')\n }\n }\n}\n","<script setup lang=\"ts\">\nimport BaseButton from './BaseButton.vue'\nimport { computed, nextTick, onUnmounted, ref, watch } from 'vue'\nimport { X } from 'lucide-vue-next'\n\nimport { useVisualViewport } from '../composables/use-visual-viewport'\nimport { inertOutside } from '../utils/inert'\nimport { ensureSheetRoot } from '../utils/sheet-root'\n\nconst open = defineModel<boolean>({ required: true })\nconst {\n title,\n subtitle = '',\n closeLabel = 'Close',\n} = defineProps<{\n title: string\n subtitle?: string | undefined\n /**\n * Accessible name for the close button.\n *\n * A prop rather than a translation: a component that calls t() forces every\n * consumer onto one i18n setup, and this is the package's only visible string.\n */\n closeLabel?: string | undefined\n}>()\n\n/* Resolved in setup, before the first render, so the Teleport always has a\n target to find. */\nconst sheetRoot = ensureSheetRoot()\n\nconst viewport = useVisualViewport()\n\n/**\n * Pins the sheet to the area the keyboard has left visible.\n *\n * Only needed where the layout viewport does not shrink on its own — iOS. On\n * Android the numbers already agree, so this is a no-op there rather than a\n * second, competing adjustment.\n */\nconst viewportStyle = computed(() =>\n viewport.value\n ? { height: `${viewport.value.height}px`, top: `${viewport.value.offsetTop}px` }\n : undefined,\n)\n\nconst panel = ref<HTMLElement | null>(null)\nlet lastFocused: HTMLElement | null = null\n\nfunction close() {\n open.value = false\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n if (event.key === 'Escape') close()\n}\n\nwatch(open, async (isOpen) => {\n if (isOpen) {\n setBackgroundInert(true)\n lastFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null\n window.addEventListener('keydown', onKeydown)\n await nextTick()\n panel.value?.focus()\n } else {\n window.removeEventListener('keydown', onKeydown)\n // Released before focus is handed back: an inert element cannot take\n // focus, so the other order quietly dropped it on the body.\n setBackgroundInert(false)\n lastFocused?.focus()\n lastFocused = null\n }\n})\n\n/* The whole page but the sheet root goes inert while this is open. Held per\n sheet, so the unmount below can only release what this sheet took. */\nlet releaseInert: (() => void) | null = null\n\nfunction setBackgroundInert(isInert: boolean) {\n releaseInert?.()\n releaseInert = null\n\n const root = isInert ? document.querySelector(sheetRoot) : null\n if (root) releaseInert = inertOutside(root)\n}\n\nonUnmounted(() => {\n window.removeEventListener('keydown', onKeydown)\n // Unmounting while open would otherwise leave the whole app inert forever.\n setBackgroundInert(false)\n})\n</script>\n\n<template>\n <Teleport :to=\"sheetRoot\">\n <Transition name=\"sheet\">\n <div\n v-if=\"open\"\n class=\"fixed inset-x-0 top-0 bottom-0 z-50 flex items-center justify-center\"\n :style=\"viewportStyle\"\n >\n <div\n class=\"shell-frame md:rounded-shell relative flex max-h-full flex-col justify-end overflow-hidden\"\n >\n <div class=\"bg-ink/45 absolute inset-0 backdrop-blur-[2px]\" @click=\"close\" />\n\n <!-- Header and footer stay put; only the slot scrolls. Sized in dvh so\n the on-screen keyboard shrinks the sheet instead of pushing its\n content out of reach. -->\n <section\n ref=\"panel\"\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-label=\"title\"\n tabindex=\"-1\"\n class=\"sheet-panel surface-overlay relative flex max-h-[94%] min-h-[56dvh] flex-col rounded-t-[calc(var(--radius-shell)-2px)] outline-none\"\n >\n <div class=\"flex shrink-0 justify-center pt-3\" aria-hidden=\"true\">\n <span class=\"bg-hair h-1.5 w-10 rounded-full\" />\n </div>\n\n <header class=\"flex shrink-0 items-start gap-3 px-6 pt-4 pb-5\">\n <div class=\"min-w-0 flex-1\">\n <h2 class=\"text-ink text-xl leading-tight font-semibold\">{{ title }}</h2>\n <p v-if=\"subtitle\" class=\"text-ink-soft mt-1 text-sm leading-snug\">\n {{ subtitle }}\n </p>\n </div>\n\n <!-- `unstyled`, so the sheet keeps the exact button it had. What it\n gains is the focus ring it never had: this was a raw\n `<button>` with no `focus-visible` rule, so closing a sheet\n from the keyboard was invisible. -->\n <BaseButton\n variant=\"unstyled\"\n class=\"text-ink-soft hover:bg-muted hover:text-ink -mt-1 flex size-10 shrink-0 items-center justify-center rounded-full transition-colors active:scale-90\"\n :aria-label=\"closeLabel\"\n @click=\"close\"\n >\n <X class=\"size-5\" />\n </BaseButton>\n </header>\n\n <div\n class=\"min-h-0 flex-1 overflow-y-auto px-6 pb-[calc(2rem+env(safe-area-inset-bottom,0px))]\"\n >\n <slot />\n </div>\n </section>\n </div>\n </div>\n </Transition>\n </Teleport>\n</template>\n\n<style scoped>\n.sheet-enter-active,\n.sheet-leave-active {\n transition: opacity var(--duration-base) ease;\n}\n.sheet-enter-from,\n.sheet-leave-to {\n opacity: 0;\n}\n\n/* The panel travels further than the scrim fades, which is what makes the\n sheet read as rising rather than appearing. */\n.sheet-enter-active .sheet-panel,\n.sheet-leave-active .sheet-panel {\n transition: transform var(--duration-slow) var(--ease-sheet);\n}\n.sheet-enter-from .sheet-panel,\n.sheet-leave-to .sheet-panel {\n transform: translateY(6%);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .sheet-enter-from .sheet-panel,\n .sheet-leave-to .sheet-panel {\n transform: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport BaseButton from './BaseButton.vue'\nimport { computed, nextTick, onUnmounted, ref, watch } from 'vue'\nimport { X } from 'lucide-vue-next'\n\nimport { useVisualViewport } from '../composables/use-visual-viewport'\nimport { inertOutside } from '../utils/inert'\nimport { ensureSheetRoot } from '../utils/sheet-root'\n\nconst open = defineModel<boolean>({ required: true })\nconst {\n title,\n subtitle = '',\n closeLabel = 'Close',\n} = defineProps<{\n title: string\n subtitle?: string | undefined\n /**\n * Accessible name for the close button.\n *\n * A prop rather than a translation: a component that calls t() forces every\n * consumer onto one i18n setup, and this is the package's only visible string.\n */\n closeLabel?: string | undefined\n}>()\n\n/* Resolved in setup, before the first render, so the Teleport always has a\n target to find. */\nconst sheetRoot = ensureSheetRoot()\n\nconst viewport = useVisualViewport()\n\n/**\n * Pins the sheet to the area the keyboard has left visible.\n *\n * Only needed where the layout viewport does not shrink on its own — iOS. On\n * Android the numbers already agree, so this is a no-op there rather than a\n * second, competing adjustment.\n */\nconst viewportStyle = computed(() =>\n viewport.value\n ? { height: `${viewport.value.height}px`, top: `${viewport.value.offsetTop}px` }\n : undefined,\n)\n\nconst panel = ref<HTMLElement | null>(null)\nlet lastFocused: HTMLElement | null = null\n\nfunction close() {\n open.value = false\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n if (event.key === 'Escape') close()\n}\n\nwatch(open, async (isOpen) => {\n if (isOpen) {\n setBackgroundInert(true)\n lastFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null\n window.addEventListener('keydown', onKeydown)\n await nextTick()\n panel.value?.focus()\n } else {\n window.removeEventListener('keydown', onKeydown)\n // Released before focus is handed back: an inert element cannot take\n // focus, so the other order quietly dropped it on the body.\n setBackgroundInert(false)\n lastFocused?.focus()\n lastFocused = null\n }\n})\n\n/* The whole page but the sheet root goes inert while this is open. Held per\n sheet, so the unmount below can only release what this sheet took. */\nlet releaseInert: (() => void) | null = null\n\nfunction setBackgroundInert(isInert: boolean) {\n releaseInert?.()\n releaseInert = null\n\n const root = isInert ? document.querySelector(sheetRoot) : null\n if (root) releaseInert = inertOutside(root)\n}\n\nonUnmounted(() => {\n window.removeEventListener('keydown', onKeydown)\n // Unmounting while open would otherwise leave the whole app inert forever.\n setBackgroundInert(false)\n})\n</script>\n\n<template>\n <Teleport :to=\"sheetRoot\">\n <Transition name=\"sheet\">\n <div\n v-if=\"open\"\n class=\"fixed inset-x-0 top-0 bottom-0 z-50 flex items-center justify-center\"\n :style=\"viewportStyle\"\n >\n <div\n class=\"shell-frame md:rounded-shell relative flex max-h-full flex-col justify-end overflow-hidden\"\n >\n <div class=\"bg-ink/45 absolute inset-0 backdrop-blur-[2px]\" @click=\"close\" />\n\n <!-- Header and footer stay put; only the slot scrolls. Sized in dvh so\n the on-screen keyboard shrinks the sheet instead of pushing its\n content out of reach. -->\n <section\n ref=\"panel\"\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-label=\"title\"\n tabindex=\"-1\"\n class=\"sheet-panel surface-overlay relative flex max-h-[94%] min-h-[56dvh] flex-col rounded-t-[calc(var(--radius-shell)-2px)] outline-none\"\n >\n <div class=\"flex shrink-0 justify-center pt-3\" aria-hidden=\"true\">\n <span class=\"bg-hair h-1.5 w-10 rounded-full\" />\n </div>\n\n <header class=\"flex shrink-0 items-start gap-3 px-6 pt-4 pb-5\">\n <div class=\"min-w-0 flex-1\">\n <h2 class=\"text-ink text-xl leading-tight font-semibold\">{{ title }}</h2>\n <p v-if=\"subtitle\" class=\"text-ink-soft mt-1 text-sm leading-snug\">\n {{ subtitle }}\n </p>\n </div>\n\n <!-- `unstyled`, so the sheet keeps the exact button it had. What it\n gains is the focus ring it never had: this was a raw\n `<button>` with no `focus-visible` rule, so closing a sheet\n from the keyboard was invisible. -->\n <BaseButton\n variant=\"unstyled\"\n class=\"text-ink-soft hover:bg-muted hover:text-ink -mt-1 flex size-10 shrink-0 items-center justify-center rounded-full transition-colors active:scale-90\"\n :aria-label=\"closeLabel\"\n @click=\"close\"\n >\n <X class=\"size-5\" />\n </BaseButton>\n </header>\n\n <div\n class=\"min-h-0 flex-1 overflow-y-auto px-6 pb-[calc(2rem+env(safe-area-inset-bottom,0px))]\"\n >\n <slot />\n </div>\n </section>\n </div>\n </div>\n </Transition>\n </Teleport>\n</template>\n\n<style scoped>\n.sheet-enter-active,\n.sheet-leave-active {\n transition: opacity var(--duration-base) ease;\n}\n.sheet-enter-from,\n.sheet-leave-to {\n opacity: 0;\n}\n\n/* The panel travels further than the scrim fades, which is what makes the\n sheet read as rising rather than appearing. */\n.sheet-enter-active .sheet-panel,\n.sheet-leave-active .sheet-panel {\n transition: transform var(--duration-slow) var(--ease-sheet);\n}\n.sheet-enter-from .sheet-panel,\n.sheet-leave-to .sheet-panel {\n transform: translateY(6%);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .sheet-enter-from .sheet-panel,\n .sheet-leave-to .sheet-panel {\n transform: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, useId } from 'vue'\n\n/**\n * A single checkbox, with its label beside it.\n *\n * Deliberately not built on `FormField`. That component stacks a label above\n * its control, which is right for every field where the control is a box you\n * type into and wrong here: a checkbox is read as one sentence with a mark in\n * front of it, and putting the words above the box breaks the association a\n * sighted reader makes before they get to the accessible name.\n *\n * The whole row is the label, so the words are part of the hit target. On a\n * phone that is the difference between a control and a coin toss.\n */\nconst {\n label,\n error = '',\n hint = '',\n disabled = false,\n size = 'md',\n indeterminate = false,\n labelHidden = false,\n} = defineProps<{\n label: string\n error?: string | undefined\n hint?: string | undefined\n disabled?: boolean | undefined\n /**\n * `md` is a setting: a line the reader came here to change, in ink.\n * `sm` is an aside — \"remember me\" under a sign-in form, \"show the ones I\n * have learned\" above a list — quieter and tighter.\n *\n * The two are not a guess. Of the five hand-written checkboxes across the\n * three consuming apps, four were the aside and one was the setting, and\n * they differed in exactly these two ways.\n */\n size?: 'sm' | 'md' | undefined\n /**\n * Neither checked nor unchecked: some of what this box stands for is.\n *\n * It is a DOM property and not an attribute, so it cannot be written in\n * markup and there is no `indeterminate=\"\"` to set — which is why a\n * \"select all\" box on top of a list is the one checkbox every app ends up\n * writing by hand. The kit's own `DataTable` did, for exactly this\n * reason.\n *\n * It is presentation only: the value underneath is still `true` or\n * `false`, and pressing the box resolves it.\n */\n indeterminate?: boolean | undefined\n /**\n * Hides the words but keeps them as the accessible name. For a box in a\n * table header or a list row whose surroundings already say what it is —\n * dropping the label would leave the control named nothing at all.\n */\n labelHidden?: boolean | undefined\n}>()\n\nconst model = defineModel<boolean>({ default: false })\n\nconst id = useId()\nconst errorId = `${id}-error`\nconst hintId = `${id}-hint`\n\nconst describedBy = computed(() => {\n if (error) return errorId\n if (hint) return hintId\n return undefined\n})\n</script>\n\n<template>\n <div class=\"flex flex-col gap-1.5\">\n <label\n :for=\"id\"\n class=\"flex items-center\"\n :class=\"[\n labelHidden ? '' : size === 'sm' ? 'gap-2' : 'gap-3',\n disabled ? 'opacity-50' : 'cursor-pointer',\n ]\"\n >\n <input\n :id=\"id\"\n v-model=\"model\"\n type=\"checkbox\"\n :disabled=\"disabled\"\n :indeterminate=\"indeterminate\"\n :aria-invalid=\"Boolean(error)\"\n :aria-describedby=\"describedBy\"\n class=\"accent-primary focus-visible:outline-primary size-4 shrink-0 focus-visible:outline-2 focus-visible:outline-offset-2\"\n />\n <span\n class=\"text-sm\"\n :class=\"[labelHidden ? 'sr-only' : '', size === 'sm' ? 'text-ink-soft' : 'text-ink']\"\n >\n {{ label }}\n </span>\n </label>\n\n <p v-if=\"error\" :id=\"errorId\" class=\"text-negative text-xs\">{{ error }}</p>\n <p v-else-if=\"hint\" :id=\"hintId\" class=\"text-ink-soft text-xs\">{{ hint }}</p>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, useId } from 'vue'\n\n/**\n * A single checkbox, with its label beside it.\n *\n * Deliberately not built on `FormField`. That component stacks a label above\n * its control, which is right for every field where the control is a box you\n * type into and wrong here: a checkbox is read as one sentence with a mark in\n * front of it, and putting the words above the box breaks the association a\n * sighted reader makes before they get to the accessible name.\n *\n * The whole row is the label, so the words are part of the hit target. On a\n * phone that is the difference between a control and a coin toss.\n */\nconst {\n label,\n error = '',\n hint = '',\n disabled = false,\n size = 'md',\n indeterminate = false,\n labelHidden = false,\n} = defineProps<{\n label: string\n error?: string | undefined\n hint?: string | undefined\n disabled?: boolean | undefined\n /**\n * `md` is a setting: a line the reader came here to change, in ink.\n * `sm` is an aside — \"remember me\" under a sign-in form, \"show the ones I\n * have learned\" above a list — quieter and tighter.\n *\n * The two are not a guess. Of the five hand-written checkboxes across the\n * three consuming apps, four were the aside and one was the setting, and\n * they differed in exactly these two ways.\n */\n size?: 'sm' | 'md' | undefined\n /**\n * Neither checked nor unchecked: some of what this box stands for is.\n *\n * It is a DOM property and not an attribute, so it cannot be written in\n * markup and there is no `indeterminate=\"\"` to set — which is why a\n * \"select all\" box on top of a list is the one checkbox every app ends up\n * writing by hand. The kit's own `DataTable` did, for exactly this\n * reason.\n *\n * It is presentation only: the value underneath is still `true` or\n * `false`, and pressing the box resolves it.\n */\n indeterminate?: boolean | undefined\n /**\n * Hides the words but keeps them as the accessible name. For a box in a\n * table header or a list row whose surroundings already say what it is —\n * dropping the label would leave the control named nothing at all.\n */\n labelHidden?: boolean | undefined\n}>()\n\nconst model = defineModel<boolean>({ default: false })\n\nconst id = useId()\nconst errorId = `${id}-error`\nconst hintId = `${id}-hint`\n\nconst describedBy = computed(() => {\n if (error) return errorId\n if (hint) return hintId\n return undefined\n})\n</script>\n\n<template>\n <div class=\"flex flex-col gap-1.5\">\n <label\n :for=\"id\"\n class=\"flex items-center\"\n :class=\"[\n labelHidden ? '' : size === 'sm' ? 'gap-2' : 'gap-3',\n disabled ? 'opacity-50' : 'cursor-pointer',\n ]\"\n >\n <input\n :id=\"id\"\n v-model=\"model\"\n type=\"checkbox\"\n :disabled=\"disabled\"\n :indeterminate=\"indeterminate\"\n :aria-invalid=\"Boolean(error)\"\n :aria-describedby=\"describedBy\"\n class=\"accent-primary focus-visible:outline-primary size-4 shrink-0 focus-visible:outline-2 focus-visible:outline-offset-2\"\n />\n <span\n class=\"text-sm\"\n :class=\"[labelHidden ? 'sr-only' : '', size === 'sm' ? 'text-ink-soft' : 'text-ink']\"\n >\n {{ label }}\n </span>\n </label>\n\n <p v-if=\"error\" :id=\"errorId\" class=\"text-negative text-xs\">{{ error }}</p>\n <p v-else-if=\"hint\" :id=\"hintId\" class=\"text-ink-soft text-xs\">{{ hint }}</p>\n </div>\n</template>\n","/** Which way a language is written. */\nexport type TextDirection = 'ltr' | 'rtl'\n\n/*\n * The language subtags written right to left.\n *\n * A list rather than `Intl.Locale.prototype.getTextInfo()`, which is recent,\n * was a property before it was a method, and is missing or differently\n * spelled depending on the engine. Feature-detecting three spellings to\n * answer a question with twelve possible answers is more code and more ways\n * to be wrong than the list, and the list does not change: a language does\n * not switch direction.\n */\nconst RTL = new Set([\n 'ar', // Arabic\n 'arc', // Aramaic\n 'ckb', // Central Kurdish\n 'dv', // Divehi\n 'fa', // Persian\n 'he', // Hebrew\n 'ks', // Kashmiri\n 'ku', // Kurdish\n 'nqo', // N'Ko\n 'ps', // Pashto\n 'sd', // Sindhi\n 'syr', // Syriac\n 'ug', // Uyghur\n 'ur', // Urdu\n 'yi', // Yiddish\n])\n\n/**\n * Which way a locale tag is written.\n *\n * The kit is built on logical properties and `start`/`end` props all the\n * way through — `BaseDrawer`'s side, every inset, every margin — and none\n * of that does anything at all until `dir` is set on the document. So the\n * direction has to come from somewhere, and a locale tag is the only thing\n * that knows it.\n *\n * Only the language subtag is read, so `ar-EG` and `ar` answer the same.\n *\n * @example\n * ```ts\n * textDirection('he-IL') // 'rtl'\n * textDirection('tr') // 'ltr'\n * ```\n */\nexport function textDirection(locale: string): TextDirection {\n // `und-Arab` and the like are a real spelling, so the script subtag is\n // worth the two lines it costs.\n const parts = locale.toLowerCase().split(/[-_]/)\n const script = parts[1]\n\n if (script === 'arab' || script === 'hebr' || script === 'thaa' || script === 'syrc') return 'rtl'\n\n return RTL.has(parts[0] ?? '') ? 'rtl' : 'ltr'\n}\n\n/**\n * Which way the text around an element runs.\n *\n * The companion to `textDirection`, and the other end of it: that one asks\n * a locale, this one asks the page. An element inside a quoted passage may\n * run the other way from the document it is in, so the nearest `dir`\n * attribute wins and the document is the fallback.\n *\n * The attribute rather than `getComputedStyle().direction`: the computed\n * value is the real answer, but it is inherited through the cascade, and\n * jsdom does not inherit it — so the tests that exist to prove the\n * right-to-left behaviour would all pass with the behaviour removed. What\n * actually sets the direction in this kit is the attribute, in one place,\n * on `<html>`, written by `createI18nRuntime`.\n */\nexport function elementDirection(from: Element | null | undefined): TextDirection {\n const declared = from?.closest('[dir]')?.getAttribute('dir') ?? document.dir\n\n return declared === 'rtl' ? 'rtl' : 'ltr'\n}\n\n/**\n * `ArrowLeft` and `ArrowRight` as a direction of travel: `-1` back through\n * a list, `1` on through it, `0` for any other key.\n *\n * They are mirrored where the writing is, which is not a nicety — it is the\n * whole of what the arrows mean. A row of tabs in Arabic runs right to\n * left, so the tab to the *left* of the current one is the next one, and a\n * handler that reads `ArrowRight` as \"forward\" walks backwards through\n * every roving-tabindex control in the kit at once. WAI-ARIA says so\n * explicitly, and it is invisible to every check: the code type-checks, the\n * styles are all logical, and the keys are the same keys.\n *\n * @example\n * ```ts\n * function onKeydown(event: KeyboardEvent) {\n * const step = horizontalStep(event.key, event.currentTarget as Element)\n * if (step === 0) return\n * event.preventDefault()\n * moveTo(index + step)\n * }\n * ```\n */\nexport function horizontalStep(key: string, from: Element | null | undefined): -1 | 0 | 1 {\n if (key !== 'ArrowLeft' && key !== 'ArrowRight') return 0\n\n const forward = elementDirection(from) === 'rtl' ? 'ArrowLeft' : 'ArrowRight'\n\n return key === forward ? 1 : -1\n}\n","/**\n * Moves focus to an element that was never meant to take it.\n *\n * A `<main>`, a heading, the top of a view: none of them are focusable, and\n * both places the kit needs this — `SkipLink` and `createRouteAnnouncer` —\n * fail in the same invisible way without it. A skip link scrolls the page\n * and leaves focus in the header; a route change swaps the content and\n * leaves focus on the link that is no longer there.\n *\n * `tabindex=\"-1\"` makes the element focusable by script and never by Tab,\n * which is the whole trick. It is left behind rather than cleaned up: a\n * reader that moves out and comes back would otherwise find an unfocusable\n * target the second time.\n *\n * @param target - The element, or the `id` of one. A missing one is a no-op.\n * @returns Whether focus moved, so a caller can fall back.\n */\nexport function focusTarget(target: Element | string | null): boolean {\n const element = typeof target === 'string' ? document.getElementById(target) : target\n if (!(element instanceof HTMLElement)) return false\n\n if (!element.hasAttribute('tabindex')) element.setAttribute('tabindex', '-1')\n\n // `focus()` scrolls it into view on its own, which is why there is no\n // `scrollIntoView` beside it.\n element.focus()\n\n return true\n}\n"],"mappings":";;;;;;AACA,IAAa,gBAAgB;;;;;;;;;;;;;;;;;;AAmB7B,SAAgB,kBAA0B;CACxC,MAAM,WAAW,IAAI;CAGrB,IAAI,OAAO,aAAa,aAAa,OAAO;CAC5C,IAAI,SAAS,eAAA,YAA4B,GAAG,OAAO;CAEnD,MAAM,OAAO,SAAS,cAAc,KAAK;CACzC,KAAK,KAAK;CACV,SAAS,KAAK,YAAY,IAAI;CAE9B,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;ACLA,SAAgB,oBAAoB;CAClC,MAAM,OAAO,IAA+B,IAAI;CAEhD,MAAM,WAAW,OAAO,WAAW,cAAc,KAAA,IAAY,OAAO;CACpE,IAAI,CAAC,UAAU,OAAO,SAAS,IAAI;CAEnC,SAAS,OAAO;EACd,IAAI,CAAC,UAAU;EAEf,KAAK,QAAQ;GAAE,QAAQ,SAAS;GAAQ,WAAW,SAAS;EAAU;CACxE;CAEA,KAAK;CAIL,SAAS,iBAAiB,UAAU,IAAI;CACxC,SAAS,iBAAiB,UAAU,IAAI;CAExC,qBAAqB;EACnB,SAAS,oBAAoB,UAAU,IAAI;EAC3C,SAAS,oBAAoB,UAAU,IAAI;CAC7C,CAAC;CAED,OAAO,SAAS,IAAI;AACtB;;;;;;;;;;;;;;;;;;;;;AC/BA,SAAgB,cACd,MACA,OACoC;CACpC,MAAM,QAAQ,IAAI,KAAK,CAAC;CAExB,MAAM,OAAO,SAAS;EACpB,MAAM,QAAQ;CAChB,CAAC;CAED,OAAO,SAAS;EACd,WAAW,MAAM;EACjB,MAAM,SAAS;GACb,MAAM,QAAQ;GACd,MAAM,IAAS;EACjB;CACF,CAAC;AACH;;;;;;;AClCA,IAAM,wBAAQ,IAAI,IAAqB;;;;;;;;;;;;;;;;AAiBvC,SAAgB,aAAa,MAA2B;CACtD,IAAI,OAAO,aAAa,aAAa,aAAa,CAAC;CAEnD,MAAM,QAAmB,CAAC;CAE1B,KAAK,MAAM,WAAW,MAAM,KAAK,SAAS,KAAK,QAAQ,GAAG;EACxD,IAAI,QAAQ,SAAS,IAAI,GAAG;EAE5B,MAAM,QAAQ,MAAM,IAAI,OAAO;EAC/B,IAAI,UAAU,KAAA,GAAW;GACvB,IAAI,QAAQ,aAAa,OAAO,GAAG;GACnC,QAAQ,aAAa,SAAS,EAAE;EAClC;EACA,MAAM,IAAI,UAAU,SAAS,KAAK,CAAC;EACnC,MAAM,KAAK,OAAO;CACpB;CAEA,IAAI,WAAW;CAEf,aAAa;EACX,IAAI,UAAU;EACd,WAAW;EAEX,KAAK,MAAM,WAAW,OAAO;GAC3B,MAAM,SAAS,MAAM,IAAI,OAAO,KAAK,KAAK;GAC1C,IAAI,QAAQ,GAAG;IACb,MAAM,IAAI,SAAS,KAAK;IACxB;GACF;GACA,MAAM,OAAO,OAAO;GACpB,QAAQ,gBAAgB,OAAO;EACjC;CACF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EC7CA,MAAM,OAAO,SAAoB,SAAA,YAAmB;EAmBpD,MAAM,YAAY,gBAAgB;EAElC,MAAM,WAAW,kBAAkB;;;;;;;;EASnC,MAAM,gBAAgB,eACpB,SAAS,QACL;GAAE,QAAQ,GAAG,SAAS,MAAM,OAAO;GAAK,KAAK,GAAG,SAAS,MAAM,UAAU;EAAI,IAC7E,KAAA,CACN;EAEA,MAAM,QAAQ,IAAwB,IAAI;EAC1C,IAAI,cAAkC;EAEtC,SAAS,QAAQ;GACf,KAAK,QAAQ;EACf;EAEA,SAAS,UAAU,OAAsB;GACvC,IAAI,MAAM,QAAQ,UAAU,MAAM;EACpC;EAEA,MAAM,MAAM,OAAO,WAAW;GAC5B,IAAI,QAAQ;IACV,mBAAmB,IAAI;IACvB,cAAc,SAAS,yBAAyB,cAAc,SAAS,gBAAgB;IACvF,OAAO,iBAAiB,WAAW,SAAS;IAC5C,MAAM,SAAS;IACf,MAAM,OAAO,MAAM;GACrB,OAAO;IACL,OAAO,oBAAoB,WAAW,SAAS;IAG/C,mBAAmB,KAAK;IACxB,aAAa,MAAM;IACnB,cAAc;GAChB;EACF,CAAC;EAID,IAAI,eAAoC;EAExC,SAAS,mBAAmB,SAAkB;GAC5C,eAAe;GACf,eAAe;GAEf,MAAM,OAAO,UAAU,SAAS,cAAc,SAAS,IAAI;GAC3D,IAAI,MAAM,eAAe,aAAa,IAAI;EAC5C;EAEA,kBAAkB;GAChB,OAAO,oBAAoB,WAAW,SAAS;GAE/C,mBAAmB,KAAK;EAC1B,CAAC;;GAIC,OAAA,UAAA,GAAA,YA0DW,UAAA,EA1DA,IAAI,MAAA,SAAA,EAAS,GAAA,CACtB,YAwDa,YAAA,EAxDD,MAAK,QAAO,GAAA;IACtB,SAAA,cAsDM,CArDE,KAAA,SADR,UAAA,GAAA,mBAsDM,OAAA;;KApDJ,OAAM;KACL,OAAK,eAAE,cAAA,KAAa;IAErB,GAAA,CAAA,mBAgDM,OAhDN,cAgDM,CA7CJ,mBAA6E,OAAA;KAAxE,OAAM;KAAkD,SAAO;IAKpE,CAAA,GAAA,mBAuCU,WAAA;KAtCJ,SAAA;KAAJ,KAAI;KACJ,MAAK;KACL,cAAW;KACV,cAAY,QAAA;KACb,UAAS;KACT,OAAM;;KAEN,OAAA,OAAA,OAAA,KAAA,mBAEM,OAAA;MAFD,OAAM;MAAoC,eAAY;KACzD,GAAA,CAAA,mBAAgD,QAAA,EAA1C,OAAM,kCAAiC,CAAA,CAAA,GAAA,EAAA;KAG/C,mBAoBS,UApBT,cAoBS,CAnBP,mBAKM,OALN,YAKM,CAJJ,mBAAyE,MAAzE,YAAyE,gBAAb,QAAA,KAAK,GAAA,CAAA,GACxD,QAAA,YAAT,UAAA,GAAA,mBAEI,KAFJ,YAEI,gBADC,QAAA,QAAQ,GAAA,CAAA,KAAA,mBAAA,IAAA,IAAA,CAAA,CAAA,GAQf,YAOa,oBAAA;MANX,SAAQ;MACR,OAAM;MACL,cAAY,QAAA;MACZ,SAAO;;MAER,SAAA,cAAoB,CAApB,YAAoB,MAAA,CAAA,GAAA,EAAjB,OAAM,SAAQ,CAAA,CAAA,CAAA;;;KAIrB,mBAIM,OAJN,YAIM,CADJ,WAAQ,KAAA,QAAA,WAAA,CAAA,GAAA,KAAA,GAAA,IAAA,CAAA,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EEtFtB,MAAM,QAAQ,SAAoB,SAAA,YAAmB;EAErD,MAAM,KAAK,MAAM;EACjB,MAAM,UAAU,GAAG,GAAG;EACtB,MAAM,SAAS,GAAG,GAAG;EAErB,MAAM,cAAc,eAAe;GACjC,IAAI,QAAA,OAAO,OAAO;GAClB,IAAI,QAAA,MAAM,OAAO;EAEnB,CAAC;;GAIC,OAAA,UAAA,GAAA,mBA6BM,OA7BN,YA6BM,CA5BJ,mBAwBQ,SAAA;IAvBL,KAAK,MAAA,EAAA;IACN,OAAK,eAAA,CAAC,qBAAmB,CACP,QAAA,cAAW,KAAQ,QAAA,SAAI,OAAA,UAAA,SAAuC,QAAA,WAAQ,eAAA,gBAAA,CAAA,CAAA;GAKxF,GAAA,CAAA,eAAA,mBASE,SAAA;IARC,IAAI,MAAA,EAAA;IACI,uBAAA,OAAA,OAAA,OAAA,MAAA,WAAA,MAAK,QAAA;IACd,MAAK;IACJ,UAAU,QAAA;IACV,eAAe,QAAA;IACf,gBAAc,QAAQ,QAAA,KAAK;IAC3B,oBAAkB,YAAA;IACnB,OAAM;GANG,GAAA,MAAA,GAAA,UAAA,GAAA,CAAA,CAAA,gBAAA,MAAA,KAAK,CAAA,CAAA,GAQhB,mBAKO,QAAA,EAJL,OAAK,eAAA,CAAC,WAAS,CACN,QAAA,cAAW,YAAA,IAAmB,QAAA,SAAI,OAAA,kBAAA,UAAA,CAAA,CAAA,EAExC,GAAA,gBAAA,QAAA,KAAK,GAAA,CAAA,CAAA,GAAA,IAAA,UAAA,GAIH,QAAA,SAAT,UAAA,GAAA,mBAA2E,KAAA;;IAA1D,IAAI;IAAS,OAAM;GAA2B,GAAA,gBAAA,QAAA,KAAK,GAAA,CAAA,KACtD,QAAA,QAAd,UAAA,GAAA,mBAA6E,KAAA;;IAAxD,IAAI;IAAQ,OAAM;GAA2B,GAAA,gBAAA,QAAA,IAAI,GAAA,CAAA,KAAA,mBAAA,IAAA,IAAA,CAAA,CAAA;;;;;;AExF1E,IAAM,sBAAM,IAAI,IAAI;CAClB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;;;;;;;;;;;;;;AAmBD,SAAgB,cAAc,QAA+B;CAG3D,MAAM,QAAQ,OAAO,YAAY,CAAC,CAAC,MAAM,MAAM;CAC/C,MAAM,SAAS,MAAM;CAErB,IAAI,WAAW,UAAU,WAAW,UAAU,WAAW,UAAU,WAAW,QAAQ,OAAO;CAE7F,OAAO,IAAI,IAAI,MAAM,MAAM,EAAE,IAAI,QAAQ;AAC3C;;;;;;;;;;;;;;;;AAiBA,SAAgB,iBAAiB,MAAiD;CAGhF,QAFiB,MAAM,QAAQ,OAAO,CAAC,EAAE,aAAa,KAAK,KAAK,SAAS,SAErD,QAAQ,QAAQ;AACtC;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,eAAe,KAAa,MAA8C;CACxF,IAAI,QAAQ,eAAe,QAAQ,cAAc,OAAO;CAIxD,OAAO,SAFS,iBAAiB,IAAI,MAAM,QAAQ,cAAc,gBAExC,IAAI;AAC/B;;;;;;;;;;;;;;;;;;;;AC3FA,SAAgB,YAAY,QAA0C;CACpE,MAAM,UAAU,OAAO,WAAW,WAAW,SAAS,eAAe,MAAM,IAAI;CAC/E,IAAI,EAAE,mBAAmB,cAAc,OAAO;CAE9C,IAAI,CAAC,QAAQ,aAAa,UAAU,GAAG,QAAQ,aAAa,YAAY,IAAI;CAI5E,QAAQ,MAAM;CAEd,OAAO;AACT"}
1
+ {"version":3,"file":"focus-target-DENXwVQ3.js","names":[],"sources":["../src/utils/sheet-root.ts","../src/composables/use-visual-viewport.ts","../src/composables/use-bound-value.ts","../src/utils/inert.ts","../src/components/BaseSheet.vue","../src/components/BaseSheet.vue","../src/components/BaseCheckbox.vue","../src/components/BaseCheckbox.vue","../src/utils/direction.ts","../src/utils/focus-target.ts"],"sourcesContent":["/** The node every sheet teleports into. */\nexport const SHEET_ROOT_ID = 'sheet-root'\n\n/**\n * Returns the sheet root, creating it if the page has none.\n *\n * `BaseSheet` teleports out of the app's tree so the app behind it can be\n * marked `inert` — a sheet inside the element it is disabling would disable\n * itself. That means it needs a mount point outside `#app`.\n *\n * It used to need the consuming app to put `<div id=\"sheet-root\">` in its\n * `index.html`, and nothing said so. A page without it got a sheet that opened,\n * blocked the page, and rendered nothing: Vue warns to the console about a\n * missing teleport target and carries on, so the build is green, the types are\n * fine, and the screen is wrong. The kit's own showcase had exactly that bug,\n * which is how it was found.\n *\n * So the node is made on demand. An app that already declares one keeps it —\n * this only fills a gap, it never replaces.\n */\nexport function ensureSheetRoot(): string {\n const selector = `#${SHEET_ROOT_ID}`\n\n // Server-rendered: there is no document, and Teleport is skipped anyway.\n if (typeof document === 'undefined') return selector\n if (document.getElementById(SHEET_ROOT_ID)) return selector\n\n const root = document.createElement('div')\n root.id = SHEET_ROOT_ID\n document.body.appendChild(root)\n\n return selector\n}\n","import { onScopeDispose, readonly, ref } from 'vue'\n\n/** The visible area, once the on-screen keyboard has taken its share. */\nexport interface VisualViewportRect {\n height: number\n offsetTop: number\n}\n\n/**\n * Tracks the visual viewport.\n *\n * Chrome and Android browsers honour `interactive-widget=resizes-content`, so\n * the layout viewport already shrinks for the keyboard there. Safari on iOS\n * does not implement it: it shrinks only the *visual* viewport, leaving a sheet\n * sized in `dvh` sitting partly underneath the keyboard.\n *\n * `null` means the API is unavailable, which callers should read as \"trust the\n * layout viewport\" rather than as zero. A server has no viewport at all, so it\n * gets that same `null` — this runs during `setup`, and a component using it\n * has to survive being rendered there.\n *\n * @example\n * ```ts\n * const viewport = useVisualViewport()\n * // :style=\"viewport ? { height: `${viewport.height}px` } : undefined\"\n * ```\n */\nexport function useVisualViewport() {\n const rect = ref<VisualViewportRect | null>(null)\n\n const viewport = typeof window === 'undefined' ? undefined : window.visualViewport\n if (!viewport) return readonly(rect)\n\n function read() {\n if (!viewport) return\n\n rect.value = { height: viewport.height, offsetTop: viewport.offsetTop }\n }\n\n read()\n\n // `scroll` matters as much as `resize`: iOS shifts the visual viewport up to\n // keep the focused field visible, without changing its height.\n viewport.addEventListener('resize', read)\n viewport.addEventListener('scroll', read)\n\n onScopeDispose(() => {\n viewport.removeEventListener('resize', read)\n viewport.removeEventListener('scroll', read)\n })\n\n return readonly(rect)\n}\n","import { computed, ref, watch } from 'vue'\nimport type { Ref, WritableComputedRef } from 'vue'\n\n/**\n * A `v-model` that accepts `undefined` and emits only what a field produces.\n *\n * `defineModel` could not be typed both ways. Without a default it declares\n * that it may emit `undefined`, so under vue-tsc's strictTemplates a\n * `ref('')` cannot be bound to it. With a default it drops `undefined` from\n * what it accepts, so under `exactOptionalPropertyTypes` a form library's\n * `string | undefined` cannot be. Hibi binds exactly that, and 2.4.0 as first\n * written broke it — the consumer check caught it before the tag.\n *\n * So the prop and the event are declared by hand — the prop as\n * `T | undefined`, the event as `T` — and this is the rest of what\n * `defineModel` did: follow the prop, and keep a value of its own when\n * nothing is bound. Vue applies `.trim` and `.number` to the event itself, so\n * modifiers behave as they did.\n *\n * Internal: not exported from the package.\n */\nexport function useBoundValue<T>(\n read: () => T | undefined,\n write: (value: T) => void,\n): WritableComputedRef<T | undefined> {\n const local = ref(read()) as Ref<T | undefined>\n\n watch(read, (next) => {\n local.value = next\n })\n\n return computed({\n get: () => local.value,\n set: (next) => {\n local.value = next\n write(next as T)\n },\n })\n}\n","/**\n * How many open layers hold each element inert. An element set inert by the\n * app itself is never in here, so releasing never undoes the app's own choice.\n */\nconst holds = new Map<Element, number>()\n\n/**\n * Takes everything on the page except the branch holding `keep` out of tab\n * order and pointer events, and returns the function that gives it back.\n *\n * `inert` is a real focus trap without keydown bookkeeping, and the only one\n * that also stops a screen reader's virtual cursor. It used to be applied to\n * `document.getElementById('app')` — the id Vite's template happens to use —\n * so an app mounted on `#root` got a dialog whose background was still\n * reachable with Tab, and nothing said so. Working from `<body>`'s children\n * instead holds whatever the app is mounted on, and anything else teleported\n * there too.\n *\n * Counted per element, so a sheet opened over a sheet, or a tour over either,\n * does not hand the page back when the inner one closes.\n */\nexport function inertOutside(keep: Element): () => void {\n if (typeof document === 'undefined') return () => {}\n\n const taken: Element[] = []\n\n for (const element of Array.from(document.body.children)) {\n if (element.contains(keep)) continue\n\n const count = holds.get(element)\n if (count === undefined) {\n if (element.hasAttribute('inert')) continue\n element.setAttribute('inert', '')\n }\n holds.set(element, (count ?? 0) + 1)\n taken.push(element)\n }\n\n let released = false\n\n return () => {\n if (released) return\n released = true\n\n for (const element of taken) {\n const count = (holds.get(element) ?? 1) - 1\n if (count > 0) {\n holds.set(element, count)\n continue\n }\n holds.delete(element)\n element.removeAttribute('inert')\n }\n }\n}\n","<script setup lang=\"ts\">\nimport BaseButton from './BaseButton.vue'\nimport { computed, nextTick, onUnmounted, ref, watch } from 'vue'\nimport { X } from 'lucide-vue-next'\n\nimport { useVisualViewport } from '../composables/use-visual-viewport'\nimport { inertOutside } from '../utils/inert'\nimport { ensureSheetRoot } from '../utils/sheet-root'\n\nconst open = defineModel<boolean>({ required: true })\nconst {\n title,\n subtitle = '',\n closeLabel = 'Close',\n} = defineProps<{\n title: string\n subtitle?: string | undefined\n /**\n * Accessible name for the close button.\n *\n * A prop rather than a translation: a component that calls t() forces every\n * consumer onto one i18n setup, and this is the package's only visible string.\n */\n closeLabel?: string | undefined\n}>()\n\n/* Resolved in setup, before the first render, so the Teleport always has a\n target to find. */\nconst sheetRoot = ensureSheetRoot()\n\nconst viewport = useVisualViewport()\n\n/**\n * Pins the sheet to the area the keyboard has left visible.\n *\n * Only needed where the layout viewport does not shrink on its own — iOS. On\n * Android the numbers already agree, so this is a no-op there rather than a\n * second, competing adjustment.\n */\nconst viewportStyle = computed(() =>\n viewport.value\n ? { height: `${viewport.value.height}px`, top: `${viewport.value.offsetTop}px` }\n : undefined,\n)\n\nconst panel = ref<HTMLElement | null>(null)\nlet lastFocused: HTMLElement | null = null\n\nfunction close() {\n open.value = false\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n if (event.key === 'Escape') close()\n}\n\nwatch(open, async (isOpen) => {\n if (isOpen) {\n setBackgroundInert(true)\n lastFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null\n window.addEventListener('keydown', onKeydown)\n await nextTick()\n panel.value?.focus()\n } else {\n window.removeEventListener('keydown', onKeydown)\n // Released before focus is handed back: an inert element cannot take\n // focus, so the other order quietly dropped it on the body.\n setBackgroundInert(false)\n lastFocused?.focus()\n lastFocused = null\n }\n})\n\n/* The whole page but the sheet root goes inert while this is open. Held per\n sheet, so the unmount below can only release what this sheet took. */\nlet releaseInert: (() => void) | null = null\n\nfunction setBackgroundInert(isInert: boolean) {\n releaseInert?.()\n releaseInert = null\n\n const root = isInert ? document.querySelector(sheetRoot) : null\n if (root) releaseInert = inertOutside(root)\n}\n\nonUnmounted(() => {\n window.removeEventListener('keydown', onKeydown)\n // Unmounting while open would otherwise leave the whole app inert forever.\n setBackgroundInert(false)\n})\n</script>\n\n<template>\n <Teleport :to=\"sheetRoot\">\n <Transition name=\"sheet\">\n <div\n v-if=\"open\"\n class=\"fixed inset-x-0 top-0 bottom-0 z-50 flex items-center justify-center\"\n :style=\"viewportStyle\"\n >\n <div\n class=\"shell-frame md:rounded-shell relative flex max-h-full flex-col justify-end overflow-hidden\"\n >\n <div class=\"bg-ink/45 absolute inset-0 backdrop-blur-[2px]\" @click=\"close\" />\n\n <!-- Header and footer stay put; only the slot scrolls. Sized in dvh so\n the on-screen keyboard shrinks the sheet instead of pushing its\n content out of reach. -->\n <section\n ref=\"panel\"\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-label=\"title\"\n tabindex=\"-1\"\n class=\"sheet-panel surface-overlay relative flex max-h-[94%] min-h-[56dvh] flex-col rounded-t-[calc(var(--radius-shell)-2px)] outline-none\"\n >\n <div class=\"flex shrink-0 justify-center pt-3\" aria-hidden=\"true\">\n <span class=\"bg-hair h-1.5 w-10 rounded-full\" />\n </div>\n\n <header class=\"flex shrink-0 items-start gap-3 px-6 pt-4 pb-5\">\n <div class=\"min-w-0 flex-1\">\n <h2 class=\"text-ink text-xl leading-tight font-semibold\">{{ title }}</h2>\n <p v-if=\"subtitle\" class=\"text-ink-soft mt-1 text-sm leading-snug\">\n {{ subtitle }}\n </p>\n </div>\n\n <!-- `unstyled`, so the sheet keeps the exact button it had. What it\n gains is the focus ring it never had: this was a raw\n `<button>` with no `focus-visible` rule, so closing a sheet\n from the keyboard was invisible. -->\n <BaseButton\n variant=\"unstyled\"\n class=\"text-ink-soft hover:bg-muted hover:text-ink -mt-1 flex size-10 shrink-0 items-center justify-center rounded-full transition-colors active:scale-90\"\n :aria-label=\"closeLabel\"\n @click=\"close\"\n >\n <X class=\"size-5\" />\n </BaseButton>\n </header>\n\n <div\n class=\"min-h-0 flex-1 overflow-y-auto px-6 pb-[calc(2rem+env(safe-area-inset-bottom,0px))]\"\n >\n <slot />\n </div>\n </section>\n </div>\n </div>\n </Transition>\n </Teleport>\n</template>\n\n<style scoped>\n.sheet-enter-active,\n.sheet-leave-active {\n transition: opacity var(--duration-base) ease;\n}\n.sheet-enter-from,\n.sheet-leave-to {\n opacity: 0;\n}\n\n/* The panel travels further than the scrim fades, which is what makes the\n sheet read as rising rather than appearing. */\n.sheet-enter-active .sheet-panel,\n.sheet-leave-active .sheet-panel {\n transition: transform var(--duration-slow) var(--ease-sheet);\n}\n.sheet-enter-from .sheet-panel,\n.sheet-leave-to .sheet-panel {\n transform: translateY(6%);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .sheet-enter-from .sheet-panel,\n .sheet-leave-to .sheet-panel {\n transform: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport BaseButton from './BaseButton.vue'\nimport { computed, nextTick, onUnmounted, ref, watch } from 'vue'\nimport { X } from 'lucide-vue-next'\n\nimport { useVisualViewport } from '../composables/use-visual-viewport'\nimport { inertOutside } from '../utils/inert'\nimport { ensureSheetRoot } from '../utils/sheet-root'\n\nconst open = defineModel<boolean>({ required: true })\nconst {\n title,\n subtitle = '',\n closeLabel = 'Close',\n} = defineProps<{\n title: string\n subtitle?: string | undefined\n /**\n * Accessible name for the close button.\n *\n * A prop rather than a translation: a component that calls t() forces every\n * consumer onto one i18n setup, and this is the package's only visible string.\n */\n closeLabel?: string | undefined\n}>()\n\n/* Resolved in setup, before the first render, so the Teleport always has a\n target to find. */\nconst sheetRoot = ensureSheetRoot()\n\nconst viewport = useVisualViewport()\n\n/**\n * Pins the sheet to the area the keyboard has left visible.\n *\n * Only needed where the layout viewport does not shrink on its own — iOS. On\n * Android the numbers already agree, so this is a no-op there rather than a\n * second, competing adjustment.\n */\nconst viewportStyle = computed(() =>\n viewport.value\n ? { height: `${viewport.value.height}px`, top: `${viewport.value.offsetTop}px` }\n : undefined,\n)\n\nconst panel = ref<HTMLElement | null>(null)\nlet lastFocused: HTMLElement | null = null\n\nfunction close() {\n open.value = false\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n if (event.key === 'Escape') close()\n}\n\nwatch(open, async (isOpen) => {\n if (isOpen) {\n setBackgroundInert(true)\n lastFocused = document.activeElement instanceof HTMLElement ? document.activeElement : null\n window.addEventListener('keydown', onKeydown)\n await nextTick()\n panel.value?.focus()\n } else {\n window.removeEventListener('keydown', onKeydown)\n // Released before focus is handed back: an inert element cannot take\n // focus, so the other order quietly dropped it on the body.\n setBackgroundInert(false)\n lastFocused?.focus()\n lastFocused = null\n }\n})\n\n/* The whole page but the sheet root goes inert while this is open. Held per\n sheet, so the unmount below can only release what this sheet took. */\nlet releaseInert: (() => void) | null = null\n\nfunction setBackgroundInert(isInert: boolean) {\n releaseInert?.()\n releaseInert = null\n\n const root = isInert ? document.querySelector(sheetRoot) : null\n if (root) releaseInert = inertOutside(root)\n}\n\nonUnmounted(() => {\n window.removeEventListener('keydown', onKeydown)\n // Unmounting while open would otherwise leave the whole app inert forever.\n setBackgroundInert(false)\n})\n</script>\n\n<template>\n <Teleport :to=\"sheetRoot\">\n <Transition name=\"sheet\">\n <div\n v-if=\"open\"\n class=\"fixed inset-x-0 top-0 bottom-0 z-50 flex items-center justify-center\"\n :style=\"viewportStyle\"\n >\n <div\n class=\"shell-frame md:rounded-shell relative flex max-h-full flex-col justify-end overflow-hidden\"\n >\n <div class=\"bg-ink/45 absolute inset-0 backdrop-blur-[2px]\" @click=\"close\" />\n\n <!-- Header and footer stay put; only the slot scrolls. Sized in dvh so\n the on-screen keyboard shrinks the sheet instead of pushing its\n content out of reach. -->\n <section\n ref=\"panel\"\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-label=\"title\"\n tabindex=\"-1\"\n class=\"sheet-panel surface-overlay relative flex max-h-[94%] min-h-[56dvh] flex-col rounded-t-[calc(var(--radius-shell)-2px)] outline-none\"\n >\n <div class=\"flex shrink-0 justify-center pt-3\" aria-hidden=\"true\">\n <span class=\"bg-hair h-1.5 w-10 rounded-full\" />\n </div>\n\n <header class=\"flex shrink-0 items-start gap-3 px-6 pt-4 pb-5\">\n <div class=\"min-w-0 flex-1\">\n <h2 class=\"text-ink text-xl leading-tight font-semibold\">{{ title }}</h2>\n <p v-if=\"subtitle\" class=\"text-ink-soft mt-1 text-sm leading-snug\">\n {{ subtitle }}\n </p>\n </div>\n\n <!-- `unstyled`, so the sheet keeps the exact button it had. What it\n gains is the focus ring it never had: this was a raw\n `<button>` with no `focus-visible` rule, so closing a sheet\n from the keyboard was invisible. -->\n <BaseButton\n variant=\"unstyled\"\n class=\"text-ink-soft hover:bg-muted hover:text-ink -mt-1 flex size-10 shrink-0 items-center justify-center rounded-full transition-colors active:scale-90\"\n :aria-label=\"closeLabel\"\n @click=\"close\"\n >\n <X class=\"size-5\" />\n </BaseButton>\n </header>\n\n <div\n class=\"min-h-0 flex-1 overflow-y-auto px-6 pb-[calc(2rem+env(safe-area-inset-bottom,0px))]\"\n >\n <slot />\n </div>\n </section>\n </div>\n </div>\n </Transition>\n </Teleport>\n</template>\n\n<style scoped>\n.sheet-enter-active,\n.sheet-leave-active {\n transition: opacity var(--duration-base) ease;\n}\n.sheet-enter-from,\n.sheet-leave-to {\n opacity: 0;\n}\n\n/* The panel travels further than the scrim fades, which is what makes the\n sheet read as rising rather than appearing. */\n.sheet-enter-active .sheet-panel,\n.sheet-leave-active .sheet-panel {\n transition: transform var(--duration-slow) var(--ease-sheet);\n}\n.sheet-enter-from .sheet-panel,\n.sheet-leave-to .sheet-panel {\n transform: translateY(6%);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .sheet-enter-from .sheet-panel,\n .sheet-leave-to .sheet-panel {\n transform: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, useId } from 'vue'\n\n/**\n * A single checkbox, with its label beside it.\n *\n * Deliberately not built on `FormField`. That component stacks a label above\n * its control, which is right for every field where the control is a box you\n * type into and wrong here: a checkbox is read as one sentence with a mark in\n * front of it, and putting the words above the box breaks the association a\n * sighted reader makes before they get to the accessible name.\n *\n * The whole row is the label, so the words are part of the hit target. On a\n * phone that is the difference between a control and a coin toss.\n */\nconst {\n label,\n error = '',\n hint = '',\n disabled = false,\n size = 'md',\n indeterminate = false,\n labelHidden = false,\n} = defineProps<{\n label: string\n error?: string | undefined\n hint?: string | undefined\n disabled?: boolean | undefined\n /**\n * `md` is a setting: a line the reader came here to change, in ink.\n * `sm` is an aside — \"remember me\" under a sign-in form, \"show the ones I\n * have learned\" above a list — quieter and tighter.\n *\n * The two are not a guess. Of the five hand-written checkboxes across the\n * three consuming apps, four were the aside and one was the setting, and\n * they differed in exactly these two ways.\n */\n size?: 'sm' | 'md' | undefined\n /**\n * Neither checked nor unchecked: some of what this box stands for is.\n *\n * It is a DOM property and not an attribute, so it cannot be written in\n * markup and there is no `indeterminate=\"\"` to set — which is why a\n * \"select all\" box on top of a list is the one checkbox every app ends up\n * writing by hand. The kit's own `DataTable` did, for exactly this\n * reason.\n *\n * It is presentation only: the value underneath is still `true` or\n * `false`, and pressing the box resolves it.\n */\n indeterminate?: boolean | undefined\n /**\n * Hides the words but keeps them as the accessible name. For a box in a\n * table header or a list row whose surroundings already say what it is —\n * dropping the label would leave the control named nothing at all.\n */\n labelHidden?: boolean | undefined\n}>()\n\nconst model = defineModel<boolean>({ default: false })\n\nconst id = useId()\nconst errorId = `${id}-error`\nconst hintId = `${id}-hint`\n\nconst describedBy = computed(() => {\n if (error) return errorId\n if (hint) return hintId\n return undefined\n})\n</script>\n\n<template>\n <div class=\"flex flex-col gap-1.5\">\n <label\n :for=\"id\"\n class=\"flex items-center\"\n :class=\"[\n labelHidden ? '' : size === 'sm' ? 'gap-2' : 'gap-3',\n disabled ? 'opacity-50' : 'cursor-pointer',\n ]\"\n >\n <input\n :id=\"id\"\n v-model=\"model\"\n type=\"checkbox\"\n :disabled=\"disabled\"\n :indeterminate=\"indeterminate\"\n :aria-invalid=\"Boolean(error)\"\n :aria-describedby=\"describedBy\"\n class=\"accent-primary focus-visible:outline-primary size-4 shrink-0 focus-visible:outline-2 focus-visible:outline-offset-2\"\n />\n <span\n class=\"text-sm\"\n :class=\"[labelHidden ? 'sr-only' : '', size === 'sm' ? 'text-ink-soft' : 'text-ink']\"\n >\n {{ label }}\n </span>\n </label>\n\n <p v-if=\"error\" :id=\"errorId\" class=\"text-negative text-xs\">{{ error }}</p>\n <p v-else-if=\"hint\" :id=\"hintId\" class=\"text-ink-soft text-xs\">{{ hint }}</p>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, useId } from 'vue'\n\n/**\n * A single checkbox, with its label beside it.\n *\n * Deliberately not built on `FormField`. That component stacks a label above\n * its control, which is right for every field where the control is a box you\n * type into and wrong here: a checkbox is read as one sentence with a mark in\n * front of it, and putting the words above the box breaks the association a\n * sighted reader makes before they get to the accessible name.\n *\n * The whole row is the label, so the words are part of the hit target. On a\n * phone that is the difference between a control and a coin toss.\n */\nconst {\n label,\n error = '',\n hint = '',\n disabled = false,\n size = 'md',\n indeterminate = false,\n labelHidden = false,\n} = defineProps<{\n label: string\n error?: string | undefined\n hint?: string | undefined\n disabled?: boolean | undefined\n /**\n * `md` is a setting: a line the reader came here to change, in ink.\n * `sm` is an aside — \"remember me\" under a sign-in form, \"show the ones I\n * have learned\" above a list — quieter and tighter.\n *\n * The two are not a guess. Of the five hand-written checkboxes across the\n * three consuming apps, four were the aside and one was the setting, and\n * they differed in exactly these two ways.\n */\n size?: 'sm' | 'md' | undefined\n /**\n * Neither checked nor unchecked: some of what this box stands for is.\n *\n * It is a DOM property and not an attribute, so it cannot be written in\n * markup and there is no `indeterminate=\"\"` to set — which is why a\n * \"select all\" box on top of a list is the one checkbox every app ends up\n * writing by hand. The kit's own `DataTable` did, for exactly this\n * reason.\n *\n * It is presentation only: the value underneath is still `true` or\n * `false`, and pressing the box resolves it.\n */\n indeterminate?: boolean | undefined\n /**\n * Hides the words but keeps them as the accessible name. For a box in a\n * table header or a list row whose surroundings already say what it is —\n * dropping the label would leave the control named nothing at all.\n */\n labelHidden?: boolean | undefined\n}>()\n\nconst model = defineModel<boolean>({ default: false })\n\nconst id = useId()\nconst errorId = `${id}-error`\nconst hintId = `${id}-hint`\n\nconst describedBy = computed(() => {\n if (error) return errorId\n if (hint) return hintId\n return undefined\n})\n</script>\n\n<template>\n <div class=\"flex flex-col gap-1.5\">\n <label\n :for=\"id\"\n class=\"flex items-center\"\n :class=\"[\n labelHidden ? '' : size === 'sm' ? 'gap-2' : 'gap-3',\n disabled ? 'opacity-50' : 'cursor-pointer',\n ]\"\n >\n <input\n :id=\"id\"\n v-model=\"model\"\n type=\"checkbox\"\n :disabled=\"disabled\"\n :indeterminate=\"indeterminate\"\n :aria-invalid=\"Boolean(error)\"\n :aria-describedby=\"describedBy\"\n class=\"accent-primary focus-visible:outline-primary size-4 shrink-0 focus-visible:outline-2 focus-visible:outline-offset-2\"\n />\n <span\n class=\"text-sm\"\n :class=\"[labelHidden ? 'sr-only' : '', size === 'sm' ? 'text-ink-soft' : 'text-ink']\"\n >\n {{ label }}\n </span>\n </label>\n\n <p v-if=\"error\" :id=\"errorId\" class=\"text-negative text-xs\">{{ error }}</p>\n <p v-else-if=\"hint\" :id=\"hintId\" class=\"text-ink-soft text-xs\">{{ hint }}</p>\n </div>\n</template>\n","/** Which way a language is written. */\nexport type TextDirection = 'ltr' | 'rtl'\n\n/*\n * The language subtags written right to left.\n *\n * A list rather than `Intl.Locale.prototype.getTextInfo()`, which is recent,\n * was a property before it was a method, and is missing or differently\n * spelled depending on the engine. Feature-detecting three spellings to\n * answer a question with twelve possible answers is more code and more ways\n * to be wrong than the list, and the list does not change: a language does\n * not switch direction.\n */\nconst RTL = new Set([\n 'ar', // Arabic\n 'arc', // Aramaic\n 'ckb', // Central Kurdish\n 'dv', // Divehi\n 'fa', // Persian\n 'he', // Hebrew\n 'ks', // Kashmiri\n 'ku', // Kurdish\n 'nqo', // N'Ko\n 'ps', // Pashto\n 'sd', // Sindhi\n 'syr', // Syriac\n 'ug', // Uyghur\n 'ur', // Urdu\n 'yi', // Yiddish\n])\n\n/**\n * Which way a locale tag is written.\n *\n * The kit is built on logical properties and `start`/`end` props all the\n * way through — `BaseDrawer`'s side, every inset, every margin — and none\n * of that does anything at all until `dir` is set on the document. So the\n * direction has to come from somewhere, and a locale tag is the only thing\n * that knows it.\n *\n * Only the language subtag is read, so `ar-EG` and `ar` answer the same.\n *\n * @example\n * ```ts\n * textDirection('he-IL') // 'rtl'\n * textDirection('tr') // 'ltr'\n * ```\n */\nexport function textDirection(locale: string): TextDirection {\n // `und-Arab` and the like are a real spelling, so the script subtag is\n // worth the two lines it costs.\n const parts = locale.toLowerCase().split(/[-_]/)\n const script = parts[1]\n\n if (script === 'arab' || script === 'hebr' || script === 'thaa' || script === 'syrc') return 'rtl'\n\n return RTL.has(parts[0] ?? '') ? 'rtl' : 'ltr'\n}\n\n/**\n * Which way the text around an element runs.\n *\n * The companion to `textDirection`, and the other end of it: that one asks\n * a locale, this one asks the page. An element inside a quoted passage may\n * run the other way from the document it is in, so the nearest `dir`\n * attribute wins and the document is the fallback.\n *\n * The attribute rather than `getComputedStyle().direction`: the computed\n * value is the real answer, but it is inherited through the cascade, and\n * jsdom does not inherit it — so the tests that exist to prove the\n * right-to-left behaviour would all pass with the behaviour removed. What\n * actually sets the direction in this kit is the attribute, in one place,\n * on `<html>`, written by `createI18nRuntime`.\n */\nexport function elementDirection(from: Element | null | undefined): TextDirection {\n const declared = from?.closest('[dir]')?.getAttribute('dir') ?? document.dir\n\n return declared === 'rtl' ? 'rtl' : 'ltr'\n}\n\n/**\n * `ArrowLeft` and `ArrowRight` as a direction of travel: `-1` back through\n * a list, `1` on through it, `0` for any other key.\n *\n * They are mirrored where the writing is, which is not a nicety — it is the\n * whole of what the arrows mean. A row of tabs in Arabic runs right to\n * left, so the tab to the *left* of the current one is the next one, and a\n * handler that reads `ArrowRight` as \"forward\" walks backwards through\n * every roving-tabindex control in the kit at once. WAI-ARIA says so\n * explicitly, and it is invisible to every check: the code type-checks, the\n * styles are all logical, and the keys are the same keys.\n *\n * @example\n * ```ts\n * function onKeydown(event: KeyboardEvent) {\n * const step = horizontalStep(event.key, event.currentTarget as Element)\n * if (step === 0) return\n * event.preventDefault()\n * moveTo(index + step)\n * }\n * ```\n */\nexport function horizontalStep(key: string, from: Element | null | undefined): -1 | 0 | 1 {\n if (key !== 'ArrowLeft' && key !== 'ArrowRight') return 0\n\n const forward = elementDirection(from) === 'rtl' ? 'ArrowLeft' : 'ArrowRight'\n\n return key === forward ? 1 : -1\n}\n","/**\n * Moves focus to an element that was never meant to take it.\n *\n * A `<main>`, a heading, the top of a view: none of them are focusable, and\n * both places the kit needs this — `SkipLink` and `createRouteAnnouncer` —\n * fail in the same invisible way without it. A skip link scrolls the page\n * and leaves focus in the header; a route change swaps the content and\n * leaves focus on the link that is no longer there.\n *\n * `tabindex=\"-1\"` makes the element focusable by script and never by Tab,\n * which is the whole trick. It is left behind rather than cleaned up: a\n * reader that moves out and comes back would otherwise find an unfocusable\n * target the second time.\n *\n * @param target - The element, or the `id` of one. A missing one is a no-op.\n * @returns Whether focus moved, so a caller can fall back.\n */\nexport function focusTarget(target: Element | string | null): boolean {\n const element = typeof target === 'string' ? document.getElementById(target) : target\n if (!(element instanceof HTMLElement)) return false\n\n if (!element.hasAttribute('tabindex')) element.setAttribute('tabindex', '-1')\n\n // `focus()` scrolls it into view on its own, which is why there is no\n // `scrollIntoView` beside it.\n element.focus()\n\n return true\n}\n"],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -208,6 +208,24 @@ export type { ComboboxOption } from './components/BaseCombobox.vue';
208
208
  * @see {@link BaseSwitch} — the near-neighbour this is mistaken for
209
209
  */
210
210
  export { default as BaseCheckbox } from './components/BaseCheckbox.vue';
211
+ /**
212
+ * Several answers from a set that is all on screen.
213
+ *
214
+ * @example
215
+ * ```vue
216
+ * <BaseCheckboxGroup
217
+ * v-model="days"
218
+ * legend="Remind me on"
219
+ * :options="weekdays"
220
+ * select-all-label="Every weekday"
221
+ * :columns="2"
222
+ * />
223
+ * ```
224
+ *
225
+ * @see {@link BaseListbox}, {@link BaseRadioGroup} — the near-neighbours this is mistaken for
226
+ */
227
+ export { default as BaseCheckboxGroup } from './components/BaseCheckboxGroup.vue';
228
+ export type { CheckboxOption } from './components/BaseCheckboxGroup.vue';
211
229
  /**
212
230
  * A set of radios, and the reason there is no `BaseRadio`.
213
231
  *
@@ -216,7 +234,7 @@ export { default as BaseCheckbox } from './components/BaseCheckbox.vue';
216
234
  * <BaseRadioGroup v-model="plan" legend="Plan" :options="plans" />
217
235
  * ```
218
236
  *
219
- * @see {@link BaseListbox}, {@link BaseSelect} — the near-neighbours this is mistaken for
237
+ * @see {@link BaseCheckboxGroup}, {@link BaseListbox}, {@link BaseSelect} — the near-neighbours this is mistaken for
220
238
  */
221
239
  export { default as BaseRadioGroup } from './components/BaseRadioGroup.vue';
222
240
  /**
@@ -764,7 +782,7 @@ export { default as BaseKbd } from './components/BaseKbd.vue';
764
782
  * <BaseListbox v-model="chosen" mode="multiple" :options="people" label="People with access" />
765
783
  * ```
766
784
  *
767
- * @see {@link BaseRadioGroup}, {@link BaseSelect}, {@link TransferList} — the near-neighbours this is mistaken for
785
+ * @see {@link BaseCheckboxGroup}, {@link BaseRadioGroup}, {@link BaseSelect}, {@link TransferList} — the near-neighbours this is mistaken for
768
786
  */
769
787
  export { default as BaseListbox } from './components/BaseListbox.vue';
770
788
  export type { ListboxOption } from './components/BaseListbox.vue';
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import { a as addDays, c as lastNDays, d as toDateKey, f as todayKey, i as needsIosInstall, l as leadingBlanks, n as isApplePortable, o as eachDayOfYear, r as isInstalled, s as fromDateKey, t as SettingsGroup_default, u as startOfWeek } from "./SettingsGroup-BXkajKGl.js";
2
2
  import { a as formatNumber, i as formatDate, o as setFormatLocale, r as watchVisibility } from "./environment-Cz5KiC1t.js";
3
- import { a as BaseCheckbox_default, c as useBoundValue, d as ensureSheetRoot, i as textDirection, l as useVisualViewport, n as elementDirection, o as BaseSheet_default, r as horizontalStep, t as focusTarget, u as SHEET_ROOT_ID } from "./focus-target-BtcoJdmz.js";
4
- import { _ as toRedirectPath, a as useFieldAttrs, c as useToast, d as isThemePreference, f as readStoredTheme, g as safeRedirect, h as tapFeedback, i as BaseInput_default, l as useOnline, m as useTheme, n as useAnnounce, o as FormField_default, p as setThemeStorageKey, r as GoogleButton_default, s as BaseAlert_default, t as announce, u as applyTheme } from "./use-announce-B8Ln-LsE.js";
3
+ import { a as BaseCheckbox_default, c as useBoundValue, d as ensureSheetRoot, i as textDirection, l as useVisualViewport, n as elementDirection, o as BaseSheet_default, r as horizontalStep, t as focusTarget, u as SHEET_ROOT_ID } from "./focus-target-DENXwVQ3.js";
4
+ import { _ as toRedirectPath, a as useFieldAttrs, c as useToast, d as isThemePreference, f as readStoredTheme, g as safeRedirect, h as tapFeedback, i as BaseInput_default, l as useOnline, m as useTheme, n as useAnnounce, o as FormField_default, p as setThemeStorageKey, r as GoogleButton_default, s as BaseAlert_default, t as announce, u as applyTheme } from "./use-announce-COM3S_ER.js";
5
5
  import { n as registerErrorMapper, r as toAppError, t as AppError } from "./app-error-DF9cijE0.js";
6
- import { a as BaseSkeleton_default, c as useMediaQuery, l as useDragScroll, n as ScrollArea_default, o as useMenuKeys, s as useAnchoredPanel, t as BaseListbox_default } from "./BaseListbox-D-NQ6nDd.js";
6
+ import { a as BaseSkeleton_default, c as useMediaQuery, l as useDragScroll, n as ScrollArea_default, o as useMenuKeys, s as useAnchoredPanel, t as BaseListbox_default } from "./BaseListbox-tS7ZpcoF.js";
7
7
  import { t as _plugin_vue_export_helper_default } from "./_plugin-vue_export-helper-BOaGB7Aw.js";
8
- import { t as BaseButton_default } from "./BaseButton-BD2HKB1L.js";
8
+ import { t as BaseButton_default } from "./BaseButton-C3VGgIuR.js";
9
9
  import { t as SettingsRow_default } from "./SettingsRow-C6Spi7Np.js";
10
10
  import { Fragment, Teleport, Transition, TransitionGroup, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, guardReactiveProps, isRef, mergeModels, mergeProps, nextTick, normalizeClass, normalizeProps, normalizeStyle, onBeforeUnmount, onErrorCaptured, onMounted, onScopeDispose, openBlock, readonly, ref, renderList, renderSlot, resolveDynamicComponent, toDisplayString, unref, useId, useModel, useTemplateRef, vModelRadio, vModelSelect, vModelText, vShow, watch, watchEffect, withCtx, withDirectives, withKeys, withModifiers } from "vue";
11
11
  import { ArrowDown, ArrowRight, ArrowUp, ArrowUpRight, CalendarDays, Check, CheckCircle2, ChevronDown, ChevronLeft, ChevronRight, Clock, Copy, Eye, EyeOff, Info, Minus, Paperclip, Plus, Star, TriangleAlert, Upload, X, XCircle } from "lucide-vue-next";
@@ -272,7 +272,7 @@ function useDebouncedCallback(callback, delay = 800) {
272
272
  }
273
273
  //#endregion
274
274
  //#region src/components/BaseAvatar.vue?vue&type=script&setup=true&lang.ts
275
- var _hoisted_1$51 = [
275
+ var _hoisted_1$52 = [
276
276
  "role",
277
277
  "aria-label",
278
278
  "aria-hidden"
@@ -337,7 +337,7 @@ var BaseAvatar_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__P
337
337
  d: "M3.75 17c0-3.05 2.8-5.25 6.25-5.25S16.25 13.95 16.25 17",
338
338
  "stroke-width": "1.5",
339
339
  "stroke-linecap": "round"
340
- }, null, -1)])]))], 10, _hoisted_1$51);
340
+ }, null, -1)])]))], 10, _hoisted_1$52);
341
341
  };
342
342
  }
343
343
  }), [["__scopeId", "data-v-47586bec"]]);
@@ -362,7 +362,7 @@ var BaseBadge_default = /* @__PURE__ */ defineComponent({
362
362
  });
363
363
  //#endregion
364
364
  //#region src/components/BaseMenu.vue?vue&type=script&setup=true&lang.ts
365
- var _hoisted_1$50 = [
365
+ var _hoisted_1$51 = [
366
366
  "aria-label",
367
367
  "aria-expanded",
368
368
  "aria-controls"
@@ -441,7 +441,7 @@ var BaseMenu_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PUR
441
441
  "aria-haspopup": true,
442
442
  "aria-controls": open.value ? unref(id) : void 0,
443
443
  onClick: _cache[0] || (_cache[0] = ($event) => open.value = !open.value)
444
- }, [renderSlot(_ctx.$slots, "trigger", { open: open.value }, void 0, true)], 8, _hoisted_1$50), open.value ? (openBlock(), createElementBlock("div", {
444
+ }, [renderSlot(_ctx.$slots, "trigger", { open: open.value }, void 0, true)], 8, _hoisted_1$51), open.value ? (openBlock(), createElementBlock("div", {
445
445
  key: 0,
446
446
  id: unref(id),
447
447
  ref_key: "panel",
@@ -574,7 +574,7 @@ function useVirtualWindow({ count, rowHeight, threshold }) {
574
574
  }
575
575
  //#endregion
576
576
  //#region src/components/BaseCombobox.vue?vue&type=script&setup=true&lang.ts
577
- var _hoisted_1$49 = ["for"];
577
+ var _hoisted_1$50 = ["for"];
578
578
  var _hoisted_2$43 = [
579
579
  "aria-label",
580
580
  "disabled",
@@ -590,7 +590,7 @@ var _hoisted_3$30 = [
590
590
  "aria-describedby"
591
591
  ];
592
592
  var _hoisted_4$21 = ["disabled"];
593
- var _hoisted_5$12 = [
593
+ var _hoisted_5$11 = [
594
594
  "aria-label",
595
595
  "aria-multiselectable",
596
596
  "aria-busy"
@@ -816,7 +816,7 @@ var BaseCombobox_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @_
816
816
  createElementVNode("label", {
817
817
  for: unref(id),
818
818
  class: "rk-combo-label"
819
- }, toDisplayString(__props.label), 9, _hoisted_1$49),
819
+ }, toDisplayString(__props.label), 9, _hoisted_1$50),
820
820
  createElementVNode("div", { class: normalizeClass(["rk-combo-field", multiple.value ? "is-multiple control" : ""]) }, [
821
821
  (openBlock(true), createElementBlock(Fragment, null, renderList(multiple.value ? chosenOptions.value : [], (option) => {
822
822
  return openBlock(), createElementBlock("span", {
@@ -904,7 +904,7 @@ var BaseCombobox_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @_
904
904
  height: "0.75rem"
905
905
  });
906
906
  }), 64))])])) : matches.value.length === 0 ? (openBlock(), createElementBlock("li", _hoisted_10$3, toDisplayString(__props.emptyLabel), 1)) : createCommentVNode("", true)
907
- ], 46, _hoisted_5$12), [[vShow, open.value]]),
907
+ ], 46, _hoisted_5$11), [[vShow, open.value]]),
908
908
  __props.error ? (openBlock(), createElementBlock("p", {
909
909
  key: 0,
910
910
  id: errorId,
@@ -919,6 +919,102 @@ var BaseCombobox_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @_
919
919
  }
920
920
  }), [["__scopeId", "data-v-f02a0924"]]);
921
921
  //#endregion
922
+ //#region src/components/BaseCheckboxGroup.vue?vue&type=script&setup=true&lang.ts
923
+ var _hoisted_1$49 = ["aria-describedby", "aria-invalid"];
924
+ //#endregion
925
+ //#region src/components/BaseCheckboxGroup.vue
926
+ var BaseCheckboxGroup_default = /* @__PURE__ */ defineComponent({
927
+ __name: "BaseCheckboxGroup",
928
+ props: /*@__PURE__*/ mergeModels({
929
+ options: {},
930
+ legend: {},
931
+ error: { default: "" },
932
+ hint: { default: "" },
933
+ legendHidden: {
934
+ type: Boolean,
935
+ default: false
936
+ },
937
+ selectAllLabel: { default: "" },
938
+ disabled: {
939
+ type: Boolean,
940
+ default: false
941
+ },
942
+ columns: { default: 1 }
943
+ }, {
944
+ "modelValue": { default: () => [] },
945
+ "modelModifiers": {}
946
+ }),
947
+ emits: ["update:modelValue"],
948
+ setup(__props) {
949
+ const model = useModel(__props, "modelValue");
950
+ const id = useId();
951
+ const errorId = `${id}-error`;
952
+ const hintId = `${id}-hint`;
953
+ const describedBy = computed(() => {
954
+ if (__props.error) return errorId;
955
+ if (__props.hint) return hintId;
956
+ });
957
+ const choosable = computed(() => __props.options.filter((option) => !option.disabled));
958
+ const allChosen = computed(() => choosable.value.length > 0 && choosable.value.every((option) => model.value.includes(option.value)));
959
+ const someChosen = computed(() => !allChosen.value && choosable.value.some((option) => model.value.includes(option.value)));
960
+ function toggle(value, chosen) {
961
+ model.value = chosen ? [...model.value, value] : model.value.filter((chosenValue) => chosenValue !== value);
962
+ }
963
+ function toggleAll(chosen) {
964
+ const locked = model.value.filter((value) => __props.options.some((option) => option.value === value && option.disabled));
965
+ model.value = chosen ? [...locked, ...choosable.value.map((option) => option.value)] : locked;
966
+ }
967
+ return (_ctx, _cache) => {
968
+ return openBlock(), createElementBlock("fieldset", {
969
+ class: "flex flex-col gap-2",
970
+ "aria-describedby": describedBy.value,
971
+ "aria-invalid": Boolean(__props.error)
972
+ }, [
973
+ createElementVNode("legend", { class: normalizeClass(["text-ink mb-1 text-sm font-medium", __props.legendHidden ? "sr-only" : ""]) }, toDisplayString(__props.legend), 3),
974
+ __props.selectAllLabel ? (openBlock(), createBlock(BaseCheckbox_default, {
975
+ key: 0,
976
+ "model-value": allChosen.value,
977
+ label: __props.selectAllLabel,
978
+ indeterminate: someChosen.value,
979
+ disabled: __props.disabled || choosable.value.length === 0,
980
+ class: "border-hair/70 border-b pb-2",
981
+ "onUpdate:modelValue": toggleAll
982
+ }, null, 8, [
983
+ "model-value",
984
+ "label",
985
+ "indeterminate",
986
+ "disabled"
987
+ ])) : createCommentVNode("", true),
988
+ createElementVNode("div", { class: normalizeClass(["grid gap-2", __props.columns === 2 ? "sm:grid-cols-2" : ""]) }, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.options, (option) => {
989
+ return openBlock(), createBlock(BaseCheckbox_default, {
990
+ key: option.value,
991
+ "model-value": model.value.includes(option.value),
992
+ label: option.label,
993
+ hint: option.hint ?? "",
994
+ disabled: __props.disabled || option.disabled,
995
+ "onUpdate:modelValue": (chosen) => toggle(option.value, chosen)
996
+ }, null, 8, [
997
+ "model-value",
998
+ "label",
999
+ "hint",
1000
+ "disabled",
1001
+ "onUpdate:modelValue"
1002
+ ]);
1003
+ }), 128))], 2),
1004
+ __props.error ? (openBlock(), createElementBlock("p", {
1005
+ key: 1,
1006
+ id: errorId,
1007
+ class: "text-negative text-xs"
1008
+ }, toDisplayString(__props.error), 1)) : __props.hint ? (openBlock(), createElementBlock("p", {
1009
+ key: 2,
1010
+ id: hintId,
1011
+ class: "text-ink-soft text-xs"
1012
+ }, toDisplayString(__props.hint), 1)) : createCommentVNode("", true)
1013
+ ], 8, _hoisted_1$49);
1014
+ };
1015
+ }
1016
+ });
1017
+ //#endregion
922
1018
  //#region src/components/BaseRadioGroup.vue?vue&type=script&setup=true&lang.ts
923
1019
  var _hoisted_1$48 = ["aria-describedby"];
924
1020
  var _hoisted_2$42 = [
@@ -1095,7 +1191,7 @@ var _hoisted_1$46 = { class: "rk-slider" };
1095
1191
  var _hoisted_2$40 = { class: "rk-slider-head" };
1096
1192
  var _hoisted_3$27 = ["for"];
1097
1193
  var _hoisted_4$19 = ["for"];
1098
- var _hoisted_5$11 = [
1194
+ var _hoisted_5$10 = [
1099
1195
  "id",
1100
1196
  "min",
1101
1197
  "max",
@@ -1160,7 +1256,7 @@ var BaseSlider_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__P
1160
1256
  "aria-valuetext": text.value,
1161
1257
  "aria-describedby": __props.hint ? hintId : void 0,
1162
1258
  style: normalizeStyle({ "--rk-slider-filled": `${filled.value}%` })
1163
- }, null, 12, _hoisted_5$11), [[
1259
+ }, null, 12, _hoisted_5$10), [[
1164
1260
  vModelText,
1165
1261
  model.value,
1166
1262
  void 0,
@@ -1699,7 +1795,7 @@ var _hoisted_4$16 = {
1699
1795
  key: 1,
1700
1796
  class: "text-ink-soft max-w-[36ch] text-sm"
1701
1797
  };
1702
- var _hoisted_5$10 = {
1798
+ var _hoisted_5$9 = {
1703
1799
  key: 2,
1704
1800
  class: "mt-2 flex w-full flex-col gap-2"
1705
1801
  };
@@ -1725,7 +1821,7 @@ var EmptyState_default = /* @__PURE__ */ defineComponent({
1725
1821
  _ctx.$slots.icon ? (openBlock(), createElementBlock("div", _hoisted_2$35, [renderSlot(_ctx.$slots, "icon")])) : createCommentVNode("", true),
1726
1822
  createElementVNode("h3", _hoisted_3$23, toDisplayString(__props.title), 1),
1727
1823
  __props.description ? (openBlock(), createElementBlock("p", _hoisted_4$16, toDisplayString(__props.description), 1)) : createCommentVNode("", true),
1728
- _ctx.$slots.action ? (openBlock(), createElementBlock("div", _hoisted_5$10, [renderSlot(_ctx.$slots, "action")])) : createCommentVNode("", true)
1824
+ _ctx.$slots.action ? (openBlock(), createElementBlock("div", _hoisted_5$9, [renderSlot(_ctx.$slots, "action")])) : createCommentVNode("", true)
1729
1825
  ]);
1730
1826
  };
1731
1827
  }
@@ -1794,14 +1890,16 @@ var PageContainer_default = /* @__PURE__ */ defineComponent({
1794
1890
  //#region src/components/PageHeader.vue?vue&type=script&setup=true&lang.ts
1795
1891
  var _hoisted_1$38 = { class: "grid h-12 shrink-0 grid-cols-[2.5rem_1fr_2.5rem] items-center" };
1796
1892
  var _hoisted_2$34 = { class: "justify-self-start" };
1797
- var _hoisted_3$22 = { class: "text-ink flex min-w-0 justify-center text-base font-semibold tabular-nums" };
1798
- var _hoisted_4$15 = { class: "truncate" };
1799
- var _hoisted_5$9 = { class: "justify-self-end" };
1893
+ var _hoisted_3$22 = { class: "truncate" };
1894
+ var _hoisted_4$15 = { class: "justify-self-end" };
1800
1895
  //#endregion
1801
1896
  //#region src/components/PageHeader.vue
1802
1897
  var PageHeader_default = /* @__PURE__ */ defineComponent({
1803
1898
  __name: "PageHeader",
1804
- props: { title: {} },
1899
+ props: {
1900
+ title: {},
1901
+ as: { default: "h1" }
1902
+ },
1805
1903
  setup(__props) {
1806
1904
  /**
1807
1905
  * The bar at the top of a screen: a title with room either side of it.
@@ -1815,8 +1913,11 @@ var PageHeader_default = /* @__PURE__ */ defineComponent({
1815
1913
  return (_ctx, _cache) => {
1816
1914
  return openBlock(), createElementBlock("header", _hoisted_1$38, [
1817
1915
  createElementVNode("div", _hoisted_2$34, [renderSlot(_ctx.$slots, "left")]),
1818
- createElementVNode("h1", _hoisted_3$22, [renderSlot(_ctx.$slots, "title", {}, () => [createElementVNode("span", _hoisted_4$15, toDisplayString(__props.title), 1)])]),
1819
- createElementVNode("div", _hoisted_5$9, [renderSlot(_ctx.$slots, "right")])
1916
+ (openBlock(), createBlock(resolveDynamicComponent(__props.as), { class: "text-ink flex min-w-0 justify-center text-base font-semibold tabular-nums" }, {
1917
+ default: withCtx(() => [renderSlot(_ctx.$slots, "title", {}, () => [createElementVNode("span", _hoisted_3$22, toDisplayString(__props.title), 1)])]),
1918
+ _: 3
1919
+ })),
1920
+ createElementVNode("div", _hoisted_4$15, [renderSlot(_ctx.$slots, "right")])
1820
1921
  ]);
1821
1922
  };
1822
1923
  }
@@ -2067,7 +2168,7 @@ var SkeletonList_default = /* @__PURE__ */ defineComponent({
2067
2168
  return openBlock(), createElementBlock("div", _hoisted_1$32, [createElementVNode("span", _hoisted_2$30, toDisplayString(__props.label), 1), (openBlock(true), createElementBlock(Fragment, null, renderList(__props.rows, (row) => {
2068
2169
  return openBlock(), createElementBlock("div", {
2069
2170
  key: row,
2070
- class: normalizeClass(["bg-muted rounded-card animate-pulse", isLength.value ? void 0 : __props.rowHeight]),
2171
+ class: normalizeClass(["bg-muted rounded-card motion-safe:animate-pulse", isLength.value ? void 0 : __props.rowHeight]),
2071
2172
  style: normalizeStyle(isLength.value ? { height: __props.rowHeight } : void 0),
2072
2173
  "aria-hidden": "true"
2073
2174
  }, null, 6);
@@ -5198,8 +5299,8 @@ function createI18nRuntime(options) {
5198
5299
  *
5199
5300
  * The fallback keeps `vitest` and `vite dev` honest, where no define runs.
5200
5301
  */
5201
- var VERSION = "4.2.0";
5302
+ var VERSION = "4.4.0";
5202
5303
  //#endregion
5203
- export { ActivityGrid_default as ActivityGrid, AnnounceHost_default as AnnounceHost, AppError, AvatarStack_default as AvatarStack, BarChart_default as BarChart, BaseAlert_default as BaseAlert, BaseAvatar_default as BaseAvatar, BaseBadge_default as BaseBadge, BaseButton_default as BaseButton, BaseCalendar_default as BaseCalendar, BaseCard_default as BaseCard, BaseCheckbox_default as BaseCheckbox, BaseChip_default as BaseChip, BaseCombobox_default as BaseCombobox, BaseDatePicker_default as BaseDatePicker, BaseInput_default as BaseInput, BaseKbd_default as BaseKbd, BaseLink_default as BaseLink, BaseListbox_default as BaseListbox, BaseMenu_default as BaseMenu, BasePopconfirm_default as BasePopconfirm, BasePopover_default as BasePopover, BaseRadioGroup_default as BaseRadioGroup, BaseRating_default as BaseRating, BaseSelect_default as BaseSelect, BaseSeparator_default as BaseSeparator, BaseSheet_default as BaseSheet, BaseSkeleton_default as BaseSkeleton, BaseSlider_default as BaseSlider, BaseSpinner_default as BaseSpinner, BaseStepper_default as BaseStepper, BaseSwitch_default as BaseSwitch, BaseTable_default as BaseTable, BaseTextarea_default as BaseTextarea, BaseTimeline_default as BaseTimeline, CircularProgress_default as CircularProgress, CodeBlock_default as CodeBlock, ColorPicker_default as ColorPicker, CopyButton_default as CopyButton, DescriptionList_default as DescriptionList, DonutChart_default as DonutChart, EmptyState_default as EmptyState, ErrorBoundary_default as ErrorBoundary, ErrorSummary_default as ErrorSummary, FileDrop_default as FileDrop, FormField_default as FormField, GoogleButton_default as GoogleButton, LocaleLinks_default as LocaleLinks, MATERIALS, NumberInput_default as NumberInput, PALETTES, PageContainer_default as PageContainer, PageHeader_default as PageHeader, PasswordInput_default as PasswordInput, PinInput_default as PinInput, PriceCard_default as PriceCard, ProgressBar_default as ProgressBar, SHEET_ROOT_ID, ScrollArea_default as ScrollArea, SectionHeading_default as SectionHeading, SegmentedControl_default as SegmentedControl, SettingsGroup_default as SettingsGroup, SettingsRow_default as SettingsRow, SkeletonList_default as SkeletonList, SliderField_default as SliderField, StatCard_default as StatCard, TabBar_default as TabBar, TagsInput_default as TagsInput, TimePicker_default as TimePicker, ToastHost_default as ToastHost, ToggleGroup_default as ToggleGroup, ToneDot_default as ToneDot, VERSION, VirtualList_default as VirtualList, addDays, announce, applyMaterial, applyPalette, applyTheme, createI18nRuntime, downloadJson, eachDayOfYear, elementDirection, ensureSheetRoot, formatDate, formatNumber, fromDateKey, horizontalStep, isApplePortable, isInstalled, isMaterial, isPaletteName, isThemePreference, lastNDays, leadingBlanks, needsIosInstall, readStoredTheme, registerErrorMapper, relativeDayLabel, safeRedirect, setFormatLocale, setMaterialStorageKey, setPaletteStorageKey, setThemeStorageKey, startOfWeek, tapFeedback, textDirection, toAppError, toDateKey, toRedirectPath, todayKey, useAnnounce, useDebouncedCallback, useDragScroll, useMaterial, useMediaQuery, useOnline, usePalette, useTheme, useToast, useToday, useVisualViewport };
5304
+ export { ActivityGrid_default as ActivityGrid, AnnounceHost_default as AnnounceHost, AppError, AvatarStack_default as AvatarStack, BarChart_default as BarChart, BaseAlert_default as BaseAlert, BaseAvatar_default as BaseAvatar, BaseBadge_default as BaseBadge, BaseButton_default as BaseButton, BaseCalendar_default as BaseCalendar, BaseCard_default as BaseCard, BaseCheckbox_default as BaseCheckbox, BaseCheckboxGroup_default as BaseCheckboxGroup, BaseChip_default as BaseChip, BaseCombobox_default as BaseCombobox, BaseDatePicker_default as BaseDatePicker, BaseInput_default as BaseInput, BaseKbd_default as BaseKbd, BaseLink_default as BaseLink, BaseListbox_default as BaseListbox, BaseMenu_default as BaseMenu, BasePopconfirm_default as BasePopconfirm, BasePopover_default as BasePopover, BaseRadioGroup_default as BaseRadioGroup, BaseRating_default as BaseRating, BaseSelect_default as BaseSelect, BaseSeparator_default as BaseSeparator, BaseSheet_default as BaseSheet, BaseSkeleton_default as BaseSkeleton, BaseSlider_default as BaseSlider, BaseSpinner_default as BaseSpinner, BaseStepper_default as BaseStepper, BaseSwitch_default as BaseSwitch, BaseTable_default as BaseTable, BaseTextarea_default as BaseTextarea, BaseTimeline_default as BaseTimeline, CircularProgress_default as CircularProgress, CodeBlock_default as CodeBlock, ColorPicker_default as ColorPicker, CopyButton_default as CopyButton, DescriptionList_default as DescriptionList, DonutChart_default as DonutChart, EmptyState_default as EmptyState, ErrorBoundary_default as ErrorBoundary, ErrorSummary_default as ErrorSummary, FileDrop_default as FileDrop, FormField_default as FormField, GoogleButton_default as GoogleButton, LocaleLinks_default as LocaleLinks, MATERIALS, NumberInput_default as NumberInput, PALETTES, PageContainer_default as PageContainer, PageHeader_default as PageHeader, PasswordInput_default as PasswordInput, PinInput_default as PinInput, PriceCard_default as PriceCard, ProgressBar_default as ProgressBar, SHEET_ROOT_ID, ScrollArea_default as ScrollArea, SectionHeading_default as SectionHeading, SegmentedControl_default as SegmentedControl, SettingsGroup_default as SettingsGroup, SettingsRow_default as SettingsRow, SkeletonList_default as SkeletonList, SliderField_default as SliderField, StatCard_default as StatCard, TabBar_default as TabBar, TagsInput_default as TagsInput, TimePicker_default as TimePicker, ToastHost_default as ToastHost, ToggleGroup_default as ToggleGroup, ToneDot_default as ToneDot, VERSION, VirtualList_default as VirtualList, addDays, announce, applyMaterial, applyPalette, applyTheme, createI18nRuntime, downloadJson, eachDayOfYear, elementDirection, ensureSheetRoot, formatDate, formatNumber, fromDateKey, horizontalStep, isApplePortable, isInstalled, isMaterial, isPaletteName, isThemePreference, lastNDays, leadingBlanks, needsIosInstall, readStoredTheme, registerErrorMapper, relativeDayLabel, safeRedirect, setFormatLocale, setMaterialStorageKey, setPaletteStorageKey, setThemeStorageKey, startOfWeek, tapFeedback, textDirection, toAppError, toDateKey, toRedirectPath, todayKey, useAnnounce, useDebouncedCallback, useDragScroll, useMaterial, useMediaQuery, useOnline, usePalette, useTheme, useToast, useToday, useVisualViewport };
5204
5305
 
5205
5306
  //# sourceMappingURL=index.js.map