vueless 1.3.6-beta.7 → 1.3.6-beta.9

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/components.d.ts CHANGED
@@ -56,6 +56,7 @@ export { default as UPage } from "./ui.container-page/UPage.vue";
56
56
  /* Images and Icons */
57
57
  export { default as UIcon } from "./ui.image-icon/UIcon.vue";
58
58
  export { default as UAvatar } from "./ui.image-avatar/UAvatar.vue";
59
+ export { default as UAvatarGroup } from "./ui.image-avatar-group/UAvatarGroup.vue";
59
60
  /* Data */
60
61
  export { default as UTable } from "./ui.data-table/UTable.vue";
61
62
  export { default as UDataList } from "./ui.data-list/UDataList.vue";
package/components.ts CHANGED
@@ -56,6 +56,7 @@ export { default as UPage } from "./ui.container-page/UPage.vue";
56
56
  /* Images and Icons */
57
57
  export { default as UIcon } from "./ui.image-icon/UIcon.vue";
58
58
  export { default as UAvatar } from "./ui.image-avatar/UAvatar.vue";
59
+ export { default as UAvatarGroup } from "./ui.image-avatar-group/UAvatarGroup.vue";
59
60
  /* Data */
60
61
  export { default as UTable } from "./ui.data-table/UTable.vue";
61
62
  export { default as UDataList } from "./ui.data-list/UDataList.vue";
