rei-kit 2.18.1 → 2.20.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 +4 -4
- package/dist/{BaseListbox-BnaHlmMN.js → BaseListbox-Btibgnaf.js} +48 -2
- package/dist/BaseListbox-Btibgnaf.js.map +1 -0
- package/dist/components/ActivityGrid.vue.d.ts +52 -0
- package/dist/components/CodeBlock.vue.d.ts +57 -0
- package/dist/composables/use-dialog-shell.d.ts +11 -0
- package/dist/composables/use-menu-keys.d.ts +26 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +423 -217
- package/dist/index.js.map +1 -1
- package/dist/styles.css +349 -33
- package/dist/web/BaseContextMenu.vue.d.ts +77 -0
- package/dist/web/BaseDrawer.vue.d.ts +68 -0
- package/dist/web/BaseHoverCard.vue.d.ts +67 -0
- package/dist/web/index.d.ts +15 -0
- package/dist/web.js +397 -106
- package/dist/web.js.map +1 -1
- package/package.json +2 -2
- package/dist/BaseListbox-BnaHlmMN.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# rei-kit
|
|
2
2
|
|
|
3
|
-
**One kit. Every look.**
|
|
3
|
+
**One kit. Every look.** 97 accessible components for Vue 3 and Tailwind 4.
|
|
4
4
|
Change the **material** with one attribute and the **palette** with another —
|
|
5
5
|
every component follows, and none of them knows your brand.
|
|
6
6
|
|
|
@@ -112,7 +112,7 @@ Each claim here is enforced by something that fails, not by a promise.
|
|
|
112
112
|
|
|
113
113
|
| Standard | How it is held |
|
|
114
114
|
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
115
|
-
| **Open for extension, closed for edits** | A new look is a token, a material or a palette — never an edit to a component. Materials and palettes restyle all
|
|
115
|
+
| **Open for extension, closed for edits** | A new look is a token, a material or a palette — never an edit to a component. Materials and palettes restyle all 97 without touching one, and a test fails if a component holds a colour. |
|
|
116
116
|
| **Single responsibility** | One part, one job. Label-and-error wiring lives in `FormField`, not in five inputs; taking the page behind a layer out of reach lives in one helper the modal, the sheet and the guide share. |
|
|
117
117
|
| **Depend on roles, not values** | Components read `primary`, `surface`, `--shadow-card` — never a hex or a pixel shadow. An app's brand wins in both modes, tested. |
|
|
118
118
|
| **WAI-ARIA Authoring Practices** | Menus, comboboxes, tabs, sliders, dialogs and accordions follow their APG pattern: arrows move, Tab leaves, Escape closes, focus returns. Behaviour tests drive each with the keyboard. |
|
|
@@ -124,11 +124,11 @@ Each claim here is enforced by something that fails, not by a promise.
|
|
|
124
124
|
|
|
125
125
|
## Status
|
|
126
126
|
|
|
127
|
-
**v2.
|
|
127
|
+
**v2.19.0 — three consumers.**
|
|
128
128
|
|
|
129
129
|
| | |
|
|
130
130
|
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
131
|
-
| Components |
|
|
131
|
+
| Components | 97 (`AuthForm`, `BaseTable`, `BaseCombobox`, `BaseSlider`, `TabShell`, `BaseModal`, `BaseTabs`, `BaseTooltip`, `BasePagination`, `BaseBreadcrumb`, `BaseDisclosure`, `BaseAccordion`, `NavLinks`, `OfflineBanner`, `FabButton`, `BaseButton`, `BaseCard`, `BaseInput`, `BaseSelect`, `BaseTextarea`, `BaseCheckbox`, `BaseSwitch`, `BaseRadioGroup`, `BaseMenu`, `BaseAvatar`, `BaseSpinner`, `BaseAlert`, `BaseBadge`, `BaseSheet`, `ProgressBar`, `PriceCard`, `ToastHost`, `TabBar`, `GoogleButton`, `LocaleLinks`, `LocaleSheet`, `AuthShell`, `TourShell`, `InstallPrompt`, `UpdatePrompt`, `InstallSettings`, `SkeletonList`, `PageContainer`, `ErrorBoundary`, etc.) |
|
|
132
132
|
| Composables | 16 (`useToast`, `useTheme`, `useMaterial`, `usePalette`, `useToday`, `useMediaQuery`, `useInstall`, `watchInstallability`, `createTabTransition`, `useThemeSync`, `useVisualViewport`, etc.) |
|
|
133
133
|
| Utilities | 33 (`applyTheme`, `applyMaterial`, `applyPalette`, `MATERIALS`, `PALETTES`, `formatDate`, `fieldErrors`, `toAuthMessageKey`, `createAuthGuard`, `createQueryDefaults`, `createWriteReport`, `toRedirectPath`, `Supabase error mapper`, i18n runtime, etc.) |
|
|
134
134
|
| Entry Points | `rei-kit`, `rei-kit/app`, `rei-kit/web`, `rei-kit/pwa`, `rei-kit/supabase`, `rei-kit/mobile.css`, `rei-kit/web.css`, and each stylesheet on its own |
|
|
@@ -39,6 +39,52 @@ function useMediaQuery(query) {
|
|
|
39
39
|
return matches;
|
|
40
40
|
}
|
|
41
41
|
//#endregion
|
|
42
|
+
//#region src/composables/use-menu-keys.ts
|
|
43
|
+
function useMenuKeys({ panel, onClose }) {
|
|
44
|
+
const items = () => panel.value ? Array.from(panel.value.querySelectorAll("[role=\"menuitem\"]")) : [];
|
|
45
|
+
function focusAt(index) {
|
|
46
|
+
const list = items();
|
|
47
|
+
if (list.length === 0) return;
|
|
48
|
+
list[(index + list.length) % list.length]?.focus();
|
|
49
|
+
}
|
|
50
|
+
const currentIndex = () => items().indexOf(document.activeElement);
|
|
51
|
+
/** Returns whether the key was one of the menu's own. */
|
|
52
|
+
function onKeydown(event) {
|
|
53
|
+
switch (event.key) {
|
|
54
|
+
case "Escape":
|
|
55
|
+
event.preventDefault();
|
|
56
|
+
onClose();
|
|
57
|
+
return true;
|
|
58
|
+
case "ArrowDown":
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
focusAt(currentIndex() + 1);
|
|
61
|
+
return true;
|
|
62
|
+
case "ArrowUp":
|
|
63
|
+
event.preventDefault();
|
|
64
|
+
focusAt(currentIndex() - 1);
|
|
65
|
+
return true;
|
|
66
|
+
case "Home":
|
|
67
|
+
event.preventDefault();
|
|
68
|
+
focusAt(0);
|
|
69
|
+
return true;
|
|
70
|
+
case "End":
|
|
71
|
+
event.preventDefault();
|
|
72
|
+
focusAt(items().length - 1);
|
|
73
|
+
return true;
|
|
74
|
+
case "Tab":
|
|
75
|
+
onClose();
|
|
76
|
+
return true;
|
|
77
|
+
default: return false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
items,
|
|
82
|
+
focusAt,
|
|
83
|
+
currentIndex,
|
|
84
|
+
onKeydown
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
42
88
|
//#region src/components/BaseSkeleton.vue
|
|
43
89
|
var BaseSkeleton_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__ */ defineComponent({
|
|
44
90
|
__name: "BaseSkeleton",
|
|
@@ -203,6 +249,6 @@ var BaseListbox_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__
|
|
|
203
249
|
}
|
|
204
250
|
}), [["__scopeId", "data-v-7f8cd788"]]);
|
|
205
251
|
//#endregion
|
|
206
|
-
export { BaseSkeleton_default as n,
|
|
252
|
+
export { useMediaQuery as i, BaseSkeleton_default as n, useMenuKeys as r, BaseListbox_default as t };
|
|
207
253
|
|
|
208
|
-
//# sourceMappingURL=BaseListbox-
|
|
254
|
+
//# sourceMappingURL=BaseListbox-Btibgnaf.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BaseListbox-Btibgnaf.js","names":[],"sources":["../src/composables/use-media-query.ts","../src/composables/use-menu-keys.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","import type { Ref } from 'vue'\n\n/**\n * The keyboard contract `role=\"menu\"` promises.\n *\n * Declaring `role=\"menu\"` is a promise to a screen reader: the arrows move\n * through the items, Home and End go to the ends, Escape closes, and Tab\n * *leaves* rather than cycling inside. That last one is the single place a\n * menu differs from a dialog, and getting it backwards traps somebody in a\n * list of links.\n *\n * It lives here because a menu anchored to a button and a menu anchored to\n * wherever the pointer was are the same keyboard and two different\n * positions. Only the position is worth writing twice.\n */\nexport interface MenuKeysOptions {\n /** The element holding the `[role=\"menuitem\"]` children. */\n panel: Ref<HTMLElement | null>\n /** Close it — Escape, and Tab on the way out. */\n onClose: () => void\n}\n\nexport function useMenuKeys({ panel, onClose }: MenuKeysOptions) {\n const items = (): HTMLElement[] =>\n panel.value ? Array.from(panel.value.querySelectorAll<HTMLElement>('[role=\"menuitem\"]')) : []\n\n function focusAt(index: number) {\n const list = items()\n if (list.length === 0) return\n\n // Wrapping, because a list with no edges is faster than one you fall off.\n list[(index + list.length) % list.length]?.focus()\n }\n\n const currentIndex = () => items().indexOf(document.activeElement as HTMLElement)\n\n /** Returns whether the key was one of the menu's own. */\n function onKeydown(event: KeyboardEvent): boolean {\n switch (event.key) {\n case 'Escape':\n event.preventDefault()\n onClose()\n return true\n case 'ArrowDown':\n event.preventDefault()\n focusAt(currentIndex() + 1)\n return true\n case 'ArrowUp':\n event.preventDefault()\n focusAt(currentIndex() - 1)\n return true\n case 'Home':\n event.preventDefault()\n focusAt(0)\n return true\n case 'End':\n event.preventDefault()\n focusAt(items().length - 1)\n return true\n case 'Tab':\n onClose()\n return true\n default:\n return false\n }\n }\n\n return { items, focusAt, currentIndex, onKeydown }\n}\n","<script setup lang=\"ts\">\n/**\n * One grey box standing in for content that has not arrived.\n *\n * `SkeletonList` is rows of these for a list; this is the primitive, for the\n * shapes a list does not cover — an avatar, a heading, a chart.\n *\n * A height is a CSS length, never a class: `h-4` inside a component the app\n * does not control would render at zero the day that utility is not in the\n * app's stylesheet, which is exactly how every skeleton in one app came out\n * invisible.\n *\n * It is hidden from assistive tech. The thing that is loading says so —\n * through `aria-busy`, or a status message — and a screen reader reading\n * \"blank, blank, blank\" helps nobody.\n */\nconst {\n width = '100%',\n height = '1rem',\n shape = 'block',\n} = defineProps<{\n /** Any CSS length: `12rem`, `60%`, `8ch`. */\n width?: string | undefined\n height?: string | undefined\n /** `circle` makes a round one and squares the width to the height. */\n shape?: 'block' | 'text' | 'circle' | undefined\n}>()\n</script>\n\n<template>\n <span\n class=\"rk-skeleton\"\n :class=\"`is-${shape}`\"\n :style=\"{ width: shape === 'circle' ? height : width, height }\"\n aria-hidden=\"true\"\n />\n</template>\n\n<style scoped>\n.rk-skeleton {\n display: block;\n flex-shrink: 0;\n background: var(--color-muted);\n animation: rk-skeleton-pulse 1.6s ease-in-out infinite;\n}\n\n.rk-skeleton.is-block {\n border-radius: var(--radius-cell);\n}\n\n/* A line of text, rounded like one and a little short of its box. */\n.rk-skeleton.is-text {\n border-radius: 9999px;\n}\n\n.rk-skeleton.is-circle {\n border-radius: 9999px;\n}\n\n@keyframes rk-skeleton-pulse {\n 50% {\n opacity: 0.55;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .rk-skeleton {\n animation: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\n/**\n * One grey box standing in for content that has not arrived.\n *\n * `SkeletonList` is rows of these for a list; this is the primitive, for the\n * shapes a list does not cover — an avatar, a heading, a chart.\n *\n * A height is a CSS length, never a class: `h-4` inside a component the app\n * does not control would render at zero the day that utility is not in the\n * app's stylesheet, which is exactly how every skeleton in one app came out\n * invisible.\n *\n * It is hidden from assistive tech. The thing that is loading says so —\n * through `aria-busy`, or a status message — and a screen reader reading\n * \"blank, blank, blank\" helps nobody.\n */\nconst {\n width = '100%',\n height = '1rem',\n shape = 'block',\n} = defineProps<{\n /** Any CSS length: `12rem`, `60%`, `8ch`. */\n width?: string | undefined\n height?: string | undefined\n /** `circle` makes a round one and squares the width to the height. */\n shape?: 'block' | 'text' | 'circle' | undefined\n}>()\n</script>\n\n<template>\n <span\n class=\"rk-skeleton\"\n :class=\"`is-${shape}`\"\n :style=\"{ width: shape === 'circle' ? height : width, height }\"\n aria-hidden=\"true\"\n />\n</template>\n\n<style scoped>\n.rk-skeleton {\n display: block;\n flex-shrink: 0;\n background: var(--color-muted);\n animation: rk-skeleton-pulse 1.6s ease-in-out infinite;\n}\n\n.rk-skeleton.is-block {\n border-radius: var(--radius-cell);\n}\n\n/* A line of text, rounded like one and a little short of its box. */\n.rk-skeleton.is-text {\n border-radius: 9999px;\n}\n\n.rk-skeleton.is-circle {\n border-radius: 9999px;\n}\n\n@keyframes rk-skeleton-pulse {\n 50% {\n opacity: 0.55;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .rk-skeleton {\n animation: none;\n }\n}\n</style>\n","<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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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { WeekStart } from '../utils/date';
|
|
2
|
+
/**
|
|
3
|
+
* A year of days, as a grid you can read at a glance.
|
|
4
|
+
*
|
|
5
|
+
* ## It is a table, and that is not a detail
|
|
6
|
+
*
|
|
7
|
+
* Weekdays are the rows and weeks are the columns, so the markup is a
|
|
8
|
+
* `<table>` and the visual layout falls out of it. The obvious way — one
|
|
9
|
+
* CSS grid flowing column by column — puts the DOM in a different order
|
|
10
|
+
* from the picture, and anything laid over that order afterwards
|
|
11
|
+
* (`role="grid"`, arrow keys, a reader's own table navigation) then
|
|
12
|
+
* describes a shape that is not on screen.
|
|
13
|
+
*
|
|
14
|
+
* ## One stop, not three hundred and sixty-five
|
|
15
|
+
*
|
|
16
|
+
* Every day is reachable, and the whole grid is a single stop in the tab
|
|
17
|
+
* order: the arrows move between days, Home and End go to the ends of a
|
|
18
|
+
* week, and Enter chooses. A grid of focusable cells is the other way this
|
|
19
|
+
* is written, and it costs somebody using a keyboard a year of presses to
|
|
20
|
+
* get past a picture.
|
|
21
|
+
*
|
|
22
|
+
* ## The colours are yours
|
|
23
|
+
*
|
|
24
|
+
* `levelFor` returns a class, the same contract as `ToneDot`'s `fill` and
|
|
25
|
+
* `BaseTimeline`'s: the kit does not know whether four of something is a
|
|
26
|
+
* lot, or which hue your product uses for "done". `dayLabel` is the same
|
|
27
|
+
* for words — the kit has no language, and a date reads differently in
|
|
28
|
+
* every one.
|
|
29
|
+
*/
|
|
30
|
+
type __VLS_Props = {
|
|
31
|
+
/** The days to draw, as `YYYY-MM-DD`, oldest first. */
|
|
32
|
+
days: readonly string[];
|
|
33
|
+
/** Names the grid, e.g. "Your year". Already translated. */
|
|
34
|
+
label: string;
|
|
35
|
+
/** A class for one day's square, e.g. `bg-positive/40`. */
|
|
36
|
+
levelFor: (key: string) => string;
|
|
37
|
+
/** What a reader hears on a day: the date, and what happened. Translated. */
|
|
38
|
+
dayLabel: (key: string) => string;
|
|
39
|
+
/** Which days lead somewhere. Unset, the grid is a picture and nothing else. */
|
|
40
|
+
isSelectable?: ((key: string) => boolean) | undefined;
|
|
41
|
+
/** `1` Monday, `0` Sunday. */
|
|
42
|
+
weekStartsOn?: WeekStart | undefined;
|
|
43
|
+
/** Open on the most recent weeks — a year otherwise starts far off-screen. */
|
|
44
|
+
startAtEnd?: boolean | undefined;
|
|
45
|
+
};
|
|
46
|
+
declare const __VLS_export: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {
|
|
47
|
+
select: (key: string) => any;
|
|
48
|
+
}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
49
|
+
onSelect?: (key: string) => any;
|
|
50
|
+
}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
|
|
51
|
+
declare const _default: typeof __VLS_export;
|
|
52
|
+
export default _default;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A sample of code, as written.
|
|
3
|
+
*
|
|
4
|
+
* ## No highlighting, and no `v-html`
|
|
5
|
+
*
|
|
6
|
+
* A code sample is the one place where what is written is exactly what is
|
|
7
|
+
* meant, so the text goes in as text. Highlighting would mean either a
|
|
8
|
+
* parser in the bundle of every app that shows one line of shell, or
|
|
9
|
+
* `v-html` and a promise the app cannot keep about where the string came
|
|
10
|
+
* from. An app that wants colour can highlight on its own and put the
|
|
11
|
+
* result in the default slot; the frame, the scrolling and the copying
|
|
12
|
+
* stay here.
|
|
13
|
+
*
|
|
14
|
+
* ## The scroll is the accessibility problem
|
|
15
|
+
*
|
|
16
|
+
* A long line makes this scroll sideways, and a scrolling box that cannot
|
|
17
|
+
* take focus cannot be scrolled by a keyboard at all — the end of the line
|
|
18
|
+
* is simply unreachable, with nothing on screen to say so. So when it can
|
|
19
|
+
* scroll it is focusable and named, which is also what makes a screen
|
|
20
|
+
* reader announce it as a region rather than dropping the reader into loose
|
|
21
|
+
* text.
|
|
22
|
+
*
|
|
23
|
+
* `wrap` is the other answer: no scrolling, no focus stop, and long lines
|
|
24
|
+
* broken instead. Which one is right depends on the code — a shell command
|
|
25
|
+
* wraps well, a table of output does not — so it is the app's to choose.
|
|
26
|
+
*/
|
|
27
|
+
type __VLS_Props = {
|
|
28
|
+
/** The sample itself. Shown exactly as given. */
|
|
29
|
+
code: string;
|
|
30
|
+
/**
|
|
31
|
+
* Names the block, e.g. "Installing the package".
|
|
32
|
+
*
|
|
33
|
+
* Required because this is a region a reader can land in, and an unnamed
|
|
34
|
+
* one is announced as nothing at all.
|
|
35
|
+
*/
|
|
36
|
+
label: string;
|
|
37
|
+
/** Shown in the corner, e.g. `bash`, `ts`. Not used to highlight anything. */
|
|
38
|
+
language?: string | undefined;
|
|
39
|
+
/** Break long lines instead of scrolling sideways. */
|
|
40
|
+
wrap?: boolean | undefined;
|
|
41
|
+
/** Given together with `copiedLabel`, adds a copy button. Already translated. */
|
|
42
|
+
copyLabel?: string | undefined;
|
|
43
|
+
copiedLabel?: string | undefined;
|
|
44
|
+
};
|
|
45
|
+
type __VLS_Slots = {
|
|
46
|
+
/** The body, when the app has highlighted it itself. Plain text otherwise. */
|
|
47
|
+
default?: () => unknown;
|
|
48
|
+
};
|
|
49
|
+
declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
|
|
50
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
51
|
+
declare const _default: typeof __VLS_export;
|
|
52
|
+
export default _default;
|
|
53
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
54
|
+
new (): {
|
|
55
|
+
$slots: S;
|
|
56
|
+
};
|
|
57
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Ref } from 'vue';
|
|
2
|
+
export interface DialogShellOptions {
|
|
3
|
+
/** Whether Escape and the scrim close it. */
|
|
4
|
+
dismissible?: () => boolean;
|
|
5
|
+
/** Called when the shell decides it should close. */
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function useDialogShell(open: Ref<boolean>, panel: Ref<HTMLElement | null>, { dismissible, onClose }: DialogShellOptions): {
|
|
9
|
+
onKeydown: (event: KeyboardEvent) => void;
|
|
10
|
+
focusable: () => HTMLElement[];
|
|
11
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Ref } from 'vue';
|
|
2
|
+
/**
|
|
3
|
+
* The keyboard contract `role="menu"` promises.
|
|
4
|
+
*
|
|
5
|
+
* Declaring `role="menu"` is a promise to a screen reader: the arrows move
|
|
6
|
+
* through the items, Home and End go to the ends, Escape closes, and Tab
|
|
7
|
+
* *leaves* rather than cycling inside. That last one is the single place a
|
|
8
|
+
* menu differs from a dialog, and getting it backwards traps somebody in a
|
|
9
|
+
* list of links.
|
|
10
|
+
*
|
|
11
|
+
* It lives here because a menu anchored to a button and a menu anchored to
|
|
12
|
+
* wherever the pointer was are the same keyboard and two different
|
|
13
|
+
* positions. Only the position is worth writing twice.
|
|
14
|
+
*/
|
|
15
|
+
export interface MenuKeysOptions {
|
|
16
|
+
/** The element holding the `[role="menuitem"]` children. */
|
|
17
|
+
panel: Ref<HTMLElement | null>;
|
|
18
|
+
/** Close it — Escape, and Tab on the way out. */
|
|
19
|
+
onClose: () => void;
|
|
20
|
+
}
|
|
21
|
+
export declare function useMenuKeys({ panel, onClose }: MenuKeysOptions): {
|
|
22
|
+
items: () => HTMLElement[];
|
|
23
|
+
focusAt: (index: number) => void;
|
|
24
|
+
currentIndex: () => number;
|
|
25
|
+
onKeydown: (event: KeyboardEvent) => boolean;
|
|
26
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -90,6 +90,11 @@ export { default as LocaleLinks } from './components/LocaleLinks.vue';
|
|
|
90
90
|
export { default as GoogleButton } from './components/GoogleButton.vue';
|
|
91
91
|
export { default as TabBar } from './components/TabBar.vue';
|
|
92
92
|
export type { TabItem } from './components/TabBar.vue';
|
|
93
|
+
/**
|
|
94
|
+
* `ActivityGrid`, a year at a glance — a real table, so the DOM order is
|
|
95
|
+
* the picture's order, and one tab stop rather than three hundred odd.
|
|
96
|
+
*/
|
|
97
|
+
export { default as ActivityGrid } from './components/ActivityGrid.vue';
|
|
93
98
|
export { default as AvatarStack } from './components/AvatarStack.vue';
|
|
94
99
|
export { default as BaseCalendar } from './components/BaseCalendar.vue';
|
|
95
100
|
export { default as BaseChip } from './components/BaseChip.vue';
|
|
@@ -102,6 +107,11 @@ export { default as BaseSeparator } from './components/BaseSeparator.vue';
|
|
|
102
107
|
export { default as BaseSkeleton } from './components/BaseSkeleton.vue';
|
|
103
108
|
export { default as ColorPicker } from './components/ColorPicker.vue';
|
|
104
109
|
export type { ColorSwatch } from './components/ColorPicker.vue';
|
|
110
|
+
/**
|
|
111
|
+
* `CodeBlock`, a sample as written — no highlighting and no `v-html`, and
|
|
112
|
+
* focusable when it scrolls, or the end of a long line is unreachable.
|
|
113
|
+
*/
|
|
114
|
+
export { default as CodeBlock } from './components/CodeBlock.vue';
|
|
105
115
|
export { default as CopyButton } from './components/CopyButton.vue';
|
|
106
116
|
export { default as DescriptionList } from './components/DescriptionList.vue';
|
|
107
117
|
export type { DescriptionItem } from './components/DescriptionList.vue';
|