vueless 1.3.6-beta.1 → 1.3.6-beta.10

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/components.d.ts +2 -0
  2. package/components.ts +2 -0
  3. package/constants.d.ts +2 -0
  4. package/constants.js +2 -0
  5. package/package.json +2 -2
  6. package/types.ts +2 -0
  7. package/ui.button/UButton.vue +1 -1
  8. package/ui.button/storybook/stories.ts +2 -2
  9. package/ui.container-card/storybook/stories.ts +2 -2
  10. package/ui.container-drawer/UDrawer.vue +2 -2
  11. package/ui.container-drawer/storybook/stories.ts +2 -2
  12. package/ui.container-grid/UGrid.vue +39 -0
  13. package/ui.container-grid/config.ts +123 -0
  14. package/ui.container-grid/constants.ts +5 -0
  15. package/ui.container-grid/storybook/docs.mdx +17 -0
  16. package/ui.container-grid/storybook/stories.ts +246 -0
  17. package/ui.container-grid/tests/UGrid.test.ts +297 -0
  18. package/ui.container-grid/types.ts +91 -0
  19. package/ui.container-modal/storybook/stories.ts +2 -2
  20. package/ui.container-modal-confirm/storybook/stories.ts +2 -2
  21. package/ui.container-page/storybook/stories.ts +3 -3
  22. package/ui.form-calendar/tests/UCalendar.test.ts +113 -0
  23. package/ui.form-date-picker-range/UDatePickerRangeInputs.vue +5 -1
  24. package/ui.form-date-picker-range/tests/UDatePickerRange.test.ts +114 -0
  25. package/ui.form-date-picker-range/types.ts +1 -0
  26. package/ui.form-listbox/config.ts +1 -1
  27. package/ui.image-avatar/UAvatar.vue +55 -28
  28. package/ui.image-avatar/config.ts +18 -1
  29. package/ui.image-avatar/storybook/docs.mdx +16 -1
  30. package/ui.image-avatar/storybook/stories.ts +17 -3
  31. package/ui.image-avatar/tests/UAvatar.test.ts +35 -7
  32. package/ui.image-avatar/types.ts +13 -0
  33. package/ui.image-avatar-group/UAvatarGroup.vue +87 -0
  34. package/ui.image-avatar-group/config.ts +11 -0
  35. package/ui.image-avatar-group/constants.ts +5 -0
  36. package/ui.image-avatar-group/storybook/docs.mdx +16 -0
  37. package/ui.image-avatar-group/storybook/stories.ts +147 -0
  38. package/ui.image-avatar-group/tests/UAvatarGroup.test.ts +141 -0
  39. package/ui.image-avatar-group/types.ts +51 -0
  40. package/ui.navigation-pagination/storybook/stories.ts +2 -2
  41. package/ui.navigation-tab/tests/UTab.test.ts +2 -3
  42. package/ui.navigation-tabs/UTabs.vue +44 -1
  43. package/ui.navigation-tabs/storybook/stories.ts +33 -0
  44. package/ui.navigation-tabs/tests/UTabs.test.ts +88 -0
  45. package/ui.text-block/config.ts +1 -0
  46. package/ui.text-block/storybook/stories.ts +2 -2
  47. package/ui.text-notify/UNotify.vue +31 -8
  48. package/ui.text-notify/config.ts +1 -1
  49. package/ui.text-notify/tests/UNotify.test.ts +22 -7
@@ -440,6 +440,37 @@ describe("UDatePickerRange.vue", () => {
440
440
 
441
441
  expect(component.find("[vl-key='datepickerCalendar']").exists()).toBe(true);
442
442
  });