package/constants.d.ts CHANGED
@@ -194,6 +194,7 @@ export namespace COMPONENTS {
194
194
  let UDrawer: string;
195
195
  let UIcon: string;
196
196
  let UAvatar: string;
197
+ let UAvatarGroup: string;
197
198
  let UTable: string;
198
199
  let UDataList: string;
199
200
  let UTab: string;
package/constants.js CHANGED
@@ -306,6 +306,7 @@ export const COMPONENTS = {
306
306
  /* Images and Icons */
307
307
  UIcon: "ui.image-icon",
308
308
  UAvatar: "ui.image-avatar",
309
+ UAvatarGroup: "ui.image-avatar-group",
309
310
 
310
311
  /* Data */
311
312
  UTable: "ui.data-table",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vueless",
3
- "version": "1.3.6-beta.7",
3
+ "version": "1.3.6-beta.9",
4
4
  "description": "Vue Styleless UI Component Library, powered by Tailwind CSS.",
5
5
  "author": "Johnny Grid <hello@vueless.com> (https://vueless.com)",
6
6
  "homepage": "https://vueless.com",
package/types.ts CHANGED
@@ -39,6 +39,7 @@ import UTabConfig from "./ui.navigation-tab/config";
39
39
  import UTabsConfig from "./ui.navigation-tabs/config";
40
40
  import UBreadcrumbsConfig from "./ui.navigation-breadcrumbs/config";
41
41
  import UAvatarConfig from "./ui.image-avatar/config";
42
+ import UAvatarGroupConfig from "./ui.image-avatar-group/config";
42
43
  import UIconConfig from "./ui.image-icon/config";
43
44
  import UCheckboxConfig from "./ui.form-checkbox/config";
44
45
  import UCheckboxGroupConfig from "./ui.form-checkbox-group/config";
@@ -306,6 +307,7 @@ export interface Components {
306
307
  UTabs: Partial<typeof UTabsConfig>;
307
308
  UBreadcrumbs: Partial<typeof UBreadcrumbsConfig>;
308
309
  UAvatar: Partial<typeof UAvatarConfig>;
310
+ UAvatarGroup: Partial<typeof UAvatarGroupConfig>;
309
311
  UIcon: Partial<typeof UIconConfig>;
310
312
  UCheckbox: Partial<typeof UCheckboxConfig>;
311
313
  UCheckboxGroup: Partial<typeof UCheckboxGroupConfig>;
@@ -79,8 +79,8 @@ const MultiEnumTemplate: StoryFn<UButtonArgs> = (args: UButtonArgs, { argTypes }
79
79
  export const Default = DefaultTemplate.bind({});
80
80
  Default.args = { label: "Button" };
81
81
 
82
- export const Variant = EnumTemplate.bind({});
83
- Variant.args = { enum: "variant", label: "{enumValue}" };
82
+ export const Variants = EnumTemplate.bind({});
83
+ Variants.args = { enum: "variant", label: "{enumValue}" };
84
84
 
85
85
  export const Round = EnumTemplate.bind({});
86
86
  Round.args = { enum: "variant", label: "{enumValue}", round: true };
@@ -125,8 +125,8 @@ Description.args = {
125
125
  description: "Customize your cookie settings to enhance your browsing experience.",
126
126
  };
127
127
 
128
- export const Variant = EnumTemplate.bind({});
129
- Variant.args = { enum: "variant", title: "{enumValue}" };
128
+ export const Variants = EnumTemplate.bind({});
129
+ Variants.args = { enum: "variant", title: "{enumValue}" };
130
130
 
131
131
  export const BeforeTitleSlot = DefaultTemplate.bind({});
132
132
  BeforeTitleSlot.args = {
@@ -162,8 +162,8 @@ NoCloseOnCross.args = { closeOnCross: false };
162
162
  export const Position = EnumTemplate.bind({});
163
163
  Position.args = { enum: "position", modelValues: {} };
164
164
 
165
- export const Variant = EnumTemplate.bind({});
166
- Variant.args = { enum: "variant", modelValues: {} };
165
+ export const Variants = EnumTemplate.bind({});
166
+ Variants.args = { enum: "variant", modelValues: {} };
167
167
 
168
168
  export const BeforeTitleSlot = DefaultTemplate.bind({});
169
169
  BeforeTitleSlot.args = {
@@ -280,8 +280,8 @@ WithoutDivider.parameters = {
280
280
  export const Sizes = EnumTemplate.bind({});
281
281
  Sizes.args = { enum: "size", modelValues: {} };
282
282
 
283
- export const Variant = EnumTemplate.bind({});
284
- Variant.args = { enum: "variant", modelValues: {} };
283
+ export const Variants = EnumTemplate.bind({});
284
+ Variants.args = { enum: "variant", modelValues: {} };
285
285
 
286
286
  export const BackLink: StoryFn<UModalArgs> = (args: UModalArgs) => ({
287
287
  components: { UModal, UButton, UCheckbox, UCol, URow, UDivider, UInput, UInputPassword },
@@ -221,8 +221,8 @@ DisableConfirmButton.args = { confirmDisabled: true };
221
221
  export const Sizes = EnumTemplate.bind({});
222
222
  Sizes.args = { enum: "size", modelValues: {} };
223
223
 
224
- export const Variant = EnumTemplate.bind({});
225
- Variant.args = { enum: "variant", modelValues: {} };
224
+ export const Variants = EnumTemplate.bind({});
225
+ Variants.args = { enum: "variant", modelValues: {} };
226
226
 
227
227
  export const Colors: StoryFn<UModalConfirmArgs> = (args: UModalConfirmArgs, { argTypes }) => ({
228
228
  components: { UModalConfirm, UButton, URow },
@@ -212,9 +212,9 @@ Sizes.parameters = {
212
212
  },
213
213
  };
214
214
 
215
- export const Variant = EnumTemplate.bind({});
216
- Variant.args = { enum: "variant", description: "{enumValue}" };
217
- Variant.parameters = {
215
+ export const Variants = EnumTemplate.bind({});
216
+ Variants.args = { enum: "variant", description: "{enumValue}" };
217
+ Variants.parameters = {
218
218
  docs: {
219
219
  description: {
220
220
  story: "Page variant.",
@@ -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 />
@@ -0,0 +1,147 @@
1
+ import {
2
+ getArgs,
3
+ getArgTypes,
4
+ getSlotNames,
5
+ getSlotsFragment,
6
+ getDocsDescription,
7
+ } from "../../utils/storybook";
8
+
9
+ import UAvatarGroup from "../../ui.image-avatar-group/UAvatarGroup.vue";
10
+ import UAvatar from "../../ui.image-avatar/UAvatar.vue";
11
+ import UCol from "../../ui.container-col/UCol.vue";
12
+ import ULink from "../../ui.button-link/ULink.vue";
13
+
14
+ import type { Meta, StoryFn } from "@storybook/vue3-vite";
15
+ import type { Props } from "../types.ts";
16
+
17
+ interface UAvatarGroupArgs extends Props {
18
+ slotTemplate?: string;
19
+ enum: "size" | "variant" | "rounded";
20
+ }
21
+
22
+ export default {
23
+ id: "6030",
24
+ title: "Images & Icons / Avatar Group",
25
+ component: UAvatarGroup,
26
+ args: {
27
+ avatars: [
28
+ { src: "https://i.pravatar.cc/300?img=1" },
29
+ { src: "https://i.pravatar.cc/300?img=2" },
30
+ { src: "https://i.pravatar.cc/300?img=3" },
31
+ { src: "https://i.pravatar.cc/300?img=4" },
32
+ ],
33
+ rounded: "full",
34
+ },
35
+ argTypes: {
36
+ ...getArgTypes(UAvatarGroup.__name),
37
+ },
38
+ parameters: {
39
+ docs: {
40
+ ...getDocsDescription(UAvatarGroup.__name),
41
+ },
42
+ },
43
+ } as Meta;
44
+
45
+ const DefaultTemplate: StoryFn<UAvatarGroupArgs> = (args: UAvatarGroupArgs) => ({
46
+ components: { UAvatarGroup, UAvatar, ULink },
47
+ setup: () => ({
48
+ args,
49
+ slots: getSlotNames(UAvatarGroup.__name),
50
+ }),
51
+ template: `
52
+ <UAvatarGroup v-bind="args">
53
+ ${args.slotTemplate || getSlotsFragment("")}
54
+ </UAvatarGroup>
55
+ `,
56
+ });
57
+
58
+ const EnumTemplate: StoryFn<UAvatarGroupArgs> = (args: UAvatarGroupArgs, { argTypes }) => ({
59
+ components: { UCol, UAvatarGroup, UAvatar },
60
+ setup: () => ({ args, argTypes, getArgs }),
61
+ template: `
62
+ <UCol>
63
+ <UAvatarGroup
64
+ v-for="option in argTypes?.[args.enum]?.options"
65
+ v-bind="getArgs(args, option)"
66
+ :key="option"
67
+ >
68
+ ${args.slotTemplate}
69
+ </UAvatarGroup>
70
+ </UCol>
71
+ `,
72
+ });
73
+
74
+ export const Default = DefaultTemplate.bind({});
75
+ Default.args = {};
76
+
77
+ export const Max = DefaultTemplate.bind({});
78
+ Max.args = { max: 2 };
79
+ Max.parameters = {
80
+ docs: {
81
+ description: {
82
+ story:
83
+ "When the number of avatars is greater than the max, the remaining count avatar is displayed.",
84
+ },
85
+ },
86
+ };
87
+
88
+ export const Sizes = EnumTemplate.bind({});
89
+ Sizes.args = {
90
+ enum: "size",
91
+ slotTemplate: `
92
+ <template #remaining>
93
+ <UAvatar :label="option" />
94
+ </template>
95
+ `,
96
+ };
97
+
98
+ export const Variants = EnumTemplate.bind({});
99
+ Variants.args = {
100
+ enum: "variant",
101
+ avatars: [{ label: "John Doe" }],
102
+ config: { avatar: "ring-0" },
103
+ };
104
+
105
+ export const AvatarConfig = DefaultTemplate.bind({});
106
+ AvatarConfig.args = {
107
+ avatars: [
108
+ { src: "https://i.pravatar.cc/300?img=1", label: "John Doe", chip: { color: "primary" } },
109
+ { color: "warning", placeholderIcon: "person" },
110
+ {
111
+ src: "https://i.pravatar.cc/300?img=9",
112
+ label: "Jane Smith",
113
+ color: "info",
114
+ chip: { color: "grayscale" },
115
+ },
116
+ ],
117
+ };
118
+ AvatarConfig.parameters = {
119
+ docs: {
120
+ description: {
121
+ story:
122
+ // eslint-disable-next-line vue/max-len
123
+ "You can customize the `label`, `color`, `placeholderIcon` and `chip` of a specific avatar by passing the corresponding props to its object.",
124
+ },
125
+ },
126
+ };
127
+
128
+ export const AvatarSlot = DefaultTemplate.bind({});
129
+ AvatarSlot.args = {
130
+ slotTemplate: `
131
+ <template #avatar-2="{ avatar }">
132
+ <UAvatar
133
+ :src="avatar.src"
134
+ class="ring-3 ring-primary"
135
+ />
136
+ </template>
137
+ `,
138
+ };
139
+
140
+ export const RemainingSlot = DefaultTemplate.bind({});
141
+ RemainingSlot.args = {
142
+ slotTemplate: `
143
+ <template #remaining="{ remainingCount }">
144
+ <ULink :label="'+' + remainingCount" size="lg" color="info" underlined />
145
+ </template>
146
+ `,
147
+ };
@@ -0,0 +1,141 @@
1
+ import { mount } from "@vue/test-utils";
2
+ import { describe, it, expect } from "vitest";
3
+
4
+ import UAvatarGroup from "../UAvatarGroup.vue";
5
+ import UAvatar from "../../ui.image-avatar/UAvatar.vue";
6
+
7
+ import type { Props } from "../types.ts";
8
+
9
+ describe("UAvatarGroup.vue", () => {
10
+ describe("Props", () => {
11
+ it("Size – applies the correct size to child avatars", async () => {
12
+ const sizes = ["3xs", "2xs", "xs", "sm", "md", "lg", "xl", "2xl", "3xl"];
13
+
14
+ sizes.forEach((size) => {
15
+ const component = mount(UAvatarGroup, {
16
+ props: {
17
+ size: size as Props["size"],
18
+ avatars: [{ label: "John Doe" }, { label: "Jane Smith" }],
19
+ },
20
+ });
21
+
22
+ // Check if avatars have the correct size (they should inherit from group)
23
+ const avatars = component.findAllComponents(UAvatar);
24
+
25
+ expect(avatars.length).toBeGreaterThan(0);
26
+ });
27
+ });
28
+
29
+ it("Max – limits the number of avatars displayed based on max prop", async () => {
30
+ const component = mount(UAvatarGroup, {
31
+ props: {
32
+ max: 2,
33
+ avatars: [{ label: "John Doe" }, { label: "Jane Smith" }, { label: "Bob Johnson" }],
34
+ },
35
+ });
36
+
37
+ // Should have 2 visible avatars + 1 remaining avatar
38
+ const avatars = component.findAllComponents(UAvatar);
39
+
40
+ expect(avatars.length).toBe(3);
41
+
42
+ // The last avatar should be the remaining count avatar
43
+ const lastAvatar = avatars[avatars.length - 1];
44
+
45
+ expect(lastAvatar.text()).toBe("+1");
46
+ });
47
+
48
+ it("Variant – applies the correct variant to child avatars", async () => {
49
+ const variants = ["solid", "outlined", "subtle", "soft"];
50
+
51
+ variants.forEach((variant) => {
52
+ const component = mount(UAvatarGroup, {
53
+ props: {
54
+ variant: variant as Props["variant"],
55
+ avatars: [{ label: "John Doe" }, { label: "Jane Smith" }],
56
+ },
57
+ });
58
+
59
+ const avatars = component.findAllComponents(UAvatar);
60
+
61
+ expect(avatars.length).toBeGreaterThan(0);
62
+ });
63
+ });
64
+
65
+ it("Rounded – applies the correct rounded to child avatars", async () => {
66
+ const roundedValues = ["none", "sm", "md", "lg", "full"];
67
+
68
+ roundedValues.forEach((rounded) => {
69
+ const component = mount(UAvatarGroup, {
70
+ props: {
71
+ rounded: rounded as Props["rounded"],
72
+ avatars: [{ label: "John Doe" }, { label: "Jane Smith" }],
73
+ },
74
+ });
75
+
76
+ const avatars = component.findAllComponents(UAvatar);
77
+
78
+ expect(avatars.length).toBeGreaterThan(0);
79
+ });
80
+ });
81
+ });
82
+
83
+ describe("Slots", () => {
84
+ it("Avatars – renders avatars from avatars prop", async () => {
85
+ const component = mount(UAvatarGroup, {
86
+ props: {
87
+ avatars: [{ label: "John Doe" }, { label: "Jane Smith" }],
88
+ },
89
+ });
90
+
91
+ const avatars = component.findAllComponents(UAvatar);
92
+
93
+ expect(avatars.length).toBe(2);
94
+ });
95
+
96
+ it("Remaining – renders custom remaining slot", async () => {
97
+ const component = mount(UAvatarGroup, {
98
+ props: {
99
+ max: 1,
100
+ avatars: [{ label: "John Doe" }, { label: "Jane Smith" }],
101
+ },
102
+ slots: {
103
+ remaining: `
104
+ <template #remaining="{ remainingCount }">
105
+ <span class="custom-remaining">
106
+ Custom {{ remainingCount }}
107
+ </span>
108
+ </template>
109
+ `,
110
+ },
111
+ global: {
112
+ components: {
113
+ UAvatar,
114
+ },
115
+ },
116
+ });
117
+
118
+ const avatars = component.findAllComponents(UAvatar);
119
+
120
+ expect(avatars.length).toBe(2);
121
+
122
+ // Check if custom remaining slot content is rendered
123
+ const customRemaining = component.find(".custom-remaining");
124
+
125
+ expect(customRemaining.exists()).toBe(true);
126
+ expect(customRemaining.text()).toContain("Custom");
127
+ });
128
+ });
129
+
130
+ describe("Exposed refs", () => {
131
+ it("exposes avatarGroupRef", () => {
132
+ const component = mount(UAvatarGroup, {
133
+ props: {
134
+ avatars: [{ label: "John Doe" }],
135
+ },
136
+ });
137
+
138
+ expect(component.vm.avatarGroupRef).toBeDefined();
139
+ });
140
+ });
141
+ });
@@ -0,0 +1,51 @@
1
+ import defaultConfig from "./config";
2
+
3
+ import type { ComponentConfig } from "../types";
4
+ import type { ChipItem } from "../ui.image-avatar/types";
5
+
6
+ export type Config = typeof defaultConfig;
7
+
8
+ export interface AvatarItem {
9
+ src?: string;
10
+ label?: string;
11
+ color?: string;
12
+ placeholderIcon?: string;
13
+ chip?: ChipItem;
14
+ }
15
+
16
+ export interface Props {
17
+ /**
18
+ * Avatar items.
19
+ */
20
+ avatars?: AvatarItem[];
21
+
22
+ /**
23
+ * Avatar group size.
24
+ */
25
+ size?: "3xs" | "2xs" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl";
26
+
27
+ /**
28
+ * Maximum number of avatars to display.
29
+ */
30
+ max?: number;
31
+
32
+ /**
33
+ * Avatar variant.
34
+ */
35
+ variant?: "solid" | "outlined" | "subtle" | "soft";
36
+
37
+ /**
38
+ * Avatar corner rounding.
39
+ */
40
+ rounded?: "none" | "sm" | "md" | "lg" | "full";
41
+
42
+ /**
43
+ * Component config object.
44
+ */
45
+ config?: ComponentConfig<Config>;
46
+
47
+ /**
48
+ * Data-test attribute for automated testing.
49
+ */
50
+ dataTest?: string | null;
51
+ }
@@ -85,8 +85,8 @@ Limit.parameters = {
85
85
  },
86
86
  };
87
87
 
88
- export const Variant = EnumTemplate.bind({});
89
- Variant.args = { enum: "variant" };
88
+ export const Variants = EnumTemplate.bind({});
89
+ Variants.args = { enum: "variant" };
90
90
 
91
91
  export const Sizes = EnumTemplate.bind({});
92
92
  Sizes.args = { enum: "size" };
@@ -44,6 +44,7 @@ export default /*tw*/ {
44
44
  true: "leading-none",
45
45
  },
46
46
  wrap: {
47
+ true: "text-wrap wrap-anywhere",
47
48
  false: "text-nowrap",
48
49
  },
49
50
  },
@@ -90,8 +90,8 @@ Sizes.args = { enum: "size" };
90
90
  export const Color = EnumTemplate.bind({});
91
91
  Color.args = { enum: "color" };
92
92
 
93
- export const Variant = EnumTemplate.bind({});
94
- Variant.args = { enum: "variant" };
93
+ export const Variants = EnumTemplate.bind({});
94
+ Variants.args = { enum: "variant" };
95
95
 
96
96
  export const Line: StoryFn<UTextArgs> = (args: UTextArgs) => ({
97
97
  components: { UText, UCol },