rei-kit 2.12.0 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -124,7 +124,7 @@ Each claim here is enforced by something that fails, not by a promise.
124
124
 
125
125
  ## Status
126
126
 
127
- **v2.12.0 — three consumers.**
127
+ **v2.13.0 — three consumers.**
128
128
 
129
129
  | | |
130
130
  | ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -39,6 +39,43 @@ function useMediaQuery(query) {
39
39
  return matches;
40
40
  }
41
41
  //#endregion
42
+ //#region src/components/BaseSkeleton.vue
43
+ var BaseSkeleton_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__ */ defineComponent({
44
+ __name: "BaseSkeleton",
45
+ props: {
46
+ width: { default: "100%" },
47
+ height: { default: "1rem" },
48
+ shape: { default: "block" }
49
+ },
50
+ setup(__props) {
51
+ /**
52
+ * One grey box standing in for content that has not arrived.
53
+ *
54
+ * `SkeletonList` is rows of these for a list; this is the primitive, for the
55
+ * shapes a list does not cover — an avatar, a heading, a chart.
56
+ *
57
+ * A height is a CSS length, never a class: `h-4` inside a component the app
58
+ * does not control would render at zero the day that utility is not in the
59
+ * app's stylesheet, which is exactly how every skeleton in one app came out
60
+ * invisible.
61
+ *
62
+ * It is hidden from assistive tech. The thing that is loading says so —
63
+ * through `aria-busy`, or a status message — and a screen reader reading
64
+ * "blank, blank, blank" helps nobody.
65
+ */
66
+ return (_ctx, _cache) => {
67
+ return openBlock(), createElementBlock("span", {
68
+ class: normalizeClass(["rk-skeleton", `is-${__props.shape}`]),
69
+ style: normalizeStyle({
70
+ width: __props.shape === "circle" ? __props.height : __props.width,
71
+ height: __props.height
72
+ }),
73
+ "aria-hidden": "true"
74
+ }, null, 6);
75
+ };
76
+ }
77
+ }), [["__scopeId", "data-v-37327de6"]]);
78
+ //#endregion
42
79
  //#region src/components/BaseListbox.vue?vue&type=script&setup=true&lang.ts
43
80
  var _hoisted_1 = [
44
81
  "aria-label",
@@ -166,43 +203,6 @@ var BaseListbox_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__
166
203
  }
167
204
  }), [["__scopeId", "data-v-7f8cd788"]]);
168
205
  //#endregion
169
- //#region src/components/BaseSkeleton.vue
170
- var BaseSkeleton_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__ */ defineComponent({
171
- __name: "BaseSkeleton",
172
- props: {
173
- width: { default: "100%" },
174
- height: { default: "1rem" },
175
- shape: { default: "block" }
176
- },
177
- setup(__props) {
178
- /**
179
- * One grey box standing in for content that has not arrived.
180
- *
181
- * `SkeletonList` is rows of these for a list; this is the primitive, for the
182
- * shapes a list does not cover — an avatar, a heading, a chart.
183
- *
184
- * A height is a CSS length, never a class: `h-4` inside a component the app
185
- * does not control would render at zero the day that utility is not in the
186
- * app's stylesheet, which is exactly how every skeleton in one app came out
187
- * invisible.
188
- *
189
- * It is hidden from assistive tech. The thing that is loading says so —
190
- * through `aria-busy`, or a status message — and a screen reader reading
191
- * "blank, blank, blank" helps nobody.
192
- */
193
- return (_ctx, _cache) => {
194
- return openBlock(), createElementBlock("span", {
195
- class: normalizeClass(["rk-skeleton", `is-${__props.shape}`]),
196
- style: normalizeStyle({
197
- width: __props.shape === "circle" ? __props.height : __props.width,
198
- height: __props.height
199
- }),
200
- "aria-hidden": "true"
201
- }, null, 6);
202
- };
203
- }
204
- }), [["__scopeId", "data-v-37327de6"]]);
205
- //#endregion
206
- export { BaseListbox_default as n, useMediaQuery as r, BaseSkeleton_default as t };
206
+ export { BaseSkeleton_default as n, useMediaQuery as r, BaseListbox_default as t };
207
207
 
