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.
- package/README.md +49 -68
- package/dist/{BaseButton-BD2HKB1L.js → BaseButton-C3VGgIuR.js} +2 -2
- package/dist/BaseButton-C3VGgIuR.js.map +1 -0
- package/dist/{BaseListbox-D-NQ6nDd.js → BaseListbox-tS7ZpcoF.js} +2 -2
- package/dist/{BaseListbox-D-NQ6nDd.js.map → BaseListbox-tS7ZpcoF.js.map} +1 -1
- package/dist/app.js +3 -3
- package/dist/components/BaseCheckboxGroup.vue.d.ts +59 -0
- package/dist/components/PageHeader.vue.d.ts +14 -3
- package/dist/components/SectionHeading.vue.d.ts +15 -5
- package/dist/{focus-target-BtcoJdmz.js → focus-target-DENXwVQ3.js} +2 -2
- package/dist/{focus-target-BtcoJdmz.js.map → focus-target-DENXwVQ3.js.map} +1 -1
- package/dist/index.d.ts +20 -2
- package/dist/index.js +126 -25
- package/dist/index.js.map +1 -1
- package/dist/pwa.js +1 -1
- package/dist/{use-announce-B8Ln-LsE.js → use-announce-COM3S_ER.js} +2 -2
- package/dist/{use-announce-B8Ln-LsE.js.map → use-announce-COM3S_ER.js.map} +1 -1
- package/dist/web.js +3 -3
- package/package.json +17 -6
- package/dist/BaseButton-BD2HKB1L.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BaseListbox-D-NQ6nDd.js","names":[],"sources":["../src/composables/use-drag-scroll.ts","../src/composables/use-media-query.ts","../src/composables/use-anchored-panel.ts","../src/composables/use-menu-keys.ts","../src/components/BaseSkeleton.vue","../src/components/BaseSkeleton.vue","../src/utils/focusable.ts","../src/components/ScrollArea.vue","../src/components/ScrollArea.vue","../src/components/BaseListbox.vue","../src/components/BaseListbox.vue"],"sourcesContent":["import { onScopeDispose, watch } from 'vue'\nimport type { Ref } from 'vue'\n\n/** Movement before a press counts as a drag rather than a tap. */\nconst DRAG_THRESHOLD_PX = 6\n\n/**\n * Drag-to-scroll for a horizontally scrolling element.\n *\n * The app puts `touch-action: pan-y` on the page content so the tab-swipe\n * gesture keeps its pointer events — the browser never claims a horizontal\n * drag, which also means it never pans this element natively. Rather than give\n * that up, horizontal scrolling is driven here.\n *\n * @param target - The scroll container.\n * @returns `didDrag`, so a click handler can ignore the press that ended a drag.\n *\n * @example\n * ```ts\n * const scroller = ref<HTMLElement | null>(null)\n * const { didDrag } = useDragScroll(scroller)\n *\n * function onClick() {\n * if (didDrag()) return\n * // …treat as a tap\n * }\n * ```\n */\nexport function useDragScroll(target: Ref<HTMLElement | null>) {\n let pointerId: number | null = null\n let startX = 0\n let startScroll = 0\n let dragged = false\n\n function onPointerDown(event: PointerEvent) {\n const element = target.value\n if (!element || event.pointerType === 'mouse') return\n\n pointerId = event.pointerId\n startX = event.clientX\n startScroll = element.scrollLeft\n dragged = false\n }\n\n function onPointerMove(event: PointerEvent) {\n const element = target.value\n if (!element || event.pointerId !== pointerId) return\n\n const dx = event.clientX - startX\n if (!dragged && Math.abs(dx) < DRAG_THRESHOLD_PX) return\n\n // Capture only once the gesture is clearly horizontal, so a vertical scroll\n // that happens to start here still belongs to the page.\n if (!dragged) {\n dragged = true\n element.setPointerCapture(event.pointerId)\n }\n\n element.scrollLeft = startScroll - dx\n }\n\n function onPointerUp(event: PointerEvent) {\n const element = target.value\n if (element?.hasPointerCapture(event.pointerId)) {\n element.releasePointerCapture(event.pointerId)\n }\n\n pointerId = null\n }\n\n function bind(element: HTMLElement) {\n element.addEventListener('pointerdown', onPointerDown)\n element.addEventListener('pointermove', onPointerMove)\n element.addEventListener('pointerup', onPointerUp)\n element.addEventListener('pointercancel', onPointerUp)\n }\n\n function unbind(element: HTMLElement) {\n element.removeEventListener('pointerdown', onPointerDown)\n element.removeEventListener('pointermove', onPointerMove)\n element.removeEventListener('pointerup', onPointerUp)\n element.removeEventListener('pointercancel', onPointerUp)\n }\n\n watch(\n target,\n (element, previous) => {\n if (previous) unbind(previous)\n if (element) bind(element)\n },\n { immediate: true },\n )\n\n onScopeDispose(() => {\n if (target.value) unbind(target.value)\n })\n\n return { didDrag: () => dragged }\n}\n","import { onBeforeUnmount, onMounted, ref } from 'vue'\n\n/**\n * Whether a media query matches, kept up to date.\n *\n * Starts false and resolves on mount, which is deliberate: this is the one\n * place a component is tempted to branch on viewport during render, and doing\n * that under prerendering produces HTML built for a screen the server does not\n * have. Hydration then swaps it and the page jumps. False first, correct a\n * frame later, no jump — and a layout that reads badly at `false` is a layout\n * with a mobile-first bug worth knowing about.\n *\n * Guarded for the server for the same reason the rest of the kit is: this\n * package has to be importable in Node, and `matchMedia` does not exist there.\n *\n * @example\n * ```ts\n * const wide = useMediaQuery('(min-width: 64rem)')\n * ```\n */\nexport function useMediaQuery(query: string) {\n const matches = ref(false)\n\n let list: MediaQueryList | undefined\n\n function update(event: MediaQueryList | MediaQueryListEvent) {\n matches.value = event.matches\n }\n\n onMounted(() => {\n if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return\n\n list = window.matchMedia(query)\n update(list)\n list.addEventListener('change', update)\n })\n\n onBeforeUnmount(() => {\n list?.removeEventListener('change', update)\n })\n\n return matches\n}\n","import { nextTick, onBeforeUnmount, ref, watch } from 'vue'\nimport type { Ref } from 'vue'\n\n/** Above the anchor or below it. */\nexport type PanelSide = 'top' | 'bottom'\n\nexport interface AnchoredPanelOptions {\n /** The element the panel is positioned against. */\n root: Ref<HTMLElement | null>\n /** The panel itself, measured once it is on screen. */\n panel: Ref<HTMLElement | null>\n /** Whether it is showing. */\n open: Ref<boolean>\n /** Which way it opens before anything is measured. */\n side?: PanelSide\n /** How close to the window edge it may come, in pixels. */\n margin?: number\n}\n\n/**\n * Keeping a panel on screen.\n *\n * Two corrections, in this order, because they are not equal. **Flip**: a\n * panel that does not fit below its anchor opens above it when there is\n * more room there. **Shift**: whatever is left of it past the side of the\n * window is slid back in.\n *\n * A panel taller than the window fits neither way, and then the rule is\n * still \"more room wins\" — which shows more of it, and leaves the rest\n * scrolled to rather than gone. `BasePopover` carried a comment saying it\n * kept the side it asked for in that case; it never did, and nothing\n * checked, because the arithmetic had never been given a layout to work\n * on. The comment is gone and the behaviour is the one under test.\n *\n * This was written inside `BasePopover`, which is how the kit came to ship\n * a popover that flips and a menu that does not. `BaseMenu`'s panel was\n * pinned under its trigger with `top: 100%`, so a menu button near the\n * bottom of a window — a row action, the `…` on a card, anything on a\n * phone — opened a list that ran off the bottom of the screen. Nothing said\n * so: it rendered, it passed axe, and the items were all there.\n *\n * Re-measured while open, because the page can scroll or the window change\n * size underneath, and a panel that stays where it opened ends up attached\n * to nothing. The scroll listener captures, so a scroll in any ancestor\n * counts, and it is passive-by-omission: it only reads.\n */\nexport function useAnchoredPanel({\n root,\n panel,\n open,\n side = 'bottom',\n margin = 8,\n}: AnchoredPanelOptions) {\n /** Where it actually opened, after measuring. */\n const placed = ref<PanelSide>(side)\n /** Pixels slid sideways to stay on screen. */\n const shift = ref(0)\n\n function place() {\n if (!panel.value || !root.value || typeof window === 'undefined') return\n\n const rect = panel.value.getBoundingClientRect()\n const anchor = root.value.getBoundingClientRect()\n const below = window.innerHeight - anchor.bottom\n const above = anchor.top\n\n if (side === 'bottom' && rect.height > below && above > below) placed.value = 'top'\n if (side === 'top' && rect.height > above && below > above) placed.value = 'bottom'\n\n if (rect.right > window.innerWidth - margin) {\n shift.value = window.innerWidth - margin - rect.right\n }\n if (rect.left + shift.value < margin) shift.value = margin - rect.left\n }\n\n function listen(on: boolean) {\n if (typeof window === 'undefined') return\n\n const method = on ? 'addEventListener' : 'removeEventListener'\n window[method]('resize', place)\n window[method]('scroll', place, true)\n }\n\n /** Measured where it asked to be, rather than where it ended up last time. */\n function reset() {\n placed.value = side\n shift.value = 0\n }\n\n /*\n * The opening is handled here rather than left to each caller, because\n * three of them open from more than one place: `BaseCombobox` opens on a\n * keystroke, on an arrow and on a press, and a panel measured at two of\n * those three is a panel that is usually right.\n *\n * Reset before the tick, measure after it: the panel has no size until\n * it has rendered, and it must be measured where it asked to be rather\n * than where it ended up the last time it was open.\n */\n watch(\n open,\n async (isOpen) => {\n listen(isOpen)\n if (!isOpen) return\n\n reset()\n await nextTick()\n place()\n },\n { immediate: true },\n )\n\n onBeforeUnmount(() => listen(false))\n\n return { placed, shift, place, reset }\n}\n","import type { Ref } from 'vue'\n\n/**\n * The keyboard contract `role=\"menu\"` promises.\n *\n * Declaring `role=\"menu\"` is a promise to a screen reader: the arrows move\n * through the items, Home and End go to the ends, Escape closes, and Tab\n * *leaves* rather than cycling inside. That last one is the single place a\n * menu differs from a dialog, and getting it backwards traps somebody in a\n * list of links.\n *\n * It lives here because a menu anchored to a button and a menu anchored to\n * wherever the pointer was are the same keyboard and two different\n * positions. Only the position is worth writing twice.\n */\nexport interface MenuKeysOptions {\n /** The element holding the `[role=\"menuitem\"]` children. */\n panel: Ref<HTMLElement | null>\n /** Close it — Escape, and Tab on the way out. */\n onClose: () => void\n}\n\nexport function useMenuKeys({ panel, onClose }: MenuKeysOptions) {\n const items = (): HTMLElement[] =>\n panel.value ? Array.from(panel.value.querySelectorAll<HTMLElement>('[role=\"menuitem\"]')) : []\n\n function focusAt(index: number) {\n const list = items()\n if (list.length === 0) return\n\n // Wrapping, because a list with no edges is faster than one you fall off.\n list[(index + list.length) % list.length]?.focus()\n }\n\n const currentIndex = () => items().indexOf(document.activeElement as HTMLElement)\n\n /** Returns whether the key was one of the menu's own. */\n function onKeydown(event: KeyboardEvent): boolean {\n switch (event.key) {\n case 'Escape':\n event.preventDefault()\n onClose()\n return true\n case 'ArrowDown':\n event.preventDefault()\n focusAt(currentIndex() + 1)\n return true\n case 'ArrowUp':\n event.preventDefault()\n focusAt(currentIndex() - 1)\n return true\n case 'Home':\n event.preventDefault()\n focusAt(0)\n return true\n case 'End':\n event.preventDefault()\n focusAt(items().length - 1)\n return true\n case 'Tab':\n onClose()\n return true\n default:\n return false\n }\n }\n\n return { items, focusAt, currentIndex, onKeydown }\n}\n","<script setup lang=\"ts\">\n/**\n * One grey box standing in for content that has not arrived.\n *\n * `SkeletonList` is rows of these for a list; this is the primitive, for the\n * shapes a list does not cover — an avatar, a heading, a chart.\n *\n * A height is a CSS length, never a class: `h-4` inside a component the app\n * does not control would render at zero the day that utility is not in the\n * app's stylesheet, which is exactly how every skeleton in one app came out\n * invisible.\n *\n * It is hidden from assistive tech. The thing that is loading says so —\n * through `aria-busy`, or a status message — and a screen reader reading\n * \"blank, blank, blank\" helps nobody.\n */\nconst {\n width = '100%',\n height = '1rem',\n shape = 'block',\n} = defineProps<{\n /** Any CSS length: `12rem`, `60%`, `8ch`. */\n width?: string | undefined\n height?: string | undefined\n /** `circle` makes a round one and squares the width to the height. */\n shape?: 'block' | 'text' | 'circle' | undefined\n}>()\n</script>\n\n<template>\n <span\n class=\"rk-skeleton\"\n :class=\"`is-${shape}`\"\n :style=\"{ width: shape === 'circle' ? height : width, height }\"\n aria-hidden=\"true\"\n />\n</template>\n\n<style scoped>\n.rk-skeleton {\n display: block;\n flex-shrink: 0;\n background: var(--color-muted);\n animation: rk-skeleton-pulse 1.6s ease-in-out infinite;\n}\n\n.rk-skeleton.is-block {\n border-radius: var(--radius-cell);\n}\n\n/* A line of text, rounded like one and a little short of its box. */\n.rk-skeleton.is-text {\n border-radius: 9999px;\n}\n\n.rk-skeleton.is-circle {\n border-radius: 9999px;\n}\n\n@keyframes rk-skeleton-pulse {\n 50% {\n opacity: 0.55;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .rk-skeleton {\n animation: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * One grey box standing in for content that has not arrived.\n *\n * `SkeletonList` is rows of these for a list; this is the primitive, for the\n * shapes a list does not cover — an avatar, a heading, a chart.\n *\n * A height is a CSS length, never a class: `h-4` inside a component the app\n * does not control would render at zero the day that utility is not in the\n * app's stylesheet, which is exactly how every skeleton in one app came out\n * invisible.\n *\n * It is hidden from assistive tech. The thing that is loading says so —\n * through `aria-busy`, or a status message — and a screen reader reading\n * \"blank, blank, blank\" helps nobody.\n */\nconst {\n width = '100%',\n height = '1rem',\n shape = 'block',\n} = defineProps<{\n /** Any CSS length: `12rem`, `60%`, `8ch`. */\n width?: string | undefined\n height?: string | undefined\n /** `circle` makes a round one and squares the width to the height. */\n shape?: 'block' | 'text' | 'circle' | undefined\n}>()\n</script>\n\n<template>\n <span\n class=\"rk-skeleton\"\n :class=\"`is-${shape}`\"\n :style=\"{ width: shape === 'circle' ? height : width, height }\"\n aria-hidden=\"true\"\n />\n</template>\n\n<style scoped>\n.rk-skeleton {\n display: block;\n flex-shrink: 0;\n background: var(--color-muted);\n animation: rk-skeleton-pulse 1.6s ease-in-out infinite;\n}\n\n.rk-skeleton.is-block {\n border-radius: var(--radius-cell);\n}\n\n/* A line of text, rounded like one and a little short of its box. */\n.rk-skeleton.is-text {\n border-radius: 9999px;\n}\n\n.rk-skeleton.is-circle {\n border-radius: 9999px;\n}\n\n@keyframes rk-skeleton-pulse {\n 50% {\n opacity: 0.55;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .rk-skeleton {\n animation: none;\n }\n}\n</style>\n","/**\n * What the keyboard can reach, as one selector.\n *\n * Three parts of the kit ask this question and each had written its own\n * answer, which is how three answers came to differ: the dialog trap and\n * the popover agreed, and `ScrollArea` left out `:not([disabled])`, so a\n * box holding nothing but disabled buttons counted as reachable and was\n * denied the focus stop that would have been its only way in.\n *\n * `summary` belongs here for the same reason it belonged in `ScrollArea`:\n * a `<details>` heading takes focus, and a dialog whose trap skipped one\n * would let Tab out of itself.\n *\n * Two things it deliberately does not do. It does not check whether an\n * element is visible or inside an `inert` subtree — a selector cannot, and\n * every caller here is already scoped to something on screen. And it is\n * not what a roving-tabindex container should use to find its own\n * controls: `[tabindex]:not([tabindex=\"-1\"])` matches exactly the one\n * control such a container has already put in the tab order, so it would\n * find one item and then stop. `BaseToolbar` says so where it keeps its\n * own.\n */\nexport const FOCUSABLE =\n 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), summary, [tabindex]:not([tabindex=\"-1\"])'\n\n/** The reachable elements inside `root`, in document order. */\nexport function focusableWithin(root: Element | null | undefined): HTMLElement[] {\n return Array.from(root?.querySelectorAll<HTMLElement>(FOCUSABLE) ?? [])\n}\n","<script setup lang=\"ts\">\nimport { computed, onScopeDispose, ref, useTemplateRef, watchEffect } from 'vue'\n\nimport { useDragScroll } from '../composables/use-drag-scroll'\nimport { FOCUSABLE } from '../utils/focusable'\n\n/**\n * A box that scrolls, with the two things a hand-written one leaves out.\n *\n * ## The edge fade is the affordance `no-scrollbar` takes away\n *\n * Every app here hides the scrollbar on a horizontal row — a strip of\n * months, a row of filter chips — because a native bar under a 40px row is\n * louder than the row. What goes with it is the only sign on screen that\n * there is anything further along. So the fades are driven by the actual\n * scroll position: one appears at an edge exactly when there is content\n * past it, and both are gone when everything fits.\n *\n * ## A focus stop, but only when there is no other way in\n *\n * A box that scrolls and cannot take focus cannot be scrolled by a\n * keyboard, so the far end is simply unreachable. That is the fault\n * `CodeBlock` was written for, and it is not about code: it is true of any\n * scrolling box whose contents are not focusable.\n *\n * But it is *only* true of those. A row of buttons already moves under the\n * keyboard — Tab scrolls the next one into view — and giving that row a\n * stop of its own costs a press for nothing, on every row. So this checks:\n * it becomes a named region when it overflows **and** holds nothing\n * focusable, and stays out of the tab order otherwise.\n *\n * The check runs when the box or its contents resize, which covers a list\n * that arrives from a server. A swap that changes neither size — the same\n * number of rows, buttons turned into plain text — will not be noticed;\n * that is a trade against watching every mutation of a scroll container,\n * and the shape it misses is a rare one.\n */\nconst {\n label,\n axis = 'y',\n scrollbar = 'auto',\n fade = true,\n drag = false,\n} = defineProps<{\n /**\n * Names the box, e.g. \"Categories\".\n *\n * Required because this becomes a region a reader can land in, and an\n * unnamed region is announced as nothing at all. It is only used when\n * that happens, so it costs nothing in the common case.\n */\n label: string\n /** Which way it scrolls. */\n axis?: 'x' | 'y' | 'both' | undefined\n /** `hidden` takes the native bar away; the fades stay either way. */\n scrollbar?: 'auto' | 'hidden' | undefined\n /** The gradient at an edge with more content past it. */\n fade?: boolean | undefined\n /** Drag to scroll sideways, for a page that has claimed horizontal gestures. */\n drag?: boolean | undefined\n}>()\n\ndefineSlots<{ default: () => unknown }>()\n\n/* The scroll event does not bubble, so a parent cannot listen for it on the\n wrapper. `VirtualList` needs it, and needs the element it came from, so\n both are handed out rather than left to a template ref into our markup. */\nconst emit = defineEmits<{ scroll: [event: Event] }>()\n\nconst viewport = useTemplateRef<HTMLElement>('viewport')\n\nconst horizontal = computed(() => axis === 'x' || axis === 'both')\nconst vertical = computed(() => axis === 'y' || axis === 'both')\n\nconst start = ref({ x: false, y: false })\nconst end = ref({ x: false, y: false })\nconst overflows = ref(false)\nconst hasFocusable = ref(true)\n\nfunction measure() {\n const element = viewport.value\n if (!element) return\n\n const overX = element.scrollWidth - element.clientWidth\n const overY = element.scrollHeight - element.clientHeight\n\n /* A fractional pixel of overflow is a rounding artefact, not content:\n without this every box that fits exactly draws a fade at one end. */\n const slack = 1\n\n /* `scrollLeft` counts down from zero in a right-to-left box, so the\n distance travelled is its magnitude either way. That is what makes\n \"start\" and \"end\" below mean the same thing in both directions. */\n const alongX = Math.abs(element.scrollLeft)\n\n start.value = { x: alongX > slack, y: element.scrollTop > slack }\n end.value = { x: overX - alongX > slack, y: overY - element.scrollTop > slack }\n\n overflows.value = (horizontal.value && overX > slack) || (vertical.value && overY > slack)\n hasFocusable.value = element.querySelector(FOCUSABLE) !== null\n}\n\nfunction onScroll(event: Event) {\n measure()\n emit('scroll', event)\n}\n\n/** The box itself is the way in only when nothing inside it is. */\nconst isStop = computed(() => overflows.value && !hasFocusable.value)\n\nlet observer: ResizeObserver | null = null\n\nwatchEffect(() => {\n observer?.disconnect()\n observer = null\n\n const element = viewport.value\n if (!element || typeof ResizeObserver === 'undefined') return\n\n observer = new ResizeObserver(measure)\n observer.observe(element)\n // The contents too: a list that grows changes what there is to scroll\n // without the box itself changing size at all.\n for (const child of element.children) observer.observe(child)\n\n measure()\n})\n\nonScopeDispose(() => observer?.disconnect())\n\nuseDragScroll(computed(() => (drag ? viewport.value : null)))\n\ndefineExpose({\n /** The element that actually scrolls, for a parent that has to measure it. */\n viewport,\n})\n</script>\n\n<template>\n <div class=\"rk-scroll\">\n <div\n ref=\"viewport\"\n class=\"rk-scroll-viewport\"\n :class=\"[`is-${axis}`, { 'no-scrollbar': scrollbar === 'hidden' }]\"\n :tabindex=\"isStop ? 0 : undefined\"\n :role=\"isStop ? 'region' : undefined\"\n :aria-label=\"isStop ? label : undefined\"\n @scroll=\"onScroll\"\n >\n <slot />\n </div>\n\n <!-- Decoration, and never in the way of a press: the box under them is\n what scrolls. -->\n <template v-if=\"fade\">\n <span v-if=\"horizontal && start.x\" class=\"rk-scroll-fade is-start-x\" aria-hidden=\"true\" />\n <span v-if=\"horizontal && end.x\" class=\"rk-scroll-fade is-end-x\" aria-hidden=\"true\" />\n <span v-if=\"vertical && start.y\" class=\"rk-scroll-fade is-start-y\" aria-hidden=\"true\" />\n <span v-if=\"vertical && end.y\" class=\"rk-scroll-fade is-end-y\" aria-hidden=\"true\" />\n </template>\n </div>\n</template>\n\n<style scoped>\n/*\n * A flex column, so a height written on this box reaches the box that\n * scrolls.\n *\n * The caller's class lands here, on the wrapper, because that is the\n * element the component renders outermost — so `class=\"max-h-56\"` is the\n * obvious and only thing to write. It used to do nothing. The viewport\n * carried `max-height: 100%`, and a percentage height resolves against the\n * parent's *height*, which is `auto` here; a parent with only a\n * `max-height` gives it nothing to resolve against, so the rule was inert\n * and the viewport grew to the full height of its contents.\n *\n * Visually that spills. For `VirtualList` it is worse than spilling: the\n * window is sized from `clientHeight`, so an unconstrained viewport reports\n * the height of the whole list, every row is \"near the viewport\", and five\n * thousand of them render at once. That is the component's whole purpose,\n * inverted, by the one line every caller writes.\n *\n * `flex: 1 1 auto` with `min-height: 0` is what lets the item shrink below\n * its content, which is the part that is easy to leave out: a flex item's\n * default `min-height: auto` refuses to go below its content and the box\n * spills exactly as before.\n */\n.rk-scroll {\n position: relative;\n display: flex;\n flex-direction: column;\n min-width: 0;\n min-height: 0;\n}\n\n.rk-scroll-viewport {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 0;\n max-width: 100%;\n}\n\n.rk-scroll-viewport.is-x {\n overflow-x: auto;\n overflow-y: hidden;\n}\n\n.rk-scroll-viewport.is-y {\n overflow-x: hidden;\n overflow-y: auto;\n}\n\n.rk-scroll-viewport.is-both {\n overflow: auto;\n}\n\n.rk-scroll-viewport:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n/* The fade paints over whatever the box sits on, so the colour is a hook:\n a scroll area on a card sets it to the card's ground. */\n.rk-scroll-fade {\n position: absolute;\n z-index: 1;\n pointer-events: none;\n --rk-scroll-fade: var(--scroll-fade-color, var(--color-canvas));\n}\n\n.rk-scroll-fade.is-start-x,\n.rk-scroll-fade.is-end-x {\n inset-block: 0;\n width: 1.5rem;\n}\n\n.rk-scroll-fade.is-start-y,\n.rk-scroll-fade.is-end-y {\n inset-inline: 0;\n height: 1.5rem;\n}\n\n.rk-scroll-fade.is-start-x {\n inset-inline-start: 0;\n background: linear-gradient(to right, var(--rk-scroll-fade), transparent);\n}\n\n.rk-scroll-fade.is-end-x {\n inset-inline-end: 0;\n background: linear-gradient(to left, var(--rk-scroll-fade), transparent);\n}\n\n.rk-scroll-fade.is-start-y {\n inset-block-start: 0;\n background: linear-gradient(to bottom, var(--rk-scroll-fade), transparent);\n}\n\n.rk-scroll-fade.is-end-y {\n inset-block-end: 0;\n background: linear-gradient(to top, var(--rk-scroll-fade), transparent);\n}\n\n/* The insets are logical, so they land on the correct side on their own.\n A gradient has no logical direction, so these two flip by hand. */\n[dir='rtl'] .rk-scroll-fade.is-start-x {\n background: linear-gradient(to left, var(--rk-scroll-fade), transparent);\n}\n\n[dir='rtl'] .rk-scroll-fade.is-end-x {\n background: linear-gradient(to right, var(--rk-scroll-fade), transparent);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, onScopeDispose, ref, useTemplateRef, watchEffect } from 'vue'\n\nimport { useDragScroll } from '../composables/use-drag-scroll'\nimport { FOCUSABLE } from '../utils/focusable'\n\n/**\n * A box that scrolls, with the two things a hand-written one leaves out.\n *\n * ## The edge fade is the affordance `no-scrollbar` takes away\n *\n * Every app here hides the scrollbar on a horizontal row — a strip of\n * months, a row of filter chips — because a native bar under a 40px row is\n * louder than the row. What goes with it is the only sign on screen that\n * there is anything further along. So the fades are driven by the actual\n * scroll position: one appears at an edge exactly when there is content\n * past it, and both are gone when everything fits.\n *\n * ## A focus stop, but only when there is no other way in\n *\n * A box that scrolls and cannot take focus cannot be scrolled by a\n * keyboard, so the far end is simply unreachable. That is the fault\n * `CodeBlock` was written for, and it is not about code: it is true of any\n * scrolling box whose contents are not focusable.\n *\n * But it is *only* true of those. A row of buttons already moves under the\n * keyboard — Tab scrolls the next one into view — and giving that row a\n * stop of its own costs a press for nothing, on every row. So this checks:\n * it becomes a named region when it overflows **and** holds nothing\n * focusable, and stays out of the tab order otherwise.\n *\n * The check runs when the box or its contents resize, which covers a list\n * that arrives from a server. A swap that changes neither size — the same\n * number of rows, buttons turned into plain text — will not be noticed;\n * that is a trade against watching every mutation of a scroll container,\n * and the shape it misses is a rare one.\n */\nconst {\n label,\n axis = 'y',\n scrollbar = 'auto',\n fade = true,\n drag = false,\n} = defineProps<{\n /**\n * Names the box, e.g. \"Categories\".\n *\n * Required because this becomes a region a reader can land in, and an\n * unnamed region is announced as nothing at all. It is only used when\n * that happens, so it costs nothing in the common case.\n */\n label: string\n /** Which way it scrolls. */\n axis?: 'x' | 'y' | 'both' | undefined\n /** `hidden` takes the native bar away; the fades stay either way. */\n scrollbar?: 'auto' | 'hidden' | undefined\n /** The gradient at an edge with more content past it. */\n fade?: boolean | undefined\n /** Drag to scroll sideways, for a page that has claimed horizontal gestures. */\n drag?: boolean | undefined\n}>()\n\ndefineSlots<{ default: () => unknown }>()\n\n/* The scroll event does not bubble, so a parent cannot listen for it on the\n wrapper. `VirtualList` needs it, and needs the element it came from, so\n both are handed out rather than left to a template ref into our markup. */\nconst emit = defineEmits<{ scroll: [event: Event] }>()\n\nconst viewport = useTemplateRef<HTMLElement>('viewport')\n\nconst horizontal = computed(() => axis === 'x' || axis === 'both')\nconst vertical = computed(() => axis === 'y' || axis === 'both')\n\nconst start = ref({ x: false, y: false })\nconst end = ref({ x: false, y: false })\nconst overflows = ref(false)\nconst hasFocusable = ref(true)\n\nfunction measure() {\n const element = viewport.value\n if (!element) return\n\n const overX = element.scrollWidth - element.clientWidth\n const overY = element.scrollHeight - element.clientHeight\n\n /* A fractional pixel of overflow is a rounding artefact, not content:\n without this every box that fits exactly draws a fade at one end. */\n const slack = 1\n\n /* `scrollLeft` counts down from zero in a right-to-left box, so the\n distance travelled is its magnitude either way. That is what makes\n \"start\" and \"end\" below mean the same thing in both directions. */\n const alongX = Math.abs(element.scrollLeft)\n\n start.value = { x: alongX > slack, y: element.scrollTop > slack }\n end.value = { x: overX - alongX > slack, y: overY - element.scrollTop > slack }\n\n overflows.value = (horizontal.value && overX > slack) || (vertical.value && overY > slack)\n hasFocusable.value = element.querySelector(FOCUSABLE) !== null\n}\n\nfunction onScroll(event: Event) {\n measure()\n emit('scroll', event)\n}\n\n/** The box itself is the way in only when nothing inside it is. */\nconst isStop = computed(() => overflows.value && !hasFocusable.value)\n\nlet observer: ResizeObserver | null = null\n\nwatchEffect(() => {\n observer?.disconnect()\n observer = null\n\n const element = viewport.value\n if (!element || typeof ResizeObserver === 'undefined') return\n\n observer = new ResizeObserver(measure)\n observer.observe(element)\n // The contents too: a list that grows changes what there is to scroll\n // without the box itself changing size at all.\n for (const child of element.children) observer.observe(child)\n\n measure()\n})\n\nonScopeDispose(() => observer?.disconnect())\n\nuseDragScroll(computed(() => (drag ? viewport.value : null)))\n\ndefineExpose({\n /** The element that actually scrolls, for a parent that has to measure it. */\n viewport,\n})\n</script>\n\n<template>\n <div class=\"rk-scroll\">\n <div\n ref=\"viewport\"\n class=\"rk-scroll-viewport\"\n :class=\"[`is-${axis}`, { 'no-scrollbar': scrollbar === 'hidden' }]\"\n :tabindex=\"isStop ? 0 : undefined\"\n :role=\"isStop ? 'region' : undefined\"\n :aria-label=\"isStop ? label : undefined\"\n @scroll=\"onScroll\"\n >\n <slot />\n </div>\n\n <!-- Decoration, and never in the way of a press: the box under them is\n what scrolls. -->\n <template v-if=\"fade\">\n <span v-if=\"horizontal && start.x\" class=\"rk-scroll-fade is-start-x\" aria-hidden=\"true\" />\n <span v-if=\"horizontal && end.x\" class=\"rk-scroll-fade is-end-x\" aria-hidden=\"true\" />\n <span v-if=\"vertical && start.y\" class=\"rk-scroll-fade is-start-y\" aria-hidden=\"true\" />\n <span v-if=\"vertical && end.y\" class=\"rk-scroll-fade is-end-y\" aria-hidden=\"true\" />\n </template>\n </div>\n</template>\n\n<style scoped>\n/*\n * A flex column, so a height written on this box reaches the box that\n * scrolls.\n *\n * The caller's class lands here, on the wrapper, because that is the\n * element the component renders outermost — so `class=\"max-h-56\"` is the\n * obvious and only thing to write. It used to do nothing. The viewport\n * carried `max-height: 100%`, and a percentage height resolves against the\n * parent's *height*, which is `auto` here; a parent with only a\n * `max-height` gives it nothing to resolve against, so the rule was inert\n * and the viewport grew to the full height of its contents.\n *\n * Visually that spills. For `VirtualList` it is worse than spilling: the\n * window is sized from `clientHeight`, so an unconstrained viewport reports\n * the height of the whole list, every row is \"near the viewport\", and five\n * thousand of them render at once. That is the component's whole purpose,\n * inverted, by the one line every caller writes.\n *\n * `flex: 1 1 auto` with `min-height: 0` is what lets the item shrink below\n * its content, which is the part that is easy to leave out: a flex item's\n * default `min-height: auto` refuses to go below its content and the box\n * spills exactly as before.\n */\n.rk-scroll {\n position: relative;\n display: flex;\n flex-direction: column;\n min-width: 0;\n min-height: 0;\n}\n\n.rk-scroll-viewport {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 0;\n max-width: 100%;\n}\n\n.rk-scroll-viewport.is-x {\n overflow-x: auto;\n overflow-y: hidden;\n}\n\n.rk-scroll-viewport.is-y {\n overflow-x: hidden;\n overflow-y: auto;\n}\n\n.rk-scroll-viewport.is-both {\n overflow: auto;\n}\n\n.rk-scroll-viewport:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n/* The fade paints over whatever the box sits on, so the colour is a hook:\n a scroll area on a card sets it to the card's ground. */\n.rk-scroll-fade {\n position: absolute;\n z-index: 1;\n pointer-events: none;\n --rk-scroll-fade: var(--scroll-fade-color, var(--color-canvas));\n}\n\n.rk-scroll-fade.is-start-x,\n.rk-scroll-fade.is-end-x {\n inset-block: 0;\n width: 1.5rem;\n}\n\n.rk-scroll-fade.is-start-y,\n.rk-scroll-fade.is-end-y {\n inset-inline: 0;\n height: 1.5rem;\n}\n\n.rk-scroll-fade.is-start-x {\n inset-inline-start: 0;\n background: linear-gradient(to right, var(--rk-scroll-fade), transparent);\n}\n\n.rk-scroll-fade.is-end-x {\n inset-inline-end: 0;\n background: linear-gradient(to left, var(--rk-scroll-fade), transparent);\n}\n\n.rk-scroll-fade.is-start-y {\n inset-block-start: 0;\n background: linear-gradient(to bottom, var(--rk-scroll-fade), transparent);\n}\n\n.rk-scroll-fade.is-end-y {\n inset-block-end: 0;\n background: linear-gradient(to top, var(--rk-scroll-fade), transparent);\n}\n\n/* The insets are logical, so they land on the correct side on their own.\n A gradient has no logical direction, so these two flip by hand. */\n[dir='rtl'] .rk-scroll-fade.is-start-x {\n background: linear-gradient(to left, var(--rk-scroll-fade), transparent);\n}\n\n[dir='rtl'] .rk-scroll-fade.is-end-x {\n background: linear-gradient(to right, var(--rk-scroll-fade), transparent);\n}\n</style>\n","<script setup lang=\"ts\" generic=\"V extends string, M extends 'single' | 'multiple' = 'single'\">\nimport { Check } from 'lucide-vue-next'\nimport { computed, ref, useId } from 'vue'\n\nimport { useBoundValue } from '../composables/use-bound-value'\n\nexport interface ListboxOption<V extends string> {\n value: V\n /** Already translated. */\n label: string\n /** A line under the label. */\n description?: string | undefined\n disabled?: boolean | undefined\n}\n\n/**\n * A list you choose from, open on the page.\n *\n * Between `BaseRadioGroup` and `BaseSelect`: a radio group is a handful of\n * options that are all visible and all equal; a select opens the platform's\n * own picker; this is a scrollable list of many, choosing one or several,\n * that stays on screen — a list of accounts, of tags, of files to act on.\n *\n * One Tab stop. The list itself takes focus and `aria-activedescendant`\n * says which option is current, so the arrows move without thirty stops in\n * the tab order. Typing a letter jumps to the next option that starts with\n * it, which is how every native list has always worked and the first thing\n * people try.\n *\n * In `single` mode the selection follows the arrows, because that is what a\n * listbox does; in `multiple` mode Space and Enter toggle the current one.\n */\nconst {\n modelValue = undefined,\n options,\n label,\n mode = 'single' as M,\n height = '14rem',\n} = defineProps<{\n /** The chosen option, or options, with `v-model`. */\n modelValue?: Value | undefined\n options: readonly ListboxOption<V>[]\n /** The list's accessible name. */\n label: string\n /** One option, or any number of them. */\n mode?: M | undefined\n /** How tall before it scrolls. Any CSS length. */\n height?: string | undefined\n}>()\n\n/* Multiple always hands back an array — toggling one produces a list, never\n nothing — so only the single value can be undefined. */\ntype Value = M extends 'multiple' ? V[] : V | undefined\n\n/* Declared by hand rather than with defineModel, which cannot both accept\n `undefined` and promise never to emit it. See `use-bound-value.ts`. */\nconst emit = defineEmits<{ 'update:modelValue': [value: Value] }>()\nconst model = useBoundValue<Value>(\n () => modelValue as Value | undefined,\n (value) => emit('update:modelValue', value),\n)\n\nconst id = useId()\nconst active = ref(0)\n\nlet typed = ''\nlet typedAt = 0\n\nconst chosen = computed(() => {\n const value = model.value\n if (Array.isArray(value)) return new Set<V>(value)\n\n return new Set<V>(value === undefined ? [] : [value as V])\n})\n\nfunction select(option: ListboxOption<V>) {\n if (option.disabled) return\n\n if (mode === 'multiple') {\n const next = new Set(chosen.value)\n if (next.has(option.value)) next.delete(option.value)\n else next.add(option.value)\n\n // In the order of the options, so the value reads the same however the\n // reader got there.\n model.value = options\n .map((one) => one.value)\n .filter((value) => next.has(value)) as typeof model.value\n return\n }\n\n model.value = option.value as typeof model.value\n}\n\nfunction moveTo(index: number) {\n if (options.length === 0) return\n\n active.value = Math.max(0, Math.min(options.length - 1, index))\n const option = options[active.value]\n\n // Single: the selection follows the focus, which is what a listbox does.\n if (mode === 'single' && option && !option.disabled) select(option)\n\n document.getElementById(`${id}-${active.value}`)?.scrollIntoView?.({ block: 'nearest' })\n}\n\n/** The next option after the current one that starts with what was typed. */\nfunction jumpTo(letter: string) {\n const now = Date.now()\n typed = now - typedAt > 700 ? letter : typed + letter\n typedAt = now\n\n const from = typed.length === 1 ? active.value + 1 : active.value\n const order = [...options.slice(from), ...options.slice(0, from)]\n const found = order.find(\n (option) => !option.disabled && option.label.toLowerCase().startsWith(typed.toLowerCase()),\n )\n\n if (found) moveTo(options.indexOf(found))\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n const moves: Record<string, number> = {\n ArrowDown: active.value + 1,\n ArrowUp: active.value - 1,\n Home: 0,\n End: options.length - 1,\n }\n\n if (event.key in moves) {\n event.preventDefault()\n moveTo(moves[event.key]!)\n return\n }\n\n if (event.key === ' ' || event.key === 'Enter') {\n const option = options[active.value]\n if (!option) return\n event.preventDefault()\n select(option)\n return\n }\n\n // A single printable character: the typeahead every native list has.\n if (event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey) {\n jumpTo(event.key)\n }\n}\n</script>\n\n<template>\n <div\n class=\"rk-listbox control\"\n :style=\"{ maxHeight: height }\"\n role=\"listbox\"\n tabindex=\"0\"\n :aria-label=\"label\"\n :aria-multiselectable=\"mode === 'multiple' ? true : undefined\"\n :aria-activedescendant=\"options.length ? `${id}-${active}` : undefined\"\n @keydown=\"onKeydown\"\n >\n <!-- `aria-disabled` is omitted rather than `false`: an option that can be\n chosen says nothing about being disabled. -->\n <div\n v-for=\"(option, index) in options\"\n :id=\"`${id}-${index}`\"\n :key=\"option.value\"\n class=\"rk-listbox-option\"\n :class=\"{\n 'is-active': index === active,\n 'is-chosen': chosen.has(option.value),\n 'is-disabled': option.disabled,\n }\"\n role=\"option\"\n :aria-selected=\"chosen.has(option.value)\"\n :aria-disabled=\"option.disabled || undefined\"\n @click=\"((active = index), select(option))\"\n >\n <span class=\"min-w-0 flex-1\">\n <span class=\"rk-listbox-label\">{{ option.label }}</span>\n <span v-if=\"option.description\" class=\"rk-listbox-description\">{{\n option.description\n }}</span>\n </span>\n\n <Check v-if=\"chosen.has(option.value)\" class=\"size-4 shrink-0\" aria-hidden=\"true\" />\n </div>\n\n <p v-if=\"options.length === 0\" class=\"rk-listbox-empty\" />\n </div>\n</template>\n\n<style scoped>\n.rk-listbox {\n overflow-y: auto;\n border-radius: var(--radius-card);\n padding: 0.25rem;\n}\n\n.rk-listbox:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 1px;\n}\n\n.rk-listbox-option {\n display: flex;\n cursor: pointer;\n align-items: center;\n gap: 0.625rem;\n border-radius: var(--radius-cell);\n padding: 0.5rem 0.625rem;\n font-size: 0.875rem;\n color: var(--color-ink);\n}\n\n/* The current row shows only while the list has focus. A list that is not\n being used has no \"current\" row, and a highlight left behind on one reads\n as a selection — which is exactly what it is not. */\n.rk-listbox:focus-within .rk-listbox-option.is-active {\n background: var(--color-muted);\n}\n\n/* The chosen row is its tinted ground, its weight and its tick — not a\n primary-coloured label on a ten per cent tint of primary, which is the\n same fault `BaseBadge` had: a faint wash of a colour is still nearly that\n colour, and the pair measured 4.16:1. */\n.rk-listbox-option.is-chosen {\n color: var(--color-ink);\n font-weight: 500;\n}\n\n/* Chosen is the tick and the tint, and it holds whether the list is in use\n or not: the current option and the chosen one are different things. */\n.rk-listbox-option.is-chosen {\n background: color-mix(in oklab, var(--color-primary) 10%, transparent);\n}\n\n.rk-listbox:focus-within .rk-listbox-option.is-chosen.is-active {\n background: color-mix(in oklab, var(--color-primary) 20%, transparent);\n}\n\n.rk-listbox-option.is-disabled {\n cursor: not-allowed;\n opacity: 0.45;\n}\n\n.rk-listbox-label {\n display: block;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rk-listbox-description {\n display: block;\n font-size: 0.75rem;\n color: var(--color-ink-soft);\n}\n\n.rk-listbox-empty {\n padding: 1rem;\n}\n</style>\n","<script setup lang=\"ts\" generic=\"V extends string, M extends 'single' | 'multiple' = 'single'\">\nimport { Check } from 'lucide-vue-next'\nimport { computed, ref, useId } from 'vue'\n\nimport { useBoundValue } from '../composables/use-bound-value'\n\nexport interface ListboxOption<V extends string> {\n value: V\n /** Already translated. */\n label: string\n /** A line under the label. */\n description?: string | undefined\n disabled?: boolean | undefined\n}\n\n/**\n * A list you choose from, open on the page.\n *\n * Between `BaseRadioGroup` and `BaseSelect`: a radio group is a handful of\n * options that are all visible and all equal; a select opens the platform's\n * own picker; this is a scrollable list of many, choosing one or several,\n * that stays on screen — a list of accounts, of tags, of files to act on.\n *\n * One Tab stop. The list itself takes focus and `aria-activedescendant`\n * says which option is current, so the arrows move without thirty stops in\n * the tab order. Typing a letter jumps to the next option that starts with\n * it, which is how every native list has always worked and the first thing\n * people try.\n *\n * In `single` mode the selection follows the arrows, because that is what a\n * listbox does; in `multiple` mode Space and Enter toggle the current one.\n */\nconst {\n modelValue = undefined,\n options,\n label,\n mode = 'single' as M,\n height = '14rem',\n} = defineProps<{\n /** The chosen option, or options, with `v-model`. */\n modelValue?: Value | undefined\n options: readonly ListboxOption<V>[]\n /** The list's accessible name. */\n label: string\n /** One option, or any number of them. */\n mode?: M | undefined\n /** How tall before it scrolls. Any CSS length. */\n height?: string | undefined\n}>()\n\n/* Multiple always hands back an array — toggling one produces a list, never\n nothing — so only the single value can be undefined. */\ntype Value = M extends 'multiple' ? V[] : V | undefined\n\n/* Declared by hand rather than with defineModel, which cannot both accept\n `undefined` and promise never to emit it. See `use-bound-value.ts`. */\nconst emit = defineEmits<{ 'update:modelValue': [value: Value] }>()\nconst model = useBoundValue<Value>(\n () => modelValue as Value | undefined,\n (value) => emit('update:modelValue', value),\n)\n\nconst id = useId()\nconst active = ref(0)\n\nlet typed = ''\nlet typedAt = 0\n\nconst chosen = computed(() => {\n const value = model.value\n if (Array.isArray(value)) return new Set<V>(value)\n\n return new Set<V>(value === undefined ? [] : [value as V])\n})\n\nfunction select(option: ListboxOption<V>) {\n if (option.disabled) return\n\n if (mode === 'multiple') {\n const next = new Set(chosen.value)\n if (next.has(option.value)) next.delete(option.value)\n else next.add(option.value)\n\n // In the order of the options, so the value reads the same however the\n // reader got there.\n model.value = options\n .map((one) => one.value)\n .filter((value) => next.has(value)) as typeof model.value\n return\n }\n\n model.value = option.value as typeof model.value\n}\n\nfunction moveTo(index: number) {\n if (options.length === 0) return\n\n active.value = Math.max(0, Math.min(options.length - 1, index))\n const option = options[active.value]\n\n // Single: the selection follows the focus, which is what a listbox does.\n if (mode === 'single' && option && !option.disabled) select(option)\n\n document.getElementById(`${id}-${active.value}`)?.scrollIntoView?.({ block: 'nearest' })\n}\n\n/** The next option after the current one that starts with what was typed. */\nfunction jumpTo(letter: string) {\n const now = Date.now()\n typed = now - typedAt > 700 ? letter : typed + letter\n typedAt = now\n\n const from = typed.length === 1 ? active.value + 1 : active.value\n const order = [...options.slice(from), ...options.slice(0, from)]\n const found = order.find(\n (option) => !option.disabled && option.label.toLowerCase().startsWith(typed.toLowerCase()),\n )\n\n if (found) moveTo(options.indexOf(found))\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n const moves: Record<string, number> = {\n ArrowDown: active.value + 1,\n ArrowUp: active.value - 1,\n Home: 0,\n End: options.length - 1,\n }\n\n if (event.key in moves) {\n event.preventDefault()\n moveTo(moves[event.key]!)\n return\n }\n\n if (event.key === ' ' || event.key === 'Enter') {\n const option = options[active.value]\n if (!option) return\n event.preventDefault()\n select(option)\n return\n }\n\n // A single printable character: the typeahead every native list has.\n if (event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey) {\n jumpTo(event.key)\n }\n}\n</script>\n\n<template>\n <div\n class=\"rk-listbox control\"\n :style=\"{ maxHeight: height }\"\n role=\"listbox\"\n tabindex=\"0\"\n :aria-label=\"label\"\n :aria-multiselectable=\"mode === 'multiple' ? true : undefined\"\n :aria-activedescendant=\"options.length ? `${id}-${active}` : undefined\"\n @keydown=\"onKeydown\"\n >\n <!-- `aria-disabled` is omitted rather than `false`: an option that can be\n chosen says nothing about being disabled. -->\n <div\n v-for=\"(option, index) in options\"\n :id=\"`${id}-${index}`\"\n :key=\"option.value\"\n class=\"rk-listbox-option\"\n :class=\"{\n 'is-active': index === active,\n 'is-chosen': chosen.has(option.value),\n 'is-disabled': option.disabled,\n }\"\n role=\"option\"\n :aria-selected=\"chosen.has(option.value)\"\n :aria-disabled=\"option.disabled || undefined\"\n @click=\"((active = index), select(option))\"\n >\n <span class=\"min-w-0 flex-1\">\n <span class=\"rk-listbox-label\">{{ option.label }}</span>\n <span v-if=\"option.description\" class=\"rk-listbox-description\">{{\n option.description\n }}</span>\n </span>\n\n <Check v-if=\"chosen.has(option.value)\" class=\"size-4 shrink-0\" aria-hidden=\"true\" />\n </div>\n\n <p v-if=\"options.length === 0\" class=\"rk-listbox-empty\" />\n </div>\n</template>\n\n<style scoped>\n.rk-listbox {\n overflow-y: auto;\n border-radius: var(--radius-card);\n padding: 0.25rem;\n}\n\n.rk-listbox:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 1px;\n}\n\n.rk-listbox-option {\n display: flex;\n cursor: pointer;\n align-items: center;\n gap: 0.625rem;\n border-radius: var(--radius-cell);\n padding: 0.5rem 0.625rem;\n font-size: 0.875rem;\n color: var(--color-ink);\n}\n\n/* The current row shows only while the list has focus. A list that is not\n being used has no \"current\" row, and a highlight left behind on one reads\n as a selection — which is exactly what it is not. */\n.rk-listbox:focus-within .rk-listbox-option.is-active {\n background: var(--color-muted);\n}\n\n/* The chosen row is its tinted ground, its weight and its tick — not a\n primary-coloured label on a ten per cent tint of primary, which is the\n same fault `BaseBadge` had: a faint wash of a colour is still nearly that\n colour, and the pair measured 4.16:1. */\n.rk-listbox-option.is-chosen {\n color: var(--color-ink);\n font-weight: 500;\n}\n\n/* Chosen is the tick and the tint, and it holds whether the list is in use\n or not: the current option and the chosen one are different things. */\n.rk-listbox-option.is-chosen {\n background: color-mix(in oklab, var(--color-primary) 10%, transparent);\n}\n\n.rk-listbox:focus-within .rk-listbox-option.is-chosen.is-active {\n background: color-mix(in oklab, var(--color-primary) 20%, transparent);\n}\n\n.rk-listbox-option.is-disabled {\n cursor: not-allowed;\n opacity: 0.45;\n}\n\n.rk-listbox-label {\n display: block;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rk-listbox-description {\n display: block;\n font-size: 0.75rem;\n color: var(--color-ink-soft);\n}\n\n.rk-listbox-empty {\n padding: 1rem;\n}\n</style>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BaseListbox-tS7ZpcoF.js","names":[],"sources":["../src/composables/use-drag-scroll.ts","../src/composables/use-media-query.ts","../src/composables/use-anchored-panel.ts","../src/composables/use-menu-keys.ts","../src/components/BaseSkeleton.vue","../src/components/BaseSkeleton.vue","../src/utils/focusable.ts","../src/components/ScrollArea.vue","../src/components/ScrollArea.vue","../src/components/BaseListbox.vue","../src/components/BaseListbox.vue"],"sourcesContent":["import { onScopeDispose, watch } from 'vue'\nimport type { Ref } from 'vue'\n\n/** Movement before a press counts as a drag rather than a tap. */\nconst DRAG_THRESHOLD_PX = 6\n\n/**\n * Drag-to-scroll for a horizontally scrolling element.\n *\n * The app puts `touch-action: pan-y` on the page content so the tab-swipe\n * gesture keeps its pointer events — the browser never claims a horizontal\n * drag, which also means it never pans this element natively. Rather than give\n * that up, horizontal scrolling is driven here.\n *\n * @param target - The scroll container.\n * @returns `didDrag`, so a click handler can ignore the press that ended a drag.\n *\n * @example\n * ```ts\n * const scroller = ref<HTMLElement | null>(null)\n * const { didDrag } = useDragScroll(scroller)\n *\n * function onClick() {\n * if (didDrag()) return\n * // …treat as a tap\n * }\n * ```\n */\nexport function useDragScroll(target: Ref<HTMLElement | null>) {\n let pointerId: number | null = null\n let startX = 0\n let startScroll = 0\n let dragged = false\n\n function onPointerDown(event: PointerEvent) {\n const element = target.value\n if (!element || event.pointerType === 'mouse') return\n\n pointerId = event.pointerId\n startX = event.clientX\n startScroll = element.scrollLeft\n dragged = false\n }\n\n function onPointerMove(event: PointerEvent) {\n const element = target.value\n if (!element || event.pointerId !== pointerId) return\n\n const dx = event.clientX - startX\n if (!dragged && Math.abs(dx) < DRAG_THRESHOLD_PX) return\n\n // Capture only once the gesture is clearly horizontal, so a vertical scroll\n // that happens to start here still belongs to the page.\n if (!dragged) {\n dragged = true\n element.setPointerCapture(event.pointerId)\n }\n\n element.scrollLeft = startScroll - dx\n }\n\n function onPointerUp(event: PointerEvent) {\n const element = target.value\n if (element?.hasPointerCapture(event.pointerId)) {\n element.releasePointerCapture(event.pointerId)\n }\n\n pointerId = null\n }\n\n function bind(element: HTMLElement) {\n element.addEventListener('pointerdown', onPointerDown)\n element.addEventListener('pointermove', onPointerMove)\n element.addEventListener('pointerup', onPointerUp)\n element.addEventListener('pointercancel', onPointerUp)\n }\n\n function unbind(element: HTMLElement) {\n element.removeEventListener('pointerdown', onPointerDown)\n element.removeEventListener('pointermove', onPointerMove)\n element.removeEventListener('pointerup', onPointerUp)\n element.removeEventListener('pointercancel', onPointerUp)\n }\n\n watch(\n target,\n (element, previous) => {\n if (previous) unbind(previous)\n if (element) bind(element)\n },\n { immediate: true },\n )\n\n onScopeDispose(() => {\n if (target.value) unbind(target.value)\n })\n\n return { didDrag: () => dragged }\n}\n","import { onBeforeUnmount, onMounted, ref } from 'vue'\n\n/**\n * Whether a media query matches, kept up to date.\n *\n * Starts false and resolves on mount, which is deliberate: this is the one\n * place a component is tempted to branch on viewport during render, and doing\n * that under prerendering produces HTML built for a screen the server does not\n * have. Hydration then swaps it and the page jumps. False first, correct a\n * frame later, no jump — and a layout that reads badly at `false` is a layout\n * with a mobile-first bug worth knowing about.\n *\n * Guarded for the server for the same reason the rest of the kit is: this\n * package has to be importable in Node, and `matchMedia` does not exist there.\n *\n * @example\n * ```ts\n * const wide = useMediaQuery('(min-width: 64rem)')\n * ```\n */\nexport function useMediaQuery(query: string) {\n const matches = ref(false)\n\n let list: MediaQueryList | undefined\n\n function update(event: MediaQueryList | MediaQueryListEvent) {\n matches.value = event.matches\n }\n\n onMounted(() => {\n if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return\n\n list = window.matchMedia(query)\n update(list)\n list.addEventListener('change', update)\n })\n\n onBeforeUnmount(() => {\n list?.removeEventListener('change', update)\n })\n\n return matches\n}\n","import { nextTick, onBeforeUnmount, ref, watch } from 'vue'\nimport type { Ref } from 'vue'\n\n/** Above the anchor or below it. */\nexport type PanelSide = 'top' | 'bottom'\n\nexport interface AnchoredPanelOptions {\n /** The element the panel is positioned against. */\n root: Ref<HTMLElement | null>\n /** The panel itself, measured once it is on screen. */\n panel: Ref<HTMLElement | null>\n /** Whether it is showing. */\n open: Ref<boolean>\n /** Which way it opens before anything is measured. */\n side?: PanelSide\n /** How close to the window edge it may come, in pixels. */\n margin?: number\n}\n\n/**\n * Keeping a panel on screen.\n *\n * Two corrections, in this order, because they are not equal. **Flip**: a\n * panel that does not fit below its anchor opens above it when there is\n * more room there. **Shift**: whatever is left of it past the side of the\n * window is slid back in.\n *\n * A panel taller than the window fits neither way, and then the rule is\n * still \"more room wins\" — which shows more of it, and leaves the rest\n * scrolled to rather than gone. `BasePopover` carried a comment saying it\n * kept the side it asked for in that case; it never did, and nothing\n * checked, because the arithmetic had never been given a layout to work\n * on. The comment is gone and the behaviour is the one under test.\n *\n * This was written inside `BasePopover`, which is how the kit came to ship\n * a popover that flips and a menu that does not. `BaseMenu`'s panel was\n * pinned under its trigger with `top: 100%`, so a menu button near the\n * bottom of a window — a row action, the `…` on a card, anything on a\n * phone — opened a list that ran off the bottom of the screen. Nothing said\n * so: it rendered, it passed axe, and the items were all there.\n *\n * Re-measured while open, because the page can scroll or the window change\n * size underneath, and a panel that stays where it opened ends up attached\n * to nothing. The scroll listener captures, so a scroll in any ancestor\n * counts, and it is passive-by-omission: it only reads.\n */\nexport function useAnchoredPanel({\n root,\n panel,\n open,\n side = 'bottom',\n margin = 8,\n}: AnchoredPanelOptions) {\n /** Where it actually opened, after measuring. */\n const placed = ref<PanelSide>(side)\n /** Pixels slid sideways to stay on screen. */\n const shift = ref(0)\n\n function place() {\n if (!panel.value || !root.value || typeof window === 'undefined') return\n\n const rect = panel.value.getBoundingClientRect()\n const anchor = root.value.getBoundingClientRect()\n const below = window.innerHeight - anchor.bottom\n const above = anchor.top\n\n if (side === 'bottom' && rect.height > below && above > below) placed.value = 'top'\n if (side === 'top' && rect.height > above && below > above) placed.value = 'bottom'\n\n if (rect.right > window.innerWidth - margin) {\n shift.value = window.innerWidth - margin - rect.right\n }\n if (rect.left + shift.value < margin) shift.value = margin - rect.left\n }\n\n function listen(on: boolean) {\n if (typeof window === 'undefined') return\n\n const method = on ? 'addEventListener' : 'removeEventListener'\n window[method]('resize', place)\n window[method]('scroll', place, true)\n }\n\n /** Measured where it asked to be, rather than where it ended up last time. */\n function reset() {\n placed.value = side\n shift.value = 0\n }\n\n /*\n * The opening is handled here rather than left to each caller, because\n * three of them open from more than one place: `BaseCombobox` opens on a\n * keystroke, on an arrow and on a press, and a panel measured at two of\n * those three is a panel that is usually right.\n *\n * Reset before the tick, measure after it: the panel has no size until\n * it has rendered, and it must be measured where it asked to be rather\n * than where it ended up the last time it was open.\n */\n watch(\n open,\n async (isOpen) => {\n listen(isOpen)\n if (!isOpen) return\n\n reset()\n await nextTick()\n place()\n },\n { immediate: true },\n )\n\n onBeforeUnmount(() => listen(false))\n\n return { placed, shift, place, reset }\n}\n","import type { Ref } from 'vue'\n\n/**\n * The keyboard contract `role=\"menu\"` promises.\n *\n * Declaring `role=\"menu\"` is a promise to a screen reader: the arrows move\n * through the items, Home and End go to the ends, Escape closes, and Tab\n * *leaves* rather than cycling inside. That last one is the single place a\n * menu differs from a dialog, and getting it backwards traps somebody in a\n * list of links.\n *\n * It lives here because a menu anchored to a button and a menu anchored to\n * wherever the pointer was are the same keyboard and two different\n * positions. Only the position is worth writing twice.\n */\nexport interface MenuKeysOptions {\n /** The element holding the `[role=\"menuitem\"]` children. */\n panel: Ref<HTMLElement | null>\n /** Close it — Escape, and Tab on the way out. */\n onClose: () => void\n}\n\nexport function useMenuKeys({ panel, onClose }: MenuKeysOptions) {\n const items = (): HTMLElement[] =>\n panel.value ? Array.from(panel.value.querySelectorAll<HTMLElement>('[role=\"menuitem\"]')) : []\n\n function focusAt(index: number) {\n const list = items()\n if (list.length === 0) return\n\n // Wrapping, because a list with no edges is faster than one you fall off.\n list[(index + list.length) % list.length]?.focus()\n }\n\n const currentIndex = () => items().indexOf(document.activeElement as HTMLElement)\n\n /** Returns whether the key was one of the menu's own. */\n function onKeydown(event: KeyboardEvent): boolean {\n switch (event.key) {\n case 'Escape':\n event.preventDefault()\n onClose()\n return true\n case 'ArrowDown':\n event.preventDefault()\n focusAt(currentIndex() + 1)\n return true\n case 'ArrowUp':\n event.preventDefault()\n focusAt(currentIndex() - 1)\n return true\n case 'Home':\n event.preventDefault()\n focusAt(0)\n return true\n case 'End':\n event.preventDefault()\n focusAt(items().length - 1)\n return true\n case 'Tab':\n onClose()\n return true\n default:\n return false\n }\n }\n\n return { items, focusAt, currentIndex, onKeydown }\n}\n","<script setup lang=\"ts\">\n/**\n * One grey box standing in for content that has not arrived.\n *\n * `SkeletonList` is rows of these for a list; this is the primitive, for the\n * shapes a list does not cover — an avatar, a heading, a chart.\n *\n * A height is a CSS length, never a class: `h-4` inside a component the app\n * does not control would render at zero the day that utility is not in the\n * app's stylesheet, which is exactly how every skeleton in one app came out\n * invisible.\n *\n * It is hidden from assistive tech. The thing that is loading says so —\n * through `aria-busy`, or a status message — and a screen reader reading\n * \"blank, blank, blank\" helps nobody.\n */\nconst {\n width = '100%',\n height = '1rem',\n shape = 'block',\n} = defineProps<{\n /** Any CSS length: `12rem`, `60%`, `8ch`. */\n width?: string | undefined\n height?: string | undefined\n /** `circle` makes a round one and squares the width to the height. */\n shape?: 'block' | 'text' | 'circle' | undefined\n}>()\n</script>\n\n<template>\n <span\n class=\"rk-skeleton\"\n :class=\"`is-${shape}`\"\n :style=\"{ width: shape === 'circle' ? height : width, height }\"\n aria-hidden=\"true\"\n />\n</template>\n\n<style scoped>\n.rk-skeleton {\n display: block;\n flex-shrink: 0;\n background: var(--color-muted);\n animation: rk-skeleton-pulse 1.6s ease-in-out infinite;\n}\n\n.rk-skeleton.is-block {\n border-radius: var(--radius-cell);\n}\n\n/* A line of text, rounded like one and a little short of its box. */\n.rk-skeleton.is-text {\n border-radius: 9999px;\n}\n\n.rk-skeleton.is-circle {\n border-radius: 9999px;\n}\n\n@keyframes rk-skeleton-pulse {\n 50% {\n opacity: 0.55;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .rk-skeleton {\n animation: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * One grey box standing in for content that has not arrived.\n *\n * `SkeletonList` is rows of these for a list; this is the primitive, for the\n * shapes a list does not cover — an avatar, a heading, a chart.\n *\n * A height is a CSS length, never a class: `h-4` inside a component the app\n * does not control would render at zero the day that utility is not in the\n * app's stylesheet, which is exactly how every skeleton in one app came out\n * invisible.\n *\n * It is hidden from assistive tech. The thing that is loading says so —\n * through `aria-busy`, or a status message — and a screen reader reading\n * \"blank, blank, blank\" helps nobody.\n */\nconst {\n width = '100%',\n height = '1rem',\n shape = 'block',\n} = defineProps<{\n /** Any CSS length: `12rem`, `60%`, `8ch`. */\n width?: string | undefined\n height?: string | undefined\n /** `circle` makes a round one and squares the width to the height. */\n shape?: 'block' | 'text' | 'circle' | undefined\n}>()\n</script>\n\n<template>\n <span\n class=\"rk-skeleton\"\n :class=\"`is-${shape}`\"\n :style=\"{ width: shape === 'circle' ? height : width, height }\"\n aria-hidden=\"true\"\n />\n</template>\n\n<style scoped>\n.rk-skeleton {\n display: block;\n flex-shrink: 0;\n background: var(--color-muted);\n animation: rk-skeleton-pulse 1.6s ease-in-out infinite;\n}\n\n.rk-skeleton.is-block {\n border-radius: var(--radius-cell);\n}\n\n/* A line of text, rounded like one and a little short of its box. */\n.rk-skeleton.is-text {\n border-radius: 9999px;\n}\n\n.rk-skeleton.is-circle {\n border-radius: 9999px;\n}\n\n@keyframes rk-skeleton-pulse {\n 50% {\n opacity: 0.55;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .rk-skeleton {\n animation: none;\n }\n}\n</style>\n","/**\n * What the keyboard can reach, as one selector.\n *\n * Three parts of the kit ask this question and each had written its own\n * answer, which is how three answers came to differ: the dialog trap and\n * the popover agreed, and `ScrollArea` left out `:not([disabled])`, so a\n * box holding nothing but disabled buttons counted as reachable and was\n * denied the focus stop that would have been its only way in.\n *\n * `summary` belongs here for the same reason it belonged in `ScrollArea`:\n * a `<details>` heading takes focus, and a dialog whose trap skipped one\n * would let Tab out of itself.\n *\n * Two things it deliberately does not do. It does not check whether an\n * element is visible or inside an `inert` subtree — a selector cannot, and\n * every caller here is already scoped to something on screen. And it is\n * not what a roving-tabindex container should use to find its own\n * controls: `[tabindex]:not([tabindex=\"-1\"])` matches exactly the one\n * control such a container has already put in the tab order, so it would\n * find one item and then stop. `BaseToolbar` says so where it keeps its\n * own.\n */\nexport const FOCUSABLE =\n 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), summary, [tabindex]:not([tabindex=\"-1\"])'\n\n/** The reachable elements inside `root`, in document order. */\nexport function focusableWithin(root: Element | null | undefined): HTMLElement[] {\n return Array.from(root?.querySelectorAll<HTMLElement>(FOCUSABLE) ?? [])\n}\n","<script setup lang=\"ts\">\nimport { computed, onScopeDispose, ref, useTemplateRef, watchEffect } from 'vue'\n\nimport { useDragScroll } from '../composables/use-drag-scroll'\nimport { FOCUSABLE } from '../utils/focusable'\n\n/**\n * A box that scrolls, with the two things a hand-written one leaves out.\n *\n * ## The edge fade is the affordance `no-scrollbar` takes away\n *\n * Every app here hides the scrollbar on a horizontal row — a strip of\n * months, a row of filter chips — because a native bar under a 40px row is\n * louder than the row. What goes with it is the only sign on screen that\n * there is anything further along. So the fades are driven by the actual\n * scroll position: one appears at an edge exactly when there is content\n * past it, and both are gone when everything fits.\n *\n * ## A focus stop, but only when there is no other way in\n *\n * A box that scrolls and cannot take focus cannot be scrolled by a\n * keyboard, so the far end is simply unreachable. That is the fault\n * `CodeBlock` was written for, and it is not about code: it is true of any\n * scrolling box whose contents are not focusable.\n *\n * But it is *only* true of those. A row of buttons already moves under the\n * keyboard — Tab scrolls the next one into view — and giving that row a\n * stop of its own costs a press for nothing, on every row. So this checks:\n * it becomes a named region when it overflows **and** holds nothing\n * focusable, and stays out of the tab order otherwise.\n *\n * The check runs when the box or its contents resize, which covers a list\n * that arrives from a server. A swap that changes neither size — the same\n * number of rows, buttons turned into plain text — will not be noticed;\n * that is a trade against watching every mutation of a scroll container,\n * and the shape it misses is a rare one.\n */\nconst {\n label,\n axis = 'y',\n scrollbar = 'auto',\n fade = true,\n drag = false,\n} = defineProps<{\n /**\n * Names the box, e.g. \"Categories\".\n *\n * Required because this becomes a region a reader can land in, and an\n * unnamed region is announced as nothing at all. It is only used when\n * that happens, so it costs nothing in the common case.\n */\n label: string\n /** Which way it scrolls. */\n axis?: 'x' | 'y' | 'both' | undefined\n /** `hidden` takes the native bar away; the fades stay either way. */\n scrollbar?: 'auto' | 'hidden' | undefined\n /** The gradient at an edge with more content past it. */\n fade?: boolean | undefined\n /** Drag to scroll sideways, for a page that has claimed horizontal gestures. */\n drag?: boolean | undefined\n}>()\n\ndefineSlots<{ default: () => unknown }>()\n\n/* The scroll event does not bubble, so a parent cannot listen for it on the\n wrapper. `VirtualList` needs it, and needs the element it came from, so\n both are handed out rather than left to a template ref into our markup. */\nconst emit = defineEmits<{ scroll: [event: Event] }>()\n\nconst viewport = useTemplateRef<HTMLElement>('viewport')\n\nconst horizontal = computed(() => axis === 'x' || axis === 'both')\nconst vertical = computed(() => axis === 'y' || axis === 'both')\n\nconst start = ref({ x: false, y: false })\nconst end = ref({ x: false, y: false })\nconst overflows = ref(false)\nconst hasFocusable = ref(true)\n\nfunction measure() {\n const element = viewport.value\n if (!element) return\n\n const overX = element.scrollWidth - element.clientWidth\n const overY = element.scrollHeight - element.clientHeight\n\n /* A fractional pixel of overflow is a rounding artefact, not content:\n without this every box that fits exactly draws a fade at one end. */\n const slack = 1\n\n /* `scrollLeft` counts down from zero in a right-to-left box, so the\n distance travelled is its magnitude either way. That is what makes\n \"start\" and \"end\" below mean the same thing in both directions. */\n const alongX = Math.abs(element.scrollLeft)\n\n start.value = { x: alongX > slack, y: element.scrollTop > slack }\n end.value = { x: overX - alongX > slack, y: overY - element.scrollTop > slack }\n\n overflows.value = (horizontal.value && overX > slack) || (vertical.value && overY > slack)\n hasFocusable.value = element.querySelector(FOCUSABLE) !== null\n}\n\nfunction onScroll(event: Event) {\n measure()\n emit('scroll', event)\n}\n\n/** The box itself is the way in only when nothing inside it is. */\nconst isStop = computed(() => overflows.value && !hasFocusable.value)\n\nlet observer: ResizeObserver | null = null\n\nwatchEffect(() => {\n observer?.disconnect()\n observer = null\n\n const element = viewport.value\n if (!element || typeof ResizeObserver === 'undefined') return\n\n observer = new ResizeObserver(measure)\n observer.observe(element)\n // The contents too: a list that grows changes what there is to scroll\n // without the box itself changing size at all.\n for (const child of element.children) observer.observe(child)\n\n measure()\n})\n\nonScopeDispose(() => observer?.disconnect())\n\nuseDragScroll(computed(() => (drag ? viewport.value : null)))\n\ndefineExpose({\n /** The element that actually scrolls, for a parent that has to measure it. */\n viewport,\n})\n</script>\n\n<template>\n <div class=\"rk-scroll\">\n <div\n ref=\"viewport\"\n class=\"rk-scroll-viewport\"\n :class=\"[`is-${axis}`, { 'no-scrollbar': scrollbar === 'hidden' }]\"\n :tabindex=\"isStop ? 0 : undefined\"\n :role=\"isStop ? 'region' : undefined\"\n :aria-label=\"isStop ? label : undefined\"\n @scroll=\"onScroll\"\n >\n <slot />\n </div>\n\n <!-- Decoration, and never in the way of a press: the box under them is\n what scrolls. -->\n <template v-if=\"fade\">\n <span v-if=\"horizontal && start.x\" class=\"rk-scroll-fade is-start-x\" aria-hidden=\"true\" />\n <span v-if=\"horizontal && end.x\" class=\"rk-scroll-fade is-end-x\" aria-hidden=\"true\" />\n <span v-if=\"vertical && start.y\" class=\"rk-scroll-fade is-start-y\" aria-hidden=\"true\" />\n <span v-if=\"vertical && end.y\" class=\"rk-scroll-fade is-end-y\" aria-hidden=\"true\" />\n </template>\n </div>\n</template>\n\n<style scoped>\n/*\n * A flex column, so a height written on this box reaches the box that\n * scrolls.\n *\n * The caller's class lands here, on the wrapper, because that is the\n * element the component renders outermost — so `class=\"max-h-56\"` is the\n * obvious and only thing to write. It used to do nothing. The viewport\n * carried `max-height: 100%`, and a percentage height resolves against the\n * parent's *height*, which is `auto` here; a parent with only a\n * `max-height` gives it nothing to resolve against, so the rule was inert\n * and the viewport grew to the full height of its contents.\n *\n * Visually that spills. For `VirtualList` it is worse than spilling: the\n * window is sized from `clientHeight`, so an unconstrained viewport reports\n * the height of the whole list, every row is \"near the viewport\", and five\n * thousand of them render at once. That is the component's whole purpose,\n * inverted, by the one line every caller writes.\n *\n * `flex: 1 1 auto` with `min-height: 0` is what lets the item shrink below\n * its content, which is the part that is easy to leave out: a flex item's\n * default `min-height: auto` refuses to go below its content and the box\n * spills exactly as before.\n */\n.rk-scroll {\n position: relative;\n display: flex;\n flex-direction: column;\n min-width: 0;\n min-height: 0;\n}\n\n.rk-scroll-viewport {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 0;\n max-width: 100%;\n}\n\n.rk-scroll-viewport.is-x {\n overflow-x: auto;\n overflow-y: hidden;\n}\n\n.rk-scroll-viewport.is-y {\n overflow-x: hidden;\n overflow-y: auto;\n}\n\n.rk-scroll-viewport.is-both {\n overflow: auto;\n}\n\n.rk-scroll-viewport:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n/* The fade paints over whatever the box sits on, so the colour is a hook:\n a scroll area on a card sets it to the card's ground. */\n.rk-scroll-fade {\n position: absolute;\n z-index: 1;\n pointer-events: none;\n --rk-scroll-fade: var(--scroll-fade-color, var(--color-canvas));\n}\n\n.rk-scroll-fade.is-start-x,\n.rk-scroll-fade.is-end-x {\n inset-block: 0;\n width: 1.5rem;\n}\n\n.rk-scroll-fade.is-start-y,\n.rk-scroll-fade.is-end-y {\n inset-inline: 0;\n height: 1.5rem;\n}\n\n.rk-scroll-fade.is-start-x {\n inset-inline-start: 0;\n background: linear-gradient(to right, var(--rk-scroll-fade), transparent);\n}\n\n.rk-scroll-fade.is-end-x {\n inset-inline-end: 0;\n background: linear-gradient(to left, var(--rk-scroll-fade), transparent);\n}\n\n.rk-scroll-fade.is-start-y {\n inset-block-start: 0;\n background: linear-gradient(to bottom, var(--rk-scroll-fade), transparent);\n}\n\n.rk-scroll-fade.is-end-y {\n inset-block-end: 0;\n background: linear-gradient(to top, var(--rk-scroll-fade), transparent);\n}\n\n/* The insets are logical, so they land on the correct side on their own.\n A gradient has no logical direction, so these two flip by hand. */\n[dir='rtl'] .rk-scroll-fade.is-start-x {\n background: linear-gradient(to left, var(--rk-scroll-fade), transparent);\n}\n\n[dir='rtl'] .rk-scroll-fade.is-end-x {\n background: linear-gradient(to right, var(--rk-scroll-fade), transparent);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, onScopeDispose, ref, useTemplateRef, watchEffect } from 'vue'\n\nimport { useDragScroll } from '../composables/use-drag-scroll'\nimport { FOCUSABLE } from '../utils/focusable'\n\n/**\n * A box that scrolls, with the two things a hand-written one leaves out.\n *\n * ## The edge fade is the affordance `no-scrollbar` takes away\n *\n * Every app here hides the scrollbar on a horizontal row — a strip of\n * months, a row of filter chips — because a native bar under a 40px row is\n * louder than the row. What goes with it is the only sign on screen that\n * there is anything further along. So the fades are driven by the actual\n * scroll position: one appears at an edge exactly when there is content\n * past it, and both are gone when everything fits.\n *\n * ## A focus stop, but only when there is no other way in\n *\n * A box that scrolls and cannot take focus cannot be scrolled by a\n * keyboard, so the far end is simply unreachable. That is the fault\n * `CodeBlock` was written for, and it is not about code: it is true of any\n * scrolling box whose contents are not focusable.\n *\n * But it is *only* true of those. A row of buttons already moves under the\n * keyboard — Tab scrolls the next one into view — and giving that row a\n * stop of its own costs a press for nothing, on every row. So this checks:\n * it becomes a named region when it overflows **and** holds nothing\n * focusable, and stays out of the tab order otherwise.\n *\n * The check runs when the box or its contents resize, which covers a list\n * that arrives from a server. A swap that changes neither size — the same\n * number of rows, buttons turned into plain text — will not be noticed;\n * that is a trade against watching every mutation of a scroll container,\n * and the shape it misses is a rare one.\n */\nconst {\n label,\n axis = 'y',\n scrollbar = 'auto',\n fade = true,\n drag = false,\n} = defineProps<{\n /**\n * Names the box, e.g. \"Categories\".\n *\n * Required because this becomes a region a reader can land in, and an\n * unnamed region is announced as nothing at all. It is only used when\n * that happens, so it costs nothing in the common case.\n */\n label: string\n /** Which way it scrolls. */\n axis?: 'x' | 'y' | 'both' | undefined\n /** `hidden` takes the native bar away; the fades stay either way. */\n scrollbar?: 'auto' | 'hidden' | undefined\n /** The gradient at an edge with more content past it. */\n fade?: boolean | undefined\n /** Drag to scroll sideways, for a page that has claimed horizontal gestures. */\n drag?: boolean | undefined\n}>()\n\ndefineSlots<{ default: () => unknown }>()\n\n/* The scroll event does not bubble, so a parent cannot listen for it on the\n wrapper. `VirtualList` needs it, and needs the element it came from, so\n both are handed out rather than left to a template ref into our markup. */\nconst emit = defineEmits<{ scroll: [event: Event] }>()\n\nconst viewport = useTemplateRef<HTMLElement>('viewport')\n\nconst horizontal = computed(() => axis === 'x' || axis === 'both')\nconst vertical = computed(() => axis === 'y' || axis === 'both')\n\nconst start = ref({ x: false, y: false })\nconst end = ref({ x: false, y: false })\nconst overflows = ref(false)\nconst hasFocusable = ref(true)\n\nfunction measure() {\n const element = viewport.value\n if (!element) return\n\n const overX = element.scrollWidth - element.clientWidth\n const overY = element.scrollHeight - element.clientHeight\n\n /* A fractional pixel of overflow is a rounding artefact, not content:\n without this every box that fits exactly draws a fade at one end. */\n const slack = 1\n\n /* `scrollLeft` counts down from zero in a right-to-left box, so the\n distance travelled is its magnitude either way. That is what makes\n \"start\" and \"end\" below mean the same thing in both directions. */\n const alongX = Math.abs(element.scrollLeft)\n\n start.value = { x: alongX > slack, y: element.scrollTop > slack }\n end.value = { x: overX - alongX > slack, y: overY - element.scrollTop > slack }\n\n overflows.value = (horizontal.value && overX > slack) || (vertical.value && overY > slack)\n hasFocusable.value = element.querySelector(FOCUSABLE) !== null\n}\n\nfunction onScroll(event: Event) {\n measure()\n emit('scroll', event)\n}\n\n/** The box itself is the way in only when nothing inside it is. */\nconst isStop = computed(() => overflows.value && !hasFocusable.value)\n\nlet observer: ResizeObserver | null = null\n\nwatchEffect(() => {\n observer?.disconnect()\n observer = null\n\n const element = viewport.value\n if (!element || typeof ResizeObserver === 'undefined') return\n\n observer = new ResizeObserver(measure)\n observer.observe(element)\n // The contents too: a list that grows changes what there is to scroll\n // without the box itself changing size at all.\n for (const child of element.children) observer.observe(child)\n\n measure()\n})\n\nonScopeDispose(() => observer?.disconnect())\n\nuseDragScroll(computed(() => (drag ? viewport.value : null)))\n\ndefineExpose({\n /** The element that actually scrolls, for a parent that has to measure it. */\n viewport,\n})\n</script>\n\n<template>\n <div class=\"rk-scroll\">\n <div\n ref=\"viewport\"\n class=\"rk-scroll-viewport\"\n :class=\"[`is-${axis}`, { 'no-scrollbar': scrollbar === 'hidden' }]\"\n :tabindex=\"isStop ? 0 : undefined\"\n :role=\"isStop ? 'region' : undefined\"\n :aria-label=\"isStop ? label : undefined\"\n @scroll=\"onScroll\"\n >\n <slot />\n </div>\n\n <!-- Decoration, and never in the way of a press: the box under them is\n what scrolls. -->\n <template v-if=\"fade\">\n <span v-if=\"horizontal && start.x\" class=\"rk-scroll-fade is-start-x\" aria-hidden=\"true\" />\n <span v-if=\"horizontal && end.x\" class=\"rk-scroll-fade is-end-x\" aria-hidden=\"true\" />\n <span v-if=\"vertical && start.y\" class=\"rk-scroll-fade is-start-y\" aria-hidden=\"true\" />\n <span v-if=\"vertical && end.y\" class=\"rk-scroll-fade is-end-y\" aria-hidden=\"true\" />\n </template>\n </div>\n</template>\n\n<style scoped>\n/*\n * A flex column, so a height written on this box reaches the box that\n * scrolls.\n *\n * The caller's class lands here, on the wrapper, because that is the\n * element the component renders outermost — so `class=\"max-h-56\"` is the\n * obvious and only thing to write. It used to do nothing. The viewport\n * carried `max-height: 100%`, and a percentage height resolves against the\n * parent's *height*, which is `auto` here; a parent with only a\n * `max-height` gives it nothing to resolve against, so the rule was inert\n * and the viewport grew to the full height of its contents.\n *\n * Visually that spills. For `VirtualList` it is worse than spilling: the\n * window is sized from `clientHeight`, so an unconstrained viewport reports\n * the height of the whole list, every row is \"near the viewport\", and five\n * thousand of them render at once. That is the component's whole purpose,\n * inverted, by the one line every caller writes.\n *\n * `flex: 1 1 auto` with `min-height: 0` is what lets the item shrink below\n * its content, which is the part that is easy to leave out: a flex item's\n * default `min-height: auto` refuses to go below its content and the box\n * spills exactly as before.\n */\n.rk-scroll {\n position: relative;\n display: flex;\n flex-direction: column;\n min-width: 0;\n min-height: 0;\n}\n\n.rk-scroll-viewport {\n flex: 1 1 auto;\n min-width: 0;\n min-height: 0;\n max-width: 100%;\n}\n\n.rk-scroll-viewport.is-x {\n overflow-x: auto;\n overflow-y: hidden;\n}\n\n.rk-scroll-viewport.is-y {\n overflow-x: hidden;\n overflow-y: auto;\n}\n\n.rk-scroll-viewport.is-both {\n overflow: auto;\n}\n\n.rk-scroll-viewport:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n/* The fade paints over whatever the box sits on, so the colour is a hook:\n a scroll area on a card sets it to the card's ground. */\n.rk-scroll-fade {\n position: absolute;\n z-index: 1;\n pointer-events: none;\n --rk-scroll-fade: var(--scroll-fade-color, var(--color-canvas));\n}\n\n.rk-scroll-fade.is-start-x,\n.rk-scroll-fade.is-end-x {\n inset-block: 0;\n width: 1.5rem;\n}\n\n.rk-scroll-fade.is-start-y,\n.rk-scroll-fade.is-end-y {\n inset-inline: 0;\n height: 1.5rem;\n}\n\n.rk-scroll-fade.is-start-x {\n inset-inline-start: 0;\n background: linear-gradient(to right, var(--rk-scroll-fade), transparent);\n}\n\n.rk-scroll-fade.is-end-x {\n inset-inline-end: 0;\n background: linear-gradient(to left, var(--rk-scroll-fade), transparent);\n}\n\n.rk-scroll-fade.is-start-y {\n inset-block-start: 0;\n background: linear-gradient(to bottom, var(--rk-scroll-fade), transparent);\n}\n\n.rk-scroll-fade.is-end-y {\n inset-block-end: 0;\n background: linear-gradient(to top, var(--rk-scroll-fade), transparent);\n}\n\n/* The insets are logical, so they land on the correct side on their own.\n A gradient has no logical direction, so these two flip by hand. */\n[dir='rtl'] .rk-scroll-fade.is-start-x {\n background: linear-gradient(to left, var(--rk-scroll-fade), transparent);\n}\n\n[dir='rtl'] .rk-scroll-fade.is-end-x {\n background: linear-gradient(to right, var(--rk-scroll-fade), transparent);\n}\n</style>\n","<script setup lang=\"ts\" generic=\"V extends string, M extends 'single' | 'multiple' = 'single'\">\nimport { Check } from 'lucide-vue-next'\nimport { computed, ref, useId } from 'vue'\n\nimport { useBoundValue } from '../composables/use-bound-value'\n\nexport interface ListboxOption<V extends string> {\n value: V\n /** Already translated. */\n label: string\n /** A line under the label. */\n description?: string | undefined\n disabled?: boolean | undefined\n}\n\n/**\n * A list you choose from, open on the page.\n *\n * Between `BaseRadioGroup` and `BaseSelect`: a radio group is a handful of\n * options that are all visible and all equal; a select opens the platform's\n * own picker; this is a scrollable list of many, choosing one or several,\n * that stays on screen — a list of accounts, of tags, of files to act on.\n *\n * One Tab stop. The list itself takes focus and `aria-activedescendant`\n * says which option is current, so the arrows move without thirty stops in\n * the tab order. Typing a letter jumps to the next option that starts with\n * it, which is how every native list has always worked and the first thing\n * people try.\n *\n * In `single` mode the selection follows the arrows, because that is what a\n * listbox does; in `multiple` mode Space and Enter toggle the current one.\n */\nconst {\n modelValue = undefined,\n options,\n label,\n mode = 'single' as M,\n height = '14rem',\n} = defineProps<{\n /** The chosen option, or options, with `v-model`. */\n modelValue?: Value | undefined\n options: readonly ListboxOption<V>[]\n /** The list's accessible name. */\n label: string\n /** One option, or any number of them. */\n mode?: M | undefined\n /** How tall before it scrolls. Any CSS length. */\n height?: string | undefined\n}>()\n\n/* Multiple always hands back an array — toggling one produces a list, never\n nothing — so only the single value can be undefined. */\ntype Value = M extends 'multiple' ? V[] : V | undefined\n\n/* Declared by hand rather than with defineModel, which cannot both accept\n `undefined` and promise never to emit it. See `use-bound-value.ts`. */\nconst emit = defineEmits<{ 'update:modelValue': [value: Value] }>()\nconst model = useBoundValue<Value>(\n () => modelValue as Value | undefined,\n (value) => emit('update:modelValue', value),\n)\n\nconst id = useId()\nconst active = ref(0)\n\nlet typed = ''\nlet typedAt = 0\n\nconst chosen = computed(() => {\n const value = model.value\n if (Array.isArray(value)) return new Set<V>(value)\n\n return new Set<V>(value === undefined ? [] : [value as V])\n})\n\nfunction select(option: ListboxOption<V>) {\n if (option.disabled) return\n\n if (mode === 'multiple') {\n const next = new Set(chosen.value)\n if (next.has(option.value)) next.delete(option.value)\n else next.add(option.value)\n\n // In the order of the options, so the value reads the same however the\n // reader got there.\n model.value = options\n .map((one) => one.value)\n .filter((value) => next.has(value)) as typeof model.value\n return\n }\n\n model.value = option.value as typeof model.value\n}\n\nfunction moveTo(index: number) {\n if (options.length === 0) return\n\n active.value = Math.max(0, Math.min(options.length - 1, index))\n const option = options[active.value]\n\n // Single: the selection follows the focus, which is what a listbox does.\n if (mode === 'single' && option && !option.disabled) select(option)\n\n document.getElementById(`${id}-${active.value}`)?.scrollIntoView?.({ block: 'nearest' })\n}\n\n/** The next option after the current one that starts with what was typed. */\nfunction jumpTo(letter: string) {\n const now = Date.now()\n typed = now - typedAt > 700 ? letter : typed + letter\n typedAt = now\n\n const from = typed.length === 1 ? active.value + 1 : active.value\n const order = [...options.slice(from), ...options.slice(0, from)]\n const found = order.find(\n (option) => !option.disabled && option.label.toLowerCase().startsWith(typed.toLowerCase()),\n )\n\n if (found) moveTo(options.indexOf(found))\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n const moves: Record<string, number> = {\n ArrowDown: active.value + 1,\n ArrowUp: active.value - 1,\n Home: 0,\n End: options.length - 1,\n }\n\n if (event.key in moves) {\n event.preventDefault()\n moveTo(moves[event.key]!)\n return\n }\n\n if (event.key === ' ' || event.key === 'Enter') {\n const option = options[active.value]\n if (!option) return\n event.preventDefault()\n select(option)\n return\n }\n\n // A single printable character: the typeahead every native list has.\n if (event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey) {\n jumpTo(event.key)\n }\n}\n</script>\n\n<template>\n <div\n class=\"rk-listbox control\"\n :style=\"{ maxHeight: height }\"\n role=\"listbox\"\n tabindex=\"0\"\n :aria-label=\"label\"\n :aria-multiselectable=\"mode === 'multiple' ? true : undefined\"\n :aria-activedescendant=\"options.length ? `${id}-${active}` : undefined\"\n @keydown=\"onKeydown\"\n >\n <!-- `aria-disabled` is omitted rather than `false`: an option that can be\n chosen says nothing about being disabled. -->\n <div\n v-for=\"(option, index) in options\"\n :id=\"`${id}-${index}`\"\n :key=\"option.value\"\n class=\"rk-listbox-option\"\n :class=\"{\n 'is-active': index === active,\n 'is-chosen': chosen.has(option.value),\n 'is-disabled': option.disabled,\n }\"\n role=\"option\"\n :aria-selected=\"chosen.has(option.value)\"\n :aria-disabled=\"option.disabled || undefined\"\n @click=\"((active = index), select(option))\"\n >\n <span class=\"min-w-0 flex-1\">\n <span class=\"rk-listbox-label\">{{ option.label }}</span>\n <span v-if=\"option.description\" class=\"rk-listbox-description\">{{\n option.description\n }}</span>\n </span>\n\n <Check v-if=\"chosen.has(option.value)\" class=\"size-4 shrink-0\" aria-hidden=\"true\" />\n </div>\n\n <p v-if=\"options.length === 0\" class=\"rk-listbox-empty\" />\n </div>\n</template>\n\n<style scoped>\n.rk-listbox {\n overflow-y: auto;\n border-radius: var(--radius-card);\n padding: 0.25rem;\n}\n\n.rk-listbox:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 1px;\n}\n\n.rk-listbox-option {\n display: flex;\n cursor: pointer;\n align-items: center;\n gap: 0.625rem;\n border-radius: var(--radius-cell);\n padding: 0.5rem 0.625rem;\n font-size: 0.875rem;\n color: var(--color-ink);\n}\n\n/* The current row shows only while the list has focus. A list that is not\n being used has no \"current\" row, and a highlight left behind on one reads\n as a selection — which is exactly what it is not. */\n.rk-listbox:focus-within .rk-listbox-option.is-active {\n background: var(--color-muted);\n}\n\n/* The chosen row is its tinted ground, its weight and its tick — not a\n primary-coloured label on a ten per cent tint of primary, which is the\n same fault `BaseBadge` had: a faint wash of a colour is still nearly that\n colour, and the pair measured 4.16:1. */\n.rk-listbox-option.is-chosen {\n color: var(--color-ink);\n font-weight: 500;\n}\n\n/* Chosen is the tick and the tint, and it holds whether the list is in use\n or not: the current option and the chosen one are different things. */\n.rk-listbox-option.is-chosen {\n background: color-mix(in oklab, var(--color-primary) 10%, transparent);\n}\n\n.rk-listbox:focus-within .rk-listbox-option.is-chosen.is-active {\n background: color-mix(in oklab, var(--color-primary) 20%, transparent);\n}\n\n.rk-listbox-option.is-disabled {\n cursor: not-allowed;\n opacity: 0.45;\n}\n\n.rk-listbox-label {\n display: block;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rk-listbox-description {\n display: block;\n font-size: 0.75rem;\n color: var(--color-ink-soft);\n}\n\n.rk-listbox-empty {\n padding: 1rem;\n}\n</style>\n","<script setup lang=\"ts\" generic=\"V extends string, M extends 'single' | 'multiple' = 'single'\">\nimport { Check } from 'lucide-vue-next'\nimport { computed, ref, useId } from 'vue'\n\nimport { useBoundValue } from '../composables/use-bound-value'\n\nexport interface ListboxOption<V extends string> {\n value: V\n /** Already translated. */\n label: string\n /** A line under the label. */\n description?: string | undefined\n disabled?: boolean | undefined\n}\n\n/**\n * A list you choose from, open on the page.\n *\n * Between `BaseRadioGroup` and `BaseSelect`: a radio group is a handful of\n * options that are all visible and all equal; a select opens the platform's\n * own picker; this is a scrollable list of many, choosing one or several,\n * that stays on screen — a list of accounts, of tags, of files to act on.\n *\n * One Tab stop. The list itself takes focus and `aria-activedescendant`\n * says which option is current, so the arrows move without thirty stops in\n * the tab order. Typing a letter jumps to the next option that starts with\n * it, which is how every native list has always worked and the first thing\n * people try.\n *\n * In `single` mode the selection follows the arrows, because that is what a\n * listbox does; in `multiple` mode Space and Enter toggle the current one.\n */\nconst {\n modelValue = undefined,\n options,\n label,\n mode = 'single' as M,\n height = '14rem',\n} = defineProps<{\n /** The chosen option, or options, with `v-model`. */\n modelValue?: Value | undefined\n options: readonly ListboxOption<V>[]\n /** The list's accessible name. */\n label: string\n /** One option, or any number of them. */\n mode?: M | undefined\n /** How tall before it scrolls. Any CSS length. */\n height?: string | undefined\n}>()\n\n/* Multiple always hands back an array — toggling one produces a list, never\n nothing — so only the single value can be undefined. */\ntype Value = M extends 'multiple' ? V[] : V | undefined\n\n/* Declared by hand rather than with defineModel, which cannot both accept\n `undefined` and promise never to emit it. See `use-bound-value.ts`. */\nconst emit = defineEmits<{ 'update:modelValue': [value: Value] }>()\nconst model = useBoundValue<Value>(\n () => modelValue as Value | undefined,\n (value) => emit('update:modelValue', value),\n)\n\nconst id = useId()\nconst active = ref(0)\n\nlet typed = ''\nlet typedAt = 0\n\nconst chosen = computed(() => {\n const value = model.value\n if (Array.isArray(value)) return new Set<V>(value)\n\n return new Set<V>(value === undefined ? [] : [value as V])\n})\n\nfunction select(option: ListboxOption<V>) {\n if (option.disabled) return\n\n if (mode === 'multiple') {\n const next = new Set(chosen.value)\n if (next.has(option.value)) next.delete(option.value)\n else next.add(option.value)\n\n // In the order of the options, so the value reads the same however the\n // reader got there.\n model.value = options\n .map((one) => one.value)\n .filter((value) => next.has(value)) as typeof model.value\n return\n }\n\n model.value = option.value as typeof model.value\n}\n\nfunction moveTo(index: number) {\n if (options.length === 0) return\n\n active.value = Math.max(0, Math.min(options.length - 1, index))\n const option = options[active.value]\n\n // Single: the selection follows the focus, which is what a listbox does.\n if (mode === 'single' && option && !option.disabled) select(option)\n\n document.getElementById(`${id}-${active.value}`)?.scrollIntoView?.({ block: 'nearest' })\n}\n\n/** The next option after the current one that starts with what was typed. */\nfunction jumpTo(letter: string) {\n const now = Date.now()\n typed = now - typedAt > 700 ? letter : typed + letter\n typedAt = now\n\n const from = typed.length === 1 ? active.value + 1 : active.value\n const order = [...options.slice(from), ...options.slice(0, from)]\n const found = order.find(\n (option) => !option.disabled && option.label.toLowerCase().startsWith(typed.toLowerCase()),\n )\n\n if (found) moveTo(options.indexOf(found))\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n const moves: Record<string, number> = {\n ArrowDown: active.value + 1,\n ArrowUp: active.value - 1,\n Home: 0,\n End: options.length - 1,\n }\n\n if (event.key in moves) {\n event.preventDefault()\n moveTo(moves[event.key]!)\n return\n }\n\n if (event.key === ' ' || event.key === 'Enter') {\n const option = options[active.value]\n if (!option) return\n event.preventDefault()\n select(option)\n return\n }\n\n // A single printable character: the typeahead every native list has.\n if (event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey) {\n jumpTo(event.key)\n }\n}\n</script>\n\n<template>\n <div\n class=\"rk-listbox control\"\n :style=\"{ maxHeight: height }\"\n role=\"listbox\"\n tabindex=\"0\"\n :aria-label=\"label\"\n :aria-multiselectable=\"mode === 'multiple' ? true : undefined\"\n :aria-activedescendant=\"options.length ? `${id}-${active}` : undefined\"\n @keydown=\"onKeydown\"\n >\n <!-- `aria-disabled` is omitted rather than `false`: an option that can be\n chosen says nothing about being disabled. -->\n <div\n v-for=\"(option, index) in options\"\n :id=\"`${id}-${index}`\"\n :key=\"option.value\"\n class=\"rk-listbox-option\"\n :class=\"{\n 'is-active': index === active,\n 'is-chosen': chosen.has(option.value),\n 'is-disabled': option.disabled,\n }\"\n role=\"option\"\n :aria-selected=\"chosen.has(option.value)\"\n :aria-disabled=\"option.disabled || undefined\"\n @click=\"((active = index), select(option))\"\n >\n <span class=\"min-w-0 flex-1\">\n <span class=\"rk-listbox-label\">{{ option.label }}</span>\n <span v-if=\"option.description\" class=\"rk-listbox-description\">{{\n option.description\n }}</span>\n </span>\n\n <Check v-if=\"chosen.has(option.value)\" class=\"size-4 shrink-0\" aria-hidden=\"true\" />\n </div>\n\n <p v-if=\"options.length === 0\" class=\"rk-listbox-empty\" />\n </div>\n</template>\n\n<style scoped>\n.rk-listbox {\n overflow-y: auto;\n border-radius: var(--radius-card);\n padding: 0.25rem;\n}\n\n.rk-listbox:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 1px;\n}\n\n.rk-listbox-option {\n display: flex;\n cursor: pointer;\n align-items: center;\n gap: 0.625rem;\n border-radius: var(--radius-cell);\n padding: 0.5rem 0.625rem;\n font-size: 0.875rem;\n color: var(--color-ink);\n}\n\n/* The current row shows only while the list has focus. A list that is not\n being used has no \"current\" row, and a highlight left behind on one reads\n as a selection — which is exactly what it is not. */\n.rk-listbox:focus-within .rk-listbox-option.is-active {\n background: var(--color-muted);\n}\n\n/* The chosen row is its tinted ground, its weight and its tick — not a\n primary-coloured label on a ten per cent tint of primary, which is the\n same fault `BaseBadge` had: a faint wash of a colour is still nearly that\n colour, and the pair measured 4.16:1. */\n.rk-listbox-option.is-chosen {\n color: var(--color-ink);\n font-weight: 500;\n}\n\n/* Chosen is the tick and the tint, and it holds whether the list is in use\n or not: the current option and the chosen one are different things. */\n.rk-listbox-option.is-chosen {\n background: color-mix(in oklab, var(--color-primary) 10%, transparent);\n}\n\n.rk-listbox:focus-within .rk-listbox-option.is-chosen.is-active {\n background: color-mix(in oklab, var(--color-primary) 20%, transparent);\n}\n\n.rk-listbox-option.is-disabled {\n cursor: not-allowed;\n opacity: 0.45;\n}\n\n.rk-listbox-label {\n display: block;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rk-listbox-description {\n display: block;\n font-size: 0.75rem;\n color: var(--color-ink-soft);\n}\n\n.rk-listbox-empty {\n padding: 1rem;\n}\n</style>\n"],"mappings":"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"}
|
package/dist/app.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { a as BaseCheckbox_default, o as BaseSheet_default, r as horizontalStep, s as inertOutside, t as focusTarget } from "./focus-target-
|
|
2
|
-
import { _ as toRedirectPath, c as useToast, d as isThemePreference, g as safeRedirect, h as tapFeedback, i as BaseInput_default, l as useOnline, m as useTheme, r as GoogleButton_default, s as BaseAlert_default, t as announce } from "./use-announce-
|
|
1
|
+
import { a as BaseCheckbox_default, o as BaseSheet_default, r as horizontalStep, s as inertOutside, t as focusTarget } from "./focus-target-DENXwVQ3.js";
|
|
2
|
+
import { _ as toRedirectPath, c as useToast, d as isThemePreference, g as safeRedirect, h as tapFeedback, i as BaseInput_default, l as useOnline, m as useTheme, r as GoogleButton_default, s as BaseAlert_default, t as announce } from "./use-announce-COM3S_ER.js";
|
|
3
3
|
import { t as _plugin_vue_export_helper_default } from "./_plugin-vue_export-helper-BOaGB7Aw.js";
|
|
4
|
-
import { t as BaseButton_default } from "./BaseButton-
|
|
4
|
+
import { t as BaseButton_default } from "./BaseButton-C3VGgIuR.js";
|
|
5
5
|
import { t as SettingsRow_default } from "./SettingsRow-C6Spi7Np.js";
|
|
6
6
|
import { Fragment, Teleport, Transition, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, mergeModels, nextTick, normalizeClass, onUnmounted, openBlock, reactive, readonly, ref, renderList, renderSlot, toDisplayString, unref, useModel, watch, withCtx, withModifiers } from "vue";
|
|
7
7
|
import { Check, Languages } from "lucide-vue-next";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** One answer in a group of checkboxes. */
|
|
2
|
+
export interface CheckboxOption<V extends string = string> {
|
|
3
|
+
value: V;
|
|
4
|
+
label: string;
|
|
5
|
+
/** A reason this one cannot be chosen, shown under its label. */
|
|
6
|
+
hint?: string | undefined;
|
|
7
|
+
disabled?: boolean | undefined;
|
|
8
|
+
}
|
|
9
|
+
declare const __VLS_export: <V extends string = string>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
|
|
10
|
+
props: import('vue').PublicProps & __VLS_PrettifyLocal<({
|
|
11
|
+
options: readonly CheckboxOption<V>[];
|
|
12
|
+
legend: string;
|
|
13
|
+
/** Shown under the group, and it marks every box invalid. */
|
|
14
|
+
error?: string | undefined;
|
|
15
|
+
hint?: string | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Hides the question but keeps it as the group's accessible name — for a
|
|
18
|
+
* group whose surroundings already ask it. Dropping the legend instead
|
|
19
|
+
* leaves the set named nothing at all.
|
|
20
|
+
*/
|
|
21
|
+
legendHidden?: boolean | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Adds a box above the others that chooses or clears all of them, named by
|
|
24
|
+
* this string. Empty means no such box: it earns its place on a list long
|
|
25
|
+
* enough that ticking each one is work, and gets in the way on three.
|
|
26
|
+
*
|
|
27
|
+
* It never chooses a disabled option, so pressing it twice is not a way to
|
|
28
|
+
* select something the form has ruled out.
|
|
29
|
+
*/
|
|
30
|
+
selectAllLabel?: string | undefined;
|
|
31
|
+
/** Disables every box, including the select-all. */
|
|
32
|
+
disabled?: boolean | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Lay the boxes out in two columns from the `sm` breakpoint. One column on
|
|
35
|
+
* a phone either way: a two-column list of checkboxes on a 390px screen is
|
|
36
|
+
* two columns of truncated labels.
|
|
37
|
+
*/
|
|
38
|
+
columns?: 1 | 2 | undefined;
|
|
39
|
+
} & {
|
|
40
|
+
modelValue?: V[];
|
|
41
|
+
}) & {
|
|
42
|
+
"onUpdate:modelValue"?: (value: V[]) => any;
|
|
43
|
+
}> & (typeof globalThis extends {
|
|
44
|
+
__VLS_PROPS_FALLBACK: infer P;
|
|
45
|
+
} ? P : {});
|
|
46
|
+
expose: (exposed: {}) => void;
|
|
47
|
+
attrs: any;
|
|
48
|
+
slots: {};
|
|
49
|
+
emit: (event: "update:modelValue", value: V[]) => void;
|
|
50
|
+
}>) => import('vue').VNode & {
|
|
51
|
+
__ctx?: NonNullable<Awaited<typeof __VLS_setup>>;
|
|
52
|
+
};
|
|
53
|
+
declare const _default: typeof __VLS_export;
|
|
54
|
+
export default _default;
|
|
55
|
+
type __VLS_PrettifyLocal<T> = (T extends any ? {
|
|
56
|
+
[K in keyof T]: T[K];
|
|
57
|
+
} : {
|
|
58
|
+
[K in keyof T as K]: T[K];
|
|
59
|
+
}) & {};
|
|
@@ -9,14 +9,25 @@
|
|
|
9
9
|
*/
|
|
10
10
|
type __VLS_Props = {
|
|
11
11
|
title: string;
|
|
12
|
+
/**
|
|
13
|
+
* The element the title is, when `h1` is not it.
|
|
14
|
+
*
|
|
15
|
+
* A screen's title is the page's one `h1`, which is the default. It is the
|
|
16
|
+
* wrong answer in two places: a screen rendered inside another page — a
|
|
17
|
+
* documentation site, a preview, a phone frame beside three others — and a
|
|
18
|
+
* header that sits under a heading that already owns the rank. Both put a
|
|
19
|
+
* second `h1` in the document, which is how this kit's own showcase ended
|
|
20
|
+
* up with three.
|
|
21
|
+
*/
|
|
22
|
+
as?: 'h1' | 'h2' | 'h3' | 'p' | undefined;
|
|
12
23
|
};
|
|
13
|
-
declare var __VLS_1: {},
|
|
24
|
+
declare var __VLS_1: {}, __VLS_9: {}, __VLS_11: {};
|
|
14
25
|
type __VLS_Slots = {} & {
|
|
15
26
|
left?: (props: typeof __VLS_1) => any;
|
|
16
27
|
} & {
|
|
17
|
-
title?: (props: typeof
|
|
28
|
+
title?: (props: typeof __VLS_9) => any;
|
|
18
29
|
} & {
|
|
19
|
-
right?: (props: typeof
|
|
30
|
+
right?: (props: typeof __VLS_11) => any;
|
|
20
31
|
};
|
|
21
32
|
declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
|
|
22
33
|
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
@@ -4,7 +4,16 @@ export interface Tone {
|
|
|
4
4
|
fill: string;
|
|
5
5
|
/** Tinted surface for the pill, e.g. `bg-positive/5 border-positive/25`. */
|
|
6
6
|
card: string;
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Foreground for the label. `text-ink`, in almost every case.
|
|
9
|
+
*
|
|
10
|
+
* Not the role: `text-positive` on a `bg-positive/5` pill is a colour on a
|
|
11
|
+
* faint wash of itself, and it measures about 2:1 painted — the fault
|
|
12
|
+
* `BaseBadge`, `BaseChip`, `BaseAlert`, `BaseListbox` and `PriceCard` were
|
|
13
|
+
* all fixed for in 2.25.0. This one takes its classes from the app, so no
|
|
14
|
+
* source-reading test could see it; the contrast scan on the showcase
|
|
15
|
+
* caught it once the playground started rendering it.
|
|
16
|
+
*/
|
|
8
17
|
text: string;
|
|
9
18
|
}
|
|
10
19
|
/**
|
|
@@ -17,10 +26,11 @@ export interface Tone {
|
|
|
17
26
|
*/
|
|
18
27
|
type __VLS_Props = {
|
|
19
28
|
/**
|
|
20
|
-
* The colour classes for the dot and the label, as written-out
|
|
21
|
-
* `Tone` is a
|
|
22
|
-
* the app's own, and Tailwind reads source as plain
|
|
23
|
-
* at runtime never reaches the stylesheet.
|
|
29
|
+
* The colour classes for the dot, the pill and the label, as written-out
|
|
30
|
+
* class names. `Tone` is a set of classes rather than a role token because
|
|
31
|
+
* the categories here are the app's own, and Tailwind reads source as plain
|
|
32
|
+
* text: a class assembled at runtime never reaches the stylesheet. Put the
|
|
33
|
+
* role on `fill` and `card` and leave `text` as `text-ink`.
|
|
24
34
|
*/
|
|
25
35
|
tone: Tone;
|
|
26
36
|
label: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as _plugin_vue_export_helper_default } from "./_plugin-vue_export-helper-BOaGB7Aw.js";
|
|
2
|
-
import { t as BaseButton_default } from "./BaseButton-
|
|
2
|
+
import { t as BaseButton_default } from "./BaseButton-C3VGgIuR.js";
|
|
3
3
|
import { Teleport, Transition, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createVNode, defineComponent, mergeModels, nextTick, normalizeClass, normalizeStyle, onScopeDispose, onUnmounted, openBlock, readonly, ref, renderSlot, toDisplayString, unref, useId, useModel, vModelCheckbox, watch, withCtx, withDirectives } from "vue";
|
|
4
4
|
import { X } from "lucide-vue-next";
|
|
5
5
|
//#region src/utils/sheet-root.ts
|
|
@@ -458,4 +458,4 @@ function focusTarget(target) {
|
|
|
458
458
|
//#endregion
|
|
459
459
|
export { BaseCheckbox_default as a, useBoundValue as c, ensureSheetRoot as d, textDirection as i, useVisualViewport as l, elementDirection as n, BaseSheet_default as o, horizontalStep as r, inertOutside as s, focusTarget as t, SHEET_ROOT_ID as u };
|
|
460
460
|
|
|
461
|
-
//# sourceMappingURL=focus-target-
|
|
461
|
+
//# sourceMappingURL=focus-target-DENXwVQ3.js.map
|