443
+
444
+ it("Menu – allows selecting the same day for from and to in range mode", async () => {
445
+ const component = mount(UDatePickerRange, {
446
+ props: {
447
+ variant: "input",
448
+ modelValue: { from: null, to: null },
449
+ dateFormat: "Y-m-d",
450
+ "onUpdate:modelValue": (value: RangeDate) => {
451
+ component.setProps({ modelValue: value });
452
+ },
453
+ },
454
+ });
455
+
456
+ const input = component.findComponent(UInput).get("input");
457
+
458
+ await input.trigger("focus");
459
+
460
+ const days = component.findAll("[vl-key='day']");
461
+
462
+ await days[10].trigger("click");
463
+ await days[10].trigger("click");
464
+
465
+ expect(component.emitted("update:modelValue")).toBeTruthy();
466
+
467
+ const emittedValues = component.emitted("update:modelValue")!;
468
+ const lastEmittedValue = emittedValues[emittedValues.length - 1][0] as RangeDate;
469
+
470
+ expect(lastEmittedValue.from).not.toBeNull();
471
+ expect(lastEmittedValue.to).not.toBeNull();
472
+ expect(lastEmittedValue.from).toBe(lastEmittedValue.to);
473
+ });
443
474
  });
444
475
 
445
476
  describe("Range Navigation", () => {
@@ -554,6 +585,89 @@ describe("UDatePickerRange.vue", () => {
554
585
  });
555
586
  });
556
587
 