208
- //# sourceMappingURL=BaseSkeleton-DnqwogzV.js.map
208
+ //# sourceMappingURL=BaseListbox-BnaHlmMN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BaseListbox-BnaHlmMN.js","names":[],"sources":["../src/composables/use-media-query.ts","../src/components/BaseSkeleton.vue","../src/components/BaseSkeleton.vue","../src/components/BaseListbox.vue","../src/components/BaseListbox.vue"],"sourcesContent":["import { onBeforeUnmount, onMounted, ref } from 'vue'\n\n/**\n * Whether a media query matches, kept up to date.\n *\n * Starts false and resolves on mount, which is deliberate: this is the one\n * place a component is tempted to branch on viewport during render, and doing\n * that under prerendering produces HTML built for a screen the server does not\n * have. Hydration then swaps it and the page jumps. False first, correct a\n * frame later, no jump — and a layout that reads badly at `false` is a layout\n * with a mobile-first bug worth knowing about.\n *\n * Guarded for the server for the same reason the rest of the kit is: this\n * package has to be importable in Node, and `matchMedia` does not exist there.\n *\n * @example\n * ```ts\n * const wide = useMediaQuery('(min-width: 64rem)')\n * ```\n */\nexport function useMediaQuery(query: string) {\n const matches = ref(false)\n\n let list: MediaQueryList | undefined\n\n function update(event: MediaQueryList | MediaQueryListEvent) {\n matches.value = event.matches\n }\n\n onMounted(() => {\n if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return\n\n list = window.matchMedia(query)\n update(list)\n list.addEventListener('change', update)\n })\n\n onBeforeUnmount(() => {\n list?.removeEventListener('change', update)\n })\n\n return matches\n}\n","<script setup lang=\"ts\">\n/**\n * One grey box standing in for content that has not arrived.\n *\n * `SkeletonList` is rows of these for a list; this is the primitive, for the\n * shapes a list does not cover — an avatar, a heading, a chart.\n *\n * A height is a CSS length, never a class: `h-4` inside a component the app\n * does not control would render at zero the day that utility is not in the\n * app's stylesheet, which is exactly how every skeleton in one app came out\n * invisible.\n *\n * It is hidden from assistive tech. The thing that is loading says so —\n * through `aria-busy`, or a status message — and a screen reader reading\n * \"blank, blank, blank\" helps nobody.\n */\nconst {\n width = '100%',\n height = '1rem',\n shape = 'block',\n} = defineProps<{\n /** Any CSS length: `12rem`, `60%`, `8ch`. */\n width?: string | undefined\n height?: string | undefined\n /** `circle` makes a round one and squares the width to the height. */\n shape?: 'block' | 'text' | 'circle' | undefined\n}>()\n</script>\n\n<template>\n <span\n class=\"rk-skeleton\"\n :class=\"`is-${shape}`\"\n :style=\"{ width: shape === 'circle' ? height : width, height }\"\n aria-hidden=\"true\"\n />\n</template>\n\n<style scoped>\n.rk-skeleton {\n display: block;\n flex-shrink: 0;\n background: var(--color-muted);\n animation: rk-skeleton-pulse 1.6s ease-in-out infinite;\n}\n\n.rk-skeleton.is-block {\n border-radius: var(--radius-cell);\n}\n\n/* A line of text, rounded like one and a little short of its box. */\n.rk-skeleton.is-text {\n border-radius: 9999px;\n}\n\n.rk-skeleton.is-circle {\n border-radius: 9999px;\n}\n\n@keyframes rk-skeleton-pulse {\n 50% {\n opacity: 0.55;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .rk-skeleton {\n animation: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * One grey box standing in for content that has not arrived.\n *\n * `SkeletonList` is rows of these for a list; this is the primitive, for the\n * shapes a list does not cover — an avatar, a heading, a chart.\n *\n * A height is a CSS length, never a class: `h-4` inside a component the app\n * does not control would render at zero the day that utility is not in the\n * app's stylesheet, which is exactly how every skeleton in one app came out\n * invisible.\n *\n * It is hidden from assistive tech. The thing that is loading says so —\n * through `aria-busy`, or a status message — and a screen reader reading\n * \"blank, blank, blank\" helps nobody.\n */\nconst {\n width = '100%',\n height = '1rem',\n shape = 'block',\n} = defineProps<{\n /** Any CSS length: `12rem`, `60%`, `8ch`. */\n width?: string | undefined\n height?: string | undefined\n /** `circle` makes a round one and squares the width to the height. */\n shape?: 'block' | 'text' | 'circle' | undefined\n}>()\n</script>\n\n<template>\n <span\n class=\"rk-skeleton\"\n :class=\"`is-${shape}`\"\n :style=\"{ width: shape === 'circle' ? height : width, height }\"\n aria-hidden=\"true\"\n />\n</template>\n\n<style scoped>\n.rk-skeleton {\n display: block;\n flex-shrink: 0;\n background: var(--color-muted);\n animation: rk-skeleton-pulse 1.6s ease-in-out infinite;\n}\n\n.rk-skeleton.is-block {\n border-radius: var(--radius-cell);\n}\n\n/* A line of text, rounded like one and a little short of its box. */\n.rk-skeleton.is-text {\n border-radius: 9999px;\n}\n\n.rk-skeleton.is-circle {\n border-radius: 9999px;\n}\n\n@keyframes rk-skeleton-pulse {\n 50% {\n opacity: 0.55;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .rk-skeleton {\n animation: none;\n }\n}\n</style>\n","<script setup lang=\"ts\" generic=\"V extends string, M extends 'single' | 'multiple' = 'single'\">\nimport { Check } from 'lucide-vue-next'\nimport { computed, ref, useId } from 'vue'\n\nimport { useBoundValue } from '../composables/use-bound-value'\n\nexport interface ListboxOption<V extends string> {\n value: V\n /** Already translated. */\n label: string\n /** A line under the label. */\n description?: string | undefined\n disabled?: boolean | undefined\n}\n\n/**\n * A list you choose from, open on the page.\n *\n * Between `BaseRadioGroup` and `BaseSelect`: a radio group is a handful of\n * options that are all visible and all equal; a select opens the platform's\n * own picker; this is a scrollable list of many, choosing one or several,\n * that stays on screen — a list of accounts, of tags, of files to act on.\n *\n * One Tab stop. The list itself takes focus and `aria-activedescendant`\n * says which option is current, so the arrows move without thirty stops in\n * the tab order. Typing a letter jumps to the next option that starts with\n * it, which is how every native list has always worked and the first thing\n * people try.\n *\n * In `single` mode the selection follows the arrows, because that is what a\n * listbox does; in `multiple` mode Space and Enter toggle the current one.\n */\nconst {\n modelValue = undefined,\n options,\n label,\n mode = 'single' as M,\n height = '14rem',\n} = defineProps<{\n /** The chosen option, or options, with `v-model`. */\n modelValue?: Value | undefined\n options: readonly ListboxOption<V>[]\n /** The list's accessible name. */\n label: string\n /** One option, or any number of them. */\n mode?: M | undefined\n /** How tall before it scrolls. Any CSS length. */\n height?: string | undefined\n}>()\n\n/* Multiple always hands back an array — toggling one produces a list, never\n nothing — so only the single value can be undefined. */\ntype Value = M extends 'multiple' ? V[] : V | undefined\n\n/* Declared by hand rather than with defineModel, which cannot both accept\n `undefined` and promise never to emit it. See `use-bound-value.ts`. */\nconst emit = defineEmits<{ 'update:modelValue': [value: Value] }>()\nconst model = useBoundValue<Value>(\n () => modelValue as Value | undefined,\n (value) => emit('update:modelValue', value),\n)\n\nconst id = useId()\nconst active = ref(0)\n\nlet typed = ''\nlet typedAt = 0\n\nconst chosen = computed(() => {\n const value = model.value\n if (Array.isArray(value)) return new Set<V>(value)\n\n return new Set<V>(value === undefined ? [] : [value as V])\n})\n\nfunction select(option: ListboxOption<V>) {\n if (option.disabled) return\n\n if (mode === 'multiple') {\n const next = new Set(chosen.value)\n if (next.has(option.value)) next.delete(option.value)\n else next.add(option.value)\n\n // In the order of the options, so the value reads the same however the\n // reader got there.\n model.value = options\n .map((one) => one.value)\n .filter((value) => next.has(value)) as typeof model.value\n return\n }\n\n model.value = option.value as typeof model.value\n}\n\nfunction moveTo(index: number) {\n if (options.length === 0) return\n\n active.value = Math.max(0, Math.min(options.length - 1, index))\n const option = options[active.value]\n\n // Single: the selection follows the focus, which is what a listbox does.\n if (mode === 'single' && option && !option.disabled) select(option)\n\n document.getElementById(`${id}-${active.value}`)?.scrollIntoView?.({ block: 'nearest' })\n}\n\n/** The next option after the current one that starts with what was typed. */\nfunction jumpTo(letter: string) {\n const now = Date.now()\n typed = now - typedAt > 700 ? letter : typed + letter\n typedAt = now\n\n const from = typed.length === 1 ? active.value + 1 : active.value\n const order = [...options.slice(from), ...options.slice(0, from)]\n const found = order.find(\n (option) => !option.disabled && option.label.toLowerCase().startsWith(typed.toLowerCase()),\n )\n\n if (found) moveTo(options.indexOf(found))\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n const moves: Record<string, number> = {\n ArrowDown: active.value + 1,\n ArrowUp: active.value - 1,\n Home: 0,\n End: options.length - 1,\n }\n\n if (event.key in moves) {\n event.preventDefault()\n moveTo(moves[event.key]!)\n return\n }\n\n if (event.key === ' ' || event.key === 'Enter') {\n const option = options[active.value]\n if (!option) return\n event.preventDefault()\n select(option)\n return\n }\n\n // A single printable character: the typeahead every native list has.\n if (event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey) {\n jumpTo(event.key)\n }\n}\n</script>\n\n<template>\n <div\n class=\"rk-listbox control\"\n :style=\"{ maxHeight: height }\"\n role=\"listbox\"\n tabindex=\"0\"\n :aria-label=\"label\"\n :aria-multiselectable=\"mode === 'multiple' ? true : undefined\"\n :aria-activedescendant=\"options.length ? `${id}-${active}` : undefined\"\n @keydown=\"onKeydown\"\n >\n <!-- `aria-disabled` is omitted rather than `false`: an option that can be\n chosen says nothing about being disabled. -->\n <div\n v-for=\"(option, index) in options\"\n :id=\"`${id}-${index}`\"\n :key=\"option.value\"\n class=\"rk-listbox-option\"\n :class=\"{\n 'is-active': index === active,\n 'is-chosen': chosen.has(option.value),\n 'is-disabled': option.disabled,\n }\"\n role=\"option\"\n :aria-selected=\"chosen.has(option.value)\"\n :aria-disabled=\"option.disabled || undefined\"\n @click=\"((active = index), select(option))\"\n >\n <span class=\"min-w-0 flex-1\">\n <span class=\"rk-listbox-label\">{{ option.label }}</span>\n <span v-if=\"option.description\" class=\"rk-listbox-description\">{{\n option.description\n }}</span>\n </span>\n\n <Check v-if=\"chosen.has(option.value)\" class=\"size-4 shrink-0\" aria-hidden=\"true\" />\n </div>\n\n <p v-if=\"options.length === 0\" class=\"rk-listbox-empty\" />\n </div>\n</template>\n\n<style scoped>\n.rk-listbox {\n overflow-y: auto;\n border-radius: var(--radius-card);\n padding: 0.25rem;\n}\n\n.rk-listbox:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 1px;\n}\n\n.rk-listbox-option {\n display: flex;\n cursor: pointer;\n align-items: center;\n gap: 0.625rem;\n border-radius: var(--radius-cell);\n padding: 0.5rem 0.625rem;\n font-size: 0.875rem;\n color: var(--color-ink);\n}\n\n/* The current row shows only while the list has focus. A list that is not\n being used has no \"current\" row, and a highlight left behind on one reads\n as a selection — which is exactly what it is not. */\n.rk-listbox:focus-within .rk-listbox-option.is-active {\n background: var(--color-muted);\n}\n\n.rk-listbox-option.is-chosen {\n color: var(--color-primary);\n font-weight: 500;\n}\n\n/* Chosen is the tick and the tint, and it holds whether the list is in use\n or not: the current option and the chosen one are different things. */\n.rk-listbox-option.is-chosen {\n background: color-mix(in oklab, var(--color-primary) 10%, transparent);\n}\n\n.rk-listbox:focus-within .rk-listbox-option.is-chosen.is-active {\n background: color-mix(in oklab, var(--color-primary) 20%, transparent);\n}\n\n.rk-listbox-option.is-disabled {\n cursor: not-allowed;\n opacity: 0.45;\n}\n\n.rk-listbox-label {\n display: block;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rk-listbox-description {\n display: block;\n font-size: 0.75rem;\n color: var(--color-ink-soft);\n}\n\n.rk-listbox-empty {\n padding: 1rem;\n}\n</style>\n","<script setup lang=\"ts\" generic=\"V extends string, M extends 'single' | 'multiple' = 'single'\">\nimport { Check } from 'lucide-vue-next'\nimport { computed, ref, useId } from 'vue'\n\nimport { useBoundValue } from '../composables/use-bound-value'\n\nexport interface ListboxOption<V extends string> {\n value: V\n /** Already translated. */\n label: string\n /** A line under the label. */\n description?: string | undefined\n disabled?: boolean | undefined\n}\n\n/**\n * A list you choose from, open on the page.\n *\n * Between `BaseRadioGroup` and `BaseSelect`: a radio group is a handful of\n * options that are all visible and all equal; a select opens the platform's\n * own picker; this is a scrollable list of many, choosing one or several,\n * that stays on screen — a list of accounts, of tags, of files to act on.\n *\n * One Tab stop. The list itself takes focus and `aria-activedescendant`\n * says which option is current, so the arrows move without thirty stops in\n * the tab order. Typing a letter jumps to the next option that starts with\n * it, which is how every native list has always worked and the first thing\n * people try.\n *\n * In `single` mode the selection follows the arrows, because that is what a\n * listbox does; in `multiple` mode Space and Enter toggle the current one.\n */\nconst {\n modelValue = undefined,\n options,\n label,\n mode = 'single' as M,\n height = '14rem',\n} = defineProps<{\n /** The chosen option, or options, with `v-model`. */\n modelValue?: Value | undefined\n options: readonly ListboxOption<V>[]\n /** The list's accessible name. */\n label: string\n /** One option, or any number of them. */\n mode?: M | undefined\n /** How tall before it scrolls. Any CSS length. */\n height?: string | undefined\n}>()\n\n/* Multiple always hands back an array — toggling one produces a list, never\n nothing — so only the single value can be undefined. */\ntype Value = M extends 'multiple' ? V[] : V | undefined\n\n/* Declared by hand rather than with defineModel, which cannot both accept\n `undefined` and promise never to emit it. See `use-bound-value.ts`. */\nconst emit = defineEmits<{ 'update:modelValue': [value: Value] }>()\nconst model = useBoundValue<Value>(\n () => modelValue as Value | undefined,\n (value) => emit('update:modelValue', value),\n)\n\nconst id = useId()\nconst active = ref(0)\n\nlet typed = ''\nlet typedAt = 0\n\nconst chosen = computed(() => {\n const value = model.value\n if (Array.isArray(value)) return new Set<V>(value)\n\n return new Set<V>(value === undefined ? [] : [value as V])\n})\n\nfunction select(option: ListboxOption<V>) {\n if (option.disabled) return\n\n if (mode === 'multiple') {\n const next = new Set(chosen.value)\n if (next.has(option.value)) next.delete(option.value)\n else next.add(option.value)\n\n // In the order of the options, so the value reads the same however the\n // reader got there.\n model.value = options\n .map((one) => one.value)\n .filter((value) => next.has(value)) as typeof model.value\n return\n }\n\n model.value = option.value as typeof model.value\n}\n\nfunction moveTo(index: number) {\n if (options.length === 0) return\n\n active.value = Math.max(0, Math.min(options.length - 1, index))\n const option = options[active.value]\n\n // Single: the selection follows the focus, which is what a listbox does.\n if (mode === 'single' && option && !option.disabled) select(option)\n\n document.getElementById(`${id}-${active.value}`)?.scrollIntoView?.({ block: 'nearest' })\n}\n\n/** The next option after the current one that starts with what was typed. */\nfunction jumpTo(letter: string) {\n const now = Date.now()\n typed = now - typedAt > 700 ? letter : typed + letter\n typedAt = now\n\n const from = typed.length === 1 ? active.value + 1 : active.value\n const order = [...options.slice(from), ...options.slice(0, from)]\n const found = order.find(\n (option) => !option.disabled && option.label.toLowerCase().startsWith(typed.toLowerCase()),\n )\n\n if (found) moveTo(options.indexOf(found))\n}\n\nfunction onKeydown(event: KeyboardEvent) {\n const moves: Record<string, number> = {\n ArrowDown: active.value + 1,\n ArrowUp: active.value - 1,\n Home: 0,\n End: options.length - 1,\n }\n\n if (event.key in moves) {\n event.preventDefault()\n moveTo(moves[event.key]!)\n return\n }\n\n if (event.key === ' ' || event.key === 'Enter') {\n const option = options[active.value]\n if (!option) return\n event.preventDefault()\n select(option)\n return\n }\n\n // A single printable character: the typeahead every native list has.\n if (event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey) {\n jumpTo(event.key)\n }\n}\n</script>\n\n<template>\n <div\n class=\"rk-listbox control\"\n :style=\"{ maxHeight: height }\"\n role=\"listbox\"\n tabindex=\"0\"\n :aria-label=\"label\"\n :aria-multiselectable=\"mode === 'multiple' ? true : undefined\"\n :aria-activedescendant=\"options.length ? `${id}-${active}` : undefined\"\n @keydown=\"onKeydown\"\n >\n <!-- `aria-disabled` is omitted rather than `false`: an option that can be\n chosen says nothing about being disabled. -->\n <div\n v-for=\"(option, index) in options\"\n :id=\"`${id}-${index}`\"\n :key=\"option.value\"\n class=\"rk-listbox-option\"\n :class=\"{\n 'is-active': index === active,\n 'is-chosen': chosen.has(option.value),\n 'is-disabled': option.disabled,\n }\"\n role=\"option\"\n :aria-selected=\"chosen.has(option.value)\"\n :aria-disabled=\"option.disabled || undefined\"\n @click=\"((active = index), select(option))\"\n >\n <span class=\"min-w-0 flex-1\">\n <span class=\"rk-listbox-label\">{{ option.label }}</span>\n <span v-if=\"option.description\" class=\"rk-listbox-description\">{{\n option.description\n }}</span>\n </span>\n\n <Check v-if=\"chosen.has(option.value)\" class=\"size-4 shrink-0\" aria-hidden=\"true\" />\n </div>\n\n <p v-if=\"options.length === 0\" class=\"rk-listbox-empty\" />\n </div>\n</template>\n\n<style scoped>\n.rk-listbox {\n overflow-y: auto;\n border-radius: var(--radius-card);\n padding: 0.25rem;\n}\n\n.rk-listbox:focus-visible {\n outline: 2px solid var(--color-primary);\n outline-offset: 1px;\n}\n\n.rk-listbox-option {\n display: flex;\n cursor: pointer;\n align-items: center;\n gap: 0.625rem;\n border-radius: var(--radius-cell);\n padding: 0.5rem 0.625rem;\n font-size: 0.875rem;\n color: var(--color-ink);\n}\n\n/* The current row shows only while the list has focus. A list that is not\n being used has no \"current\" row, and a highlight left behind on one reads\n as a selection — which is exactly what it is not. */\n.rk-listbox:focus-within .rk-listbox-option.is-active {\n background: var(--color-muted);\n}\n\n.rk-listbox-option.is-chosen {\n color: var(--color-primary);\n font-weight: 500;\n}\n\n/* Chosen is the tick and the tint, and it holds whether the list is in use\n or not: the current option and the chosen one are different things. */\n.rk-listbox-option.is-chosen {\n background: color-mix(in oklab, var(--color-primary) 10%, transparent);\n}\n\n.rk-listbox:focus-within .rk-listbox-option.is-chosen.is-active {\n background: color-mix(in oklab, var(--color-primary) 20%, transparent);\n}\n\n.rk-listbox-option.is-disabled {\n cursor: not-allowed;\n opacity: 0.45;\n}\n\n.rk-listbox-label {\n display: block;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.rk-listbox-description {\n display: block;\n font-size: 0.75rem;\n color: var(--color-ink-soft);\n}\n\n.rk-listbox-empty {\n padding: 1rem;\n}\n</style>\n"],"mappings":"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"}
@@ -3,8 +3,10 @@ export interface ComboboxOption<V extends string> {
3
3
  /** Already translated. */
4
4
  label: string;
5
5
  }
