@aggc/ui 0.7.1 → 0.9.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.
Files changed (48) hide show
  1. package/dist/chunks/DataTable-CIiU5Jx3.js +8688 -0
  2. package/dist/components/DataTable.styles.d.ts +48 -0
  3. package/dist/components/DataTable.types.d.ts +38 -0
  4. package/dist/components/DataTable.vue.d.ts +72 -0
  5. package/dist/components/UiAvatar.styles.d.ts +53 -0
  6. package/dist/components/UiAvatar.vue.d.ts +13 -0
  7. package/dist/components/UiModal.styles.d.ts +31 -0
  8. package/dist/components/UiModal.vue.d.ts +30 -0
  9. package/dist/components/UiToast.styles.d.ts +41 -0
  10. package/dist/components/UiToast.vue.d.ts +13 -0
  11. package/dist/components/UiToastProvider.vue.d.ts +13 -0
  12. package/dist/components/UiTooltip.styles.d.ts +1 -0
  13. package/dist/components/UiTooltip.vue.d.ts +25 -0
  14. package/dist/components/index.d.ts +11 -0
  15. package/dist/components.js +30 -12
  16. package/dist/composables/useToast.d.ts +27 -0
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.js +82 -63
  19. package/dist/ui.css +995 -294
  20. package/package.json +3 -2
  21. package/src/components/DataTable.styles.ts +493 -0
  22. package/src/components/DataTable.test.ts +249 -0
  23. package/src/components/DataTable.types.ts +42 -0
  24. package/src/components/DataTable.vue +567 -0
  25. package/src/components/UiAvatar.styles.ts +81 -0
  26. package/src/components/UiAvatar.test.ts +43 -0
  27. package/src/components/UiAvatar.vue +41 -0
  28. package/src/components/UiModal.styles.ts +143 -0
  29. package/src/components/UiModal.test.ts +64 -0
  30. package/src/components/UiModal.vue +82 -0
  31. package/src/components/UiToast.styles.ts +143 -0
  32. package/src/components/UiToast.test.ts +47 -0
  33. package/src/components/UiToast.vue +65 -0
  34. package/src/components/UiToastProvider.vue +22 -0
  35. package/src/components/UiTooltip.styles.ts +25 -0
  36. package/src/components/UiTooltip.test.ts +37 -0
  37. package/src/components/UiTooltip.vue +37 -0
  38. package/src/components/index.ts +17 -0
  39. package/src/composables/useToast.ts +43 -0
  40. package/src/css/base.css +61 -1
  41. package/src/index.ts +1 -0
  42. package/src/stories/feedback/UiToast.stories.ts +72 -0
  43. package/src/stories/layout/DataTable.stories.ts +141 -0
  44. package/src/stories/layout/UiModal.stories.ts +89 -0
  45. package/src/stories/primitives/UiAvatar.stories.ts +83 -0
  46. package/src/stories/primitives/UiTooltip.stories.ts +46 -0
  47. package/src/stories/support/sources.ts +81 -0
  48. package/dist/chunks/UiSkeleton.vue_vue_type_script_setup_true_lang-DUse1KRc.js +0 -1201
