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/dist/styles.css
CHANGED
|
@@ -218,6 +218,34 @@
|
|
|
218
218
|
opacity: 0;
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
.rk-skeleton[data-v-37327de6] {
|
|
222
|
+
display: block;
|
|
223
|
+
flex-shrink: 0;
|
|
224
|
+
background: var(--color-muted);
|
|
225
|
+
animation: rk-skeleton-pulse-37327de6 1.6s ease-in-out infinite;
|
|
226
|
+
}
|
|
227
|
+
.rk-skeleton.is-block[data-v-37327de6] {
|
|
228
|
+
border-radius: var(--radius-cell);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* A line of text, rounded like one and a little short of its box. */
|
|
232
|
+
.rk-skeleton.is-text[data-v-37327de6] {
|
|
233
|
+
border-radius: 9999px;
|
|
234
|
+
}
|
|
235
|
+
.rk-skeleton.is-circle[data-v-37327de6] {
|
|
236
|
+
border-radius: 9999px;
|
|
237
|
+
}
|
|
238
|
+
@keyframes rk-skeleton-pulse-37327de6 {
|
|
239
|
+
50% {
|
|
240
|
+
opacity: 0.55;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
@media (prefers-reduced-motion: reduce) {
|
|
244
|
+
.rk-skeleton[data-v-37327de6] {
|
|
245
|
+
animation: none;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
221
249
|
.rk-listbox[data-v-7f8cd788] {
|
|
222
250
|
overflow-y: auto;
|
|
223
251
|
border-radius: var(--radius-card);
|
|
@@ -276,34 +304,6 @@
|
|
|
276
304
|
padding: 1rem;
|
|
277
305
|
}
|
|
278
306
|
|
|
279
|
-
.rk-skeleton[data-v-37327de6] {
|
|
280
|
-
display: block;
|
|
281
|
-
flex-shrink: 0;
|
|
282
|
-
background: var(--color-muted);
|
|
283
|
-
animation: rk-skeleton-pulse-37327de6 1.6s ease-in-out infinite;
|
|
284
|
-
}
|
|
285
|
-
.rk-skeleton.is-block[data-v-37327de6] {
|
|
286
|
-
border-radius: var(--radius-cell);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/* A line of text, rounded like one and a little short of its box. */
|
|
290
|
-
.rk-skeleton.is-text[data-v-37327de6] {
|
|
291
|
-
border-radius: 9999px;
|
|
292
|
-
}
|
|
293
|
-
.rk-skeleton.is-circle[data-v-37327de6] {
|
|
294
|
-
border-radius: 9999px;
|
|
295
|
-
}
|
|
296
|
-
@keyframes rk-skeleton-pulse-37327de6 {
|
|
297
|
-
50% {
|
|
298
|
-
opacity: 0.55;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
@media (prefers-reduced-motion: reduce) {
|
|
302
|
-
.rk-skeleton[data-v-37327de6] {
|
|
303
|
-
animation: none;
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
307
|
.rk-avatar[data-v-47586bec] {
|
|
308
308
|
display: grid;
|
|
309
309
|
place-items: center;
|
|
@@ -399,26 +399,58 @@
|
|
|
399
399
|
background: color-mix(in srgb, var(--color-hair) 70%, transparent);
|
|
400
400
|
}
|
|
401
401
|
|
|
402
|
-
.rk-combo[data-v-
|
|
402
|
+
.rk-combo[data-v-aa2335fa] {
|
|
403
403
|
position: relative;
|
|
404
404
|
display: flex;
|
|
405
405
|
flex-direction: column;
|
|
406
406
|
gap: 0.375rem;
|
|
407
407
|
}
|
|
408
|
-
.rk-combo-label[data-v-
|
|
408
|
+
.rk-combo-label[data-v-aa2335fa] {
|
|
409
409
|
font-size: 0.875rem;
|
|
410
410
|
font-weight: 500;
|
|
411
411
|
color: var(--color-ink);
|
|
412
412
|
}
|
|
413
|
-
.rk-combo-field[data-v-
|
|
413
|
+
.rk-combo-field[data-v-aa2335fa] {
|
|
414
414
|
position: relative;
|
|
415
415
|
display: flex;
|
|
416
416
|
align-items: center;
|
|
417
417
|
}
|
|
418
418
|
|
|
419
|
+
/* With chips the field is a box that wraps, and the input is one item in it
|
|
420
|
+
that grows into whatever is left. */
|
|
421
|
+
.rk-combo-field.is-multiple[data-v-aa2335fa] {
|
|
422
|
+
flex-wrap: wrap;
|
|
423
|
+
gap: 0.25rem;
|
|
424
|
+
min-height: 2.75rem;
|
|
425
|
+
border-radius: var(--radius-card);
|
|
426
|
+
padding: 0.3125rem 2rem 0.3125rem 0.375rem;
|
|
427
|
+
}
|
|
428
|
+
.rk-combo-chip[data-v-aa2335fa] {
|
|
429
|
+
display: inline-flex;
|
|
430
|
+
align-items: center;
|
|
431
|
+
gap: 0.25rem;
|
|
432
|
+
border-radius: 9999px;
|
|
433
|
+
background: var(--color-muted);
|
|
434
|
+
padding: 0.125rem 0.25rem 0.125rem 0.5rem;
|
|
435
|
+
font-size: 0.8125rem;
|
|
436
|
+
color: var(--color-ink);
|
|
437
|
+
}
|
|
438
|
+
.rk-combo-chip-x[data-v-aa2335fa] {
|
|
439
|
+
display: grid;
|
|
440
|
+
width: 1.125rem;
|
|
441
|
+
height: 1.125rem;
|
|
442
|
+
place-items: center;
|
|
443
|
+
border-radius: 9999px;
|
|
444
|
+
color: var(--color-ink-soft);
|
|
445
|
+
line-height: 1;
|
|
446
|
+
}
|
|
447
|
+
.rk-combo-chip-x[data-v-aa2335fa]:hover {
|
|
448
|
+
color: var(--color-ink);
|
|
449
|
+
}
|
|
450
|
+
|
|
419
451
|
/* 16px, like every other text control here: iOS zooms the viewport when it
|
|
420
452
|
focuses a field under 16px and never zooms back. */
|
|
421
|
-
.rk-combo-input[data-v-
|
|
453
|
+
.rk-combo-input[data-v-aa2335fa] {
|
|
422
454
|
width: 100%;
|
|
423
455
|
height: 2.75rem;
|
|
424
456
|
border-radius: var(--radius-card);
|
|
@@ -426,20 +458,40 @@
|
|
|
426
458
|
font-size: 1rem;
|
|
427
459
|
color: var(--color-ink);
|
|
428
460
|
}
|
|
429
|
-
.rk-combo-input[data-v-
|
|
461
|
+
.is-multiple .rk-combo-input[data-v-aa2335fa] {
|
|
462
|
+
width: auto;
|
|
463
|
+
height: 1.75rem;
|
|
464
|
+
min-width: 6rem;
|
|
465
|
+
flex: 1;
|
|
466
|
+
border: 0;
|
|
467
|
+
border-radius: 0;
|
|
468
|
+
padding: 0;
|
|
469
|
+
background: transparent;
|
|
470
|
+
box-shadow: none;
|
|
471
|
+
}
|
|
472
|
+
.rk-combo-input[data-v-aa2335fa]:focus-visible {
|
|
473
|
+
outline: 2px solid var(--color-primary);
|
|
474
|
+
outline-offset: -1px;
|
|
475
|
+
}
|
|
476
|
+
.is-multiple .rk-combo-input[data-v-aa2335fa]:focus-visible {
|
|
477
|
+
outline: none;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/* The ring belongs to the whole box once the box is the control. */
|
|
481
|
+
.rk-combo-field.is-multiple[data-v-aa2335fa]:focus-within {
|
|
430
482
|
outline: 2px solid var(--color-primary);
|
|
431
483
|
outline-offset: -1px;
|
|
432
484
|
}
|
|
433
|
-
.rk-combo-input[data-v-
|
|
485
|
+
.rk-combo-input[data-v-aa2335fa]:disabled {
|
|
434
486
|
opacity: 0.5;
|
|
435
487
|
}
|
|
436
|
-
.rk-combo-toggle[data-v-
|
|
488
|
+
.rk-combo-toggle[data-v-aa2335fa] {
|
|
437
489
|
position: absolute;
|
|
438
490
|
right: 0.5rem;
|
|
439
491
|
color: var(--color-ink-soft);
|
|
440
492
|
font-size: 0.75rem;
|
|
441
493
|
}
|
|
442
|
-
.rk-combo-list[data-v-
|
|
494
|
+
.rk-combo-list[data-v-aa2335fa] {
|
|
443
495
|
position: absolute;
|
|
444
496
|
top: calc(100% - 1.25rem);
|
|
445
497
|
z-index: 50;
|
|
@@ -449,29 +501,48 @@
|
|
|
449
501
|
border-radius: var(--radius-card);
|
|
450
502
|
padding: 0.25rem;
|
|
451
503
|
}
|
|
452
|
-
.rk-combo-option[data-v-
|
|
504
|
+
.rk-combo-option[data-v-aa2335fa] {
|
|
453
505
|
cursor: pointer;
|
|
454
506
|
border-radius: var(--radius-cell);
|
|
455
507
|
padding: 0.5rem 0.625rem;
|
|
456
508
|
font-size: 0.875rem;
|
|
457
509
|
color: var(--color-ink);
|
|
458
510
|
}
|
|
459
|
-
|
|
511
|
+
|
|
512
|
+
/* A virtual row's height is the arithmetic the spacers are built on, so it
|
|
513
|
+
cannot be left to the text inside it. */
|
|
514
|
+
.rk-combo-option[style][data-v-aa2335fa] {
|
|
515
|
+
display: flex;
|
|
516
|
+
align-items: center;
|
|
517
|
+
padding-block: 0;
|
|
518
|
+
}
|
|
519
|
+
.rk-combo-option.is-highlighted[data-v-aa2335fa] {
|
|
460
520
|
background: var(--color-muted);
|
|
461
521
|
}
|
|
462
|
-
.rk-combo-option.is-selected[data-v-
|
|
522
|
+
.rk-combo-option.is-selected[data-v-aa2335fa] {
|
|
463
523
|
font-weight: 600;
|
|
464
524
|
}
|
|
465
|
-
.rk-combo-empty[data-v-
|
|
525
|
+
.rk-combo-empty[data-v-aa2335fa],
|
|
526
|
+
.rk-combo-status[data-v-aa2335fa] {
|
|
466
527
|
padding: 0.75rem 0.625rem;
|
|
467
528
|
font-size: 0.875rem;
|
|
468
529
|
color: var(--color-ink-soft);
|
|
469
530
|
}
|
|
470
|
-
.rk-combo-
|
|
531
|
+
.rk-combo-status[data-v-aa2335fa] {
|
|
532
|
+
display: flex;
|
|
533
|
+
flex-direction: column;
|
|
534
|
+
gap: 0.5rem;
|
|
535
|
+
}
|
|
536
|
+
.rk-combo-status-rows[data-v-aa2335fa] {
|
|
537
|
+
display: flex;
|
|
538
|
+
flex-direction: column;
|
|
539
|
+
gap: 0.5rem;
|
|
540
|
+
}
|
|
541
|
+
.rk-combo-error[data-v-aa2335fa] {
|
|
471
542
|
font-size: 0.75rem;
|
|
472
543
|
color: var(--color-negative);
|
|
473
544
|
}
|
|
474
|
-
.rk-combo-hint[data-v-
|
|
545
|
+
.rk-combo-hint[data-v-aa2335fa] {
|
|
475
546
|
font-size: 0.75rem;
|
|
476
547
|
color: var(--color-ink-soft);
|
|
477
548
|
}
|
package/dist/web.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as inertOutside, o as ensureSheetRoot, r as useBoundValue, t as BaseSheet_default } from "./BaseSheet-B7401rS7.js";
|
|
2
|
-
import { n as
|
|
2
|
+
import { n as BaseSkeleton_default, r as useMediaQuery, t as BaseListbox_default } from "./BaseListbox-BnaHlmMN.js";
|
|
3
3
|
import { t as _plugin_vue_export_helper_default } from "./_plugin-vue_export-helper-BOaGB7Aw.js";
|
|
4
4
|
import { t as BaseButton_default } from "./BaseButton-RN2an975.js";
|
|
5
5
|
import { Fragment, Teleport, Transition, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createSlots, createTextVNode, createVNode, defineComponent, mergeModels, nextTick, normalizeClass, normalizeStyle, onBeforeUnmount, onMounted, openBlock, ref, renderList, renderSlot, resolveDynamicComponent, shallowRef, toDisplayString, unref, useId, useModel, useTemplateRef, vModelText, vShow, watch, withCtx, withDirectives, withModifiers } from "vue";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rei-kit",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.13.0",
|
|
4
4
|
"description": "One kit, every look. 88 accessible Vue 3 + Tailwind 4 components, four materials (quiet, glass, brutal, soft) and ten WCAG-checked palettes, each switched with one attribute.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"vue",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"BaseSkeleton-DnqwogzV.js","names":[],"sources":["../src/composables/use-media-query.ts","../src/components/BaseListbox.vue","../src/components/BaseListbox.vue","../src/components/BaseSkeleton.vue","../src/components/BaseSkeleton.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\" 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","<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"],"mappings":"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"}
|