vueless 1.3.6-beta.7 → 1.3.6-beta.8
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 +1 -0
- package/components.ts +1 -0
- package/constants.d.ts +1 -0
- package/constants.js +1 -0
- package/package.json +1 -1
- package/types.ts +2 -0
- package/ui.button/storybook/stories.ts +2 -2
- package/ui.container-card/storybook/stories.ts +2 -2
- package/ui.container-drawer/storybook/stories.ts +2 -2
- package/ui.container-modal/storybook/stories.ts +2 -2
- package/ui.container-modal-confirm/storybook/stories.ts +2 -2
- package/ui.container-page/storybook/stories.ts +3 -3
- package/ui.image-avatar/UAvatar.vue +55 -28
- package/ui.image-avatar/config.ts +18 -1
- package/ui.image-avatar/storybook/docs.mdx +16 -1
- package/ui.image-avatar/storybook/stories.ts +17 -3
- package/ui.image-avatar/tests/UAvatar.test.ts +35 -7
- package/ui.image-avatar/types.ts +13 -0
- package/ui.image-avatar-group/UAvatarGroup.vue +87 -0
- package/ui.image-avatar-group/config.ts +11 -0
- package/ui.image-avatar-group/constants.ts +5 -0
- package/ui.image-avatar-group/storybook/docs.mdx +16 -0
- package/ui.image-avatar-group/storybook/stories.ts +147 -0
- package/ui.image-avatar-group/tests/UAvatarGroup.test.ts +141 -0
- package/ui.image-avatar-group/types.ts +51 -0
- package/ui.navigation-pagination/storybook/stories.ts +2 -2
- package/ui.text-block/storybook/stories.ts +2 -2
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
package/constants.js
CHANGED
package/package.json
CHANGED
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
|
|
83
|
-
|
|
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
|
|
129
|
-
|
|
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
|
|
166
|
-
|
|
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
|
|
284
|
-
|
|
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
|
|
225
|
-
|
|
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
|
|
216
|
-
|
|
217
|
-
|
|
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,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 } =
|
|
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
|
-
<
|
|
78
|
-
|
|
79
|
-
:
|
|
80
|
-
:
|
|
81
|
-
|
|
82
|
-
:
|
|
83
|
-
|
|
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
|
-
<
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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"> </span>
|
|
100
127
|
</template>
|
|
101
|
-
</
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
package/ui.image-avatar/types.ts
CHANGED
|
@@ -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,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
|
|
89
|
-
|
|
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" };
|
|
@@ -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
|
|
94
|
-
|
|
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 },
|