@aleph-alpha/ui-library 1.18.0 → 1.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/config.d.ts +7 -0
- package/config.js +100 -0
- package/dist/system/index.d.ts +15 -1
- package/dist/system/lib.js +11471 -11418
- package/docs/public-docs/getting-started-designers.md +19 -6
- package/package.json +1 -1
- package/src/components/UiDropdownMenu/UiDropdownMenu.stories.ts +110 -0
- package/src/components/UiField/UiField.vue +2 -2
- package/src/components/UiField/UiFieldError.vue +1 -1
- package/src/components/core/badge/Badge.vue +25 -2
- package/src/components/core/badge/index.ts +4 -5
- package/src/components/core/button/Button.vue +30 -2
- package/src/components/core/button/index.ts +7 -7
- package/src/directives/index.ts +1 -0
- package/src/directives/vHover.stories.ts +129 -0
- package/src/directives/vHover.ts +70 -0
- package/src/index.ts +4 -0
- package/src/patterns/UiDataTable/UiDataTable.stories.ts +98 -1
- package/src/patterns/UiDataTable/UiDataTable.vue +15 -0
- package/src/patterns/UiDataTable/UiDataTableColumnHeader.vue +29 -55
- package/src/patterns/UiDataTable/__tests__/UiDataTable.test.ts +90 -8
- package/src/patterns/UiDataTable/__tests__/UiDataTableColumnHeader.test.ts +27 -81
- package/src/patterns/UiDataTable/types.ts +5 -0
- package/src/patterns/UiDatePicker/__tests__/UiDatePicker.test.ts +8 -6
- package/src/theme/Overlay.stories.ts +12 -12
- package/tokens.json +4048 -1388
|
@@ -2,16 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
## Figma
|
|
5
|
+
## Figma design system
|
|
6
6
|
|
|
7
|
-
Figma components and
|
|
7
|
+
The design system is published as a Figma **library** for the Aleph Alpha organisation. Add it to each design file and build with **library instances** (**components** from **Assets**, and **variables** and **styles** from property pickers) so your work stays connected when the team publishes updates.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
### Adding Components to your Figma file
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
1. In the left sidebar, click the **Assets** tab next to **Files**. That switches the sidebar from the layer list to Assets.
|
|
12
|
+
2. Click **Browse team libraries**.
|
|
13
|
+
3. In the libraries dialog, select **Your organisation** in the left sidebar.
|
|
14
|
+
4. Select **AA26 (WIP)**, then enable it with **Add to file**.
|
|
12
15
|
|
|
13
|
-
|
|
16
|
+
You can then drag **components** from the **Assets** tab into your file.
|
|
17
|
+
|
|
18
|
+
See Figma documentation for more details on how to manage libraries in [Add or remove a library from a design file](https://help.figma.com/hc/en-us/articles/1500008731201-Add-or-remove-a-library-from-a-design-file).
|
|
19
|
+
|
|
20
|
+
!!! note "Beta"
|
|
21
|
+
|
|
22
|
+
The Figma library is in **beta**. Components will change in look and feel (and sometimes structure) as we iterate with the Design team. Expect updates over time.
|
|
23
|
+
|
|
24
|
+
### Design tokens
|
|
25
|
+
|
|
26
|
+
With the library enabled on the file, open the **Style and variables** picker in the **right sidebar** on properties such as fill, stroke, spacing, and typography to apply **variables** and **styles** from the design system.
|
|
14
27
|
|
|
15
28
|
## Link to Contribution Model
|
|
16
29
|
|
|
17
|
-
Coming soon.
|
|
30
|
+
Coming soon.
|
package/package.json
CHANGED
|
@@ -20,6 +20,7 @@ import UiButton from '../UiButton/UiButton.vue';
|
|
|
20
20
|
import UiIconButton from '../UiIconButton/UiIconButton.vue';
|
|
21
21
|
import UiToggle from '../UiToggle/UiToggle.vue';
|
|
22
22
|
import { UiIcon } from '../UiIcon';
|
|
23
|
+
import { UiAvatar, UiAvatarFallback, UiAvatarImage } from '../UiAvatar';
|
|
23
24
|
|
|
24
25
|
const meta: Meta<typeof UiDropdownMenu> = {
|
|
25
26
|
title: 'Components/UiDropdownMenu',
|
|
@@ -1016,3 +1017,112 @@ export const WithSoftToggleTriggerAndText: Story = {
|
|
|
1016
1017
|
},
|
|
1017
1018
|
},
|
|
1018
1019
|
};
|
|
1020
|
+
|
|
1021
|
+
const avatarTriggerTemplateSource = `<script setup lang="ts">
|
|
1022
|
+
import { ref } from 'vue'
|
|
1023
|
+
import {
|
|
1024
|
+
UiDropdownMenu,
|
|
1025
|
+
UiDropdownMenuTrigger,
|
|
1026
|
+
UiDropdownMenuContent,
|
|
1027
|
+
UiDropdownMenuItem,
|
|
1028
|
+
UiDropdownMenuSeparator,
|
|
1029
|
+
UiButton,
|
|
1030
|
+
UiAvatar,
|
|
1031
|
+
UiAvatarImage,
|
|
1032
|
+
UiAvatarFallback,
|
|
1033
|
+
UiIcon,
|
|
1034
|
+
} from '@aleph-alpha/ui-library'
|
|
1035
|
+
|
|
1036
|
+
const open = ref(false)
|
|
1037
|
+
</script>
|
|
1038
|
+
|
|
1039
|
+
<template>
|
|
1040
|
+
<UiDropdownMenu v-model:open="open">
|
|
1041
|
+
<UiDropdownMenuTrigger as-child>
|
|
1042
|
+
<UiButton variant="outline" class="h-auto gap-3 px-3 py-2">
|
|
1043
|
+
<UiAvatar>
|
|
1044
|
+
<UiAvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
|
1045
|
+
<UiAvatarFallback>CN</UiAvatarFallback>
|
|
1046
|
+
</UiAvatar>
|
|
1047
|
+
<div class="flex flex-col items-start overflow-hidden">
|
|
1048
|
+
<span class="truncate text-sm font-medium">Sofia Davis</span>
|
|
1049
|
+
<span class="truncate text-xs text-muted-foreground font-normal">sofia@example.com</span>
|
|
1050
|
+
</div>
|
|
1051
|
+
<UiIcon name="chevrons-up-down" :size="16" class="shrink-0 text-muted-foreground" />
|
|
1052
|
+
</UiButton>
|
|
1053
|
+
</UiDropdownMenuTrigger>
|
|
1054
|
+
<UiDropdownMenuContent class="w-56" align="start">
|
|
1055
|
+
<UiDropdownMenuItem>Profile</UiDropdownMenuItem>
|
|
1056
|
+
<UiDropdownMenuItem>Settings</UiDropdownMenuItem>
|
|
1057
|
+
<UiDropdownMenuItem>Notifications</UiDropdownMenuItem>
|
|
1058
|
+
<UiDropdownMenuSeparator />
|
|
1059
|
+
<UiDropdownMenuItem>Sign Out</UiDropdownMenuItem>
|
|
1060
|
+
</UiDropdownMenuContent>
|
|
1061
|
+
</UiDropdownMenu>
|
|
1062
|
+
</template>`;
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* Dropdown menu with a trigger containing an avatar, title, description, and chevron icon.
|
|
1066
|
+
*/
|
|
1067
|
+
export const WithAvatarTrigger: Story = {
|
|
1068
|
+
render: (args) => ({
|
|
1069
|
+
components: {
|
|
1070
|
+
UiDropdownMenu,
|
|
1071
|
+
UiDropdownMenuTrigger,
|
|
1072
|
+
UiDropdownMenuContent,
|
|
1073
|
+
UiDropdownMenuItem,
|
|
1074
|
+
UiDropdownMenuSeparator,
|
|
1075
|
+
UiButton,
|
|
1076
|
+
UiAvatar,
|
|
1077
|
+
UiAvatarFallback,
|
|
1078
|
+
UiAvatarImage,
|
|
1079
|
+
UiIcon,
|
|
1080
|
+
},
|
|
1081
|
+
setup() {
|
|
1082
|
+
const open = ref(args.open);
|
|
1083
|
+
const modal = ref(args.modal);
|
|
1084
|
+
watch(
|
|
1085
|
+
() => args.open,
|
|
1086
|
+
(v) => {
|
|
1087
|
+
open.value = v;
|
|
1088
|
+
},
|
|
1089
|
+
);
|
|
1090
|
+
watch(
|
|
1091
|
+
() => args.modal,
|
|
1092
|
+
(v) => {
|
|
1093
|
+
modal.value = v;
|
|
1094
|
+
},
|
|
1095
|
+
);
|
|
1096
|
+
return { open, modal };
|
|
1097
|
+
},
|
|
1098
|
+
template: `<UiDropdownMenu v-model:open="open" :modal="modal">
|
|
1099
|
+
<UiDropdownMenuTrigger as-child>
|
|
1100
|
+
<UiButton variant="outline" class="h-auto gap-3 px-3 py-2">
|
|
1101
|
+
<UiAvatar>
|
|
1102
|
+
<UiAvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
|
1103
|
+
<UiAvatarFallback>CN</UiAvatarFallback>
|
|
1104
|
+
</UiAvatar>
|
|
1105
|
+
<div class="flex flex-col items-start overflow-hidden">
|
|
1106
|
+
<span class="truncate text-sm font-medium">Sofia Davis</span>
|
|
1107
|
+
<span class="truncate text-xs text-muted-foreground font-normal">sofia@example.com</span>
|
|
1108
|
+
</div>
|
|
1109
|
+
<UiIcon name="chevrons-up-down" :size="16" class="shrink-0 text-muted-foreground" />
|
|
1110
|
+
</UiButton>
|
|
1111
|
+
</UiDropdownMenuTrigger>
|
|
1112
|
+
<UiDropdownMenuContent class="w-56" align="start">
|
|
1113
|
+
<UiDropdownMenuItem>Profile</UiDropdownMenuItem>
|
|
1114
|
+
<UiDropdownMenuItem>Settings</UiDropdownMenuItem>
|
|
1115
|
+
<UiDropdownMenuItem>Notifications</UiDropdownMenuItem>
|
|
1116
|
+
<UiDropdownMenuSeparator />
|
|
1117
|
+
<UiDropdownMenuItem>Sign Out</UiDropdownMenuItem>
|
|
1118
|
+
</UiDropdownMenuContent>
|
|
1119
|
+
</UiDropdownMenu>`,
|
|
1120
|
+
}),
|
|
1121
|
+
parameters: {
|
|
1122
|
+
docs: {
|
|
1123
|
+
source: {
|
|
1124
|
+
code: avatarTriggerTemplateSource,
|
|
1125
|
+
},
|
|
1126
|
+
},
|
|
1127
|
+
},
|
|
1128
|
+
};
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
function partitionSlots(groupNames: string[]) {
|
|
43
|
-
const children = slots.default?.() ?? [];
|
|
43
|
+
const children = slots.default?.({}) ?? [];
|
|
44
44
|
const group = children.filter((v) => groupNames.includes(getSlotName(v) ?? ''));
|
|
45
45
|
const rest = children.filter((v) => !groupNames.includes(getSlotName(v) ?? ''));
|
|
46
46
|
return { group, rest };
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
const labelPartition = computed(() => partitionSlots(['UiFieldLabel', 'UiFieldDescription']));
|
|
50
50
|
|
|
51
51
|
const inputPartition = computed(() => {
|
|
52
|
-
const children = slots.default?.() ?? [];
|
|
52
|
+
const children = slots.default?.({}) ?? [];
|
|
53
53
|
const content = children.filter((v) => getSlotName(v) === 'UiFieldContent');
|
|
54
54
|
const desc = children.filter((v) => getSlotName(v) === 'UiFieldDescription');
|
|
55
55
|
const rest = children.filter((v) => {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
const props = defineProps<UiFieldErrorProps>();
|
|
11
11
|
|
|
12
12
|
const slots = useSlots();
|
|
13
|
-
const hasDefaultSlotContent = computed(() => (slots.default?.().length ?? 0) > 0);
|
|
13
|
+
const hasDefaultSlotContent = computed(() => (slots.default?.({}).length ?? 0) > 0);
|
|
14
14
|
</script>
|
|
15
15
|
|
|
16
16
|
<template>
|
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
import type { PrimitiveProps } from 'reka-ui';
|
|
3
3
|
import type { HTMLAttributes } from 'vue';
|
|
4
4
|
import type { BadgeVariants } from '.';
|
|
5
|
+
import type { VHoverVariant } from '@/directives/vHover';
|
|
5
6
|
import { reactiveOmit } from '@vueuse/core';
|
|
6
7
|
import { Primitive } from 'reka-ui';
|
|
8
|
+
import { computed } from 'vue';
|
|
7
9
|
import { cn } from '@/lib/utils';
|
|
8
10
|
import { badgeVariants } from '.';
|
|
11
|
+
import { vHover } from '@/directives/vHover';
|
|
9
12
|
|
|
10
13
|
const props = defineProps<
|
|
11
14
|
PrimitiveProps & {
|
|
@@ -14,13 +17,33 @@
|
|
|
14
17
|
}
|
|
15
18
|
>();
|
|
16
19
|
|
|
17
|
-
|
|
20
|
+
type BadgeVariant = NonNullable<BadgeVariants['variant']>;
|
|
21
|
+
|
|
22
|
+
const VARIANT_TO_V_HOVER_OVERLAY = new Map<BadgeVariant, VHoverVariant | false>([
|
|
23
|
+
['default', 'subtle'],
|
|
24
|
+
['secondary', 'default'],
|
|
25
|
+
['destructive', 'default'],
|
|
26
|
+
['outline', 'default'],
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const delegatedProps = reactiveOmit(props, 'class', 'variant');
|
|
30
|
+
|
|
31
|
+
const badgeClass = computed(() => cn(badgeVariants({ variant: props.variant }), props.class));
|
|
32
|
+
|
|
33
|
+
const hoverOverlayVariant = computed(
|
|
34
|
+
() => VARIANT_TO_V_HOVER_OVERLAY.get(props.variant ?? 'default') ?? false,
|
|
35
|
+
);
|
|
18
36
|
</script>
|
|
19
37
|
|
|
20
38
|
<template>
|
|
39
|
+
<Primitive v-if="asChild" data-slot="badge" :class="badgeClass" v-bind="delegatedProps">
|
|
40
|
+
<slot />
|
|
41
|
+
</Primitive>
|
|
21
42
|
<Primitive
|
|
43
|
+
v-else
|
|
44
|
+
v-hover="hoverOverlayVariant"
|
|
22
45
|
data-slot="badge"
|
|
23
|
-
:class="
|
|
46
|
+
:class="badgeClass"
|
|
24
47
|
v-bind="delegatedProps"
|
|
25
48
|
>
|
|
26
49
|
<slot />
|
|
@@ -8,12 +8,11 @@ export const badgeVariants = cva(
|
|
|
8
8
|
{
|
|
9
9
|
variants: {
|
|
10
10
|
variant: {
|
|
11
|
-
default: 'border-transparent bg-primary text-primary-foreground
|
|
12
|
-
secondary:
|
|
13
|
-
'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90',
|
|
11
|
+
default: 'border-transparent bg-primary text-primary-foreground',
|
|
12
|
+
secondary: 'border-transparent bg-secondary text-secondary-foreground',
|
|
14
13
|
destructive:
|
|
15
|
-
'border-transparent bg-destructive text-white
|
|
16
|
-
outline: 'text-foreground
|
|
14
|
+
'border-transparent bg-destructive text-white focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-[#FF8D8F] dark:text-black',
|
|
15
|
+
outline: 'text-foreground',
|
|
17
16
|
},
|
|
18
17
|
},
|
|
19
18
|
defaultVariants: {
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
import type { PrimitiveProps } from 'reka-ui';
|
|
3
3
|
import type { HTMLAttributes } from 'vue';
|
|
4
4
|
import type { ButtonVariants } from '.';
|
|
5
|
+
import type { VHoverVariant } from '@/directives/vHover';
|
|
5
6
|
import { Primitive } from 'reka-ui';
|
|
7
|
+
import { computed } from 'vue';
|
|
6
8
|
import { cn } from '@/lib/utils';
|
|
7
9
|
import { buttonVariants } from '.';
|
|
10
|
+
import { vHover } from '@/directives/vHover';
|
|
8
11
|
|
|
9
12
|
interface Props extends PrimitiveProps {
|
|
10
13
|
variant?: ButtonVariants['variant'];
|
|
@@ -15,14 +18,39 @@
|
|
|
15
18
|
const props = withDefaults(defineProps<Props>(), {
|
|
16
19
|
as: 'button',
|
|
17
20
|
});
|
|
21
|
+
|
|
22
|
+
type ButtonVariant = NonNullable<ButtonVariants['variant']>;
|
|
23
|
+
|
|
24
|
+
/** Background overlay via `v-hover`; `link` uses `overlay-link-hover-subtle` (color) instead. */
|
|
25
|
+
const VARIANT_TO_V_HOVER_OVERLAY = new Map<ButtonVariant, VHoverVariant | false>([
|
|
26
|
+
['default', 'subtle'],
|
|
27
|
+
['link', false],
|
|
28
|
+
['secondary', 'default'],
|
|
29
|
+
['destructive', 'default'],
|
|
30
|
+
['outline', 'default'],
|
|
31
|
+
['ghost', 'default'],
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const buttonClass = computed(() =>
|
|
35
|
+
cn(buttonVariants({ variant: props.variant, size: props.size }), props.class),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const hoverOverlayVariant = computed(
|
|
39
|
+
() => VARIANT_TO_V_HOVER_OVERLAY.get(props.variant ?? 'default') ?? false,
|
|
40
|
+
);
|
|
18
41
|
</script>
|
|
19
42
|
|
|
20
43
|
<template>
|
|
44
|
+
<Primitive v-if="asChild" data-slot="button" :as="as" :as-child="true" :class="buttonClass">
|
|
45
|
+
<slot />
|
|
46
|
+
</Primitive>
|
|
21
47
|
<Primitive
|
|
48
|
+
v-else
|
|
49
|
+
v-hover="hoverOverlayVariant"
|
|
22
50
|
data-slot="button"
|
|
23
51
|
:as="as"
|
|
24
|
-
:as-child="
|
|
25
|
-
:class="
|
|
52
|
+
:as-child="false"
|
|
53
|
+
:class="buttonClass"
|
|
26
54
|
>
|
|
27
55
|
<slot />
|
|
28
56
|
</Primitive>
|
|
@@ -8,14 +8,14 @@ export const buttonVariants = cva(
|
|
|
8
8
|
{
|
|
9
9
|
variants: {
|
|
10
10
|
variant: {
|
|
11
|
-
default: 'bg-primary text-primary-foreground
|
|
11
|
+
default: 'bg-primary text-primary-foreground',
|
|
12
12
|
destructive:
|
|
13
|
-
'bg-destructive text-destructive-foreground
|
|
14
|
-
outline:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
link: 'text-link
|
|
13
|
+
'bg-destructive text-destructive-foreground focus-visible:border-transparent focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40',
|
|
14
|
+
outline: 'border border-input bg-transparent text-foreground shadow-xs',
|
|
15
|
+
secondary: 'bg-secondary text-secondary-foreground',
|
|
16
|
+
ghost:
|
|
17
|
+
'text-foreground hover:border-border-button-outlined focus-visible:border-border-button-outlined',
|
|
18
|
+
link: 'text-link overlay-link-hover-subtle',
|
|
19
19
|
},
|
|
20
20
|
size: {
|
|
21
21
|
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { vHover, type VHoverVariant } from './vHover';
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3-vite';
|
|
2
|
+
import { defineComponent } from 'vue';
|
|
3
|
+
import { vHover } from '@/directives/vHover';
|
|
4
|
+
|
|
5
|
+
const VHoverDemo = defineComponent({
|
|
6
|
+
name: 'VHoverDemo',
|
|
7
|
+
directives: { hover: vHover },
|
|
8
|
+
methods: {
|
|
9
|
+
copyVHover(variant: 'subtle' | 'default' | 'strong', e: MouseEvent) {
|
|
10
|
+
const el = e.currentTarget as HTMLElement;
|
|
11
|
+
const snippet = `v-hover="'${variant}'"`;
|
|
12
|
+
navigator.clipboard
|
|
13
|
+
.writeText(snippet)
|
|
14
|
+
.then(() => {
|
|
15
|
+
el.classList.add('copied');
|
|
16
|
+
setTimeout(() => el.classList.remove('copied'), 1500);
|
|
17
|
+
})
|
|
18
|
+
.catch(() => {
|
|
19
|
+
/* clipboard unavailable (e.g. some test / non-secure contexts) */
|
|
20
|
+
});
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
template: `
|
|
24
|
+
<div class="p-8 bg-white dark:bg-background min-h-screen">
|
|
25
|
+
<style>
|
|
26
|
+
.copy-btn { cursor: pointer; transition: all 0.15s; }
|
|
27
|
+
.copy-btn:hover { background: rgba(0,0,0,0.05); }
|
|
28
|
+
.copy-btn.copied { background: #22c55e22; }
|
|
29
|
+
.copy-btn.copied::after { content: ' ✓'; color: #22c55e; }
|
|
30
|
+
</style>
|
|
31
|
+
|
|
32
|
+
<div class="mb-6">
|
|
33
|
+
<h2 class="text-xl font-bold text-content-on-surface-primary mb-1">v-hover</h2>
|
|
34
|
+
<p class="text-sm text-content-on-surface-muted max-w-2xl">
|
|
35
|
+
Registers as <code class="text-xs">v-hover</code> when using
|
|
36
|
+
<code class="text-xs">app.directive('hover', vHover)</code> from the library install.
|
|
37
|
+
Injects a single overlay layer; visible on <code class="text-xs">:hover</code> when
|
|
38
|
+
<code class="text-xs">@media (hover: hover)</code> and on <code class="text-xs">:focus-visible</code>.
|
|
39
|
+
Variants map to
|
|
40
|
+
<code class="text-xs">--overlay-hover-subtle</code>,
|
|
41
|
+
<code class="text-xs">--overlay-hover-default</code>, and
|
|
42
|
+
<code class="text-xs">--overlay-hover-strong</code>.
|
|
43
|
+
Hover or Tab+focus each tile to compare intensity.
|
|
44
|
+
</p>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="flex flex-wrap gap-8">
|
|
48
|
+
<div class="flex flex-col items-center gap-2">
|
|
49
|
+
<button
|
|
50
|
+
type="button"
|
|
51
|
+
v-hover="'subtle'"
|
|
52
|
+
class="h-24 w-40 rounded-md bg-background-button-primary-button text-content-on-button-primary-button text-sm font-medium flex items-center justify-center border border-border-surface-default shadow-xs"
|
|
53
|
+
>
|
|
54
|
+
subtle
|
|
55
|
+
</button>
|
|
56
|
+
<div class="text-center">
|
|
57
|
+
<span class="text-xs text-content-on-surface-primary font-medium block">subtle</span>
|
|
58
|
+
<code
|
|
59
|
+
class="copy-btn text-[10px] text-content-on-surface-muted block mt-0.5 px-1 py-0.5 rounded"
|
|
60
|
+
@click="copyVHover('subtle', $event)"
|
|
61
|
+
>v-hover="'subtle'"</code>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
<div class="flex flex-col items-center gap-2">
|
|
66
|
+
<button
|
|
67
|
+
type="button"
|
|
68
|
+
v-hover="'default'"
|
|
69
|
+
class="h-24 w-40 rounded-md bg-background-button-primary-button text-content-on-button-primary-button text-sm font-medium flex items-center justify-center border border-border-surface-default shadow-xs"
|
|
70
|
+
>
|
|
71
|
+
default
|
|
72
|
+
</button>
|
|
73
|
+
<div class="text-center">
|
|
74
|
+
<span class="text-xs text-content-on-surface-primary font-medium block">default</span>
|
|
75
|
+
<code
|
|
76
|
+
class="copy-btn text-[10px] text-content-on-surface-muted block mt-0.5 px-1 py-0.5 rounded"
|
|
77
|
+
@click="copyVHover('default', $event)"
|
|
78
|
+
>v-hover="'default'"</code>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div class="flex flex-col items-center gap-2">
|
|
83
|
+
<button
|
|
84
|
+
type="button"
|
|
85
|
+
v-hover="'strong'"
|
|
86
|
+
class="h-24 w-40 rounded-md bg-background-button-primary-button text-content-on-button-primary-button text-sm font-medium flex items-center justify-center border border-border-surface-default shadow-xs"
|
|
87
|
+
>
|
|
88
|
+
strong
|
|
89
|
+
</button>
|
|
90
|
+
<div class="text-center">
|
|
91
|
+
<span class="text-xs text-content-on-surface-primary font-medium block">strong</span>
|
|
92
|
+
<code
|
|
93
|
+
class="copy-btn text-[10px] text-content-on-surface-muted block mt-0.5 px-1 py-0.5 rounded"
|
|
94
|
+
@click="copyVHover('strong', $event)"
|
|
95
|
+
>v-hover="'strong'"</code>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
`,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const meta: Meta<typeof VHoverDemo> = {
|
|
104
|
+
title: 'Directives/v-hover',
|
|
105
|
+
component: VHoverDemo,
|
|
106
|
+
tags: ['autodocs'],
|
|
107
|
+
parameters: {
|
|
108
|
+
layout: 'fullscreen',
|
|
109
|
+
docs: {
|
|
110
|
+
description: {
|
|
111
|
+
component:
|
|
112
|
+
'Overlay hover directive: `false` disables; string `subtle` | `default` | `strong` or `{ variant }`. Used by Button and Badge.',
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export default meta;
|
|
119
|
+
type Story = StoryObj<typeof VHoverDemo>;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Three overlay intensities on the same button fill so you can compare subtle, default, and strong.
|
|
123
|
+
*/
|
|
124
|
+
export const Variants: Story = {
|
|
125
|
+
render: () => ({
|
|
126
|
+
components: { VHoverDemo },
|
|
127
|
+
template: '<VHoverDemo />',
|
|
128
|
+
}),
|
|
129
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { DirectiveBinding, ObjectDirective } from 'vue';
|
|
2
|
+
|
|
3
|
+
export type VHoverVariant = 'subtle' | 'default' | 'strong';
|
|
4
|
+
|
|
5
|
+
const OVERLAY_EL = Symbol('vHoverOverlayEl');
|
|
6
|
+
|
|
7
|
+
type VHoverBinding = VHoverVariant | false | null | undefined | { variant: VHoverVariant };
|
|
8
|
+
|
|
9
|
+
function resolveVariant(binding: DirectiveBinding<VHoverBinding>): VHoverVariant | null {
|
|
10
|
+
const v = binding.value;
|
|
11
|
+
if (v === false || v === null || v === undefined) return null;
|
|
12
|
+
if (typeof v === 'object' && v !== null && 'variant' in v) {
|
|
13
|
+
const { variant } = v;
|
|
14
|
+
if (variant === 'subtle' || variant === 'default' || variant === 'strong') return variant;
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
if (v === 'subtle' || v === 'default' || v === 'strong') return v;
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function detach(el: HTMLElement): void {
|
|
22
|
+
const overlay = (el as unknown as Record<symbol, HTMLElement>)[OVERLAY_EL];
|
|
23
|
+
overlay?.remove();
|
|
24
|
+
delete (el as unknown as Record<symbol, unknown>)[OVERLAY_EL];
|
|
25
|
+
el.classList.remove('v-hover-root');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function attach(el: HTMLElement, variant: VHoverVariant): void {
|
|
29
|
+
detach(el);
|
|
30
|
+
|
|
31
|
+
el.classList.add('v-hover-root');
|
|
32
|
+
|
|
33
|
+
const overlay = document.createElement('span');
|
|
34
|
+
overlay.className = 'v-hover-overlay';
|
|
35
|
+
overlay.setAttribute('data-v-hover-variant', variant);
|
|
36
|
+
overlay.setAttribute('aria-hidden', 'true');
|
|
37
|
+
el.prepend(overlay);
|
|
38
|
+
(el as unknown as Record<symbol, HTMLElement>)[OVERLAY_EL] = overlay;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function setVariant(el: HTMLElement, variant: VHoverVariant): void {
|
|
42
|
+
const overlay = (el as unknown as Record<symbol, HTMLElement>)[OVERLAY_EL];
|
|
43
|
+
if (overlay?.isConnected) {
|
|
44
|
+
overlay.setAttribute('data-v-hover-variant', variant);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const vHover: ObjectDirective<HTMLElement, VHoverBinding> = {
|
|
49
|
+
mounted(el, binding) {
|
|
50
|
+
const variant = resolveVariant(binding);
|
|
51
|
+
if (variant) attach(el, variant);
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
updated(el, binding) {
|
|
55
|
+
const variant = resolveVariant(binding);
|
|
56
|
+
if (!variant) {
|
|
57
|
+
detach(el);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if ((el as unknown as Record<symbol, HTMLElement>)[OVERLAY_EL]?.isConnected) {
|
|
61
|
+
setVariant(el, variant);
|
|
62
|
+
} else {
|
|
63
|
+
attach(el, variant);
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
unmounted(el) {
|
|
68
|
+
detach(el);
|
|
69
|
+
},
|
|
70
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
export * from './components';
|
|
6
6
|
export * from './patterns';
|
|
7
7
|
export * from './templates';
|
|
8
|
+
export * from './directives';
|
|
9
|
+
|
|
10
|
+
import { vHover } from './directives/vHover';
|
|
8
11
|
|
|
9
12
|
const components = import.meta.glob(
|
|
10
13
|
['@/components/**/*.vue', '@/patterns/**/*.vue', '@/templates/**/*.vue'],
|
|
@@ -15,6 +18,7 @@ const components = import.meta.glob(
|
|
|
15
18
|
|
|
16
19
|
export default {
|
|
17
20
|
install(app, options) {
|
|
21
|
+
app.directive('hover', vHover);
|
|
18
22
|
Object.values(components).forEach((component: any) => {
|
|
19
23
|
app.component(component.default.__name, component.default);
|
|
20
24
|
});
|
|
@@ -229,7 +229,7 @@ const data: Payment[] = [
|
|
|
229
229
|
{ id: '3', amount: 837, status: 'processing', email: 'monserrat44@gmail.com' },
|
|
230
230
|
];
|
|
231
231
|
|
|
232
|
-
// Use UiDataTableColumnHeader for sortable columns
|
|
232
|
+
// Use UiDataTableColumnHeader for sortable columns (click to cycle: unsorted → asc → desc)
|
|
233
233
|
const columns: ColumnDef<Payment>[] = [
|
|
234
234
|
{
|
|
235
235
|
accessorKey: 'status',
|
|
@@ -1575,3 +1575,100 @@ export const WithCellFormatting: Story = {
|
|
|
1575
1575
|
template: '<UiDataTable v-bind="args" />',
|
|
1576
1576
|
}),
|
|
1577
1577
|
};
|
|
1578
|
+
|
|
1579
|
+
const withRowClickTemplateSource = `<script setup lang="ts">
|
|
1580
|
+
import { ref, h } from 'vue';
|
|
1581
|
+
import { UiDataTable, UiDataTableColumnHeader } from '@aleph-alpha/ui-library';
|
|
1582
|
+
import type { ColumnDef } from '@tanstack/vue-table';
|
|
1583
|
+
|
|
1584
|
+
interface Payment {
|
|
1585
|
+
id: string;
|
|
1586
|
+
amount: number;
|
|
1587
|
+
status: string;
|
|
1588
|
+
email: string;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
const data: Payment[] = [
|
|
1592
|
+
{ id: '1', amount: 316, status: 'success', email: 'ken99@yahoo.com' },
|
|
1593
|
+
{ id: '2', amount: 242, status: 'success', email: 'abe45@gmail.com' },
|
|
1594
|
+
{ id: '3', amount: 837, status: 'processing', email: 'monserrat44@gmail.com' },
|
|
1595
|
+
];
|
|
1596
|
+
|
|
1597
|
+
const columns: ColumnDef<Payment>[] = [
|
|
1598
|
+
{
|
|
1599
|
+
accessorKey: 'status',
|
|
1600
|
+
header: ({ column }) => h(UiDataTableColumnHeader, { column, title: 'Status' }),
|
|
1601
|
+
size: 120,
|
|
1602
|
+
},
|
|
1603
|
+
{
|
|
1604
|
+
accessorKey: 'email',
|
|
1605
|
+
header: ({ column }) => h(UiDataTableColumnHeader, { column, title: 'Email' }),
|
|
1606
|
+
size: 250,
|
|
1607
|
+
},
|
|
1608
|
+
{
|
|
1609
|
+
accessorKey: 'amount',
|
|
1610
|
+
header: ({ column }) => h(UiDataTableColumnHeader, {
|
|
1611
|
+
column,
|
|
1612
|
+
title: 'Amount',
|
|
1613
|
+
class: 'justify-end'
|
|
1614
|
+
}),
|
|
1615
|
+
cell: ({ row }) => {
|
|
1616
|
+
const amount = Number.parseFloat(row.getValue('amount'));
|
|
1617
|
+
return new Intl.NumberFormat('de-DE', {
|
|
1618
|
+
style: 'currency',
|
|
1619
|
+
currency: 'EUR',
|
|
1620
|
+
}).format(amount);
|
|
1621
|
+
},
|
|
1622
|
+
size: 120,
|
|
1623
|
+
},
|
|
1624
|
+
];
|
|
1625
|
+
|
|
1626
|
+
const clickedRow = ref<Payment | null>(null);
|
|
1627
|
+
</script>
|
|
1628
|
+
|
|
1629
|
+
<template>
|
|
1630
|
+
<UiDataTable
|
|
1631
|
+
:data="data"
|
|
1632
|
+
:columns="columns"
|
|
1633
|
+
:on-row-click="(row) => (clickedRow = row)"
|
|
1634
|
+
/>
|
|
1635
|
+
<div v-if="clickedRow" class="mt-4 rounded border p-3 text-sm">
|
|
1636
|
+
Clicked: {{ clickedRow.email }} - {{ clickedRow.status }}
|
|
1637
|
+
</div>
|
|
1638
|
+
</template>`;
|
|
1639
|
+
|
|
1640
|
+
export const WithRowClick: Story = {
|
|
1641
|
+
args: {
|
|
1642
|
+
data: payments,
|
|
1643
|
+
columns: sortableColumns,
|
|
1644
|
+
},
|
|
1645
|
+
parameters: {
|
|
1646
|
+
docs: {
|
|
1647
|
+
source: {
|
|
1648
|
+
code: withRowClickTemplateSource,
|
|
1649
|
+
},
|
|
1650
|
+
},
|
|
1651
|
+
},
|
|
1652
|
+
render: (args) => ({
|
|
1653
|
+
components: { UiDataTable },
|
|
1654
|
+
setup() {
|
|
1655
|
+
const clickedRow = ref<Payment | null>(null);
|
|
1656
|
+
return {
|
|
1657
|
+
args,
|
|
1658
|
+
clickedRow,
|
|
1659
|
+
handleRowClick: (row: Payment) => {
|
|
1660
|
+
clickedRow.value = row;
|
|
1661
|
+
},
|
|
1662
|
+
};
|
|
1663
|
+
},
|
|
1664
|
+
template: `
|
|
1665
|
+
<div>
|
|
1666
|
+
<UiDataTable v-bind="args" :on-row-click="handleRowClick" />
|
|
1667
|
+
<div v-if="clickedRow" class="mt-4 rounded border p-3 text-sm">
|
|
1668
|
+
Clicked: <strong>{{ clickedRow.email }}</strong> - {{ clickedRow.status }} - {{ new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(clickedRow.amount) }}
|
|
1669
|
+
</div>
|
|
1670
|
+
<div v-else class="text-muted-foreground mt-4 text-sm">Click a row to see its data</div>
|
|
1671
|
+
</div>
|
|
1672
|
+
`,
|
|
1673
|
+
}),
|
|
1674
|
+
};
|