588
+ describe("Events", () => {
589
+ it("ChangeRange – handles change-range event from calendar when dates are selected", async () => {
590
+ const component = mount(UDatePickerRange, {
591
+ props: {
592
+ variant: "input",
593
+ modelValue: { from: null, to: null },
594
+ dateFormat: "Y-m-d",
595
+ "onUpdate:modelValue": (value: RangeDate) => {
596
+ component.setProps({ modelValue: value });
597
+ },
598
+ },
599
+ });
600
+
601
+ const input = component.findComponent(UInput).find("input");
602
+
603
+ await input.trigger("focus");
604
+
605
+ const days = component.findAll("[vl-key='day']");
606
+
607
+ expect(days.length).toBeGreaterThan(0);
608
+
609
+ await days[0].trigger("click");
610
+ await days[3].trigger("click");
611
+
612
+ expect(component.emitted("update:modelValue")).toBeTruthy();
613
+ });
614
+
615
+ it("ChangeRange – handles change-range event from calendar when both dates are selected", async () => {
616
+ const component = mount(UDatePickerRange, {
617
+ props: {
618
+ variant: "input",
619
+ modelValue: { from: null, to: null },
620
+ dateFormat: "Y-m-d",
621
+ "onUpdate:modelValue": (value: RangeDate) => {
622
+ component.setProps({ modelValue: value });
623
+ },
624
+ },
625
+ });
626
+
627
+ const input = component.findComponent(UInput).find("input");
628
+
629
+ await input.trigger("focus");
630
+
631
+ const days = component.findAll("[vl-key='day']");
632
+
633
+ await days[0].trigger("click");
634
+ await days[3].trigger("click");
635
+
636
+ expect(component.emitted("update:modelValue")).toBeTruthy();
637
+ });
638
+
639
+ it("ChangeRange – handles change-range event from calendar when same date is selected twice", async () => {
640
+ const component = mount(UDatePickerRange, {
641
+ props: {
642
+ variant: "input",
643
+ modelValue: { from: null, to: null },
644
+ dateFormat: "Y-m-d",
645
+ "onUpdate:modelValue": (value: RangeDate) => {
646
+ component.setProps({ modelValue: value });
647
+ },
648
+ },
649
+ });
650
+
651
+ const input = component.findComponent(UInput).find("input");
652
+
653
+ await input.trigger("focus");
654
+
655
+ const days = component.findAll("[vl-key='day']");
656
+
657
+ await days[10].trigger("click");
658
+ await days[10].trigger("click");
659
+
660
+ expect(component.emitted("update:modelValue")).toBeTruthy();
661
+
662
+ const emittedValues = component.emitted("update:modelValue")!;
663
+ const lastEmittedValue = emittedValues[emittedValues.length - 1][0] as RangeDate;
664
+
665
+ expect(lastEmittedValue.from).not.toBeNull();
666
+ expect(lastEmittedValue.to).not.toBeNull();
667
+ expect(lastEmittedValue.from).toBe(lastEmittedValue.to);
668
+ });
669
+ });
670
+
557
671
  describe("Exposed Properties", () => {
558
672
  it("Exposes wrapper element ref", () => {
559
673
  const component = mount(UDatePickerRange, {
@@ -74,6 +74,7 @@ export interface UDatePickerRangeInputsProps {
74
74
  minDate: string | Date | undefined;
75
75
  config: Config;
76
76
  attrs: UDatePickerRangeInputsAttrs;
77
+ dataTest?: string | null;
77
78
  }
78
79
 
79
80
  export interface CustomRangeButton {
@@ -1,7 +1,7 @@
1
1
  export default /*tw*/ {
2
2
  wrapper: {
3
3
  base: `
4
- my-2 p-1 flex flex-col gap-1 w-auto absolute z-50 shadow-sm
4
+ p-1 flex flex-col gap-1 w-auto absolute z-50 shadow-sm
5
5
  rounded-medium border border-solid border-default bg-default
6
6
  overflow-auto [-webkit-overflow-scrolling:touch]
7
7
  focus:outline-hidden
@@ -1,10 +1,11 @@
1
1
  <script setup lang="ts">
2
- import { computed, useTemplateRef } from "vue";
2
+ import { computed, inject, useTemplateRef } from "vue";
3
3
 
4
4
  import { useUI } from "../composables/useUI";
5
5
  import { getDefaults } from "../utils/ui";
6
6
 
7
7
  import UIcon from "../ui.image-icon/UIcon.vue";
8
+ import UChip from "../ui.other-chip/UChip.vue";
8
9
 
9
10
  import { COMPONENT_NAME } from "./constants";
10
11
  import defaultConfig from "./config";
@@ -16,6 +17,7 @@ defineOptions({ inheritAttrs: false });
16
17
  const props = withDefaults(defineProps<Props>(), {
17
18
  ...getDefaults<Props, Config>(defaultConfig, COMPONENT_NAME),
18
19
  label: "",
20
+ chip: () => ({}),
19
21
  });
20
22
 
21
23
  const emit = defineEmits([
@@ -25,6 +27,16 @@ const emit = defineEmits([
25
27
  "click",
26
28
  ]);
27
29
 
30
+ const getAvatarGroupSize = inject<() => Props["size"]>("getAvatarGroupSize", () => undefined);
31
+ const getAvatarGroupVariant = inject<() => Props["variant"]>(
32
+ "getAvatarGroupVariant",
33
+ () => undefined,
34
+ );
35
+ const getAvatarGroupRounded = inject<() => Props["rounded"]>(
36
+ "getAvatarGroupRounded",
37
+ () => undefined,
38
+ );
39
+
28
40
  const avatarRef = useTemplateRef<HTMLDivElement>("avatar");
29
41
 
30
42
  const labelFirstLetters = computed(() => {
@@ -42,6 +54,8 @@ const backgroundImage = computed(() => {
42
54
  return props.src ? `background-image: url(${props.src});` : "";
43
55
  });
44
56
 
57
+ const hasChip = computed(() => props.chip != null && Boolean(Object.keys(props.chip).length));
58
+
45
59
  function onClick(event: MouseEvent) {
46
60
  emit("click", event);
47
61
  }
@@ -65,38 +79,51 @@ defineExpose({
65
79
  const mutatedProps = computed(() => ({
66
80
  /* component state, not a props */
67
81
  src: Boolean(props.src),
82
+ size: getAvatarGroupSize() || props.size,
83
+ variant: getAvatarGroupVariant() || props.variant,
84
+ rounded: getAvatarGroupRounded() || props.rounded,
68
85
  }));
69
86
 
70
- const { getDataTest, config, avatarAttrs, placeholderIconAttrs } = useUI<Config>(
71
- defaultConfig,
72
- mutatedProps,
73
- );
87
+ const { getDataTest, config, avatarAttrs, placeholderIconAttrs, chipAttrs, hiddenAttrs } =
88
+ useUI<Config>(defaultConfig, mutatedProps);
74
89
  </script>
75
90
 
76
91
  <template>
77
- <div
78
- ref="avatar"
79
- :title="label"
80
- :style="backgroundImage"
81
- v-bind="avatarAttrs"
82
- :data-test="getDataTest()"
83
- @click="onClick"
92
+ <UChip
93
+ :icon="chip.icon"
94
+ :color="chip.color"
95
+ :inset="chip.inset"
96
+ :x-position="chip.xPosition"
97
+ :y-position="chip.yPosition"
98
+ v-bind="chipAttrs"
84
99
  >
85
- <template v-if="!backgroundImage">
86
- <!--
87
- @slot Use it to add something instead of the avatar image placeholder.
88
- @binding {string} icon-name
89
- -->
90
- <slot name="placeholder" :icon-name="placeholderIconName">
91
- <template v-if="labelFirstLetters">{{ labelFirstLetters }}</template>
92
- <UIcon
93
- v-else
94
- :size="size"
95
- color="inherit"
96
- :name="placeholderIconName"
97
- v-bind="placeholderIconAttrs"
98
- />
99
- </slot>
100
+ <div
101
+ ref="avatar"
102
+ :title="label"
103
+ :style="backgroundImage"
104
+ v-bind="avatarAttrs"
105
+ :data-test="getDataTest()"
106
+ @click="onClick"
107
+ >
108
+ <template v-if="!backgroundImage">
109
+ <!--
110
+ @slot Use it to add something instead of the avatar image placeholder.
111
+ @binding {string} icon-name
112
+ -->
113
+ <slot name="placeholder" :icon-name="placeholderIconName">
114
+ <template v-if="labelFirstLetters">{{ labelFirstLetters }}</template>
115
+ <UIcon
116
+ v-else
117
+ :size="size"
118
+ color="inherit"
119
+ :name="placeholderIconName"
120
+ v-bind="placeholderIconAttrs"
121
+ />
122
+ </slot>
123
+ </template>
124
+ </div>
125
+ <template v-if="!hasChip" #chip>
126
+ <span v-bind="hiddenAttrs">&nbsp;</span>
100
127
  </template>
101
- </div>
128
+ </UChip>
102
129
  </template>
@@ -1,7 +1,7 @@
1
1
  export default /*tw*/ {
2
2
  avatar: {
3
3
  base: `
4
- flex items-center justify-center shrink-0 border
4
+ flex items-center justify-center shrink-0 border relative
5
5
  text-{color} bg-{color}/10 bg-contain bg-center bg-no-repeat
6
6
  `,
7
7
  variants: {
@@ -34,6 +34,23 @@ export default /*tw*/ {
34
34
  },
35
35
  },
36
36
  },
37
+ chip: {
38
+ base: "{UChip}",
39
+ defaults: {
40
+ size: {
41
+ "3xs": "xs",
42
+ "2xs": "xs",
43
+ xs: "sm",
44
+ sm: "md",
45
+ md: "md",
46
+ lg: "lg",
47
+ xl: "xl",
48
+ "2xl": "xl",
49
+ "3xl": "xl",
50
+ },
51
+ },
52
+ },
53
+ hidden: "hidden",
37
54
  placeholderIcon: "{UIcon}",
38
55
  defaults: {
39
56
  color: "grayscale",
@@ -1,4 +1,4 @@
1
- import { Meta, Title, Subtitle, Description, Primary, Controls, Stories, Source } from "@storybook/addon-docs/blocks";
1
+ import { Meta, Title, Subtitle, Description, Primary, Controls, Stories, Source, Markdown } from "@storybook/addon-docs/blocks";
2
2
  import { getSource } from "../../utils/storybook";
3
3
 
4
4
  import * as stories from "./stories";
@@ -12,5 +12,20 @@ import defaultConfig from "../config?raw"
12
12
  <Controls of={stories.Default} />
13
13
  <Stories of={stories} />
14
14
 
15
+ ## Chip Object Properties
16
+ Keys you may/have to provide to the component in a `chip` object.
17
+
18
+ <Markdown>
19
+ {`
20
+ | Key name | Description | Type |
21
+ | ---------------------- | ----------------------------------------------------- | ----------------------- |
22
+ | icon | Chip icon name | String |
23
+ | color | Chip color | String |
24
+ | xPosition | Chip x-axis position | String |
25
+ | yPosition | Chip y-axis position | String |
26
+ | inset | Display the chip inside the component | Boolean |
27
+ `}
28
+ </Markdown>
29
+
15
30
  ## Default config
16
31
  <Source code={getSource(defaultConfig)} language="jsx" dark />
@@ -9,7 +9,6 @@ import {
9
9
  import UAvatar from "../../ui.image-avatar/UAvatar.vue";
10
10
  import URow from "../../ui.container-row/URow.vue";
11
11
  import ULoader from "../../ui.loader/ULoader.vue";
12
- import tooltip from "../../v.tooltip/vTooltip";
13
12
 
14
13
  import johnDoeImg from "./assets/john-doe.png";
15
14
 
@@ -22,7 +21,7 @@ interface UAvatarArgs extends Props {
22
21
  }
23
22
 
24
23
  export default {
25
- id: "6030",
24
+ id: "6020",
26
25
  title: "Images & Icons / Avatar",
27
26
  component: UAvatar,
28
27
  args: {
@@ -51,7 +50,6 @@ const DefaultTemplate: StoryFn<UAvatarArgs> = (args: UAvatarArgs) => ({
51
50
 
52
51
  const EnumTemplate: StoryFn<UAvatarArgs> = (args: UAvatarArgs, { argTypes }) => ({
53
52
  components: { URow, UAvatar },
54
- directives: { tooltip },
55
53
  setup: () => ({ args, argTypes, getArgs }),
56
54
  template: `
57
55
  <URow>
@@ -106,6 +104,22 @@ PlaceholderIcon.args = {
106
104
  placeholderIcon: "account_circle",
107
105
  };
108
106
 
107
+ export const Chip = DefaultTemplate.bind({});
108
+ Chip.args = {
109
+ rounded: "full",
110
+ chip: {
111
+ color: "warning",
112
+ inset: true,
113
+ },
114
+ };
115
+ Chip.parameters = {
116
+ docs: {
117
+ description: {
118
+ story: "For the full list of chip object properties, see the table below.",
119
+ },
120
+ },
121
+ };
122
+
109
123
  export const Variants = EnumTemplate.bind({});
110
124
  Variants.args = { enum: "variant" };
111
125
 
@@ -49,7 +49,11 @@ describe("UAvatar.vue", () => {
49
49
  },
50
50
  });
51
51
 
52
- expect(component.attributes("style")).toContain(expectedStyle);
52
+ // Use the exposed ref to access the avatar div
53
+ const avatarRef = component.vm.avatarRef;
54
+
55
+ expect(avatarRef).toBeDefined();
56
+ expect(avatarRef?.getAttribute("style")).toContain(expectedStyle);
53
57
  // When src is provided, the component should not render any text content
54
58
  expect(component.text()).toBe(expectedText);
55
59
  });
@@ -94,7 +98,11 @@ describe("UAvatar.vue", () => {
94
98
  },
95
99
  });
96
100
 
97
- expect(component.attributes("class")).toContain(classes);
101
+ // Use the exposed ref to access the avatar div
102
+ const avatarRef = component.vm.avatarRef;
103
+
104
+ expect(avatarRef).toBeDefined();
105
+ expect(avatarRef?.className).toContain(classes);
98
106
  });
99
107
  });
100
108
 
@@ -119,7 +127,11 @@ describe("UAvatar.vue", () => {
119
127
  },
120
128
  });
