@revojs/ui 0.2.11 → 0.2.13

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.
Files changed (59) hide show
  1. package/dist/components/button.d.ts +1 -0
  2. package/dist/components/card.d.ts +8 -2
  3. package/dist/components/collection.d.ts +3 -0
  4. package/dist/components/container.d.ts +2 -3
  5. package/dist/components/dialog.d.ts +3 -3
  6. package/dist/components/entry.d.ts +6 -0
  7. package/dist/components/entry.js +1 -0
  8. package/dist/components/field.d.ts +4 -0
  9. package/dist/components/input.d.ts +3 -0
  10. package/dist/components/notification.d.ts +9 -0
  11. package/dist/components/notification.js +1 -0
  12. package/dist/components/pagination.d.ts +8 -0
  13. package/dist/components/pagination.js +1 -0
  14. package/dist/components/popover.d.ts +9 -1
  15. package/dist/components/select.d.ts +8 -5
  16. package/dist/components/sheet.d.ts +3 -3
  17. package/dist/components/shell.d.ts +6 -0
  18. package/dist/components/shell.js +1 -0
  19. package/dist/components/textarea.d.ts +13 -0
  20. package/dist/components/textarea.js +1 -0
  21. package/dist/components/upload.d.ts +2 -0
  22. package/dist/index.d.ts +5 -0
  23. package/dist/module/index.d.ts +16 -0
  24. package/dist/module/index.js +32 -0
  25. package/dist/style.css +16 -0
  26. package/dist/vue/components/breadcrumb.vue +6 -1
  27. package/dist/vue/components/button.vue +34 -13
  28. package/dist/vue/components/card.vue +27 -14
  29. package/dist/vue/components/checkbox.vue +18 -4
  30. package/dist/vue/components/collection.vue +22 -7
  31. package/dist/vue/components/container.vue +11 -2
  32. package/dist/vue/components/counter.vue +7 -4
  33. package/dist/vue/components/dialog.vue +53 -36
  34. package/dist/vue/components/entry.vue +52 -0
  35. package/dist/vue/components/field.vue +44 -9
  36. package/dist/vue/components/input.vue +33 -6
  37. package/dist/vue/components/link.vue +1 -1
  38. package/dist/vue/components/navigate.vue +1 -0
  39. package/dist/vue/components/notification.vue +58 -0
  40. package/dist/vue/components/pagination.vue +24 -0
  41. package/dist/vue/components/popover.vue +46 -18
  42. package/dist/vue/components/progress.vue +2 -2
  43. package/dist/vue/components/radio.vue +19 -5
  44. package/dist/vue/components/range.vue +1 -1
  45. package/dist/vue/components/rating.vue +2 -2
  46. package/dist/vue/components/select.vue +116 -35
  47. package/dist/vue/components/separator.vue +2 -2
  48. package/dist/vue/components/sheet.vue +56 -22
  49. package/dist/vue/components/shell.vue +38 -0
  50. package/dist/vue/components/switch.vue +21 -7
  51. package/dist/vue/components/textarea.vue +49 -0
  52. package/dist/vue/components/upload.vue +74 -23
  53. package/dist/vue/index.d.ts +5 -0
  54. package/dist/vue/index.js +6 -1
  55. package/dist/vue/utilities/use-control-attributes.d.ts +11 -0
  56. package/dist/vue/utilities/use-control-attributes.js +14 -0
  57. package/dist/vue/utilities/use-modal-dialog.d.ts +9 -0
  58. package/dist/vue/utilities/use-modal-dialog.js +48 -0
  59. package/package.json +1 -1
@@ -1,5 +1,14 @@
1
1
  <template>
2
- <Stack gap="5" :padding="padding" :class="sizes[size]" class="mx-auto w-full">
2
+ <Stack
3
+ :gap="gap"
4
+ :padding="padding"
5
+ :direction="direction"
6
+ :align="align"
7
+ :justify="justify"
8
+ :wrap="wrap"
9
+ :class="sizes[size]"
10
+ class="mx-auto w-full overflow-y-scroll [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
11
+ >
3
12
  <slot />
4
13
  </Stack>
5
14
  </template>
@@ -8,7 +17,7 @@
8
17
  import type { ContainerProps, ContainerSize } from "../../components/container";
9
18
  import Stack from "./stack.vue";
10
19
 
