rei-kit 2.11.0 → 2.12.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/dist/web.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"web.js","names":["$slots","$slots"],"sources":["../src/web/BaseAccordion.vue","../src/web/BaseAccordion.vue","../src/web/BaseBreadcrumb.vue","../src/web/BaseBreadcrumb.vue","../src/web/BaseDisclosure.vue","../src/web/BaseDisclosure.vue","../src/web/BaseModal.vue","../src/web/BaseModal.vue","../src/web/BasePagination.vue","../src/web/BasePagination.vue","../src/web/CommandMenu.vue","../src/web/CommandMenu.vue","../src/web/DataTable.vue","../src/web/DataTable.vue","../src/web/BaseSplitter.vue","../src/web/BaseSplitter.vue","../src/web/BaseToolbar.vue","../src/web/BaseToolbar.vue","../src/web/BaseTree.vue","../src/web/BaseTree.vue","../src/web/TransferList.vue","../src/web/TransferList.vue","../src/web/ResponsiveDialog.vue","../src/web/ResponsiveDialog.vue","../src/web/BaseTabs.vue","../src/web/BaseTabs.vue","../src/web/BaseTooltip.vue","../src/web/BaseTooltip.vue","../src/web/NavLinks.vue","../src/web/NavLinks.vue"],"sourcesContent":["<script setup lang=\"ts\" generic=\"K extends string\">\nimport { shallowRef, useId } from 'vue'\n\nexport interface AccordionItem<K extends string> {\n key: K\n /** The question. Already translated. */\n title: string\n}\n\n/**\n * A list that opens one section at a time.\n *\n * ## Why not `<details>`\n *\n * `<details>` is the honest element for this and it was the first choice. A\n * browser removes its content from layout entirely when closed, so there is\n * nothing to animate between — the panel can only appear. Faking it by leaving\n * `open` set and hiding with CSS tells a screen reader the section is expanded\n * while a sighted reader sees it shut, which is worse than no animation.\n *\n * So: a button carrying `aria-expanded`, the answer always in the markup — the\n * prerendered HTML carries every answer, which is what a crawler and a reader\n * without JavaScript get — and a grid row that travels from `0fr` to `1fr`,\n * which is the one way to animate to a height nobody has measured.\n *\n * The content is always present, so anything expensive in it is paid for on\n * first paint. That is the trade: a section a crawler can read costs a section\n * that is never lazy.\n */\nconst {\n items,\n multiple = false,\n headingLevel = 3,\n} = defineProps<{\n items: readonly AccordionItem<K>[]\n /** Allow several open at once. Off by default, which is what makes it an accordion. */\n multiple?: boolean | undefined\n /**\n * Which heading the control sits inside.\n *\n * A heading, always — it is how a screen reader user moves between sections\n * without opening any of them. The level is a prop because it depends on what\n * is above it on the page, and getting it wrong breaks the outline.\n */\n headingLevel?: 2 | 3 | 4 | undefined\n}>()\n\ndefineSlots<{\n /** The answer, per item. */\n default: (props: { item: AccordionItem<K>; open: boolean }) => unknown\n /** The question, when the item's `title` is not enough. */\n title?: (props: { item: AccordionItem<K>; open: boolean }) => unknown\n}>()\n\nconst uid = useId()\n/* `shallowRef` because the list is replaced wholesale rather than mutated, and\n because a deep ref unwraps a generic key type into something TypeScript can\n no longer match against `K`. */\nconst open = shallowRef<K[]>([])\n\nconst isOpen = (key: K) => open.value.includes(key)\n\nfunction toggle(key: K) {\n if (isOpen(key)) {\n open.value = open.value.filter((k) => k !== key)\n } else {\n open.value = multiple ? [...open.value, key] : [key]\n }\n}\n</script>\n\n<template>\n <ul class=\"rk-accordion\">\n <li v-for=\"item in items\" :key=\"item.key\" class=\"rk-accordion-item\">\n <component :is=\"`h${headingLevel}`\">\n <button\n type=\"button\"\n class=\"rk-accordion-control\"\n :aria-expanded=\"isOpen(item.key)\"\n :aria-controls=\"`${uid}-${item.key}`\"\n @click=\"toggle(item.key)\"\n >\n <span class=\"rk-accordion-title\">\n <slot name=\"title\" :item=\"item\" :open=\"isOpen(item.key)\">{{ item.title }}</slot>\n </span>\n\n <!-- A plus that becomes a minus: one stroke turns, so the control\n keeps its place while it changes meaning. -->\n <span class=\"rk-accordion-mark\" aria-hidden=\"true\">\n <span class=\"rk-accordion-bar\" />\n <span\n class=\"rk-accordion-bar rk-accordion-bar-turn\"\n :class=\"{ 'is-open': isOpen(item.key) }\"\n />\n </span>\n </button>\n </component>\n\n <div\n :id=\"`${uid}-${item.key}`\"\n class=\"rk-accordion-panel\"\n :class=\"{ 'is-open': isOpen(item.key) }\"\n >\n <div class=\"rk-accordion-clip\">\n <div class=\"rk-accordion-body\" :class=\"{ 'is-open': isOpen(item.key) }\">\n <slot :item=\"item\" :open=\"isOpen(item.key)\" />\n </div>\n </div>\n </div>\n </li>\n </ul>\n</template>\n\n<style scoped>\n.rk-accordion {\n border-top: 1px solid color-mix(in srgb, var(--color-hair) 70%, transparent);\n}\n\n.rk-accordion-item {\n border-bottom: 1px solid color-mix(in srgb, var(--color-hair) 70%, transparent);\n}\n\n.rk-accordion-control {\n display: flex;\n width: 100%;\n cursor: pointer;\n align-items: center;\n justify-content: space-between;\n gap: 1.5rem;\n padding: 1.25rem 0;\n text-align: left;\n transition: color var(--duration-fast);\n}\n\n.rk-accordion-control:hover {\n color: var(--color-primary);\n}\n\n.rk-accordion-control:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n.rk-accordion-title {\n color: var(--color-ink);\n font-size: 1rem;\n font-weight: 500;\n}\n\n.rk-accordion-mark {\n position: relative;\n display: grid;\n place-items: center;\n flex-shrink: 0;\n width: 1.5rem;\n height: 1.5rem;\n}\n\n.rk-accordion-bar {\n position: absolute;\n height: 1px;\n width: 0.875rem;\n background: var(--color-ink-soft);\n}\n\n.rk-accordion-bar-turn {\n transform: rotate(90deg);\n transition: transform var(--duration-slower) ease-out;\n}\n\n.rk-accordion-bar-turn.is-open {\n transform: rotate(0deg);\n}\n\n/* 0fr to 1fr is the whole trick: a grid row can be animated to a height that\n was never measured, which is what `height: auto` cannot do. */\n.rk-accordion-panel {\n display: grid;\n grid-template-rows: 0fr;\n transition: grid-template-rows var(--duration-slower) ease-out;\n}\n\n.rk-accordion-panel.is-open {\n grid-template-rows: 1fr;\n}\n\n.rk-accordion-clip {\n overflow: hidden;\n}\n\n.rk-accordion-body {\n max-width: 68ch;\n padding-bottom: 1.5rem;\n color: var(--color-ink-soft);\n line-height: 1.625;\n opacity: 0;\n transition: opacity var(--duration-slow);\n}\n\n.rk-accordion-body.is-open {\n opacity: 1;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .rk-accordion-panel,\n .rk-accordion-bar-turn,\n .rk-accordion-body {\n transition: none;\n }\n}\n</style>\n","<script setup lang=\"ts\" generic=\"K extends string\">\nimport { shallowRef, useId } from 'vue'\n\nexport interface AccordionItem<K extends string> {\n key: K\n /** The question. Already translated. */\n title: string\n}\n\n/**\n * A list that opens one section at a time.\n *\n * ## Why not `<details>`\n *\n * `<details>` is the honest element for this and it was the first choice. A\n * browser removes its content from layout entirely when closed, so there is\n * nothing to animate between — the panel can only appear. Faking it by leaving\n * `open` set and hiding with CSS tells a screen reader the section is expanded\n * while a sighted reader sees it shut, which is worse than no animation.\n *\n * So: a button carrying `aria-expanded`, the answer always in the markup — the\n * prerendered HTML carries every answer, which is what a crawler and a reader\n * without JavaScript get — and a grid row that travels from `0fr` to `1fr`,\n * which is the one way to animate to a height nobody has measured.\n *\n * The content is always present, so anything expensive in it is paid for on\n * first paint. That is the trade: a section a crawler can read costs a section\n * that is never lazy.\n */\nconst {\n items,\n multiple = false,\n headingLevel = 3,\n} = defineProps<{\n items: readonly AccordionItem<K>[]\n /** Allow several open at once. Off by default, which is what makes it an accordion. */\n multiple?: boolean | undefined\n /**\n * Which heading the control sits inside.\n *\n * A heading, always — it is how a screen reader user moves between sections\n * without opening any of them. The level is a prop because it depends on what\n * is above it on the page, and getting it wrong breaks the outline.\n */\n headingLevel?: 2 | 3 | 4 | undefined\n}>()\n\ndefineSlots<{\n /** The answer, per item. */\n default: (props: { item: AccordionItem<K>; open: boolean }) => unknown\n /** The question, when the item's `title` is not enough. */\n title?: (props: { item: AccordionItem<K>; open: boolean }) => unknown\n}>()\n\nconst uid = useId()\n/* `shallowRef` because the list is replaced wholesale rather than mutated, and\n because a deep ref unwraps a generic key type into something TypeScript can\n no longer match against `K`. */\nconst open = shallowRef<K[]>([])\n\nconst isOpen = (key: K) => open.value.includes(key)\n\nfunction toggle(key: K) {\n if (isOpen(key)) {\n open.value = open.value.filter((k) => k !== key)\n } else {\n open.value = multiple ? [...open.value, key] : [key]\n }\n}\n</script>\n\n<template>\n <ul class=\"rk-accordion\">\n <li v-for=\"item in items\" :key=\"item.key\" class=\"rk-accordion-item\">\n <component :is=\"`h${headingLevel}`\">\n <button\n type=\"button\"\n class=\"rk-accordion-control\"\n :aria-expanded=\"isOpen(item.key)\"\n :aria-controls=\"`${uid}-${item.key}`\"\n @click=\"toggle(item.key)\"\n >\n <span class=\"rk-accordion-title\">\n <slot name=\"title\" :item=\"item\" :open=\"isOpen(item.key)\">{{ item.title }}</slot>\n </span>\n\n <!-- A plus that becomes a minus: one stroke turns, so the control\n keeps its place while it changes meaning. -->\n <span class=\"rk-accordion-mark\" aria-hidden=\"true\">\n <span class=\"rk-accordion-bar\" />\n <span\n class=\"rk-accordion-bar rk-accordion-bar-turn\"\n :class=\"{ 'is-open': isOpen(item.key) }\"\n />\n </span>\n </button>\n </component>\n\n <div\n :id=\"`${uid}-${item.key}`\"\n class=\"rk-accordion-panel\"\n :class=\"{ 'is-open': isOpen(item.key) }\"\n >\n <div class=\"rk-accordion-clip\">\n <div class=\"rk-accordion-body\" :class=\"{ 'is-open': isOpen(item.key) }\">\n <slot :item=\"item\" :open=\"isOpen(item.key)\" />\n </div>\n </div>\n </div>\n </li>\n </ul>\n</template>\n\n<style scoped>\n.rk-accordion {\n border-top: 1px solid color-mix(in srgb, var(--color-hair) 70%, transparent);\n}\n\n.rk-accordion-item {\n border-bottom: 1px solid color-mix(in srgb, var(--color-hair) 70%, transparent);\n}\n\n.rk-accordion-control {\n display: flex;\n width: 100%;\n cursor: pointer;\n align-items: center;\n justify-content: space-between;\n gap: 1.5rem;\n padding: 1.25rem 0;\n text-align: left;\n transition: color var(--duration-fast);\n}\n\n.rk-accordion-control:hover {\n color: var(--color-primary);\n}\n\n.rk-accordion-control:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n.rk-accordion-title {\n color: var(--color-ink);\n font-size: 1rem;\n font-weight: 500;\n}\n\n.rk-accordion-mark {\n position: relative;\n display: grid;\n place-items: center;\n flex-shrink: 0;\n width: 1.5rem;\n height: 1.5rem;\n}\n\n.rk-accordion-bar {\n position: absolute;\n height: 1px;\n width: 0.875rem;\n background: var(--color-ink-soft);\n}\n\n.rk-accordion-bar-turn {\n transform: rotate(90deg);\n transition: transform var(--duration-slower) ease-out;\n}\n\n.rk-accordion-bar-turn.is-open {\n transform: rotate(0deg);\n}\n\n/* 0fr to 1fr is the whole trick: a grid row can be animated to a height that\n was never measured, which is what `height: auto` cannot do. */\n.rk-accordion-panel {\n display: grid;\n grid-template-rows: 0fr;\n transition: grid-template-rows var(--duration-slower) ease-out;\n}\n\n.rk-accordion-panel.is-open {\n grid-template-rows: 1fr;\n}\n\n.rk-accordion-clip {\n overflow: hidden;\n}\n\n.rk-accordion-body {\n max-width: 68ch;\n padding-bottom: 1.5rem;\n color: var(--color-ink-soft);\n line-height: 1.625;\n opacity: 0;\n transition: opacity var(--duration-slow);\n}\n\n.rk-accordion-body.is-open {\n opacity: 1;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .rk-accordion-panel,\n .rk-accordion-bar-turn,\n .rk-accordion-body {\n transition: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { RouterLink } from 'vue-router'\n\nexport interface Crumb {\n /** Already translated. */\n label: string\n /** Omitted for the current page, which is not a link to itself. */\n to?: string | undefined\n}\n\n/**\n * Where this page sits, and the way back up.\n *\n * An ordered list inside a `nav`, because the order is the meaning. The last\n * crumb is the current page and is deliberately not a link: a link to the page\n * you are on is a control that does nothing, and `aria-current=\"page\"` is what\n * says so to everyone else.\n *\n * The separators are `aria-hidden`. A screen reader announces a list and its\n * position in it; reading \"slash\" between every item is noise on top of\n * information the reader already has.\n */\nconst { items, label = '' } = defineProps<{\n items: readonly Crumb[]\n /** Accessible name for the landmark. */\n label?: string | undefined\n}>()\n</script>\n\n<template>\n <nav :aria-label=\"label || undefined\">\n <ol class=\"rk-crumbs\">\n <li v-for=\"(item, index) in items\" :key=\"item.label\" class=\"rk-crumb\">\n <RouterLink v-if=\"item.to\" :to=\"item.to\" class=\"rk-crumb-link\">\n {{ item.label }}\n </RouterLink>\n <span v-else class=\"rk-crumb-current\" aria-current=\"page\">{{ item.label }}</span>\n\n <span v-if=\"index < items.length - 1\" class=\"rk-crumb-sep\" aria-hidden=\"true\">/</span>\n </li>\n </ol>\n </nav>\n</template>\n\n<style scoped>\n.rk-crumbs {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 0.5rem;\n font-size: 0.8125rem;\n}\n\n.rk-crumb {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n\n.rk-crumb-link {\n color: var(--color-ink-soft);\n transition: color var(--duration-fast);\n}\n\n.rk-crumb-link:hover {\n color: var(--color-ink);\n}\n\n.rk-crumb-link:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n border-radius: 2px;\n}\n\n.rk-crumb-current {\n color: var(--color-ink);\n font-weight: 500;\n}\n\n.rk-crumb-sep {\n color: var(--color-hair);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { RouterLink } from 'vue-router'\n\nexport interface Crumb {\n /** Already translated. */\n label: string\n /** Omitted for the current page, which is not a link to itself. */\n to?: string | undefined\n}\n\n/**\n * Where this page sits, and the way back up.\n *\n * An ordered list inside a `nav`, because the order is the meaning. The last\n * crumb is the current page and is deliberately not a link: a link to the page\n * you are on is a control that does nothing, and `aria-current=\"page\"` is what\n * says so to everyone else.\n *\n * The separators are `aria-hidden`. A screen reader announces a list and its\n * position in it; reading \"slash\" between every item is noise on top of\n * information the reader already has.\n */\nconst { items, label = '' } = defineProps<{\n items: readonly Crumb[]\n /** Accessible name for the landmark. */\n label?: string | undefined\n}>()\n</script>\n\n<template>\n <nav :aria-label=\"label || undefined\">\n <ol class=\"rk-crumbs\">\n <li v-for=\"(item, index) in items\" :key=\"item.label\" class=\"rk-crumb\">\n <RouterLink v-if=\"item.to\" :to=\"item.to\" class=\"rk-crumb-link\">\n {{ item.label }}\n </RouterLink>\n <span v-else class=\"rk-crumb-current\" aria-current=\"page\">{{ item.label }}</span>\n\n <span v-if=\"index < items.length - 1\" class=\"rk-crumb-sep\" aria-hidden=\"true\">/</span>\n </li>\n </ol>\n </nav>\n</template>\n\n<style scoped>\n.rk-crumbs {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 0.5rem;\n font-size: 0.8125rem;\n}\n\n.rk-crumb {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n\n.rk-crumb-link {\n color: var(--color-ink-soft);\n transition: color var(--duration-fast);\n}\n\n.rk-crumb-link:hover {\n color: var(--color-ink);\n}\n\n.rk-crumb-link:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n border-radius: 2px;\n}\n\n.rk-crumb-current {\n color: var(--color-ink);\n font-weight: 500;\n}\n\n.rk-crumb-sep {\n color: var(--color-hair);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { useId } from 'vue'\n\n/**\n * One section that opens and closes.\n *\n * `BaseAccordion` is a list of these with the rule that only one stays open.\n * This is the same row on its own, for the common case where the list belongs\n * to the app — because it is staggered as it scrolls in, or interleaved with\n * something else, or rendered from a source the accordion cannot know about.\n *\n * That case is not hypothetical: the first list this kit met decorated every\n * row with a reveal directive, which has to sit on the element the accordion\n * would have owned. Shipping only the list would have meant choosing between\n * the component and the design.\n *\n * ## Why not `<details>`\n *\n * A browser removes `<details>` content from layout entirely when closed, so\n * there is nothing to animate between — the panel can only appear. Faking it by\n * leaving `open` set and hiding with CSS tells a screen reader the section is\n * expanded while a sighted reader sees it shut, which is worse than no\n * animation.\n *\n * So: a button carrying `aria-expanded`, the content always in the markup —\n * which is what a crawler and a reader without JavaScript get — and a grid row\n * travelling from `0fr` to `1fr`, the one way to animate to a height nobody has\n * measured.\n */\nconst { title = '', headingLevel = 3 } = defineProps<{\n /**\n * The question. Already translated.\n *\n * Optional only because the `title` slot is the other way to give one — pass\n * exactly one of them. A disclosure with neither has a control nobody can\n * read, which is why this is documented rather than defaulted to something.\n */\n title?: string | undefined\n /**\n * Which heading the control sits inside.\n *\n * A heading, always — it is how a screen reader user moves between sections\n * without opening any of them. The level depends on what is above it on the\n * page, and getting it wrong breaks the outline.\n */\n headingLevel?: 2 | 3 | 4 | undefined\n}>()\n\nconst open = defineModel<boolean>({ default: false })\n\ndefineSlots<{\n /** The answer. */\n default: () => unknown\n /**\n * The question, when the prop is not enough.\n *\n * A slot as well as a prop because the size of a heading is a decision about\n * the page it is on, not about disclosure: the first list to use this wanted\n * the question a step larger on a wide screen, and a component that hard-codes\n * one size makes that an override against its own stylesheet.\n */\n title?: () => unknown\n}>()\n\nconst id = useId()\n</script>\n\n<template>\n <div class=\"rk-disclosure\">\n <component :is=\"`h${headingLevel}`\">\n <button\n type=\"button\"\n class=\"rk-disclosure-control\"\n :aria-expanded=\"open\"\n :aria-controls=\"id\"\n @click=\"open = !open\"\n >\n <span class=\"rk-disclosure-title\">\n <slot name=\"title\">{{ title }}</slot>\n </span>\n\n <!-- A plus that becomes a minus: one stroke turns, so the control keeps\n its place while it changes meaning. -->\n <span class=\"rk-disclosure-mark\" aria-hidden=\"true\">\n <span class=\"rk-disclosure-bar\" />\n <span class=\"rk-disclosure-bar rk-disclosure-turn\" :class=\"{ 'is-open': open }\" />\n </span>\n </button>\n </component>\n\n <div :id=\"id\" class=\"rk-disclosure-panel\" :class=\"{ 'is-open': open }\">\n <div class=\"rk-disclosure-clip\">\n <div class=\"rk-disclosure-body\" :class=\"{ 'is-open': open }\"><slot /></div>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.rk-disclosure-control {\n display: flex;\n width: 100%;\n cursor: pointer;\n align-items: center;\n justify-content: space-between;\n gap: 1.5rem;\n padding: 1.25rem 0;\n text-align: left;\n transition: color var(--duration-fast);\n}\n\n.rk-disclosure-control:hover {\n color: var(--color-primary);\n}\n\n.rk-disclosure-control:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n.rk-disclosure-title {\n color: var(--color-ink);\n font-size: 1rem;\n font-weight: 500;\n}\n\n.rk-disclosure-mark {\n position: relative;\n display: grid;\n place-items: center;\n flex-shrink: 0;\n width: 1.5rem;\n height: 1.5rem;\n}\n\n.rk-disclosure-bar {\n position: absolute;\n height: 1px;\n width: 0.875rem;\n background: var(--color-ink-soft);\n}\n\n.rk-disclosure-turn {\n transform: rotate(90deg);\n transition: transform var(--duration-slower) ease-out;\n}\n\n.rk-disclosure-turn.is-open {\n transform: rotate(0deg);\n}\n\n/* 0fr to 1fr is the whole trick: a grid row can be animated to a height that\n was never measured, which `height: auto` cannot do. */\n.rk-disclosure-panel {\n display: grid;\n grid-template-rows: 0fr;\n transition: grid-template-rows var(--duration-slower) ease-out;\n}\n\n.rk-disclosure-panel.is-open {\n grid-template-rows: 1fr;\n}\n\n.rk-disclosure-clip {\n overflow: hidden;\n}\n\n.rk-disclosure-body {\n max-width: 68ch;\n padding-bottom: 1.5rem;\n color: var(--color-ink-soft);\n line-height: 1.625;\n opacity: 0;\n transition: opacity var(--duration-slow);\n}\n\n.rk-disclosure-body.is-open {\n opacity: 1;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .rk-disclosure-panel,\n .rk-disclosure-turn,\n .rk-disclosure-body {\n transition: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { useId } from 'vue'\n\n/**\n * One section that opens and closes.\n *\n * `BaseAccordion` is a list of these with the rule that only one stays open.\n * This is the same row on its own, for the common case where the list belongs\n * to the app — because it is staggered as it scrolls in, or interleaved with\n * something else, or rendered from a source the accordion cannot know about.\n *\n * That case is not hypothetical: the first list this kit met decorated every\n * row with a reveal directive, which has to sit on the element the accordion\n * would have owned. Shipping only the list would have meant choosing between\n * the component and the design.\n *\n * ## Why not `<details>`\n *\n * A browser removes `<details>` content from layout entirely when closed, so\n * there is nothing to animate between — the panel can only appear. Faking it by\n * leaving `open` set and hiding with CSS tells a screen reader the section is\n * expanded while a sighted reader sees it shut, which is worse than no\n * animation.\n *\n * So: a button carrying `aria-expanded`, the content always in the markup —\n * which is what a crawler and a reader without JavaScript get — and a grid row\n * travelling from `0fr` to `1fr`, the one way to animate to a height nobody has\n * measured.\n */\nconst { title = '', headingLevel = 3 } = defineProps<{\n /**\n * The question. Already translated.\n *\n * Optional only because the `title` slot is the other way to give one — pass\n * exactly one of them. A disclosure with neither has a control nobody can\n * read, which is why this is documented rather than defaulted to something.\n */\n title?: string | undefined\n /**\n * Which heading the control sits inside.\n *\n * A heading, always — it is how a screen reader user moves between sections\n * without opening any of them. The level depends on what is above it on the\n * page, and getting it wrong breaks the outline.\n */\n headingLevel?: 2 | 3 | 4 | undefined\n}>()\n\nconst open = defineModel<boolean>({ default: false })\n\ndefineSlots<{\n /** The answer. */\n default: () => unknown\n /**\n * The question, when the prop is not enough.\n *\n * A slot as well as a prop because the size of a heading is a decision about\n * the page it is on, not about disclosure: the first list to use this wanted\n * the question a step larger on a wide screen, and a component that hard-codes\n * one size makes that an override against its own stylesheet.\n */\n title?: () => unknown\n}>()\n\nconst id = useId()\n</script>\n\n<template>\n <div class=\"rk-disclosure\">\n <component :is=\"`h${headingLevel}`\">\n <button\n type=\"button\"\n class=\"rk-disclosure-control\"\n :aria-expanded=\"open\"\n :aria-controls=\"id\"\n @click=\"open = !open\"\n >\n <span class=\"rk-disclosure-title\">\n <slot name=\"title\">{{ title }}</slot>\n </span>\n\n <!-- A plus that becomes a minus: one stroke turns, so the control keeps\n its place while it changes meaning. -->\n <span class=\"rk-disclosure-mark\" aria-hidden=\"true\">\n <span class=\"rk-disclosure-bar\" />\n <span class=\"rk-disclosure-bar rk-disclosure-turn\" :class=\"{ 'is-open': open }\" />\n </span>\n </button>\n </component>\n\n <div :id=\"id\" class=\"rk-disclosure-panel\" :class=\"{ 'is-open': open }\">\n <div class=\"rk-disclosure-clip\">\n <div class=\"rk-disclosure-body\" :class=\"{ 'is-open': open }\"><slot /></div>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.rk-disclosure-control {\n display: flex;\n width: 100%;\n cursor: pointer;\n align-items: center;\n justify-content: space-between;\n gap: 1.5rem;\n padding: 1.25rem 0;\n text-align: left;\n transition: color var(--duration-fast);\n}\n\n.rk-disclosure-control:hover {\n color: var(--color-primary);\n}\n\n.rk-disclosure-control:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n.rk-disclosure-title {\n color: var(--color-ink);\n font-size: 1rem;\n font-weight: 500;\n}\n\n.rk-disclosure-mark {\n position: relative;\n display: grid;\n place-items: center;\n flex-shrink: 0;\n width: 1.5rem;\n height: 1.5rem;\n}\n\n.rk-disclosure-bar {\n position: absolute;\n height: 1px;\n width: 0.875rem;\n background: var(--color-ink-soft);\n}\n\n.rk-disclosure-turn {\n transform: rotate(90deg);\n transition: transform var(--duration-slower) ease-out;\n}\n\n.rk-disclosure-turn.is-open {\n transform: rotate(0deg);\n}\n\n/* 0fr to 1fr is the whole trick: a grid row can be animated to a height that\n was never measured, which `height: auto` cannot do. */\n.rk-disclosure-panel {\n display: grid;\n grid-template-rows: 0fr;\n transition: grid-template-rows var(--duration-slower) ease-out;\n}\n\n.rk-disclosure-panel.is-open {\n grid-template-rows: 1fr;\n}\n\n.rk-disclosure-clip {\n overflow: hidden;\n}\n\n.rk-disclosure-body {\n max-width: 68ch;\n padding-bottom: 1.5rem;\n color: var(--color-ink-soft);\n line-height: 1.625;\n opacity: 0;\n transition: opacity var(--duration-slow);\n}\n\n.rk-disclosure-body.is-open {\n opacity: 1;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .rk-disclosure-panel,\n .rk-disclosure-turn,\n .rk-disclosure-body {\n transition: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { nextTick, onBeforeUnmount, ref, watch } from 'vue'\n\nimport { inertOutside } from '../utils/inert'\n\n/**\n * A dialog that arrives from nowhere.\n *\n * Not `BaseSheet` with different padding. A sheet slides up from the bottom\n * edge, is dismissed by dragging it back down, and belongs to a thumb; a modal\n * appears in the middle of what you were reading and is dismissed by leaving\n * it. They are two answers to two questions and merging them would give a\n * component that is wrong on a phone and wrong on a desktop.\n *\n * ## The parts that are easy to get wrong\n *\n * This is why it exists rather than being written per dialog, which is what the\n * one wide consumer was doing in two places. None of the following is visible\n * on screen when it is missing:\n *\n * - **Focus moves in and comes back.** Opening without moving focus leaves a\n * keyboard user behind the dialog, operating a page they cannot see. Closing\n * without restoring it drops them at the top of the document.\n * - **Tab does not leave.** A dialog you can Tab out of is a dialog that is\n * modal only to somebody using a mouse.\n * - **Escape closes.** Every dialog in every application does this.\n * - **The page underneath does not scroll.** Otherwise dismissing the dialog\n * returns you somewhere else.\n * - **The backdrop closes it, the panel does not.** A click that starts inside\n * the panel and ends on the backdrop is not a click on the backdrop, which is\n * why this listens for a press on the backdrop itself rather than any click\n * that reaches it.\n */\nconst {\n title,\n closeLabel,\n tone = 'default',\n dismissible = true,\n} = defineProps<{\n /** Names the dialog for assistive tech. Required; an unnamed dialog is a box. */\n title: string\n /** Accessible name for the close button. */\n closeLabel: string\n /**\n * `alert` marks it as a decision that interrupts — `role=\"alertdialog\"`,\n * which a screen reader announces immediately rather than on arrival.\n */\n tone?: 'default' | 'alert' | undefined\n /**\n * Whether Escape and the backdrop close it.\n *\n * Off for a dialog that must be answered — but never off as a way of forcing\n * a reader to a decision they have not been given the information for.\n */\n dismissible?: boolean | undefined\n}>()\n\nconst open = defineModel<boolean>({ default: false })\n\ndefineSlots<{\n /** The body. */\n default: () => unknown\n /** Buttons, at the foot. */\n actions?: () => unknown\n}>()\n\nconst panel = ref<HTMLElement | null>(null)\n/** Who had focus before this opened, so it can be given back. */\nlet restoreTo: HTMLElement | null = null\n/** Gives the page behind back; set while open. */\nlet releaseInert: (() => void) | null = null\n\nconst FOCUSABLE =\n 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])'\n\nfunction focusable(): HTMLElement[] {\n if (!panel.value) return []\n\n return Array.from(panel.value.querySelectorAll<HTMLElement>(FOCUSABLE))\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n if (event.key === 'Escape' && dismissible) {\n event.stopPropagation()\n open.value = false\n\n return\n }\n\n if (event.key !== 'Tab') return\n\n const items = focusable()\n if (items.length === 0) return event.preventDefault()\n\n const first = items[0]!\n const last = items[items.length - 1]!\n const active = document.activeElement\n\n // The wrap has to be done by hand: the browser's own Tab order is the whole\n // document, and the dialog is only part of it.\n if (event.shiftKey && (active === first || !panel.value?.contains(active))) {\n event.preventDefault()\n last.focus()\n } else if (!event.shiftKey && active === last) {\n event.preventDefault()\n first.focus()\n }\n}\n\nwatch(open, async (isOpen) => {\n if (isOpen) {\n restoreTo = document.activeElement as HTMLElement | null\n document.body.style.overflow = 'hidden'\n await nextTick()\n // The Tab wrap above keeps focus cycling; `inert` is what also stops a\n // screen reader's virtual cursor, which walks past keydown handlers.\n if (panel.value) releaseInert = inertOutside(panel.value)\n // The panel itself when it holds nothing focusable, so focus is at least\n // inside the dialog rather than behind it.\n ;(focusable()[0] ?? panel.value)?.focus()\n } else {\n document.body.style.overflow = ''\n // Before focus goes back: an inert element cannot take it.\n releaseInert?.()\n releaseInert = null\n restoreTo?.focus()\n restoreTo = null\n }\n})\n\n// A dialog unmounted while open would otherwise leave the page unscrollable,\n// with nothing on screen to explain why.\nonBeforeUnmount(() => {\n if (open.value) document.body.style.overflow = ''\n releaseInert?.()\n})\n</script>\n\n<template>\n <Teleport to=\"body\">\n <Transition name=\"rk-modal\">\n <div\n v-if=\"open\"\n class=\"rk-modal-scrim\"\n @keydown=\"onKeydown\"\n @mousedown.self=\"dismissible && (open = false)\"\n >\n <div\n ref=\"panel\"\n class=\"rk-modal-panel surface-overlay\"\n :role=\"tone === 'alert' ? 'alertdialog' : 'dialog'\"\n aria-modal=\"true\"\n :aria-label=\"title\"\n tabindex=\"-1\"\n >\n <div class=\"rk-modal-head\">\n <h2 class=\"rk-modal-title\">{{ title }}</h2>\n\n <button\n v-if=\"dismissible\"\n type=\"button\"\n class=\"rk-modal-close\"\n :aria-label=\"closeLabel\"\n @click=\"open = false\"\n >\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M18 6 6 18M6 6l12 12\" stroke-linecap=\"round\" />\n </svg>\n </button>\n </div>\n\n <div class=\"rk-modal-body\"><slot /></div>\n\n <div v-if=\"$slots.actions\" class=\"rk-modal-actions\"><slot name=\"actions\" /></div>\n </div>\n </div>\n </Transition>\n </Teleport>\n</template>\n\n<style scoped>\n.rk-modal-scrim {\n position: fixed;\n inset: 0;\n z-index: 50;\n display: grid;\n place-items: center;\n padding: 1.5rem;\n background: color-mix(in srgb, var(--color-ink) 70%, transparent);\n backdrop-filter: blur(4px);\n}\n\n.rk-modal-panel {\n width: 100%;\n max-width: 28rem;\n max-height: 85vh;\n overflow-y: auto;\n border-radius: var(--radius-card);\n padding: 1.5rem;\n}\n\n.rk-modal-panel:focus {\n outline: none;\n}\n\n.rk-modal-head {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 1rem;\n}\n\n.rk-modal-title {\n font-size: 1.0625rem;\n font-weight: 600;\n color: var(--color-ink);\n}\n\n.rk-modal-close {\n display: grid;\n place-items: center;\n flex-shrink: 0;\n width: 2rem;\n height: 2rem;\n border-radius: 9999px;\n color: var(--color-ink-soft);\n transition: background-color var(--duration-fast);\n}\n\n.rk-modal-close:hover {\n background: var(--color-muted);\n}\n\n.rk-modal-close:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n.rk-modal-close svg {\n width: 1rem;\n height: 1rem;\n}\n\n.rk-modal-body {\n margin-top: 0.75rem;\n color: var(--color-ink-soft);\n font-size: 0.875rem;\n line-height: 1.625;\n}\n\n.rk-modal-actions {\n margin-top: 1.5rem;\n display: flex;\n justify-content: flex-end;\n gap: 0.5rem;\n}\n\n.rk-modal-enter-active,\n.rk-modal-leave-active {\n transition: opacity var(--duration-base) ease;\n}\n\n.rk-modal-enter-active .rk-modal-panel,\n.rk-modal-leave-active .rk-modal-panel {\n transition:\n transform var(--duration-base) var(--ease-sheet),\n opacity var(--duration-base) ease;\n}\n\n.rk-modal-enter-from,\n.rk-modal-leave-to {\n opacity: 0;\n}\n\n/* From nowhere, not from an edge: a scale is what separates this from a sheet. */\n.rk-modal-enter-from .rk-modal-panel,\n.rk-modal-leave-to .rk-modal-panel {\n transform: scale(0.96);\n opacity: 0;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .rk-modal-enter-active .rk-modal-panel,\n .rk-modal-leave-active .rk-modal-panel {\n transition: opacity var(--duration-base) ease;\n transform: none;\n }\n\n .rk-modal-enter-from .rk-modal-panel,\n .rk-modal-leave-to .rk-modal-panel {\n transform: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { nextTick, onBeforeUnmount, ref, watch } from 'vue'\n\nimport { inertOutside } from '../utils/inert'\n\n/**\n * A dialog that arrives from nowhere.\n *\n * Not `BaseSheet` with different padding. A sheet slides up from the bottom\n * edge, is dismissed by dragging it back down, and belongs to a thumb; a modal\n * appears in the middle of what you were reading and is dismissed by leaving\n * it. They are two answers to two questions and merging them would give a\n * component that is wrong on a phone and wrong on a desktop.\n *\n * ## The parts that are easy to get wrong\n *\n * This is why it exists rather than being written per dialog, which is what the\n * one wide consumer was doing in two places. None of the following is visible\n * on screen when it is missing:\n *\n * - **Focus moves in and comes back.** Opening without moving focus leaves a\n * keyboard user behind the dialog, operating a page they cannot see. Closing\n * without restoring it drops them at the top of the document.\n * - **Tab does not leave.** A dialog you can Tab out of is a dialog that is\n * modal only to somebody using a mouse.\n * - **Escape closes.** Every dialog in every application does this.\n * - **The page underneath does not scroll.** Otherwise dismissing the dialog\n * returns you somewhere else.\n * - **The backdrop closes it, the panel does not.** A click that starts inside\n * the panel and ends on the backdrop is not a click on the backdrop, which is\n * why this listens for a press on the backdrop itself rather than any click\n * that reaches it.\n */\nconst {\n title,\n closeLabel,\n tone = 'default',\n dismissible = true,\n} = defineProps<{\n /** Names the dialog for assistive tech. Required; an unnamed dialog is a box. */\n title: string\n /** Accessible name for the close button. */\n closeLabel: string\n /**\n * `alert` marks it as a decision that interrupts — `role=\"alertdialog\"`,\n * which a screen reader announces immediately rather than on arrival.\n */\n tone?: 'default' | 'alert' | undefined\n /**\n * Whether Escape and the backdrop close it.\n *\n * Off for a dialog that must be answered — but never off as a way of forcing\n * a reader to a decision they have not been given the information for.\n */\n dismissible?: boolean | undefined\n}>()\n\nconst open = defineModel<boolean>({ default: false })\n\ndefineSlots<{\n /** The body. */\n default: () => unknown\n /** Buttons, at the foot. */\n actions?: () => unknown\n}>()\n\nconst panel = ref<HTMLElement | null>(null)\n/** Who had focus before this opened, so it can be given back. */\nlet restoreTo: HTMLElement | null = null\n/** Gives the page behind back; set while open. */\nlet releaseInert: (() => void) | null = null\n\nconst FOCUSABLE =\n 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])'\n\nfunction focusable(): HTMLElement[] {\n if (!panel.value) return []\n\n return Array.from(panel.value.querySelectorAll<HTMLElement>(FOCUSABLE))\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n if (event.key === 'Escape' && dismissible) {\n event.stopPropagation()\n open.value = false\n\n return\n }\n\n if (event.key !== 'Tab') return\n\n const items = focusable()\n if (items.length === 0) return event.preventDefault()\n\n const first = items[0]!\n const last = items[items.length - 1]!\n const active = document.activeElement\n\n // The wrap has to be done by hand: the browser's own Tab order is the whole\n // document, and the dialog is only part of it.\n if (event.shiftKey && (active === first || !panel.value?.contains(active))) {\n event.preventDefault()\n last.focus()\n } else if (!event.shiftKey && active === last) {\n event.preventDefault()\n first.focus()\n }\n}\n\nwatch(open, async (isOpen) => {\n if (isOpen) {\n restoreTo = document.activeElement as HTMLElement | null\n document.body.style.overflow = 'hidden'\n await nextTick()\n // The Tab wrap above keeps focus cycling; `inert` is what also stops a\n // screen reader's virtual cursor, which walks past keydown handlers.\n if (panel.value) releaseInert = inertOutside(panel.value)\n // The panel itself when it holds nothing focusable, so focus is at least\n // inside the dialog rather than behind it.\n ;(focusable()[0] ?? panel.value)?.focus()\n } else {\n document.body.style.overflow = ''\n // Before focus goes back: an inert element cannot take it.\n releaseInert?.()\n releaseInert = null\n restoreTo?.focus()\n restoreTo = null\n }\n})\n\n// A dialog unmounted while open would otherwise leave the page unscrollable,\n// with nothing on screen to explain why.\nonBeforeUnmount(() => {\n if (open.value) document.body.style.overflow = ''\n releaseInert?.()\n})\n</script>\n\n<template>\n <Teleport to=\"body\">\n <Transition name=\"rk-modal\">\n <div\n v-if=\"open\"\n class=\"rk-modal-scrim\"\n @keydown=\"onKeydown\"\n @mousedown.self=\"dismissible && (open = false)\"\n >\n <div\n ref=\"panel\"\n class=\"rk-modal-panel surface-overlay\"\n :role=\"tone === 'alert' ? 'alertdialog' : 'dialog'\"\n aria-modal=\"true\"\n :aria-label=\"title\"\n tabindex=\"-1\"\n >\n <div class=\"rk-modal-head\">\n <h2 class=\"rk-modal-title\">{{ title }}</h2>\n\n <button\n v-if=\"dismissible\"\n type=\"button\"\n class=\"rk-modal-close\"\n :aria-label=\"closeLabel\"\n @click=\"open = false\"\n >\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M18 6 6 18M6 6l12 12\" stroke-linecap=\"round\" />\n </svg>\n </button>\n </div>\n\n <div class=\"rk-modal-body\"><slot /></div>\n\n <div v-if=\"$slots.actions\" class=\"rk-modal-actions\"><slot name=\"actions\" /></div>\n </div>\n </div>\n </Transition>\n </Teleport>\n</template>\n\n<style scoped>\n.rk-modal-scrim {\n position: fixed;\n inset: 0;\n z-index: 50;\n display: grid;\n place-items: center;\n padding: 1.5rem;\n background: color-mix(in srgb, var(--color-ink) 70%, transparent);\n backdrop-filter: blur(4px);\n}\n\n.rk-modal-panel {\n width: 100%;\n max-width: 28rem;\n max-height: 85vh;\n overflow-y: auto;\n border-radius: var(--radius-card);\n padding: 1.5rem;\n}\n\n.rk-modal-panel:focus {\n outline: none;\n}\n\n.rk-modal-head {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 1rem;\n}\n\n.rk-modal-title {\n font-size: 1.0625rem;\n font-weight: 600;\n color: var(--color-ink);\n}\n\n.rk-modal-close {\n display: grid;\n place-items: center;\n flex-shrink: 0;\n width: 2rem;\n height: 2rem;\n border-radius: 9999px;\n color: var(--color-ink-soft);\n transition: background-color var(--duration-fast);\n}\n\n.rk-modal-close:hover {\n background: var(--color-muted);\n}\n\n.rk-modal-close:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n.rk-modal-close svg {\n width: 1rem;\n height: 1rem;\n}\n\n.rk-modal-body {\n margin-top: 0.75rem;\n color: var(--color-ink-soft);\n font-size: 0.875rem;\n line-height: 1.625;\n}\n\n.rk-modal-actions {\n margin-top: 1.5rem;\n display: flex;\n justify-content: flex-end;\n gap: 0.5rem;\n}\n\n.rk-modal-enter-active,\n.rk-modal-leave-active {\n transition: opacity var(--duration-base) ease;\n}\n\n.rk-modal-enter-active .rk-modal-panel,\n.rk-modal-leave-active .rk-modal-panel {\n transition:\n transform var(--duration-base) var(--ease-sheet),\n opacity var(--duration-base) ease;\n}\n\n.rk-modal-enter-from,\n.rk-modal-leave-to {\n opacity: 0;\n}\n\n/* From nowhere, not from an edge: a scale is what separates this from a sheet. */\n.rk-modal-enter-from .rk-modal-panel,\n.rk-modal-leave-to .rk-modal-panel {\n transform: scale(0.96);\n opacity: 0;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .rk-modal-enter-active .rk-modal-panel,\n .rk-modal-leave-active .rk-modal-panel {\n transition: opacity var(--duration-base) ease;\n transform: none;\n }\n\n .rk-modal-enter-from .rk-modal-panel,\n .rk-modal-leave-to .rk-modal-panel {\n transform: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\n/**\n * Moving between pages of a list.\n *\n * A `nav` of buttons rather than links, because this component does not know\n * whether the app pages by route, by query or in memory. It emits a number and\n * the app decides what that means.\n *\n * ## The ellipsis\n *\n * Every page number is unusable past about a dozen, and cutting to\n * \"1 … 7 8 9 … 40\" is what every list does. The window is a fixed size around\n * the current page and the first and last are always present, so the control\n * never changes width as you move through it — a row of numbers that reflows\n * under the pointer is a row you have to re-aim at.\n *\n * The gap is a `<span>`, not a disabled button: it is not a control, and\n * announcing it as one gives a screen reader something to try to press.\n */\nconst {\n page,\n pages,\n siblings = 1,\n label = '',\n previousLabel,\n nextLabel,\n} = defineProps<{\n /** Current page, 1-based. */\n page: number\n /** How many there are. */\n pages: number\n /** How many numbers to show either side of the current one. */\n siblings?: number | undefined\n /** Accessible name for the landmark. */\n label?: string | undefined\n /**\n * Names for the arrows, already translated.\n *\n * Required, because `‹` is a glyph and not a word: a screen reader reading\n * the arrow announces nothing a person can act on.\n */\n previousLabel: string\n nextLabel: string\n}>()\n\nconst emit = defineEmits<{ change: [page: number] }>()\n\nconst GAP = 'gap' as const\n\nconst slots = computed<(number | typeof GAP)[]>(() => {\n if (pages <= 1) return [1]\n\n const first = 1\n const last = pages\n\n /* The widest the windowed form ever gets: first, last, the current page, its\n siblings either side, and the two gaps. Below that the window saves\n nothing, and \"1 2 … 5\" is a worse control than \"1 2 3 4 5\" — so short lists\n are shown whole rather than cut on principle. */\n if (pages <= siblings * 2 + 5) {\n return Array.from({ length: pages }, (_, i) => i + 1)\n }\n const from = Math.max(first + 1, page - siblings)\n const to = Math.min(last - 1, page + siblings)\n\n const out: (number | typeof GAP)[] = [first]\n // A gap standing in for a single page is longer than the page it replaces.\n if (from > first + 1) out.push(GAP)\n for (let n = from; n <= to; n++) out.push(n)\n if (to < last - 1) out.push(GAP)\n if (last > first) out.push(last)\n\n return out\n})\n\nconst go = (next: number) => {\n if (next >= 1 && next <= pages && next !== page) emit('change', next)\n}\n</script>\n\n<template>\n <nav class=\"rk-pager\" :aria-label=\"label || undefined\">\n <button\n type=\"button\"\n class=\"rk-pager-step\"\n :disabled=\"page <= 1\"\n :aria-label=\"previousLabel\"\n @click=\"go(page - 1)\"\n >\n ‹\n </button>\n\n <template v-for=\"(slot, index) in slots\" :key=\"`${slot}-${index}`\">\n <span v-if=\"slot === 'gap'\" class=\"rk-pager-gap\" aria-hidden=\"true\">…</span>\n <button\n v-else\n type=\"button\"\n class=\"rk-pager-page\"\n :class=\"{ 'is-active': slot === page }\"\n :aria-current=\"slot === page ? 'page' : undefined\"\n @click=\"go(slot)\"\n >\n {{ slot }}\n </button>\n </template>\n\n <button\n type=\"button\"\n class=\"rk-pager-step\"\n :disabled=\"page >= pages\"\n :aria-label=\"nextLabel\"\n @click=\"go(page + 1)\"\n >\n ›\n </button>\n </nav>\n</template>\n\n<style scoped>\n.rk-pager {\n display: flex;\n align-items: center;\n gap: 0.25rem;\n}\n\n.rk-pager-step,\n.rk-pager-page {\n display: grid;\n place-items: center;\n min-width: 2rem;\n height: 2rem;\n padding: 0 0.5rem;\n border-radius: var(--radius-cell);\n font-size: 0.875rem;\n color: var(--color-ink-soft);\n transition:\n background-color var(--duration-fast),\n color var(--duration-fast);\n}\n\n.rk-pager-step:hover:not(:disabled),\n.rk-pager-page:hover {\n background: var(--color-muted);\n color: var(--color-ink);\n}\n\n.rk-pager-step:focus-visible,\n.rk-pager-page:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n.rk-pager-step:disabled {\n opacity: 0.4;\n pointer-events: none;\n}\n\n.rk-pager-page.is-active {\n background: var(--color-primary);\n color: var(--color-on-primary);\n font-weight: 500;\n}\n\n.rk-pager-gap {\n display: grid;\n place-items: center;\n min-width: 2rem;\n height: 2rem;\n color: var(--color-ink-soft);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\n/**\n * Moving between pages of a list.\n *\n * A `nav` of buttons rather than links, because this component does not know\n * whether the app pages by route, by query or in memory. It emits a number and\n * the app decides what that means.\n *\n * ## The ellipsis\n *\n * Every page number is unusable past about a dozen, and cutting to\n * \"1 … 7 8 9 … 40\" is what every list does. The window is a fixed size around\n * the current page and the first and last are always present, so the control\n * never changes width as you move through it — a row of numbers that reflows\n * under the pointer is a row you have to re-aim at.\n *\n * The gap is a `<span>`, not a disabled button: it is not a control, and\n * announcing it as one gives a screen reader something to try to press.\n */\nconst {\n page,\n pages,\n siblings = 1,\n label = '',\n previousLabel,\n nextLabel,\n} = defineProps<{\n /** Current page, 1-based. */\n page: number\n /** How many there are. */\n pages: number\n /** How many numbers to show either side of the current one. */\n siblings?: number | undefined\n /** Accessible name for the landmark. */\n label?: string | undefined\n /**\n * Names for the arrows, already translated.\n *\n * Required, because `‹` is a glyph and not a word: a screen reader reading\n * the arrow announces nothing a person can act on.\n */\n previousLabel: string\n nextLabel: string\n}>()\n\nconst emit = defineEmits<{ change: [page: number] }>()\n\nconst GAP = 'gap' as const\n\nconst slots = computed<(number | typeof GAP)[]>(() => {\n if (pages <= 1) return [1]\n\n const first = 1\n const last = pages\n\n /* The widest the windowed form ever gets: first, last, the current page, its\n siblings either side, and the two gaps. Below that the window saves\n nothing, and \"1 2 … 5\" is a worse control than \"1 2 3 4 5\" — so short lists\n are shown whole rather than cut on principle. */\n if (pages <= siblings * 2 + 5) {\n return Array.from({ length: pages }, (_, i) => i + 1)\n }\n const from = Math.max(first + 1, page - siblings)\n const to = Math.min(last - 1, page + siblings)\n\n const out: (number | typeof GAP)[] = [first]\n // A gap standing in for a single page is longer than the page it replaces.\n if (from > first + 1) out.push(GAP)\n for (let n = from; n <= to; n++) out.push(n)\n if (to < last - 1) out.push(GAP)\n if (last > first) out.push(last)\n\n return out\n})\n\nconst go = (next: number) => {\n if (next >= 1 && next <= pages && next !== page) emit('change', next)\n}\n</script>\n\n<template>\n <nav class=\"rk-pager\" :aria-label=\"label || undefined\">\n <button\n type=\"button\"\n class=\"rk-pager-step\"\n :disabled=\"page <= 1\"\n :aria-label=\"previousLabel\"\n @click=\"go(page - 1)\"\n >\n ‹\n </button>\n\n <template v-for=\"(slot, index) in slots\" :key=\"`${slot}-${index}`\">\n <span v-if=\"slot === 'gap'\" class=\"rk-pager-gap\" aria-hidden=\"true\">…</span>\n <button\n v-else\n type=\"button\"\n class=\"rk-pager-page\"\n :class=\"{ 'is-active': slot === page }\"\n :aria-current=\"slot === page ? 'page' : undefined\"\n @click=\"go(slot)\"\n >\n {{ slot }}\n </button>\n </template>\n\n <button\n type=\"button\"\n class=\"rk-pager-step\"\n :disabled=\"page >= pages\"\n :aria-label=\"nextLabel\"\n @click=\"go(page + 1)\"\n >\n ›\n </button>\n </nav>\n</template>\n\n<style scoped>\n.rk-pager {\n display: flex;\n align-items: center;\n gap: 0.25rem;\n}\n\n.rk-pager-step,\n.rk-pager-page {\n display: grid;\n place-items: center;\n min-width: 2rem;\n height: 2rem;\n padding: 0 0.5rem;\n border-radius: var(--radius-cell);\n font-size: 0.875rem;\n color: var(--color-ink-soft);\n transition:\n background-color var(--duration-fast),\n color var(--duration-fast);\n}\n\n.rk-pager-step:hover:not(:disabled),\n.rk-pager-page:hover {\n background: var(--color-muted);\n color: var(--color-ink);\n}\n\n.rk-pager-step:focus-visible,\n.rk-pager-page:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n.rk-pager-step:disabled {\n opacity: 0.4;\n pointer-events: none;\n}\n\n.rk-pager-page.is-active {\n background: var(--color-primary);\n color: var(--color-on-primary);\n font-weight: 500;\n}\n\n.rk-pager-gap {\n display: grid;\n place-items: center;\n min-width: 2rem;\n height: 2rem;\n color: var(--color-ink-soft);\n}\n</style>\n","<script setup lang=\"ts\" generic=\"Id extends string\">\nimport { Search } from 'lucide-vue-next'\nimport { computed, nextTick, onBeforeUnmount, ref, useId, useTemplateRef, watch } from 'vue'\nimport type { Component } from 'vue'\n\nimport { inertOutside } from '../utils/inert'\nimport { ensureSheetRoot } from '../utils/sheet-root'\n\nexport interface CommandItem<Id extends string> {\n id: Id\n /** Already translated. */\n label: string\n /** A line under it, or a shortcut to show on the right. */\n hint?: string | undefined\n icon?: Component | undefined\n /** Extra words that should find it — synonyms, the English name. */\n keywords?: readonly string[] | undefined\n}\n\nexport interface CommandGroup<Id extends string> {\n /** Already translated. Omitted, the items sit without a heading. */\n label?: string | undefined\n items: readonly CommandItem<Id>[]\n}\n\n/**\n * Everything the app can do, behind one shortcut.\n *\n * The pattern people know from their editor: a dialog with a field at the\n * top, results under it, arrows to move and Enter to run. It is the fastest\n * way to reach the twelfth thing in a menu, and the only way that does not\n * cost a menu.\n *\n * The field is a combobox and the results are its listbox, which is what\n * makes it work with a screen reader: focus stays in the field the whole\n * time and `aria-activedescendant` says which result is current, so typing\n * and moving do not fight each other.\n *\n * `hotkey` is the letter pressed with ⌘ or Ctrl — 'k' for the usual one.\n * Matching is over the label and the `keywords` beside it, so \"new\" can find\n * \"Write an entry\" in an app whose language is not English.\n */\nconst {\n groups,\n label,\n placeholder,\n emptyLabel,\n hotkey = undefined,\n} = defineProps<{\n groups: readonly CommandGroup<Id>[]\n /** The dialog's accessible name, e.g. \"Commands\". */\n label: string\n /** The field's placeholder, e.g. \"Type a command or search\". */\n placeholder: string\n /** Shown when nothing matches, e.g. \"Nothing found\". */\n emptyLabel: string\n /** The letter that opens it with ⌘ or Ctrl. Unset, the app opens it itself. */\n hotkey?: string | undefined\n}>()\n\nconst emit = defineEmits<{ select: [id: Id] }>()\n\n/** Open or closed, with `v-model`. */\nconst open = defineModel<boolean>({ default: false })\n\nconst query = ref('')\nconst active = ref(0)\nconst id = useId()\nconst field = useTemplateRef<HTMLInputElement>('field')\n\nconst sheetRoot = ensureSheetRoot()\nlet release: (() => void) | null = null\n\nconst matches = computed(() => {\n const needle = query.value.trim().toLowerCase()\n const keep = (item: CommandItem<Id>) =>\n needle === '' ||\n [item.label, ...(item.keywords ?? [])].some((word) => word.toLowerCase().includes(needle))\n\n return groups\n .map((group) => ({ ...group, items: group.items.filter(keep) }))\n .filter((group) => group.items.length > 0)\n})\n\n/** Flattened, because the arrows walk the results and not the groups. */\nconst flat = computed(() => matches.value.flatMap((group) => group.items))\n\nfunction choose(item: CommandItem<Id>) {\n emit('select', item.id)\n open.value = false\n}\n\nfunction move(step: number) {\n if (flat.value.length === 0) return\n active.value = (active.value + step + flat.value.length) % flat.value.length\n\n // `?.()`: scrollIntoView is not everywhere — jsdom has no layout, and a\n // missing scroll must not take the keyboard down with it.\n void nextTick(() => {\n document.getElementById(`${id}-${active.value}`)?.scrollIntoView?.({ block: 'nearest' })\n })\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n if (event.key === 'ArrowDown') {\n event.preventDefault()\n move(1)\n } else if (event.key === 'ArrowUp') {\n event.preventDefault()\n move(-1)\n } else if (event.key === 'Home') {\n event.preventDefault()\n active.value = 0\n } else if (event.key === 'End') {\n event.preventDefault()\n active.value = Math.max(0, flat.value.length - 1)\n } else if (event.key === 'Enter') {\n const item = flat.value[active.value]\n if (item) {\n event.preventDefault()\n choose(item)\n }\n } else if (event.key === 'Escape') {\n event.preventDefault()\n open.value = false\n }\n}\n\n/* The shortcut is global, because a command menu you have to find a button\n for is a menu, not a shortcut. */\nfunction onWindowKeydown(event: KeyboardEvent) {\n if (!hotkey || event.key.toLowerCase() !== hotkey.toLowerCase()) return\n if (!event.metaKey && !event.ctrlKey) return\n\n event.preventDefault()\n open.value = !open.value\n}\n\nif (typeof window !== 'undefined') window.addEventListener('keydown', onWindowKeydown)\n\n/* `immediate`, because a palette can be mounted already open — driven from a\n route, or in a test. Without it that one never takes focus and never holds\n the page: it looks open and behaves like a div. */\nwatch(\n open,\n async (isOpen) => {\n if (typeof document === 'undefined') return\n\n if (!isOpen) {\n release?.()\n release = null\n return\n }\n\n query.value = ''\n active.value = 0\n await nextTick()\n field.value?.focus()\n const panel = field.value?.closest('.rk-command-panel')\n if (panel) release = inertOutside(panel)\n },\n { immediate: true },\n)\n\nwatch(query, () => (active.value = 0))\n\nonBeforeUnmount(() => {\n release?.()\n if (typeof window !== 'undefined') window.removeEventListener('keydown', onWindowKeydown)\n})\n\nconst indexOf = (item: CommandItem<Id>) => flat.value.indexOf(item)\n</script>\n\n<template>\n <Teleport :to=\"sheetRoot\">\n <Transition name=\"rk-command\">\n <div v-if=\"open\" class=\"rk-command\" @keydown=\"onKeydown\">\n <div class=\"rk-command-scrim\" @click=\"open = false\" />\n\n <div\n class=\"rk-command-panel surface-overlay\"\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-label=\"label\"\n >\n <div class=\"rk-command-field\">\n <Search class=\"text-ink-soft size-4 shrink-0\" aria-hidden=\"true\" />\n <input\n ref=\"field\"\n v-model=\"query\"\n type=\"text\"\n role=\"combobox\"\n class=\"rk-command-input\"\n autocomplete=\"off\"\n :placeholder=\"placeholder\"\n :aria-label=\"label\"\n :aria-expanded=\"true\"\n :aria-controls=\"id\"\n :aria-activedescendant=\"flat.length ? `${id}-${active}` : undefined\"\n />\n </div>\n\n <div :id=\"id\" class=\"rk-command-list\" role=\"listbox\" :aria-label=\"label\">\n <template v-for=\"group in matches\" :key=\"group.label ?? 'items'\">\n <p v-if=\"group.label\" class=\"rk-command-group\">{{ group.label }}</p>\n\n <div\n v-for=\"item in group.items\"\n :id=\"`${id}-${indexOf(item)}`\"\n :key=\"item.id\"\n class=\"rk-command-item\"\n :class=\"{ 'is-active': indexOf(item) === active }\"\n role=\"option\"\n :aria-selected=\"indexOf(item) === active\"\n @click=\"choose(item)\"\n @pointermove=\"active = indexOf(item)\"\n >\n <component\n :is=\"item.icon\"\n v-if=\"item.icon\"\n class=\"size-4 shrink-0\"\n aria-hidden=\"true\"\n />\n <span class=\"truncate\">{{ item.label }}</span>\n <span v-if=\"item.hint\" class=\"rk-command-hint\">{{ item.hint }}</span>\n </div>\n </template>\n\n <p v-if=\"flat.length === 0\" class=\"rk-command-empty\">{{ emptyLabel }}</p>\n </div>\n </div>\n </div>\n </Transition>\n </Teleport>\n</template>\n\n<style scoped>\n.rk-command {\n position: fixed;\n inset: 0;\n z-index: 90;\n display: flex;\n justify-content: center;\n padding: 1rem;\n /* Near the top, not centred: the eye is already up there, and the list\n grows downwards into space rather than pushing itself off both edges. */\n padding-top: min(12vh, 6rem);\n}\n\n.rk-command-scrim {\n position: absolute;\n inset: 0;\n background: color-mix(in oklab, var(--color-ink) 45%, transparent);\n backdrop-filter: blur(2px);\n}\n\n.rk-command-panel {\n position: relative;\n display: flex;\n width: 100%;\n max-width: 34rem;\n max-height: min(28rem, 70vh);\n flex-direction: column;\n overflow: hidden;\n border-radius: var(--radius-card);\n}\n\n.rk-command-field {\n display: flex;\n align-items: center;\n gap: 0.625rem;\n border-bottom: 1px solid var(--surface-border-color);\n padding: 0.875rem 1rem;\n}\n\n.rk-command-input {\n min-width: 0;\n flex: 1;\n background: transparent;\n font-size: 1rem;\n color: var(--color-ink);\n outline: none;\n}\n\n/* The field takes focus the moment this opens, so a full ring around it\n would be on the whole time. The rule under it turns instead: enough to\n say where the keyboard is, quiet enough to live with. */\n.rk-command-field:has(.rk-command-input:focus-visible) {\n border-bottom-color: var(--color-primary);\n box-shadow: inset 0 -1px 0 var(--color-primary);\n}\n\n.rk-command-list {\n overflow-y: auto;\n padding: 0.375rem;\n}\n\n.rk-command-group {\n padding: 0.5rem 0.625rem 0.25rem;\n font-size: 0.6875rem;\n font-weight: 600;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--color-ink-soft);\n}\n\n.rk-command-item {\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.rk-command-item.is-active {\n background: var(--color-muted);\n}\n\n.rk-command-hint {\n margin-left: auto;\n flex-shrink: 0;\n font-size: 0.75rem;\n color: var(--color-ink-soft);\n}\n\n.rk-command-empty {\n padding: 1.5rem 0.625rem;\n text-align: center;\n font-size: 0.875rem;\n color: var(--color-ink-soft);\n}\n\n.rk-command-enter-active,\n.rk-command-leave-active {\n transition: opacity var(--duration-fast) var(--ease-standard);\n}\n\n.rk-command-enter-active .rk-command-panel,\n.rk-command-leave-active .rk-command-panel {\n transition: transform var(--duration-fast) var(--ease-standard);\n}\n\n.rk-command-enter-from,\n.rk-command-leave-to {\n opacity: 0;\n}\n\n.rk-command-enter-from .rk-command-panel,\n.rk-command-leave-to .rk-command-panel {\n transform: translateY(-0.5rem) scale(0.98);\n}\n</style>\n","<script setup lang=\"ts\" generic=\"Id extends string\">\nimport { Search } from 'lucide-vue-next'\nimport { computed, nextTick, onBeforeUnmount, ref, useId, useTemplateRef, watch } from 'vue'\nimport type { Component } from 'vue'\n\nimport { inertOutside } from '../utils/inert'\nimport { ensureSheetRoot } from '../utils/sheet-root'\n\nexport interface CommandItem<Id extends string> {\n id: Id\n /** Already translated. */\n label: string\n /** A line under it, or a shortcut to show on the right. */\n hint?: string | undefined\n icon?: Component | undefined\n /** Extra words that should find it — synonyms, the English name. */\n keywords?: readonly string[] | undefined\n}\n\nexport interface CommandGroup<Id extends string> {\n /** Already translated. Omitted, the items sit without a heading. */\n label?: string | undefined\n items: readonly CommandItem<Id>[]\n}\n\n/**\n * Everything the app can do, behind one shortcut.\n *\n * The pattern people know from their editor: a dialog with a field at the\n * top, results under it, arrows to move and Enter to run. It is the fastest\n * way to reach the twelfth thing in a menu, and the only way that does not\n * cost a menu.\n *\n * The field is a combobox and the results are its listbox, which is what\n * makes it work with a screen reader: focus stays in the field the whole\n * time and `aria-activedescendant` says which result is current, so typing\n * and moving do not fight each other.\n *\n * `hotkey` is the letter pressed with ⌘ or Ctrl — 'k' for the usual one.\n * Matching is over the label and the `keywords` beside it, so \"new\" can find\n * \"Write an entry\" in an app whose language is not English.\n */\nconst {\n groups,\n label,\n placeholder,\n emptyLabel,\n hotkey = undefined,\n} = defineProps<{\n groups: readonly CommandGroup<Id>[]\n /** The dialog's accessible name, e.g. \"Commands\". */\n label: string\n /** The field's placeholder, e.g. \"Type a command or search\". */\n placeholder: string\n /** Shown when nothing matches, e.g. \"Nothing found\". */\n emptyLabel: string\n /** The letter that opens it with ⌘ or Ctrl. Unset, the app opens it itself. */\n hotkey?: string | undefined\n}>()\n\nconst emit = defineEmits<{ select: [id: Id] }>()\n\n/** Open or closed, with `v-model`. */\nconst open = defineModel<boolean>({ default: false })\n\nconst query = ref('')\nconst active = ref(0)\nconst id = useId()\nconst field = useTemplateRef<HTMLInputElement>('field')\n\nconst sheetRoot = ensureSheetRoot()\nlet release: (() => void) | null = null\n\nconst matches = computed(() => {\n const needle = query.value.trim().toLowerCase()\n const keep = (item: CommandItem<Id>) =>\n needle === '' ||\n [item.label, ...(item.keywords ?? [])].some((word) => word.toLowerCase().includes(needle))\n\n return groups\n .map((group) => ({ ...group, items: group.items.filter(keep) }))\n .filter((group) => group.items.length > 0)\n})\n\n/** Flattened, because the arrows walk the results and not the groups. */\nconst flat = computed(() => matches.value.flatMap((group) => group.items))\n\nfunction choose(item: CommandItem<Id>) {\n emit('select', item.id)\n open.value = false\n}\n\nfunction move(step: number) {\n if (flat.value.length === 0) return\n active.value = (active.value + step + flat.value.length) % flat.value.length\n\n // `?.()`: scrollIntoView is not everywhere — jsdom has no layout, and a\n // missing scroll must not take the keyboard down with it.\n void nextTick(() => {\n document.getElementById(`${id}-${active.value}`)?.scrollIntoView?.({ block: 'nearest' })\n })\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n if (event.key === 'ArrowDown') {\n event.preventDefault()\n move(1)\n } else if (event.key === 'ArrowUp') {\n event.preventDefault()\n move(-1)\n } else if (event.key === 'Home') {\n event.preventDefault()\n active.value = 0\n } else if (event.key === 'End') {\n event.preventDefault()\n active.value = Math.max(0, flat.value.length - 1)\n } else if (event.key === 'Enter') {\n const item = flat.value[active.value]\n if (item) {\n event.preventDefault()\n choose(item)\n }\n } else if (event.key === 'Escape') {\n event.preventDefault()\n open.value = false\n }\n}\n\n/* The shortcut is global, because a command menu you have to find a button\n for is a menu, not a shortcut. */\nfunction onWindowKeydown(event: KeyboardEvent) {\n if (!hotkey || event.key.toLowerCase() !== hotkey.toLowerCase()) return\n if (!event.metaKey && !event.ctrlKey) return\n\n event.preventDefault()\n open.value = !open.value\n}\n\nif (typeof window !== 'undefined') window.addEventListener('keydown', onWindowKeydown)\n\n/* `immediate`, because a palette can be mounted already open — driven from a\n route, or in a test. Without it that one never takes focus and never holds\n the page: it looks open and behaves like a div. */\nwatch(\n open,\n async (isOpen) => {\n if (typeof document === 'undefined') return\n\n if (!isOpen) {\n release?.()\n release = null\n return\n }\n\n query.value = ''\n active.value = 0\n await nextTick()\n field.value?.focus()\n const panel = field.value?.closest('.rk-command-panel')\n if (panel) release = inertOutside(panel)\n },\n { immediate: true },\n)\n\nwatch(query, () => (active.value = 0))\n\nonBeforeUnmount(() => {\n release?.()\n if (typeof window !== 'undefined') window.removeEventListener('keydown', onWindowKeydown)\n})\n\nconst indexOf = (item: CommandItem<Id>) => flat.value.indexOf(item)\n</script>\n\n<template>\n <Teleport :to=\"sheetRoot\">\n <Transition name=\"rk-command\">\n <div v-if=\"open\" class=\"rk-command\" @keydown=\"onKeydown\">\n <div class=\"rk-command-scrim\" @click=\"open = false\" />\n\n <div\n class=\"rk-command-panel surface-overlay\"\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-label=\"label\"\n >\n <div class=\"rk-command-field\">\n <Search class=\"text-ink-soft size-4 shrink-0\" aria-hidden=\"true\" />\n <input\n ref=\"field\"\n v-model=\"query\"\n type=\"text\"\n role=\"combobox\"\n class=\"rk-command-input\"\n autocomplete=\"off\"\n :placeholder=\"placeholder\"\n :aria-label=\"label\"\n :aria-expanded=\"true\"\n :aria-controls=\"id\"\n :aria-activedescendant=\"flat.length ? `${id}-${active}` : undefined\"\n />\n </div>\n\n <div :id=\"id\" class=\"rk-command-list\" role=\"listbox\" :aria-label=\"label\">\n <template v-for=\"group in matches\" :key=\"group.label ?? 'items'\">\n <p v-if=\"group.label\" class=\"rk-command-group\">{{ group.label }}</p>\n\n <div\n v-for=\"item in group.items\"\n :id=\"`${id}-${indexOf(item)}`\"\n :key=\"item.id\"\n class=\"rk-command-item\"\n :class=\"{ 'is-active': indexOf(item) === active }\"\n role=\"option\"\n :aria-selected=\"indexOf(item) === active\"\n @click=\"choose(item)\"\n @pointermove=\"active = indexOf(item)\"\n >\n <component\n :is=\"item.icon\"\n v-if=\"item.icon\"\n class=\"size-4 shrink-0\"\n aria-hidden=\"true\"\n />\n <span class=\"truncate\">{{ item.label }}</span>\n <span v-if=\"item.hint\" class=\"rk-command-hint\">{{ item.hint }}</span>\n </div>\n </template>\n\n <p v-if=\"flat.length === 0\" class=\"rk-command-empty\">{{ emptyLabel }}</p>\n </div>\n </div>\n </div>\n </Transition>\n </Teleport>\n</template>\n\n<style scoped>\n.rk-command {\n position: fixed;\n inset: 0;\n z-index: 90;\n display: flex;\n justify-content: center;\n padding: 1rem;\n /* Near the top, not centred: the eye is already up there, and the list\n grows downwards into space rather than pushing itself off both edges. */\n padding-top: min(12vh, 6rem);\n}\n\n.rk-command-scrim {\n position: absolute;\n inset: 0;\n background: color-mix(in oklab, var(--color-ink) 45%, transparent);\n backdrop-filter: blur(2px);\n}\n\n.rk-command-panel {\n position: relative;\n display: flex;\n width: 100%;\n max-width: 34rem;\n max-height: min(28rem, 70vh);\n flex-direction: column;\n overflow: hidden;\n border-radius: var(--radius-card);\n}\n\n.rk-command-field {\n display: flex;\n align-items: center;\n gap: 0.625rem;\n border-bottom: 1px solid var(--surface-border-color);\n padding: 0.875rem 1rem;\n}\n\n.rk-command-input {\n min-width: 0;\n flex: 1;\n background: transparent;\n font-size: 1rem;\n color: var(--color-ink);\n outline: none;\n}\n\n/* The field takes focus the moment this opens, so a full ring around it\n would be on the whole time. The rule under it turns instead: enough to\n say where the keyboard is, quiet enough to live with. */\n.rk-command-field:has(.rk-command-input:focus-visible) {\n border-bottom-color: var(--color-primary);\n box-shadow: inset 0 -1px 0 var(--color-primary);\n}\n\n.rk-command-list {\n overflow-y: auto;\n padding: 0.375rem;\n}\n\n.rk-command-group {\n padding: 0.5rem 0.625rem 0.25rem;\n font-size: 0.6875rem;\n font-weight: 600;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--color-ink-soft);\n}\n\n.rk-command-item {\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.rk-command-item.is-active {\n background: var(--color-muted);\n}\n\n.rk-command-hint {\n margin-left: auto;\n flex-shrink: 0;\n font-size: 0.75rem;\n color: var(--color-ink-soft);\n}\n\n.rk-command-empty {\n padding: 1.5rem 0.625rem;\n text-align: center;\n font-size: 0.875rem;\n color: var(--color-ink-soft);\n}\n\n.rk-command-enter-active,\n.rk-command-leave-active {\n transition: opacity var(--duration-fast) var(--ease-standard);\n}\n\n.rk-command-enter-active .rk-command-panel,\n.rk-command-leave-active .rk-command-panel {\n transition: transform var(--duration-fast) var(--ease-standard);\n}\n\n.rk-command-enter-from,\n.rk-command-leave-to {\n opacity: 0;\n}\n\n.rk-command-enter-from .rk-command-panel,\n.rk-command-leave-to .rk-command-panel {\n transform: translateY(-0.5rem) scale(0.98);\n}\n</style>\n","<script setup lang=\"ts\" generic=\"Row extends Record<string, unknown>\">\nimport { ArrowDown, ArrowUp, ChevronsUpDown } from 'lucide-vue-next'\nimport { computed } from 'vue'\n\nimport BaseSkeleton from '../components/BaseSkeleton.vue'\nimport type { Column } from '../components/BaseTable.vue'\n\nexport interface DataColumn<Row> extends Column<Row> {\n /** Adds a button to the header that sorts by this column. */\n sortable?: boolean | undefined\n}\n\nexport interface TableSort<Row> {\n key: string & keyof Row\n direction: 'asc' | 'desc'\n}\n\n/**\n * `BaseTable` with the three things a table of real data grows: a sort, a\n * selection, and something to show while it is loading.\n *\n * Sorting happens here by default — comparing numbers as numbers and strings\n * with `localeCompare`, so \"Ömer\" lands where a reader expects. Set\n * `manualSort` and it only reports what was asked for, which is what a\n * server-paged table needs: sorting the twenty rows on screen would be a\n * lie about the other nine thousand.\n *\n * The header's sort control is a real button inside the `th`, and the `th`\n * carries `aria-sort`. That pair is what a screen reader reads as \"sorted\n * ascending\"; a `div` with a click handler and an arrow glyph is neither.\n *\n * Selection needs `rowKey`, because a selection of row indexes is a\n * selection that changes meaning the moment the table is sorted.\n */\nconst {\n columns,\n rows,\n caption,\n captionHidden = false,\n rowKey = undefined,\n selectAllLabel = undefined,\n rowLabel = undefined,\n manualSort = false,\n loading = false,\n loadingRows = 5,\n stickyHeader = false,\n} = defineProps<{\n columns: readonly DataColumn<Row>[]\n rows: readonly Row[]\n /** What this table is. Required — an unnamed table is a box of numbers. */\n caption: string\n captionHidden?: boolean | undefined\n /** Which field identifies a row. Required for selection. */\n rowKey?: (string & keyof Row) | undefined\n /** Given with `rowKey`, the table grows a checkbox column. Names the header one. */\n selectAllLabel?: string | undefined\n /** Names each row's checkbox from the row: `(row) => \\`Select ${row.name}\\``. */\n rowLabel?: ((row: Row) => string) | undefined\n /** Report the sort instead of doing it — for a server that pages the data. */\n manualSort?: boolean | undefined\n /** Draws skeleton rows instead of the body. */\n loading?: boolean | undefined\n loadingRows?: number | undefined\n /** Keeps the header on screen while the body scrolls under it. */\n stickyHeader?: boolean | undefined\n}>()\n\ndefineSlots<{\n /** Per-column cell override, named for the column key. */\n [key: string]: (props: { row: Row; value: unknown }) => unknown\n /** Shown instead of the body when there are no rows. */\n empty?: () => unknown\n}>()\n\n/** Which column is sorted, and which way. `v-model:sort`. */\nconst sort = defineModel<TableSort<Row> | undefined>('sort', { default: undefined })\n/** The chosen rows' keys. `v-model:selected`. */\nconst selected = defineModel<string[]>('selected', { default: () => [] })\n\nconst selectable = computed(() => Boolean(rowKey && selectAllLabel && rowLabel))\n\nconst keyOf = (row: Row, index: number) => (rowKey ? String(row[rowKey]) : String(index))\n\n/* Numbers compared as numbers, everything else through the reader's\n collation: `'ö' < 'p'` is false as a code point and true in Turkish. */\nfunction compare(a: unknown, b: unknown) {\n if (typeof a === 'number' && typeof b === 'number') return a - b\n if (a == null) return b == null ? 0 : -1\n if (b == null) return 1\n\n return String(a).localeCompare(String(b))\n}\n\nconst shown = computed(() => {\n if (!sort.value || manualSort) return rows\n\n const { key, direction } = sort.value\n const sorted = [...rows].sort((a, b) => compare(a[key], b[key]))\n\n return direction === 'asc' ? sorted : sorted.reverse()\n})\n\n/** Ascending, then descending, then back to the order it arrived in. */\nfunction toggleSort(column: DataColumn<Row>) {\n if (!column.sortable) return\n\n if (sort.value?.key !== column.key) {\n sort.value = { key: column.key, direction: 'asc' }\n return\n }\n\n sort.value = sort.value.direction === 'asc' ? { key: column.key, direction: 'desc' } : undefined\n}\n\nconst ariaSort = (column: DataColumn<Row>) => {\n if (!column.sortable) return undefined\n if (sort.value?.key !== column.key) return 'none'\n\n return sort.value.direction === 'asc' ? 'ascending' : 'descending'\n}\n\nconst allKeys = computed(() => shown.value.map((row, index) => keyOf(row, index)))\nconst allChosen = computed(\n () => allKeys.value.length > 0 && allKeys.value.every((key) => selected.value.includes(key)),\n)\nconst someChosen = computed(\n () => !allChosen.value && allKeys.value.some((key) => selected.value.includes(key)),\n)\n\nfunction toggleAll() {\n selected.value = allChosen.value ? [] : allKeys.value\n}\n\nfunction toggleRow(key: string) {\n selected.value = selected.value.includes(key)\n ? selected.value.filter((one) => one !== key)\n : [...selected.value, key]\n}\n\nconst columnCount = computed(() => columns.length + (selectable.value ? 1 : 0))\n</script>\n\n<template>\n <div class=\"rk-data-scroll\" tabindex=\"0\" role=\"region\" :aria-label=\"caption\">\n <table class=\"rk-data\" :class=\"{ 'is-sticky': stickyHeader }\">\n <caption :class=\"captionHidden ? 'rk-data-caption-hidden' : 'rk-data-caption'\">\n {{\n caption\n }}\n </caption>\n\n <thead>\n <tr>\n <th v-if=\"selectable\" scope=\"col\" class=\"rk-data-pick\">\n <input\n type=\"checkbox\"\n class=\"rk-data-box focus-ring\"\n :checked=\"allChosen\"\n :indeterminate=\"someChosen\"\n :aria-label=\"selectAllLabel\"\n @change=\"toggleAll\"\n />\n </th>\n\n <th\n v-for=\"column in columns\"\n :key=\"column.key\"\n scope=\"col\"\n :aria-sort=\"ariaSort(column)\"\n :class=\"[column.align === 'end' && 'is-end', column.nowrap && 'is-nowrap']\"\n >\n <button\n v-if=\"column.sortable\"\n type=\"button\"\n class=\"rk-data-sort focus-ring\"\n @click=\"toggleSort(column)\"\n >\n {{ column.label }}\n <ArrowUp\n v-if=\"ariaSort(column) === 'ascending'\"\n class=\"size-3.5\"\n aria-hidden=\"true\"\n />\n <ArrowDown\n v-else-if=\"ariaSort(column) === 'descending'\"\n class=\"size-3.5\"\n aria-hidden=\"true\"\n />\n <ChevronsUpDown v-else class=\"size-3.5 opacity-40\" aria-hidden=\"true\" />\n </button>\n <template v-else>{{ column.label }}</template>\n </th>\n </tr>\n </thead>\n\n <tbody v-if=\"loading\" aria-busy=\"true\">\n <tr v-for=\"row in loadingRows\" :key=\"row\">\n <td v-if=\"selectable\" />\n <td v-for=\"column in columns\" :key=\"column.key\">\n <BaseSkeleton shape=\"text\" height=\"0.75rem\" width=\"70%\" />\n </td>\n </tr>\n </tbody>\n\n <tbody v-else-if=\"shown.length > 0\">\n <tr\n v-for=\"(row, index) in shown\"\n :key=\"keyOf(row, index)\"\n :class=\"{ 'is-chosen': selected.includes(keyOf(row, index)) }\"\n >\n <td v-if=\"selectable\" class=\"rk-data-pick\">\n <input\n type=\"checkbox\"\n class=\"rk-data-box focus-ring\"\n :checked=\"selected.includes(keyOf(row, index))\"\n :aria-label=\"rowLabel!(row)\"\n @change=\"toggleRow(keyOf(row, index))\"\n />\n </td>\n\n <td\n v-for=\"column in columns\"\n :key=\"column.key\"\n :class=\"[column.align === 'end' && 'is-end', column.nowrap && 'is-nowrap']\"\n >\n <slot :name=\"column.key\" :row=\"row\" :value=\"row[column.key]\">\n {{ row[column.key] }}\n </slot>\n </td>\n </tr>\n </tbody>\n\n <tbody v-else>\n <tr>\n <td :colspan=\"columnCount\" class=\"rk-data-empty\">\n <slot name=\"empty\" />\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n</template>\n\n<style scoped>\n/* The scroller is focusable: a region only a drag can reach is a region a\n keyboard cannot read at all. */\n.rk-data-scroll {\n overflow-x: auto;\n border-radius: var(--radius-card);\n}\n\n.rk-data-scroll:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n.rk-data {\n width: 100%;\n border-collapse: collapse;\n font-size: 0.875rem;\n}\n\n.rk-data-caption {\n padding-bottom: 0.5rem;\n text-align: left;\n font-size: 0.8125rem;\n color: var(--color-ink-soft);\n}\n\n.rk-data-caption-hidden {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n}\n\n.rk-data th {\n border-bottom: 1px solid var(--color-hair);\n padding: 0.625rem 0.75rem;\n text-align: left;\n font-size: 0.75rem;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--color-ink-soft);\n white-space: nowrap;\n}\n\n.rk-data.is-sticky thead th {\n position: sticky;\n top: 0;\n z-index: 1;\n background: var(--color-surface);\n}\n\n.rk-data td {\n border-bottom: 1px solid color-mix(in oklab, var(--color-hair) 60%, transparent);\n padding: 0.625rem 0.75rem;\n color: var(--color-ink);\n vertical-align: middle;\n}\n\n.rk-data tbody tr:last-child td {\n border-bottom: 0;\n}\n\n.rk-data tbody tr.is-chosen td {\n background: color-mix(in oklab, var(--color-primary) 8%, transparent);\n}\n\n.is-end {\n text-align: right;\n font-variant-numeric: tabular-nums;\n}\n\n.is-nowrap {\n white-space: nowrap;\n}\n\n.rk-data-sort {\n display: inline-flex;\n align-items: center;\n gap: 0.25rem;\n border-radius: var(--radius-cell);\n color: inherit;\n transition: color var(--duration-fast) var(--ease-standard);\n}\n\n.rk-data-sort:hover {\n color: var(--color-ink);\n}\n\n.rk-data-pick {\n width: 2.75rem;\n padding-right: 0;\n}\n\n.rk-data-box {\n width: 1rem;\n height: 1rem;\n accent-color: var(--color-primary);\n}\n\n.rk-data-empty {\n padding: 2rem 0.75rem;\n text-align: center;\n color: var(--color-ink-soft);\n}\n</style>\n","<script setup lang=\"ts\" generic=\"Row extends Record<string, unknown>\">\nimport { ArrowDown, ArrowUp, ChevronsUpDown } from 'lucide-vue-next'\nimport { computed } from 'vue'\n\nimport BaseSkeleton from '../components/BaseSkeleton.vue'\nimport type { Column } from '../components/BaseTable.vue'\n\nexport interface DataColumn<Row> extends Column<Row> {\n /** Adds a button to the header that sorts by this column. */\n sortable?: boolean | undefined\n}\n\nexport interface TableSort<Row> {\n key: string & keyof Row\n direction: 'asc' | 'desc'\n}\n\n/**\n * `BaseTable` with the three things a table of real data grows: a sort, a\n * selection, and something to show while it is loading.\n *\n * Sorting happens here by default — comparing numbers as numbers and strings\n * with `localeCompare`, so \"Ömer\" lands where a reader expects. Set\n * `manualSort` and it only reports what was asked for, which is what a\n * server-paged table needs: sorting the twenty rows on screen would be a\n * lie about the other nine thousand.\n *\n * The header's sort control is a real button inside the `th`, and the `th`\n * carries `aria-sort`. That pair is what a screen reader reads as \"sorted\n * ascending\"; a `div` with a click handler and an arrow glyph is neither.\n *\n * Selection needs `rowKey`, because a selection of row indexes is a\n * selection that changes meaning the moment the table is sorted.\n */\nconst {\n columns,\n rows,\n caption,\n captionHidden = false,\n rowKey = undefined,\n selectAllLabel = undefined,\n rowLabel = undefined,\n manualSort = false,\n loading = false,\n loadingRows = 5,\n stickyHeader = false,\n} = defineProps<{\n columns: readonly DataColumn<Row>[]\n rows: readonly Row[]\n /** What this table is. Required — an unnamed table is a box of numbers. */\n caption: string\n captionHidden?: boolean | undefined\n /** Which field identifies a row. Required for selection. */\n rowKey?: (string & keyof Row) | undefined\n /** Given with `rowKey`, the table grows a checkbox column. Names the header one. */\n selectAllLabel?: string | undefined\n /** Names each row's checkbox from the row: `(row) => \\`Select ${row.name}\\``. */\n rowLabel?: ((row: Row) => string) | undefined\n /** Report the sort instead of doing it — for a server that pages the data. */\n manualSort?: boolean | undefined\n /** Draws skeleton rows instead of the body. */\n loading?: boolean | undefined\n loadingRows?: number | undefined\n /** Keeps the header on screen while the body scrolls under it. */\n stickyHeader?: boolean | undefined\n}>()\n\ndefineSlots<{\n /** Per-column cell override, named for the column key. */\n [key: string]: (props: { row: Row; value: unknown }) => unknown\n /** Shown instead of the body when there are no rows. */\n empty?: () => unknown\n}>()\n\n/** Which column is sorted, and which way. `v-model:sort`. */\nconst sort = defineModel<TableSort<Row> | undefined>('sort', { default: undefined })\n/** The chosen rows' keys. `v-model:selected`. */\nconst selected = defineModel<string[]>('selected', { default: () => [] })\n\nconst selectable = computed(() => Boolean(rowKey && selectAllLabel && rowLabel))\n\nconst keyOf = (row: Row, index: number) => (rowKey ? String(row[rowKey]) : String(index))\n\n/* Numbers compared as numbers, everything else through the reader's\n collation: `'ö' < 'p'` is false as a code point and true in Turkish. */\nfunction compare(a: unknown, b: unknown) {\n if (typeof a === 'number' && typeof b === 'number') return a - b\n if (a == null) return b == null ? 0 : -1\n if (b == null) return 1\n\n return String(a).localeCompare(String(b))\n}\n\nconst shown = computed(() => {\n if (!sort.value || manualSort) return rows\n\n const { key, direction } = sort.value\n const sorted = [...rows].sort((a, b) => compare(a[key], b[key]))\n\n return direction === 'asc' ? sorted : sorted.reverse()\n})\n\n/** Ascending, then descending, then back to the order it arrived in. */\nfunction toggleSort(column: DataColumn<Row>) {\n if (!column.sortable) return\n\n if (sort.value?.key !== column.key) {\n sort.value = { key: column.key, direction: 'asc' }\n return\n }\n\n sort.value = sort.value.direction === 'asc' ? { key: column.key, direction: 'desc' } : undefined\n}\n\nconst ariaSort = (column: DataColumn<Row>) => {\n if (!column.sortable) return undefined\n if (sort.value?.key !== column.key) return 'none'\n\n return sort.value.direction === 'asc' ? 'ascending' : 'descending'\n}\n\nconst allKeys = computed(() => shown.value.map((row, index) => keyOf(row, index)))\nconst allChosen = computed(\n () => allKeys.value.length > 0 && allKeys.value.every((key) => selected.value.includes(key)),\n)\nconst someChosen = computed(\n () => !allChosen.value && allKeys.value.some((key) => selected.value.includes(key)),\n)\n\nfunction toggleAll() {\n selected.value = allChosen.value ? [] : allKeys.value\n}\n\nfunction toggleRow(key: string) {\n selected.value = selected.value.includes(key)\n ? selected.value.filter((one) => one !== key)\n : [...selected.value, key]\n}\n\nconst columnCount = computed(() => columns.length + (selectable.value ? 1 : 0))\n</script>\n\n<template>\n <div class=\"rk-data-scroll\" tabindex=\"0\" role=\"region\" :aria-label=\"caption\">\n <table class=\"rk-data\" :class=\"{ 'is-sticky': stickyHeader }\">\n <caption :class=\"captionHidden ? 'rk-data-caption-hidden' : 'rk-data-caption'\">\n {{\n caption\n }}\n </caption>\n\n <thead>\n <tr>\n <th v-if=\"selectable\" scope=\"col\" class=\"rk-data-pick\">\n <input\n type=\"checkbox\"\n class=\"rk-data-box focus-ring\"\n :checked=\"allChosen\"\n :indeterminate=\"someChosen\"\n :aria-label=\"selectAllLabel\"\n @change=\"toggleAll\"\n />\n </th>\n\n <th\n v-for=\"column in columns\"\n :key=\"column.key\"\n scope=\"col\"\n :aria-sort=\"ariaSort(column)\"\n :class=\"[column.align === 'end' && 'is-end', column.nowrap && 'is-nowrap']\"\n >\n <button\n v-if=\"column.sortable\"\n type=\"button\"\n class=\"rk-data-sort focus-ring\"\n @click=\"toggleSort(column)\"\n >\n {{ column.label }}\n <ArrowUp\n v-if=\"ariaSort(column) === 'ascending'\"\n class=\"size-3.5\"\n aria-hidden=\"true\"\n />\n <ArrowDown\n v-else-if=\"ariaSort(column) === 'descending'\"\n class=\"size-3.5\"\n aria-hidden=\"true\"\n />\n <ChevronsUpDown v-else class=\"size-3.5 opacity-40\" aria-hidden=\"true\" />\n </button>\n <template v-else>{{ column.label }}</template>\n </th>\n </tr>\n </thead>\n\n <tbody v-if=\"loading\" aria-busy=\"true\">\n <tr v-for=\"row in loadingRows\" :key=\"row\">\n <td v-if=\"selectable\" />\n <td v-for=\"column in columns\" :key=\"column.key\">\n <BaseSkeleton shape=\"text\" height=\"0.75rem\" width=\"70%\" />\n </td>\n </tr>\n </tbody>\n\n <tbody v-else-if=\"shown.length > 0\">\n <tr\n v-for=\"(row, index) in shown\"\n :key=\"keyOf(row, index)\"\n :class=\"{ 'is-chosen': selected.includes(keyOf(row, index)) }\"\n >\n <td v-if=\"selectable\" class=\"rk-data-pick\">\n <input\n type=\"checkbox\"\n class=\"rk-data-box focus-ring\"\n :checked=\"selected.includes(keyOf(row, index))\"\n :aria-label=\"rowLabel!(row)\"\n @change=\"toggleRow(keyOf(row, index))\"\n />\n </td>\n\n <td\n v-for=\"column in columns\"\n :key=\"column.key\"\n :class=\"[column.align === 'end' && 'is-end', column.nowrap && 'is-nowrap']\"\n >\n <slot :name=\"column.key\" :row=\"row\" :value=\"row[column.key]\">\n {{ row[column.key] }}\n </slot>\n </td>\n </tr>\n </tbody>\n\n <tbody v-else>\n <tr>\n <td :colspan=\"columnCount\" class=\"rk-data-empty\">\n <slot name=\"empty\" />\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n</template>\n\n<style scoped>\n/* The scroller is focusable: a region only a drag can reach is a region a\n keyboard cannot read at all. */\n.rk-data-scroll {\n overflow-x: auto;\n border-radius: var(--radius-card);\n}\n\n.rk-data-scroll:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n.rk-data {\n width: 100%;\n border-collapse: collapse;\n font-size: 0.875rem;\n}\n\n.rk-data-caption {\n padding-bottom: 0.5rem;\n text-align: left;\n font-size: 0.8125rem;\n color: var(--color-ink-soft);\n}\n\n.rk-data-caption-hidden {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n}\n\n.rk-data th {\n border-bottom: 1px solid var(--color-hair);\n padding: 0.625rem 0.75rem;\n text-align: left;\n font-size: 0.75rem;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--color-ink-soft);\n white-space: nowrap;\n}\n\n.rk-data.is-sticky thead th {\n position: sticky;\n top: 0;\n z-index: 1;\n background: var(--color-surface);\n}\n\n.rk-data td {\n border-bottom: 1px solid color-mix(in oklab, var(--color-hair) 60%, transparent);\n padding: 0.625rem 0.75rem;\n color: var(--color-ink);\n vertical-align: middle;\n}\n\n.rk-data tbody tr:last-child td {\n border-bottom: 0;\n}\n\n.rk-data tbody tr.is-chosen td {\n background: color-mix(in oklab, var(--color-primary) 8%, transparent);\n}\n\n.is-end {\n text-align: right;\n font-variant-numeric: tabular-nums;\n}\n\n.is-nowrap {\n white-space: nowrap;\n}\n\n.rk-data-sort {\n display: inline-flex;\n align-items: center;\n gap: 0.25rem;\n border-radius: var(--radius-cell);\n color: inherit;\n transition: color var(--duration-fast) var(--ease-standard);\n}\n\n.rk-data-sort:hover {\n color: var(--color-ink);\n}\n\n.rk-data-pick {\n width: 2.75rem;\n padding-right: 0;\n}\n\n.rk-data-box {\n width: 1rem;\n height: 1rem;\n accent-color: var(--color-primary);\n}\n\n.rk-data-empty {\n padding: 2rem 0.75rem;\n text-align: center;\n color: var(--color-ink-soft);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, useTemplateRef } from 'vue'\n\n/**\n * Two panes and a handle between them — a list beside a preview, an editor\n * beside its output.\n *\n * The handle is a `separator` with a value, which is the part a hand-written\n * one never has: it can be moved with the arrow keys, Home and End send it\n * to its limits, and Enter puts it back where it started. A divider that\n * only answers to a drag is a layout a keyboard cannot change at all.\n *\n * The split is a percentage, so the panes keep their proportions when the\n * window changes rather than one of them swallowing the other.\n */\nconst {\n label,\n orientation = 'horizontal',\n min = 15,\n max = 85,\n step = 2,\n} = defineProps<{\n /** The handle's accessible name, e.g. \"Resize the list\". */\n label: string\n /** `horizontal`: side by side, the handle moves left and right. */\n orientation?: 'horizontal' | 'vertical' | undefined\n /** How small the first pane may get, as a percentage. */\n min?: number | undefined\n max?: number | undefined\n /** How far one arrow press moves it, in percentage points. */\n step?: number | undefined\n}>()\n\ndefineSlots<{\n /** The first pane: the list, the tree, the sidebar. */\n start: () => unknown\n /** The second pane. */\n end: () => unknown\n}>()\n\n/** Where the split sits, as a percentage of the whole. `v-model`. */\nconst split = defineModel<number>({ default: 50 })\n\nconst root = useTemplateRef<HTMLElement>('root')\nconst dragging = ref(false)\nconst startedAt = split.value\n\nconst clamp = (value: number) => Math.min(max, Math.max(min, value))\n\nconst horizontal = computed(() => orientation === 'horizontal')\n\nfunction moveTo(point: number) {\n const box = root.value?.getBoundingClientRect()\n if (!box) return\n\n const ratio = horizontal.value ? (point - box.left) / box.width : (point - box.top) / box.height\n\n split.value = clamp(Math.round(ratio * 100))\n}\n\nfunction onPointerDown(event: PointerEvent) {\n dragging.value = true\n ;(event.target as HTMLElement).setPointerCapture(event.pointerId)\n}\n\nfunction onPointerMove(event: PointerEvent) {\n if (!dragging.value) return\n event.preventDefault()\n moveTo(horizontal.value ? event.clientX : event.clientY)\n}\n\nfunction onPointerUp(event: PointerEvent) {\n dragging.value = false\n ;(event.target as HTMLElement).releasePointerCapture?.(event.pointerId)\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n const back = horizontal.value ? 'ArrowLeft' : 'ArrowUp'\n const forward = horizontal.value ? 'ArrowRight' : 'ArrowDown'\n\n const moves: Record<string, number> = {\n [back]: split.value - step,\n [forward]: split.value + step,\n Home: min,\n End: max,\n // Enter puts it back where it started, which is the undo a drag has no\n // other way of offering.\n Enter: startedAt,\n }\n\n const next = moves[event.key]\n if (next === undefined) return\n\n event.preventDefault()\n split.value = clamp(next)\n}\n\nonBeforeUnmount(() => (dragging.value = false))\n</script>\n\n<template>\n <div\n ref=\"root\"\n class=\"rk-split\"\n :class=\"[`is-${orientation}`, { 'is-dragging': dragging }]\"\n :style=\"{ '--rk-split': `${split}%` }\"\n >\n <div class=\"rk-split-pane is-start\"><slot name=\"start\" /></div>\n\n <div\n class=\"rk-split-handle focus-ring\"\n role=\"separator\"\n tabindex=\"0\"\n :aria-label=\"label\"\n :aria-orientation=\"orientation === 'horizontal' ? 'vertical' : 'horizontal'\"\n :aria-valuenow=\"split\"\n :aria-valuemin=\"min\"\n :aria-valuemax=\"max\"\n @pointerdown=\"onPointerDown\"\n @pointermove=\"onPointerMove\"\n @pointerup=\"onPointerUp\"\n @pointercancel=\"onPointerUp\"\n @keydown=\"onKeydown\"\n >\n <span class=\"rk-split-grip\" aria-hidden=\"true\" />\n </div>\n\n <div class=\"rk-split-pane is-end\"><slot name=\"end\" /></div>\n </div>\n</template>\n\n<style scoped>\n.rk-split {\n display: flex;\n width: 100%;\n min-height: 0;\n}\n\n.rk-split.is-vertical {\n flex-direction: column;\n}\n\n.rk-split.is-dragging {\n user-select: none;\n}\n\n.rk-split-pane {\n min-width: 0;\n min-height: 0;\n overflow: auto;\n}\n\n.is-horizontal .rk-split-pane.is-start {\n width: var(--rk-split);\n}\n\n.is-vertical .rk-split-pane.is-start {\n height: var(--rk-split);\n}\n\n.rk-split-pane.is-end {\n flex: 1;\n}\n\n/* The handle is wider than the line it draws: a 1px target is a target\n nobody hits. */\n.rk-split-handle {\n display: grid;\n flex-shrink: 0;\n place-items: center;\n background: transparent;\n touch-action: none;\n}\n\n.is-horizontal .rk-split-handle {\n width: 0.75rem;\n cursor: col-resize;\n}\n\n.is-vertical .rk-split-handle {\n height: 0.75rem;\n cursor: row-resize;\n}\n\n.rk-split-grip {\n display: block;\n border-radius: 9999px;\n background: var(--color-hair);\n transition: background-color var(--duration-fast) var(--ease-standard);\n}\n\n.is-horizontal .rk-split-grip {\n width: 2px;\n height: 100%;\n}\n\n.is-vertical .rk-split-grip {\n width: 100%;\n height: 2px;\n}\n\n.rk-split-handle:hover .rk-split-grip,\n.rk-split-handle:focus-visible .rk-split-grip,\n.is-dragging .rk-split-grip {\n background: var(--color-primary);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, useTemplateRef } from 'vue'\n\n/**\n * Two panes and a handle between them — a list beside a preview, an editor\n * beside its output.\n *\n * The handle is a `separator` with a value, which is the part a hand-written\n * one never has: it can be moved with the arrow keys, Home and End send it\n * to its limits, and Enter puts it back where it started. A divider that\n * only answers to a drag is a layout a keyboard cannot change at all.\n *\n * The split is a percentage, so the panes keep their proportions when the\n * window changes rather than one of them swallowing the other.\n */\nconst {\n label,\n orientation = 'horizontal',\n min = 15,\n max = 85,\n step = 2,\n} = defineProps<{\n /** The handle's accessible name, e.g. \"Resize the list\". */\n label: string\n /** `horizontal`: side by side, the handle moves left and right. */\n orientation?: 'horizontal' | 'vertical' | undefined\n /** How small the first pane may get, as a percentage. */\n min?: number | undefined\n max?: number | undefined\n /** How far one arrow press moves it, in percentage points. */\n step?: number | undefined\n}>()\n\ndefineSlots<{\n /** The first pane: the list, the tree, the sidebar. */\n start: () => unknown\n /** The second pane. */\n end: () => unknown\n}>()\n\n/** Where the split sits, as a percentage of the whole. `v-model`. */\nconst split = defineModel<number>({ default: 50 })\n\nconst root = useTemplateRef<HTMLElement>('root')\nconst dragging = ref(false)\nconst startedAt = split.value\n\nconst clamp = (value: number) => Math.min(max, Math.max(min, value))\n\nconst horizontal = computed(() => orientation === 'horizontal')\n\nfunction moveTo(point: number) {\n const box = root.value?.getBoundingClientRect()\n if (!box) return\n\n const ratio = horizontal.value ? (point - box.left) / box.width : (point - box.top) / box.height\n\n split.value = clamp(Math.round(ratio * 100))\n}\n\nfunction onPointerDown(event: PointerEvent) {\n dragging.value = true\n ;(event.target as HTMLElement).setPointerCapture(event.pointerId)\n}\n\nfunction onPointerMove(event: PointerEvent) {\n if (!dragging.value) return\n event.preventDefault()\n moveTo(horizontal.value ? event.clientX : event.clientY)\n}\n\nfunction onPointerUp(event: PointerEvent) {\n dragging.value = false\n ;(event.target as HTMLElement).releasePointerCapture?.(event.pointerId)\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n const back = horizontal.value ? 'ArrowLeft' : 'ArrowUp'\n const forward = horizontal.value ? 'ArrowRight' : 'ArrowDown'\n\n const moves: Record<string, number> = {\n [back]: split.value - step,\n [forward]: split.value + step,\n Home: min,\n End: max,\n // Enter puts it back where it started, which is the undo a drag has no\n // other way of offering.\n Enter: startedAt,\n }\n\n const next = moves[event.key]\n if (next === undefined) return\n\n event.preventDefault()\n split.value = clamp(next)\n}\n\nonBeforeUnmount(() => (dragging.value = false))\n</script>\n\n<template>\n <div\n ref=\"root\"\n class=\"rk-split\"\n :class=\"[`is-${orientation}`, { 'is-dragging': dragging }]\"\n :style=\"{ '--rk-split': `${split}%` }\"\n >\n <div class=\"rk-split-pane is-start\"><slot name=\"start\" /></div>\n\n <div\n class=\"rk-split-handle focus-ring\"\n role=\"separator\"\n tabindex=\"0\"\n :aria-label=\"label\"\n :aria-orientation=\"orientation === 'horizontal' ? 'vertical' : 'horizontal'\"\n :aria-valuenow=\"split\"\n :aria-valuemin=\"min\"\n :aria-valuemax=\"max\"\n @pointerdown=\"onPointerDown\"\n @pointermove=\"onPointerMove\"\n @pointerup=\"onPointerUp\"\n @pointercancel=\"onPointerUp\"\n @keydown=\"onKeydown\"\n >\n <span class=\"rk-split-grip\" aria-hidden=\"true\" />\n </div>\n\n <div class=\"rk-split-pane is-end\"><slot name=\"end\" /></div>\n </div>\n</template>\n\n<style scoped>\n.rk-split {\n display: flex;\n width: 100%;\n min-height: 0;\n}\n\n.rk-split.is-vertical {\n flex-direction: column;\n}\n\n.rk-split.is-dragging {\n user-select: none;\n}\n\n.rk-split-pane {\n min-width: 0;\n min-height: 0;\n overflow: auto;\n}\n\n.is-horizontal .rk-split-pane.is-start {\n width: var(--rk-split);\n}\n\n.is-vertical .rk-split-pane.is-start {\n height: var(--rk-split);\n}\n\n.rk-split-pane.is-end {\n flex: 1;\n}\n\n/* The handle is wider than the line it draws: a 1px target is a target\n nobody hits. */\n.rk-split-handle {\n display: grid;\n flex-shrink: 0;\n place-items: center;\n background: transparent;\n touch-action: none;\n}\n\n.is-horizontal .rk-split-handle {\n width: 0.75rem;\n cursor: col-resize;\n}\n\n.is-vertical .rk-split-handle {\n height: 0.75rem;\n cursor: row-resize;\n}\n\n.rk-split-grip {\n display: block;\n border-radius: 9999px;\n background: var(--color-hair);\n transition: background-color var(--duration-fast) var(--ease-standard);\n}\n\n.is-horizontal .rk-split-grip {\n width: 2px;\n height: 100%;\n}\n\n.is-vertical .rk-split-grip {\n width: 100%;\n height: 2px;\n}\n\n.rk-split-handle:hover .rk-split-grip,\n.rk-split-handle:focus-visible .rk-split-grip,\n.is-dragging .rk-split-grip {\n background: var(--color-primary);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { onMounted, ref, useTemplateRef } from 'vue'\n\n/**\n * A row of controls that belong together: a formatting bar, a row of view\n * switches, the actions over a table.\n *\n * One Tab stop for the lot. The arrows move between the controls inside it,\n * Home and End jump to the ends — the toolbar pattern, and the reason to\n * use it is arithmetic: twelve buttons in the tab order cost twelve presses\n * to get past on the way to the thing under them.\n *\n * The controls are yours: anything focusable inside is picked up, so a\n * toolbar can mix the kit's buttons, a `ToggleGroup` and a separator\n * without declaring any of them as data.\n */\nconst { label, orientation = 'horizontal' } = defineProps<{\n /** The toolbar's accessible name, e.g. \"Formatting\". */\n label: string\n orientation?: 'horizontal' | 'vertical' | undefined\n}>()\n\ndefineSlots<{ default: () => unknown }>()\n\nconst root = useTemplateRef<HTMLElement>('root')\nconst at = ref(0)\n\nconst FOCUSABLE = 'button:not([disabled]), a[href], input:not([disabled]), select, textarea'\n\nfunction controls(): HTMLElement[] {\n return Array.from(root.value?.querySelectorAll<HTMLElement>(FOCUSABLE) ?? [])\n}\n\n/* Exactly one control is in the tab order, and which one is remembered:\n coming back to a toolbar puts you where you left it. */\nfunction applyRoving() {\n const list = controls()\n at.value = Math.min(at.value, Math.max(0, list.length - 1))\n list.forEach((control, index) => {\n control.tabIndex = index === at.value ? 0 : -1\n })\n}\n\nfunction focusAt(index: number) {\n const list = controls()\n if (list.length === 0) return\n\n at.value = (index + list.length) % list.length\n applyRoving()\n list[at.value]?.focus()\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n const back = orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp'\n const forward = orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown'\n const list = controls()\n const current = list.indexOf(document.activeElement as HTMLElement)\n\n if (event.key === back) {\n event.preventDefault()\n focusAt(current - 1)\n } else if (event.key === forward) {\n event.preventDefault()\n focusAt(current + 1)\n } else if (event.key === 'Home') {\n event.preventDefault()\n focusAt(0)\n } else if (event.key === 'End') {\n event.preventDefault()\n focusAt(list.length - 1)\n }\n}\n\nfunction onFocusin(event: FocusEvent) {\n const index = controls().indexOf(event.target as HTMLElement)\n if (index >= 0) {\n at.value = index\n applyRoving()\n }\n}\n\nonMounted(applyRoving)\n</script>\n\n<template>\n <div\n ref=\"root\"\n class=\"rk-toolbar\"\n :class=\"`is-${orientation}`\"\n role=\"toolbar\"\n :aria-label=\"label\"\n :aria-orientation=\"orientation\"\n @keydown=\"onKeydown\"\n @focusin=\"onFocusin\"\n >\n <slot />\n </div>\n</template>\n\n<style scoped>\n.rk-toolbar {\n display: flex;\n align-items: center;\n gap: 0.375rem;\n}\n\n.rk-toolbar.is-vertical {\n flex-direction: column;\n align-items: stretch;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { onMounted, ref, useTemplateRef } from 'vue'\n\n/**\n * A row of controls that belong together: a formatting bar, a row of view\n * switches, the actions over a table.\n *\n * One Tab stop for the lot. The arrows move between the controls inside it,\n * Home and End jump to the ends — the toolbar pattern, and the reason to\n * use it is arithmetic: twelve buttons in the tab order cost twelve presses\n * to get past on the way to the thing under them.\n *\n * The controls are yours: anything focusable inside is picked up, so a\n * toolbar can mix the kit's buttons, a `ToggleGroup` and a separator\n * without declaring any of them as data.\n */\nconst { label, orientation = 'horizontal' } = defineProps<{\n /** The toolbar's accessible name, e.g. \"Formatting\". */\n label: string\n orientation?: 'horizontal' | 'vertical' | undefined\n}>()\n\ndefineSlots<{ default: () => unknown }>()\n\nconst root = useTemplateRef<HTMLElement>('root')\nconst at = ref(0)\n\nconst FOCUSABLE = 'button:not([disabled]), a[href], input:not([disabled]), select, textarea'\n\nfunction controls(): HTMLElement[] {\n return Array.from(root.value?.querySelectorAll<HTMLElement>(FOCUSABLE) ?? [])\n}\n\n/* Exactly one control is in the tab order, and which one is remembered:\n coming back to a toolbar puts you where you left it. */\nfunction applyRoving() {\n const list = controls()\n at.value = Math.min(at.value, Math.max(0, list.length - 1))\n list.forEach((control, index) => {\n control.tabIndex = index === at.value ? 0 : -1\n })\n}\n\nfunction focusAt(index: number) {\n const list = controls()\n if (list.length === 0) return\n\n at.value = (index + list.length) % list.length\n applyRoving()\n list[at.value]?.focus()\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n const back = orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp'\n const forward = orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown'\n const list = controls()\n const current = list.indexOf(document.activeElement as HTMLElement)\n\n if (event.key === back) {\n event.preventDefault()\n focusAt(current - 1)\n } else if (event.key === forward) {\n event.preventDefault()\n focusAt(current + 1)\n } else if (event.key === 'Home') {\n event.preventDefault()\n focusAt(0)\n } else if (event.key === 'End') {\n event.preventDefault()\n focusAt(list.length - 1)\n }\n}\n\nfunction onFocusin(event: FocusEvent) {\n const index = controls().indexOf(event.target as HTMLElement)\n if (index >= 0) {\n at.value = index\n applyRoving()\n }\n}\n\nonMounted(applyRoving)\n</script>\n\n<template>\n <div\n ref=\"root\"\n class=\"rk-toolbar\"\n :class=\"`is-${orientation}`\"\n role=\"toolbar\"\n :aria-label=\"label\"\n :aria-orientation=\"orientation\"\n @keydown=\"onKeydown\"\n @focusin=\"onFocusin\"\n >\n <slot />\n </div>\n</template>\n\n<style scoped>\n.rk-toolbar {\n display: flex;\n align-items: center;\n gap: 0.375rem;\n}\n\n.rk-toolbar.is-vertical {\n flex-direction: column;\n align-items: stretch;\n}\n</style>\n","<script setup lang=\"ts\" generic=\"K extends string, M extends 'single' | 'multiple' = 'single'\">\nimport { ChevronRight } from 'lucide-vue-next'\nimport { computed, ref } from 'vue'\n\nimport { useBoundValue } from '../composables/use-bound-value'\nimport type { Component } from 'vue'\n\nexport interface TreeNode<K extends string> {\n key: K\n /** Already translated. */\n label: string\n icon?: Component | undefined\n disabled?: boolean | undefined\n /** Omitted or empty, it is a leaf. */\n children?: readonly TreeNode<K>[] | undefined\n}\n\n/**\n * A tree of things that contain things: folders, a category list, a table of\n * contents.\n *\n * The keyboard is the tree pattern, and it is the whole reason to reach for\n * this rather than nested lists: up and down move through what is on screen,\n * right opens a branch and then steps into it, left closes it and then steps\n * out to its parent, Home and End jump to the ends. One Tab stop for the\n * whole tree.\n *\n * Levels are stated with `aria-level`, `aria-setsize` and `aria-posinset`,\n * so a screen reader can say \"level 2, 3 of 7\" — the only way somebody\n * listening knows where they are in a shape they cannot see.\n *\n * What is open and what is chosen are both the app's: `v-model:expanded`\n * and `v-model`, so a tree can be restored from a URL or a saved state.\n */\nconst {\n modelValue = undefined,\n nodes,\n label,\n mode = 'single' as M,\n} = defineProps<{\n /** The chosen key, or keys in `multiple` mode, with `v-model`. */\n modelValue?: Value | undefined\n nodes: readonly TreeNode<K>[]\n /** The tree's accessible name. */\n label: string\n /** `multiple` lets several be chosen, each with its own tick. */\n mode?: M | undefined\n}>()\n\n/* Multiple always hands back an array; only a single choice can be nothing. */\ntype Value = M extends 'multiple' ? K[] : K | undefined\n\nconst emit = defineEmits<{ 'update:modelValue': [value: Value] }>()\nconst model = useBoundValue<Value>(\n () => modelValue as Value | undefined,\n (value) => emit('update:modelValue', value),\n)\n/** Which branches are open. */\nconst expanded = defineModel<K[]>('expanded', { default: () => [] })\n\ninterface Row {\n node: TreeNode<K>\n level: number\n /** 1-based position among its siblings, and how many there are. */\n position: number\n size: number\n parent: K | null\n}\n\n/** Everything on screen, flattened: the arrows walk this, not the shape. */\nconst rows = computed(() => {\n const out: Row[] = []\n\n const walk = (list: readonly TreeNode<K>[], level: number, parent: K | null) => {\n list.forEach((node, index) => {\n out.push({ node, level, position: index + 1, size: list.length, parent })\n if (node.children?.length && expanded.value.includes(node.key)) {\n walk(node.children, level + 1, node.key)\n }\n })\n }\n\n walk(nodes, 1, null)\n\n return out\n})\n\nconst active = ref(0)\n\nconst chosen = computed(() => {\n const value = model.value as K | K[] | undefined\n if (Array.isArray(value)) return new Set<K>(value)\n\n return new Set<K>(value === undefined ? [] : [value])\n})\n\nconst isOpen = (node: TreeNode<K>) => expanded.value.includes(node.key)\nconst hasChildren = (node: TreeNode<K>) => Boolean(node.children?.length)\n\nfunction setOpen(node: TreeNode<K>, open: boolean) {\n if (!hasChildren(node)) return\n\n expanded.value = open\n ? [...expanded.value, node.key]\n : expanded.value.filter((key) => key !== node.key)\n}\n\nfunction choose(node: TreeNode<K>) {\n if (node.disabled) return\n\n if (mode === 'multiple') {\n const next = new Set(chosen.value)\n if (next.has(node.key)) next.delete(node.key)\n else next.add(node.key)\n\n model.value = [...next] as Value\n return\n }\n\n model.value = node.key as Value\n}\n\nfunction moveTo(index: number) {\n active.value = Math.max(0, Math.min(rows.value.length - 1, index))\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n const row = rows.value[active.value]\n if (!row) return\n\n switch (event.key) {\n case 'ArrowDown':\n event.preventDefault()\n moveTo(active.value + 1)\n break\n case 'ArrowUp':\n event.preventDefault()\n moveTo(active.value - 1)\n break\n case 'ArrowRight':\n event.preventDefault()\n // Open, then step in: the two halves of \"go deeper\".\n if (hasChildren(row.node) && !isOpen(row.node)) setOpen(row.node, true)\n else if (hasChildren(row.node)) moveTo(active.value + 1)\n break\n case 'ArrowLeft': {\n event.preventDefault()\n if (hasChildren(row.node) && isOpen(row.node)) {\n setOpen(row.node, false)\n break\n }\n // Already closed, or a leaf: step out to the parent.\n const parent = rows.value.findIndex((one) => one.node.key === row.parent)\n if (parent >= 0) moveTo(parent)\n break\n }\n case 'Home':\n event.preventDefault()\n moveTo(0)\n break\n case 'End':\n event.preventDefault()\n moveTo(rows.value.length - 1)\n break\n case 'Enter':\n case ' ':\n event.preventDefault()\n choose(row.node)\n break\n }\n}\n\nfunction onClick(index: number, row: Row) {\n active.value = index\n if (hasChildren(row.node)) setOpen(row.node, !isOpen(row.node))\n else choose(row.node)\n}\n</script>\n\n<template>\n <ul\n class=\"rk-tree\"\n role=\"tree\"\n :aria-label=\"label\"\n :aria-multiselectable=\"mode === 'multiple' ? true : undefined\"\n tabindex=\"0\"\n @keydown=\"onKeydown\"\n >\n <li\n v-for=\"(row, index) in rows\"\n :key=\"row.node.key\"\n class=\"rk-tree-row\"\n :class=\"{\n 'is-active': index === active,\n 'is-chosen': chosen.has(row.node.key),\n 'is-disabled': row.node.disabled,\n }\"\n role=\"treeitem\"\n :aria-level=\"row.level\"\n :aria-setsize=\"row.size\"\n :aria-posinset=\"row.position\"\n :aria-expanded=\"row.node.children?.length ? isOpen(row.node) : undefined\"\n :aria-selected=\"chosen.has(row.node.key)\"\n :aria-disabled=\"row.node.disabled || undefined\"\n :style=\"{ '--rk-tree-level': row.level }\"\n @click=\"onClick(index, row)\"\n >\n <ChevronRight\n v-if=\"row.node.children?.length\"\n class=\"rk-tree-twist\"\n :class=\"{ 'is-open': isOpen(row.node) }\"\n aria-hidden=\"true\"\n />\n <span v-else class=\"rk-tree-twist-space\" aria-hidden=\"true\" />\n\n <component\n :is=\"row.node.icon\"\n v-if=\"row.node.icon\"\n class=\"size-4 shrink-0\"\n aria-hidden=\"true\"\n />\n <span class=\"truncate\">{{ row.node.label }}</span>\n </li>\n </ul>\n</template>\n\n<style scoped>\n.rk-tree {\n display: flex;\n flex-direction: column;\n gap: 1px;\n border-radius: var(--radius-card);\n}\n\n.rk-tree:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n.rk-tree-row {\n display: flex;\n cursor: pointer;\n align-items: center;\n gap: 0.375rem;\n border-radius: var(--radius-cell);\n /* Indented by level, so a branch reads as being inside its parent. */\n padding: 0.3125rem 0.5rem 0.3125rem calc(0.5rem + (var(--rk-tree-level) - 1) * 1rem);\n font-size: 0.875rem;\n color: var(--color-ink);\n}\n\n.rk-tree-row.is-active {\n background: var(--color-muted);\n}\n\n.rk-tree-row.is-chosen {\n color: var(--color-primary);\n font-weight: 500;\n}\n\n.rk-tree-row.is-chosen.is-active {\n background: color-mix(in oklab, var(--color-primary) 12%, transparent);\n}\n\n.rk-tree-row.is-disabled {\n cursor: not-allowed;\n opacity: 0.45;\n}\n\n.rk-tree-twist,\n.rk-tree-twist-space {\n width: 1rem;\n height: 1rem;\n flex-shrink: 0;\n}\n\n.rk-tree-twist {\n color: var(--color-ink-soft);\n transition: rotate var(--duration-fast) var(--ease-standard);\n}\n\n.rk-tree-twist.is-open {\n rotate: 90deg;\n}\n</style>\n","<script setup lang=\"ts\" generic=\"K extends string, M extends 'single' | 'multiple' = 'single'\">\nimport { ChevronRight } from 'lucide-vue-next'\nimport { computed, ref } from 'vue'\n\nimport { useBoundValue } from '../composables/use-bound-value'\nimport type { Component } from 'vue'\n\nexport interface TreeNode<K extends string> {\n key: K\n /** Already translated. */\n label: string\n icon?: Component | undefined\n disabled?: boolean | undefined\n /** Omitted or empty, it is a leaf. */\n children?: readonly TreeNode<K>[] | undefined\n}\n\n/**\n * A tree of things that contain things: folders, a category list, a table of\n * contents.\n *\n * The keyboard is the tree pattern, and it is the whole reason to reach for\n * this rather than nested lists: up and down move through what is on screen,\n * right opens a branch and then steps into it, left closes it and then steps\n * out to its parent, Home and End jump to the ends. One Tab stop for the\n * whole tree.\n *\n * Levels are stated with `aria-level`, `aria-setsize` and `aria-posinset`,\n * so a screen reader can say \"level 2, 3 of 7\" — the only way somebody\n * listening knows where they are in a shape they cannot see.\n *\n * What is open and what is chosen are both the app's: `v-model:expanded`\n * and `v-model`, so a tree can be restored from a URL or a saved state.\n */\nconst {\n modelValue = undefined,\n nodes,\n label,\n mode = 'single' as M,\n} = defineProps<{\n /** The chosen key, or keys in `multiple` mode, with `v-model`. */\n modelValue?: Value | undefined\n nodes: readonly TreeNode<K>[]\n /** The tree's accessible name. */\n label: string\n /** `multiple` lets several be chosen, each with its own tick. */\n mode?: M | undefined\n}>()\n\n/* Multiple always hands back an array; only a single choice can be nothing. */\ntype Value = M extends 'multiple' ? K[] : K | undefined\n\nconst emit = defineEmits<{ 'update:modelValue': [value: Value] }>()\nconst model = useBoundValue<Value>(\n () => modelValue as Value | undefined,\n (value) => emit('update:modelValue', value),\n)\n/** Which branches are open. */\nconst expanded = defineModel<K[]>('expanded', { default: () => [] })\n\ninterface Row {\n node: TreeNode<K>\n level: number\n /** 1-based position among its siblings, and how many there are. */\n position: number\n size: number\n parent: K | null\n}\n\n/** Everything on screen, flattened: the arrows walk this, not the shape. */\nconst rows = computed(() => {\n const out: Row[] = []\n\n const walk = (list: readonly TreeNode<K>[], level: number, parent: K | null) => {\n list.forEach((node, index) => {\n out.push({ node, level, position: index + 1, size: list.length, parent })\n if (node.children?.length && expanded.value.includes(node.key)) {\n walk(node.children, level + 1, node.key)\n }\n })\n }\n\n walk(nodes, 1, null)\n\n return out\n})\n\nconst active = ref(0)\n\nconst chosen = computed(() => {\n const value = model.value as K | K[] | undefined\n if (Array.isArray(value)) return new Set<K>(value)\n\n return new Set<K>(value === undefined ? [] : [value])\n})\n\nconst isOpen = (node: TreeNode<K>) => expanded.value.includes(node.key)\nconst hasChildren = (node: TreeNode<K>) => Boolean(node.children?.length)\n\nfunction setOpen(node: TreeNode<K>, open: boolean) {\n if (!hasChildren(node)) return\n\n expanded.value = open\n ? [...expanded.value, node.key]\n : expanded.value.filter((key) => key !== node.key)\n}\n\nfunction choose(node: TreeNode<K>) {\n if (node.disabled) return\n\n if (mode === 'multiple') {\n const next = new Set(chosen.value)\n if (next.has(node.key)) next.delete(node.key)\n else next.add(node.key)\n\n model.value = [...next] as Value\n return\n }\n\n model.value = node.key as Value\n}\n\nfunction moveTo(index: number) {\n active.value = Math.max(0, Math.min(rows.value.length - 1, index))\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n const row = rows.value[active.value]\n if (!row) return\n\n switch (event.key) {\n case 'ArrowDown':\n event.preventDefault()\n moveTo(active.value + 1)\n break\n case 'ArrowUp':\n event.preventDefault()\n moveTo(active.value - 1)\n break\n case 'ArrowRight':\n event.preventDefault()\n // Open, then step in: the two halves of \"go deeper\".\n if (hasChildren(row.node) && !isOpen(row.node)) setOpen(row.node, true)\n else if (hasChildren(row.node)) moveTo(active.value + 1)\n break\n case 'ArrowLeft': {\n event.preventDefault()\n if (hasChildren(row.node) && isOpen(row.node)) {\n setOpen(row.node, false)\n break\n }\n // Already closed, or a leaf: step out to the parent.\n const parent = rows.value.findIndex((one) => one.node.key === row.parent)\n if (parent >= 0) moveTo(parent)\n break\n }\n case 'Home':\n event.preventDefault()\n moveTo(0)\n break\n case 'End':\n event.preventDefault()\n moveTo(rows.value.length - 1)\n break\n case 'Enter':\n case ' ':\n event.preventDefault()\n choose(row.node)\n break\n }\n}\n\nfunction onClick(index: number, row: Row) {\n active.value = index\n if (hasChildren(row.node)) setOpen(row.node, !isOpen(row.node))\n else choose(row.node)\n}\n</script>\n\n<template>\n <ul\n class=\"rk-tree\"\n role=\"tree\"\n :aria-label=\"label\"\n :aria-multiselectable=\"mode === 'multiple' ? true : undefined\"\n tabindex=\"0\"\n @keydown=\"onKeydown\"\n >\n <li\n v-for=\"(row, index) in rows\"\n :key=\"row.node.key\"\n class=\"rk-tree-row\"\n :class=\"{\n 'is-active': index === active,\n 'is-chosen': chosen.has(row.node.key),\n 'is-disabled': row.node.disabled,\n }\"\n role=\"treeitem\"\n :aria-level=\"row.level\"\n :aria-setsize=\"row.size\"\n :aria-posinset=\"row.position\"\n :aria-expanded=\"row.node.children?.length ? isOpen(row.node) : undefined\"\n :aria-selected=\"chosen.has(row.node.key)\"\n :aria-disabled=\"row.node.disabled || undefined\"\n :style=\"{ '--rk-tree-level': row.level }\"\n @click=\"onClick(index, row)\"\n >\n <ChevronRight\n v-if=\"row.node.children?.length\"\n class=\"rk-tree-twist\"\n :class=\"{ 'is-open': isOpen(row.node) }\"\n aria-hidden=\"true\"\n />\n <span v-else class=\"rk-tree-twist-space\" aria-hidden=\"true\" />\n\n <component\n :is=\"row.node.icon\"\n v-if=\"row.node.icon\"\n class=\"size-4 shrink-0\"\n aria-hidden=\"true\"\n />\n <span class=\"truncate\">{{ row.node.label }}</span>\n </li>\n </ul>\n</template>\n\n<style scoped>\n.rk-tree {\n display: flex;\n flex-direction: column;\n gap: 1px;\n border-radius: var(--radius-card);\n}\n\n.rk-tree:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n.rk-tree-row {\n display: flex;\n cursor: pointer;\n align-items: center;\n gap: 0.375rem;\n border-radius: var(--radius-cell);\n /* Indented by level, so a branch reads as being inside its parent. */\n padding: 0.3125rem 0.5rem 0.3125rem calc(0.5rem + (var(--rk-tree-level) - 1) * 1rem);\n font-size: 0.875rem;\n color: var(--color-ink);\n}\n\n.rk-tree-row.is-active {\n background: var(--color-muted);\n}\n\n.rk-tree-row.is-chosen {\n color: var(--color-primary);\n font-weight: 500;\n}\n\n.rk-tree-row.is-chosen.is-active {\n background: color-mix(in oklab, var(--color-primary) 12%, transparent);\n}\n\n.rk-tree-row.is-disabled {\n cursor: not-allowed;\n opacity: 0.45;\n}\n\n.rk-tree-twist,\n.rk-tree-twist-space {\n width: 1rem;\n height: 1rem;\n flex-shrink: 0;\n}\n\n.rk-tree-twist {\n color: var(--color-ink-soft);\n transition: rotate var(--duration-fast) var(--ease-standard);\n}\n\n.rk-tree-twist.is-open {\n rotate: 90deg;\n}\n</style>\n","<script setup lang=\"ts\" generic=\"V extends string\">\nimport { ChevronLeft, ChevronRight } from 'lucide-vue-next'\nimport { computed, ref } from 'vue'\nimport type { Ref } from 'vue'\n\nimport BaseButton from '../components/BaseButton.vue'\nimport BaseListbox from '../components/BaseListbox.vue'\nimport type { ListboxOption } from '../components/BaseListbox.vue'\n\n/**\n * Two lists and the way between them: what is available, what is chosen.\n *\n * The shape people know from permissions and from building a playlist. It\n * is `BaseListbox` twice with two buttons between, which is also why the\n * keyboard works: each side is one Tab stop with arrows inside it, and the\n * buttons say how many are about to move rather than only \"→\".\n *\n * The chosen side keeps the order things were added in, not the order of\n * the source, because that order is usually the point.\n */\nconst {\n options,\n availableLabel,\n chosenLabel,\n addLabel,\n removeLabel,\n height = '14rem',\n} = defineProps<{\n options: readonly ListboxOption<V>[]\n /** Heading over the left list, e.g. \"Available\". Already translated. */\n availableLabel: string\n /** Heading over the right list. */\n chosenLabel: string\n /** The → button's accessible name, e.g. \"Add the chosen ones\". */\n addLabel: string\n /** The ← button's accessible name. */\n removeLabel: string\n height?: string | undefined\n}>()\n\n/** What has been moved across, in the order it was moved. `v-model`. */\nconst model = defineModel<V[]>({ default: () => [] })\n\n/* `as Ref<V[]>`: `ref` unwraps a generic array to `UnwrapRefSimple`, which\n is the same list to a reader and a different type to the compiler. */\nconst markedAvailable = ref([]) as Ref<V[]>\nconst markedChosen = ref([]) as Ref<V[]>\n\nconst available = computed(() => options.filter((option) => !model.value.includes(option.value)))\nconst chosen = computed(() =>\n model.value\n .map((value) => options.find((option) => option.value === value))\n .filter((option): option is ListboxOption<V> => option !== undefined),\n)\n\nfunction add() {\n model.value = [...model.value, ...markedAvailable.value]\n markedAvailable.value = []\n}\n\nfunction remove() {\n model.value = model.value.filter((value) => !markedChosen.value.includes(value))\n markedChosen.value = []\n}\n</script>\n\n<template>\n <div class=\"rk-transfer\">\n <div class=\"rk-transfer-side\">\n <p class=\"rk-transfer-title\">{{ availableLabel }}</p>\n <BaseListbox\n v-model=\"markedAvailable\"\n mode=\"multiple\"\n :options=\"available\"\n :label=\"availableLabel\"\n :height=\"height\"\n />\n </div>\n\n <div class=\"rk-transfer-middle\">\n <BaseButton\n icon\n variant=\"secondary\"\n :aria-label=\"addLabel\"\n :disabled=\"markedAvailable.length === 0\"\n @click=\"add\"\n >\n <ChevronRight class=\"size-4\" />\n </BaseButton>\n <BaseButton\n icon\n variant=\"secondary\"\n :aria-label=\"removeLabel\"\n :disabled=\"markedChosen.length === 0\"\n @click=\"remove\"\n >\n <ChevronLeft class=\"size-4\" />\n </BaseButton>\n </div>\n\n <div class=\"rk-transfer-side\">\n <p class=\"rk-transfer-title\">{{ chosenLabel }}</p>\n <BaseListbox\n v-model=\"markedChosen\"\n mode=\"multiple\"\n :options=\"chosen\"\n :label=\"chosenLabel\"\n :height=\"height\"\n />\n </div>\n </div>\n</template>\n\n<style scoped>\n.rk-transfer {\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);\n align-items: center;\n gap: 0.75rem;\n}\n\n.rk-transfer-side {\n min-width: 0;\n}\n\n.rk-transfer-title {\n margin-bottom: 0.375rem;\n font-size: 0.75rem;\n font-weight: 600;\n color: var(--color-ink-soft);\n}\n\n.rk-transfer-middle {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n/* On a phone the two lists stack and the buttons turn to face them. */\n@media (max-width: 40rem) {\n .rk-transfer {\n grid-template-columns: minmax(0, 1fr);\n }\n\n .rk-transfer-middle {\n flex-direction: row;\n justify-content: center;\n rotate: 90deg;\n }\n}\n</style>\n","<script setup lang=\"ts\" generic=\"V extends string\">\nimport { ChevronLeft, ChevronRight } from 'lucide-vue-next'\nimport { computed, ref } from 'vue'\nimport type { Ref } from 'vue'\n\nimport BaseButton from '../components/BaseButton.vue'\nimport BaseListbox from '../components/BaseListbox.vue'\nimport type { ListboxOption } from '../components/BaseListbox.vue'\n\n/**\n * Two lists and the way between them: what is available, what is chosen.\n *\n * The shape people know from permissions and from building a playlist. It\n * is `BaseListbox` twice with two buttons between, which is also why the\n * keyboard works: each side is one Tab stop with arrows inside it, and the\n * buttons say how many are about to move rather than only \"→\".\n *\n * The chosen side keeps the order things were added in, not the order of\n * the source, because that order is usually the point.\n */\nconst {\n options,\n availableLabel,\n chosenLabel,\n addLabel,\n removeLabel,\n height = '14rem',\n} = defineProps<{\n options: readonly ListboxOption<V>[]\n /** Heading over the left list, e.g. \"Available\". Already translated. */\n availableLabel: string\n /** Heading over the right list. */\n chosenLabel: string\n /** The → button's accessible name, e.g. \"Add the chosen ones\". */\n addLabel: string\n /** The ← button's accessible name. */\n removeLabel: string\n height?: string | undefined\n}>()\n\n/** What has been moved across, in the order it was moved. `v-model`. */\nconst model = defineModel<V[]>({ default: () => [] })\n\n/* `as Ref<V[]>`: `ref` unwraps a generic array to `UnwrapRefSimple`, which\n is the same list to a reader and a different type to the compiler. */\nconst markedAvailable = ref([]) as Ref<V[]>\nconst markedChosen = ref([]) as Ref<V[]>\n\nconst available = computed(() => options.filter((option) => !model.value.includes(option.value)))\nconst chosen = computed(() =>\n model.value\n .map((value) => options.find((option) => option.value === value))\n .filter((option): option is ListboxOption<V> => option !== undefined),\n)\n\nfunction add() {\n model.value = [...model.value, ...markedAvailable.value]\n markedAvailable.value = []\n}\n\nfunction remove() {\n model.value = model.value.filter((value) => !markedChosen.value.includes(value))\n markedChosen.value = []\n}\n</script>\n\n<template>\n <div class=\"rk-transfer\">\n <div class=\"rk-transfer-side\">\n <p class=\"rk-transfer-title\">{{ availableLabel }}</p>\n <BaseListbox\n v-model=\"markedAvailable\"\n mode=\"multiple\"\n :options=\"available\"\n :label=\"availableLabel\"\n :height=\"height\"\n />\n </div>\n\n <div class=\"rk-transfer-middle\">\n <BaseButton\n icon\n variant=\"secondary\"\n :aria-label=\"addLabel\"\n :disabled=\"markedAvailable.length === 0\"\n @click=\"add\"\n >\n <ChevronRight class=\"size-4\" />\n </BaseButton>\n <BaseButton\n icon\n variant=\"secondary\"\n :aria-label=\"removeLabel\"\n :disabled=\"markedChosen.length === 0\"\n @click=\"remove\"\n >\n <ChevronLeft class=\"size-4\" />\n </BaseButton>\n </div>\n\n <div class=\"rk-transfer-side\">\n <p class=\"rk-transfer-title\">{{ chosenLabel }}</p>\n <BaseListbox\n v-model=\"markedChosen\"\n mode=\"multiple\"\n :options=\"chosen\"\n :label=\"chosenLabel\"\n :height=\"height\"\n />\n </div>\n </div>\n</template>\n\n<style scoped>\n.rk-transfer {\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);\n align-items: center;\n gap: 0.75rem;\n}\n\n.rk-transfer-side {\n min-width: 0;\n}\n\n.rk-transfer-title {\n margin-bottom: 0.375rem;\n font-size: 0.75rem;\n font-weight: 600;\n color: var(--color-ink-soft);\n}\n\n.rk-transfer-middle {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n/* On a phone the two lists stack and the buttons turn to face them. */\n@media (max-width: 40rem) {\n .rk-transfer {\n grid-template-columns: minmax(0, 1fr);\n }\n\n .rk-transfer-middle {\n flex-direction: row;\n justify-content: center;\n rotate: 90deg;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nimport BaseSheet from '../components/BaseSheet.vue'\nimport { useMediaQuery } from '../composables/use-media-query'\nimport BaseModal from './BaseModal.vue'\n\n/**\n * One question, asked the way the screen it lands on expects: a modal on a\n * wide screen, a sheet from the bottom edge on a phone.\n *\n * The two are the same thing at different sizes, and apps keep writing both\n * and choosing between them by hand — which is two markups, two sets of\n * labels and one of them usually left behind.\n *\n * The query is read after mount rather than during render: a server has no\n * screen, and a layout guessed there and swapped on hydration is a page that\n * jumps. Until it is known, it is the sheet — the narrow answer, because the\n * narrow one fits on both.\n */\nconst {\n title,\n closeLabel,\n subtitle = '',\n tone = 'default',\n dismissible = true,\n breakpoint = '48rem',\n} = defineProps<{\n /** The dialog's heading, and its accessible name. Already translated. */\n title: string\n /** Accessible name of the close button. */\n closeLabel: string\n /** A line under the title, on the sheet. */\n subtitle?: string | undefined\n /** `alert` makes the wide one an `alertdialog`: an answer is required. */\n tone?: 'default' | 'alert' | undefined\n dismissible?: boolean | undefined\n /** Where it turns from a sheet into a modal. Any media width. */\n breakpoint?: string | undefined\n}>()\n\ndefineSlots<{\n /** The body. */\n default: () => unknown\n /** Buttons, at the foot. */\n actions?: () => unknown\n}>()\n\n/** Open or closed, with `v-model`. */\nconst open = defineModel<boolean>({ default: false })\n\nconst wide = useMediaQuery(`(min-width: ${breakpoint})`)\nconst asModal = computed(() => wide.value)\n</script>\n\n<template>\n <BaseModal\n v-if=\"asModal\"\n v-model=\"open\"\n :title=\"title\"\n :close-label=\"closeLabel\"\n :tone=\"tone\"\n :dismissible=\"dismissible\"\n >\n <slot />\n\n <template v-if=\"$slots.actions\" #actions>\n <slot name=\"actions\" />\n </template>\n </BaseModal>\n\n <BaseSheet v-else v-model=\"open\" :title=\"title\" :subtitle=\"subtitle\" :close-label=\"closeLabel\">\n <slot />\n\n <div v-if=\"$slots.actions\" class=\"rk-responsive-actions\">\n <slot name=\"actions\" />\n </div>\n </BaseSheet>\n</template>\n\n<style scoped>\n/* The sheet has no actions slot of its own: on a phone the buttons belong in\n the flow, at the end of what they are about, within reach of a thumb. */\n.rk-responsive-actions {\n display: flex;\n flex-direction: column-reverse;\n gap: 0.5rem;\n padding-top: 1rem;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nimport BaseSheet from '../components/BaseSheet.vue'\nimport { useMediaQuery } from '../composables/use-media-query'\nimport BaseModal from './BaseModal.vue'\n\n/**\n * One question, asked the way the screen it lands on expects: a modal on a\n * wide screen, a sheet from the bottom edge on a phone.\n *\n * The two are the same thing at different sizes, and apps keep writing both\n * and choosing between them by hand — which is two markups, two sets of\n * labels and one of them usually left behind.\n *\n * The query is read after mount rather than during render: a server has no\n * screen, and a layout guessed there and swapped on hydration is a page that\n * jumps. Until it is known, it is the sheet — the narrow answer, because the\n * narrow one fits on both.\n */\nconst {\n title,\n closeLabel,\n subtitle = '',\n tone = 'default',\n dismissible = true,\n breakpoint = '48rem',\n} = defineProps<{\n /** The dialog's heading, and its accessible name. Already translated. */\n title: string\n /** Accessible name of the close button. */\n closeLabel: string\n /** A line under the title, on the sheet. */\n subtitle?: string | undefined\n /** `alert` makes the wide one an `alertdialog`: an answer is required. */\n tone?: 'default' | 'alert' | undefined\n dismissible?: boolean | undefined\n /** Where it turns from a sheet into a modal. Any media width. */\n breakpoint?: string | undefined\n}>()\n\ndefineSlots<{\n /** The body. */\n default: () => unknown\n /** Buttons, at the foot. */\n actions?: () => unknown\n}>()\n\n/** Open or closed, with `v-model`. */\nconst open = defineModel<boolean>({ default: false })\n\nconst wide = useMediaQuery(`(min-width: ${breakpoint})`)\nconst asModal = computed(() => wide.value)\n</script>\n\n<template>\n <BaseModal\n v-if=\"asModal\"\n v-model=\"open\"\n :title=\"title\"\n :close-label=\"closeLabel\"\n :tone=\"tone\"\n :dismissible=\"dismissible\"\n >\n <slot />\n\n <template v-if=\"$slots.actions\" #actions>\n <slot name=\"actions\" />\n </template>\n </BaseModal>\n\n <BaseSheet v-else v-model=\"open\" :title=\"title\" :subtitle=\"subtitle\" :close-label=\"closeLabel\">\n <slot />\n\n <div v-if=\"$slots.actions\" class=\"rk-responsive-actions\">\n <slot name=\"actions\" />\n </div>\n </BaseSheet>\n</template>\n\n<style scoped>\n/* The sheet has no actions slot of its own: on a phone the buttons belong in\n the flow, at the end of what they are about, within reach of a thumb. */\n.rk-responsive-actions {\n display: flex;\n flex-direction: column-reverse;\n gap: 0.5rem;\n padding-top: 1rem;\n}\n</style>\n","<script setup lang=\"ts\" generic=\"K extends string\">\nimport { ref, useId } from 'vue'\n\nexport interface TabPanel<K extends string> {\n key: K\n /** Already translated. */\n label: string\n}\n\n/**\n * Sections of one page, one visible at a time.\n *\n * Not `TabBar` and not `NavLinks`: those navigate, and the browser's back\n * button undoes them. These switch a panel inside a page that does not change,\n * so there is no history entry and no route.\n *\n * ## The keyboard, which is the whole reason this is a component\n *\n * The pattern is prescribed and it is not what a row of buttons does by\n * default. Tab enters the tablist once and leaves it once — it does not walk\n * through every tab — and the arrow keys move between them. That is roving\n * tabindex: exactly one tab is reachable by Tab at a time, and moving the\n * selection moves it. Home and End jump to the ends, and the selection wraps,\n * because a list with no edges is faster than one you can fall off.\n *\n * Hand-written tabs almost always get this wrong in the same way: every tab is\n * tabbable, so a keyboard user presses Tab six times to reach the content.\n */\nconst { items, label = '' } = defineProps<{\n items: readonly TabPanel<K>[]\n /** Accessible name for the tablist. */\n label?: string | undefined\n}>()\n\ndefineSlots<{\n /** The panel for the selected tab. */\n default: (props: { item: TabPanel<K>; active: K }) => unknown\n}>()\n\nconst active = defineModel<K>({ required: true })\n\nconst uid = useId()\nconst tabs = ref<HTMLElement[]>([])\n\nconst tabId = (key: K) => `${uid}-tab-${key}`\nconst panelId = (key: K) => `${uid}-panel-${key}`\n\nfunction select(key: K, focus = false) {\n active.value = key\n if (!focus) return\n\n // Focus follows selection, which is what the arrow keys are for. The element\n // is looked up after the update so the new tab is the one that is reachable.\n const index = items.findIndex((item) => item.key === key)\n requestAnimationFrame(() => tabs.value[index]?.focus())\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n const index = items.findIndex((item) => item.key === active.value)\n if (index < 0) return\n\n const last = items.length - 1\n let next: number | undefined\n\n if (event.key === 'ArrowRight') next = index === last ? 0 : index + 1\n else if (event.key === 'ArrowLeft') next = index === 0 ? last : index - 1\n else if (event.key === 'Home') next = 0\n else if (event.key === 'End') next = last\n else return\n\n event.preventDefault()\n const item = items[next]\n if (item) select(item.key, true)\n}\n</script>\n\n<template>\n <div>\n <div class=\"rk-tabs\" role=\"tablist\" :aria-label=\"label || undefined\" @keydown=\"onKeydown\">\n <button\n v-for=\"item in items\"\n :id=\"tabId(item.key)\"\n :key=\"item.key\"\n ref=\"tabs\"\n type=\"button\"\n role=\"tab\"\n class=\"rk-tab\"\n :class=\"{ 'is-active': item.key === active }\"\n :aria-selected=\"item.key === active\"\n :aria-controls=\"panelId(item.key)\"\n :tabindex=\"item.key === active ? 0 : -1\"\n @click=\"select(item.key)\"\n >\n {{ item.label }}\n </button>\n </div>\n\n <div\n v-for=\"item in items\"\n v-show=\"item.key === active\"\n :id=\"panelId(item.key)\"\n :key=\"item.key\"\n role=\"tabpanel\"\n class=\"rk-tabpanel\"\n :aria-labelledby=\"tabId(item.key)\"\n tabindex=\"0\"\n >\n <!-- Every panel is rendered and hidden rather than swapped, so the page\n keeps its height and a crawler sees all of it. -->\n <slot :item=\"item\" :active=\"active\" />\n </div>\n </div>\n</template>\n\n<style scoped>\n.rk-tabs {\n display: flex;\n align-items: center;\n gap: 0.25rem;\n border-bottom: 1px solid color-mix(in srgb, var(--color-hair) 70%, transparent);\n}\n\n.rk-tab {\n position: relative;\n padding: 0.625rem 0.875rem;\n font-size: 0.875rem;\n color: var(--color-ink-soft);\n transition: color var(--duration-fast);\n}\n\n.rk-tab:hover {\n color: var(--color-ink);\n}\n\n.rk-tab:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: -2px;\n border-radius: var(--radius-cell);\n}\n\n.rk-tab.is-active {\n color: var(--color-ink);\n font-weight: 500;\n}\n\n.rk-tab.is-active::after {\n content: '';\n position: absolute;\n inset-inline: 0.5rem;\n bottom: -1px;\n height: 2px;\n border-radius: 9999px;\n background: var(--color-primary);\n}\n\n.rk-tabpanel {\n padding-top: 1rem;\n}\n\n.rk-tabpanel:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n</style>\n","<script setup lang=\"ts\" generic=\"K extends string\">\nimport { ref, useId } from 'vue'\n\nexport interface TabPanel<K extends string> {\n key: K\n /** Already translated. */\n label: string\n}\n\n/**\n * Sections of one page, one visible at a time.\n *\n * Not `TabBar` and not `NavLinks`: those navigate, and the browser's back\n * button undoes them. These switch a panel inside a page that does not change,\n * so there is no history entry and no route.\n *\n * ## The keyboard, which is the whole reason this is a component\n *\n * The pattern is prescribed and it is not what a row of buttons does by\n * default. Tab enters the tablist once and leaves it once — it does not walk\n * through every tab — and the arrow keys move between them. That is roving\n * tabindex: exactly one tab is reachable by Tab at a time, and moving the\n * selection moves it. Home and End jump to the ends, and the selection wraps,\n * because a list with no edges is faster than one you can fall off.\n *\n * Hand-written tabs almost always get this wrong in the same way: every tab is\n * tabbable, so a keyboard user presses Tab six times to reach the content.\n */\nconst { items, label = '' } = defineProps<{\n items: readonly TabPanel<K>[]\n /** Accessible name for the tablist. */\n label?: string | undefined\n}>()\n\ndefineSlots<{\n /** The panel for the selected tab. */\n default: (props: { item: TabPanel<K>; active: K }) => unknown\n}>()\n\nconst active = defineModel<K>({ required: true })\n\nconst uid = useId()\nconst tabs = ref<HTMLElement[]>([])\n\nconst tabId = (key: K) => `${uid}-tab-${key}`\nconst panelId = (key: K) => `${uid}-panel-${key}`\n\nfunction select(key: K, focus = false) {\n active.value = key\n if (!focus) return\n\n // Focus follows selection, which is what the arrow keys are for. The element\n // is looked up after the update so the new tab is the one that is reachable.\n const index = items.findIndex((item) => item.key === key)\n requestAnimationFrame(() => tabs.value[index]?.focus())\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n const index = items.findIndex((item) => item.key === active.value)\n if (index < 0) return\n\n const last = items.length - 1\n let next: number | undefined\n\n if (event.key === 'ArrowRight') next = index === last ? 0 : index + 1\n else if (event.key === 'ArrowLeft') next = index === 0 ? last : index - 1\n else if (event.key === 'Home') next = 0\n else if (event.key === 'End') next = last\n else return\n\n event.preventDefault()\n const item = items[next]\n if (item) select(item.key, true)\n}\n</script>\n\n<template>\n <div>\n <div class=\"rk-tabs\" role=\"tablist\" :aria-label=\"label || undefined\" @keydown=\"onKeydown\">\n <button\n v-for=\"item in items\"\n :id=\"tabId(item.key)\"\n :key=\"item.key\"\n ref=\"tabs\"\n type=\"button\"\n role=\"tab\"\n class=\"rk-tab\"\n :class=\"{ 'is-active': item.key === active }\"\n :aria-selected=\"item.key === active\"\n :aria-controls=\"panelId(item.key)\"\n :tabindex=\"item.key === active ? 0 : -1\"\n @click=\"select(item.key)\"\n >\n {{ item.label }}\n </button>\n </div>\n\n <div\n v-for=\"item in items\"\n v-show=\"item.key === active\"\n :id=\"panelId(item.key)\"\n :key=\"item.key\"\n role=\"tabpanel\"\n class=\"rk-tabpanel\"\n :aria-labelledby=\"tabId(item.key)\"\n tabindex=\"0\"\n >\n <!-- Every panel is rendered and hidden rather than swapped, so the page\n keeps its height and a crawler sees all of it. -->\n <slot :item=\"item\" :active=\"active\" />\n </div>\n </div>\n</template>\n\n<style scoped>\n.rk-tabs {\n display: flex;\n align-items: center;\n gap: 0.25rem;\n border-bottom: 1px solid color-mix(in srgb, var(--color-hair) 70%, transparent);\n}\n\n.rk-tab {\n position: relative;\n padding: 0.625rem 0.875rem;\n font-size: 0.875rem;\n color: var(--color-ink-soft);\n transition: color var(--duration-fast);\n}\n\n.rk-tab:hover {\n color: var(--color-ink);\n}\n\n.rk-tab:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: -2px;\n border-radius: var(--radius-cell);\n}\n\n.rk-tab.is-active {\n color: var(--color-ink);\n font-weight: 500;\n}\n\n.rk-tab.is-active::after {\n content: '';\n position: absolute;\n inset-inline: 0.5rem;\n bottom: -1px;\n height: 2px;\n border-radius: 9999px;\n background: var(--color-primary);\n}\n\n.rk-tabpanel {\n padding-top: 1rem;\n}\n\n.rk-tabpanel:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { useId } from 'vue'\n\n/**\n * A short label that appears beside a control.\n *\n * ## CSS, not JavaScript, and that is a decision\n *\n * The bubble is shown by `:hover` and `:focus-within` in the stylesheet. No\n * listener, no state, no positioning library. That costs collision detection —\n * a tooltip near the right edge is clipped rather than flipped — and buys the\n * only thing that matters more: it works in a prerendered page, before the\n * bundle has arrived, and with JavaScript switched off entirely.\n *\n * `:focus-within` rather than `:hover` alone is the part people leave out. A\n * tooltip that only answers a mouse is a tooltip that does not exist for anyone\n * using a keyboard, and the content is usually the only explanation of what the\n * control does.\n *\n * The bubble is wired with `aria-describedby`, so a screen reader reads it as a\n * description of the control rather than as loose text nearby. That is why the\n * trigger goes in a slot: the component has to own the id on both ends.\n *\n * ## When not to use it\n *\n * For anything the reader must have. A tooltip is unreachable on a touch screen\n * without a hover state, so text that changes a decision belongs on the page.\n */\nconst { label, placement = 'top' } = defineProps<{\n /** The description. Already translated. */\n label: string\n placement?: 'top' | 'bottom' | undefined\n}>()\n\ndefineSlots<{\n /** The control being described. */\n default: (props: { describedBy: string }) => unknown\n}>()\n\nconst id = useId()\n</script>\n\n<template>\n <span class=\"rk-tip\">\n <slot :described-by=\"id\" />\n\n <!-- `role=\"tooltip\"` and the id, so the control can point at it. Not\n `aria-hidden`: it is the description, not decoration. -->\n <span :id=\"id\" role=\"tooltip\" class=\"rk-tip-bubble\" :class=\"`is-${placement}`\">\n {{ label }}\n </span>\n </span>\n</template>\n\n<style scoped>\n.rk-tip {\n position: relative;\n display: inline-flex;\n}\n\n.rk-tip-bubble {\n position: absolute;\n left: 50%;\n z-index: 60;\n width: max-content;\n max-width: 16rem;\n transform: translateX(-50%);\n border-radius: var(--radius-cell);\n background: var(--color-ink);\n padding: 0.375rem 0.5rem;\n font-size: 0.75rem;\n line-height: 1.4;\n color: var(--color-canvas);\n opacity: 0;\n /* Out of the hit area while hidden, or it would swallow the pointer on its\n way to the control it describes. */\n pointer-events: none;\n transition: opacity var(--duration-fast) ease;\n}\n\n.rk-tip-bubble.is-top {\n bottom: calc(100% + 0.375rem);\n}\n\n.rk-tip-bubble.is-bottom {\n top: calc(100% + 0.375rem);\n}\n\n.rk-tip:hover .rk-tip-bubble,\n.rk-tip:focus-within .rk-tip-bubble {\n opacity: 1;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .rk-tip-bubble {\n transition: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { useId } from 'vue'\n\n/**\n * A short label that appears beside a control.\n *\n * ## CSS, not JavaScript, and that is a decision\n *\n * The bubble is shown by `:hover` and `:focus-within` in the stylesheet. No\n * listener, no state, no positioning library. That costs collision detection —\n * a tooltip near the right edge is clipped rather than flipped — and buys the\n * only thing that matters more: it works in a prerendered page, before the\n * bundle has arrived, and with JavaScript switched off entirely.\n *\n * `:focus-within` rather than `:hover` alone is the part people leave out. A\n * tooltip that only answers a mouse is a tooltip that does not exist for anyone\n * using a keyboard, and the content is usually the only explanation of what the\n * control does.\n *\n * The bubble is wired with `aria-describedby`, so a screen reader reads it as a\n * description of the control rather than as loose text nearby. That is why the\n * trigger goes in a slot: the component has to own the id on both ends.\n *\n * ## When not to use it\n *\n * For anything the reader must have. A tooltip is unreachable on a touch screen\n * without a hover state, so text that changes a decision belongs on the page.\n */\nconst { label, placement = 'top' } = defineProps<{\n /** The description. Already translated. */\n label: string\n placement?: 'top' | 'bottom' | undefined\n}>()\n\ndefineSlots<{\n /** The control being described. */\n default: (props: { describedBy: string }) => unknown\n}>()\n\nconst id = useId()\n</script>\n\n<template>\n <span class=\"rk-tip\">\n <slot :described-by=\"id\" />\n\n <!-- `role=\"tooltip\"` and the id, so the control can point at it. Not\n `aria-hidden`: it is the description, not decoration. -->\n <span :id=\"id\" role=\"tooltip\" class=\"rk-tip-bubble\" :class=\"`is-${placement}`\">\n {{ label }}\n </span>\n </span>\n</template>\n\n<style scoped>\n.rk-tip {\n position: relative;\n display: inline-flex;\n}\n\n.rk-tip-bubble {\n position: absolute;\n left: 50%;\n z-index: 60;\n width: max-content;\n max-width: 16rem;\n transform: translateX(-50%);\n border-radius: var(--radius-cell);\n background: var(--color-ink);\n padding: 0.375rem 0.5rem;\n font-size: 0.75rem;\n line-height: 1.4;\n color: var(--color-canvas);\n opacity: 0;\n /* Out of the hit area while hidden, or it would swallow the pointer on its\n way to the control it describes. */\n pointer-events: none;\n transition: opacity var(--duration-fast) ease;\n}\n\n.rk-tip-bubble.is-top {\n bottom: calc(100% + 0.375rem);\n}\n\n.rk-tip-bubble.is-bottom {\n top: calc(100% + 0.375rem);\n}\n\n.rk-tip:hover .rk-tip-bubble,\n.rk-tip:focus-within .rk-tip-bubble {\n opacity: 1;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .rk-tip-bubble {\n transition: none;\n }\n}\n</style>\n","<script setup lang=\"ts\" generic=\"K extends string\">\nimport { RouterLink } from 'vue-router'\n\nexport interface NavLinkItem<K extends string> {\n /** Identity, compared against `active`. */\n key: K\n /** Router destination. */\n to: string\n /** Already translated. */\n label: string\n}\n\n/**\n * The primary navigation of a wide site.\n *\n * The same contract as `TabBar` minus the icon, so moving an app between a\n * bottom bar and a top one is a change of component and not of data. That is\n * the whole reason the shapes match.\n *\n * A rule under the current section rather than a filled pill: at this size a\n * pill reads as a button, and these are not buttons — they are where you are.\n * The rule is a scaled element rather than a border so it can travel, and it is\n * `aria-hidden` because `aria-current` already says the same thing to anyone not\n * looking at it.\n */\nconst {\n items,\n active,\n label = '',\n} = defineProps<{\n items: readonly NavLinkItem<K>[]\n /** Which item is current. Usually derived from the route. */\n active?: K | undefined\n /** Accessible name for the navigation landmark. */\n label?: string | undefined\n}>()\n</script>\n\n<template>\n <nav :aria-label=\"label || undefined\">\n <div class=\"rk-nav-inner\">\n <RouterLink\n v-for=\"item in items\"\n :key=\"item.key\"\n :to=\"item.to\"\n class=\"rk-nav-link\"\n :class=\"{ 'is-active': item.key === active }\"\n :aria-current=\"item.key === active ? 'page' : undefined\"\n >\n {{ item.label }}\n <span class=\"rk-nav-rule\" aria-hidden=\"true\" />\n </RouterLink>\n </div>\n </nav>\n</template>\n\n<style scoped>\n/* The root carries no layout of its own, and that is the fix for a real bug\n rather than a style preference.\n\n A rule on the root loses to nothing: Vue's scoped CSS compiles `.rk-nav` to\n `.rk-nav[data-v-hash]`, which outranks a utility class like Tailwind's\n `hidden` outright — so `class=\"hidden sm:flex\"` did nothing and the nav\n appeared on a phone, on top of the call to action. `:where()` does not save\n it either; the scoping attribute puts the specificity back.\n\n So the flex row lives on an element the caller does not own, and the root is\n left for them to style. `TabBar` is built the same way for the same reason. */\n.rk-nav-inner {\n display: flex;\n align-items: center;\n gap: 0.25rem;\n}\n\n.rk-nav-link {\n position: relative;\n border-radius: var(--radius-cell);\n padding: 0.5rem 0.75rem;\n font-size: 0.875rem;\n color: var(--color-ink-soft);\n transition: color var(--duration-fast);\n}\n\n.rk-nav-link:hover {\n color: var(--color-ink);\n}\n\n.rk-nav-link:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n.rk-nav-link.is-active {\n color: var(--color-ink);\n font-weight: 500;\n}\n\n.rk-nav-rule {\n position: absolute;\n inset-inline: 0.75rem;\n bottom: -1px;\n height: 2px;\n border-radius: 9999px;\n background: var(--color-primary);\n transform: scaleX(0);\n transform-origin: left;\n transition: transform var(--duration-slow);\n}\n\n.rk-nav-link.is-active .rk-nav-rule {\n transform: scaleX(1);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .rk-nav-rule {\n transition: none;\n }\n}\n</style>\n","<script setup lang=\"ts\" generic=\"K extends string\">\nimport { RouterLink } from 'vue-router'\n\nexport interface NavLinkItem<K extends string> {\n /** Identity, compared against `active`. */\n key: K\n /** Router destination. */\n to: string\n /** Already translated. */\n label: string\n}\n\n/**\n * The primary navigation of a wide site.\n *\n * The same contract as `TabBar` minus the icon, so moving an app between a\n * bottom bar and a top one is a change of component and not of data. That is\n * the whole reason the shapes match.\n *\n * A rule under the current section rather than a filled pill: at this size a\n * pill reads as a button, and these are not buttons — they are where you are.\n * The rule is a scaled element rather than a border so it can travel, and it is\n * `aria-hidden` because `aria-current` already says the same thing to anyone not\n * looking at it.\n */\nconst {\n items,\n active,\n label = '',\n} = defineProps<{\n items: readonly NavLinkItem<K>[]\n /** Which item is current. Usually derived from the route. */\n active?: K | undefined\n /** Accessible name for the navigation landmark. */\n label?: string | undefined\n}>()\n</script>\n\n<template>\n <nav :aria-label=\"label || undefined\">\n <div class=\"rk-nav-inner\">\n <RouterLink\n v-for=\"item in items\"\n :key=\"item.key\"\n :to=\"item.to\"\n class=\"rk-nav-link\"\n :class=\"{ 'is-active': item.key === active }\"\n :aria-current=\"item.key === active ? 'page' : undefined\"\n >\n {{ item.label }}\n <span class=\"rk-nav-rule\" aria-hidden=\"true\" />\n </RouterLink>\n </div>\n </nav>\n</template>\n\n<style scoped>\n/* The root carries no layout of its own, and that is the fix for a real bug\n rather than a style preference.\n\n A rule on the root loses to nothing: Vue's scoped CSS compiles `.rk-nav` to\n `.rk-nav[data-v-hash]`, which outranks a utility class like Tailwind's\n `hidden` outright — so `class=\"hidden sm:flex\"` did nothing and the nav\n appeared on a phone, on top of the call to action. `:where()` does not save\n it either; the scoping attribute puts the specificity back.\n\n So the flex row lives on an element the caller does not own, and the root is\n left for them to style. `TabBar` is built the same way for the same reason. */\n.rk-nav-inner {\n display: flex;\n align-items: center;\n gap: 0.25rem;\n}\n\n.rk-nav-link {\n position: relative;\n border-radius: var(--radius-cell);\n padding: 0.5rem 0.75rem;\n font-size: 0.875rem;\n color: var(--color-ink-soft);\n transition: color var(--duration-fast);\n}\n\n.rk-nav-link:hover {\n color: var(--color-ink);\n}\n\n.rk-nav-link:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n.rk-nav-link.is-active {\n color: var(--color-ink);\n font-weight: 500;\n}\n\n.rk-nav-rule {\n position: absolute;\n inset-inline: 0.75rem;\n bottom: -1px;\n height: 2px;\n border-radius: 9999px;\n background: var(--color-primary);\n transform: scaleX(0);\n transform-origin: left;\n transition: transform var(--duration-slow);\n}\n\n.rk-nav-link.is-active .rk-nav-rule {\n transform: scaleX(1);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .rk-nav-rule {\n transition: none;\n }\n}\n</style>\n"],"mappings":"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"}
1
+ {"version":3,"file":"web.js","names":["$slots","$slots"],"sources":["../src/web/BaseAccordion.vue","../src/web/BaseAccordion.vue","../src/web/BaseBreadcrumb.vue","../src/web/BaseBreadcrumb.vue","../src/web/BaseDisclosure.vue","../src/web/BaseDisclosure.vue","../src/web/BaseModal.vue","../src/web/BaseModal.vue","../src/web/BasePagination.vue","../src/web/BasePagination.vue","../src/web/CommandMenu.vue","../src/web/CommandMenu.vue","../src/web/DataTable.vue","../src/web/DataTable.vue","../src/web/BaseSplitter.vue","../src/web/BaseSplitter.vue","../src/web/BaseToolbar.vue","../src/web/BaseToolbar.vue","../src/web/BaseTree.vue","../src/web/BaseTree.vue","../src/web/TransferList.vue","../src/web/TransferList.vue","../src/web/ResponsiveDialog.vue","../src/web/ResponsiveDialog.vue","../src/web/BaseTabs.vue","../src/web/BaseTabs.vue","../src/web/BaseTooltip.vue","../src/web/BaseTooltip.vue","../src/web/NavLinks.vue","../src/web/NavLinks.vue"],"sourcesContent":["<script setup lang=\"ts\" generic=\"K extends string\">\nimport { shallowRef, useId } from 'vue'\n\nexport interface AccordionItem<K extends string> {\n key: K\n /** The question. Already translated. */\n title: string\n}\n\n/**\n * A list that opens one section at a time.\n *\n * ## Why not `<details>`\n *\n * `<details>` is the honest element for this and it was the first choice. A\n * browser removes its content from layout entirely when closed, so there is\n * nothing to animate between — the panel can only appear. Faking it by leaving\n * `open` set and hiding with CSS tells a screen reader the section is expanded\n * while a sighted reader sees it shut, which is worse than no animation.\n *\n * So: a button carrying `aria-expanded`, the answer always in the markup — the\n * prerendered HTML carries every answer, which is what a crawler and a reader\n * without JavaScript get — and a grid row that travels from `0fr` to `1fr`,\n * which is the one way to animate to a height nobody has measured.\n *\n * The content is always present, so anything expensive in it is paid for on\n * first paint. That is the trade: a section a crawler can read costs a section\n * that is never lazy.\n */\nconst {\n items,\n multiple = false,\n headingLevel = 3,\n} = defineProps<{\n items: readonly AccordionItem<K>[]\n /** Allow several open at once. Off by default, which is what makes it an accordion. */\n multiple?: boolean | undefined\n /**\n * Which heading the control sits inside.\n *\n * A heading, always — it is how a screen reader user moves between sections\n * without opening any of them. The level is a prop because it depends on what\n * is above it on the page, and getting it wrong breaks the outline.\n */\n headingLevel?: 2 | 3 | 4 | undefined\n}>()\n\ndefineSlots<{\n /** The answer, per item. */\n default: (props: { item: AccordionItem<K>; open: boolean }) => unknown\n /** The question, when the item's `title` is not enough. */\n title?: (props: { item: AccordionItem<K>; open: boolean }) => unknown\n}>()\n\nconst uid = useId()\n/* `shallowRef` because the list is replaced wholesale rather than mutated, and\n because a deep ref unwraps a generic key type into something TypeScript can\n no longer match against `K`. */\nconst open = shallowRef<K[]>([])\n\nconst isOpen = (key: K) => open.value.includes(key)\n\nfunction toggle(key: K) {\n if (isOpen(key)) {\n open.value = open.value.filter((k) => k !== key)\n } else {\n open.value = multiple ? [...open.value, key] : [key]\n }\n}\n</script>\n\n<template>\n <ul class=\"rk-accordion\">\n <li v-for=\"item in items\" :key=\"item.key\" class=\"rk-accordion-item\">\n <component :is=\"`h${headingLevel}`\">\n <button\n type=\"button\"\n class=\"rk-accordion-control\"\n :aria-expanded=\"isOpen(item.key)\"\n :aria-controls=\"`${uid}-${item.key}`\"\n @click=\"toggle(item.key)\"\n >\n <span class=\"rk-accordion-title\">\n <slot name=\"title\" :item=\"item\" :open=\"isOpen(item.key)\">{{ item.title }}</slot>\n </span>\n\n <!-- A plus that becomes a minus: one stroke turns, so the control\n keeps its place while it changes meaning. -->\n <span class=\"rk-accordion-mark\" aria-hidden=\"true\">\n <span class=\"rk-accordion-bar\" />\n <span\n class=\"rk-accordion-bar rk-accordion-bar-turn\"\n :class=\"{ 'is-open': isOpen(item.key) }\"\n />\n </span>\n </button>\n </component>\n\n <div\n :id=\"`${uid}-${item.key}`\"\n class=\"rk-accordion-panel\"\n :class=\"{ 'is-open': isOpen(item.key) }\"\n >\n <div class=\"rk-accordion-clip\">\n <div class=\"rk-accordion-body\" :class=\"{ 'is-open': isOpen(item.key) }\">\n <slot :item=\"item\" :open=\"isOpen(item.key)\" />\n </div>\n </div>\n </div>\n </li>\n </ul>\n</template>\n\n<style scoped>\n.rk-accordion {\n border-top: 1px solid color-mix(in srgb, var(--color-hair) 70%, transparent);\n}\n\n.rk-accordion-item {\n border-bottom: 1px solid color-mix(in srgb, var(--color-hair) 70%, transparent);\n}\n\n.rk-accordion-control {\n display: flex;\n width: 100%;\n cursor: pointer;\n align-items: center;\n justify-content: space-between;\n gap: 1.5rem;\n padding: 1.25rem 0;\n text-align: left;\n transition: color var(--duration-fast);\n}\n\n.rk-accordion-control:hover {\n color: var(--color-primary);\n}\n\n.rk-accordion-control:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n.rk-accordion-title {\n color: var(--color-ink);\n font-size: 1rem;\n font-weight: 500;\n}\n\n.rk-accordion-mark {\n position: relative;\n display: grid;\n place-items: center;\n flex-shrink: 0;\n width: 1.5rem;\n height: 1.5rem;\n}\n\n.rk-accordion-bar {\n position: absolute;\n height: 1px;\n width: 0.875rem;\n background: var(--color-ink-soft);\n}\n\n.rk-accordion-bar-turn {\n transform: rotate(90deg);\n transition: transform var(--duration-slower) ease-out;\n}\n\n.rk-accordion-bar-turn.is-open {\n transform: rotate(0deg);\n}\n\n/* 0fr to 1fr is the whole trick: a grid row can be animated to a height that\n was never measured, which is what `height: auto` cannot do. */\n.rk-accordion-panel {\n display: grid;\n grid-template-rows: 0fr;\n transition: grid-template-rows var(--duration-slower) ease-out;\n}\n\n.rk-accordion-panel.is-open {\n grid-template-rows: 1fr;\n}\n\n.rk-accordion-clip {\n overflow: hidden;\n}\n\n.rk-accordion-body {\n max-width: 68ch;\n padding-bottom: 1.5rem;\n color: var(--color-ink-soft);\n line-height: 1.625;\n opacity: 0;\n transition: opacity var(--duration-slow);\n}\n\n.rk-accordion-body.is-open {\n opacity: 1;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .rk-accordion-panel,\n .rk-accordion-bar-turn,\n .rk-accordion-body {\n transition: none;\n }\n}\n</style>\n","<script setup lang=\"ts\" generic=\"K extends string\">\nimport { shallowRef, useId } from 'vue'\n\nexport interface AccordionItem<K extends string> {\n key: K\n /** The question. Already translated. */\n title: string\n}\n\n/**\n * A list that opens one section at a time.\n *\n * ## Why not `<details>`\n *\n * `<details>` is the honest element for this and it was the first choice. A\n * browser removes its content from layout entirely when closed, so there is\n * nothing to animate between — the panel can only appear. Faking it by leaving\n * `open` set and hiding with CSS tells a screen reader the section is expanded\n * while a sighted reader sees it shut, which is worse than no animation.\n *\n * So: a button carrying `aria-expanded`, the answer always in the markup — the\n * prerendered HTML carries every answer, which is what a crawler and a reader\n * without JavaScript get — and a grid row that travels from `0fr` to `1fr`,\n * which is the one way to animate to a height nobody has measured.\n *\n * The content is always present, so anything expensive in it is paid for on\n * first paint. That is the trade: a section a crawler can read costs a section\n * that is never lazy.\n */\nconst {\n items,\n multiple = false,\n headingLevel = 3,\n} = defineProps<{\n items: readonly AccordionItem<K>[]\n /** Allow several open at once. Off by default, which is what makes it an accordion. */\n multiple?: boolean | undefined\n /**\n * Which heading the control sits inside.\n *\n * A heading, always — it is how a screen reader user moves between sections\n * without opening any of them. The level is a prop because it depends on what\n * is above it on the page, and getting it wrong breaks the outline.\n */\n headingLevel?: 2 | 3 | 4 | undefined\n}>()\n\ndefineSlots<{\n /** The answer, per item. */\n default: (props: { item: AccordionItem<K>; open: boolean }) => unknown\n /** The question, when the item's `title` is not enough. */\n title?: (props: { item: AccordionItem<K>; open: boolean }) => unknown\n}>()\n\nconst uid = useId()\n/* `shallowRef` because the list is replaced wholesale rather than mutated, and\n because a deep ref unwraps a generic key type into something TypeScript can\n no longer match against `K`. */\nconst open = shallowRef<K[]>([])\n\nconst isOpen = (key: K) => open.value.includes(key)\n\nfunction toggle(key: K) {\n if (isOpen(key)) {\n open.value = open.value.filter((k) => k !== key)\n } else {\n open.value = multiple ? [...open.value, key] : [key]\n }\n}\n</script>\n\n<template>\n <ul class=\"rk-accordion\">\n <li v-for=\"item in items\" :key=\"item.key\" class=\"rk-accordion-item\">\n <component :is=\"`h${headingLevel}`\">\n <button\n type=\"button\"\n class=\"rk-accordion-control\"\n :aria-expanded=\"isOpen(item.key)\"\n :aria-controls=\"`${uid}-${item.key}`\"\n @click=\"toggle(item.key)\"\n >\n <span class=\"rk-accordion-title\">\n <slot name=\"title\" :item=\"item\" :open=\"isOpen(item.key)\">{{ item.title }}</slot>\n </span>\n\n <!-- A plus that becomes a minus: one stroke turns, so the control\n keeps its place while it changes meaning. -->\n <span class=\"rk-accordion-mark\" aria-hidden=\"true\">\n <span class=\"rk-accordion-bar\" />\n <span\n class=\"rk-accordion-bar rk-accordion-bar-turn\"\n :class=\"{ 'is-open': isOpen(item.key) }\"\n />\n </span>\n </button>\n </component>\n\n <div\n :id=\"`${uid}-${item.key}`\"\n class=\"rk-accordion-panel\"\n :class=\"{ 'is-open': isOpen(item.key) }\"\n >\n <div class=\"rk-accordion-clip\">\n <div class=\"rk-accordion-body\" :class=\"{ 'is-open': isOpen(item.key) }\">\n <slot :item=\"item\" :open=\"isOpen(item.key)\" />\n </div>\n </div>\n </div>\n </li>\n </ul>\n</template>\n\n<style scoped>\n.rk-accordion {\n border-top: 1px solid color-mix(in srgb, var(--color-hair) 70%, transparent);\n}\n\n.rk-accordion-item {\n border-bottom: 1px solid color-mix(in srgb, var(--color-hair) 70%, transparent);\n}\n\n.rk-accordion-control {\n display: flex;\n width: 100%;\n cursor: pointer;\n align-items: center;\n justify-content: space-between;\n gap: 1.5rem;\n padding: 1.25rem 0;\n text-align: left;\n transition: color var(--duration-fast);\n}\n\n.rk-accordion-control:hover {\n color: var(--color-primary);\n}\n\n.rk-accordion-control:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n.rk-accordion-title {\n color: var(--color-ink);\n font-size: 1rem;\n font-weight: 500;\n}\n\n.rk-accordion-mark {\n position: relative;\n display: grid;\n place-items: center;\n flex-shrink: 0;\n width: 1.5rem;\n height: 1.5rem;\n}\n\n.rk-accordion-bar {\n position: absolute;\n height: 1px;\n width: 0.875rem;\n background: var(--color-ink-soft);\n}\n\n.rk-accordion-bar-turn {\n transform: rotate(90deg);\n transition: transform var(--duration-slower) ease-out;\n}\n\n.rk-accordion-bar-turn.is-open {\n transform: rotate(0deg);\n}\n\n/* 0fr to 1fr is the whole trick: a grid row can be animated to a height that\n was never measured, which is what `height: auto` cannot do. */\n.rk-accordion-panel {\n display: grid;\n grid-template-rows: 0fr;\n transition: grid-template-rows var(--duration-slower) ease-out;\n}\n\n.rk-accordion-panel.is-open {\n grid-template-rows: 1fr;\n}\n\n.rk-accordion-clip {\n overflow: hidden;\n}\n\n.rk-accordion-body {\n max-width: 68ch;\n padding-bottom: 1.5rem;\n color: var(--color-ink-soft);\n line-height: 1.625;\n opacity: 0;\n transition: opacity var(--duration-slow);\n}\n\n.rk-accordion-body.is-open {\n opacity: 1;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .rk-accordion-panel,\n .rk-accordion-bar-turn,\n .rk-accordion-body {\n transition: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { RouterLink } from 'vue-router'\n\nexport interface Crumb {\n /** Already translated. */\n label: string\n /** Omitted for the current page, which is not a link to itself. */\n to?: string | undefined\n}\n\n/**\n * Where this page sits, and the way back up.\n *\n * An ordered list inside a `nav`, because the order is the meaning. The last\n * crumb is the current page and is deliberately not a link: a link to the page\n * you are on is a control that does nothing, and `aria-current=\"page\"` is what\n * says so to everyone else.\n *\n * The separators are `aria-hidden`. A screen reader announces a list and its\n * position in it; reading \"slash\" between every item is noise on top of\n * information the reader already has.\n */\nconst { items, label = '' } = defineProps<{\n items: readonly Crumb[]\n /** Accessible name for the landmark. */\n label?: string | undefined\n}>()\n</script>\n\n<template>\n <nav :aria-label=\"label || undefined\">\n <ol class=\"rk-crumbs\">\n <li v-for=\"(item, index) in items\" :key=\"item.label\" class=\"rk-crumb\">\n <RouterLink v-if=\"item.to\" :to=\"item.to\" class=\"rk-crumb-link\">\n {{ item.label }}\n </RouterLink>\n <span v-else class=\"rk-crumb-current\" aria-current=\"page\">{{ item.label }}</span>\n\n <span v-if=\"index < items.length - 1\" class=\"rk-crumb-sep\" aria-hidden=\"true\">/</span>\n </li>\n </ol>\n </nav>\n</template>\n\n<style scoped>\n.rk-crumbs {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 0.5rem;\n font-size: 0.8125rem;\n}\n\n.rk-crumb {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n\n.rk-crumb-link {\n color: var(--color-ink-soft);\n transition: color var(--duration-fast);\n}\n\n.rk-crumb-link:hover {\n color: var(--color-ink);\n}\n\n.rk-crumb-link:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n border-radius: 2px;\n}\n\n.rk-crumb-current {\n color: var(--color-ink);\n font-weight: 500;\n}\n\n.rk-crumb-sep {\n color: var(--color-hair);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { RouterLink } from 'vue-router'\n\nexport interface Crumb {\n /** Already translated. */\n label: string\n /** Omitted for the current page, which is not a link to itself. */\n to?: string | undefined\n}\n\n/**\n * Where this page sits, and the way back up.\n *\n * An ordered list inside a `nav`, because the order is the meaning. The last\n * crumb is the current page and is deliberately not a link: a link to the page\n * you are on is a control that does nothing, and `aria-current=\"page\"` is what\n * says so to everyone else.\n *\n * The separators are `aria-hidden`. A screen reader announces a list and its\n * position in it; reading \"slash\" between every item is noise on top of\n * information the reader already has.\n */\nconst { items, label = '' } = defineProps<{\n items: readonly Crumb[]\n /** Accessible name for the landmark. */\n label?: string | undefined\n}>()\n</script>\n\n<template>\n <nav :aria-label=\"label || undefined\">\n <ol class=\"rk-crumbs\">\n <li v-for=\"(item, index) in items\" :key=\"item.label\" class=\"rk-crumb\">\n <RouterLink v-if=\"item.to\" :to=\"item.to\" class=\"rk-crumb-link\">\n {{ item.label }}\n </RouterLink>\n <span v-else class=\"rk-crumb-current\" aria-current=\"page\">{{ item.label }}</span>\n\n <span v-if=\"index < items.length - 1\" class=\"rk-crumb-sep\" aria-hidden=\"true\">/</span>\n </li>\n </ol>\n </nav>\n</template>\n\n<style scoped>\n.rk-crumbs {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 0.5rem;\n font-size: 0.8125rem;\n}\n\n.rk-crumb {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n\n.rk-crumb-link {\n color: var(--color-ink-soft);\n transition: color var(--duration-fast);\n}\n\n.rk-crumb-link:hover {\n color: var(--color-ink);\n}\n\n.rk-crumb-link:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n border-radius: 2px;\n}\n\n.rk-crumb-current {\n color: var(--color-ink);\n font-weight: 500;\n}\n\n.rk-crumb-sep {\n color: var(--color-hair);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { useId } from 'vue'\n\n/**\n * One section that opens and closes.\n *\n * `BaseAccordion` is a list of these with the rule that only one stays open.\n * This is the same row on its own, for the common case where the list belongs\n * to the app — because it is staggered as it scrolls in, or interleaved with\n * something else, or rendered from a source the accordion cannot know about.\n *\n * That case is not hypothetical: the first list this kit met decorated every\n * row with a reveal directive, which has to sit on the element the accordion\n * would have owned. Shipping only the list would have meant choosing between\n * the component and the design.\n *\n * ## Why not `<details>`\n *\n * A browser removes `<details>` content from layout entirely when closed, so\n * there is nothing to animate between — the panel can only appear. Faking it by\n * leaving `open` set and hiding with CSS tells a screen reader the section is\n * expanded while a sighted reader sees it shut, which is worse than no\n * animation.\n *\n * So: a button carrying `aria-expanded`, the content always in the markup —\n * which is what a crawler and a reader without JavaScript get — and a grid row\n * travelling from `0fr` to `1fr`, the one way to animate to a height nobody has\n * measured.\n */\nconst { title = '', headingLevel = 3 } = defineProps<{\n /**\n * The question. Already translated.\n *\n * Optional only because the `title` slot is the other way to give one — pass\n * exactly one of them. A disclosure with neither has a control nobody can\n * read, which is why this is documented rather than defaulted to something.\n */\n title?: string | undefined\n /**\n * Which heading the control sits inside.\n *\n * A heading, always — it is how a screen reader user moves between sections\n * without opening any of them. The level depends on what is above it on the\n * page, and getting it wrong breaks the outline.\n */\n headingLevel?: 2 | 3 | 4 | undefined\n}>()\n\nconst open = defineModel<boolean>({ default: false })\n\ndefineSlots<{\n /** The answer. */\n default: () => unknown\n /**\n * The question, when the prop is not enough.\n *\n * A slot as well as a prop because the size of a heading is a decision about\n * the page it is on, not about disclosure: the first list to use this wanted\n * the question a step larger on a wide screen, and a component that hard-codes\n * one size makes that an override against its own stylesheet.\n */\n title?: () => unknown\n}>()\n\nconst id = useId()\n</script>\n\n<template>\n <div class=\"rk-disclosure\">\n <component :is=\"`h${headingLevel}`\">\n <button\n type=\"button\"\n class=\"rk-disclosure-control\"\n :aria-expanded=\"open\"\n :aria-controls=\"id\"\n @click=\"open = !open\"\n >\n <span class=\"rk-disclosure-title\">\n <slot name=\"title\">{{ title }}</slot>\n </span>\n\n <!-- A plus that becomes a minus: one stroke turns, so the control keeps\n its place while it changes meaning. -->\n <span class=\"rk-disclosure-mark\" aria-hidden=\"true\">\n <span class=\"rk-disclosure-bar\" />\n <span class=\"rk-disclosure-bar rk-disclosure-turn\" :class=\"{ 'is-open': open }\" />\n </span>\n </button>\n </component>\n\n <div :id=\"id\" class=\"rk-disclosure-panel\" :class=\"{ 'is-open': open }\">\n <div class=\"rk-disclosure-clip\">\n <div class=\"rk-disclosure-body\" :class=\"{ 'is-open': open }\"><slot /></div>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.rk-disclosure-control {\n display: flex;\n width: 100%;\n cursor: pointer;\n align-items: center;\n justify-content: space-between;\n gap: 1.5rem;\n padding: 1.25rem 0;\n text-align: left;\n transition: color var(--duration-fast);\n}\n\n.rk-disclosure-control:hover {\n color: var(--color-primary);\n}\n\n.rk-disclosure-control:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n.rk-disclosure-title {\n color: var(--color-ink);\n font-size: 1rem;\n font-weight: 500;\n}\n\n.rk-disclosure-mark {\n position: relative;\n display: grid;\n place-items: center;\n flex-shrink: 0;\n width: 1.5rem;\n height: 1.5rem;\n}\n\n.rk-disclosure-bar {\n position: absolute;\n height: 1px;\n width: 0.875rem;\n background: var(--color-ink-soft);\n}\n\n.rk-disclosure-turn {\n transform: rotate(90deg);\n transition: transform var(--duration-slower) ease-out;\n}\n\n.rk-disclosure-turn.is-open {\n transform: rotate(0deg);\n}\n\n/* 0fr to 1fr is the whole trick: a grid row can be animated to a height that\n was never measured, which `height: auto` cannot do. */\n.rk-disclosure-panel {\n display: grid;\n grid-template-rows: 0fr;\n transition: grid-template-rows var(--duration-slower) ease-out;\n}\n\n.rk-disclosure-panel.is-open {\n grid-template-rows: 1fr;\n}\n\n.rk-disclosure-clip {\n overflow: hidden;\n}\n\n.rk-disclosure-body {\n max-width: 68ch;\n padding-bottom: 1.5rem;\n color: var(--color-ink-soft);\n line-height: 1.625;\n opacity: 0;\n transition: opacity var(--duration-slow);\n}\n\n.rk-disclosure-body.is-open {\n opacity: 1;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .rk-disclosure-panel,\n .rk-disclosure-turn,\n .rk-disclosure-body {\n transition: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { useId } from 'vue'\n\n/**\n * One section that opens and closes.\n *\n * `BaseAccordion` is a list of these with the rule that only one stays open.\n * This is the same row on its own, for the common case where the list belongs\n * to the app — because it is staggered as it scrolls in, or interleaved with\n * something else, or rendered from a source the accordion cannot know about.\n *\n * That case is not hypothetical: the first list this kit met decorated every\n * row with a reveal directive, which has to sit on the element the accordion\n * would have owned. Shipping only the list would have meant choosing between\n * the component and the design.\n *\n * ## Why not `<details>`\n *\n * A browser removes `<details>` content from layout entirely when closed, so\n * there is nothing to animate between — the panel can only appear. Faking it by\n * leaving `open` set and hiding with CSS tells a screen reader the section is\n * expanded while a sighted reader sees it shut, which is worse than no\n * animation.\n *\n * So: a button carrying `aria-expanded`, the content always in the markup —\n * which is what a crawler and a reader without JavaScript get — and a grid row\n * travelling from `0fr` to `1fr`, the one way to animate to a height nobody has\n * measured.\n */\nconst { title = '', headingLevel = 3 } = defineProps<{\n /**\n * The question. Already translated.\n *\n * Optional only because the `title` slot is the other way to give one — pass\n * exactly one of them. A disclosure with neither has a control nobody can\n * read, which is why this is documented rather than defaulted to something.\n */\n title?: string | undefined\n /**\n * Which heading the control sits inside.\n *\n * A heading, always — it is how a screen reader user moves between sections\n * without opening any of them. The level depends on what is above it on the\n * page, and getting it wrong breaks the outline.\n */\n headingLevel?: 2 | 3 | 4 | undefined\n}>()\n\nconst open = defineModel<boolean>({ default: false })\n\ndefineSlots<{\n /** The answer. */\n default: () => unknown\n /**\n * The question, when the prop is not enough.\n *\n * A slot as well as a prop because the size of a heading is a decision about\n * the page it is on, not about disclosure: the first list to use this wanted\n * the question a step larger on a wide screen, and a component that hard-codes\n * one size makes that an override against its own stylesheet.\n */\n title?: () => unknown\n}>()\n\nconst id = useId()\n</script>\n\n<template>\n <div class=\"rk-disclosure\">\n <component :is=\"`h${headingLevel}`\">\n <button\n type=\"button\"\n class=\"rk-disclosure-control\"\n :aria-expanded=\"open\"\n :aria-controls=\"id\"\n @click=\"open = !open\"\n >\n <span class=\"rk-disclosure-title\">\n <slot name=\"title\">{{ title }}</slot>\n </span>\n\n <!-- A plus that becomes a minus: one stroke turns, so the control keeps\n its place while it changes meaning. -->\n <span class=\"rk-disclosure-mark\" aria-hidden=\"true\">\n <span class=\"rk-disclosure-bar\" />\n <span class=\"rk-disclosure-bar rk-disclosure-turn\" :class=\"{ 'is-open': open }\" />\n </span>\n </button>\n </component>\n\n <div :id=\"id\" class=\"rk-disclosure-panel\" :class=\"{ 'is-open': open }\">\n <div class=\"rk-disclosure-clip\">\n <div class=\"rk-disclosure-body\" :class=\"{ 'is-open': open }\"><slot /></div>\n </div>\n </div>\n </div>\n</template>\n\n<style scoped>\n.rk-disclosure-control {\n display: flex;\n width: 100%;\n cursor: pointer;\n align-items: center;\n justify-content: space-between;\n gap: 1.5rem;\n padding: 1.25rem 0;\n text-align: left;\n transition: color var(--duration-fast);\n}\n\n.rk-disclosure-control:hover {\n color: var(--color-primary);\n}\n\n.rk-disclosure-control:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n.rk-disclosure-title {\n color: var(--color-ink);\n font-size: 1rem;\n font-weight: 500;\n}\n\n.rk-disclosure-mark {\n position: relative;\n display: grid;\n place-items: center;\n flex-shrink: 0;\n width: 1.5rem;\n height: 1.5rem;\n}\n\n.rk-disclosure-bar {\n position: absolute;\n height: 1px;\n width: 0.875rem;\n background: var(--color-ink-soft);\n}\n\n.rk-disclosure-turn {\n transform: rotate(90deg);\n transition: transform var(--duration-slower) ease-out;\n}\n\n.rk-disclosure-turn.is-open {\n transform: rotate(0deg);\n}\n\n/* 0fr to 1fr is the whole trick: a grid row can be animated to a height that\n was never measured, which `height: auto` cannot do. */\n.rk-disclosure-panel {\n display: grid;\n grid-template-rows: 0fr;\n transition: grid-template-rows var(--duration-slower) ease-out;\n}\n\n.rk-disclosure-panel.is-open {\n grid-template-rows: 1fr;\n}\n\n.rk-disclosure-clip {\n overflow: hidden;\n}\n\n.rk-disclosure-body {\n max-width: 68ch;\n padding-bottom: 1.5rem;\n color: var(--color-ink-soft);\n line-height: 1.625;\n opacity: 0;\n transition: opacity var(--duration-slow);\n}\n\n.rk-disclosure-body.is-open {\n opacity: 1;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .rk-disclosure-panel,\n .rk-disclosure-turn,\n .rk-disclosure-body {\n transition: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { nextTick, onBeforeUnmount, ref, watch } from 'vue'\n\nimport { inertOutside } from '../utils/inert'\n\n/**\n * A dialog that arrives from nowhere.\n *\n * Not `BaseSheet` with different padding. A sheet slides up from the bottom\n * edge, is dismissed by dragging it back down, and belongs to a thumb; a modal\n * appears in the middle of what you were reading and is dismissed by leaving\n * it. They are two answers to two questions and merging them would give a\n * component that is wrong on a phone and wrong on a desktop.\n *\n * ## The parts that are easy to get wrong\n *\n * This is why it exists rather than being written per dialog, which is what the\n * one wide consumer was doing in two places. None of the following is visible\n * on screen when it is missing:\n *\n * - **Focus moves in and comes back.** Opening without moving focus leaves a\n * keyboard user behind the dialog, operating a page they cannot see. Closing\n * without restoring it drops them at the top of the document.\n * - **Tab does not leave.** A dialog you can Tab out of is a dialog that is\n * modal only to somebody using a mouse.\n * - **Escape closes.** Every dialog in every application does this.\n * - **The page underneath does not scroll.** Otherwise dismissing the dialog\n * returns you somewhere else.\n * - **The backdrop closes it, the panel does not.** A click that starts inside\n * the panel and ends on the backdrop is not a click on the backdrop, which is\n * why this listens for a press on the backdrop itself rather than any click\n * that reaches it.\n */\nconst {\n title,\n closeLabel,\n tone = 'default',\n dismissible = true,\n} = defineProps<{\n /** Names the dialog for assistive tech. Required; an unnamed dialog is a box. */\n title: string\n /** Accessible name for the close button. */\n closeLabel: string\n /**\n * `alert` marks it as a decision that interrupts — `role=\"alertdialog\"`,\n * which a screen reader announces immediately rather than on arrival.\n */\n tone?: 'default' | 'alert' | undefined\n /**\n * Whether Escape and the backdrop close it.\n *\n * Off for a dialog that must be answered — but never off as a way of forcing\n * a reader to a decision they have not been given the information for.\n */\n dismissible?: boolean | undefined\n}>()\n\nconst open = defineModel<boolean>({ default: false })\n\ndefineSlots<{\n /** The body. */\n default: () => unknown\n /** Buttons, at the foot. */\n actions?: () => unknown\n}>()\n\nconst panel = ref<HTMLElement | null>(null)\n/** Who had focus before this opened, so it can be given back. */\nlet restoreTo: HTMLElement | null = null\n/** Gives the page behind back; set while open. */\nlet releaseInert: (() => void) | null = null\n\nconst FOCUSABLE =\n 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])'\n\nfunction focusable(): HTMLElement[] {\n if (!panel.value) return []\n\n return Array.from(panel.value.querySelectorAll<HTMLElement>(FOCUSABLE))\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n if (event.key === 'Escape' && dismissible) {\n event.stopPropagation()\n open.value = false\n\n return\n }\n\n if (event.key !== 'Tab') return\n\n const items = focusable()\n if (items.length === 0) return event.preventDefault()\n\n const first = items[0]!\n const last = items[items.length - 1]!\n const active = document.activeElement\n\n // The wrap has to be done by hand: the browser's own Tab order is the whole\n // document, and the dialog is only part of it.\n if (event.shiftKey && (active === first || !panel.value?.contains(active))) {\n event.preventDefault()\n last.focus()\n } else if (!event.shiftKey && active === last) {\n event.preventDefault()\n first.focus()\n }\n}\n\nwatch(open, async (isOpen) => {\n if (isOpen) {\n restoreTo = document.activeElement as HTMLElement | null\n document.body.style.overflow = 'hidden'\n await nextTick()\n // The Tab wrap above keeps focus cycling; `inert` is what also stops a\n // screen reader's virtual cursor, which walks past keydown handlers.\n if (panel.value) releaseInert = inertOutside(panel.value)\n // The panel itself when it holds nothing focusable, so focus is at least\n // inside the dialog rather than behind it.\n ;(focusable()[0] ?? panel.value)?.focus()\n } else {\n document.body.style.overflow = ''\n // Before focus goes back: an inert element cannot take it.\n releaseInert?.()\n releaseInert = null\n restoreTo?.focus()\n restoreTo = null\n }\n})\n\n// A dialog unmounted while open would otherwise leave the page unscrollable,\n// with nothing on screen to explain why.\nonBeforeUnmount(() => {\n if (open.value) document.body.style.overflow = ''\n releaseInert?.()\n})\n</script>\n\n<template>\n <Teleport to=\"body\">\n <Transition name=\"rk-modal\">\n <div\n v-if=\"open\"\n class=\"rk-modal-scrim\"\n @keydown=\"onKeydown\"\n @mousedown.self=\"dismissible && (open = false)\"\n >\n <div\n ref=\"panel\"\n class=\"rk-modal-panel surface-overlay\"\n :role=\"tone === 'alert' ? 'alertdialog' : 'dialog'\"\n aria-modal=\"true\"\n :aria-label=\"title\"\n tabindex=\"-1\"\n >\n <div class=\"rk-modal-head\">\n <h2 class=\"rk-modal-title\">{{ title }}</h2>\n\n <button\n v-if=\"dismissible\"\n type=\"button\"\n class=\"rk-modal-close\"\n :aria-label=\"closeLabel\"\n @click=\"open = false\"\n >\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M18 6 6 18M6 6l12 12\" stroke-linecap=\"round\" />\n </svg>\n </button>\n </div>\n\n <div class=\"rk-modal-body\"><slot /></div>\n\n <div v-if=\"$slots.actions\" class=\"rk-modal-actions\"><slot name=\"actions\" /></div>\n </div>\n </div>\n </Transition>\n </Teleport>\n</template>\n\n<style scoped>\n.rk-modal-scrim {\n position: fixed;\n inset: 0;\n z-index: 50;\n display: grid;\n place-items: center;\n padding: 1.5rem;\n background: color-mix(in srgb, var(--color-ink) 70%, transparent);\n backdrop-filter: blur(4px);\n}\n\n.rk-modal-panel {\n width: 100%;\n max-width: 28rem;\n max-height: 85vh;\n overflow-y: auto;\n border-radius: var(--radius-card);\n padding: 1.5rem;\n}\n\n.rk-modal-panel:focus {\n outline: none;\n}\n\n.rk-modal-head {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 1rem;\n}\n\n.rk-modal-title {\n font-size: 1.0625rem;\n font-weight: 600;\n color: var(--color-ink);\n}\n\n.rk-modal-close {\n display: grid;\n place-items: center;\n flex-shrink: 0;\n width: 2rem;\n height: 2rem;\n border-radius: 9999px;\n color: var(--color-ink-soft);\n transition: background-color var(--duration-fast);\n}\n\n.rk-modal-close:hover {\n background: var(--color-muted);\n}\n\n.rk-modal-close:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n.rk-modal-close svg {\n width: 1rem;\n height: 1rem;\n}\n\n.rk-modal-body {\n margin-top: 0.75rem;\n color: var(--color-ink-soft);\n font-size: 0.875rem;\n line-height: 1.625;\n}\n\n.rk-modal-actions {\n margin-top: 1.5rem;\n display: flex;\n justify-content: flex-end;\n gap: 0.5rem;\n}\n\n.rk-modal-enter-active,\n.rk-modal-leave-active {\n transition: opacity var(--duration-base) ease;\n}\n\n.rk-modal-enter-active .rk-modal-panel,\n.rk-modal-leave-active .rk-modal-panel {\n transition:\n transform var(--duration-base) var(--ease-sheet),\n opacity var(--duration-base) ease;\n}\n\n.rk-modal-enter-from,\n.rk-modal-leave-to {\n opacity: 0;\n}\n\n/* From nowhere, not from an edge: a scale is what separates this from a sheet. */\n.rk-modal-enter-from .rk-modal-panel,\n.rk-modal-leave-to .rk-modal-panel {\n transform: scale(0.96);\n opacity: 0;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .rk-modal-enter-active .rk-modal-panel,\n .rk-modal-leave-active .rk-modal-panel {\n transition: opacity var(--duration-base) ease;\n transform: none;\n }\n\n .rk-modal-enter-from .rk-modal-panel,\n .rk-modal-leave-to .rk-modal-panel {\n transform: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { nextTick, onBeforeUnmount, ref, watch } from 'vue'\n\nimport { inertOutside } from '../utils/inert'\n\n/**\n * A dialog that arrives from nowhere.\n *\n * Not `BaseSheet` with different padding. A sheet slides up from the bottom\n * edge, is dismissed by dragging it back down, and belongs to a thumb; a modal\n * appears in the middle of what you were reading and is dismissed by leaving\n * it. They are two answers to two questions and merging them would give a\n * component that is wrong on a phone and wrong on a desktop.\n *\n * ## The parts that are easy to get wrong\n *\n * This is why it exists rather than being written per dialog, which is what the\n * one wide consumer was doing in two places. None of the following is visible\n * on screen when it is missing:\n *\n * - **Focus moves in and comes back.** Opening without moving focus leaves a\n * keyboard user behind the dialog, operating a page they cannot see. Closing\n * without restoring it drops them at the top of the document.\n * - **Tab does not leave.** A dialog you can Tab out of is a dialog that is\n * modal only to somebody using a mouse.\n * - **Escape closes.** Every dialog in every application does this.\n * - **The page underneath does not scroll.** Otherwise dismissing the dialog\n * returns you somewhere else.\n * - **The backdrop closes it, the panel does not.** A click that starts inside\n * the panel and ends on the backdrop is not a click on the backdrop, which is\n * why this listens for a press on the backdrop itself rather than any click\n * that reaches it.\n */\nconst {\n title,\n closeLabel,\n tone = 'default',\n dismissible = true,\n} = defineProps<{\n /** Names the dialog for assistive tech. Required; an unnamed dialog is a box. */\n title: string\n /** Accessible name for the close button. */\n closeLabel: string\n /**\n * `alert` marks it as a decision that interrupts — `role=\"alertdialog\"`,\n * which a screen reader announces immediately rather than on arrival.\n */\n tone?: 'default' | 'alert' | undefined\n /**\n * Whether Escape and the backdrop close it.\n *\n * Off for a dialog that must be answered — but never off as a way of forcing\n * a reader to a decision they have not been given the information for.\n */\n dismissible?: boolean | undefined\n}>()\n\nconst open = defineModel<boolean>({ default: false })\n\ndefineSlots<{\n /** The body. */\n default: () => unknown\n /** Buttons, at the foot. */\n actions?: () => unknown\n}>()\n\nconst panel = ref<HTMLElement | null>(null)\n/** Who had focus before this opened, so it can be given back. */\nlet restoreTo: HTMLElement | null = null\n/** Gives the page behind back; set while open. */\nlet releaseInert: (() => void) | null = null\n\nconst FOCUSABLE =\n 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex=\"-1\"])'\n\nfunction focusable(): HTMLElement[] {\n if (!panel.value) return []\n\n return Array.from(panel.value.querySelectorAll<HTMLElement>(FOCUSABLE))\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n if (event.key === 'Escape' && dismissible) {\n event.stopPropagation()\n open.value = false\n\n return\n }\n\n if (event.key !== 'Tab') return\n\n const items = focusable()\n if (items.length === 0) return event.preventDefault()\n\n const first = items[0]!\n const last = items[items.length - 1]!\n const active = document.activeElement\n\n // The wrap has to be done by hand: the browser's own Tab order is the whole\n // document, and the dialog is only part of it.\n if (event.shiftKey && (active === first || !panel.value?.contains(active))) {\n event.preventDefault()\n last.focus()\n } else if (!event.shiftKey && active === last) {\n event.preventDefault()\n first.focus()\n }\n}\n\nwatch(open, async (isOpen) => {\n if (isOpen) {\n restoreTo = document.activeElement as HTMLElement | null\n document.body.style.overflow = 'hidden'\n await nextTick()\n // The Tab wrap above keeps focus cycling; `inert` is what also stops a\n // screen reader's virtual cursor, which walks past keydown handlers.\n if (panel.value) releaseInert = inertOutside(panel.value)\n // The panel itself when it holds nothing focusable, so focus is at least\n // inside the dialog rather than behind it.\n ;(focusable()[0] ?? panel.value)?.focus()\n } else {\n document.body.style.overflow = ''\n // Before focus goes back: an inert element cannot take it.\n releaseInert?.()\n releaseInert = null\n restoreTo?.focus()\n restoreTo = null\n }\n})\n\n// A dialog unmounted while open would otherwise leave the page unscrollable,\n// with nothing on screen to explain why.\nonBeforeUnmount(() => {\n if (open.value) document.body.style.overflow = ''\n releaseInert?.()\n})\n</script>\n\n<template>\n <Teleport to=\"body\">\n <Transition name=\"rk-modal\">\n <div\n v-if=\"open\"\n class=\"rk-modal-scrim\"\n @keydown=\"onKeydown\"\n @mousedown.self=\"dismissible && (open = false)\"\n >\n <div\n ref=\"panel\"\n class=\"rk-modal-panel surface-overlay\"\n :role=\"tone === 'alert' ? 'alertdialog' : 'dialog'\"\n aria-modal=\"true\"\n :aria-label=\"title\"\n tabindex=\"-1\"\n >\n <div class=\"rk-modal-head\">\n <h2 class=\"rk-modal-title\">{{ title }}</h2>\n\n <button\n v-if=\"dismissible\"\n type=\"button\"\n class=\"rk-modal-close\"\n :aria-label=\"closeLabel\"\n @click=\"open = false\"\n >\n <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n <path d=\"M18 6 6 18M6 6l12 12\" stroke-linecap=\"round\" />\n </svg>\n </button>\n </div>\n\n <div class=\"rk-modal-body\"><slot /></div>\n\n <div v-if=\"$slots.actions\" class=\"rk-modal-actions\"><slot name=\"actions\" /></div>\n </div>\n </div>\n </Transition>\n </Teleport>\n</template>\n\n<style scoped>\n.rk-modal-scrim {\n position: fixed;\n inset: 0;\n z-index: 50;\n display: grid;\n place-items: center;\n padding: 1.5rem;\n background: color-mix(in srgb, var(--color-ink) 70%, transparent);\n backdrop-filter: blur(4px);\n}\n\n.rk-modal-panel {\n width: 100%;\n max-width: 28rem;\n max-height: 85vh;\n overflow-y: auto;\n border-radius: var(--radius-card);\n padding: 1.5rem;\n}\n\n.rk-modal-panel:focus {\n outline: none;\n}\n\n.rk-modal-head {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 1rem;\n}\n\n.rk-modal-title {\n font-size: 1.0625rem;\n font-weight: 600;\n color: var(--color-ink);\n}\n\n.rk-modal-close {\n display: grid;\n place-items: center;\n flex-shrink: 0;\n width: 2rem;\n height: 2rem;\n border-radius: 9999px;\n color: var(--color-ink-soft);\n transition: background-color var(--duration-fast);\n}\n\n.rk-modal-close:hover {\n background: var(--color-muted);\n}\n\n.rk-modal-close:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n.rk-modal-close svg {\n width: 1rem;\n height: 1rem;\n}\n\n.rk-modal-body {\n margin-top: 0.75rem;\n color: var(--color-ink-soft);\n font-size: 0.875rem;\n line-height: 1.625;\n}\n\n.rk-modal-actions {\n margin-top: 1.5rem;\n display: flex;\n justify-content: flex-end;\n gap: 0.5rem;\n}\n\n.rk-modal-enter-active,\n.rk-modal-leave-active {\n transition: opacity var(--duration-base) ease;\n}\n\n.rk-modal-enter-active .rk-modal-panel,\n.rk-modal-leave-active .rk-modal-panel {\n transition:\n transform var(--duration-base) var(--ease-sheet),\n opacity var(--duration-base) ease;\n}\n\n.rk-modal-enter-from,\n.rk-modal-leave-to {\n opacity: 0;\n}\n\n/* From nowhere, not from an edge: a scale is what separates this from a sheet. */\n.rk-modal-enter-from .rk-modal-panel,\n.rk-modal-leave-to .rk-modal-panel {\n transform: scale(0.96);\n opacity: 0;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .rk-modal-enter-active .rk-modal-panel,\n .rk-modal-leave-active .rk-modal-panel {\n transition: opacity var(--duration-base) ease;\n transform: none;\n }\n\n .rk-modal-enter-from .rk-modal-panel,\n .rk-modal-leave-to .rk-modal-panel {\n transform: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\n/**\n * Moving between pages of a list.\n *\n * A `nav` of buttons rather than links, because this component does not know\n * whether the app pages by route, by query or in memory. It emits a number and\n * the app decides what that means.\n *\n * ## The ellipsis\n *\n * Every page number is unusable past about a dozen, and cutting to\n * \"1 … 7 8 9 … 40\" is what every list does. The window is a fixed size around\n * the current page and the first and last are always present, so the control\n * never changes width as you move through it — a row of numbers that reflows\n * under the pointer is a row you have to re-aim at.\n *\n * The gap is a `<span>`, not a disabled button: it is not a control, and\n * announcing it as one gives a screen reader something to try to press.\n */\nconst {\n page,\n pages,\n siblings = 1,\n label = '',\n previousLabel,\n nextLabel,\n} = defineProps<{\n /** Current page, 1-based. */\n page: number\n /** How many there are. */\n pages: number\n /** How many numbers to show either side of the current one. */\n siblings?: number | undefined\n /** Accessible name for the landmark. */\n label?: string | undefined\n /**\n * Names for the arrows, already translated.\n *\n * Required, because `‹` is a glyph and not a word: a screen reader reading\n * the arrow announces nothing a person can act on.\n */\n previousLabel: string\n nextLabel: string\n}>()\n\nconst emit = defineEmits<{ change: [page: number] }>()\n\nconst GAP = 'gap' as const\n\nconst slots = computed<(number | typeof GAP)[]>(() => {\n if (pages <= 1) return [1]\n\n const first = 1\n const last = pages\n\n /* The widest the windowed form ever gets: first, last, the current page, its\n siblings either side, and the two gaps. Below that the window saves\n nothing, and \"1 2 … 5\" is a worse control than \"1 2 3 4 5\" — so short lists\n are shown whole rather than cut on principle. */\n if (pages <= siblings * 2 + 5) {\n return Array.from({ length: pages }, (_, i) => i + 1)\n }\n const from = Math.max(first + 1, page - siblings)\n const to = Math.min(last - 1, page + siblings)\n\n const out: (number | typeof GAP)[] = [first]\n // A gap standing in for a single page is longer than the page it replaces.\n if (from > first + 1) out.push(GAP)\n for (let n = from; n <= to; n++) out.push(n)\n if (to < last - 1) out.push(GAP)\n if (last > first) out.push(last)\n\n return out\n})\n\nconst go = (next: number) => {\n if (next >= 1 && next <= pages && next !== page) emit('change', next)\n}\n</script>\n\n<template>\n <nav class=\"rk-pager\" :aria-label=\"label || undefined\">\n <button\n type=\"button\"\n class=\"rk-pager-step\"\n :disabled=\"page <= 1\"\n :aria-label=\"previousLabel\"\n @click=\"go(page - 1)\"\n >\n ‹\n </button>\n\n <template v-for=\"(slot, index) in slots\" :key=\"`${slot}-${index}`\">\n <span v-if=\"slot === 'gap'\" class=\"rk-pager-gap\" aria-hidden=\"true\">…</span>\n <button\n v-else\n type=\"button\"\n class=\"rk-pager-page\"\n :class=\"{ 'is-active': slot === page }\"\n :aria-current=\"slot === page ? 'page' : undefined\"\n @click=\"go(slot)\"\n >\n {{ slot }}\n </button>\n </template>\n\n <button\n type=\"button\"\n class=\"rk-pager-step\"\n :disabled=\"page >= pages\"\n :aria-label=\"nextLabel\"\n @click=\"go(page + 1)\"\n >\n ›\n </button>\n </nav>\n</template>\n\n<style scoped>\n.rk-pager {\n display: flex;\n align-items: center;\n gap: 0.25rem;\n}\n\n.rk-pager-step,\n.rk-pager-page {\n display: grid;\n place-items: center;\n min-width: 2rem;\n height: 2rem;\n padding: 0 0.5rem;\n border-radius: var(--radius-cell);\n font-size: 0.875rem;\n color: var(--color-ink-soft);\n transition:\n background-color var(--duration-fast),\n color var(--duration-fast);\n}\n\n.rk-pager-step:hover:not(:disabled),\n.rk-pager-page:hover {\n background: var(--color-muted);\n color: var(--color-ink);\n}\n\n.rk-pager-step:focus-visible,\n.rk-pager-page:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n.rk-pager-step:disabled {\n opacity: 0.4;\n pointer-events: none;\n}\n\n.rk-pager-page.is-active {\n background: var(--color-primary);\n color: var(--color-on-primary);\n font-weight: 500;\n}\n\n.rk-pager-gap {\n display: grid;\n place-items: center;\n min-width: 2rem;\n height: 2rem;\n color: var(--color-ink-soft);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\n/**\n * Moving between pages of a list.\n *\n * A `nav` of buttons rather than links, because this component does not know\n * whether the app pages by route, by query or in memory. It emits a number and\n * the app decides what that means.\n *\n * ## The ellipsis\n *\n * Every page number is unusable past about a dozen, and cutting to\n * \"1 … 7 8 9 … 40\" is what every list does. The window is a fixed size around\n * the current page and the first and last are always present, so the control\n * never changes width as you move through it — a row of numbers that reflows\n * under the pointer is a row you have to re-aim at.\n *\n * The gap is a `<span>`, not a disabled button: it is not a control, and\n * announcing it as one gives a screen reader something to try to press.\n */\nconst {\n page,\n pages,\n siblings = 1,\n label = '',\n previousLabel,\n nextLabel,\n} = defineProps<{\n /** Current page, 1-based. */\n page: number\n /** How many there are. */\n pages: number\n /** How many numbers to show either side of the current one. */\n siblings?: number | undefined\n /** Accessible name for the landmark. */\n label?: string | undefined\n /**\n * Names for the arrows, already translated.\n *\n * Required, because `‹` is a glyph and not a word: a screen reader reading\n * the arrow announces nothing a person can act on.\n */\n previousLabel: string\n nextLabel: string\n}>()\n\nconst emit = defineEmits<{ change: [page: number] }>()\n\nconst GAP = 'gap' as const\n\nconst slots = computed<(number | typeof GAP)[]>(() => {\n if (pages <= 1) return [1]\n\n const first = 1\n const last = pages\n\n /* The widest the windowed form ever gets: first, last, the current page, its\n siblings either side, and the two gaps. Below that the window saves\n nothing, and \"1 2 … 5\" is a worse control than \"1 2 3 4 5\" — so short lists\n are shown whole rather than cut on principle. */\n if (pages <= siblings * 2 + 5) {\n return Array.from({ length: pages }, (_, i) => i + 1)\n }\n const from = Math.max(first + 1, page - siblings)\n const to = Math.min(last - 1, page + siblings)\n\n const out: (number | typeof GAP)[] = [first]\n // A gap standing in for a single page is longer than the page it replaces.\n if (from > first + 1) out.push(GAP)\n for (let n = from; n <= to; n++) out.push(n)\n if (to < last - 1) out.push(GAP)\n if (last > first) out.push(last)\n\n return out\n})\n\nconst go = (next: number) => {\n if (next >= 1 && next <= pages && next !== page) emit('change', next)\n}\n</script>\n\n<template>\n <nav class=\"rk-pager\" :aria-label=\"label || undefined\">\n <button\n type=\"button\"\n class=\"rk-pager-step\"\n :disabled=\"page <= 1\"\n :aria-label=\"previousLabel\"\n @click=\"go(page - 1)\"\n >\n ‹\n </button>\n\n <template v-for=\"(slot, index) in slots\" :key=\"`${slot}-${index}`\">\n <span v-if=\"slot === 'gap'\" class=\"rk-pager-gap\" aria-hidden=\"true\">…</span>\n <button\n v-else\n type=\"button\"\n class=\"rk-pager-page\"\n :class=\"{ 'is-active': slot === page }\"\n :aria-current=\"slot === page ? 'page' : undefined\"\n @click=\"go(slot)\"\n >\n {{ slot }}\n </button>\n </template>\n\n <button\n type=\"button\"\n class=\"rk-pager-step\"\n :disabled=\"page >= pages\"\n :aria-label=\"nextLabel\"\n @click=\"go(page + 1)\"\n >\n ›\n </button>\n </nav>\n</template>\n\n<style scoped>\n.rk-pager {\n display: flex;\n align-items: center;\n gap: 0.25rem;\n}\n\n.rk-pager-step,\n.rk-pager-page {\n display: grid;\n place-items: center;\n min-width: 2rem;\n height: 2rem;\n padding: 0 0.5rem;\n border-radius: var(--radius-cell);\n font-size: 0.875rem;\n color: var(--color-ink-soft);\n transition:\n background-color var(--duration-fast),\n color var(--duration-fast);\n}\n\n.rk-pager-step:hover:not(:disabled),\n.rk-pager-page:hover {\n background: var(--color-muted);\n color: var(--color-ink);\n}\n\n.rk-pager-step:focus-visible,\n.rk-pager-page:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n.rk-pager-step:disabled {\n opacity: 0.4;\n pointer-events: none;\n}\n\n.rk-pager-page.is-active {\n background: var(--color-primary);\n color: var(--color-on-primary);\n font-weight: 500;\n}\n\n.rk-pager-gap {\n display: grid;\n place-items: center;\n min-width: 2rem;\n height: 2rem;\n color: var(--color-ink-soft);\n}\n</style>\n","<script setup lang=\"ts\" generic=\"Id extends string\">\nimport { Search } from 'lucide-vue-next'\nimport { computed, nextTick, onBeforeUnmount, ref, useId, useTemplateRef, watch } from 'vue'\nimport type { Component } from 'vue'\n\nimport { inertOutside } from '../utils/inert'\nimport { ensureSheetRoot } from '../utils/sheet-root'\n\nexport interface CommandItem<Id extends string> {\n id: Id\n /** Already translated. */\n label: string\n /** A line under it, or a shortcut to show on the right. */\n hint?: string | undefined\n icon?: Component | undefined\n /** Extra words that should find it — synonyms, the English name. */\n keywords?: readonly string[] | undefined\n}\n\nexport interface CommandGroup<Id extends string> {\n /** Already translated. Omitted, the items sit without a heading. */\n label?: string | undefined\n items: readonly CommandItem<Id>[]\n}\n\n/**\n * Everything the app can do, behind one shortcut.\n *\n * The pattern people know from their editor: a dialog with a field at the\n * top, results under it, arrows to move and Enter to run. It is the fastest\n * way to reach the twelfth thing in a menu, and the only way that does not\n * cost a menu.\n *\n * The field is a combobox and the results are its listbox, which is what\n * makes it work with a screen reader: focus stays in the field the whole\n * time and `aria-activedescendant` says which result is current, so typing\n * and moving do not fight each other.\n *\n * `hotkey` is the letter pressed with ⌘ or Ctrl — 'k' for the usual one.\n * Matching is over the label and the `keywords` beside it, so \"new\" can find\n * \"Write an entry\" in an app whose language is not English.\n */\nconst {\n groups,\n label,\n placeholder,\n emptyLabel,\n hotkey = undefined,\n} = defineProps<{\n groups: readonly CommandGroup<Id>[]\n /** The dialog's accessible name, e.g. \"Commands\". */\n label: string\n /** The field's placeholder, e.g. \"Type a command or search\". */\n placeholder: string\n /** Shown when nothing matches, e.g. \"Nothing found\". */\n emptyLabel: string\n /** The letter that opens it with ⌘ or Ctrl. Unset, the app opens it itself. */\n hotkey?: string | undefined\n}>()\n\nconst emit = defineEmits<{ select: [id: Id] }>()\n\n/** Open or closed, with `v-model`. */\nconst open = defineModel<boolean>({ default: false })\n\nconst query = ref('')\nconst active = ref(0)\nconst id = useId()\nconst field = useTemplateRef<HTMLInputElement>('field')\n\nconst sheetRoot = ensureSheetRoot()\nlet release: (() => void) | null = null\n\nconst matches = computed(() => {\n const needle = query.value.trim().toLowerCase()\n const keep = (item: CommandItem<Id>) =>\n needle === '' ||\n [item.label, ...(item.keywords ?? [])].some((word) => word.toLowerCase().includes(needle))\n\n return groups\n .map((group) => ({ ...group, items: group.items.filter(keep) }))\n .filter((group) => group.items.length > 0)\n})\n\n/** Flattened, because the arrows walk the results and not the groups. */\nconst flat = computed(() => matches.value.flatMap((group) => group.items))\n\nfunction choose(item: CommandItem<Id>) {\n emit('select', item.id)\n open.value = false\n}\n\nfunction move(step: number) {\n if (flat.value.length === 0) return\n active.value = (active.value + step + flat.value.length) % flat.value.length\n\n // `?.()`: scrollIntoView is not everywhere — jsdom has no layout, and a\n // missing scroll must not take the keyboard down with it.\n void nextTick(() => {\n document.getElementById(`${id}-${active.value}`)?.scrollIntoView?.({ block: 'nearest' })\n })\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n if (event.key === 'ArrowDown') {\n event.preventDefault()\n move(1)\n } else if (event.key === 'ArrowUp') {\n event.preventDefault()\n move(-1)\n } else if (event.key === 'Home') {\n event.preventDefault()\n active.value = 0\n } else if (event.key === 'End') {\n event.preventDefault()\n active.value = Math.max(0, flat.value.length - 1)\n } else if (event.key === 'Enter') {\n const item = flat.value[active.value]\n if (item) {\n event.preventDefault()\n choose(item)\n }\n } else if (event.key === 'Escape') {\n event.preventDefault()\n open.value = false\n }\n}\n\n/* The shortcut is global, because a command menu you have to find a button\n for is a menu, not a shortcut. */\nfunction onWindowKeydown(event: KeyboardEvent) {\n if (!hotkey || event.key.toLowerCase() !== hotkey.toLowerCase()) return\n if (!event.metaKey && !event.ctrlKey) return\n\n event.preventDefault()\n open.value = !open.value\n}\n\nif (typeof window !== 'undefined') window.addEventListener('keydown', onWindowKeydown)\n\n/* `immediate`, because a palette can be mounted already open — driven from a\n route, or in a test. Without it that one never takes focus and never holds\n the page: it looks open and behaves like a div. */\nwatch(\n open,\n async (isOpen) => {\n if (typeof document === 'undefined') return\n\n if (!isOpen) {\n release?.()\n release = null\n return\n }\n\n query.value = ''\n active.value = 0\n await nextTick()\n field.value?.focus()\n const panel = field.value?.closest('.rk-command-panel')\n if (panel) release = inertOutside(panel)\n },\n { immediate: true },\n)\n\nwatch(query, () => (active.value = 0))\n\nonBeforeUnmount(() => {\n release?.()\n if (typeof window !== 'undefined') window.removeEventListener('keydown', onWindowKeydown)\n})\n\nconst indexOf = (item: CommandItem<Id>) => flat.value.indexOf(item)\n</script>\n\n<template>\n <Teleport :to=\"sheetRoot\">\n <Transition name=\"rk-command\">\n <div v-if=\"open\" class=\"rk-command\" @keydown=\"onKeydown\">\n <div class=\"rk-command-scrim\" @click=\"open = false\" />\n\n <div\n class=\"rk-command-panel surface-overlay\"\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-label=\"label\"\n >\n <div class=\"rk-command-field\">\n <Search class=\"text-ink-soft size-4 shrink-0\" aria-hidden=\"true\" />\n <input\n ref=\"field\"\n v-model=\"query\"\n type=\"text\"\n role=\"combobox\"\n class=\"rk-command-input\"\n autocomplete=\"off\"\n :placeholder=\"placeholder\"\n :aria-label=\"label\"\n :aria-expanded=\"true\"\n :aria-controls=\"id\"\n :aria-activedescendant=\"flat.length ? `${id}-${active}` : undefined\"\n />\n </div>\n\n <div :id=\"id\" class=\"rk-command-list\" role=\"listbox\" :aria-label=\"label\">\n <template v-for=\"group in matches\" :key=\"group.label ?? 'items'\">\n <p v-if=\"group.label\" class=\"rk-command-group\">{{ group.label }}</p>\n\n <div\n v-for=\"item in group.items\"\n :id=\"`${id}-${indexOf(item)}`\"\n :key=\"item.id\"\n class=\"rk-command-item\"\n :class=\"{ 'is-active': indexOf(item) === active }\"\n role=\"option\"\n :aria-selected=\"indexOf(item) === active\"\n @click=\"choose(item)\"\n @pointermove=\"active = indexOf(item)\"\n >\n <component\n :is=\"item.icon\"\n v-if=\"item.icon\"\n class=\"size-4 shrink-0\"\n aria-hidden=\"true\"\n />\n <span class=\"truncate\">{{ item.label }}</span>\n <span v-if=\"item.hint\" class=\"rk-command-hint\">{{ item.hint }}</span>\n </div>\n </template>\n\n <p v-if=\"flat.length === 0\" class=\"rk-command-empty\">{{ emptyLabel }}</p>\n </div>\n </div>\n </div>\n </Transition>\n </Teleport>\n</template>\n\n<style scoped>\n.rk-command {\n position: fixed;\n inset: 0;\n z-index: 90;\n display: flex;\n justify-content: center;\n padding: 1rem;\n /* Near the top, not centred: the eye is already up there, and the list\n grows downwards into space rather than pushing itself off both edges. */\n padding-top: min(12vh, 6rem);\n}\n\n.rk-command-scrim {\n position: absolute;\n inset: 0;\n background: color-mix(in oklab, var(--color-ink) 45%, transparent);\n backdrop-filter: blur(2px);\n}\n\n.rk-command-panel {\n position: relative;\n display: flex;\n width: 100%;\n max-width: 34rem;\n max-height: min(28rem, 70vh);\n flex-direction: column;\n overflow: hidden;\n border-radius: var(--radius-card);\n}\n\n.rk-command-field {\n display: flex;\n align-items: center;\n gap: 0.625rem;\n border-bottom: 1px solid var(--surface-border-color);\n padding: 0.875rem 1rem;\n}\n\n.rk-command-input {\n min-width: 0;\n flex: 1;\n background: transparent;\n font-size: 1rem;\n color: var(--color-ink);\n outline: none;\n}\n\n/* The field takes focus the moment this opens, so a full ring around it\n would be on the whole time. The rule under it turns instead: enough to\n say where the keyboard is, quiet enough to live with. */\n.rk-command-field:has(.rk-command-input:focus-visible) {\n border-bottom-color: var(--color-primary);\n box-shadow: inset 0 -1px 0 var(--color-primary);\n}\n\n.rk-command-list {\n overflow-y: auto;\n padding: 0.375rem;\n}\n\n.rk-command-group {\n padding: 0.5rem 0.625rem 0.25rem;\n font-size: 0.6875rem;\n font-weight: 600;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--color-ink-soft);\n}\n\n.rk-command-item {\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.rk-command-item.is-active {\n background: var(--color-muted);\n}\n\n.rk-command-hint {\n margin-left: auto;\n flex-shrink: 0;\n font-size: 0.75rem;\n color: var(--color-ink-soft);\n}\n\n.rk-command-empty {\n padding: 1.5rem 0.625rem;\n text-align: center;\n font-size: 0.875rem;\n color: var(--color-ink-soft);\n}\n\n.rk-command-enter-active,\n.rk-command-leave-active {\n transition: opacity var(--duration-fast) var(--ease-standard);\n}\n\n.rk-command-enter-active .rk-command-panel,\n.rk-command-leave-active .rk-command-panel {\n transition: transform var(--duration-fast) var(--ease-standard);\n}\n\n.rk-command-enter-from,\n.rk-command-leave-to {\n opacity: 0;\n}\n\n.rk-command-enter-from .rk-command-panel,\n.rk-command-leave-to .rk-command-panel {\n transform: translateY(-0.5rem) scale(0.98);\n}\n</style>\n","<script setup lang=\"ts\" generic=\"Id extends string\">\nimport { Search } from 'lucide-vue-next'\nimport { computed, nextTick, onBeforeUnmount, ref, useId, useTemplateRef, watch } from 'vue'\nimport type { Component } from 'vue'\n\nimport { inertOutside } from '../utils/inert'\nimport { ensureSheetRoot } from '../utils/sheet-root'\n\nexport interface CommandItem<Id extends string> {\n id: Id\n /** Already translated. */\n label: string\n /** A line under it, or a shortcut to show on the right. */\n hint?: string | undefined\n icon?: Component | undefined\n /** Extra words that should find it — synonyms, the English name. */\n keywords?: readonly string[] | undefined\n}\n\nexport interface CommandGroup<Id extends string> {\n /** Already translated. Omitted, the items sit without a heading. */\n label?: string | undefined\n items: readonly CommandItem<Id>[]\n}\n\n/**\n * Everything the app can do, behind one shortcut.\n *\n * The pattern people know from their editor: a dialog with a field at the\n * top, results under it, arrows to move and Enter to run. It is the fastest\n * way to reach the twelfth thing in a menu, and the only way that does not\n * cost a menu.\n *\n * The field is a combobox and the results are its listbox, which is what\n * makes it work with a screen reader: focus stays in the field the whole\n * time and `aria-activedescendant` says which result is current, so typing\n * and moving do not fight each other.\n *\n * `hotkey` is the letter pressed with ⌘ or Ctrl — 'k' for the usual one.\n * Matching is over the label and the `keywords` beside it, so \"new\" can find\n * \"Write an entry\" in an app whose language is not English.\n */\nconst {\n groups,\n label,\n placeholder,\n emptyLabel,\n hotkey = undefined,\n} = defineProps<{\n groups: readonly CommandGroup<Id>[]\n /** The dialog's accessible name, e.g. \"Commands\". */\n label: string\n /** The field's placeholder, e.g. \"Type a command or search\". */\n placeholder: string\n /** Shown when nothing matches, e.g. \"Nothing found\". */\n emptyLabel: string\n /** The letter that opens it with ⌘ or Ctrl. Unset, the app opens it itself. */\n hotkey?: string | undefined\n}>()\n\nconst emit = defineEmits<{ select: [id: Id] }>()\n\n/** Open or closed, with `v-model`. */\nconst open = defineModel<boolean>({ default: false })\n\nconst query = ref('')\nconst active = ref(0)\nconst id = useId()\nconst field = useTemplateRef<HTMLInputElement>('field')\n\nconst sheetRoot = ensureSheetRoot()\nlet release: (() => void) | null = null\n\nconst matches = computed(() => {\n const needle = query.value.trim().toLowerCase()\n const keep = (item: CommandItem<Id>) =>\n needle === '' ||\n [item.label, ...(item.keywords ?? [])].some((word) => word.toLowerCase().includes(needle))\n\n return groups\n .map((group) => ({ ...group, items: group.items.filter(keep) }))\n .filter((group) => group.items.length > 0)\n})\n\n/** Flattened, because the arrows walk the results and not the groups. */\nconst flat = computed(() => matches.value.flatMap((group) => group.items))\n\nfunction choose(item: CommandItem<Id>) {\n emit('select', item.id)\n open.value = false\n}\n\nfunction move(step: number) {\n if (flat.value.length === 0) return\n active.value = (active.value + step + flat.value.length) % flat.value.length\n\n // `?.()`: scrollIntoView is not everywhere — jsdom has no layout, and a\n // missing scroll must not take the keyboard down with it.\n void nextTick(() => {\n document.getElementById(`${id}-${active.value}`)?.scrollIntoView?.({ block: 'nearest' })\n })\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n if (event.key === 'ArrowDown') {\n event.preventDefault()\n move(1)\n } else if (event.key === 'ArrowUp') {\n event.preventDefault()\n move(-1)\n } else if (event.key === 'Home') {\n event.preventDefault()\n active.value = 0\n } else if (event.key === 'End') {\n event.preventDefault()\n active.value = Math.max(0, flat.value.length - 1)\n } else if (event.key === 'Enter') {\n const item = flat.value[active.value]\n if (item) {\n event.preventDefault()\n choose(item)\n }\n } else if (event.key === 'Escape') {\n event.preventDefault()\n open.value = false\n }\n}\n\n/* The shortcut is global, because a command menu you have to find a button\n for is a menu, not a shortcut. */\nfunction onWindowKeydown(event: KeyboardEvent) {\n if (!hotkey || event.key.toLowerCase() !== hotkey.toLowerCase()) return\n if (!event.metaKey && !event.ctrlKey) return\n\n event.preventDefault()\n open.value = !open.value\n}\n\nif (typeof window !== 'undefined') window.addEventListener('keydown', onWindowKeydown)\n\n/* `immediate`, because a palette can be mounted already open — driven from a\n route, or in a test. Without it that one never takes focus and never holds\n the page: it looks open and behaves like a div. */\nwatch(\n open,\n async (isOpen) => {\n if (typeof document === 'undefined') return\n\n if (!isOpen) {\n release?.()\n release = null\n return\n }\n\n query.value = ''\n active.value = 0\n await nextTick()\n field.value?.focus()\n const panel = field.value?.closest('.rk-command-panel')\n if (panel) release = inertOutside(panel)\n },\n { immediate: true },\n)\n\nwatch(query, () => (active.value = 0))\n\nonBeforeUnmount(() => {\n release?.()\n if (typeof window !== 'undefined') window.removeEventListener('keydown', onWindowKeydown)\n})\n\nconst indexOf = (item: CommandItem<Id>) => flat.value.indexOf(item)\n</script>\n\n<template>\n <Teleport :to=\"sheetRoot\">\n <Transition name=\"rk-command\">\n <div v-if=\"open\" class=\"rk-command\" @keydown=\"onKeydown\">\n <div class=\"rk-command-scrim\" @click=\"open = false\" />\n\n <div\n class=\"rk-command-panel surface-overlay\"\n role=\"dialog\"\n aria-modal=\"true\"\n :aria-label=\"label\"\n >\n <div class=\"rk-command-field\">\n <Search class=\"text-ink-soft size-4 shrink-0\" aria-hidden=\"true\" />\n <input\n ref=\"field\"\n v-model=\"query\"\n type=\"text\"\n role=\"combobox\"\n class=\"rk-command-input\"\n autocomplete=\"off\"\n :placeholder=\"placeholder\"\n :aria-label=\"label\"\n :aria-expanded=\"true\"\n :aria-controls=\"id\"\n :aria-activedescendant=\"flat.length ? `${id}-${active}` : undefined\"\n />\n </div>\n\n <div :id=\"id\" class=\"rk-command-list\" role=\"listbox\" :aria-label=\"label\">\n <template v-for=\"group in matches\" :key=\"group.label ?? 'items'\">\n <p v-if=\"group.label\" class=\"rk-command-group\">{{ group.label }}</p>\n\n <div\n v-for=\"item in group.items\"\n :id=\"`${id}-${indexOf(item)}`\"\n :key=\"item.id\"\n class=\"rk-command-item\"\n :class=\"{ 'is-active': indexOf(item) === active }\"\n role=\"option\"\n :aria-selected=\"indexOf(item) === active\"\n @click=\"choose(item)\"\n @pointermove=\"active = indexOf(item)\"\n >\n <component\n :is=\"item.icon\"\n v-if=\"item.icon\"\n class=\"size-4 shrink-0\"\n aria-hidden=\"true\"\n />\n <span class=\"truncate\">{{ item.label }}</span>\n <span v-if=\"item.hint\" class=\"rk-command-hint\">{{ item.hint }}</span>\n </div>\n </template>\n\n <p v-if=\"flat.length === 0\" class=\"rk-command-empty\">{{ emptyLabel }}</p>\n </div>\n </div>\n </div>\n </Transition>\n </Teleport>\n</template>\n\n<style scoped>\n.rk-command {\n position: fixed;\n inset: 0;\n z-index: 90;\n display: flex;\n justify-content: center;\n padding: 1rem;\n /* Near the top, not centred: the eye is already up there, and the list\n grows downwards into space rather than pushing itself off both edges. */\n padding-top: min(12vh, 6rem);\n}\n\n.rk-command-scrim {\n position: absolute;\n inset: 0;\n background: color-mix(in oklab, var(--color-ink) 45%, transparent);\n backdrop-filter: blur(2px);\n}\n\n.rk-command-panel {\n position: relative;\n display: flex;\n width: 100%;\n max-width: 34rem;\n max-height: min(28rem, 70vh);\n flex-direction: column;\n overflow: hidden;\n border-radius: var(--radius-card);\n}\n\n.rk-command-field {\n display: flex;\n align-items: center;\n gap: 0.625rem;\n border-bottom: 1px solid var(--surface-border-color);\n padding: 0.875rem 1rem;\n}\n\n.rk-command-input {\n min-width: 0;\n flex: 1;\n background: transparent;\n font-size: 1rem;\n color: var(--color-ink);\n outline: none;\n}\n\n/* The field takes focus the moment this opens, so a full ring around it\n would be on the whole time. The rule under it turns instead: enough to\n say where the keyboard is, quiet enough to live with. */\n.rk-command-field:has(.rk-command-input:focus-visible) {\n border-bottom-color: var(--color-primary);\n box-shadow: inset 0 -1px 0 var(--color-primary);\n}\n\n.rk-command-list {\n overflow-y: auto;\n padding: 0.375rem;\n}\n\n.rk-command-group {\n padding: 0.5rem 0.625rem 0.25rem;\n font-size: 0.6875rem;\n font-weight: 600;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--color-ink-soft);\n}\n\n.rk-command-item {\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.rk-command-item.is-active {\n background: var(--color-muted);\n}\n\n.rk-command-hint {\n margin-left: auto;\n flex-shrink: 0;\n font-size: 0.75rem;\n color: var(--color-ink-soft);\n}\n\n.rk-command-empty {\n padding: 1.5rem 0.625rem;\n text-align: center;\n font-size: 0.875rem;\n color: var(--color-ink-soft);\n}\n\n.rk-command-enter-active,\n.rk-command-leave-active {\n transition: opacity var(--duration-fast) var(--ease-standard);\n}\n\n.rk-command-enter-active .rk-command-panel,\n.rk-command-leave-active .rk-command-panel {\n transition: transform var(--duration-fast) var(--ease-standard);\n}\n\n.rk-command-enter-from,\n.rk-command-leave-to {\n opacity: 0;\n}\n\n.rk-command-enter-from .rk-command-panel,\n.rk-command-leave-to .rk-command-panel {\n transform: translateY(-0.5rem) scale(0.98);\n}\n</style>\n","<script setup lang=\"ts\" generic=\"Row extends Record<string, unknown>\">\nimport { ArrowDown, ArrowUp, ChevronsUpDown } from 'lucide-vue-next'\nimport { computed } from 'vue'\n\nimport BaseSkeleton from '../components/BaseSkeleton.vue'\nimport type { Column } from '../components/BaseTable.vue'\n\nexport interface DataColumn<Row> extends Column<Row> {\n /** Adds a button to the header that sorts by this column. */\n sortable?: boolean | undefined\n}\n\nexport interface TableSort<Row> {\n key: string & keyof Row\n direction: 'asc' | 'desc'\n}\n\n/**\n * `BaseTable` with the three things a table of real data grows: a sort, a\n * selection, and something to show while it is loading.\n *\n * Sorting happens here by default — comparing numbers as numbers and strings\n * with `localeCompare`, so \"Ömer\" lands where a reader expects. Set\n * `manualSort` and it only reports what was asked for, which is what a\n * server-paged table needs: sorting the twenty rows on screen would be a\n * lie about the other nine thousand.\n *\n * The header's sort control is a real button inside the `th`, and the `th`\n * carries `aria-sort`. That pair is what a screen reader reads as \"sorted\n * ascending\"; a `div` with a click handler and an arrow glyph is neither.\n *\n * Selection needs `rowKey`, because a selection of row indexes is a\n * selection that changes meaning the moment the table is sorted.\n */\nconst {\n columns,\n rows,\n caption,\n captionHidden = false,\n rowKey = undefined,\n selectAllLabel = undefined,\n rowLabel = undefined,\n manualSort = false,\n loading = false,\n loadingRows = 5,\n stickyHeader = false,\n} = defineProps<{\n columns: readonly DataColumn<Row>[]\n rows: readonly Row[]\n /** What this table is. Required — an unnamed table is a box of numbers. */\n caption: string\n captionHidden?: boolean | undefined\n /** Which field identifies a row. Required for selection. */\n rowKey?: (string & keyof Row) | undefined\n /** Given with `rowKey`, the table grows a checkbox column. Names the header one. */\n selectAllLabel?: string | undefined\n /** Names each row's checkbox from the row: `(row) => \\`Select ${row.name}\\``. */\n rowLabel?: ((row: Row) => string) | undefined\n /** Report the sort instead of doing it — for a server that pages the data. */\n manualSort?: boolean | undefined\n /** Draws skeleton rows instead of the body. */\n loading?: boolean | undefined\n loadingRows?: number | undefined\n /** Keeps the header on screen while the body scrolls under it. */\n stickyHeader?: boolean | undefined\n}>()\n\ndefineSlots<{\n /** Per-column cell override, named for the column key. */\n [key: string]: (props: { row: Row; value: unknown }) => unknown\n /** Shown instead of the body when there are no rows. */\n empty?: () => unknown\n}>()\n\n/** Which column is sorted, and which way. `v-model:sort`. */\nconst sort = defineModel<TableSort<Row> | undefined>('sort', { default: undefined })\n/** The chosen rows' keys. `v-model:selected`. */\nconst selected = defineModel<string[]>('selected', { default: () => [] })\n\nconst selectable = computed(() => Boolean(rowKey && selectAllLabel && rowLabel))\n\nconst keyOf = (row: Row, index: number) => (rowKey ? String(row[rowKey]) : String(index))\n\n/* Numbers compared as numbers, everything else through the reader's\n collation: `'ö' < 'p'` is false as a code point and true in Turkish. */\nfunction compare(a: unknown, b: unknown) {\n if (typeof a === 'number' && typeof b === 'number') return a - b\n if (a == null) return b == null ? 0 : -1\n if (b == null) return 1\n\n return String(a).localeCompare(String(b))\n}\n\nconst shown = computed(() => {\n if (!sort.value || manualSort) return rows\n\n const { key, direction } = sort.value\n const sorted = [...rows].sort((a, b) => compare(a[key], b[key]))\n\n return direction === 'asc' ? sorted : sorted.reverse()\n})\n\n/** Ascending, then descending, then back to the order it arrived in. */\nfunction toggleSort(column: DataColumn<Row>) {\n if (!column.sortable) return\n\n if (sort.value?.key !== column.key) {\n sort.value = { key: column.key, direction: 'asc' }\n return\n }\n\n sort.value = sort.value.direction === 'asc' ? { key: column.key, direction: 'desc' } : undefined\n}\n\nconst ariaSort = (column: DataColumn<Row>) => {\n if (!column.sortable) return undefined\n if (sort.value?.key !== column.key) return 'none'\n\n return sort.value.direction === 'asc' ? 'ascending' : 'descending'\n}\n\nconst allKeys = computed(() => shown.value.map((row, index) => keyOf(row, index)))\nconst allChosen = computed(\n () => allKeys.value.length > 0 && allKeys.value.every((key) => selected.value.includes(key)),\n)\nconst someChosen = computed(\n () => !allChosen.value && allKeys.value.some((key) => selected.value.includes(key)),\n)\n\nfunction toggleAll() {\n selected.value = allChosen.value ? [] : allKeys.value\n}\n\nfunction toggleRow(key: string) {\n selected.value = selected.value.includes(key)\n ? selected.value.filter((one) => one !== key)\n : [...selected.value, key]\n}\n\nconst columnCount = computed(() => columns.length + (selectable.value ? 1 : 0))\n</script>\n\n<template>\n <div class=\"rk-data-scroll\" tabindex=\"0\" role=\"region\" :aria-label=\"caption\">\n <table class=\"rk-data\" :class=\"{ 'is-sticky': stickyHeader }\">\n <caption :class=\"captionHidden ? 'rk-data-caption-hidden' : 'rk-data-caption'\">\n {{\n caption\n }}\n </caption>\n\n <thead>\n <tr>\n <th v-if=\"selectable\" scope=\"col\" class=\"rk-data-pick\">\n <input\n type=\"checkbox\"\n class=\"rk-data-box focus-ring\"\n :checked=\"allChosen\"\n :indeterminate=\"someChosen\"\n :aria-label=\"selectAllLabel\"\n @change=\"toggleAll\"\n />\n </th>\n\n <th\n v-for=\"column in columns\"\n :key=\"column.key\"\n scope=\"col\"\n :aria-sort=\"ariaSort(column)\"\n :class=\"[column.align && `is-${column.align}`, column.nowrap && 'is-nowrap']\"\n >\n <button\n v-if=\"column.sortable\"\n type=\"button\"\n class=\"rk-data-sort focus-ring\"\n @click=\"toggleSort(column)\"\n >\n {{ column.label }}\n <ArrowUp\n v-if=\"ariaSort(column) === 'ascending'\"\n class=\"size-3.5\"\n aria-hidden=\"true\"\n />\n <ArrowDown\n v-else-if=\"ariaSort(column) === 'descending'\"\n class=\"size-3.5\"\n aria-hidden=\"true\"\n />\n <ChevronsUpDown v-else class=\"size-3.5 opacity-40\" aria-hidden=\"true\" />\n </button>\n <template v-else>{{ column.label }}</template>\n </th>\n </tr>\n </thead>\n\n <tbody v-if=\"loading\" aria-busy=\"true\">\n <tr v-for=\"row in loadingRows\" :key=\"row\">\n <td v-if=\"selectable\" />\n <td v-for=\"column in columns\" :key=\"column.key\">\n <BaseSkeleton shape=\"text\" height=\"0.75rem\" width=\"70%\" />\n </td>\n </tr>\n </tbody>\n\n <tbody v-else-if=\"shown.length > 0\">\n <tr\n v-for=\"(row, index) in shown\"\n :key=\"keyOf(row, index)\"\n :class=\"{ 'is-chosen': selected.includes(keyOf(row, index)) }\"\n >\n <td v-if=\"selectable\" class=\"rk-data-pick\">\n <input\n type=\"checkbox\"\n class=\"rk-data-box focus-ring\"\n :checked=\"selected.includes(keyOf(row, index))\"\n :aria-label=\"rowLabel!(row)\"\n @change=\"toggleRow(keyOf(row, index))\"\n />\n </td>\n\n <td\n v-for=\"column in columns\"\n :key=\"column.key\"\n :class=\"[column.align && `is-${column.align}`, column.nowrap && 'is-nowrap']\"\n >\n <slot :name=\"column.key\" :row=\"row\" :value=\"row[column.key]\">\n {{ row[column.key] }}\n </slot>\n </td>\n </tr>\n </tbody>\n\n <tbody v-else>\n <tr>\n <td :colspan=\"columnCount\" class=\"rk-data-empty\">\n <slot name=\"empty\" />\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n</template>\n\n<style scoped>\n/* The scroller is focusable: a region only a drag can reach is a region a\n keyboard cannot read at all. */\n.rk-data-scroll {\n overflow-x: auto;\n border-radius: var(--radius-card);\n}\n\n.rk-data-scroll:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n.rk-data {\n width: 100%;\n border-collapse: collapse;\n font-size: 0.875rem;\n}\n\n.rk-data-caption {\n padding-bottom: 0.5rem;\n text-align: left;\n font-size: 0.8125rem;\n color: var(--color-ink-soft);\n}\n\n.rk-data-caption-hidden {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n}\n\n.rk-data th {\n border-bottom: 1px solid var(--color-hair);\n padding: 0.625rem 0.75rem;\n text-align: left;\n font-size: 0.75rem;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--color-ink-soft);\n white-space: nowrap;\n}\n\n.rk-data.is-sticky thead th {\n position: sticky;\n top: 0;\n z-index: 1;\n background: var(--color-surface);\n}\n\n.rk-data td {\n border-bottom: 1px solid color-mix(in oklab, var(--color-hair) 60%, transparent);\n padding: 0.625rem 0.75rem;\n color: var(--color-ink);\n vertical-align: middle;\n}\n\n.rk-data tbody tr:last-child td {\n border-bottom: 0;\n}\n\n.rk-data tbody tr.is-chosen td {\n background: color-mix(in oklab, var(--color-primary) 8%, transparent);\n}\n\n/* Named against `.rk-data th`, which sets `text-align: left` and would\n otherwise win: the heading stayed on the left while its numbers sat at the\n far edge, so one column read as two. The sort button is inline-flex, so it\n follows the heading's alignment and carries the arrow with it. */\n.rk-data th.is-end,\n.rk-data td.is-end {\n text-align: right;\n font-variant-numeric: tabular-nums;\n}\n\n.rk-data th.is-center,\n.rk-data td.is-center {\n text-align: center;\n}\n\n.rk-data th.is-start,\n.rk-data td.is-start {\n text-align: left;\n}\n\n.is-nowrap {\n white-space: nowrap;\n}\n\n.rk-data-sort {\n display: inline-flex;\n align-items: center;\n gap: 0.25rem;\n border-radius: var(--radius-cell);\n color: inherit;\n transition: color var(--duration-fast) var(--ease-standard);\n}\n\n.rk-data-sort:hover {\n color: var(--color-ink);\n}\n\n.rk-data-pick {\n width: 2.75rem;\n padding-right: 0;\n}\n\n.rk-data-box {\n width: 1rem;\n height: 1rem;\n accent-color: var(--color-primary);\n}\n\n.rk-data-empty {\n padding: 2rem 0.75rem;\n text-align: center;\n color: var(--color-ink-soft);\n}\n</style>\n","<script setup lang=\"ts\" generic=\"Row extends Record<string, unknown>\">\nimport { ArrowDown, ArrowUp, ChevronsUpDown } from 'lucide-vue-next'\nimport { computed } from 'vue'\n\nimport BaseSkeleton from '../components/BaseSkeleton.vue'\nimport type { Column } from '../components/BaseTable.vue'\n\nexport interface DataColumn<Row> extends Column<Row> {\n /** Adds a button to the header that sorts by this column. */\n sortable?: boolean | undefined\n}\n\nexport interface TableSort<Row> {\n key: string & keyof Row\n direction: 'asc' | 'desc'\n}\n\n/**\n * `BaseTable` with the three things a table of real data grows: a sort, a\n * selection, and something to show while it is loading.\n *\n * Sorting happens here by default — comparing numbers as numbers and strings\n * with `localeCompare`, so \"Ömer\" lands where a reader expects. Set\n * `manualSort` and it only reports what was asked for, which is what a\n * server-paged table needs: sorting the twenty rows on screen would be a\n * lie about the other nine thousand.\n *\n * The header's sort control is a real button inside the `th`, and the `th`\n * carries `aria-sort`. That pair is what a screen reader reads as \"sorted\n * ascending\"; a `div` with a click handler and an arrow glyph is neither.\n *\n * Selection needs `rowKey`, because a selection of row indexes is a\n * selection that changes meaning the moment the table is sorted.\n */\nconst {\n columns,\n rows,\n caption,\n captionHidden = false,\n rowKey = undefined,\n selectAllLabel = undefined,\n rowLabel = undefined,\n manualSort = false,\n loading = false,\n loadingRows = 5,\n stickyHeader = false,\n} = defineProps<{\n columns: readonly DataColumn<Row>[]\n rows: readonly Row[]\n /** What this table is. Required — an unnamed table is a box of numbers. */\n caption: string\n captionHidden?: boolean | undefined\n /** Which field identifies a row. Required for selection. */\n rowKey?: (string & keyof Row) | undefined\n /** Given with `rowKey`, the table grows a checkbox column. Names the header one. */\n selectAllLabel?: string | undefined\n /** Names each row's checkbox from the row: `(row) => \\`Select ${row.name}\\``. */\n rowLabel?: ((row: Row) => string) | undefined\n /** Report the sort instead of doing it — for a server that pages the data. */\n manualSort?: boolean | undefined\n /** Draws skeleton rows instead of the body. */\n loading?: boolean | undefined\n loadingRows?: number | undefined\n /** Keeps the header on screen while the body scrolls under it. */\n stickyHeader?: boolean | undefined\n}>()\n\ndefineSlots<{\n /** Per-column cell override, named for the column key. */\n [key: string]: (props: { row: Row; value: unknown }) => unknown\n /** Shown instead of the body when there are no rows. */\n empty?: () => unknown\n}>()\n\n/** Which column is sorted, and which way. `v-model:sort`. */\nconst sort = defineModel<TableSort<Row> | undefined>('sort', { default: undefined })\n/** The chosen rows' keys. `v-model:selected`. */\nconst selected = defineModel<string[]>('selected', { default: () => [] })\n\nconst selectable = computed(() => Boolean(rowKey && selectAllLabel && rowLabel))\n\nconst keyOf = (row: Row, index: number) => (rowKey ? String(row[rowKey]) : String(index))\n\n/* Numbers compared as numbers, everything else through the reader's\n collation: `'ö' < 'p'` is false as a code point and true in Turkish. */\nfunction compare(a: unknown, b: unknown) {\n if (typeof a === 'number' && typeof b === 'number') return a - b\n if (a == null) return b == null ? 0 : -1\n if (b == null) return 1\n\n return String(a).localeCompare(String(b))\n}\n\nconst shown = computed(() => {\n if (!sort.value || manualSort) return rows\n\n const { key, direction } = sort.value\n const sorted = [...rows].sort((a, b) => compare(a[key], b[key]))\n\n return direction === 'asc' ? sorted : sorted.reverse()\n})\n\n/** Ascending, then descending, then back to the order it arrived in. */\nfunction toggleSort(column: DataColumn<Row>) {\n if (!column.sortable) return\n\n if (sort.value?.key !== column.key) {\n sort.value = { key: column.key, direction: 'asc' }\n return\n }\n\n sort.value = sort.value.direction === 'asc' ? { key: column.key, direction: 'desc' } : undefined\n}\n\nconst ariaSort = (column: DataColumn<Row>) => {\n if (!column.sortable) return undefined\n if (sort.value?.key !== column.key) return 'none'\n\n return sort.value.direction === 'asc' ? 'ascending' : 'descending'\n}\n\nconst allKeys = computed(() => shown.value.map((row, index) => keyOf(row, index)))\nconst allChosen = computed(\n () => allKeys.value.length > 0 && allKeys.value.every((key) => selected.value.includes(key)),\n)\nconst someChosen = computed(\n () => !allChosen.value && allKeys.value.some((key) => selected.value.includes(key)),\n)\n\nfunction toggleAll() {\n selected.value = allChosen.value ? [] : allKeys.value\n}\n\nfunction toggleRow(key: string) {\n selected.value = selected.value.includes(key)\n ? selected.value.filter((one) => one !== key)\n : [...selected.value, key]\n}\n\nconst columnCount = computed(() => columns.length + (selectable.value ? 1 : 0))\n</script>\n\n<template>\n <div class=\"rk-data-scroll\" tabindex=\"0\" role=\"region\" :aria-label=\"caption\">\n <table class=\"rk-data\" :class=\"{ 'is-sticky': stickyHeader }\">\n <caption :class=\"captionHidden ? 'rk-data-caption-hidden' : 'rk-data-caption'\">\n {{\n caption\n }}\n </caption>\n\n <thead>\n <tr>\n <th v-if=\"selectable\" scope=\"col\" class=\"rk-data-pick\">\n <input\n type=\"checkbox\"\n class=\"rk-data-box focus-ring\"\n :checked=\"allChosen\"\n :indeterminate=\"someChosen\"\n :aria-label=\"selectAllLabel\"\n @change=\"toggleAll\"\n />\n </th>\n\n <th\n v-for=\"column in columns\"\n :key=\"column.key\"\n scope=\"col\"\n :aria-sort=\"ariaSort(column)\"\n :class=\"[column.align && `is-${column.align}`, column.nowrap && 'is-nowrap']\"\n >\n <button\n v-if=\"column.sortable\"\n type=\"button\"\n class=\"rk-data-sort focus-ring\"\n @click=\"toggleSort(column)\"\n >\n {{ column.label }}\n <ArrowUp\n v-if=\"ariaSort(column) === 'ascending'\"\n class=\"size-3.5\"\n aria-hidden=\"true\"\n />\n <ArrowDown\n v-else-if=\"ariaSort(column) === 'descending'\"\n class=\"size-3.5\"\n aria-hidden=\"true\"\n />\n <ChevronsUpDown v-else class=\"size-3.5 opacity-40\" aria-hidden=\"true\" />\n </button>\n <template v-else>{{ column.label }}</template>\n </th>\n </tr>\n </thead>\n\n <tbody v-if=\"loading\" aria-busy=\"true\">\n <tr v-for=\"row in loadingRows\" :key=\"row\">\n <td v-if=\"selectable\" />\n <td v-for=\"column in columns\" :key=\"column.key\">\n <BaseSkeleton shape=\"text\" height=\"0.75rem\" width=\"70%\" />\n </td>\n </tr>\n </tbody>\n\n <tbody v-else-if=\"shown.length > 0\">\n <tr\n v-for=\"(row, index) in shown\"\n :key=\"keyOf(row, index)\"\n :class=\"{ 'is-chosen': selected.includes(keyOf(row, index)) }\"\n >\n <td v-if=\"selectable\" class=\"rk-data-pick\">\n <input\n type=\"checkbox\"\n class=\"rk-data-box focus-ring\"\n :checked=\"selected.includes(keyOf(row, index))\"\n :aria-label=\"rowLabel!(row)\"\n @change=\"toggleRow(keyOf(row, index))\"\n />\n </td>\n\n <td\n v-for=\"column in columns\"\n :key=\"column.key\"\n :class=\"[column.align && `is-${column.align}`, column.nowrap && 'is-nowrap']\"\n >\n <slot :name=\"column.key\" :row=\"row\" :value=\"row[column.key]\">\n {{ row[column.key] }}\n </slot>\n </td>\n </tr>\n </tbody>\n\n <tbody v-else>\n <tr>\n <td :colspan=\"columnCount\" class=\"rk-data-empty\">\n <slot name=\"empty\" />\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n</template>\n\n<style scoped>\n/* The scroller is focusable: a region only a drag can reach is a region a\n keyboard cannot read at all. */\n.rk-data-scroll {\n overflow-x: auto;\n border-radius: var(--radius-card);\n}\n\n.rk-data-scroll:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n.rk-data {\n width: 100%;\n border-collapse: collapse;\n font-size: 0.875rem;\n}\n\n.rk-data-caption {\n padding-bottom: 0.5rem;\n text-align: left;\n font-size: 0.8125rem;\n color: var(--color-ink-soft);\n}\n\n.rk-data-caption-hidden {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n}\n\n.rk-data th {\n border-bottom: 1px solid var(--color-hair);\n padding: 0.625rem 0.75rem;\n text-align: left;\n font-size: 0.75rem;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--color-ink-soft);\n white-space: nowrap;\n}\n\n.rk-data.is-sticky thead th {\n position: sticky;\n top: 0;\n z-index: 1;\n background: var(--color-surface);\n}\n\n.rk-data td {\n border-bottom: 1px solid color-mix(in oklab, var(--color-hair) 60%, transparent);\n padding: 0.625rem 0.75rem;\n color: var(--color-ink);\n vertical-align: middle;\n}\n\n.rk-data tbody tr:last-child td {\n border-bottom: 0;\n}\n\n.rk-data tbody tr.is-chosen td {\n background: color-mix(in oklab, var(--color-primary) 8%, transparent);\n}\n\n/* Named against `.rk-data th`, which sets `text-align: left` and would\n otherwise win: the heading stayed on the left while its numbers sat at the\n far edge, so one column read as two. The sort button is inline-flex, so it\n follows the heading's alignment and carries the arrow with it. */\n.rk-data th.is-end,\n.rk-data td.is-end {\n text-align: right;\n font-variant-numeric: tabular-nums;\n}\n\n.rk-data th.is-center,\n.rk-data td.is-center {\n text-align: center;\n}\n\n.rk-data th.is-start,\n.rk-data td.is-start {\n text-align: left;\n}\n\n.is-nowrap {\n white-space: nowrap;\n}\n\n.rk-data-sort {\n display: inline-flex;\n align-items: center;\n gap: 0.25rem;\n border-radius: var(--radius-cell);\n color: inherit;\n transition: color var(--duration-fast) var(--ease-standard);\n}\n\n.rk-data-sort:hover {\n color: var(--color-ink);\n}\n\n.rk-data-pick {\n width: 2.75rem;\n padding-right: 0;\n}\n\n.rk-data-box {\n width: 1rem;\n height: 1rem;\n accent-color: var(--color-primary);\n}\n\n.rk-data-empty {\n padding: 2rem 0.75rem;\n text-align: center;\n color: var(--color-ink-soft);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, useTemplateRef } from 'vue'\n\n/**\n * Two panes and a handle between them — a list beside a preview, an editor\n * beside its output.\n *\n * The handle is a `separator` with a value, which is the part a hand-written\n * one never has: it can be moved with the arrow keys, Home and End send it\n * to its limits, and Enter puts it back where it started. A divider that\n * only answers to a drag is a layout a keyboard cannot change at all.\n *\n * The split is a percentage, so the panes keep their proportions when the\n * window changes rather than one of them swallowing the other.\n */\nconst {\n label,\n orientation = 'horizontal',\n min = 15,\n max = 85,\n step = 2,\n} = defineProps<{\n /** The handle's accessible name, e.g. \"Resize the list\". */\n label: string\n /** `horizontal`: side by side, the handle moves left and right. */\n orientation?: 'horizontal' | 'vertical' | undefined\n /** How small the first pane may get, as a percentage. */\n min?: number | undefined\n max?: number | undefined\n /** How far one arrow press moves it, in percentage points. */\n step?: number | undefined\n}>()\n\ndefineSlots<{\n /** The first pane: the list, the tree, the sidebar. */\n start: () => unknown\n /** The second pane. */\n end: () => unknown\n}>()\n\n/** Where the split sits, as a percentage of the whole. `v-model`. */\nconst split = defineModel<number>({ default: 50 })\n\nconst root = useTemplateRef<HTMLElement>('root')\nconst dragging = ref(false)\nconst startedAt = split.value\n\nconst clamp = (value: number) => Math.min(max, Math.max(min, value))\n\nconst horizontal = computed(() => orientation === 'horizontal')\n\nfunction moveTo(point: number) {\n const box = root.value?.getBoundingClientRect()\n if (!box) return\n\n const ratio = horizontal.value ? (point - box.left) / box.width : (point - box.top) / box.height\n\n split.value = clamp(Math.round(ratio * 100))\n}\n\nfunction onPointerDown(event: PointerEvent) {\n dragging.value = true\n ;(event.target as HTMLElement).setPointerCapture(event.pointerId)\n}\n\nfunction onPointerMove(event: PointerEvent) {\n if (!dragging.value) return\n event.preventDefault()\n moveTo(horizontal.value ? event.clientX : event.clientY)\n}\n\nfunction onPointerUp(event: PointerEvent) {\n dragging.value = false\n ;(event.target as HTMLElement).releasePointerCapture?.(event.pointerId)\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n const back = horizontal.value ? 'ArrowLeft' : 'ArrowUp'\n const forward = horizontal.value ? 'ArrowRight' : 'ArrowDown'\n\n const moves: Record<string, number> = {\n [back]: split.value - step,\n [forward]: split.value + step,\n Home: min,\n End: max,\n // Enter puts it back where it started, which is the undo a drag has no\n // other way of offering.\n Enter: startedAt,\n }\n\n const next = moves[event.key]\n if (next === undefined) return\n\n event.preventDefault()\n split.value = clamp(next)\n}\n\nonBeforeUnmount(() => (dragging.value = false))\n</script>\n\n<template>\n <div\n ref=\"root\"\n class=\"rk-split\"\n :class=\"[`is-${orientation}`, { 'is-dragging': dragging }]\"\n :style=\"{ '--rk-split': `${split}%` }\"\n >\n <div class=\"rk-split-pane is-start\"><slot name=\"start\" /></div>\n\n <div\n class=\"rk-split-handle focus-ring\"\n role=\"separator\"\n tabindex=\"0\"\n :aria-label=\"label\"\n :aria-orientation=\"orientation === 'horizontal' ? 'vertical' : 'horizontal'\"\n :aria-valuenow=\"split\"\n :aria-valuemin=\"min\"\n :aria-valuemax=\"max\"\n @pointerdown=\"onPointerDown\"\n @pointermove=\"onPointerMove\"\n @pointerup=\"onPointerUp\"\n @pointercancel=\"onPointerUp\"\n @keydown=\"onKeydown\"\n >\n <span class=\"rk-split-grip\" aria-hidden=\"true\" />\n </div>\n\n <div class=\"rk-split-pane is-end\"><slot name=\"end\" /></div>\n </div>\n</template>\n\n<style scoped>\n.rk-split {\n display: flex;\n width: 100%;\n min-height: 0;\n}\n\n.rk-split.is-vertical {\n flex-direction: column;\n}\n\n.rk-split.is-dragging {\n user-select: none;\n}\n\n.rk-split-pane {\n min-width: 0;\n min-height: 0;\n overflow: auto;\n}\n\n.is-horizontal .rk-split-pane.is-start {\n width: var(--rk-split);\n}\n\n.is-vertical .rk-split-pane.is-start {\n height: var(--rk-split);\n}\n\n.rk-split-pane.is-end {\n flex: 1;\n}\n\n/* The handle is wider than the line it draws: a 1px target is a target\n nobody hits. */\n.rk-split-handle {\n display: grid;\n flex-shrink: 0;\n place-items: center;\n background: transparent;\n touch-action: none;\n}\n\n.is-horizontal .rk-split-handle {\n width: 0.75rem;\n cursor: col-resize;\n}\n\n.is-vertical .rk-split-handle {\n height: 0.75rem;\n cursor: row-resize;\n}\n\n.rk-split-grip {\n display: block;\n border-radius: 9999px;\n background: var(--color-hair);\n transition: background-color var(--duration-fast) var(--ease-standard);\n}\n\n.is-horizontal .rk-split-grip {\n width: 2px;\n height: 100%;\n}\n\n.is-vertical .rk-split-grip {\n width: 100%;\n height: 2px;\n}\n\n.rk-split-handle:hover .rk-split-grip,\n.rk-split-handle:focus-visible .rk-split-grip,\n.is-dragging .rk-split-grip {\n background: var(--color-primary);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed, onBeforeUnmount, ref, useTemplateRef } from 'vue'\n\n/**\n * Two panes and a handle between them — a list beside a preview, an editor\n * beside its output.\n *\n * The handle is a `separator` with a value, which is the part a hand-written\n * one never has: it can be moved with the arrow keys, Home and End send it\n * to its limits, and Enter puts it back where it started. A divider that\n * only answers to a drag is a layout a keyboard cannot change at all.\n *\n * The split is a percentage, so the panes keep their proportions when the\n * window changes rather than one of them swallowing the other.\n */\nconst {\n label,\n orientation = 'horizontal',\n min = 15,\n max = 85,\n step = 2,\n} = defineProps<{\n /** The handle's accessible name, e.g. \"Resize the list\". */\n label: string\n /** `horizontal`: side by side, the handle moves left and right. */\n orientation?: 'horizontal' | 'vertical' | undefined\n /** How small the first pane may get, as a percentage. */\n min?: number | undefined\n max?: number | undefined\n /** How far one arrow press moves it, in percentage points. */\n step?: number | undefined\n}>()\n\ndefineSlots<{\n /** The first pane: the list, the tree, the sidebar. */\n start: () => unknown\n /** The second pane. */\n end: () => unknown\n}>()\n\n/** Where the split sits, as a percentage of the whole. `v-model`. */\nconst split = defineModel<number>({ default: 50 })\n\nconst root = useTemplateRef<HTMLElement>('root')\nconst dragging = ref(false)\nconst startedAt = split.value\n\nconst clamp = (value: number) => Math.min(max, Math.max(min, value))\n\nconst horizontal = computed(() => orientation === 'horizontal')\n\nfunction moveTo(point: number) {\n const box = root.value?.getBoundingClientRect()\n if (!box) return\n\n const ratio = horizontal.value ? (point - box.left) / box.width : (point - box.top) / box.height\n\n split.value = clamp(Math.round(ratio * 100))\n}\n\nfunction onPointerDown(event: PointerEvent) {\n dragging.value = true\n ;(event.target as HTMLElement).setPointerCapture(event.pointerId)\n}\n\nfunction onPointerMove(event: PointerEvent) {\n if (!dragging.value) return\n event.preventDefault()\n moveTo(horizontal.value ? event.clientX : event.clientY)\n}\n\nfunction onPointerUp(event: PointerEvent) {\n dragging.value = false\n ;(event.target as HTMLElement).releasePointerCapture?.(event.pointerId)\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n const back = horizontal.value ? 'ArrowLeft' : 'ArrowUp'\n const forward = horizontal.value ? 'ArrowRight' : 'ArrowDown'\n\n const moves: Record<string, number> = {\n [back]: split.value - step,\n [forward]: split.value + step,\n Home: min,\n End: max,\n // Enter puts it back where it started, which is the undo a drag has no\n // other way of offering.\n Enter: startedAt,\n }\n\n const next = moves[event.key]\n if (next === undefined) return\n\n event.preventDefault()\n split.value = clamp(next)\n}\n\nonBeforeUnmount(() => (dragging.value = false))\n</script>\n\n<template>\n <div\n ref=\"root\"\n class=\"rk-split\"\n :class=\"[`is-${orientation}`, { 'is-dragging': dragging }]\"\n :style=\"{ '--rk-split': `${split}%` }\"\n >\n <div class=\"rk-split-pane is-start\"><slot name=\"start\" /></div>\n\n <div\n class=\"rk-split-handle focus-ring\"\n role=\"separator\"\n tabindex=\"0\"\n :aria-label=\"label\"\n :aria-orientation=\"orientation === 'horizontal' ? 'vertical' : 'horizontal'\"\n :aria-valuenow=\"split\"\n :aria-valuemin=\"min\"\n :aria-valuemax=\"max\"\n @pointerdown=\"onPointerDown\"\n @pointermove=\"onPointerMove\"\n @pointerup=\"onPointerUp\"\n @pointercancel=\"onPointerUp\"\n @keydown=\"onKeydown\"\n >\n <span class=\"rk-split-grip\" aria-hidden=\"true\" />\n </div>\n\n <div class=\"rk-split-pane is-end\"><slot name=\"end\" /></div>\n </div>\n</template>\n\n<style scoped>\n.rk-split {\n display: flex;\n width: 100%;\n min-height: 0;\n}\n\n.rk-split.is-vertical {\n flex-direction: column;\n}\n\n.rk-split.is-dragging {\n user-select: none;\n}\n\n.rk-split-pane {\n min-width: 0;\n min-height: 0;\n overflow: auto;\n}\n\n.is-horizontal .rk-split-pane.is-start {\n width: var(--rk-split);\n}\n\n.is-vertical .rk-split-pane.is-start {\n height: var(--rk-split);\n}\n\n.rk-split-pane.is-end {\n flex: 1;\n}\n\n/* The handle is wider than the line it draws: a 1px target is a target\n nobody hits. */\n.rk-split-handle {\n display: grid;\n flex-shrink: 0;\n place-items: center;\n background: transparent;\n touch-action: none;\n}\n\n.is-horizontal .rk-split-handle {\n width: 0.75rem;\n cursor: col-resize;\n}\n\n.is-vertical .rk-split-handle {\n height: 0.75rem;\n cursor: row-resize;\n}\n\n.rk-split-grip {\n display: block;\n border-radius: 9999px;\n background: var(--color-hair);\n transition: background-color var(--duration-fast) var(--ease-standard);\n}\n\n.is-horizontal .rk-split-grip {\n width: 2px;\n height: 100%;\n}\n\n.is-vertical .rk-split-grip {\n width: 100%;\n height: 2px;\n}\n\n.rk-split-handle:hover .rk-split-grip,\n.rk-split-handle:focus-visible .rk-split-grip,\n.is-dragging .rk-split-grip {\n background: var(--color-primary);\n}\n</style>\n","<script setup lang=\"ts\">\nimport { onMounted, ref, useTemplateRef } from 'vue'\n\n/**\n * A row of controls that belong together: a formatting bar, a row of view\n * switches, the actions over a table.\n *\n * One Tab stop for the lot. The arrows move between the controls inside it,\n * Home and End jump to the ends — the toolbar pattern, and the reason to\n * use it is arithmetic: twelve buttons in the tab order cost twelve presses\n * to get past on the way to the thing under them.\n *\n * The controls are yours: anything focusable inside is picked up, so a\n * toolbar can mix the kit's buttons, a `ToggleGroup` and a separator\n * without declaring any of them as data.\n */\nconst { label, orientation = 'horizontal' } = defineProps<{\n /** The toolbar's accessible name, e.g. \"Formatting\". */\n label: string\n orientation?: 'horizontal' | 'vertical' | undefined\n}>()\n\ndefineSlots<{ default: () => unknown }>()\n\nconst root = useTemplateRef<HTMLElement>('root')\nconst at = ref(0)\n\nconst FOCUSABLE = 'button:not([disabled]), a[href], input:not([disabled]), select, textarea'\n\nfunction controls(): HTMLElement[] {\n return Array.from(root.value?.querySelectorAll<HTMLElement>(FOCUSABLE) ?? [])\n}\n\n/* Exactly one control is in the tab order, and which one is remembered:\n coming back to a toolbar puts you where you left it. */\nfunction applyRoving() {\n const list = controls()\n at.value = Math.min(at.value, Math.max(0, list.length - 1))\n list.forEach((control, index) => {\n control.tabIndex = index === at.value ? 0 : -1\n })\n}\n\nfunction focusAt(index: number) {\n const list = controls()\n if (list.length === 0) return\n\n at.value = (index + list.length) % list.length\n applyRoving()\n list[at.value]?.focus()\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n const back = orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp'\n const forward = orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown'\n const list = controls()\n const current = list.indexOf(document.activeElement as HTMLElement)\n\n if (event.key === back) {\n event.preventDefault()\n focusAt(current - 1)\n } else if (event.key === forward) {\n event.preventDefault()\n focusAt(current + 1)\n } else if (event.key === 'Home') {\n event.preventDefault()\n focusAt(0)\n } else if (event.key === 'End') {\n event.preventDefault()\n focusAt(list.length - 1)\n }\n}\n\nfunction onFocusin(event: FocusEvent) {\n const index = controls().indexOf(event.target as HTMLElement)\n if (index >= 0) {\n at.value = index\n applyRoving()\n }\n}\n\nonMounted(applyRoving)\n</script>\n\n<template>\n <div\n ref=\"root\"\n class=\"rk-toolbar\"\n :class=\"`is-${orientation}`\"\n role=\"toolbar\"\n :aria-label=\"label\"\n :aria-orientation=\"orientation\"\n @keydown=\"onKeydown\"\n @focusin=\"onFocusin\"\n >\n <slot />\n </div>\n</template>\n\n<style scoped>\n.rk-toolbar {\n display: flex;\n align-items: center;\n gap: 0.375rem;\n}\n\n.rk-toolbar.is-vertical {\n flex-direction: column;\n align-items: stretch;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { onMounted, ref, useTemplateRef } from 'vue'\n\n/**\n * A row of controls that belong together: a formatting bar, a row of view\n * switches, the actions over a table.\n *\n * One Tab stop for the lot. The arrows move between the controls inside it,\n * Home and End jump to the ends — the toolbar pattern, and the reason to\n * use it is arithmetic: twelve buttons in the tab order cost twelve presses\n * to get past on the way to the thing under them.\n *\n * The controls are yours: anything focusable inside is picked up, so a\n * toolbar can mix the kit's buttons, a `ToggleGroup` and a separator\n * without declaring any of them as data.\n */\nconst { label, orientation = 'horizontal' } = defineProps<{\n /** The toolbar's accessible name, e.g. \"Formatting\". */\n label: string\n orientation?: 'horizontal' | 'vertical' | undefined\n}>()\n\ndefineSlots<{ default: () => unknown }>()\n\nconst root = useTemplateRef<HTMLElement>('root')\nconst at = ref(0)\n\nconst FOCUSABLE = 'button:not([disabled]), a[href], input:not([disabled]), select, textarea'\n\nfunction controls(): HTMLElement[] {\n return Array.from(root.value?.querySelectorAll<HTMLElement>(FOCUSABLE) ?? [])\n}\n\n/* Exactly one control is in the tab order, and which one is remembered:\n coming back to a toolbar puts you where you left it. */\nfunction applyRoving() {\n const list = controls()\n at.value = Math.min(at.value, Math.max(0, list.length - 1))\n list.forEach((control, index) => {\n control.tabIndex = index === at.value ? 0 : -1\n })\n}\n\nfunction focusAt(index: number) {\n const list = controls()\n if (list.length === 0) return\n\n at.value = (index + list.length) % list.length\n applyRoving()\n list[at.value]?.focus()\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n const back = orientation === 'horizontal' ? 'ArrowLeft' : 'ArrowUp'\n const forward = orientation === 'horizontal' ? 'ArrowRight' : 'ArrowDown'\n const list = controls()\n const current = list.indexOf(document.activeElement as HTMLElement)\n\n if (event.key === back) {\n event.preventDefault()\n focusAt(current - 1)\n } else if (event.key === forward) {\n event.preventDefault()\n focusAt(current + 1)\n } else if (event.key === 'Home') {\n event.preventDefault()\n focusAt(0)\n } else if (event.key === 'End') {\n event.preventDefault()\n focusAt(list.length - 1)\n }\n}\n\nfunction onFocusin(event: FocusEvent) {\n const index = controls().indexOf(event.target as HTMLElement)\n if (index >= 0) {\n at.value = index\n applyRoving()\n }\n}\n\nonMounted(applyRoving)\n</script>\n\n<template>\n <div\n ref=\"root\"\n class=\"rk-toolbar\"\n :class=\"`is-${orientation}`\"\n role=\"toolbar\"\n :aria-label=\"label\"\n :aria-orientation=\"orientation\"\n @keydown=\"onKeydown\"\n @focusin=\"onFocusin\"\n >\n <slot />\n </div>\n</template>\n\n<style scoped>\n.rk-toolbar {\n display: flex;\n align-items: center;\n gap: 0.375rem;\n}\n\n.rk-toolbar.is-vertical {\n flex-direction: column;\n align-items: stretch;\n}\n</style>\n","<script setup lang=\"ts\" generic=\"K extends string, M extends 'single' | 'multiple' = 'single'\">\nimport { ChevronRight } from 'lucide-vue-next'\nimport { computed, ref } from 'vue'\n\nimport { useBoundValue } from '../composables/use-bound-value'\nimport type { Component } from 'vue'\n\nexport interface TreeNode<K extends string> {\n key: K\n /** Already translated. */\n label: string\n icon?: Component | undefined\n disabled?: boolean | undefined\n /** Omitted or empty, it is a leaf. */\n children?: readonly TreeNode<K>[] | undefined\n}\n\n/**\n * A tree of things that contain things: folders, a category list, a table of\n * contents.\n *\n * The keyboard is the tree pattern, and it is the whole reason to reach for\n * this rather than nested lists: up and down move through what is on screen,\n * right opens a branch and then steps into it, left closes it and then steps\n * out to its parent, Home and End jump to the ends. One Tab stop for the\n * whole tree.\n *\n * Levels are stated with `aria-level`, `aria-setsize` and `aria-posinset`,\n * so a screen reader can say \"level 2, 3 of 7\" — the only way somebody\n * listening knows where they are in a shape they cannot see.\n *\n * What is open and what is chosen are both the app's: `v-model:expanded`\n * and `v-model`, so a tree can be restored from a URL or a saved state.\n */\nconst {\n modelValue = undefined,\n nodes,\n label,\n mode = 'single' as M,\n} = defineProps<{\n /** The chosen key, or keys in `multiple` mode, with `v-model`. */\n modelValue?: Value | undefined\n nodes: readonly TreeNode<K>[]\n /** The tree's accessible name. */\n label: string\n /** `multiple` lets several be chosen, each with its own tick. */\n mode?: M | undefined\n}>()\n\n/* Multiple always hands back an array; only a single choice can be nothing. */\ntype Value = M extends 'multiple' ? K[] : K | undefined\n\nconst emit = defineEmits<{ 'update:modelValue': [value: Value] }>()\nconst model = useBoundValue<Value>(\n () => modelValue as Value | undefined,\n (value) => emit('update:modelValue', value),\n)\n/** Which branches are open. */\nconst expanded = defineModel<K[]>('expanded', { default: () => [] })\n\ninterface Row {\n node: TreeNode<K>\n level: number\n /** 1-based position among its siblings, and how many there are. */\n position: number\n size: number\n parent: K | null\n}\n\n/** Everything on screen, flattened: the arrows walk this, not the shape. */\nconst rows = computed(() => {\n const out: Row[] = []\n\n const walk = (list: readonly TreeNode<K>[], level: number, parent: K | null) => {\n list.forEach((node, index) => {\n out.push({ node, level, position: index + 1, size: list.length, parent })\n if (node.children?.length && expanded.value.includes(node.key)) {\n walk(node.children, level + 1, node.key)\n }\n })\n }\n\n walk(nodes, 1, null)\n\n return out\n})\n\nconst active = ref(0)\n\nconst chosen = computed(() => {\n const value = model.value as K | K[] | undefined\n if (Array.isArray(value)) return new Set<K>(value)\n\n return new Set<K>(value === undefined ? [] : [value])\n})\n\nconst isOpen = (node: TreeNode<K>) => expanded.value.includes(node.key)\nconst hasChildren = (node: TreeNode<K>) => Boolean(node.children?.length)\n\nfunction setOpen(node: TreeNode<K>, open: boolean) {\n if (!hasChildren(node)) return\n\n expanded.value = open\n ? [...expanded.value, node.key]\n : expanded.value.filter((key) => key !== node.key)\n}\n\nfunction choose(node: TreeNode<K>) {\n if (node.disabled) return\n\n if (mode === 'multiple') {\n const next = new Set(chosen.value)\n if (next.has(node.key)) next.delete(node.key)\n else next.add(node.key)\n\n model.value = [...next] as Value\n return\n }\n\n model.value = node.key as Value\n}\n\nfunction moveTo(index: number) {\n active.value = Math.max(0, Math.min(rows.value.length - 1, index))\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n const row = rows.value[active.value]\n if (!row) return\n\n switch (event.key) {\n case 'ArrowDown':\n event.preventDefault()\n moveTo(active.value + 1)\n break\n case 'ArrowUp':\n event.preventDefault()\n moveTo(active.value - 1)\n break\n case 'ArrowRight':\n event.preventDefault()\n // Open, then step in: the two halves of \"go deeper\".\n if (hasChildren(row.node) && !isOpen(row.node)) setOpen(row.node, true)\n else if (hasChildren(row.node)) moveTo(active.value + 1)\n break\n case 'ArrowLeft': {\n event.preventDefault()\n if (hasChildren(row.node) && isOpen(row.node)) {\n setOpen(row.node, false)\n break\n }\n // Already closed, or a leaf: step out to the parent.\n const parent = rows.value.findIndex((one) => one.node.key === row.parent)\n if (parent >= 0) moveTo(parent)\n break\n }\n case 'Home':\n event.preventDefault()\n moveTo(0)\n break\n case 'End':\n event.preventDefault()\n moveTo(rows.value.length - 1)\n break\n case 'Enter':\n case ' ':\n event.preventDefault()\n choose(row.node)\n break\n }\n}\n\nfunction onClick(index: number, row: Row) {\n active.value = index\n if (hasChildren(row.node)) setOpen(row.node, !isOpen(row.node))\n else choose(row.node)\n}\n</script>\n\n<template>\n <ul\n class=\"rk-tree\"\n role=\"tree\"\n :aria-label=\"label\"\n :aria-multiselectable=\"mode === 'multiple' ? true : undefined\"\n tabindex=\"0\"\n @keydown=\"onKeydown\"\n >\n <li\n v-for=\"(row, index) in rows\"\n :key=\"row.node.key\"\n class=\"rk-tree-row\"\n :class=\"{\n 'is-active': index === active,\n 'is-chosen': chosen.has(row.node.key),\n 'is-disabled': row.node.disabled,\n }\"\n role=\"treeitem\"\n :aria-level=\"row.level\"\n :aria-setsize=\"row.size\"\n :aria-posinset=\"row.position\"\n :aria-expanded=\"row.node.children?.length ? isOpen(row.node) : undefined\"\n :aria-selected=\"chosen.has(row.node.key)\"\n :aria-disabled=\"row.node.disabled || undefined\"\n :style=\"{ '--rk-tree-level': row.level }\"\n @click=\"onClick(index, row)\"\n >\n <ChevronRight\n v-if=\"row.node.children?.length\"\n class=\"rk-tree-twist\"\n :class=\"{ 'is-open': isOpen(row.node) }\"\n aria-hidden=\"true\"\n />\n <span v-else class=\"rk-tree-twist-space\" aria-hidden=\"true\" />\n\n <component\n :is=\"row.node.icon\"\n v-if=\"row.node.icon\"\n class=\"size-4 shrink-0\"\n aria-hidden=\"true\"\n />\n <span class=\"truncate\">{{ row.node.label }}</span>\n </li>\n </ul>\n</template>\n\n<style scoped>\n.rk-tree {\n display: flex;\n flex-direction: column;\n gap: 1px;\n border-radius: var(--radius-card);\n}\n\n.rk-tree:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n.rk-tree-row {\n display: flex;\n cursor: pointer;\n align-items: center;\n gap: 0.375rem;\n border-radius: var(--radius-cell);\n /* Indented by level, so a branch reads as being inside its parent. */\n padding: 0.3125rem 0.5rem 0.3125rem calc(0.5rem + (var(--rk-tree-level) - 1) * 1rem);\n font-size: 0.875rem;\n color: var(--color-ink);\n}\n\n.rk-tree-row.is-active {\n background: var(--color-muted);\n}\n\n.rk-tree-row.is-chosen {\n color: var(--color-primary);\n font-weight: 500;\n}\n\n.rk-tree-row.is-chosen.is-active {\n background: color-mix(in oklab, var(--color-primary) 12%, transparent);\n}\n\n.rk-tree-row.is-disabled {\n cursor: not-allowed;\n opacity: 0.45;\n}\n\n.rk-tree-twist,\n.rk-tree-twist-space {\n width: 1rem;\n height: 1rem;\n flex-shrink: 0;\n}\n\n.rk-tree-twist {\n color: var(--color-ink-soft);\n transition: rotate var(--duration-fast) var(--ease-standard);\n}\n\n.rk-tree-twist.is-open {\n rotate: 90deg;\n}\n</style>\n","<script setup lang=\"ts\" generic=\"K extends string, M extends 'single' | 'multiple' = 'single'\">\nimport { ChevronRight } from 'lucide-vue-next'\nimport { computed, ref } from 'vue'\n\nimport { useBoundValue } from '../composables/use-bound-value'\nimport type { Component } from 'vue'\n\nexport interface TreeNode<K extends string> {\n key: K\n /** Already translated. */\n label: string\n icon?: Component | undefined\n disabled?: boolean | undefined\n /** Omitted or empty, it is a leaf. */\n children?: readonly TreeNode<K>[] | undefined\n}\n\n/**\n * A tree of things that contain things: folders, a category list, a table of\n * contents.\n *\n * The keyboard is the tree pattern, and it is the whole reason to reach for\n * this rather than nested lists: up and down move through what is on screen,\n * right opens a branch and then steps into it, left closes it and then steps\n * out to its parent, Home and End jump to the ends. One Tab stop for the\n * whole tree.\n *\n * Levels are stated with `aria-level`, `aria-setsize` and `aria-posinset`,\n * so a screen reader can say \"level 2, 3 of 7\" — the only way somebody\n * listening knows where they are in a shape they cannot see.\n *\n * What is open and what is chosen are both the app's: `v-model:expanded`\n * and `v-model`, so a tree can be restored from a URL or a saved state.\n */\nconst {\n modelValue = undefined,\n nodes,\n label,\n mode = 'single' as M,\n} = defineProps<{\n /** The chosen key, or keys in `multiple` mode, with `v-model`. */\n modelValue?: Value | undefined\n nodes: readonly TreeNode<K>[]\n /** The tree's accessible name. */\n label: string\n /** `multiple` lets several be chosen, each with its own tick. */\n mode?: M | undefined\n}>()\n\n/* Multiple always hands back an array; only a single choice can be nothing. */\ntype Value = M extends 'multiple' ? K[] : K | undefined\n\nconst emit = defineEmits<{ 'update:modelValue': [value: Value] }>()\nconst model = useBoundValue<Value>(\n () => modelValue as Value | undefined,\n (value) => emit('update:modelValue', value),\n)\n/** Which branches are open. */\nconst expanded = defineModel<K[]>('expanded', { default: () => [] })\n\ninterface Row {\n node: TreeNode<K>\n level: number\n /** 1-based position among its siblings, and how many there are. */\n position: number\n size: number\n parent: K | null\n}\n\n/** Everything on screen, flattened: the arrows walk this, not the shape. */\nconst rows = computed(() => {\n const out: Row[] = []\n\n const walk = (list: readonly TreeNode<K>[], level: number, parent: K | null) => {\n list.forEach((node, index) => {\n out.push({ node, level, position: index + 1, size: list.length, parent })\n if (node.children?.length && expanded.value.includes(node.key)) {\n walk(node.children, level + 1, node.key)\n }\n })\n }\n\n walk(nodes, 1, null)\n\n return out\n})\n\nconst active = ref(0)\n\nconst chosen = computed(() => {\n const value = model.value as K | K[] | undefined\n if (Array.isArray(value)) return new Set<K>(value)\n\n return new Set<K>(value === undefined ? [] : [value])\n})\n\nconst isOpen = (node: TreeNode<K>) => expanded.value.includes(node.key)\nconst hasChildren = (node: TreeNode<K>) => Boolean(node.children?.length)\n\nfunction setOpen(node: TreeNode<K>, open: boolean) {\n if (!hasChildren(node)) return\n\n expanded.value = open\n ? [...expanded.value, node.key]\n : expanded.value.filter((key) => key !== node.key)\n}\n\nfunction choose(node: TreeNode<K>) {\n if (node.disabled) return\n\n if (mode === 'multiple') {\n const next = new Set(chosen.value)\n if (next.has(node.key)) next.delete(node.key)\n else next.add(node.key)\n\n model.value = [...next] as Value\n return\n }\n\n model.value = node.key as Value\n}\n\nfunction moveTo(index: number) {\n active.value = Math.max(0, Math.min(rows.value.length - 1, index))\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n const row = rows.value[active.value]\n if (!row) return\n\n switch (event.key) {\n case 'ArrowDown':\n event.preventDefault()\n moveTo(active.value + 1)\n break\n case 'ArrowUp':\n event.preventDefault()\n moveTo(active.value - 1)\n break\n case 'ArrowRight':\n event.preventDefault()\n // Open, then step in: the two halves of \"go deeper\".\n if (hasChildren(row.node) && !isOpen(row.node)) setOpen(row.node, true)\n else if (hasChildren(row.node)) moveTo(active.value + 1)\n break\n case 'ArrowLeft': {\n event.preventDefault()\n if (hasChildren(row.node) && isOpen(row.node)) {\n setOpen(row.node, false)\n break\n }\n // Already closed, or a leaf: step out to the parent.\n const parent = rows.value.findIndex((one) => one.node.key === row.parent)\n if (parent >= 0) moveTo(parent)\n break\n }\n case 'Home':\n event.preventDefault()\n moveTo(0)\n break\n case 'End':\n event.preventDefault()\n moveTo(rows.value.length - 1)\n break\n case 'Enter':\n case ' ':\n event.preventDefault()\n choose(row.node)\n break\n }\n}\n\nfunction onClick(index: number, row: Row) {\n active.value = index\n if (hasChildren(row.node)) setOpen(row.node, !isOpen(row.node))\n else choose(row.node)\n}\n</script>\n\n<template>\n <ul\n class=\"rk-tree\"\n role=\"tree\"\n :aria-label=\"label\"\n :aria-multiselectable=\"mode === 'multiple' ? true : undefined\"\n tabindex=\"0\"\n @keydown=\"onKeydown\"\n >\n <li\n v-for=\"(row, index) in rows\"\n :key=\"row.node.key\"\n class=\"rk-tree-row\"\n :class=\"{\n 'is-active': index === active,\n 'is-chosen': chosen.has(row.node.key),\n 'is-disabled': row.node.disabled,\n }\"\n role=\"treeitem\"\n :aria-level=\"row.level\"\n :aria-setsize=\"row.size\"\n :aria-posinset=\"row.position\"\n :aria-expanded=\"row.node.children?.length ? isOpen(row.node) : undefined\"\n :aria-selected=\"chosen.has(row.node.key)\"\n :aria-disabled=\"row.node.disabled || undefined\"\n :style=\"{ '--rk-tree-level': row.level }\"\n @click=\"onClick(index, row)\"\n >\n <ChevronRight\n v-if=\"row.node.children?.length\"\n class=\"rk-tree-twist\"\n :class=\"{ 'is-open': isOpen(row.node) }\"\n aria-hidden=\"true\"\n />\n <span v-else class=\"rk-tree-twist-space\" aria-hidden=\"true\" />\n\n <component\n :is=\"row.node.icon\"\n v-if=\"row.node.icon\"\n class=\"size-4 shrink-0\"\n aria-hidden=\"true\"\n />\n <span class=\"truncate\">{{ row.node.label }}</span>\n </li>\n </ul>\n</template>\n\n<style scoped>\n.rk-tree {\n display: flex;\n flex-direction: column;\n gap: 1px;\n border-radius: var(--radius-card);\n}\n\n.rk-tree:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n.rk-tree-row {\n display: flex;\n cursor: pointer;\n align-items: center;\n gap: 0.375rem;\n border-radius: var(--radius-cell);\n /* Indented by level, so a branch reads as being inside its parent. */\n padding: 0.3125rem 0.5rem 0.3125rem calc(0.5rem + (var(--rk-tree-level) - 1) * 1rem);\n font-size: 0.875rem;\n color: var(--color-ink);\n}\n\n.rk-tree-row.is-active {\n background: var(--color-muted);\n}\n\n.rk-tree-row.is-chosen {\n color: var(--color-primary);\n font-weight: 500;\n}\n\n.rk-tree-row.is-chosen.is-active {\n background: color-mix(in oklab, var(--color-primary) 12%, transparent);\n}\n\n.rk-tree-row.is-disabled {\n cursor: not-allowed;\n opacity: 0.45;\n}\n\n.rk-tree-twist,\n.rk-tree-twist-space {\n width: 1rem;\n height: 1rem;\n flex-shrink: 0;\n}\n\n.rk-tree-twist {\n color: var(--color-ink-soft);\n transition: rotate var(--duration-fast) var(--ease-standard);\n}\n\n.rk-tree-twist.is-open {\n rotate: 90deg;\n}\n</style>\n","<script setup lang=\"ts\" generic=\"V extends string\">\nimport { ChevronLeft, ChevronRight } from 'lucide-vue-next'\nimport { computed, ref } from 'vue'\nimport type { Ref } from 'vue'\n\nimport BaseButton from '../components/BaseButton.vue'\nimport BaseListbox from '../components/BaseListbox.vue'\nimport type { ListboxOption } from '../components/BaseListbox.vue'\n\n/**\n * Two lists and the way between them: what is available, what is chosen.\n *\n * The shape people know from permissions and from building a playlist. It\n * is `BaseListbox` twice with two buttons between, which is also why the\n * keyboard works: each side is one Tab stop with arrows inside it, and the\n * buttons say how many are about to move rather than only \"→\".\n *\n * The chosen side keeps the order things were added in, not the order of\n * the source, because that order is usually the point.\n */\nconst {\n options,\n availableLabel,\n chosenLabel,\n addLabel,\n removeLabel,\n height = '14rem',\n} = defineProps<{\n options: readonly ListboxOption<V>[]\n /** Heading over the left list, e.g. \"Available\". Already translated. */\n availableLabel: string\n /** Heading over the right list. */\n chosenLabel: string\n /** The → button's accessible name, e.g. \"Add the chosen ones\". */\n addLabel: string\n /** The ← button's accessible name. */\n removeLabel: string\n height?: string | undefined\n}>()\n\n/** What has been moved across, in the order it was moved. `v-model`. */\nconst model = defineModel<V[]>({ default: () => [] })\n\n/* `as Ref<V[]>`: `ref` unwraps a generic array to `UnwrapRefSimple`, which\n is the same list to a reader and a different type to the compiler. */\nconst markedAvailable = ref([]) as Ref<V[]>\nconst markedChosen = ref([]) as Ref<V[]>\n\nconst available = computed(() => options.filter((option) => !model.value.includes(option.value)))\nconst chosen = computed(() =>\n model.value\n .map((value) => options.find((option) => option.value === value))\n .filter((option): option is ListboxOption<V> => option !== undefined),\n)\n\nfunction add() {\n model.value = [...model.value, ...markedAvailable.value]\n markedAvailable.value = []\n}\n\nfunction remove() {\n model.value = model.value.filter((value) => !markedChosen.value.includes(value))\n markedChosen.value = []\n}\n</script>\n\n<template>\n <div class=\"rk-transfer\">\n <div class=\"rk-transfer-side\">\n <p class=\"rk-transfer-title\">{{ availableLabel }}</p>\n <BaseListbox\n v-model=\"markedAvailable\"\n mode=\"multiple\"\n :options=\"available\"\n :label=\"availableLabel\"\n :height=\"height\"\n />\n </div>\n\n <div class=\"rk-transfer-middle\">\n <BaseButton\n icon\n variant=\"secondary\"\n :aria-label=\"addLabel\"\n :disabled=\"markedAvailable.length === 0\"\n @click=\"add\"\n >\n <ChevronRight class=\"size-4\" />\n </BaseButton>\n <BaseButton\n icon\n variant=\"secondary\"\n :aria-label=\"removeLabel\"\n :disabled=\"markedChosen.length === 0\"\n @click=\"remove\"\n >\n <ChevronLeft class=\"size-4\" />\n </BaseButton>\n </div>\n\n <div class=\"rk-transfer-side\">\n <p class=\"rk-transfer-title\">{{ chosenLabel }}</p>\n <BaseListbox\n v-model=\"markedChosen\"\n mode=\"multiple\"\n :options=\"chosen\"\n :label=\"chosenLabel\"\n :height=\"height\"\n />\n </div>\n </div>\n</template>\n\n<style scoped>\n.rk-transfer {\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);\n align-items: center;\n gap: 0.75rem;\n}\n\n.rk-transfer-side {\n min-width: 0;\n}\n\n.rk-transfer-title {\n margin-bottom: 0.375rem;\n font-size: 0.75rem;\n font-weight: 600;\n color: var(--color-ink-soft);\n}\n\n.rk-transfer-middle {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n/* On a phone the two lists stack and the buttons turn to face them. */\n@media (max-width: 40rem) {\n .rk-transfer {\n grid-template-columns: minmax(0, 1fr);\n }\n\n .rk-transfer-middle {\n flex-direction: row;\n justify-content: center;\n rotate: 90deg;\n }\n}\n</style>\n","<script setup lang=\"ts\" generic=\"V extends string\">\nimport { ChevronLeft, ChevronRight } from 'lucide-vue-next'\nimport { computed, ref } from 'vue'\nimport type { Ref } from 'vue'\n\nimport BaseButton from '../components/BaseButton.vue'\nimport BaseListbox from '../components/BaseListbox.vue'\nimport type { ListboxOption } from '../components/BaseListbox.vue'\n\n/**\n * Two lists and the way between them: what is available, what is chosen.\n *\n * The shape people know from permissions and from building a playlist. It\n * is `BaseListbox` twice with two buttons between, which is also why the\n * keyboard works: each side is one Tab stop with arrows inside it, and the\n * buttons say how many are about to move rather than only \"→\".\n *\n * The chosen side keeps the order things were added in, not the order of\n * the source, because that order is usually the point.\n */\nconst {\n options,\n availableLabel,\n chosenLabel,\n addLabel,\n removeLabel,\n height = '14rem',\n} = defineProps<{\n options: readonly ListboxOption<V>[]\n /** Heading over the left list, e.g. \"Available\". Already translated. */\n availableLabel: string\n /** Heading over the right list. */\n chosenLabel: string\n /** The → button's accessible name, e.g. \"Add the chosen ones\". */\n addLabel: string\n /** The ← button's accessible name. */\n removeLabel: string\n height?: string | undefined\n}>()\n\n/** What has been moved across, in the order it was moved. `v-model`. */\nconst model = defineModel<V[]>({ default: () => [] })\n\n/* `as Ref<V[]>`: `ref` unwraps a generic array to `UnwrapRefSimple`, which\n is the same list to a reader and a different type to the compiler. */\nconst markedAvailable = ref([]) as Ref<V[]>\nconst markedChosen = ref([]) as Ref<V[]>\n\nconst available = computed(() => options.filter((option) => !model.value.includes(option.value)))\nconst chosen = computed(() =>\n model.value\n .map((value) => options.find((option) => option.value === value))\n .filter((option): option is ListboxOption<V> => option !== undefined),\n)\n\nfunction add() {\n model.value = [...model.value, ...markedAvailable.value]\n markedAvailable.value = []\n}\n\nfunction remove() {\n model.value = model.value.filter((value) => !markedChosen.value.includes(value))\n markedChosen.value = []\n}\n</script>\n\n<template>\n <div class=\"rk-transfer\">\n <div class=\"rk-transfer-side\">\n <p class=\"rk-transfer-title\">{{ availableLabel }}</p>\n <BaseListbox\n v-model=\"markedAvailable\"\n mode=\"multiple\"\n :options=\"available\"\n :label=\"availableLabel\"\n :height=\"height\"\n />\n </div>\n\n <div class=\"rk-transfer-middle\">\n <BaseButton\n icon\n variant=\"secondary\"\n :aria-label=\"addLabel\"\n :disabled=\"markedAvailable.length === 0\"\n @click=\"add\"\n >\n <ChevronRight class=\"size-4\" />\n </BaseButton>\n <BaseButton\n icon\n variant=\"secondary\"\n :aria-label=\"removeLabel\"\n :disabled=\"markedChosen.length === 0\"\n @click=\"remove\"\n >\n <ChevronLeft class=\"size-4\" />\n </BaseButton>\n </div>\n\n <div class=\"rk-transfer-side\">\n <p class=\"rk-transfer-title\">{{ chosenLabel }}</p>\n <BaseListbox\n v-model=\"markedChosen\"\n mode=\"multiple\"\n :options=\"chosen\"\n :label=\"chosenLabel\"\n :height=\"height\"\n />\n </div>\n </div>\n</template>\n\n<style scoped>\n.rk-transfer {\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);\n align-items: center;\n gap: 0.75rem;\n}\n\n.rk-transfer-side {\n min-width: 0;\n}\n\n.rk-transfer-title {\n margin-bottom: 0.375rem;\n font-size: 0.75rem;\n font-weight: 600;\n color: var(--color-ink-soft);\n}\n\n.rk-transfer-middle {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n/* On a phone the two lists stack and the buttons turn to face them. */\n@media (max-width: 40rem) {\n .rk-transfer {\n grid-template-columns: minmax(0, 1fr);\n }\n\n .rk-transfer-middle {\n flex-direction: row;\n justify-content: center;\n rotate: 90deg;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nimport BaseSheet from '../components/BaseSheet.vue'\nimport { useMediaQuery } from '../composables/use-media-query'\nimport BaseModal from './BaseModal.vue'\n\n/**\n * One question, asked the way the screen it lands on expects: a modal on a\n * wide screen, a sheet from the bottom edge on a phone.\n *\n * The two are the same thing at different sizes, and apps keep writing both\n * and choosing between them by hand — which is two markups, two sets of\n * labels and one of them usually left behind.\n *\n * The query is read after mount rather than during render: a server has no\n * screen, and a layout guessed there and swapped on hydration is a page that\n * jumps. Until it is known, it is the sheet — the narrow answer, because the\n * narrow one fits on both.\n */\nconst {\n title,\n closeLabel,\n subtitle = '',\n tone = 'default',\n dismissible = true,\n breakpoint = '48rem',\n} = defineProps<{\n /** The dialog's heading, and its accessible name. Already translated. */\n title: string\n /** Accessible name of the close button. */\n closeLabel: string\n /** A line under the title, on the sheet. */\n subtitle?: string | undefined\n /** `alert` makes the wide one an `alertdialog`: an answer is required. */\n tone?: 'default' | 'alert' | undefined\n dismissible?: boolean | undefined\n /** Where it turns from a sheet into a modal. Any media width. */\n breakpoint?: string | undefined\n}>()\n\ndefineSlots<{\n /** The body. */\n default: () => unknown\n /** Buttons, at the foot. */\n actions?: () => unknown\n}>()\n\n/** Open or closed, with `v-model`. */\nconst open = defineModel<boolean>({ default: false })\n\nconst wide = useMediaQuery(`(min-width: ${breakpoint})`)\nconst asModal = computed(() => wide.value)\n</script>\n\n<template>\n <BaseModal\n v-if=\"asModal\"\n v-model=\"open\"\n :title=\"title\"\n :close-label=\"closeLabel\"\n :tone=\"tone\"\n :dismissible=\"dismissible\"\n >\n <slot />\n\n <template v-if=\"$slots.actions\" #actions>\n <slot name=\"actions\" />\n </template>\n </BaseModal>\n\n <BaseSheet v-else v-model=\"open\" :title=\"title\" :subtitle=\"subtitle\" :close-label=\"closeLabel\">\n <slot />\n\n <div v-if=\"$slots.actions\" class=\"rk-responsive-actions\">\n <slot name=\"actions\" />\n </div>\n </BaseSheet>\n</template>\n\n<style scoped>\n/* The sheet has no actions slot of its own: on a phone the buttons belong in\n the flow, at the end of what they are about, within reach of a thumb. */\n.rk-responsive-actions {\n display: flex;\n flex-direction: column-reverse;\n gap: 0.5rem;\n padding-top: 1rem;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nimport BaseSheet from '../components/BaseSheet.vue'\nimport { useMediaQuery } from '../composables/use-media-query'\nimport BaseModal from './BaseModal.vue'\n\n/**\n * One question, asked the way the screen it lands on expects: a modal on a\n * wide screen, a sheet from the bottom edge on a phone.\n *\n * The two are the same thing at different sizes, and apps keep writing both\n * and choosing between them by hand — which is two markups, two sets of\n * labels and one of them usually left behind.\n *\n * The query is read after mount rather than during render: a server has no\n * screen, and a layout guessed there and swapped on hydration is a page that\n * jumps. Until it is known, it is the sheet — the narrow answer, because the\n * narrow one fits on both.\n */\nconst {\n title,\n closeLabel,\n subtitle = '',\n tone = 'default',\n dismissible = true,\n breakpoint = '48rem',\n} = defineProps<{\n /** The dialog's heading, and its accessible name. Already translated. */\n title: string\n /** Accessible name of the close button. */\n closeLabel: string\n /** A line under the title, on the sheet. */\n subtitle?: string | undefined\n /** `alert` makes the wide one an `alertdialog`: an answer is required. */\n tone?: 'default' | 'alert' | undefined\n dismissible?: boolean | undefined\n /** Where it turns from a sheet into a modal. Any media width. */\n breakpoint?: string | undefined\n}>()\n\ndefineSlots<{\n /** The body. */\n default: () => unknown\n /** Buttons, at the foot. */\n actions?: () => unknown\n}>()\n\n/** Open or closed, with `v-model`. */\nconst open = defineModel<boolean>({ default: false })\n\nconst wide = useMediaQuery(`(min-width: ${breakpoint})`)\nconst asModal = computed(() => wide.value)\n</script>\n\n<template>\n <BaseModal\n v-if=\"asModal\"\n v-model=\"open\"\n :title=\"title\"\n :close-label=\"closeLabel\"\n :tone=\"tone\"\n :dismissible=\"dismissible\"\n >\n <slot />\n\n <template v-if=\"$slots.actions\" #actions>\n <slot name=\"actions\" />\n </template>\n </BaseModal>\n\n <BaseSheet v-else v-model=\"open\" :title=\"title\" :subtitle=\"subtitle\" :close-label=\"closeLabel\">\n <slot />\n\n <div v-if=\"$slots.actions\" class=\"rk-responsive-actions\">\n <slot name=\"actions\" />\n </div>\n </BaseSheet>\n</template>\n\n<style scoped>\n/* The sheet has no actions slot of its own: on a phone the buttons belong in\n the flow, at the end of what they are about, within reach of a thumb. */\n.rk-responsive-actions {\n display: flex;\n flex-direction: column-reverse;\n gap: 0.5rem;\n padding-top: 1rem;\n}\n</style>\n","<script setup lang=\"ts\" generic=\"K extends string\">\nimport { ref, useId } from 'vue'\n\nexport interface TabPanel<K extends string> {\n key: K\n /** Already translated. */\n label: string\n}\n\n/**\n * Sections of one page, one visible at a time.\n *\n * Not `TabBar` and not `NavLinks`: those navigate, and the browser's back\n * button undoes them. These switch a panel inside a page that does not change,\n * so there is no history entry and no route.\n *\n * ## The keyboard, which is the whole reason this is a component\n *\n * The pattern is prescribed and it is not what a row of buttons does by\n * default. Tab enters the tablist once and leaves it once — it does not walk\n * through every tab — and the arrow keys move between them. That is roving\n * tabindex: exactly one tab is reachable by Tab at a time, and moving the\n * selection moves it. Home and End jump to the ends, and the selection wraps,\n * because a list with no edges is faster than one you can fall off.\n *\n * Hand-written tabs almost always get this wrong in the same way: every tab is\n * tabbable, so a keyboard user presses Tab six times to reach the content.\n */\nconst { items, label = '' } = defineProps<{\n items: readonly TabPanel<K>[]\n /** Accessible name for the tablist. */\n label?: string | undefined\n}>()\n\ndefineSlots<{\n /** The panel for the selected tab. */\n default: (props: { item: TabPanel<K>; active: K }) => unknown\n}>()\n\nconst active = defineModel<K>({ required: true })\n\nconst uid = useId()\nconst tabs = ref<HTMLElement[]>([])\n\nconst tabId = (key: K) => `${uid}-tab-${key}`\nconst panelId = (key: K) => `${uid}-panel-${key}`\n\nfunction select(key: K, focus = false) {\n active.value = key\n if (!focus) return\n\n // Focus follows selection, which is what the arrow keys are for. The element\n // is looked up after the update so the new tab is the one that is reachable.\n const index = items.findIndex((item) => item.key === key)\n requestAnimationFrame(() => tabs.value[index]?.focus())\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n const index = items.findIndex((item) => item.key === active.value)\n if (index < 0) return\n\n const last = items.length - 1\n let next: number | undefined\n\n if (event.key === 'ArrowRight') next = index === last ? 0 : index + 1\n else if (event.key === 'ArrowLeft') next = index === 0 ? last : index - 1\n else if (event.key === 'Home') next = 0\n else if (event.key === 'End') next = last\n else return\n\n event.preventDefault()\n const item = items[next]\n if (item) select(item.key, true)\n}\n</script>\n\n<template>\n <div>\n <div class=\"rk-tabs\" role=\"tablist\" :aria-label=\"label || undefined\" @keydown=\"onKeydown\">\n <button\n v-for=\"item in items\"\n :id=\"tabId(item.key)\"\n :key=\"item.key\"\n ref=\"tabs\"\n type=\"button\"\n role=\"tab\"\n class=\"rk-tab\"\n :class=\"{ 'is-active': item.key === active }\"\n :aria-selected=\"item.key === active\"\n :aria-controls=\"panelId(item.key)\"\n :tabindex=\"item.key === active ? 0 : -1\"\n @click=\"select(item.key)\"\n >\n {{ item.label }}\n </button>\n </div>\n\n <div\n v-for=\"item in items\"\n v-show=\"item.key === active\"\n :id=\"panelId(item.key)\"\n :key=\"item.key\"\n role=\"tabpanel\"\n class=\"rk-tabpanel\"\n :aria-labelledby=\"tabId(item.key)\"\n tabindex=\"0\"\n >\n <!-- Every panel is rendered and hidden rather than swapped, so the page\n keeps its height and a crawler sees all of it. -->\n <slot :item=\"item\" :active=\"active\" />\n </div>\n </div>\n</template>\n\n<style scoped>\n.rk-tabs {\n display: flex;\n align-items: center;\n gap: 0.25rem;\n border-bottom: 1px solid color-mix(in srgb, var(--color-hair) 70%, transparent);\n}\n\n.rk-tab {\n position: relative;\n padding: 0.625rem 0.875rem;\n font-size: 0.875rem;\n color: var(--color-ink-soft);\n transition: color var(--duration-fast);\n}\n\n.rk-tab:hover {\n color: var(--color-ink);\n}\n\n.rk-tab:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: -2px;\n border-radius: var(--radius-cell);\n}\n\n.rk-tab.is-active {\n color: var(--color-ink);\n font-weight: 500;\n}\n\n.rk-tab.is-active::after {\n content: '';\n position: absolute;\n inset-inline: 0.5rem;\n bottom: -1px;\n height: 2px;\n border-radius: 9999px;\n background: var(--color-primary);\n}\n\n.rk-tabpanel {\n padding-top: 1rem;\n}\n\n.rk-tabpanel:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n</style>\n","<script setup lang=\"ts\" generic=\"K extends string\">\nimport { ref, useId } from 'vue'\n\nexport interface TabPanel<K extends string> {\n key: K\n /** Already translated. */\n label: string\n}\n\n/**\n * Sections of one page, one visible at a time.\n *\n * Not `TabBar` and not `NavLinks`: those navigate, and the browser's back\n * button undoes them. These switch a panel inside a page that does not change,\n * so there is no history entry and no route.\n *\n * ## The keyboard, which is the whole reason this is a component\n *\n * The pattern is prescribed and it is not what a row of buttons does by\n * default. Tab enters the tablist once and leaves it once — it does not walk\n * through every tab — and the arrow keys move between them. That is roving\n * tabindex: exactly one tab is reachable by Tab at a time, and moving the\n * selection moves it. Home and End jump to the ends, and the selection wraps,\n * because a list with no edges is faster than one you can fall off.\n *\n * Hand-written tabs almost always get this wrong in the same way: every tab is\n * tabbable, so a keyboard user presses Tab six times to reach the content.\n */\nconst { items, label = '' } = defineProps<{\n items: readonly TabPanel<K>[]\n /** Accessible name for the tablist. */\n label?: string | undefined\n}>()\n\ndefineSlots<{\n /** The panel for the selected tab. */\n default: (props: { item: TabPanel<K>; active: K }) => unknown\n}>()\n\nconst active = defineModel<K>({ required: true })\n\nconst uid = useId()\nconst tabs = ref<HTMLElement[]>([])\n\nconst tabId = (key: K) => `${uid}-tab-${key}`\nconst panelId = (key: K) => `${uid}-panel-${key}`\n\nfunction select(key: K, focus = false) {\n active.value = key\n if (!focus) return\n\n // Focus follows selection, which is what the arrow keys are for. The element\n // is looked up after the update so the new tab is the one that is reachable.\n const index = items.findIndex((item) => item.key === key)\n requestAnimationFrame(() => tabs.value[index]?.focus())\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n const index = items.findIndex((item) => item.key === active.value)\n if (index < 0) return\n\n const last = items.length - 1\n let next: number | undefined\n\n if (event.key === 'ArrowRight') next = index === last ? 0 : index + 1\n else if (event.key === 'ArrowLeft') next = index === 0 ? last : index - 1\n else if (event.key === 'Home') next = 0\n else if (event.key === 'End') next = last\n else return\n\n event.preventDefault()\n const item = items[next]\n if (item) select(item.key, true)\n}\n</script>\n\n<template>\n <div>\n <div class=\"rk-tabs\" role=\"tablist\" :aria-label=\"label || undefined\" @keydown=\"onKeydown\">\n <button\n v-for=\"item in items\"\n :id=\"tabId(item.key)\"\n :key=\"item.key\"\n ref=\"tabs\"\n type=\"button\"\n role=\"tab\"\n class=\"rk-tab\"\n :class=\"{ 'is-active': item.key === active }\"\n :aria-selected=\"item.key === active\"\n :aria-controls=\"panelId(item.key)\"\n :tabindex=\"item.key === active ? 0 : -1\"\n @click=\"select(item.key)\"\n >\n {{ item.label }}\n </button>\n </div>\n\n <div\n v-for=\"item in items\"\n v-show=\"item.key === active\"\n :id=\"panelId(item.key)\"\n :key=\"item.key\"\n role=\"tabpanel\"\n class=\"rk-tabpanel\"\n :aria-labelledby=\"tabId(item.key)\"\n tabindex=\"0\"\n >\n <!-- Every panel is rendered and hidden rather than swapped, so the page\n keeps its height and a crawler sees all of it. -->\n <slot :item=\"item\" :active=\"active\" />\n </div>\n </div>\n</template>\n\n<style scoped>\n.rk-tabs {\n display: flex;\n align-items: center;\n gap: 0.25rem;\n border-bottom: 1px solid color-mix(in srgb, var(--color-hair) 70%, transparent);\n}\n\n.rk-tab {\n position: relative;\n padding: 0.625rem 0.875rem;\n font-size: 0.875rem;\n color: var(--color-ink-soft);\n transition: color var(--duration-fast);\n}\n\n.rk-tab:hover {\n color: var(--color-ink);\n}\n\n.rk-tab:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: -2px;\n border-radius: var(--radius-cell);\n}\n\n.rk-tab.is-active {\n color: var(--color-ink);\n font-weight: 500;\n}\n\n.rk-tab.is-active::after {\n content: '';\n position: absolute;\n inset-inline: 0.5rem;\n bottom: -1px;\n height: 2px;\n border-radius: 9999px;\n background: var(--color-primary);\n}\n\n.rk-tabpanel {\n padding-top: 1rem;\n}\n\n.rk-tabpanel:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n</style>\n","<script setup lang=\"ts\">\nimport { useId } from 'vue'\n\n/**\n * A short label that appears beside a control.\n *\n * ## CSS, not JavaScript, and that is a decision\n *\n * The bubble is shown by `:hover` and `:focus-within` in the stylesheet. No\n * listener, no state, no positioning library. That costs collision detection —\n * a tooltip near the right edge is clipped rather than flipped — and buys the\n * only thing that matters more: it works in a prerendered page, before the\n * bundle has arrived, and with JavaScript switched off entirely.\n *\n * `:focus-within` rather than `:hover` alone is the part people leave out. A\n * tooltip that only answers a mouse is a tooltip that does not exist for anyone\n * using a keyboard, and the content is usually the only explanation of what the\n * control does.\n *\n * The bubble is wired with `aria-describedby`, so a screen reader reads it as a\n * description of the control rather than as loose text nearby. That is why the\n * trigger goes in a slot: the component has to own the id on both ends.\n *\n * ## When not to use it\n *\n * For anything the reader must have. A tooltip is unreachable on a touch screen\n * without a hover state, so text that changes a decision belongs on the page.\n */\nconst { label, placement = 'top' } = defineProps<{\n /** The description. Already translated. */\n label: string\n placement?: 'top' | 'bottom' | undefined\n}>()\n\ndefineSlots<{\n /** The control being described. */\n default: (props: { describedBy: string }) => unknown\n}>()\n\nconst id = useId()\n</script>\n\n<template>\n <span class=\"rk-tip\">\n <slot :described-by=\"id\" />\n\n <!-- `role=\"tooltip\"` and the id, so the control can point at it. Not\n `aria-hidden`: it is the description, not decoration. -->\n <span :id=\"id\" role=\"tooltip\" class=\"rk-tip-bubble\" :class=\"`is-${placement}`\">\n {{ label }}\n </span>\n </span>\n</template>\n\n<style scoped>\n.rk-tip {\n position: relative;\n display: inline-flex;\n}\n\n.rk-tip-bubble {\n position: absolute;\n left: 50%;\n z-index: 60;\n width: max-content;\n max-width: 16rem;\n transform: translateX(-50%);\n border-radius: var(--radius-cell);\n background: var(--color-ink);\n padding: 0.375rem 0.5rem;\n font-size: 0.75rem;\n line-height: 1.4;\n color: var(--color-canvas);\n opacity: 0;\n /* Out of the hit area while hidden, or it would swallow the pointer on its\n way to the control it describes. */\n pointer-events: none;\n transition: opacity var(--duration-fast) ease;\n}\n\n.rk-tip-bubble.is-top {\n bottom: calc(100% + 0.375rem);\n}\n\n.rk-tip-bubble.is-bottom {\n top: calc(100% + 0.375rem);\n}\n\n.rk-tip:hover .rk-tip-bubble,\n.rk-tip:focus-within .rk-tip-bubble {\n opacity: 1;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .rk-tip-bubble {\n transition: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { useId } from 'vue'\n\n/**\n * A short label that appears beside a control.\n *\n * ## CSS, not JavaScript, and that is a decision\n *\n * The bubble is shown by `:hover` and `:focus-within` in the stylesheet. No\n * listener, no state, no positioning library. That costs collision detection —\n * a tooltip near the right edge is clipped rather than flipped — and buys the\n * only thing that matters more: it works in a prerendered page, before the\n * bundle has arrived, and with JavaScript switched off entirely.\n *\n * `:focus-within` rather than `:hover` alone is the part people leave out. A\n * tooltip that only answers a mouse is a tooltip that does not exist for anyone\n * using a keyboard, and the content is usually the only explanation of what the\n * control does.\n *\n * The bubble is wired with `aria-describedby`, so a screen reader reads it as a\n * description of the control rather than as loose text nearby. That is why the\n * trigger goes in a slot: the component has to own the id on both ends.\n *\n * ## When not to use it\n *\n * For anything the reader must have. A tooltip is unreachable on a touch screen\n * without a hover state, so text that changes a decision belongs on the page.\n */\nconst { label, placement = 'top' } = defineProps<{\n /** The description. Already translated. */\n label: string\n placement?: 'top' | 'bottom' | undefined\n}>()\n\ndefineSlots<{\n /** The control being described. */\n default: (props: { describedBy: string }) => unknown\n}>()\n\nconst id = useId()\n</script>\n\n<template>\n <span class=\"rk-tip\">\n <slot :described-by=\"id\" />\n\n <!-- `role=\"tooltip\"` and the id, so the control can point at it. Not\n `aria-hidden`: it is the description, not decoration. -->\n <span :id=\"id\" role=\"tooltip\" class=\"rk-tip-bubble\" :class=\"`is-${placement}`\">\n {{ label }}\n </span>\n </span>\n</template>\n\n<style scoped>\n.rk-tip {\n position: relative;\n display: inline-flex;\n}\n\n.rk-tip-bubble {\n position: absolute;\n left: 50%;\n z-index: 60;\n width: max-content;\n max-width: 16rem;\n transform: translateX(-50%);\n border-radius: var(--radius-cell);\n background: var(--color-ink);\n padding: 0.375rem 0.5rem;\n font-size: 0.75rem;\n line-height: 1.4;\n color: var(--color-canvas);\n opacity: 0;\n /* Out of the hit area while hidden, or it would swallow the pointer on its\n way to the control it describes. */\n pointer-events: none;\n transition: opacity var(--duration-fast) ease;\n}\n\n.rk-tip-bubble.is-top {\n bottom: calc(100% + 0.375rem);\n}\n\n.rk-tip-bubble.is-bottom {\n top: calc(100% + 0.375rem);\n}\n\n.rk-tip:hover .rk-tip-bubble,\n.rk-tip:focus-within .rk-tip-bubble {\n opacity: 1;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .rk-tip-bubble {\n transition: none;\n }\n}\n</style>\n","<script setup lang=\"ts\" generic=\"K extends string\">\nimport { RouterLink } from 'vue-router'\n\nexport interface NavLinkItem<K extends string> {\n /** Identity, compared against `active`. */\n key: K\n /** Router destination. */\n to: string\n /** Already translated. */\n label: string\n}\n\n/**\n * The primary navigation of a wide site.\n *\n * The same contract as `TabBar` minus the icon, so moving an app between a\n * bottom bar and a top one is a change of component and not of data. That is\n * the whole reason the shapes match.\n *\n * A rule under the current section rather than a filled pill: at this size a\n * pill reads as a button, and these are not buttons — they are where you are.\n * The rule is a scaled element rather than a border so it can travel, and it is\n * `aria-hidden` because `aria-current` already says the same thing to anyone not\n * looking at it.\n */\nconst {\n items,\n active,\n label = '',\n} = defineProps<{\n items: readonly NavLinkItem<K>[]\n /** Which item is current. Usually derived from the route. */\n active?: K | undefined\n /** Accessible name for the navigation landmark. */\n label?: string | undefined\n}>()\n</script>\n\n<template>\n <nav :aria-label=\"label || undefined\">\n <div class=\"rk-nav-inner\">\n <RouterLink\n v-for=\"item in items\"\n :key=\"item.key\"\n :to=\"item.to\"\n class=\"rk-nav-link\"\n :class=\"{ 'is-active': item.key === active }\"\n :aria-current=\"item.key === active ? 'page' : undefined\"\n >\n {{ item.label }}\n <span class=\"rk-nav-rule\" aria-hidden=\"true\" />\n </RouterLink>\n </div>\n </nav>\n</template>\n\n<style scoped>\n/* The root carries no layout of its own, and that is the fix for a real bug\n rather than a style preference.\n\n A rule on the root loses to nothing: Vue's scoped CSS compiles `.rk-nav` to\n `.rk-nav[data-v-hash]`, which outranks a utility class like Tailwind's\n `hidden` outright — so `class=\"hidden sm:flex\"` did nothing and the nav\n appeared on a phone, on top of the call to action. `:where()` does not save\n it either; the scoping attribute puts the specificity back.\n\n So the flex row lives on an element the caller does not own, and the root is\n left for them to style. `TabBar` is built the same way for the same reason. */\n.rk-nav-inner {\n display: flex;\n align-items: center;\n gap: 0.25rem;\n}\n\n.rk-nav-link {\n position: relative;\n border-radius: var(--radius-cell);\n padding: 0.5rem 0.75rem;\n font-size: 0.875rem;\n color: var(--color-ink-soft);\n transition: color var(--duration-fast);\n}\n\n.rk-nav-link:hover {\n color: var(--color-ink);\n}\n\n.rk-nav-link:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n.rk-nav-link.is-active {\n color: var(--color-ink);\n font-weight: 500;\n}\n\n.rk-nav-rule {\n position: absolute;\n inset-inline: 0.75rem;\n bottom: -1px;\n height: 2px;\n border-radius: 9999px;\n background: var(--color-primary);\n transform: scaleX(0);\n transform-origin: left;\n transition: transform var(--duration-slow);\n}\n\n.rk-nav-link.is-active .rk-nav-rule {\n transform: scaleX(1);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .rk-nav-rule {\n transition: none;\n }\n}\n</style>\n","<script setup lang=\"ts\" generic=\"K extends string\">\nimport { RouterLink } from 'vue-router'\n\nexport interface NavLinkItem<K extends string> {\n /** Identity, compared against `active`. */\n key: K\n /** Router destination. */\n to: string\n /** Already translated. */\n label: string\n}\n\n/**\n * The primary navigation of a wide site.\n *\n * The same contract as `TabBar` minus the icon, so moving an app between a\n * bottom bar and a top one is a change of component and not of data. That is\n * the whole reason the shapes match.\n *\n * A rule under the current section rather than a filled pill: at this size a\n * pill reads as a button, and these are not buttons — they are where you are.\n * The rule is a scaled element rather than a border so it can travel, and it is\n * `aria-hidden` because `aria-current` already says the same thing to anyone not\n * looking at it.\n */\nconst {\n items,\n active,\n label = '',\n} = defineProps<{\n items: readonly NavLinkItem<K>[]\n /** Which item is current. Usually derived from the route. */\n active?: K | undefined\n /** Accessible name for the navigation landmark. */\n label?: string | undefined\n}>()\n</script>\n\n<template>\n <nav :aria-label=\"label || undefined\">\n <div class=\"rk-nav-inner\">\n <RouterLink\n v-for=\"item in items\"\n :key=\"item.key\"\n :to=\"item.to\"\n class=\"rk-nav-link\"\n :class=\"{ 'is-active': item.key === active }\"\n :aria-current=\"item.key === active ? 'page' : undefined\"\n >\n {{ item.label }}\n <span class=\"rk-nav-rule\" aria-hidden=\"true\" />\n </RouterLink>\n </div>\n </nav>\n</template>\n\n<style scoped>\n/* The root carries no layout of its own, and that is the fix for a real bug\n rather than a style preference.\n\n A rule on the root loses to nothing: Vue's scoped CSS compiles `.rk-nav` to\n `.rk-nav[data-v-hash]`, which outranks a utility class like Tailwind's\n `hidden` outright — so `class=\"hidden sm:flex\"` did nothing and the nav\n appeared on a phone, on top of the call to action. `:where()` does not save\n it either; the scoping attribute puts the specificity back.\n\n So the flex row lives on an element the caller does not own, and the root is\n left for them to style. `TabBar` is built the same way for the same reason. */\n.rk-nav-inner {\n display: flex;\n align-items: center;\n gap: 0.25rem;\n}\n\n.rk-nav-link {\n position: relative;\n border-radius: var(--radius-cell);\n padding: 0.5rem 0.75rem;\n font-size: 0.875rem;\n color: var(--color-ink-soft);\n transition: color var(--duration-fast);\n}\n\n.rk-nav-link:hover {\n color: var(--color-ink);\n}\n\n.rk-nav-link:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 2px;\n}\n\n.rk-nav-link.is-active {\n color: var(--color-ink);\n font-weight: 500;\n}\n\n.rk-nav-rule {\n position: absolute;\n inset-inline: 0.75rem;\n bottom: -1px;\n height: 2px;\n border-radius: 9999px;\n background: var(--color-primary);\n transform: scaleX(0);\n transform-origin: left;\n transition: transform var(--duration-slow);\n}\n\n.rk-nav-link.is-active .rk-nav-rule {\n transform: scaleX(1);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .rk-nav-rule {\n transition: none;\n }\n}\n</style>\n"],"mappings":"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"}