121
129
 
122
- expect(component.attributes("class")).toContain(classes);
130
+ // Use the exposed ref to access the avatar div
131
+ const avatarRef = component.vm.avatarRef;
132
+
133
+ expect(avatarRef).toBeDefined();
134
+ expect(avatarRef?.className).toContain(classes);
123
135
  });
124
136
  });
125
137
 
@@ -144,7 +156,11 @@ describe("UAvatar.vue", () => {
144
156
  },
145
157
  });
146
158
 
147
- expect(component.attributes("class")).toContain(color);
159
+ // Use the exposed ref to access the avatar div
160
+ const avatarRef = component.vm.avatarRef;
161
+
162
+ expect(avatarRef).toBeDefined();
163
+ expect(avatarRef?.className).toContain(color);
148
164
  });
149
165
  });
150
166
 
@@ -165,7 +181,11 @@ describe("UAvatar.vue", () => {
165
181
  },
166
182
  });
167
183
 
168
- expect(component.attributes("class")).toContain(classes);
184
+ // Use the exposed ref to access the avatar div
185
+ const avatarRef = component.vm.avatarRef;
186
+
187
+ expect(avatarRef).toBeDefined();
188
+ expect(avatarRef?.className).toContain(classes);
169
189
  });
170
190
  });
171
191
 
