@imaginario27/air-ui-ds 1.14.2 → 1.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/components/cards/specific/MetricCard.vue +165 -72
- package/components/icons/ContainedIcon.vue +4 -0
- package/components/layouts/section/SectionTitle.vue +7 -2
- package/components/notifications/NotificationListItem.vue +162 -0
- package/components/notifications/NotificationsPopover.vue +360 -0
- package/components/popovers/Popover.vue +1 -1
- package/models/enums/icons.ts +2 -0
- package/models/enums/metrics.ts +13 -2
- package/models/types/notifications.ts +11 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ All notable changes to this package are documented in this file.
|
|
|
5
5
|
Historical releases were reconstructed from git history (GitHub repository) and npm publish dates.
|
|
6
6
|
Future releases will include detailed entries generated with Changesets.
|
|
7
7
|
|
|
8
|
+
## 1.14.2 - 2026-06-23
|
|
9
|
+
|
|
10
|
+
Release type: patch.
|
|
11
|
+
Commits found in range: 1.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
1. add zIndex prop to CompactHeader and NavMenu ([5bed1f1](https://github.com/imaginario27/air-ui/commit/5bed1f16e9315eed16ed042020416c9a1a528762))
|
|
16
|
+
|
|
17
|
+
- Package: @imaginario27/air-ui-ds.
|
|
18
|
+
|
|
8
19
|
## 1.14.1 - 2026-06-22
|
|
9
20
|
|
|
10
21
|
Release type: patch.
|
|
@@ -1,25 +1,36 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<Card :class="['gap-5!', containerClass]">
|
|
3
|
-
<
|
|
4
|
-
|
|
2
|
+
<Card :class="['gap-5!', 'relative', containerClass]">
|
|
3
|
+
<div
|
|
4
|
+
v-if="backgroundIcon"
|
|
5
|
+
:class="['absolute', 'right-4', 'flex', 'pointer-events-none', 'opacity-20', backgroundIconPositionClass, backgroundIconContainerClass]"
|
|
6
|
+
aria-hidden="true"
|
|
7
|
+
>
|
|
8
|
+
<Icon
|
|
9
|
+
:name="backgroundIcon"
|
|
10
|
+
:iconClass="backgroundIconFinalClass"
|
|
11
|
+
/>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<CardHeader :class="cardHeaderClass">
|
|
15
|
+
<template v-if="icon && iconPosition === MetricCardIconPosition.LEFT">
|
|
5
16
|
<Icon
|
|
6
17
|
v-if="
|
|
7
|
-
styleType ===
|
|
8
|
-
styleType ===
|
|
9
|
-
styleType ===
|
|
10
|
-
styleType ===
|
|
18
|
+
styleType === MetricCardStyle.NEUTRAL_FILLED ||
|
|
19
|
+
styleType === MetricCardStyle.PRIMARY_BRAND_FILLED ||
|
|
20
|
+
styleType === MetricCardStyle.SECONDARY_BRAND_FILLED ||
|
|
21
|
+
styleType === MetricCardStyle.CUSTOM_FILLED
|
|
11
22
|
"
|
|
12
23
|
:name="icon"
|
|
13
24
|
:class="textColorClass"
|
|
25
|
+
:size="iconSize"
|
|
14
26
|
/>
|
|
15
27
|
<ContainedIcon
|
|
16
28
|
v-else
|
|
17
29
|
:icon
|
|
18
|
-
:
|
|
30
|
+
:size="iconContainerSize"
|
|
19
31
|
:color="iconColor"
|
|
20
32
|
:styleType="containedIconStyleType"
|
|
21
33
|
:shape="containedIconShape"
|
|
22
|
-
class="w-6! h-6! min-w-6! min-h-6!"
|
|
23
34
|
/>
|
|
24
35
|
</template>
|
|
25
36
|
|
|
@@ -27,32 +38,54 @@
|
|
|
27
38
|
:title
|
|
28
39
|
:class="textColorClass"
|
|
29
40
|
/>
|
|
41
|
+
|
|
42
|
+
<template v-if="icon && iconPosition === MetricCardIconPosition.RIGHT">
|
|
43
|
+
<Icon
|
|
44
|
+
v-if="
|
|
45
|
+
styleType === MetricCardStyle.NEUTRAL_FILLED ||
|
|
46
|
+
styleType === MetricCardStyle.PRIMARY_BRAND_FILLED ||
|
|
47
|
+
styleType === MetricCardStyle.SECONDARY_BRAND_FILLED ||
|
|
48
|
+
styleType === MetricCardStyle.CUSTOM_FILLED
|
|
49
|
+
"
|
|
50
|
+
:name="icon"
|
|
51
|
+
:class="textColorClass"
|
|
52
|
+
:size="iconSize"
|
|
53
|
+
/>
|
|
54
|
+
<ContainedIcon
|
|
55
|
+
v-else
|
|
56
|
+
:icon
|
|
57
|
+
:size="iconContainerSize"
|
|
58
|
+
:color="iconColor"
|
|
59
|
+
:styleType="containedIconStyleType"
|
|
60
|
+
:shape="containedIconShape"
|
|
61
|
+
/>
|
|
62
|
+
</template>
|
|
30
63
|
</CardHeader>
|
|
31
|
-
<CardBody class="flex
|
|
64
|
+
<CardBody :class="['flex', 'gap-2!', cardBodyClass]">
|
|
32
65
|
<div :class="['flex', 'items-end', 'gap-1', textColorClass]">
|
|
33
|
-
<span class="text-3xl font-semibold">
|
|
66
|
+
<span :class="['text-3xl', 'font-semibold', amountClass]">
|
|
34
67
|
{{ amount }}
|
|
35
68
|
</span>
|
|
36
69
|
|
|
37
70
|
<span
|
|
38
71
|
v-if="unit"
|
|
39
|
-
class="text-lg font-semibold"
|
|
72
|
+
:class="['text-lg', 'font-semibold', unitClass]"
|
|
40
73
|
>
|
|
41
74
|
/{{ unit }}
|
|
42
75
|
</span>
|
|
43
76
|
</div>
|
|
44
77
|
|
|
45
|
-
<div class="flex flex-col gap-0.5">
|
|
78
|
+
<div :class="['flex', 'flex-col', 'gap-0.5', descriptionWrapperClass]">
|
|
46
79
|
<p
|
|
47
80
|
v-if="featuredDescription"
|
|
48
|
-
:class="['text-sm', 'font-semibold', textColorClass]"
|
|
81
|
+
:class="['text-sm', 'font-semibold', textColorClass, featuredDescriptionClass]"
|
|
49
82
|
>
|
|
50
83
|
{{ featuredDescription }}
|
|
51
84
|
</p>
|
|
52
85
|
|
|
53
86
|
<p
|
|
54
87
|
v-if="description"
|
|
55
|
-
:class="['text-sm', styleType ===
|
|
88
|
+
:class="['text-sm', styleType === MetricCardStyle.DEFAULT ? 'text-text-neutral-subtle' : textColorClass, descriptionClass]"
|
|
56
89
|
>
|
|
57
90
|
{{ description }}
|
|
58
91
|
</p>
|
|
@@ -60,14 +93,14 @@
|
|
|
60
93
|
|
|
61
94
|
<div
|
|
62
95
|
v-if="trend"
|
|
63
|
-
:class="['flex', 'items-center', 'gap-1', trendTextColorClass]"
|
|
96
|
+
:class="['flex', 'items-center', 'gap-1', trendTextColorClass, trendWrapperClass]"
|
|
64
97
|
>
|
|
65
98
|
<Icon
|
|
66
99
|
:name="trendIcon"
|
|
67
|
-
:size="
|
|
100
|
+
:size="trendIconSize"
|
|
68
101
|
/>
|
|
69
102
|
|
|
70
|
-
<span class="text-sm mt-0.5">
|
|
103
|
+
<span :class="['text-sm', 'mt-0.5', trendTextClass]">
|
|
71
104
|
{{ trend }}
|
|
72
105
|
</span>
|
|
73
106
|
</div>
|
|
@@ -79,20 +112,43 @@
|
|
|
79
112
|
// Props
|
|
80
113
|
const props = defineProps({
|
|
81
114
|
styleType: {
|
|
82
|
-
type: String as PropType<
|
|
83
|
-
default:
|
|
84
|
-
validator: (value:
|
|
115
|
+
type: String as PropType<MetricCardStyle>,
|
|
116
|
+
default: MetricCardStyle.DEFAULT,
|
|
117
|
+
validator: (value: MetricCardStyle) => Object.values(MetricCardStyle).includes(value),
|
|
85
118
|
},
|
|
86
119
|
title: {
|
|
87
120
|
type: String as PropType<string>,
|
|
88
121
|
default: "Metric title",
|
|
89
122
|
},
|
|
90
123
|
icon: String as PropType<string>,
|
|
124
|
+
backgroundIcon: String as PropType<string>,
|
|
125
|
+
backgroundIconPosition: {
|
|
126
|
+
type: String as PropType<MetricCardBackgroundIconPosition>,
|
|
127
|
+
default: MetricCardBackgroundIconPosition.MIDDLE,
|
|
128
|
+
validator: (value: MetricCardBackgroundIconPosition) => Object.values(MetricCardBackgroundIconPosition).includes(value),
|
|
129
|
+
},
|
|
130
|
+
backgroundIconContainerClass: String as PropType<string>,
|
|
131
|
+
backgroundIconClass: String as PropType<string>,
|
|
91
132
|
iconContainerShape: {
|
|
92
133
|
type: String as PropType<IconContainerShape>,
|
|
93
134
|
default: IconContainerShape.SQUARE,
|
|
94
135
|
validator: (value: IconContainerShape) => Object.values(IconContainerShape).includes(value),
|
|
95
136
|
},
|
|
137
|
+
iconContainerSize: {
|
|
138
|
+
type: String as PropType<IconContainerSize>,
|
|
139
|
+
default: IconContainerSize.SM,
|
|
140
|
+
validator: (value: IconContainerSize) => Object.values(IconContainerSize).includes(value),
|
|
141
|
+
},
|
|
142
|
+
iconSize: {
|
|
143
|
+
type: String as PropType<IconSize>,
|
|
144
|
+
default: IconSize.MD,
|
|
145
|
+
validator: (value: IconSize) => Object.values(IconSize).includes(value),
|
|
146
|
+
},
|
|
147
|
+
iconPosition: {
|
|
148
|
+
type: String as PropType<MetricCardIconPosition>,
|
|
149
|
+
default: MetricCardIconPosition.LEFT,
|
|
150
|
+
validator: (value: MetricCardIconPosition) => Object.values(MetricCardIconPosition).includes(value),
|
|
151
|
+
},
|
|
96
152
|
defaultStyleIconColor: {
|
|
97
153
|
type: String as PropType<ColorAccent>,
|
|
98
154
|
default: ColorAccent.NEUTRAL,
|
|
@@ -112,44 +168,59 @@ const props = defineProps({
|
|
|
112
168
|
description: String as PropType<string>,
|
|
113
169
|
trend: String as PropType<string>,
|
|
114
170
|
trendDirection: {
|
|
115
|
-
type: String as PropType<
|
|
116
|
-
default:
|
|
117
|
-
validator: (value:
|
|
171
|
+
type: String as PropType<MetricTrendDirection>,
|
|
172
|
+
default: MetricTrendDirection.UP,
|
|
173
|
+
validator: (value: MetricTrendDirection) => Object.values(MetricTrendDirection).includes(value),
|
|
174
|
+
},
|
|
175
|
+
trendIconSize: {
|
|
176
|
+
type: String as PropType<IconSize>,
|
|
177
|
+
default: IconSize.SM,
|
|
178
|
+
validator: (value: IconSize) => Object.values(IconSize).includes(value),
|
|
118
179
|
},
|
|
119
180
|
customFilledColorClass: {
|
|
120
181
|
type: String as PropType<string>,
|
|
121
182
|
default: "bg-background-neutral-bold",
|
|
122
183
|
},
|
|
184
|
+
cardHeaderClass: String as PropType<string>,
|
|
185
|
+
cardBodyClass: String as PropType<string>,
|
|
186
|
+
cardTitleClass: String as PropType<string>,
|
|
187
|
+
amountClass: String as PropType<string>,
|
|
188
|
+
unitClass: String as PropType<string>,
|
|
189
|
+
descriptionWrapperClass: String as PropType<string>,
|
|
190
|
+
featuredDescriptionClass: String as PropType<string>,
|
|
191
|
+
descriptionClass: String as PropType<string>,
|
|
192
|
+
trendWrapperClass: String as PropType<string>,
|
|
193
|
+
trendTextClass: String as PropType<string>,
|
|
123
194
|
});
|
|
124
195
|
|
|
125
196
|
// Computed
|
|
126
197
|
const trendIcon = computed(() => {
|
|
127
|
-
const variant: Record<
|
|
128
|
-
[
|
|
129
|
-
[
|
|
130
|
-
[
|
|
198
|
+
const variant: Record<MetricTrendDirection, string> = {
|
|
199
|
+
[MetricTrendDirection.UP]: "mdi:arrow-up",
|
|
200
|
+
[MetricTrendDirection.DOWN]: "mdi:arrow-down",
|
|
201
|
+
[MetricTrendDirection.NEUTRAL]: "mdi:minus",
|
|
131
202
|
};
|
|
132
203
|
|
|
133
|
-
return variant[props.trendDirection as
|
|
204
|
+
return variant[props.trendDirection as MetricTrendDirection] || "mdi:arrow-up";
|
|
134
205
|
});
|
|
135
206
|
|
|
136
207
|
const containerClass = computed(() => {
|
|
137
|
-
const variant: Record<
|
|
138
|
-
[
|
|
139
|
-
[
|
|
140
|
-
[
|
|
141
|
-
[
|
|
142
|
-
[
|
|
143
|
-
[
|
|
144
|
-
[
|
|
145
|
-
[
|
|
208
|
+
const variant: Record<MetricCardStyle, string> = {
|
|
209
|
+
[MetricCardStyle.DEFAULT]: "bg-background-container-surface",
|
|
210
|
+
[MetricCardStyle.PRIMARY_BRAND_FILLED]: "bg-background-primary-brand-default border-border-primary-brand-default dark",
|
|
211
|
+
[MetricCardStyle.PRIMARY_BRAND_SOFT]: "bg-background-primary-brand-soft",
|
|
212
|
+
[MetricCardStyle.SECONDARY_BRAND_FILLED]: "bg-background-secondary-brand-default border-border-secondary-brand dark",
|
|
213
|
+
[MetricCardStyle.SECONDARY_BRAND_SOFT]: "bg-background-secondary-brand-soft",
|
|
214
|
+
[MetricCardStyle.NEUTRAL_FILLED]: "bg-background-neutral-bold dark",
|
|
215
|
+
[MetricCardStyle.NEUTRAL_SOFT]: "bg-background-neutral-subtler",
|
|
216
|
+
[MetricCardStyle.CUSTOM_FILLED]: props.customFilledColorClass,
|
|
146
217
|
};
|
|
147
218
|
|
|
148
|
-
return variant[props.styleType as
|
|
219
|
+
return variant[props.styleType as MetricCardStyle] || "bg-background-container-surface";
|
|
149
220
|
});
|
|
150
221
|
|
|
151
222
|
const containedIconStyleType = computed(() => {
|
|
152
|
-
if (props.styleType ===
|
|
223
|
+
if (props.styleType === MetricCardStyle.DEFAULT) {
|
|
153
224
|
return props.defaultStyleIconContainerType;
|
|
154
225
|
}
|
|
155
226
|
|
|
@@ -157,7 +228,7 @@ const containedIconStyleType = computed(() => {
|
|
|
157
228
|
});
|
|
158
229
|
|
|
159
230
|
const containedIconShape = computed(() => {
|
|
160
|
-
if (props.styleType ===
|
|
231
|
+
if (props.styleType === MetricCardStyle.DEFAULT) {
|
|
161
232
|
return props.iconContainerShape;
|
|
162
233
|
}
|
|
163
234
|
|
|
@@ -165,46 +236,46 @@ const containedIconShape = computed(() => {
|
|
|
165
236
|
});
|
|
166
237
|
|
|
167
238
|
const iconColor = computed(() => {
|
|
168
|
-
if (props.defaultStyleIconColor && props.styleType ===
|
|
239
|
+
if (props.defaultStyleIconColor && props.styleType === MetricCardStyle.DEFAULT) {
|
|
169
240
|
return props.defaultStyleIconColor;
|
|
170
241
|
}
|
|
171
242
|
|
|
172
|
-
const variant: Record<
|
|
173
|
-
[
|
|
174
|
-
[
|
|
175
|
-
[
|
|
176
|
-
[
|
|
177
|
-
[
|
|
178
|
-
[
|
|
179
|
-
[
|
|
180
|
-
[
|
|
243
|
+
const variant: Record<MetricCardStyle, ColorAccent> = {
|
|
244
|
+
[MetricCardStyle.DEFAULT]: ColorAccent.NEUTRAL,
|
|
245
|
+
[MetricCardStyle.PRIMARY_BRAND_FILLED]: ColorAccent.PRIMARY_BRAND,
|
|
246
|
+
[MetricCardStyle.PRIMARY_BRAND_SOFT]: ColorAccent.PRIMARY_BRAND,
|
|
247
|
+
[MetricCardStyle.SECONDARY_BRAND_FILLED]: ColorAccent.SECONDARY_BRAND,
|
|
248
|
+
[MetricCardStyle.SECONDARY_BRAND_SOFT]: ColorAccent.SECONDARY_BRAND,
|
|
249
|
+
[MetricCardStyle.NEUTRAL_FILLED]: ColorAccent.NEUTRAL,
|
|
250
|
+
[MetricCardStyle.NEUTRAL_SOFT]: ColorAccent.NEUTRAL,
|
|
251
|
+
[MetricCardStyle.CUSTOM_FILLED]: ColorAccent.NEUTRAL,
|
|
181
252
|
};
|
|
182
253
|
|
|
183
|
-
return variant[props.styleType as
|
|
254
|
+
return variant[props.styleType as MetricCardStyle] || ColorAccent.NEUTRAL;
|
|
184
255
|
});
|
|
185
256
|
|
|
186
257
|
const textColorClass = computed(() => {
|
|
187
|
-
const variant: Record<
|
|
188
|
-
[
|
|
189
|
-
[
|
|
190
|
-
[
|
|
191
|
-
[
|
|
192
|
-
[
|
|
193
|
-
[
|
|
194
|
-
[
|
|
195
|
-
[
|
|
258
|
+
const variant: Record<MetricCardStyle, string> = {
|
|
259
|
+
[MetricCardStyle.DEFAULT]: "text-text-neutral-default",
|
|
260
|
+
[MetricCardStyle.PRIMARY_BRAND_FILLED]: "text-text-neutral-on-filled",
|
|
261
|
+
[MetricCardStyle.PRIMARY_BRAND_SOFT]: "text-text-primary-brand-on-soft-bg",
|
|
262
|
+
[MetricCardStyle.SECONDARY_BRAND_FILLED]: "text-text-neutral-on-filled",
|
|
263
|
+
[MetricCardStyle.SECONDARY_BRAND_SOFT]: "text-text-secondary-brand-on-soft-bg",
|
|
264
|
+
[MetricCardStyle.NEUTRAL_FILLED]: "text-text-neutral-on-filled",
|
|
265
|
+
[MetricCardStyle.NEUTRAL_SOFT]: "text-text-neutral-on-neutral-bg",
|
|
266
|
+
[MetricCardStyle.CUSTOM_FILLED]: "text-text-neutral-on-filled",
|
|
196
267
|
};
|
|
197
268
|
|
|
198
|
-
return variant[props.styleType as
|
|
269
|
+
return variant[props.styleType as MetricCardStyle] || "text-text-neutral-default";
|
|
199
270
|
});
|
|
200
271
|
|
|
201
272
|
const isFilledStyle = computed(() => {
|
|
202
273
|
return [
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
].includes(props.styleType as
|
|
274
|
+
MetricCardStyle.PRIMARY_BRAND_FILLED,
|
|
275
|
+
MetricCardStyle.SECONDARY_BRAND_FILLED,
|
|
276
|
+
MetricCardStyle.NEUTRAL_FILLED,
|
|
277
|
+
MetricCardStyle.CUSTOM_FILLED,
|
|
278
|
+
].includes(props.styleType as MetricCardStyle);
|
|
208
279
|
});
|
|
209
280
|
|
|
210
281
|
const trendUpClass = computed(() => {
|
|
@@ -219,13 +290,35 @@ const trendNeutralClass = computed(() => {
|
|
|
219
290
|
return isFilledStyle.value ? "text-text-neutral-on-filled" : "text-text-neutral-subtle";
|
|
220
291
|
});
|
|
221
292
|
|
|
293
|
+
const backgroundIconPositionClass = computed(() => {
|
|
294
|
+
const variant: Record<MetricCardBackgroundIconPosition, string> = {
|
|
295
|
+
[MetricCardBackgroundIconPosition.TOP]: 'top-4 items-start',
|
|
296
|
+
[MetricCardBackgroundIconPosition.MIDDLE]: 'inset-y-0 items-center',
|
|
297
|
+
[MetricCardBackgroundIconPosition.BOTTOM]: 'bottom-4 items-end',
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
return variant[props.backgroundIconPosition as MetricCardBackgroundIconPosition] || 'inset-y-0 items-center';
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
const backgroundIconColorClass = computed(() => {
|
|
304
|
+
return props.styleType === MetricCardStyle.DEFAULT
|
|
305
|
+
? 'text-text-neutral-subtle'
|
|
306
|
+
: textColorClass.value;
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const backgroundIconFinalClass = computed(() => {
|
|
310
|
+
const classes: string[] = ['w-[80px]!', 'h-[80px]!', backgroundIconColorClass.value];
|
|
311
|
+
if (props.backgroundIconClass) classes.push(props.backgroundIconClass);
|
|
312
|
+
return classes;
|
|
313
|
+
});
|
|
314
|
+
|
|
222
315
|
const trendTextColorClass = computed(() => {
|
|
223
|
-
const variant: Record<
|
|
224
|
-
[
|
|
225
|
-
[
|
|
226
|
-
[
|
|
316
|
+
const variant: Record<MetricTrendDirection, string> = {
|
|
317
|
+
[MetricTrendDirection.UP]: trendUpClass.value,
|
|
318
|
+
[MetricTrendDirection.DOWN]: trendDownClass.value,
|
|
319
|
+
[MetricTrendDirection.NEUTRAL]: trendNeutralClass.value,
|
|
227
320
|
};
|
|
228
321
|
|
|
229
|
-
return variant[props.trendDirection as
|
|
322
|
+
return variant[props.trendDirection as MetricTrendDirection] || trendUpClass.value;
|
|
230
323
|
});
|
|
231
324
|
</script>
|
|
@@ -116,6 +116,8 @@ const iconColorClass = computed(() => {
|
|
|
116
116
|
|
|
117
117
|
const iconContainerSizeClass = computed(() => {
|
|
118
118
|
const sizeVariants = {
|
|
119
|
+
[IconContainerSize.SM]: 'w-[24px] h-[24px]',
|
|
120
|
+
[IconContainerSize.MD]: 'w-[32px] h-[32px]',
|
|
119
121
|
[IconContainerSize.LG]: 'w-[40px] h-[40px]',
|
|
120
122
|
[IconContainerSize.XL]: 'w-[48px] h-[48px]',
|
|
121
123
|
[IconContainerSize.XXL]: 'w-[56px] h-[56px]',
|
|
@@ -127,6 +129,8 @@ const iconContainerSizeClass = computed(() => {
|
|
|
127
129
|
|
|
128
130
|
const iconSizeClass = computed(() => {
|
|
129
131
|
const sizeVariants = {
|
|
132
|
+
[IconContainerSize.SM]: 'w-[16px] h-[16px] min-w-[16px] min-h-[16px]',
|
|
133
|
+
[IconContainerSize.MD]: 'w-[20px] h-[20px] min-w-[20px] min-h-[20px]',
|
|
130
134
|
[IconContainerSize.LG]: 'w-[24px] h-[24px] min-w-[24px] min-h-[24px]',
|
|
131
135
|
[IconContainerSize.XL]: 'w-[24px] h-[24px] min-w-[24px] min-h-[24px]',
|
|
132
136
|
[IconContainerSize.XXL]: 'w-[40px] h-[40px] min-w-[40px] min-h-[40px]',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
2
|
+
<component :is="headingTag" class="text-xl font-semibold">
|
|
3
3
|
{{ title }}
|
|
4
|
-
</
|
|
4
|
+
</component>
|
|
5
5
|
</template>
|
|
6
6
|
<script setup lang="ts">
|
|
7
7
|
defineProps({
|
|
@@ -9,5 +9,10 @@ defineProps({
|
|
|
9
9
|
type: String as PropType<string>,
|
|
10
10
|
default: 'Title',
|
|
11
11
|
},
|
|
12
|
+
headingTag: {
|
|
13
|
+
type: String as PropType<HeadingTag>,
|
|
14
|
+
default: 'h2',
|
|
15
|
+
validator: (value: string ) => ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(value as string)
|
|
16
|
+
},
|
|
12
17
|
})
|
|
13
18
|
</script>
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
:class="[
|
|
4
|
+
'w-full',
|
|
5
|
+
'flex',
|
|
6
|
+
'gap-2',
|
|
7
|
+
'py-2 px-3',
|
|
8
|
+
'cursor-pointer',
|
|
9
|
+
!modelValue && 'bg-background-neutral-subtlest'
|
|
10
|
+
]"
|
|
11
|
+
|
|
12
|
+
@click="onItemClick"
|
|
13
|
+
>
|
|
14
|
+
<!-- Icon -->
|
|
15
|
+
<ContainedIcon
|
|
16
|
+
v-if="icon && isIconContained"
|
|
17
|
+
:icon="icon"
|
|
18
|
+
:size="containedIconSize"
|
|
19
|
+
:color="iconColor"
|
|
20
|
+
:shape="containedIconShape"
|
|
21
|
+
:styleType="containedStyleType"
|
|
22
|
+
/>
|
|
23
|
+
<Icon
|
|
24
|
+
v-else-if="icon && !isIconContained"
|
|
25
|
+
:name="icon"
|
|
26
|
+
:size="iconSize"
|
|
27
|
+
:color="iconColor"
|
|
28
|
+
/>
|
|
29
|
+
|
|
30
|
+
<!-- Content -->
|
|
31
|
+
<slot name="content" />
|
|
32
|
+
|
|
33
|
+
<component
|
|
34
|
+
:is="to ? NuxtLink : 'div'"
|
|
35
|
+
v-if="!$slots.content"
|
|
36
|
+
:to="to || undefined"
|
|
37
|
+
class="w-full flex flex-col gap-3"
|
|
38
|
+
>
|
|
39
|
+
<div class="w-full flex flex-col gap-1">
|
|
40
|
+
<span class="text-xs font-semibold text-text-neutral-subtle">
|
|
41
|
+
{{ title }}
|
|
42
|
+
</span>
|
|
43
|
+
|
|
44
|
+
<slot name="description" />
|
|
45
|
+
|
|
46
|
+
<p v-if="!$slots.description" class="text-sm text-text-default">
|
|
47
|
+
{{ description }}
|
|
48
|
+
</p>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div class="w-full flex gap-5">
|
|
52
|
+
<div class="flex gap-1">
|
|
53
|
+
<Icon
|
|
54
|
+
:name="timeAgoIcon"
|
|
55
|
+
:size="IconSize.XS"
|
|
56
|
+
/>
|
|
57
|
+
<span class="text-xs text-text-neutral-subtle">
|
|
58
|
+
{{ timeAgo }}
|
|
59
|
+
</span>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="flex gap-1">
|
|
62
|
+
<Icon
|
|
63
|
+
:name="authorIcon"
|
|
64
|
+
:size="IconSize.XS"
|
|
65
|
+
/>
|
|
66
|
+
<span class="text-xs text-text-neutral-subtle">
|
|
67
|
+
{{ author }}
|
|
68
|
+
</span>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</component>
|
|
72
|
+
|
|
73
|
+
<ActionIconButton
|
|
74
|
+
:icon="removeItemIcon"
|
|
75
|
+
:size="ButtonSize.XS"
|
|
76
|
+
:styleType="ButtonStyleType.NEUTRAL_TRANSPARENT"
|
|
77
|
+
:ariaLabel="removeAriaLabel"
|
|
78
|
+
@click.stop="$emit('remove')"
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
81
|
+
</template>
|
|
82
|
+
<script setup lang="ts">
|
|
83
|
+
import { NuxtLink } from '#components'
|
|
84
|
+
|
|
85
|
+
const props = defineProps({
|
|
86
|
+
modelValue: {
|
|
87
|
+
type: Boolean as PropType<boolean>,
|
|
88
|
+
default: false,
|
|
89
|
+
},
|
|
90
|
+
icon: String as PropType<string>,
|
|
91
|
+
iconColor: {
|
|
92
|
+
type: String as PropType<ColorAccent>,
|
|
93
|
+
default: ColorAccent.NEUTRAL,
|
|
94
|
+
validator: (value: ColorAccent) => Object.values(ColorAccent).includes(value),
|
|
95
|
+
},
|
|
96
|
+
iconSize: {
|
|
97
|
+
type: String as PropType<IconSize>,
|
|
98
|
+
default: IconSize.MD,
|
|
99
|
+
validator: (value: IconSize) => Object.values(IconSize).includes(value),
|
|
100
|
+
},
|
|
101
|
+
isIconContained: {
|
|
102
|
+
type: Boolean as PropType<boolean>,
|
|
103
|
+
default: true,
|
|
104
|
+
},
|
|
105
|
+
containedStyleType: {
|
|
106
|
+
type: String as PropType<IconContainerStyleType>,
|
|
107
|
+
default: IconContainerStyleType.FLAT,
|
|
108
|
+
validator: (value: IconContainerStyleType) => Object.values(IconContainerStyleType).includes(value),
|
|
109
|
+
},
|
|
110
|
+
containedIconShape: {
|
|
111
|
+
type: String as PropType<IconContainerShape>,
|
|
112
|
+
default: IconContainerShape.CIRCLE,
|
|
113
|
+
validator: (value: IconContainerShape) => Object.values(IconContainerShape).includes(value),
|
|
114
|
+
},
|
|
115
|
+
containedIconSize: {
|
|
116
|
+
type: String as PropType<IconContainerSize>,
|
|
117
|
+
default: IconContainerSize.XL,
|
|
118
|
+
validator: (value: IconContainerSize) => Object.values(IconContainerSize).includes(value),
|
|
119
|
+
},
|
|
120
|
+
to: String as PropType<string>,
|
|
121
|
+
title: {
|
|
122
|
+
type: String as PropType<string>,
|
|
123
|
+
default: 'Title',
|
|
124
|
+
},
|
|
125
|
+
description: {
|
|
126
|
+
type: String as PropType<string>,
|
|
127
|
+
default: 'Description',
|
|
128
|
+
},
|
|
129
|
+
timeAgo: {
|
|
130
|
+
type: String as PropType<string>,
|
|
131
|
+
default: 'Time ago',
|
|
132
|
+
},
|
|
133
|
+
timeAgoIcon: {
|
|
134
|
+
type: String as PropType<string>,
|
|
135
|
+
default: 'mdi:clock-time-four-outline',
|
|
136
|
+
},
|
|
137
|
+
author: {
|
|
138
|
+
type: String as PropType<string>,
|
|
139
|
+
default: 'Author',
|
|
140
|
+
},
|
|
141
|
+
authorIcon: {
|
|
142
|
+
type: String as PropType<string>,
|
|
143
|
+
default: 'mdi:account-outline',
|
|
144
|
+
},
|
|
145
|
+
removeItemIcon: {
|
|
146
|
+
type: String as PropType<string>,
|
|
147
|
+
default: 'mdi:close',
|
|
148
|
+
},
|
|
149
|
+
removeAriaLabel: {
|
|
150
|
+
type: String as PropType<string>,
|
|
151
|
+
default: 'Remove notification',
|
|
152
|
+
},
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
const emit = defineEmits(['update:modelValue', 'remove'])
|
|
156
|
+
|
|
157
|
+
function onItemClick() {
|
|
158
|
+
if (!props.modelValue) {
|
|
159
|
+
emit('update:modelValue', true)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
</script>
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Popover
|
|
3
|
+
:position
|
|
4
|
+
:align
|
|
5
|
+
:trigger
|
|
6
|
+
:popoverClass="['px-0 py-3', popoverClass]"
|
|
7
|
+
>
|
|
8
|
+
<template #content>
|
|
9
|
+
<div
|
|
10
|
+
v-if="isLoading && !hasError"
|
|
11
|
+
class="w-full flex justify-center py-3"
|
|
12
|
+
>
|
|
13
|
+
<Loading
|
|
14
|
+
:text="loadingText"
|
|
15
|
+
spinnerClass="w-[16px]! h-[16px]! border-2!"
|
|
16
|
+
class="flex-row gap-2! text-sm!"
|
|
17
|
+
/>
|
|
18
|
+
</div>
|
|
19
|
+
|
|
20
|
+
<div
|
|
21
|
+
v-else-if="!isLoading && hasError"
|
|
22
|
+
class="w-full flex justify-center py-3"
|
|
23
|
+
>
|
|
24
|
+
<span class="text-sm text-text-error">
|
|
25
|
+
{{ errorText }}
|
|
26
|
+
</span>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<div
|
|
30
|
+
v-else
|
|
31
|
+
class="w-full flex flex-col gap-2"
|
|
32
|
+
>
|
|
33
|
+
<!-- Header -->
|
|
34
|
+
<div class="w-full flex flex-col gap-2.5 px-3">
|
|
35
|
+
<div class="w-full flex gap-4 items-center justify-between">
|
|
36
|
+
<div class="flex gap-2 items-center min-h-[24px]">
|
|
37
|
+
<span class="text-sm font-semibold">
|
|
38
|
+
{{ title }}
|
|
39
|
+
</span>
|
|
40
|
+
|
|
41
|
+
<Badge
|
|
42
|
+
v-if="unreadCount"
|
|
43
|
+
:text="unreadCount.toString()"
|
|
44
|
+
:color="badgeColor"
|
|
45
|
+
:styleType="badgeStyleType"
|
|
46
|
+
:shape="badgeShape"
|
|
47
|
+
/>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<NavLink
|
|
51
|
+
v-if="internalList.length"
|
|
52
|
+
:text="viewAllText"
|
|
53
|
+
:to="viewAllLink"
|
|
54
|
+
textClass="text-text-default text-sm! hover:text-text-primary-brand-hover font-normal"
|
|
55
|
+
/>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<!-- Filters -->
|
|
59
|
+
<ToggleButtonsGroupField
|
|
60
|
+
v-if="internalList.length"
|
|
61
|
+
id="notifications-filters"
|
|
62
|
+
v-model="selectedFilterValue"
|
|
63
|
+
:buttons="toggleFilterButtons"
|
|
64
|
+
:groupStyle="filterGroupStyle"
|
|
65
|
+
/>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<!-- Notifications List -->
|
|
69
|
+
<div :class="['w-full flex flex-col overflow-y-auto', listMaxHeightClass]">
|
|
70
|
+
<slot name="list" />
|
|
71
|
+
|
|
72
|
+
<template v-if="!$slots.list && filteredList.length">
|
|
73
|
+
<NotificationListItem
|
|
74
|
+
v-for="notification in filteredList"
|
|
75
|
+
:key="notification.id"
|
|
76
|
+
:modelValue="notification.read"
|
|
77
|
+
:icon="notification.icon"
|
|
78
|
+
:iconColor="notification.iconColor"
|
|
79
|
+
:iconSize="listIconSize"
|
|
80
|
+
:isIconContained="isListIconContained"
|
|
81
|
+
:containedIconShape="listContainedIconShape"
|
|
82
|
+
:containedIconSize="listContainedIconSize"
|
|
83
|
+
:containedStyleType="listContainedStyleType"
|
|
84
|
+
:to="notification.link"
|
|
85
|
+
:title="notification.title"
|
|
86
|
+
:description="notification.description"
|
|
87
|
+
:timeAgo="notification.timeAgo"
|
|
88
|
+
:timeAgoIcon="listTimeAgoIcon"
|
|
89
|
+
:author="notification.author"
|
|
90
|
+
:authorIcon="listAuthorIcon"
|
|
91
|
+
:removeItemIcon="listRemoveItemIcon"
|
|
92
|
+
:removeAriaLabel="listRemoveItemAriaLabel"
|
|
93
|
+
@update:model-value="onReadStatusChange(notification.id, $event)"
|
|
94
|
+
@remove="onRemoveNotification(notification.id)"
|
|
95
|
+
/>
|
|
96
|
+
</template>
|
|
97
|
+
|
|
98
|
+
<div v-else class="p-3">
|
|
99
|
+
<p class="text-sm text-text-neutral-subtle text-center">{{ listEmptyText }}</p>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
|
|
103
|
+
<!-- Footer -->
|
|
104
|
+
<div
|
|
105
|
+
v-if="internalList.length"
|
|
106
|
+
class="w-full flex gap-2 px-3"
|
|
107
|
+
>
|
|
108
|
+
<ActionButton
|
|
109
|
+
:icon="buttonAllReadIcon"
|
|
110
|
+
:iconPosition="IconPosition.LEFT"
|
|
111
|
+
:text="buttonAllReadText"
|
|
112
|
+
:size="ButtonSize.XS"
|
|
113
|
+
isFullWidth
|
|
114
|
+
@click="onMarkAllAsRead"
|
|
115
|
+
/>
|
|
116
|
+
<ActionButton
|
|
117
|
+
:icon="buttonClearAllIcon"
|
|
118
|
+
:iconPosition="IconPosition.LEFT"
|
|
119
|
+
:text="buttonClearAllText"
|
|
120
|
+
:size="ButtonSize.XS"
|
|
121
|
+
isFullWidth
|
|
122
|
+
@click="onClearAll"
|
|
123
|
+
/>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
</template>
|
|
127
|
+
<template #activator>
|
|
128
|
+
<slot name="activator" />
|
|
129
|
+
</template>
|
|
130
|
+
</Popover>
|
|
131
|
+
</template>
|
|
132
|
+
<script setup lang="ts">
|
|
133
|
+
// Props
|
|
134
|
+
const props = defineProps({
|
|
135
|
+
list: {
|
|
136
|
+
type: Array as PropType<NotificationItem[]>,
|
|
137
|
+
default: () => [
|
|
138
|
+
{
|
|
139
|
+
id: '1',
|
|
140
|
+
read: false,
|
|
141
|
+
icon: 'mdi:bell-outline',
|
|
142
|
+
iconColor: ColorAccent.NEUTRAL,
|
|
143
|
+
title: 'New Notification',
|
|
144
|
+
description: 'This is a sample notification description.',
|
|
145
|
+
timeAgo: '5 minutes ago',
|
|
146
|
+
author: 'John Doe',
|
|
147
|
+
link: '',
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: '2',
|
|
151
|
+
read: true,
|
|
152
|
+
icon: 'mdi:bell-outline',
|
|
153
|
+
iconColor: ColorAccent.SECONDARY_BRAND,
|
|
154
|
+
title: 'Another Notification',
|
|
155
|
+
description: 'This is another sample notification description.',
|
|
156
|
+
timeAgo: '10 minutes ago',
|
|
157
|
+
author: 'Jane Doe',
|
|
158
|
+
link: '',
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
id: '3',
|
|
162
|
+
read: false,
|
|
163
|
+
icon: 'mdi:bell-outline',
|
|
164
|
+
iconColor: ColorAccent.PRIMARY_BRAND,
|
|
165
|
+
title: 'Third Notification',
|
|
166
|
+
description: 'This is the third sample notification description.',
|
|
167
|
+
timeAgo: '15 minutes ago',
|
|
168
|
+
author: 'Jim Doe',
|
|
169
|
+
link: '',
|
|
170
|
+
}
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
isListIconContained: {
|
|
174
|
+
type: Boolean as PropType<boolean>,
|
|
175
|
+
default: true,
|
|
176
|
+
},
|
|
177
|
+
listIconSize: {
|
|
178
|
+
type: String as PropType<IconSize>,
|
|
179
|
+
default: IconSize.MD,
|
|
180
|
+
validator: (value: IconSize) => Object.values(IconSize).includes(value),
|
|
181
|
+
},
|
|
182
|
+
listContainedIconShape: {
|
|
183
|
+
type: String as PropType<IconContainerShape>,
|
|
184
|
+
default: IconContainerShape.CIRCLE,
|
|
185
|
+
validator: (value: IconContainerShape) => Object.values(IconContainerShape).includes(value),
|
|
186
|
+
},
|
|
187
|
+
listContainedIconSize: {
|
|
188
|
+
type: String as PropType<IconContainerSize>,
|
|
189
|
+
default: IconContainerSize.SM,
|
|
190
|
+
validator: (value: IconContainerSize) => Object.values(IconContainerSize).includes(value),
|
|
191
|
+
},
|
|
192
|
+
listContainedStyleType: {
|
|
193
|
+
type: String as PropType<IconContainerStyleType>,
|
|
194
|
+
default: IconContainerStyleType.FLAT,
|
|
195
|
+
validator: (value: IconContainerStyleType) => Object.values(IconContainerStyleType).includes(value),
|
|
196
|
+
},
|
|
197
|
+
listTimeAgoIcon: {
|
|
198
|
+
type: String as PropType<string>,
|
|
199
|
+
default: 'mdi:clock-time-four-outline',
|
|
200
|
+
},
|
|
201
|
+
listMaxHeightClass: {
|
|
202
|
+
type: String as PropType<string>,
|
|
203
|
+
default: 'max-h-[400px]',
|
|
204
|
+
},
|
|
205
|
+
listAuthorIcon: {
|
|
206
|
+
type: String as PropType<string>,
|
|
207
|
+
default: 'mdi:account-outline',
|
|
208
|
+
},
|
|
209
|
+
listRemoveItemIcon: {
|
|
210
|
+
type: String as PropType<string>,
|
|
211
|
+
default: 'mdi:close',
|
|
212
|
+
},
|
|
213
|
+
listRemoveItemAriaLabel: {
|
|
214
|
+
type: String as PropType<string>,
|
|
215
|
+
default: 'Remove notification',
|
|
216
|
+
},
|
|
217
|
+
listEmptyText: {
|
|
218
|
+
type: String as PropType<string>,
|
|
219
|
+
default: 'No notifications available.',
|
|
220
|
+
},
|
|
221
|
+
isLoading: {
|
|
222
|
+
type: Boolean as PropType<boolean>,
|
|
223
|
+
default: false,
|
|
224
|
+
},
|
|
225
|
+
loadingText: {
|
|
226
|
+
type: String as PropType<string>,
|
|
227
|
+
default: "Loading notifications",
|
|
228
|
+
},
|
|
229
|
+
errorText: {
|
|
230
|
+
type: String as PropType<string>,
|
|
231
|
+
default: "",
|
|
232
|
+
},
|
|
233
|
+
title: {
|
|
234
|
+
type: String as PropType<string>,
|
|
235
|
+
default: 'Notifications',
|
|
236
|
+
},
|
|
237
|
+
position: {
|
|
238
|
+
type: String as PropType<Position>,
|
|
239
|
+
default: Position.BOTTOM,
|
|
240
|
+
validator: (value: Position) => Object.values(Position).includes(value),
|
|
241
|
+
},
|
|
242
|
+
align: {
|
|
243
|
+
type: String as PropType<Align>,
|
|
244
|
+
default: Align.RIGHT,
|
|
245
|
+
validator: (value: Align) => Object.values(Align).includes(value),
|
|
246
|
+
},
|
|
247
|
+
trigger: {
|
|
248
|
+
type: String as PropType<Trigger>,
|
|
249
|
+
default: Trigger.CLICK,
|
|
250
|
+
validator: (value: Trigger) => Object.values(Trigger).includes(value),
|
|
251
|
+
},
|
|
252
|
+
popoverClass: {
|
|
253
|
+
type: String as PropType<string>,
|
|
254
|
+
default: 'min-w-[332px]'
|
|
255
|
+
},
|
|
256
|
+
badgeStyleType: {
|
|
257
|
+
type: String as PropType<BadgeStyle>,
|
|
258
|
+
default: BadgeStyle.FILLED,
|
|
259
|
+
validator: (value: BadgeStyle) => Object.values(BadgeStyle).includes(value),
|
|
260
|
+
},
|
|
261
|
+
badgeShape: {
|
|
262
|
+
type: String as PropType<BadgeShape>,
|
|
263
|
+
default: BadgeShape.PILL,
|
|
264
|
+
validator: (value: BadgeShape) => Object.values(BadgeShape).includes(value),
|
|
265
|
+
},
|
|
266
|
+
badgeColor: {
|
|
267
|
+
type: String as PropType<ColorAccent>,
|
|
268
|
+
default: ColorAccent.SECONDARY_BRAND,
|
|
269
|
+
validator: (value: ColorAccent) => Object.values(ColorAccent).includes(value),
|
|
270
|
+
},
|
|
271
|
+
viewAllText: {
|
|
272
|
+
type: String as PropType<string>,
|
|
273
|
+
default: 'View all',
|
|
274
|
+
},
|
|
275
|
+
viewAllLink: {
|
|
276
|
+
type: String as PropType<string>,
|
|
277
|
+
default: '',
|
|
278
|
+
},
|
|
279
|
+
buttonAllReadIcon: {
|
|
280
|
+
type: String as PropType<string>,
|
|
281
|
+
default: 'mdi:check-all',
|
|
282
|
+
},
|
|
283
|
+
buttonAllReadText: {
|
|
284
|
+
type: String as PropType<string>,
|
|
285
|
+
default: 'Mark all as read',
|
|
286
|
+
},
|
|
287
|
+
buttonClearAllIcon: {
|
|
288
|
+
type: String as PropType<string>,
|
|
289
|
+
default: 'mdi:close-circle-outline',
|
|
290
|
+
},
|
|
291
|
+
buttonClearAllText: {
|
|
292
|
+
type: String as PropType<string>,
|
|
293
|
+
default: 'Clear all',
|
|
294
|
+
},
|
|
295
|
+
filterAllButtonText: {
|
|
296
|
+
type: String as PropType<string>,
|
|
297
|
+
default: 'All',
|
|
298
|
+
},
|
|
299
|
+
filterUnreadButtonText: {
|
|
300
|
+
type: String as PropType<string>,
|
|
301
|
+
default: 'Unread',
|
|
302
|
+
},
|
|
303
|
+
filterGroupStyle: {
|
|
304
|
+
type: String as PropType<ToggleButtonGroupStyle>,
|
|
305
|
+
default: ToggleButtonGroupStyle.GROUPED,
|
|
306
|
+
validator: (value: ToggleButtonGroupStyle) => Object.values(ToggleButtonGroupStyle).includes(value),
|
|
307
|
+
},
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
// Emits
|
|
311
|
+
const emit = defineEmits(['markAllAsRead', 'clearAll', 'remove'])
|
|
312
|
+
|
|
313
|
+
// States
|
|
314
|
+
const selectedFilterValue = ref<string>('all')
|
|
315
|
+
|
|
316
|
+
const toggleFilterButtons = ref<ToggleButton[]>([
|
|
317
|
+
{ text: props.filterAllButtonText, value: 'all', size: ButtonSize.SM },
|
|
318
|
+
{ text: props.filterUnreadButtonText, value: 'unread', size: ButtonSize.SM },
|
|
319
|
+
])
|
|
320
|
+
|
|
321
|
+
const internalList = ref<NotificationItem[]>([...props.list])
|
|
322
|
+
|
|
323
|
+
// Watchers
|
|
324
|
+
watch(() => props.list, (newList) => {
|
|
325
|
+
internalList.value = [...newList]
|
|
326
|
+
}, { deep: true })
|
|
327
|
+
|
|
328
|
+
// Computed
|
|
329
|
+
const hasError = computed(() => props.errorText.trim().length > 0)
|
|
330
|
+
|
|
331
|
+
const unreadCount = computed(() => internalList.value.filter(notification => !notification.read).length)
|
|
332
|
+
|
|
333
|
+
const filteredList = computed(() => {
|
|
334
|
+
if (selectedFilterValue.value === 'unread') {
|
|
335
|
+
return internalList.value.filter(notification => !notification.read)
|
|
336
|
+
}
|
|
337
|
+
return internalList.value
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
// Handlers
|
|
341
|
+
const onRemoveNotification = (id: string) => {
|
|
342
|
+
internalList.value = internalList.value.filter(notification => notification.id !== id)
|
|
343
|
+
emit('remove', { id })
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const onReadStatusChange = (id: string, read: boolean) => {
|
|
347
|
+
const item = internalList.value.find(notification => notification.id === id)
|
|
348
|
+
if (item) item.read = read
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const onMarkAllAsRead = () => {
|
|
352
|
+
internalList.value.forEach(notification => notification.read = true)
|
|
353
|
+
emit('markAllAsRead')
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const onClearAll = () => {
|
|
357
|
+
internalList.value = []
|
|
358
|
+
emit('clearAll')
|
|
359
|
+
}
|
|
360
|
+
</script>
|
package/models/enums/icons.ts
CHANGED
package/models/enums/metrics.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export enum
|
|
1
|
+
export enum MetricCardStyle {
|
|
2
2
|
DEFAULT = "default",
|
|
3
3
|
PRIMARY_BRAND_FILLED = "primary-brand-filled",
|
|
4
4
|
PRIMARY_BRAND_SOFT = "primary-brand-soft",
|
|
@@ -9,8 +9,19 @@ export enum DashboardMetricCardStyle {
|
|
|
9
9
|
CUSTOM_FILLED = "custom-filled",
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export enum
|
|
12
|
+
export enum MetricTrendDirection {
|
|
13
13
|
UP = "up",
|
|
14
14
|
DOWN = "down",
|
|
15
15
|
NEUTRAL = "neutral",
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
export enum MetricCardIconPosition {
|
|
19
|
+
LEFT = "left",
|
|
20
|
+
RIGHT = "right",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export enum MetricCardBackgroundIconPosition {
|
|
24
|
+
TOP = "top",
|
|
25
|
+
MIDDLE = "middle",
|
|
26
|
+
BOTTOM = "bottom",
|
|
27
|
+
}
|