@revojs/ui 0.2.12 → 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 (49) hide show
  1. package/dist/components/button.d.ts +1 -0
  2. package/dist/components/card.d.ts +8 -0
  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 -2
  6. package/dist/components/entry.d.ts +6 -0
  7. package/dist/components/entry.js +1 -0
  8. package/dist/components/field.d.ts +2 -0
  9. package/dist/components/input.d.ts +1 -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 -2
  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/index.d.ts +5 -0
  22. package/dist/module/index.d.ts +8 -0
  23. package/dist/module/index.js +16 -0
  24. package/dist/style.css +8 -0
  25. package/dist/vue/components/breadcrumb.vue +6 -1
  26. package/dist/vue/components/button.vue +25 -3
  27. package/dist/vue/components/card.vue +22 -3
  28. package/dist/vue/components/collection.vue +22 -7
  29. package/dist/vue/components/container.vue +6 -2
  30. package/dist/vue/components/dialog.vue +52 -35
  31. package/dist/vue/components/entry.vue +52 -0
  32. package/dist/vue/components/field.vue +37 -7
  33. package/dist/vue/components/input.vue +22 -2
  34. package/dist/vue/components/navigate.vue +1 -0
  35. package/dist/vue/components/notification.vue +58 -0
  36. package/dist/vue/components/pagination.vue +24 -0
  37. package/dist/vue/components/popover.vue +39 -15
  38. package/dist/vue/components/select.vue +93 -15
  39. package/dist/vue/components/sheet.vue +55 -21
  40. package/dist/vue/components/shell.vue +38 -0
  41. package/dist/vue/components/textarea.vue +49 -0
  42. package/dist/vue/components/upload.vue +43 -24
  43. package/dist/vue/index.d.ts +5 -0
  44. package/dist/vue/index.js +6 -1
  45. package/dist/vue/utilities/use-control-attributes.d.ts +11 -0
  46. package/dist/vue/utilities/use-control-attributes.js +14 -0
  47. package/dist/vue/utilities/use-modal-dialog.d.ts +9 -0
  48. package/dist/vue/utilities/use-modal-dialog.js +48 -0
  49. package/package.json +1 -1
@@ -16,4 +16,5 @@ export interface ButtonProps {
16
16
  variant?: ButtonVariant;
17
17
  type?: ButtonType;
18
18
  disabled?: boolean;
19
+ loading?: boolean;
19
20
  }
@@ -1,3 +1,11 @@
1
+ import type { StackProps } from "./stack";
1
2
  export interface CardProps {
2
3
  label?: string;
4
+ labelId?: string;
5
+ bodyPadding?: StackProps["padding"];
6
+ bodyGap?: StackProps["gap"];
7
+ bodyDirection?: StackProps["direction"];
8
+ bodyAlign?: StackProps["align"];
9
+ bodyJustify?: StackProps["justify"];
10
+ bodyWrap?: StackProps["wrap"];
3
11
  }
@@ -1,6 +1,9 @@
1
+ import type { StackProps } from "./stack";
1
2
  export type CollectionColumns = 1 | 2 | 3 | 4;
2
3
  export interface CollectionProps<T = unknown> {
3
4
  items?: Array<T>;
4
5
  columns?: CollectionColumns;
6
+ gap?: StackProps["gap"];
7
+ itemKey?: keyof T | ((item: T, index: number) => string | number);
5
8
  emptyLabel?: string;
6
9
  }
@@ -1,6 +1,5 @@
1
- import type { StackGap } from "./stack";
1
+ import type { StackProps } from "./stack";
2
2
  export type ContainerSize = "3xl" | "5xl" | "7xl";