@@ -0,0 +1,41 @@
1
+ <script setup lang="ts">
2
+ import { computed } from "vue";
3
+ import { User } from "lucide-vue-next";
4
+ import { AvatarFallback, AvatarImage, AvatarRoot } from "reka-ui";
5
+ import {
6
+ uiAvatarFallbackClass,
7
+ uiAvatarImageClass,
8
+ uiAvatarRootClass,
9
+ } from "./UiAvatar.styles";
10
+
11
+ const props = withDefaults(
12
+ defineProps<{
13
+ name: string;
14
+ src?: string;
15
+ size?: "sm" | "md" | "lg" | "xl";
16
+ shape?: "circle" | "square";
17
+ }>(),
18
+ {
19
+ src: undefined,
20
+ size: "md",
21
+ shape: "circle",
22
+ }
23
+ );
24
+
25
+ const initials = computed(() => {
26
+ const parts = props.name.trim().split(/\s+/);
27
+ if (parts.length === 1) return parts[0].charAt(0).toUpperCase();
28
+ return (parts[0].charAt(0) + parts[parts.length - 1].charAt(0)).toUpperCase();
29
+ });
30
+
31
+ const iconSizeMap = { sm: 16, md: 20, lg: 24, xl: 28 };
32
+ </script>
33
+
34
+ <template>
35
+ <AvatarRoot :class="uiAvatarRootClass({ size, shape })" :aria-label="name" role="img">
36
+ <AvatarImage v-if="src" :src="src" :alt="name" :class="uiAvatarImageClass" />
37
+ <AvatarFallback :delay-ms="300" :class="uiAvatarFallbackClass({ size })">
38
+ {{ initials }}
39
+ </AvatarFallback>
40
+ </AvatarRoot>
41
+ </template>
@@ -0,0 +1,143 @@
1
+ import { css, cva } from "@styled/css";
2
+
3
+ export const uiModalOverlayClass = css({
4
+ position: "fixed",
5
+ inset: "0",
6
+ bg: "rgba(0, 0, 0, 0.3)",
7
+ backdropFilter: "blur(8px)",
8
+ _dark: {
9
+ bg: "rgba(0, 0, 0, 0.5)",
10
+ },
11
+ zIndex: "50",
12
+ overscrollBehavior: "contain",
13
+ animation: "fadeIn 160ms cubic-bezier(0.16, 1, 0.3, 1)",
14
+ });
15
+
16
+ export const uiModalWrapperClass = css({
17
+ position: "fixed",
18
+ inset: "0",
19
+ display: "flex",
20
+ alignItems: { base: "flex-end", sm: "center" },
21
+ justifyContent: "center",
22
+ zIndex: "51",
23
+ pointerEvents: "none",
24
+ });
25
+
26
+ export const uiModalContentClass = cva({
27
+ base: {
28
+ bg: "bg.menu",
29
+ borderWidth: "1px",
30
+ borderColor: "border.subtle",
31
+ p: "0",
32
+ display: "flex",
33
+ flexDirection: "column",
34
+ // Mobile: bottom sheet — desktop: centered via wrapper
35
+ position: { base: "fixed", sm: "relative" },
36
+ bottom: { base: "0", sm: "auto" },
37
+ left: { base: "0", sm: "auto" },
38
+ right: { base: "0", sm: "auto" },
39
+ pointerEvents: "auto",
40
+ borderTopLeftRadius: "xl",
41
+ borderTopRightRadius: "xl",
42
+ borderBottomLeftRadius: { base: "0", sm: "xl" },
43
+ borderBottomRightRadius: { base: "0", sm: "xl" },
44
+ maxH: { base: "90dvh", sm: "calc(100dvh - 2rem)" },
45
+ animation: {
46
+ base: "modalSlideUp 300ms cubic-bezier(0.16, 1, 0.3, 1) backwards",
47
+ sm: "modalIn 240ms cubic-bezier(0.16, 1, 0.3, 1) backwards",
48
+ },
49
+ boxShadow: {
50
+ base: "0 -8px 32px -4px rgba(0, 0, 0, 0.12), 0 -2px 8px -2px rgba(0, 0, 0, 0.06)",
51
+ sm: "0 16px 48px -8px rgba(0, 0, 0, 0.12), 0 4px 16px -4px rgba(0, 0, 0, 0.06)",
52
+ },
53
+ _dark: {
54
+ borderColor: "border.default",
55
+ boxShadow: {
56
+ base: "0 -8px 32px -4px rgba(0, 0, 0, 0.48), 0 -2px 8px -2px rgba(0, 0, 0, 0.24)",
57
+ sm: "0 16px 48px -8px rgba(0, 0, 0, 0.56), 0 4px 16px -4px rgba(0, 0, 0, 0.32)",
58
+ },
59
+ },
60
+ _focusVisible: {
61
+ outline: "none",
62
+ },
63
+ },
64
+ variants: {
65
+ size: {
66
+ sm: { w: { base: "full", sm: "380px" } },
67
+ md: { w: { base: "full", sm: "480px" } },
68
+ lg: { w: { base: "full", sm: "640px" } },
69
+ },
70
+ },
71
+ defaultVariants: {
72
+ size: "md",
73
+ },
74
+ });
75
+
76
+ export const uiModalHeaderClass = css({
77
+ display: "flex",
78
+ justifyContent: "space-between",
79
+ alignItems: "center",
80
+ p: { base: "4", sm: "6" },
81
+ pb: { base: "3", sm: "4" },
82
+ });
83
+
84
+ export const uiModalHeaderContentClass = css({
85
+ display: "flex",
86
+ flexDirection: "column",
87
+ gap: "1",
88
+ });
89
+
90
+ export const uiModalTitleClass = css({
91
+ fontFamily: "heading",
92
+ fontWeight: "600",
93
+ fontSize: "lg",
94
+ color: "text.primary",
95
+ });
96
+
97
+ export const uiModalDescriptionClass = css({
98
+ fontSize: "sm",
99
+ color: "text.secondary",
100
+ lineHeight: "1.5",
101
+ });
102
+
103
+ export const uiModalCloseClass = css({
104
+ display: "flex",
105
+ alignItems: "center",
106
+ justifyContent: "center",
107
+ w: "8",
108
+ h: "8",
109
+ borderRadius: "lg",
110
+ border: "none",
111
+ bg: "transparent",
112
+ color: "text.muted",
113
+ cursor: "pointer",
114
+ flexShrink: "0",
115
+ transition:
116
+ "color 160ms cubic-bezier(0.25, 0.1, 0.25, 1), background 160ms cubic-bezier(0.25, 0.1, 0.25, 1)",
117
+ _hover: {
118
+ bg: "bg.hover",
119
+ color: "text.primary",
120
+ },
121
+ _focusVisible: {
122
+ outline: "2px solid",
123
+ outlineColor: "border.accent",
124
+ outlineOffset: "2px",
125
+ },
126
+ });
127
+
128
+ export const uiModalBodyClass = css({
129
+ px: { base: "4", sm: "6" },
130
+ pb: { base: "4", sm: "6" },
131
+ overflowY: "auto",
132
+ overscrollBehavior: "contain",
133
+ });
134
+
135
+ export const uiModalActionsClass = css({
136
+ display: "flex",
137
+ justifyContent: "flex-end",
138
+ gap: "3",
139
+ p: { base: "4", sm: "6" },
140
+ pt: "3",
141
+ borderTopWidth: "1px",
142
+ borderTopColor: "border.soft",
143
+ });
@@ -0,0 +1,64 @@
1
+ import { mount } from "@vue/test-utils";
2
+ import { describe, expect, it } from "vitest";
3
+ import { DialogRoot } from "reka-ui";
4
+ import UiModal from "./UiModal.vue";
5
+
6
+ describe("UiModal", () => {
7
+ it("mounts without errors when open", () => {
8
+ const wrapper = mount(UiModal, {
9
+ props: {
10
+ open: true,
11
+ title: "Test Modal",
12
+ },
13
+ });
14
+
15
+ expect(wrapper.findComponent(DialogRoot).exists()).toBe(true);
16
+ });
17
+
18
+ it("passes title prop to DialogTitle", () => {
19
+ const wrapper = mount(UiModal, {
20
+ props: {
21
+ open: true,
22
+ title: "Create Workspace",
23
+ },
24
+ });
25
+
26
+ expect(wrapper.props("title")).toBe("Create Workspace");
27
+ });
28
+
29
+ it("passes size prop correctly", () => {
30
+ const wrapper = mount(UiModal, {
31
+ props: {
32
+ open: true,
33
+ title: "Test",
34
+ size: "lg",
35
+ },
36
+ });
37
+
38
+ expect(wrapper.props("size")).toBe("lg");
39
+ });
40
+
41
+ it("renders with default size md", () => {
42
+ const wrapper = mount(UiModal, {
43
+ props: {
44
+ open: true,
45
+ title: "Test",
46
+ },
47
+ });
48
+
49
+ expect(wrapper.props("size")).toBe("md");
50
+ });
51
+
52
+ it("emits update:open when DialogRoot state changes", async () => {
53
+ const wrapper = mount(UiModal, {
54
+ props: {
55
+ open: true,
56
+ title: "Test",
57
+ },
58
+ });
59
+
60
+ const dialogRoot = wrapper.findComponent(DialogRoot);
61
+ await dialogRoot.vm.$emit("update:open", false);
62
+ expect(wrapper.emitted("update:open")?.[0]).toEqual([false]);
63
+ });
64
+ });
@@ -0,0 +1,82 @@
1
+ <script setup lang="ts">
2
+ import { X } from "lucide-vue-next";
3
+ import {
4
+ DialogClose,
5
+ DialogContent,
6
+ DialogDescription,
7
+ DialogOverlay,
8
+ DialogPortal,
9
+ DialogRoot,
10
+ DialogTitle,
11
+ VisuallyHidden,
12
+ } from "reka-ui";
13
+ import {
14
+ uiModalActionsClass,
15
+ uiModalBodyClass,
16
+ uiModalCloseClass,
17
+ uiModalContentClass,
18
+ uiModalDescriptionClass,
19
+ uiModalHeaderClass,
20
+ uiModalHeaderContentClass,
21
+ uiModalOverlayClass,
22
+ uiModalTitleClass,
23
+ uiModalWrapperClass,
24
+ } from "./UiModal.styles";
25
+
26
+ withDefaults(
27
+ defineProps<{
28
+ open: boolean;
29
+ size?: "sm" | "md" | "lg";
30
+ title: string;
31
+ description?: string;
32
+ }>(),
33
+ {
34
+ size: "md",
35
+ description: undefined,
36
+ }
37
+ );
38
+
39
+ const emit = defineEmits<{
40
+ "update:open": [value: boolean];
41
+ close: [];
42
+ }>();
43
+
44
+ function handleOpenChange(open: boolean) {
45
+ emit("update:open", open);
46
+ if (!open) {
47
+ emit("close");
48
+ }
49
+ }
50
+ </script>
51
+
52
+ <template>
53
+ <DialogRoot :open="open" @update:open="handleOpenChange">
54
+ <DialogPortal>
55
+ <DialogOverlay :class="uiModalOverlayClass" />
56
+ <div :class="uiModalWrapperClass">
57
+ <DialogContent :class="uiModalContentClass({ size })">
58
+ <div :class="uiModalHeaderClass">
59
+ <div :class="uiModalHeaderContentClass">
60
+ <DialogTitle :class="uiModalTitleClass">{{ title }}</DialogTitle>
61
+ <DialogDescription v-if="description" :class="uiModalDescriptionClass">
62
+ {{ description }}
63
+ </DialogDescription>
64
+ <VisuallyHidden v-else>
65
+ <DialogDescription>{{ title }}</DialogDescription>
66
+ </VisuallyHidden>
67
+ </div>
68
+ <DialogClose :class="uiModalCloseClass" aria-label="Close dialog">
69
+ <X :size="20" aria-hidden="true" />
70
+ </DialogClose>
71
+ </div>
72
+ <div :class="uiModalBodyClass">
73
+ <slot />
74
+ </div>
75
+ <div v-if="$slots.actions" :class="uiModalActionsClass">
76
+ <slot name="actions" />
77
+ </div>
78
+ </DialogContent>
79
+ </div>
80
+ </DialogPortal>
81
+ </DialogRoot>
82
+ </template>
@@ -0,0 +1,143 @@
1
+ import { css, cva } from "@styled/css";
2
+
3
+ export const uiToastViewportClass = css({
4
+ position: "fixed",
5
+ bottom: "4",
6
+ right: "4",
7
+ zIndex: "60",
8
+ display: "flex",
9
+ flexDirection: "column",
10
+ gap: "2",
11
+ w: "360px",
12
+ maxW: "calc(100vw - 2rem)",
13
+ maxHeight: "calc(100dvh - 2rem)",
14
+ pointerEvents: "none",
15
+ outline: "none",
16
+ });
17
+
18
+ export const uiToastRootClass = cva({
19
+ base: {
20
+ display: "flex",
21
+ alignItems: "flex-start",
22
+ gap: "3",
23
+ p: "4",
24
+ borderRadius: "lg",
25
+ borderWidth: "1px",
26
+ bg: "bg.cardStrong",
27
+ borderColor: "border.default",
28
+ boxShadow:
29
+ "0 4px 12px -4px rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)",
30
+ pointerEvents: "auto",
31
+ position: "relative",
32
+ overflow: "hidden",
33
+ transition: "all 240ms cubic-bezier(0.16, 1, 0.3, 1)",
34
+ _dark: {
35
+ boxShadow:
36
+ "0 4px 12px -4px rgba(0, 0, 0, 0.4), 0 1px 2px 0 rgba(0, 0, 0, 0.24)",
37
+ },
38
+ '&[data-state="open"]': {
39
+ animation: "toastSlideIn 240ms cubic-bezier(0.16, 1, 0.3, 1)",
40
+ },
41
+ '&[data-state="closed"]': {
42
+ animation: "toastFadeOut 160ms cubic-bezier(0.25, 0.1, 0.25, 1)",
43
+ },
44
+ '&[data-swipe="move"]': {
45
+ transform: "translateX(var(--reka-toast-swipe-move-x))",
46
+ },
47
+ '&[data-swipe="cancel"]': {
48
+ transform: "translateX(0)",
49
+ transition: "transform 200ms ease-out",
50
+ },
51
+ '&[data-swipe="end"]': {
52
+ animation: "toastSwipeOut 100ms ease-out",
53
+ },
54
+ },
55
+ variants: {
56
+ tone: {
57
+ success: {
58
+ borderColor: "badge.successBorder",
59
+ bg: "badge.successBg",
60
+ },
61
+ error: {
62
+ borderColor: "badge.dangerBorder",
63
+ bg: "badge.dangerBg",
64
+ },
65
+ info: {
66
+ borderColor: "badge.infoBorder",
67
+ bg: "badge.infoBg",
68
+ },
69
+ warning: {
70
+ borderColor: "badge.warningBorder",
71
+ bg: "badge.warningBg",
72
+ },
73
+ },
74
+ },
75
+ defaultVariants: {
76
+ tone: "info",
77
+ },
78
+ });
79
+
80
+ export const uiToastIconClass = cva({
81
+ base: {
82
+ flexShrink: "0",
83
+ w: "5",
84
+ h: "5",
85
+ },
86
+ variants: {
87
+ tone: {
88
+ success: { color: "badge.successText" },
89
+ error: { color: "badge.dangerText" },
90
+ info: { color: "badge.infoText" },
91
+ warning: { color: "badge.warningText" },
92
+ },
93
+ },
94
+ defaultVariants: {
95
+ tone: "info",
96
+ },
97
+ });
98
+
99
+ export const uiToastContentClass = css({
100
+ flex: "1",
101
+ minW: "0",
102
+ });
103
+
104
+ export const uiToastTitleClass = css({
105
+ fontWeight: "600",
106
+ fontSize: "sm",
107
+ color: "text.primary",
108
+ lineHeight: "1.4",
109
+ });
110
+
111
+ export const uiToastDescriptionClass = css({
112
+ fontSize: "sm",
113
+ color: "text.secondary",
114
+ lineHeight: "1.5",
115
+ });
116
+
117
+ export const uiToastCloseClass = css({
118
+ position: "absolute",
119
+ top: "3",
120
+ right: "3",
121
+ display: "flex",
122
+ alignItems: "center",
123
+ justifyContent: "center",
124
+ w: "6",
125
+ h: "6",
126
+ borderRadius: "md",
127
+ border: "none",
128
+ bg: "transparent",
129
+ color: "text.muted",
130
+ cursor: "pointer",
131
+ flexShrink: "0",
132
+ transition:
133
+ "color 160ms cubic-bezier(0.25, 0.1, 0.25, 1), background 160ms cubic-bezier(0.25, 0.1, 0.25, 1)",
134
+ _hover: {
135
+ bg: "bg.hover",
136
+ color: "text.primary",
137
+ },
138
+ _focusVisible: {
139
+ outline: "2px solid",
140
+ outlineColor: "border.accent",
141
+ outlineOffset: "2px",
142
+ },
143
+ });
@@ -0,0 +1,47 @@
1
+ import { mount } from "@vue/test-utils";
2
+ import { describe, expect, it } from "vitest";
3
+ import { ToastProvider } from "reka-ui";
4
+ import UiToast from "./UiToast.vue";
5
+
6
+ function mountToast(props: Record<string, unknown> = {}) {
7
+ return mount(
8
+ {
9
+ components: { ToastProvider, UiToast },
10
+ template: `<ToastProvider><UiToast v-bind="props" /></ToastProvider>`,
11
+ setup() {
12
+ return { props };
13
+ },
14
+ },
15
+ {
16
+ props: {
17
+ tone: "success",
18
+ message: "Workspace created",
19
+ ...props,
20
+ },
21
+ }
22
+ );
23
+ }
24
+
25
+ describe("UiToast", () => {
26
+ it("renders without errors", () => {
27
+ const wrapper = mountToast();
28
+ expect(wrapper.findComponent(UiToast).exists()).toBe(true);
29
+ });
30
+
31
+ it("passes tone prop correctly", () => {
32
+ const wrapper = mountToast({ tone: "error", message: "Failed" });
33
+ expect(wrapper.findComponent(UiToast).props("tone")).toBe("error");
34
+ });
35
+
36
+ it("passes message prop correctly", () => {
37
+ const wrapper = mountToast({ message: "Custom message" });
38
+ expect(wrapper.findComponent(UiToast).props("message")).toBe(
39
+ "Custom message"
40
+ );
41
+ });
42
+
43
+ it("passes duration prop", () => {
44
+ const wrapper = mountToast({ duration: 8000 });
45
+ expect(wrapper.findComponent(UiToast).props("duration")).toBe(8000);
46
+ });
47
+ });
@@ -0,0 +1,65 @@
1
+ <script setup lang="ts">
2
+ import { X } from "lucide-vue-next";
3
+ import {
4
+ ToastClose,
5
+ ToastDescription,
6
+ ToastRoot,
7
+ ToastTitle,
8
+ } from "reka-ui";
9
+ import {
10
+ uiToastCloseClass,
11
+ uiToastContentClass,
12
+ uiToastDescriptionClass,
13
+ uiToastIconClass,
14
+ uiToastRootClass,
15
+ uiToastTitleClass,
16
+ } from "./UiToast.styles";
17
+ import type { ToastTone } from "../composables/useToast";
18
+ import {
19
+ CheckCircle,
20
+ AlertCircle,
21
+ Info,
22
+ AlertTriangle,
23
+ } from "lucide-vue-next";
24
+
25
+ const props = withDefaults(
26
+ defineProps<{
27
+ tone: ToastTone;
28
+ message: string;
29
+ title?: string;
30
+ duration?: number;
31
+ }>(),
32
+ {
33
+ title: undefined,
34
+ duration: 4000,
35
+ }
36
+ );
37
+
38
+ const iconMap = {
39
+ success: CheckCircle,
40
+ error: AlertCircle,
41
+ info: Info,
42
+ warning: AlertTriangle,
43
+ };
44
+ </script>
45
+
46
+ <template>
47
+ <ToastRoot
48
+ :class="uiToastRootClass({ tone })"
49
+ :duration="duration"
50
+ :type="'foreground'"
51
+ >
52
+ <component
53
+ :is="iconMap[props.tone]"
54
+ :class="uiToastIconClass({ tone })"
55
+ aria-hidden="true"
56
+ />
57
+ <div :class="uiToastContentClass">
58
+ <ToastTitle v-if="title" :class="uiToastTitleClass">{{ title }}</ToastTitle>
59
+ <ToastDescription :class="uiToastDescriptionClass">{{ message }}</ToastDescription>
60
+ </div>
61
+ <ToastClose :class="uiToastCloseClass" aria-label="Dismiss">
62
+ <X :size="14" aria-hidden="true" />
63
+ </ToastClose>
64
+ </ToastRoot>
65
+ </template>
@@ -0,0 +1,22 @@
1
+ <script setup lang="ts">
2
+ import { ToastProvider, ToastViewport } from "reka-ui";
3
+ import { useToast } from "../composables/useToast";
4
+ import UiToast from "./UiToast.vue";
5
+ import { uiToastViewportClass } from "./UiToast.styles";
6
+
7
+ const { toasts } = useToast();
8
+ </script>
9
+
10
+ <template>
11
+ <ToastProvider swipe-direction="right">
12
+ <slot />
13
+ <template v-for="toast in toasts" :key="toast.id">
14
+ <UiToast
15
+ :tone="toast.tone"
16
+ :message="toast.message"
17
+ :duration="toast.duration"
18
+ />
19
+ </template>
20
+ <ToastViewport :class="uiToastViewportClass" />
21
+ </ToastProvider>
22
+ </template>
@@ -0,0 +1,25 @@
1
+ import { css } from "@styled/css";
2
+
3
+ // Tooltip content floating panel. Positioned by reka-ui (floating-ui),
4
+ // so this only defines the visual surface.
5
+
6
+ export const tooltipContent = css({
7
+ bg: "bg.menu",
8
+ color: "text.primary",
9
+ fontSize: "xs",
10
+ fontWeight: "500",
11
+ lineHeight: "1.4",
12
+ px: "2.5",
13
+ py: "1.5",
14
+ borderRadius: "md",
15
+ boxShadow:
16
+ "0 4px 16px -4px rgba(15,23,42,0.08), 0 2px 6px -2px rgba(15,23,42,0.04)",
17
+ zIndex: "70",
18
+ maxWidth: "90",
19
+ whiteSpace: "normal",
20
+ wordBreak: "break-word",
21
+ _dark: {
22
+ boxShadow:
23
+ "0 4px 16px -4px rgba(0,0,0,0.48), 0 2px 6px -2px rgba(0,0,0,0.28)",
24
+ },
25
+ });
@@ -0,0 +1,37 @@
1
+ import { mount } from "@vue/test-utils";
2
+ import { describe, expect, it } from "vitest";
3
+ import UiTooltip from "./UiTooltip.vue";
4
+
5
+ describe("UiTooltip", () => {
6
+ it("renders the trigger slot", () => {
7
+ const wrapper = mount(UiTooltip, {
8
+ props: { content: "Help" },
9
+ slots: { default: '<button type="button">Hover me</button>' },
10
+ });
11
+
12
+ expect(wrapper.text()).toContain("Hover me");
13
+ expect(wrapper.find("button").exists()).toBe(true);
14
+ });
15
+
16
+ it("renders the trigger bare when content is empty (no tooltip)", () => {
17
+ const wrapper = mount(UiTooltip, {
18
+ props: { content: "" },
19
+ slots: { default: '<button type="button">Bare</button>' },
20
+ });
21
+
22
+ expect(wrapper.find("button").exists()).toBe(true);
23
+ expect(wrapper.text()).toContain("Bare");
24
+ // No tooltip content rendered for empty content.
25
+ expect(wrapper.find('[role="tooltip"]').exists()).toBe(false);
26
+ });
27
+
28
+ it("keeps the trigger focusable", () => {
29
+ const wrapper = mount(UiTooltip, {
30
+ props: { content: "Help" },
31
+ slots: { default: '<button type="button">Hover me</button>' },
32
+ });
33
+
34
+ const trigger = wrapper.find("button");
35
+ expect(trigger.attributes("tabindex")).not.toBe("-1");
36
+ });
37
+ });