6
- declare const __VLS_export: <V extends string>(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
7
- props: import('vue').PublicProps & __VLS_PrettifyLocal<({
6
+ declare const __VLS_export: <V extends string, M extends "single" | "multiple" = "single">(__VLS_props: NonNullable<Awaited<typeof __VLS_setup>>["props"], __VLS_ctx?: __VLS_PrettifyLocal<Pick<NonNullable<Awaited<typeof __VLS_setup>>, "attrs" | "emit" | "slots">>, __VLS_exposed?: NonNullable<Awaited<typeof __VLS_setup>>["expose"], __VLS_setup?: Promise<{
7
+ props: import('vue').PublicProps & __VLS_PrettifyLocal<{
8
+ /** The chosen value, or values in `multiple` mode, with `v-model`. */
9
+ modelValue?: (M extends "multiple" ? V[] : "" | V) | undefined;
8
10
  label: string;
9
11
  options: readonly ComboboxOption<V>[];
10
12
  placeholder?: string | undefined;
@@ -13,17 +15,32 @@ declare const __VLS_export: <V extends string>(__VLS_props: NonNullable<Awaited<
13
15
  disabled?: boolean | undefined;
14
16
  /** Shown when nothing matches. Already translated. */
15
17
  emptyLabel: string;
18
+ /** `multiple` keeps the chosen ones as chips in the field. */
19
+ mode?: M | undefined;
20
+ /** Names a chip's remove button. Without it, chips carry no button. */
21
+ removeLabel?: ((label: string) => string) | undefined;
22
+ /** Marks the list busy while an answer is on its way. */
23
+ loading?: boolean | undefined;
24
+ /** Read while `loading`. Already translated. */
25
+ loadingLabel?: string | undefined;
26
+ /** `none` when the server has already narrowed the list. */
27
+ filter?: "local" | "none" | undefined;
28
+ /** How long typing settles before `search`, in milliseconds. */
29
+ debounce?: number | undefined;
30
+ /** Past this many options, only the rows near the viewport are rendered. */
31
+ virtualizeAfter?: number | undefined;
32
+ /** A row's height in pixels; the virtual window is measured in these. */
33
+ rowHeight?: number | undefined;
16
34
  } & {
17
- modelValue?: V | "";
18
- }) & {
19
- "onUpdate:modelValue"?: (value: "" | V) => any;
35
+ onSearch?: (query: string) => any;
36
+ "onUpdate:modelValue"?: (value: M extends "multiple" ? V[] : "" | V) => any;
20
37
  }> & (typeof globalThis extends {
21
38
  __VLS_PROPS_FALLBACK: infer P;
22
39
  } ? P : {});
23
40
  expose: (exposed: {}) => void;
24
41
  attrs: any;
25
42
  slots: {};
26
- emit: (event: "update:modelValue", value: "" | V) => void;
43
+ emit: ((evt: "search", query: string) => void) & ((evt: "update:modelValue", value: M extends "multiple" ? V[] : "" | V) => void);
27
44
  }>) => import('vue').VNode & {
28
45
  __ctx?: NonNullable<Awaited<typeof __VLS_setup>>;
29
46
  };
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { n as formatNumber, r as setFormatLocale, t as formatDate } from "./form
3
3
  import { a as SHEET_ROOT_ID, i as useVisualViewport, o as ensureSheetRoot, r as useBoundValue, t as BaseSheet_default } from "./BaseSheet-B7401rS7.js";
4
4
  import { a as BaseAlert_default, c as applyTheme, d as setThemeStorageKey, f as useTheme, h as toRedirectPath, i as FormField_default, l as isThemePreference, m as safeRedirect, n as BaseCheckbox_default, o as useToast, p as tapFeedback, r as BaseInput_default, s as useOnline, t as GoogleButton_default, u as readStoredTheme } from "./GoogleButton-mCqGbrXm.js";
5
5
  import { n as registerErrorMapper, r as toAppError, t as AppError } from "./app-error-DF9cijE0.js";
6
- import { n as BaseListbox_default, r as useMediaQuery, t as BaseSkeleton_default } from "./BaseSkeleton-DnqwogzV.js";
6
+ import { n as BaseSkeleton_default, r as useMediaQuery, t as BaseListbox_default } from "./BaseListbox-BnaHlmMN.js";
7
7
  import { t as _plugin_vue_export_helper_default } from "./_plugin-vue_export-helper-BOaGB7Aw.js";
8
8
  import { t as BaseButton_default } from "./BaseButton-RN2an975.js";
9
9
  import { t as SettingsRow_default } from "./SettingsRow-Ct1p2C9Q.js";
@@ -621,7 +621,11 @@ var BaseCard_default = /* @__PURE__ */ defineComponent({
621
621
  //#endregion
622
622
  //#region src/components/BaseCombobox.vue?vue&type=script&setup=true&lang.ts
623
623
  var _hoisted_1$38 = ["for"];
624
- var _hoisted_2$32 = { class: "rk-combo-field" };
624
+ var _hoisted_2$32 = [
625
+ "aria-label",
626
+ "disabled",
627
+ "onClick"
628
+ ];
625
629
  var _hoisted_3$21 = [
626
630
  "id",
627
631
  "placeholder",
@@ -632,22 +636,38 @@ var _hoisted_3$21 = [
632
636
  "aria-describedby"
633
637
  ];
634
638
  var _hoisted_4$16 = ["disabled"];
635
- var _hoisted_5$9 = ["aria-label"];
639
+ var _hoisted_5$9 = [
640
+ "aria-label",
641
+ "aria-multiselectable",
642
+ "aria-busy"
643
+ ];
636
644
  var _hoisted_6$5 = [
637
645
  "id",
638
646
  "aria-selected",
647
+ "aria-setsize",
648
+ "aria-posinset",
639
649
  "onPointerdown",
640
650
  "onPointermove"
641
651
  ];
642
652
  var _hoisted_7$4 = {
643
- key: 0,
653
+ key: 2,
654
+ class: "rk-combo-status"
655
+ };
656
+ var _hoisted_8$3 = { key: 0 };
657
+ var _hoisted_9$3 = {
658
+ class: "rk-combo-status-rows",
659
+ "aria-hidden": "true"
660
+ };
661
+ var _hoisted_10$3 = {
662
+ key: 3,
644
663
  class: "rk-combo-empty"
645
664
  };
646
665
  //#endregion
647
666
  //#region src/components/BaseCombobox.vue
648
667
  var BaseCombobox_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__ */ defineComponent({
649
668
  __name: "BaseCombobox",
650
- props: /*@__PURE__*/ mergeModels({
669
+ props: {
670
+ modelValue: { default: () => void 0 },
651
671
  label: {},
652
672
  options: {},
653
673
  placeholder: { default: "" },
@@ -657,14 +677,38 @@ var BaseCombobox_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @_
657
677
  type: Boolean,
658
678
  default: false
659
679
  },
660
- emptyLabel: {}
661
- }, {
662
- "modelValue": { default: "" },
663
- "modelModifiers": {}
664
- }),
665
- emits: ["update:modelValue"],
666
- setup(__props) {
667
- const model = useModel(__props, "modelValue");
680
+ emptyLabel: {},
681
+ mode: { default: "single" },
682
+ removeLabel: {
683
+ type: Function,
684
+ default: void 0
685
+ },
686
+ loading: {
687
+ type: Boolean,
688
+ default: false
689
+ },
690
+ loadingLabel: { default: "" },
691
+ filter: { default: "local" },
692
+ debounce: { default: 200 },
693
+ virtualizeAfter: { default: 150 },
694
+ rowHeight: { default: 36 }
695
+ },
696
+ emits: ["update:modelValue", "search"],
697
+ setup(__props, { emit: __emit }) {
698
+ /** Multiple always hands back an array; a single choice is the value or ''. */
699
+ const emit = __emit;
700
+ const model = useBoundValue(() => __props.modelValue, (value) => emit("update:modelValue", value));
701
+ const multiple = computed(() => __props.mode === "multiple");
702
+ /** The chosen values, however many there are, as a plain array. */
703
+ const chosen = computed(() => {
704
+ const value = model.value;
705
+ if (Array.isArray(value)) return value;
706
+ return value === void 0 || value === "" ? [] : [value];
707
+ });
708
+ const chosenOptions = computed(() => chosen.value.map((value) => __props.options.find((option) => option.value === value) ?? {
709
+ value,
710
+ label: value
711
+ }));
668
712
  const id = useId();
669
713
  const listId = `${id}-list`;
670
714
  const hintId = `${id}-hint`;
@@ -675,31 +719,79 @@ var BaseCombobox_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @_
675
719
  const highlighted = ref(0);
676
720
  const root = ref(null);
677
721
  const list = ref(null);
678
- const selected = computed(() => __props.options.find((option) => option.value === model.value));
679
- /** What the input shows: the query while typing, the chosen label otherwise. */
722
+ const single = computed(() => multiple.value ? void 0 : chosenOptions.value[0]);
723
+ /**
724
+ * What the input shows.
725
+ *
726
+ * In multiple mode it is always the query: the answers are the chips beside
727
+ * it, and writing one of them into the field would mean deleting it to search
728
+ * for the next.
729
+ */
680
730
  const text = computed({
681
- get: () => open.value ? query.value : selected.value?.label ?? "",
731
+ get: () => multiple.value || open.value ? query.value : single.value?.label ?? "",
682
732
  set: (value) => {
683
733
  query.value = value;
684
734
  open.value = true;
685
735
  highlighted.value = 0;
736
+ announce(value);
686
737
  }
687
738
  });
688
739
  const matches = computed(() => {
689
740
  const needle = query.value.trim().toLowerCase();
690
- if (!open.value || needle === "") return __props.options;
741
+ if (__props.filter === "none" || !open.value || needle === "") return __props.options;
691
742
  return __props.options.filter((option) => option.label.toLowerCase().includes(needle));
692
743
  });
693
744
  const describedBy = computed(() => {
694
745
  if (__props.error) return errorId;
695
746
  if (__props.hint) return hintId;
696
747
  });
748
+ let timer;
749
+ function announce(value) {
750
+ if (timer) clearTimeout(timer);
751
+ timer = setTimeout(() => emit("search", value.trim()), __props.debounce);
752
+ }
753
+ const virtual = computed(() => matches.value.length > __props.virtualizeAfter);
754
+ const scrollTop = ref(0);
755
+ /** How tall the list box is; read once it is open, never guessed. */
756
+ const viewport = ref(224);
757
+ const window_ = computed(() => {
758
+ if (!virtual.value) return {
759
+ start: 0,
760
+ end: matches.value.length
761
+ };
762
+ const first = Math.max(0, Math.floor(scrollTop.value / __props.rowHeight) - 3);
763
+ const count = Math.ceil(viewport.value / __props.rowHeight) + 6;
764
+ return {
765
+ start: first,
766
+ end: Math.min(matches.value.length, first + count)
767
+ };
768
+ });
769
+ const rows = computed(() => matches.value.slice(window_.value.start, window_.value.end).map((option, index) => ({
770
+ option,
771
+ /** The row's place in the whole list, not in the window. */
772
+ index: window_.value.start + index
773
+ })));
774
+ const padTop = computed(() => virtual.value ? window_.value.start * __props.rowHeight : 0);
775
+ const padBottom = computed(() => virtual.value ? (matches.value.length - window_.value.end) * __props.rowHeight : 0);
776
+ function onScroll(event) {
777
+ scrollTop.value = event.target.scrollTop;
778
+ }
697
779
  function choose(option) {
698
780
  if (!option) return;
781
+ if (multiple.value) {
782
+ const next = chosen.value.includes(option.value) ? chosen.value.filter((one) => one !== option.value) : [...chosen.value, option.value];
783
+ model.value = next;
784
+ query.value = "";
785
+ return;
786
+ }
699
787
  model.value = option.value;
700
788
  query.value = "";
701
789
  open.value = false;
702
790
  }
791
+ function remove(value) {
792
+ if (!multiple.value) return;
793
+ model.value = chosen.value.filter((one) => one !== value);
794
+ }
703
795
  function move(delta) {
704
796
  const count = matches.value.length;
705
797
  if (count === 0) return;
@@ -709,6 +801,13 @@ var BaseCombobox_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @_
709
801
  }
710
802
  async function scrollHighlightIntoView() {
711
803
  await nextTick();
804
+ if (virtual.value && list.value) {
805
+ const top = highlighted.value * __props.rowHeight;
806
+ const bottom = top + __props.rowHeight;
807
+ if (top < list.value.scrollTop) list.value.scrollTop = top;
808
+ else if (bottom > list.value.scrollTop + viewport.value) list.value.scrollTop = bottom - viewport.value;
809
+ return;
810
+ }
712
811
  (list.value?.children[highlighted.value])?.scrollIntoView?.({ block: "nearest" });
713
812
  }
714
813
  function onKeydown(event) {
@@ -726,12 +825,18 @@ var BaseCombobox_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @_
726
825
  event.preventDefault();
727
826
  choose(matches.value[highlighted.value]);
728
827
  break;
828
+ case "Backspace":
829
+ if (multiple.value && query.value === "" && chosen.value.length > 0) {
830
+ event.preventDefault();
831
+ remove(chosen.value[chosen.value.length - 1]);
832
+ }
833
+ break;
729
834
  case "Escape":
730
835
  event.preventDefault();
731
836
  if (open.value) {
732
837
  open.value = false;
733
838
  query.value = "";
734
- } else model.value = "";
839
+ } else if (!multiple.value) model.value = "";
735
840
  break;
736
841
  case "Tab": open.value = false;
737
842
  }
@@ -742,12 +847,19 @@ var BaseCombobox_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @_
742
847
  query.value = "";
743
848
  }
744
849
  }
745
- watch(open, (isOpen) => {
850
+ watch(open, async (isOpen) => {
746
851
  if (typeof document === "undefined") return;
747
- if (isOpen) document.addEventListener("pointerdown", onDocumentPointer);
748
- else document.removeEventListener("pointerdown", onDocumentPointer);
852
+ if (isOpen) {
853
+ document.addEventListener("pointerdown", onDocumentPointer);
854
+ await nextTick();
855
+ if (list.value) viewport.value = list.value.clientHeight || viewport.value;
856
+ } else document.removeEventListener("pointerdown", onDocumentPointer);
749
857
  }, { immediate: true });
858
+ watch(matches, () => {
859
+ if (highlighted.value >= matches.value.length) highlighted.value = 0;
860
+ });
750
861
  onBeforeUnmount(() => {
862
+ if (timer) clearTimeout(timer);
751
863
  if (typeof document !== "undefined") document.removeEventListener("pointerdown", onDocumentPointer);
752
864
  });
753
865
  return (_ctx, _cache) => {
@@ -760,52 +872,93 @@ var BaseCombobox_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @_
760
872
  for: unref(id),
761
873
  class: "rk-combo-label"
762
874
  }, toDisplayString(__props.label), 9, _hoisted_1$38),
763
- createElementVNode("div", _hoisted_2$32, [withDirectives(createElementVNode("input", {
764
- id: unref(id),
765
- "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => text.value = $event),
766
- type: "text",
767
- role: "combobox",
768
- class: "rk-combo-input control",
769
- autocomplete: "off",
770
- placeholder: __props.placeholder,
771
- disabled: __props.disabled,
772
- "aria-expanded": open.value,
773
- "aria-controls": listId,
774
- "aria-autocomplete": "list",
775
- "aria-activedescendant": open.value ? optionId(highlighted.value) : void 0,
776
- "aria-invalid": Boolean(__props.error),
777
- "aria-describedby": describedBy.value,
778
- onKeydown,
779
- onFocus: _cache[1] || (_cache[1] = ($event) => open.value = true)
780
- }, null, 40, _hoisted_3$21), [[vModelText, text.value]]), createElementVNode("button", {
781
- type: "button",
782
- class: "rk-combo-toggle",
783
- tabindex: "-1",
784
- "aria-hidden": "true",
785
- disabled: __props.disabled,
786
- onClick: _cache[2] || (_cache[2] = ($event) => open.value = !open.value)
787
- }, "", 8, _hoisted_4$16)]),
875
+ createElementVNode("div", { class: normalizeClass(["rk-combo-field", multiple.value ? "is-multiple control" : ""]) }, [
876
+ (openBlock(true), createElementBlock(Fragment, null, renderList(multiple.value ? chosenOptions.value : [], (option) => {
877
+ return openBlock(), createElementBlock("span", {
878
+ key: option.value,
879
+ class: "rk-combo-chip"
880
+ }, [createElementVNode("span", null, toDisplayString(option.label), 1), __props.removeLabel ? (openBlock(), createElementBlock("button", {
881
+ key: 0,
882
+ type: "button",
883
+ class: "rk-combo-chip-x focus-ring",
884
+ "aria-label": __props.removeLabel(option.label),
885
+ disabled: __props.disabled,
886
+ onClick: ($event) => remove(option.value)
887
+ }, " × ", 8, _hoisted_2$32)) : createCommentVNode("", true)]);
888
+ }), 128)),
889
+ withDirectives(createElementVNode("input", {
890
+ id: unref(id),
891
+ "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => text.value = $event),
892
+ type: "text",
893
+ role: "combobox",
894
+ class: normalizeClass(["rk-combo-input", { control: !multiple.value }]),
895
+ autocomplete: "off",
896
+ placeholder: __props.placeholder,
897
+ disabled: __props.disabled,
898
+ "aria-expanded": open.value,
899
+ "aria-controls": listId,
900
+ "aria-autocomplete": "list",
901
+ "aria-activedescendant": open.value ? optionId(highlighted.value) : void 0,
902
+ "aria-invalid": Boolean(__props.error),
903
+ "aria-describedby": describedBy.value,
904
+ onKeydown,
905
+ onFocus: _cache[1] || (_cache[1] = ($event) => open.value = true)
906
+ }, null, 42, _hoisted_3$21), [[vModelText, text.value]]),
907
+ createElementVNode("button", {
908
+ type: "button",
909
+ class: "rk-combo-toggle",
910
+ tabindex: "-1",
911
+ "aria-hidden": "true",
912
+ disabled: __props.disabled,
913
+ onClick: _cache[2] || (_cache[2] = ($event) => open.value = !open.value)
914
+ }, " ▾ ", 8, _hoisted_4$16)
915
+ ], 2),
788
916
  withDirectives(createElementVNode("ul", {
789
917
  id: listId,
790
918
  ref_key: "list",
791
919
  ref: list,
792
920
  role: "listbox",
793
921
  class: "rk-combo-list surface-overlay",
794
- "aria-label": __props.label
795
- }, [(openBlock(true), createElementBlock(Fragment, null, renderList(matches.value, (option, index) => {
796
- return openBlock(), createElementBlock("li", {
797
- id: optionId(index),
798
- key: option.value,
799
- role: "option",
800
- class: normalizeClass(["rk-combo-option", {
801
- "is-highlighted": index === highlighted.value,
802
- "is-selected": option.value === model.value
803
- }]),
804
- "aria-selected": option.value === model.value,
805
- onPointerdown: withModifiers(($event) => choose(option), ["prevent"]),
806
- onPointermove: ($event) => highlighted.value = index
807
- }, toDisplayString(option.label), 43, _hoisted_6$5);
808
- }), 128)), matches.value.length === 0 ? (openBlock(), createElementBlock("li", _hoisted_7$4, toDisplayString(__props.emptyLabel), 1)) : createCommentVNode("", true)], 8, _hoisted_5$9), [[vShow, open.value]]),
922
+ "aria-label": __props.label,
923
+ "aria-multiselectable": multiple.value ? true : void 0,
924
+ "aria-busy": __props.loading ? true : void 0,
925
+ onScroll
926
+ }, [
927
+ padTop.value > 0 ? (openBlock(), createElementBlock("li", {
928
+ key: 0,
929
+ style: normalizeStyle({ height: `${padTop.value}px` }),
930
+ "aria-hidden": "true"
931
+ }, null, 4)) : createCommentVNode("", true),
932
+ (openBlock(true), createElementBlock(Fragment, null, renderList(rows.value, (row) => {
933
+ return openBlock(), createElementBlock("li", {
934
+ id: optionId(row.index),
935
+ key: row.option.value,
936
+ role: "option",
937
+ class: normalizeClass(["rk-combo-option", {
938
+ "is-highlighted": row.index === highlighted.value,
939
+ "is-selected": chosen.value.includes(row.option.value)
940
+ }]),
941
+ style: normalizeStyle(virtual.value ? { height: `${__props.rowHeight}px` } : void 0),
942
+ "aria-selected": chosen.value.includes(row.option.value),
943
+ "aria-setsize": matches.value.length,
944
+ "aria-posinset": row.index + 1,
945
+ onPointerdown: withModifiers(($event) => choose(row.option), ["prevent"]),
946
+ onPointermove: ($event) => highlighted.value = row.index
947
+ }, toDisplayString(row.option.label), 47, _hoisted_6$5);
948
+ }), 128)),
949
+ padBottom.value > 0 ? (openBlock(), createElementBlock("li", {
950
+ key: 1,
951
+ style: normalizeStyle({ height: `${padBottom.value}px` }),
952
+ "aria-hidden": "true"
953
+ }, null, 4)) : createCommentVNode("", true),
954
+ __props.loading ? (openBlock(), createElementBlock("li", _hoisted_7$4, [__props.loadingLabel ? (openBlock(), createElementBlock("span", _hoisted_8$3, toDisplayString(__props.loadingLabel), 1)) : createCommentVNode("", true), createElementVNode("span", _hoisted_9$3, [(openBlock(), createElementBlock(Fragment, null, renderList(3, (n) => {
955
+ return createVNode(BaseSkeleton_default, {
956
+ key: n,
957
+ shape: "text",
958
+ height: "0.75rem"
959
+ });
960
+ }), 64))])])) : matches.value.length === 0 ? (openBlock(), createElementBlock("li", _hoisted_10$3, toDisplayString(__props.emptyLabel), 1)) : createCommentVNode("", true)
961
+ ], 40, _hoisted_5$9), [[vShow, open.value]]),
809
962
  __props.error ? (openBlock(), createElementBlock("p", {
810
963
  key: 0,
811
964
  id: errorId,
@@ -818,7 +971,7 @@ var BaseCombobox_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @_
818
971
  ], 512);
819
972
  };
820
973
  }
821
- }), [["__scopeId", "data-v-ce4c503f"]]);
974
+ }), [["__scopeId", "data-v-aa2335fa"]]);
822
975
  //#endregion