@@ -179,7 +199,11 @@ describe("UAvatar.vue", () => {
179
199
  },
180
200
  });
181
201
 
182
- expect(component.attributes("data-test")).toBe(dataTest);
202
+ // Use the exposed ref to access the avatar div
203
+ const avatarRef = component.vm.avatarRef;
204
+
205
+ expect(avatarRef).toBeDefined();
206
+ expect(avatarRef?.getAttribute("data-test")).toBe(dataTest);
183
207
  });
184
208
  });
185
209
 
@@ -215,7 +239,11 @@ describe("UAvatar.vue", () => {
215
239
  },
216
240
  });
217
241
 
218
- await component.trigger("click");
242
+ // Use the exposed ref to access the avatar div and trigger click
243
+ const avatarRef = component.vm.avatarRef;
244
+
245
+ expect(avatarRef).toBeDefined();
246
+ avatarRef?.dispatchEvent(new Event("click"));
219
247
 
220
248
  expect(component.emitted("click")).toBeTruthy();
221
249
  expect(component.emitted("click")?.length).toBe(expectedLength);
@@ -4,6 +4,14 @@ import type { ComponentConfig } from "../types";
4
4
 
5
5
  export type Config = typeof defaultConfig;
6
6
 
7
+ export interface ChipItem {
8
+ icon?: string;
9
+ color?: Props["color"];
10
+ xPosition?: "left" | "right";
11
+ yPosition?: "top" | "bottom";
12
+ inset?: boolean;
13
+ }
14
+
7
15
  export interface Props {
8
16
  /**
9
17
  * Avatar label (username, nickname, etc.).
@@ -49,6 +57,11 @@ export interface Props {
49
57
  */
50
58
  placeholderIcon?: string;
51
59
 
60
+ /**
61
+ * Avatar chip config.
62
+ */
63
+ chip?: ChipItem;
64
+
52
65
  /**
53
66
  * Component config object.
54
67
  */
@@ -0,0 +1,87 @@
1
+ <script setup lang="ts">
2
+ import { computed, provide, useTemplateRef } from "vue";
3
+
4
+ import { useUI } from "../composables/useUI";
5
+ import { getDefaults } from "../utils/ui";
6
+
7
+ import UAvatar from "../ui.image-avatar/UAvatar.vue";
8
+
9
+ import { COMPONENT_NAME } from "./constants";
10
+ import defaultConfig from "./config";
11
+
12
+ import type { Props, Config } from "./types";
13
+ import type { Props as AvatarProps } from "../ui.image-avatar/types";
14
+
15
+ defineOptions({ inheritAttrs: false });
16
+
17
+ const props = withDefaults(defineProps<Props>(), {
18
+ ...getDefaults<Props, Config>(defaultConfig, COMPONENT_NAME),
19
+ avatars: () => [],
20
+ });
21
+
22
+ const avatarGroupRef = useTemplateRef<HTMLDivElement>("avatarGroup");
23
+
24
+ provide("getAvatarGroupSize", () => props.size);
25
+ provide("getAvatarGroupVariant", () => props.variant);
26
+ provide("getAvatarGroupRounded", () => props.rounded);
27
+
28
+ const visibleAvatars = computed(() => {
29
+ return props.avatars.slice(0, props.max);
30
+ });
31
+
32
+ const remainingCount = computed(() => {
33
+ return props.avatars.length - props.max;
34
+ });
35
+
36
+ const hasMoreAvatars = computed(() => remainingCount.value > 0);
37
+
38
+ defineExpose({
39
+ /**
40
+ * A reference to the avatar group element for direct DOM manipulation.
41
+ * @property {HTMLDivElement}
42
+ */
43
+ avatarGroupRef,
44
+ });
45
+
46
+ /**
47
+ * Get element / nested component attributes for each config token ✨
48
+ * Applies: `class`, `config`, redefined default `props` and dev `vl-...` attributes.
49
+ */
50
+ const { getDataTest, avatarGroupAttrs, avatarAttrs, remainingAttrs } = useUI<Config>(defaultConfig);
51
+ </script>
52
+
53
+ <template>
54
+ <div ref="avatarGroup" v-bind="avatarGroupAttrs" :data-test="getDataTest()">
55
+ <template v-for="(avatar, index) in visibleAvatars" :key="index">
56
+ <!--
57
+ @slot Use it to customize a specific avatar.
58
+ @binding {number} index
59
+ @binding {object} avatar
60
+ -->
61
+ <slot :name="`avatar-${index}`" :index="index" :avatar="avatar">
62
+ <UAvatar
63
+ :src="avatar.src"
64
+ :label="avatar.label"
65
+ :color="avatar.color as AvatarProps['color']"
66
+ :placeholder-icon="avatar.placeholderIcon"
67
+ :chip="avatar.chip"
68
+ v-bind="avatarAttrs"
69
+ />
70
+ </slot>
71
+ </template>
72
+
73
+ <div v-if="hasMoreAvatars">
74
+ <UAvatar :size="size" color="neutral" :rounded="rounded" v-bind="remainingAttrs">
75
+ <template #placeholder>
76
+ <!--
77
+ @slot Use it to customize the remaining count avatar.
78
+ @binding {number} remaining-count
79
+ -->
80
+ <slot name="remaining" :remaining-count="remainingCount">
81
+ {{ `+${remainingCount}` }}
82
+ </slot>
83
+ </template>
84
+ </UAvatar>
85
+ </div>
86
+ </div>
87
+ </template>
@@ -0,0 +1,11 @@
1
+ export default /*tw*/ {
2
+ avatarGroup: "inline-flex items-center",
3
+ avatar: "{UAvatar} ring-3 -me-1.5",
4
+ remaining: "{UAvatar} ring-3",
5
+ defaults: {
6
+ variant: "solid",
7
+ rounded: "md",
8
+ size: "md",
9
+ max: 3,
10
+ },
11
+ };
@@ -0,0 +1,5 @@
1
+ /*
2
+ This const is needed to prevent the issue in script setup:
3
+ `defineProps` is referencing locally declared variables. (vue/valid-define-props)
4
+ */
5
+ export const COMPONENT_NAME = "UAvatarGroup";
@@ -0,0 +1,16 @@
1
+ import { Meta, Title, Subtitle, Description, Primary, Controls, Stories, Source } from "@storybook/addon-docs/blocks";
2
+ import { getSource } from "../../utils/storybook";
3
+
4
+ import * as stories from "./stories";
5
+ import defaultConfig from "../config?raw"
6
+
7
+ <Meta of={stories} />
8
+ <Title of={stories} />
9
+ <Subtitle of={stories} />
10
+ <Description of={stories} />
11
+ <Primary of={stories} />
12
+ <Controls of={stories.Default} />
13
+ <Stories of={stories} />
14
+
15
+ ## Default config
16
+ <Source code={getSource(defaultConfig)} language="jsx" dark />