3
- export interface ContainerProps {
3
+ export interface ContainerProps extends StackProps {
4
4
  size?: ContainerSize;
5
- padding?: StackGap;
6
5
  }
@@ -1,5 +1,6 @@
1
- export interface DialogProps {
1
+ import type { CardProps } from "./card";
2
+ export interface DialogProps extends CardProps {
2
3
  modelValue?: boolean;
3
- label?: string;
4
4
  closeLabel?: string;
5
+ dismissible?: boolean;
5
6
  }
@@ -0,0 +1,6 @@
1
+ import type { StackProps } from "./stack";
2
+ export interface EntryProps extends StackProps {
3
+ href?: string;
4
+ label?: string;
5
+ active?: boolean;
6
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -6,4 +6,6 @@ export interface FieldProps {
6
6
  text?: FieldText;
7
7
  required?: boolean;
8
8
  disabled?: boolean;
9
+ for?: string;
10
+ is?: "div" | "fieldset";
9
11
  }
@@ -12,4 +12,5 @@ export interface InputProps {
12
12
  required?: boolean;
13
13
  readonly?: boolean;
14
14
  disabled?: boolean;
15
+ invalid?: boolean;
15
16
  }
@@ -0,0 +1,9 @@
1
+ import type { IconComponent } from "./icon";
2
+ export type NotificationVariant = "error" | "attention" | "success" | "info";
3
+ export interface NotificationProps {
4
+ label?: string;
5
+ subtext?: string;
6
+ icon?: IconComponent;
7
+ variant?: NotificationVariant;
8
+ announce?: boolean;
9
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ export interface PaginationProps {
2
+ hasPrevious?: boolean;
3
+ hasNext?: boolean;
4
+ loading?: boolean;
5
+ previousLabel?: string;
6
+ nextLabel?: string;
7
+ label?: string;
8
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,10 +1,18 @@
1
- import type { ButtonLayout } from "./button";
1
+ import type { ButtonLayout, ButtonVariant } from "./button";
2
+ import type { StackProps } from "./stack";
2
3
  export interface PopoverProps {
3
4
  modelValue?: boolean;
4
5
  triggerLabel?: string;
5
6
  triggerClass?: string;
6
7
  triggerLayout?: ButtonLayout;
8
+ triggerVariant?: ButtonVariant;
7
9
  contentClass?: string;
10
+ contentPadding?: StackProps["padding"];
11
+ contentGap?: StackProps["gap"];
12
+ contentDirection?: StackProps["direction"];
13
+ contentAlign?: StackProps["align"];
14
+ contentJustify?: StackProps["justify"];
15
+ contentWrap?: StackProps["wrap"];
8
16
  matchTriggerWidth?: boolean;
9
17
  disabled?: boolean;
10
18
  }
@@ -1,17 +1,20 @@
1
1
  export type SelectValue = string | number;
2
2
  export type SelectValueState = "placeholder" | "selected";
3
- export interface SelectOption {
3
+ export interface SelectOption<Value extends SelectValue = SelectValue> {
4
4
  label: string;
5
- value: SelectValue;
5
+ value: Value;
6
6
  disabled?: boolean;
7
7
  }
8
- export interface SelectProps {
9
- modelValue?: SelectValue;
10
- options: Array<SelectOption>;
8
+ export interface SelectProps<Value extends SelectValue = SelectValue> {
9
+ modelValue?: Value;
10
+ options: Array<SelectOption<Value>>;
11
11
  label?: string;
12
12
  placeholder?: string;
13
13
  emptyLabel?: string;
14
14
  name?: string;
15
15
  required?: boolean;
16
16
  disabled?: boolean;
17
+ searchable?: boolean;
18
+ invalid?: boolean;
19
+ requiredLabel?: string;
17
20
  }
@@ -1,7 +1,8 @@
1
+ import type { CardProps } from "./card";
1
2
  export type SheetSide = "top" | "right" | "bottom" | "left";
2
- export interface SheetProps {
3
+ export interface SheetProps extends CardProps {
3
4
  modelValue?: boolean;
4
5
  side?: SheetSide;
5
- label?: string;
6
6
  closeLabel?: string;
7
+ dismissible?: boolean;
7
8
  }
@@ -0,0 +1,6 @@
1
+ export interface ShellProps {
2
+ label?: string;
3
+ headingLabel?: string;
4
+ sticky?: boolean;
5
+ navigationLabel?: string;
6
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ export type TextareaText = "sm" | "md";
2
+ export type TextareaResize = "none" | "vertical" | "horizontal" | "both";
3
+ export type TextareaVariant = "default" | "plain";
4
+ export interface TextareaProps {
5
+ modelValue?: string;
6
+ text?: TextareaText;
7
+ resize?: TextareaResize;
8
+ variant?: TextareaVariant;
9
+ disabled?: boolean;
10
+ readonly?: boolean;
11
+ required?: boolean;
12
+ invalid?: boolean;
13
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.d.ts CHANGED
@@ -9,11 +9,15 @@ export type * from "./components/counter";
9
9
  export type * from "./components/dialog";
10
10
  export type * from "./components/field";
11
11
  export type * from "./components/heading";
12
+ export type * from "./components/shell";
12
13
  export type * from "./components/icon";
13
14
  export type * from "./components/input";
14
15
  export type * from "./components/link";
16
+ export type * from "./components/entry";
17
+ export type * from "./components/notification";
15
18
  export type * from "./components/navigate";
16
19
  export type * from "./components/popover";
20
+ export type * from "./components/pagination";
17
21
  export type * from "./components/progress";
18
22
  export type * from "./components/radio";
19
23
  export type * from "./components/range";
@@ -24,3 +28,4 @@ export type * from "./components/sheet";
24
28
  export type * from "./components/stack";
25
29
  export type * from "./components/switch";
26
30
  export type * from "./components/upload";
31
+ export type * from "./components/textarea";
@@ -30,6 +30,14 @@ export interface UiTheme {
30
30
  destructiveHover: TailwindColor;
31
31
  destructiveActive: TailwindColor;
32
32
  destructiveForeground: TailwindColor;
33
+ attention: TailwindColor;
34
+ attentionHover: TailwindColor;
35
+ attentionActive: TailwindColor;
36
+ attentionForeground: TailwindColor;
37
+ success: TailwindColor;
38
+ successHover: TailwindColor;
39
+ successActive: TailwindColor;
40
+ successForeground: TailwindColor;
33
41
  border: TailwindColor;
34
42
  input: TailwindColor;
35
43
  ring: TailwindColor;
@@ -54,6 +54,14 @@ var module_default = defineModule({
54
54
  destructiveHover: "red-600",
55
55
  destructiveActive: "red-700",
56
56
  destructiveForeground: "zinc-50",
57
+ attention: "yellow-700",
58
+ attentionHover: "yellow-800",
59
+ attentionActive: "yellow-900",
60
+ attentionForeground: "white",
61
+ success: "green-700",
62
+ successHover: "green-800",
63
+ successActive: "green-900",
64
+ successForeground: "white",
57
65
  border: "zinc-200",
58
66
  input: "zinc-200",
59
67
  ring: "zinc-400"
@@ -83,6 +91,14 @@ var module_default = defineModule({
83
91
  destructiveHover: "red-600",
84
92
  destructiveActive: "red-700",
85
93
  destructiveForeground: "zinc-50",
94
+ attention: "yellow-400",
95
+ attentionHover: "yellow-500",
96
+ attentionActive: "yellow-600",
97
+ attentionForeground: "zinc-950",
98
+ success: "green-400",
99
+ successHover: "green-500",
100
+ successActive: "green-600",
101
+ successForeground: "zinc-950",
86
102
  border: "zinc-700",
87
103
  input: "zinc-700",
88
104
  ring: "zinc-700"
package/dist/style.css CHANGED
@@ -27,6 +27,14 @@
27
27
  --color-destructive-hover: var(--ui-destructive-hover);
28
28
  --color-destructive-active: var(--ui-destructive-active);
29
29
  --color-destructive-foreground: var(--ui-destructive-foreground);
30
+ --color-attention: var(--ui-attention);
31
+ --color-attention-hover: var(--ui-attention-hover);
32
+ --color-attention-active: var(--ui-attention-active);
33
+ --color-attention-foreground: var(--ui-attention-foreground);
34
+ --color-success: var(--ui-success);
35
+ --color-success-hover: var(--ui-success-hover);
36
+ --color-success-active: var(--ui-success-active);
37
+ --color-success-foreground: var(--ui-success-foreground);
30
38
  --color-border: var(--ui-border);
31
39
  --color-input: var(--ui-input);
32
40
  --color-ring: var(--ui-ring);
@@ -11,7 +11,12 @@
11
11
  >
12
12
  <span v-if="index" aria-hidden="true" class="opacity-40"> / </span>
13
13
 
14
- <Link :href="href" :label="label" :variant="index === routes.length - 1 ? 'primary' : 'muted'" />
14
+ <Link
15
+ :href="href"
16
+ :label="label"
17
+ :variant="index === routes.length - 1 ? 'primary' : 'muted'"
18
+ :aria-current="index === routes.length - 1 ? 'page' : undefined"
19
+ />
15
20
  </Stack>
16
21
  </Stack>
17
22
  </nav>
@@ -3,10 +3,19 @@
3
3
  :is="is"
4
4
  class="inline-flex shrink-0 cursor-pointer items-center justify-center gap-1 truncate rounded-ui font-[inherit] no-underline outline-ring select-none focus-visible:outline-2 focus-visible:outline-offset-2 disabled:cursor-not-allowed disabled:opacity-50 aria-disabled:cursor-not-allowed aria-disabled:opacity-50"
5
5
  :class="[fonts[font], layouts[layout], sizes[size], texts[text], variants[variant]]"
6
- :type="attributes.type ?? 'button'"
7
- :disabled="disabled"
6
+ :type="is === 'a' ? undefined : (attributes.type ?? 'button')"
7
+ :disabled="is === 'a' ? undefined : disabled || loading"
8
+ :aria-disabled="disabled || loading || undefined"
9
+ :aria-busy="loading || undefined"
10
+ @click="click"
8
11
  >
9
- <Icon v-if="iconPrefix" :is="iconPrefix" :size="iconSize[text]" />
12
+ <span
13
+ v-if="loading"
14
+ aria-hidden="true"
15
+ class="size-5 shrink-0 animate-spin rounded-full border-2 border-current border-r-transparent"
16
+ />
17
+
18
+ <Icon v-else-if="iconPrefix" :is="iconPrefix" :size="iconSize[text]" />
10
19
 
11
20
  <slot>
12
21
  <span v-if="label" class="truncate px-1" :class="labelLayouts[layout]">{{ label }}</span>
@@ -39,6 +48,7 @@ const {
39
48
  text = "sm",
40
49
  variant = "primary",
41
50
  disabled = false,
51
+ loading = false,
42
52
  } = defineProps<
43
53
  Omit<ButtonProps, "type"> & {
44
54
  is?: string | Component;
@@ -47,6 +57,18 @@ const {
47
57
 
48
58
  const attributes = useAttrs();
49
59
 
60
+ const emit = defineEmits<{ click: [event: MouseEvent] }>();
61
+
62
+ function click(event: MouseEvent) {
63
+ if (disabled || loading) {
64
+ event.preventDefault();
65
+ event.stopImmediatePropagation();
66
+ return;
67
+ }
68
+
69
+ emit("click", event);
70
+ }
71
+
50
72
  const fonts: Record<ButtonFont, string> = {
51
73
  normal: "font-normal",
52
74
  medium: "font-medium",
@@ -9,7 +9,9 @@
9
9
  class="shrink-0 border-b border-border"
10
10
  >
11
11
  <slot name="header">
12
- <span v-if="label" class="min-w-0 flex-1 px-5 py-3 text-sm/5 font-normal"> {{ label }} </span>
12
+ <span v-if="label" :id="labelId" class="min-w-0 flex-1 px-5 py-3 text-sm/5 font-normal">
13
+ {{ label }}
14
+ </span>
13
15
  </slot>
14
16
 
15
17
  <Stack v-if="$slots.actions" direction="horizontal" align="center" class="ml-auto shrink-0">
@@ -17,7 +19,15 @@
17
19
  </Stack>
18
20
  </Stack>
19
21
 
20
- <Stack gap="5" padding="5" class="min-h-0 flex-1">
22
+ <Stack
23
+ :gap="bodyGap"
24
+ :padding="bodyPadding"
25
+ :direction="bodyDirection"
26
+ :align="bodyAlign"
27
+ :justify="bodyJustify"
28
+ :wrap="bodyWrap"
29
+ class="min-h-0 flex-1"
30
+ >
21
31
  <slot />
22
32
  </Stack>
23
33
 
@@ -31,5 +41,14 @@
31
41
  import type { CardProps } from "../../components/card";
32
42
  import Stack from "./stack.vue";
33
43
 
34
- const { label } = defineProps<CardProps>();
44
+ const {
45
+ label,
46
+ labelId,
47
+ bodyPadding = "5",
48
+ bodyGap = "5",
49
+ bodyDirection,
50
+ bodyAlign,
51
+ bodyJustify,
52
+ bodyWrap,
53
+ } = defineProps<CardProps>();
35
54
  </script>
@@ -5,17 +5,22 @@
5
5
  class="w-full divide-y divide-border overflow-hidden rounded-ui border border-border bg-card"
6
6
  >
7
7
  <template v-if="items.length">
8
- <div v-for="(item, index) in items" :key="index">
8
+ <div
9
+ v-for="(item, index) in items"
10
+ :key="typeof itemKey === 'function' ? itemKey(item, index) : itemKey ? String(item[itemKey]) : index"
11
+ >
9
12
  <slot :item="item" :index="index" />
10
13
  </div>
11
14
  </template>
12
- <Stack v-else align="center" padding="5">
13
- <Icon :is="SearchIcon" class="text-2xl" />
15
+ <slot v-else name="empty">
16
+ <Stack align="center" padding="5">
17
+ <Icon :is="SearchIcon" class="text-2xl" />
14
18
 
15
- <span>{{ emptyLabel }}</span>
16
- </Stack>
19
+ <span>{{ emptyLabel }}</span>
20
+ </Stack>
21
+ </slot>
17
22
  </Stack>
18
- <div v-else class="grid gap-5" :class="columnsClass[columns]">
23
+ <div v-else class="grid" :class="[columnsClass[columns], gaps[gap]]">
19
24
  <slot />
20
25
  </div>
21
26
  </template>
@@ -23,13 +28,15 @@
23
28
  <script setup generic="T" lang="ts">
24
29
  import SearchIcon from "~icons/hugeicons/search-01";
25
30
  import type { CollectionColumns, CollectionProps } from "../../components/collection";
31
+ import type { StackGap } from "../../components/stack";
26
32
  import Icon from "./icon.vue";
27
33
  import Stack from "./stack.vue";
28
34
 
29
- const { items, columns = 1, emptyLabel = "Nothing found" } = defineProps<CollectionProps<T>>();
35
+ const { items, columns = 1, gap = "5", itemKey, emptyLabel = "Nothing found" } = defineProps<CollectionProps<T>>();
30
36
 
31
37
  defineSlots<{
32
38
  default: (() => unknown) & ((props: { item: T; index: number }) => unknown);
39
+ empty?: () => unknown;
33
40
  }>();
34
41
 
35
42
  const columnsClass: Record<CollectionColumns, string> = {
@@ -38,4 +45,12 @@ const columnsClass: Record<CollectionColumns, string> = {
38
45
  3: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3",
39
46
  4: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-4",
40
47
  };
48
+
49
+ const gaps: Record<StackGap, string> = {
50
+ "0": "gap-0",
51
+ "1": "gap-1",
52
+ "3": "gap-3",
53
+ "5": "gap-5",
54
+ "7": "gap-7",
55
+ };
41
56
  </script>
@@ -1,7 +1,11 @@
1
1
  <template>
2
2
  <Stack
3
- gap="5"
3
+ :gap="gap"
4
4
  :padding="padding"
5
+ :direction="direction"
6
+ :align="align"
7
+ :justify="justify"
8
+ :wrap="wrap"
5
9
  :class="sizes[size]"
6
10
  class="mx-auto w-full overflow-y-scroll [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
7
11
  >
@@ -13,7 +17,7 @@
13
17
  import type { ContainerProps, ContainerSize } from "../../components/container";
14
18
  import Stack from "./stack.vue";
15
19
 
16
- const { size = "5xl", padding = "5" } = defineProps<ContainerProps>();
20
+ const { size = "5xl", padding = "5", gap = "5", direction, align, justify, wrap } = defineProps<ContainerProps>();
17
21
 
18
22
  const sizes: Record<ContainerSize, string> = {
19
23
  "3xl": "max-w-3xl",
@@ -2,13 +2,39 @@
2
2
  <dialog
3
3
  ref="dialog"
4
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
- @close="$emit('update:modelValue', false)"
6
- @click="$event.target === $event.currentTarget && ($event.currentTarget as HTMLDialogElement).close()"
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 :label="label" class="my-auto max-w-3xl shrink-0 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, label, 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>