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 +1 -1
- package/dist/{BaseSkeleton-DnqwogzV.js → BaseListbox-BnaHlmMN.js} +39 -39
- package/dist/BaseListbox-BnaHlmMN.js.map +1 -0
- package/dist/components/BaseCombobox.vue.d.ts +23 -6
- package/dist/index.js +216 -63
- package/dist/index.js.map +1 -1
- package/dist/styles.css +113 -42
- package/dist/web.js +1 -1
- package/package.json +1 -1
- package/dist/BaseSkeleton-DnqwogzV.js.map +0 -1
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.
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
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: (
|
|
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
|
|
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 =
|
|
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 = [
|
|
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:
|
|
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:
|
|
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
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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
|
|
679
|
-
/**
|
|
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 :
|
|
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)
|
|
748
|
-
|
|
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",
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
"
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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-
|
|
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.
|
|
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
|
|