11
- const { size = "5xl", padding = "5" } = defineProps<ContainerProps>();
20
+ const { size = "5xl", padding = "5", gap = "5", direction, align, justify, wrap } = defineProps<ContainerProps>();
12
21
 
13
22
  const sizes: Record<ContainerSize, string> = {
14
23
  "3xl": "max-w-3xl",
@@ -1,10 +1,8 @@
1
1
  <template>
2
2
  <Stack>
3
- <span v-if="label" class="text-sm font-medium">{{ label }}</span>
3
+ <span v-if="label" class="text-sm font-normal">{{ label }}</span>
4
4
 
5
- <div
6
- class="grid h-10 grid-cols-[2.5rem_minmax(0,1fr)_2.5rem] items-center overflow-hidden rounded-ui border border-input"
7
- >
5
+ <div class="relative grid grid-cols-[auto_minmax(0,1fr)_auto] items-center overflow-hidden rounded-ui">
8
6
  <Button
9
7
  class="text-lg"
10
8
  size="icon"
@@ -26,6 +24,11 @@
26
24
  aria-label="Increase"
27
25
  @click="change(step)"
28
26
  />
27
+
28
+ <span
29
+ aria-hidden="true"
30
+ class="pointer-events-none absolute inset-0 rounded-ui ring-1 ring-input ring-inset"
31
+ />
29
32
  </div>
30
33
  </Stack>
31
34
  </template>
@@ -1,14 +1,40 @@
1
1
  <template>
2
2
  <dialog
3
3
  ref="dialog"
4
- class="m-auto w-full max-w-lg border-0 bg-transparent p-5 backdrop:bg-black/50"
5
- @close="$emit('update:modelValue', false)"
6
- @click="$event.target === $event.currentTarget && ($event.currentTarget as HTMLDialogElement).close()"
4
+ class="fixed inset-0 m-0 h-full max-h-none w-full max-w-none flex-col items-center overflow-y-auto border-0 bg-transparent p-5 backdrop:bg-black/50 open:flex [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
5
+ :aria-labelledby="label && !$slots.header ? (labelId ?? id) : ($attrs as HTMLAttributes)['aria-labelledby']"
6
+ @close="handleClose"
7
+ @cancel="dismiss"
8
+ @click="$event.target === $event.currentTarget && dismiss($event)"
7
9
  >
8
- <Card :title="title" :description="description" class="shadow-2xl">
10
+ <Card
11
+ :label="label"
12
+ :label-id="labelId ?? id"
13
+ :body-padding="bodyPadding"
14
+ :body-gap="bodyGap"
15
+ :body-direction="bodyDirection"
16
+ :body-align="bodyAlign"
17
+ :body-justify="bodyJustify"
18
+ :body-wrap="bodyWrap"
19
+ class="my-auto max-w-3xl shrink-0 shadow-2xl"
20
+ >
21
+ <template v-if="$slots.header" #header>
22
+ <slot name="header" />
23
+ </template>
24
+
9
25
  <template #actions>
10
- <Button size="icon" variant="ghost" :icon-prefix="CloseIcon" :aria-label="closeLabel" @click="close" />
26
+ <slot name="actions" />
27
+
28
+ <Button
29
+ size="icon"
30
+ variant="ghost"
31
+ :icon-prefix="CloseIcon"
32
+ :aria-label="closeLabel"
33
+ :disabled="!dismissible"
34
+ @click="dismiss"
35
+ />
11
36
  </template>
37
+
12
38
  <slot :open="open" :close="close" :toggle="toggle" />
13
39
 
14
40
  <template v-if="$slots.footer" #footer>
@@ -20,14 +46,25 @@
20
46
 
21
47
  <script setup lang="ts">
22
48
  import CloseIcon from "~icons/hugeicons/multiplication-sign";
23
- import { useTemplateRef, watchEffect } from "vue";
49
+ import { useId, useTemplateRef, type HTMLAttributes } from "vue";
24
50
  import type { DialogProps } from "../../components/dialog";
25
51
  import Button from "./button.vue";
26
52
  import Card from "./card.vue";
27
- import { useFocusTrap } from "../utilities/use-focus-trap";
28
- import { useScrollLock } from "../utilities/use-scroll-lock";
53
+ import { useModalDialog } from "../utilities/use-modal-dialog";
29
54
 
30
- const { modelValue = false, title, description, closeLabel = "Close" } = defineProps<DialogProps>();
55
+ const {
56
+ modelValue = undefined,
57
+ label,
58
+ labelId,
59
+ bodyPadding,
60
+ bodyGap,
61
+ bodyDirection,
62
+ bodyAlign,
63
+ bodyJustify,
64
+ bodyWrap,
65
+ closeLabel = "Close",
66
+ dismissible = true,
67
+ } = defineProps<DialogProps>();
31
68
 
32
69
  const emit = defineEmits<{
33
70
  "update:modelValue": [value: boolean];
@@ -35,34 +72,14 @@ const emit = defineEmits<{
35
72
 
36
73
  const dialog = useTemplateRef("dialog");
37
74
 
38
- useFocusTrap(dialog, () => modelValue);
39
-
40
- useScrollLock(() => modelValue);
41
-
42
- function open() {
43
- emit("update:modelValue", true);
44
- }
45
-
46
- function close() {
47
- dialog.value?.close();
48
- emit("update:modelValue", false);
49
- }
50
-
51
- function toggle() {
52
- if (modelValue) {
53
- close();
54
- } else {
55
- open();
56
- }
57
- }
75
+ const id = `dialog-${useId()}`;
58
76
 
59
- watchEffect(() => {
60
- if (modelValue && !dialog.value?.open) {
61
- dialog.value?.showModal();
62
- } else if (!modelValue && dialog.value?.open) {
63
- dialog.value.close();
64
- }
65
- });
77
+ const { open, close, toggle, dismiss, handleClose } = useModalDialog(
78
+ dialog,
79
+ () => modelValue,
80
+ (value) => emit("update:modelValue", value),
81
+ () => dismissible,
82
+ );
66
83
 
67
84
  defineExpose({ open, close, toggle });
68
85
  </script>
@@ -0,0 +1,52 @@
1
+ <template>
2
+ <Stack
3
+ :is="href ? 'a' : is"
4
+ :href="href"
5
+ :padding="padding"
6
+ :gap="gap"
7
+ :direction="direction"
8
+ :align="align"
9
+ :justify="justify"
10
+ :wrap="wrap"
11
+ :aria-current="active && href ? 'page' : undefined"
12
+ class="min-w-0 text-sm"
13
+ :class="[
14
+ href
15
+ ? 'cursor-pointer outline-ring hover:bg-ghost-hover active:bg-ghost-active focus-visible:outline-2 focus-visible:outline-offset-[-2px]'
16
+ : '',
17
+ active ? 'bg-secondary text-secondary-foreground' : '',
18
+ ]"
19
+ >
20
+ <Stack v-if="$slots.prefix" gap="0" class="shrink-0">
21
+ <slot name="prefix" />
22
+ </Stack>
23
+
24
+ <Stack gap="0" class="min-w-0 flex-1">
25
+ <slot
26
+ ><span class="truncate">{{ label }}</span></slot
27
+ >
28
+ </Stack>
29
+
30
+ <Stack v-if="$slots.suffix" gap="0" class="shrink-0">
31
+ <slot name="suffix" />
32
+ </Stack>
33
+ </Stack>
34
+ </template>
35
+
36
+ <script setup lang="ts">
37
+ import type { EntryProps } from "../../components/entry";
38
+ import Stack from "./stack.vue";
39
+
40
+ const {
41
+ is = "div",
42
+ href,
43
+ label,
44
+ active = false,
45
+ padding = "5",
46
+ gap = "3",
47
+ direction = "horizontal",
48
+ align = "center",
49
+ justify = "between",
50
+ wrap = false,
51
+ } = defineProps<EntryProps & { is?: string }>();
52
+ </script>
@@ -1,23 +1,58 @@
1
1
  <template>
2
- <Stack is="fieldset" :disabled="disabled">
3
- <legend v-if="label" class="mb-1 text-sm font-medium">
4
- {{ label }}<span v-if="required" aria-hidden="true"> *</span>
5
- </legend>
2
+ <Stack
3
+ :is="is ?? (controlId ? 'div' : 'fieldset')"
4
+ :disabled="is !== 'div' && !controlId ? disabled : undefined"
5
+ :class="texts[text]"
6
+ >
7
+ <component
8
+ :is="controlId ? 'label' : is === 'div' ? 'span' : 'legend'"
9
+ v-if="label"
10
+ :for="controlId"
11
+ class="font-normal"
12
+ :class="!controlId && is !== 'div' ? 'mb-1' : ''"
13
+ >
14
+ {{ label }}<span v-if="required" aria-hidden="true" class="text-destructive"> *</span>
15
+ </component>
6
16
 
7
- <slot />
17
+ <slot :control="controlAttributes" />
8
18
 
9
- <p v-if="error" class="text-sm text-destructive">
19
+ <p v-if="error" :id="`${id}-error`" role="alert" class="text-destructive">
10
20
  {{ error }}
11
21
  </p>
12
- <p v-else-if="description" class="text-sm opacity-60">
22
+ <p v-else-if="description" :id="`${id}-description`" class="opacity-60">
13
23
  {{ description }}
14
24
  </p>
15
25
  </Stack>
16
26
  </template>
17
27
 
18
28
  <script setup lang="ts">
19
- import type { FieldProps } from "../../components/field";
29
+ import { computed, useId } from "vue";
30
+ import type { FieldProps, FieldText } from "../../components/field";
20
31
  import Stack from "./stack.vue";
21
32
 
22
- const { label, description, error, required = false, disabled = false } = defineProps<FieldProps>();
33
+ const {
34
+ label,
35
+ description,
36
+ error,
37
+ text = "sm",
38
+ required = false,
39
+ disabled = false,
40
+ for: controlId,
41
+ is,
42
+ } = defineProps<FieldProps>();
43
+
44
+ const id = useId();
45
+
46
+ const controlAttributes = computed(() => ({
47
+ id: controlId,
48
+ required,
49
+ disabled,
50
+ "aria-describedby": error ? `${id}-error` : description ? `${id}-description` : undefined,
51
+ "aria-invalid": error ? true : undefined,
52
+ }));
53
+
54
+ const texts: Record<FieldText, string> = {
55
+ sm: "text-sm",
56
+ md: "text-base",
57
+ };
23
58
  </script>
@@ -1,17 +1,27 @@
1
1
  <template>
2
2
  <Stack
3
+ v-bind="rootAttributes"
3
4
  is="span"
4
5
  direction="horizontal"
5
6
  align="center"
6
- class="min-h-10 w-full cursor-text rounded-ui border border-input px-3 text-sm ring-ring focus-within:ring-2"
7
- :class="interactionVariants[interaction]"
7
+ gap="0"
8
+ class="w-full cursor-text rounded-ui bg-secondary p-3 ring-1 ring-input ring-inset focus-within:ring-2 focus-within:ring-ring"
9
+ :class="[
10
+ texts[text],
11
+ interactionVariants[interaction],
12
+ (invalid ?? (controlAttributes['aria-invalid'] === true || controlAttributes['aria-invalid'] === 'true'))
13
+ ? 'ring-2 ring-destructive'
14
+ : '',
15
+ ]"
8
16
  >
9
17
  <slot name="leading" />
10
18
 
11
19
  <Icon v-if="iconPrefix" :is="iconPrefix" :size="iconSize" />
12
20
 
13
21
  <input
14
- class="w-full min-w-0 appearance-none bg-transparent py-2 outline-none placeholder:opacity-50"
22
+ v-bind="controlAttributes"
23
+ ref="control"
24
+ class="w-full min-w-0 appearance-none bg-transparent px-1 outline-none placeholder:opacity-50"
15
25
  :value="modelValue"
16
26
  :type="type"
17
27
  :name="name"
@@ -20,6 +30,7 @@
20
30
  :required="required"
21
31
  :readonly="readonly"
22
32
  :disabled="disabled"
33
+ :aria-invalid="invalid ?? controlAttributes['aria-invalid']"
23
34
  @input="input"
24
35
  />
25
36
 
@@ -30,15 +41,19 @@
30
41
  </template>
31
42
 
32
43
  <script setup lang="ts">
33
- import { computed } from "vue";
44
+ import { computed, useTemplateRef } from "vue";
34
45
  import type { ControlInteraction } from "../../components/control.ts";
35
- import type { InputProps } from "../../components/input";
46
+ import type { InputProps, InputText } from "../../components/input";
36
47
  import Icon from "./icon.vue";
37
48
  import Stack from "./stack.vue";
49
+ import { useControlAttributes } from "../utilities/use-control-attributes";
50
+
51
+ defineOptions({ inheritAttrs: false });
38
52
 
39
53
  const {
40
54
  modelValue,
41
55
  type = "text",
56
+ text = "sm",
42
57
  iconPrefix,
43
58
  iconSuffix,
44
59
  placeholder,
@@ -47,9 +62,21 @@ const {
47
62
  required = false,
48
63
  readonly = false,
49
64
  disabled = false,
65
+ invalid = undefined,
50
66
  } = defineProps<InputProps>();
51
67
 
52
- const iconSize = 20;
68
+ const { rootAttributes, controlAttributes } = useControlAttributes();
69
+
70
+ const control = useTemplateRef("control");
71
+
72
+ defineExpose({ focus: () => control.value?.focus() });
73
+
74
+ const iconSize = computed(() => (text === "sm" ? 20 : 24));
75
+
76
+ const texts: Record<InputText, string> = {
77
+ sm: "text-sm",
78
+ md: "text-base",
79
+ };
53
80
 
54
81
  const emit = defineEmits<{
55
82
  "update:modelValue": [value: string | number];
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <component
3
3
  :is="is"
4
- class="cursor-pointer underline-offset-4 transition-colors hover:underline"
4
+ class="cursor-pointer underline-offset-4 hover:underline"
5
5
  :class="variants[variant]"
6
6
  :href="href"
7
7
  >
@@ -8,6 +8,7 @@
8
8
  :icon-prefix="iconPrefix"
9
9
  :icon-suffix="iconSuffix"
10
10
  :label="label"
11
+ :aria-current="active ? 'page' : undefined"
11
12
  />
12
13
  </template>
13
14
 
@@ -0,0 +1,58 @@
1
+ <template>
2
+ <div
3
+ :role="announce ? (variant === 'error' ? 'alert' : 'status') : undefined"
4
+ class="grid grid-cols-[auto_minmax(0,1fr)] items-center gap-x-3 gap-y-3 rounded-ui border p-3 text-sm"
5
+ :class="[variants[variant], $slots.actions ? 'sm:grid-cols-[auto_minmax(0,1fr)_auto]' : '']"
6
+ >
7
+ <Stack gap="0" class="shrink-0">
8
+ <slot name="icon">
9
+ <Icon :is="icon ?? icons[variant]" :size="20" />
10
+ </slot>
11
+ </Stack>
12
+
13
+ <Stack gap="1" class="min-w-0">
14
+ <p v-if="label" class="text-sm/5 font-medium">{{ label }}</p>
15
+
16
+ <div v-if="subtext || $slots.default" class="text-sm/5 text-muted-foreground">
17
+ <slot>{{ subtext }}</slot>
18
+ </div>
19
+ </Stack>
20
+
21
+ <Stack
22
+ v-if="$slots.actions"
23
+ direction="horizontal"
24
+ align="center"
25
+ gap="1"
26
+ wrap
27
+ class="col-start-2 sm:col-start-auto"
28
+ >
29
+ <slot name="actions" />
30
+ </Stack>
31
+ </div>
32
+ </template>
33
+
34
+ <script setup lang="ts">
35
+ import AlertIcon from "~icons/hugeicons/alert-circle";
36
+ import CheckmarkIcon from "~icons/hugeicons/checkmark-circle-02";
37
+ import InformationIcon from "~icons/hugeicons/information-circle";
38
+ import type { IconComponent } from "../../components/icon";
39
+ import type { NotificationProps, NotificationVariant } from "../../components/notification";
40
+ import Icon from "./icon.vue";
41
+ import Stack from "./stack.vue";
42
+
43
+ const { label, subtext, icon, variant = "info", announce = true } = defineProps<NotificationProps>();
44
+
45
+ const icons: Record<NotificationVariant, IconComponent> = {
46
+ error: AlertIcon,
47
+ attention: AlertIcon,
48
+ success: CheckmarkIcon,
49
+ info: InformationIcon,
50
+ };
51
+
52
+ const variants: Record<NotificationVariant, string> = {
53
+ error: "border-destructive/30 bg-destructive/5 text-destructive",
54
+ attention: "border-attention/30 bg-attention/5 text-attention",
55
+ success: "border-success/30 bg-success/5 text-success",
56
+ info: "border-border bg-muted/50 text-foreground",
57
+ };
58
+ </script>
@@ -0,0 +1,24 @@
1
+ <template>
2
+ <Stack is="nav" :aria-label="label" direction="horizontal" justify="end" gap="1" :aria-busy="loading || undefined">
3
+ <Button :label="previousLabel" variant="ghost" :disabled="!hasPrevious || loading" @click="$emit('previous')" />
4
+
5
+ <Button :label="nextLabel" variant="ghost" :disabled="!hasNext || loading" @click="$emit('next')" />
6
+ </Stack>
7
+ </template>
8
+
9
+ <script setup lang="ts">
10
+ import type { PaginationProps } from "../../components/pagination";
11
+ import Button from "./button.vue";
12
+ import Stack from "./stack.vue";
13
+
14
+ const {
15
+ hasPrevious = false,
16
+ hasNext = false,
17
+ loading = false,
18
+ previousLabel = "Previous",
19
+ nextLabel = "Next",
20
+ label = "Pagination",
21
+ } = defineProps<PaginationProps>();
22
+
23
+ defineEmits<{ previous: []; next: [] }>();
24
+ </script>
@@ -1,16 +1,38 @@
1
1
  <template>
2
- <div ref="container" class="relative inline-flex">
3
- <Button variant="outline" :class="triggerClass" :layout="triggerLayout" :disabled="disabled" @click="toggle">
4
- <slot name="trigger" :open="open" :close="close" :toggle="toggle">{{ triggerLabel }}</slot>
2
+ <div
3
+ ref="container"
4
+ class="relative w-full"
5
+ @keydown="handleKeyDown"
6
+ @focusout="$event.relatedTarget && !container?.contains($event.relatedTarget as Node) && close()"
7
+ >
8
+ <Button
9
+ :variant="triggerVariant"
10
+ :class="triggerClass"
11
+ :layout="triggerLayout"
12
+ :disabled="disabled"
13
+ :aria-expanded="isOpen"
14
+ @click="toggle"
15
+ >
16
+ <slot name="trigger" :open="open" :close="close" :toggle="toggle">
17
+ <span class="truncate px-1" :class="triggerLayout === 'center' ? '' : 'flex-1'">{{
18
+ triggerLabel
19
+ }}</span>
20
+ </slot>
5
21
  </Button>
6
22
 
7
- <div
23
+ <Stack
8
24
  v-if="isOpen"
9
- class="absolute left-0 top-full z-10 mt-1 max-w-sm rounded-ui border border-border bg-popover text-popover-foreground shadow-xl"
25
+ :padding="contentPadding"
26
+ :gap="contentGap"
27
+ :direction="contentDirection"
28
+ :align="contentAlign"
29
+ :justify="contentJustify"
30
+ :wrap="contentWrap"
31
+ class="absolute left-0 top-full z-10 mt-1 max-w-xs rounded-ui border border-border bg-popover text-popover-foreground shadow-xl"
10
32
  :class="[contentClass, matchTriggerWidth ? 'w-full' : '']"
11
33
  >
12
34
  <slot :open="open" :close="close" :toggle="toggle" />
13
- </div>
35
+ </Stack>
14
36
  </div>
15
37
  </template>
16
38
 
@@ -18,15 +40,22 @@
18
40
  import { onUnmounted, ref, useTemplateRef, watch } from "vue";
19
41
  import type { PopoverProps } from "../../components/popover";
20
42
  import Button from "./button.vue";
21
- import { useFocusTrap } from "../utilities/use-focus-trap";
43
+ import Stack from "./stack.vue";
22
44
 
23
45
  const {
24
46
  modelValue = false,
25
47
  triggerLabel = "Open",
26
48
  triggerClass,
27
49
  triggerLayout = "center",
28
- contentClass = "flex flex-col gap-1 p-1",
29
- matchTriggerWidth = false,
50
+ triggerVariant = "outline",
51
+ contentClass,
52
+ contentPadding = "1",
53
+ contentGap = "1",
54
+ contentDirection,
55
+ contentAlign,
56
+ contentJustify,
57
+ contentWrap,
58
+ matchTriggerWidth = true,
30
59
  disabled = false,
31
60
  } = defineProps<PopoverProps>();
32
61
 
@@ -38,8 +67,6 @@ const container = useTemplateRef("container");
38
67
 
39
68
  const isOpen = ref(modelValue);
40
69
 
41
- useFocusTrap(container, isOpen);
42
-
43
70
  function setOpen(value: boolean) {
44
71
  isOpen.value = value;
45
72
 
@@ -65,22 +92,25 @@ function handlePointerDown(event: PointerEvent) {
65
92
  }
66
93
 
67
94
  function handleKeyDown(event: KeyboardEvent) {
68
- if (event.key === "Escape") {
95
+ if (event.key === "Escape" && isOpen.value) {
96
+ event.preventDefault();
97
+ event.stopPropagation();
69
98
  close();
99
+ container.value?.querySelector<HTMLButtonElement>("button")?.focus();
70
100
  }
71
101
  }
72
102
 
73
103
  watch(
74
104
  isOpen,
75
105
  (value) => {
106
+ if (typeof document === "undefined") {
107
+ return;
108
+ }
109
+
76
110
  if (value) {
77
111
  document.addEventListener("pointerdown", handlePointerDown);
78
-
79
- document.addEventListener("keydown", handleKeyDown);
80
112
  } else {
81
113
  document.removeEventListener("pointerdown", handlePointerDown);
82
-
83
- document.removeEventListener("keydown", handleKeyDown);
84
114
  }
85
115
  },
86
116
  { immediate: true },
@@ -95,8 +125,6 @@ watch(
95
125
 
96
126
  onUnmounted(() => {
97
127
  document.removeEventListener("pointerdown", handlePointerDown);
98
-
99
- document.removeEventListener("keydown", handleKeyDown);
100
128
  });
101
129
 
102
130
  defineExpose({ open, close, toggle });
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <Stack>
3
3
  <Stack v-if="label" direction="horizontal" align="center" justify="between" gap="1" class="text-sm">
4
- <span class="font-medium">{{ label }}</span>
4
+ <span class="font-normal">{{ label }}</span>
5
5
 
6
6
  <span class="tabular-nums opacity-60">{{ Math.round(percent) }}%</span>
7
7
  </Stack>
@@ -16,7 +16,7 @@
16
16
  aria-valuemin="0"
17
17
  :aria-valuemax="max"
18
18
  >
19
- <div class="h-full rounded-ui bg-primary transition-[width]" :style="{ width: `${percent}%` }" />
19
+ <div class="h-full rounded-ui bg-primary" :style="{ width: `${percent}%` }" />
20
20
  </div>
21
21
  </Stack>
22
22
  </template>
@@ -1,5 +1,12 @@
1
1
  <template>
2
- <Stack is="label" direction="horizontal" align="start" gap="3" :class="interactionVariants[interaction]">
2
+ <Stack
3
+ is="label"
4
+ direction="horizontal"
5
+ align="start"
6
+ gap="3"
7
+ class="group"
8
+ :class="interactionVariants[interaction]"
9
+ >
3
10
  <input
4
11
  class="peer sr-only"
5
12
  type="radio"
@@ -13,14 +20,21 @@
13
20
 
14
21
  <span
15
22
  aria-hidden="true"
16
- class="mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-ui border transition-colors peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-ring"
17
- :class="modelValue === value ? 'border-primary bg-background' : 'border-input bg-background'"
23
+ class="mt-0.5 flex size-5 shrink-0 items-center justify-center rounded-ui border peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:outline-ring"
24
+ :class="[
25
+ modelValue === value ? 'border-primary bg-background' : 'border-input bg-background',
26
+ disabled ? '' : 'group-hover:bg-secondary-hover group-active:bg-secondary-active',
27
+ ]"
18
28
  >
19
- <span v-if="modelValue === value" class="size-2 rounded-ui bg-primary" />
29
+ <span
30
+ v-if="modelValue === value"
31
+ class="size-2 rounded-ui bg-primary"
32
+ :class="disabled ? '' : 'group-hover:bg-secondary-foreground group-active:bg-secondary-foreground'"
33
+ />
20
34
  </span>
21
35
 
22
36
  <Stack is="span" gap="0">
23
- <span class="text-sm font-medium">
37
+ <span class="text-sm font-normal">
24
38
  <slot> {{ label }}</slot>
25
39
  </span>
26
40
 
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <Stack :class="interactionVariants[interaction]">
3
3
  <Stack v-if="label" direction="horizontal" align="center" justify="between" gap="1" class="text-sm">
4
- <span class="font-medium"> {{ label }} </span>
4
+ <span class="font-normal"> {{ label }} </span>
5
5
 
6
6
  <output class="tabular-nums opacity-60">
7
7
  {{ modelValue }}