823
976
  //#region src/components/BaseRadioGroup.vue?vue&type=script&setup=true&lang.ts
824
977
  var _hoisted_1$37 = ["aria-describedby"];
@@ -4021,7 +4174,7 @@ function createI18nRuntime(options) {
4021
4174
  *
4022
4175
  * The fallback keeps `vitest` and `vite dev` honest, where no define runs.
4023
4176
  */
4024
- var VERSION = "2.12.0";
4177
+ var VERSION = "2.13.0";
4025
4178
  //#endregion
4026
4179
  export { AppError, AvatarStack_default as AvatarStack, BaseAlert_default as BaseAlert, BaseAvatar_default as BaseAvatar, BaseBadge_default as BaseBadge, BaseButton_default as BaseButton, BaseCalendar_default as BaseCalendar, BaseCard_default as BaseCard, BaseCheckbox_default as BaseCheckbox, BaseChip_default as BaseChip, BaseCombobox_default as BaseCombobox, BaseDatePicker_default as BaseDatePicker, BaseInput_default as BaseInput, BaseKbd_default as BaseKbd, BaseLink_default as BaseLink, BaseListbox_default as BaseListbox, BaseMenu_default as BaseMenu, BasePopconfirm_default as BasePopconfirm, BasePopover_default as BasePopover, BaseRadioGroup_default as BaseRadioGroup, BaseRating_default as BaseRating, BaseSelect_default as BaseSelect, BaseSeparator_default as BaseSeparator, BaseSheet_default as BaseSheet, BaseSkeleton_default as BaseSkeleton, BaseSlider_default as BaseSlider, BaseSpinner_default as BaseSpinner, BaseStepper_default as BaseStepper, BaseSwitch_default as BaseSwitch, BaseTable_default as BaseTable, BaseTextarea_default as BaseTextarea, CircularProgress_default as CircularProgress, CopyButton_default as CopyButton, DescriptionList_default as DescriptionList, EmptyState_default as EmptyState, ErrorBoundary_default as ErrorBoundary, FileDrop_default as FileDrop, FormField_default as FormField, GoogleButton_default as GoogleButton, LocaleLinks_default as LocaleLinks, MATERIALS, NumberInput_default as NumberInput, PALETTES, PageContainer_default as PageContainer, PageHeader_default as PageHeader, PinInput_default as PinInput, PriceCard_default as PriceCard, ProgressBar_default as ProgressBar, SHEET_ROOT_ID, SectionHeading_default as SectionHeading, SegmentedControl_default as SegmentedControl, SettingsGroup_default as SettingsGroup, SettingsRow_default as SettingsRow, SkeletonList_default as SkeletonList, StatCard_default as StatCard, TabBar_default as TabBar, TagsInput_default as TagsInput, TimePicker_default as TimePicker, ToastHost_default as ToastHost, ToggleGroup_default as ToggleGroup, ToneDot_default as ToneDot, VERSION, addDays, applyMaterial, applyPalette, applyTheme, createI18nRuntime, downloadJson, eachDayOfYear, ensureSheetRoot, formatDate, formatNumber, fromDateKey, isApplePortable, isInstalled, isMaterial, isPaletteName, isThemePreference, lastNDays, leadingBlanks, needsIosInstall, readStoredTheme, registerErrorMapper, relativeDayLabel, safeRedirect, setFormatLocale, setMaterialStorageKey, setPaletteStorageKey, setThemeStorageKey, startOfWeek, tapFeedback, toAppError, toDateKey, toRedirectPath, todayKey, useDebouncedCallback, useDragScroll, useMaterial, useMediaQuery, useOnline, usePalette, useTheme, useToast, useToday, useVisualViewport };
4027
4180