@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.
@@ -2,16 +2,29 @@
2
2
 
3
3
  ---
4
4
 
5
- ## Figma Access & Library Links
5
+ ## Figma design system
6
6
 
7
- Figma components and design files:
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
- - **[AA Design System Components (Figma)](https://www.figma.com/design/SrZQ19QVN1sQkuAMnMC746/AA26--WIP-?node-id=580-9181&t=2HPn1uKWpFKHnrMR-1)** — component library and design system.
9
+ ### Adding Components to your Figma file
10
10
 
11
- ## Using Design Tokens in Figma
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
- Coming soon.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aleph-alpha/ui-library",
3
- "version": "1.18.0",
3
+ "version": "1.20.0",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "main": "dist/system/lib.js",
@@ -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
- const delegatedProps = reactiveOmit(props, 'class');
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="cn(badgeVariants({ variant }), props.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 [a&]:hover:bg-primary/90',
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 [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-[#FF8D8F] dark:text-black',
16
- outline: 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-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="asChild"
25
- :class="cn(buttonVariants({ variant, size }), props.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 hover:bg-primary/90',
11
+ default: 'bg-primary text-primary-foreground',
12
12
  destructive:
13
- 'bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:border-transparent focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40',
14
- outline:
15
- 'border border-input bg-transparent text-foreground shadow-xs hover:bg-hover-default',
16
- secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
17
- ghost: 'text-foreground hover:bg-hover-default',
18
- link: 'text-link underline-offset-4 hover:underline',
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 with dropdown menu
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
+ };