@imaginario27/air-ui-ds 1.14.2 → 1.15.1

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 CHANGED
@@ -5,6 +5,28 @@ 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.15.0 - 2026-06-30
9
+
10
+ Release type: minor.
11
+ Commits found in range: 1.
12
+
13
+ ### Added
14
+
15
+ 1. add NotificationsPopover; enhance MetricCard, ContainedIcon, SectionTitle, and Popover ([352e809](https://github.com/imaginario27/air-ui/commit/352e809a77a63b6ef70d3877ad5d8c4b20a4e65e))
16
+
17
+ - Package: @imaginario27/air-ui-ds.
18
+
19
+ ## 1.14.2 - 2026-06-23
20
+
21
+ Release type: patch.
22
+ Commits found in range: 1.
23
+
24
+ ### Added
25
+
26
+ 1. add zIndex prop to CompactHeader and NavMenu ([5bed1f1](https://github.com/imaginario27/air-ui/commit/5bed1f16e9315eed16ed042020416c9a1a528762))
27
+
28
+ - Package: @imaginario27/air-ui-ds.
29
+
8
30
  ## 1.14.1 - 2026-06-22
9
31
 
10
32
  Release type: patch.
@@ -1,25 +1,36 @@
1
1
  <template>
2
- <Card :class="['gap-5!', containerClass]">
3
- <CardHeader>
4
- <template v-if="icon">
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 === DashboardMetricCardStyle.NEUTRAL_FILLED ||
8
- styleType === DashboardMetricCardStyle.PRIMARY_BRAND_FILLED ||
9
- styleType === DashboardMetricCardStyle.SECONDARY_BRAND_FILLED ||
10
- styleType === DashboardMetricCardStyle.CUSTOM_FILLED
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
- :iconSize="IconSize.SM"
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 flex-col! gap-2!">
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
- /{{ unit }}
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 === DashboardMetricCardStyle.DEFAULT ? 'text-text-neutral-subtle' : textColorClass]"
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="IconSize.SM"
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<DashboardMetricCardStyle>,
83
- default: DashboardMetricCardStyle.DEFAULT,
84
- validator: (value: DashboardMetricCardStyle) => Object.values(DashboardMetricCardStyle).includes(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<DashboardMetricTrendDirection>,
116
- default: DashboardMetricTrendDirection.UP,
117
- validator: (value: DashboardMetricTrendDirection) => Object.values(DashboardMetricTrendDirection).includes(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<DashboardMetricTrendDirection, string> = {
128
- [DashboardMetricTrendDirection.UP]: "mdi:arrow-up",
129
- [DashboardMetricTrendDirection.DOWN]: "mdi:arrow-down",
130
- [DashboardMetricTrendDirection.NEUTRAL]: "mdi:minus",
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 DashboardMetricTrendDirection] || "mdi:arrow-up";
204
+ return variant[props.trendDirection as MetricTrendDirection] || "mdi:arrow-up";
134
205
  });
135
206
 
136
207
  const containerClass = computed(() => {
137
- const variant: Record<DashboardMetricCardStyle, string> = {
138
- [DashboardMetricCardStyle.DEFAULT]: "bg-background-container-surface",
139
- [DashboardMetricCardStyle.PRIMARY_BRAND_FILLED]: "bg-background-primary-brand-default border-border-primary-brand-default dark",
140
- [DashboardMetricCardStyle.PRIMARY_BRAND_SOFT]: "bg-background-primary-brand-soft",
141
- [DashboardMetricCardStyle.SECONDARY_BRAND_FILLED]: "bg-background-secondary-brand-default border-border-secondary-brand dark",
142
- [DashboardMetricCardStyle.SECONDARY_BRAND_SOFT]: "bg-background-secondary-brand-soft",
143
- [DashboardMetricCardStyle.NEUTRAL_FILLED]: "bg-background-neutral-bold dark",
144
- [DashboardMetricCardStyle.NEUTRAL_SOFT]: "bg-background-neutral-subtler",
145
- [DashboardMetricCardStyle.CUSTOM_FILLED]: props.customFilledColorClass,
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 DashboardMetricCardStyle] || "bg-background-container-surface";
219
+ return variant[props.styleType as MetricCardStyle] || "bg-background-container-surface";
149
220
  });
150
221
 
151
222
  const containedIconStyleType = computed(() => {
152
- if (props.styleType === DashboardMetricCardStyle.DEFAULT) {
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 === DashboardMetricCardStyle.DEFAULT) {
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 === DashboardMetricCardStyle.DEFAULT) {
239
+ if (props.defaultStyleIconColor && props.styleType === MetricCardStyle.DEFAULT) {
169
240
  return props.defaultStyleIconColor;
170
241
  }
171
242
 
172
- const variant: Record<DashboardMetricCardStyle, ColorAccent> = {
173
- [DashboardMetricCardStyle.DEFAULT]: ColorAccent.NEUTRAL,
174
- [DashboardMetricCardStyle.PRIMARY_BRAND_FILLED]: ColorAccent.PRIMARY_BRAND,
175
- [DashboardMetricCardStyle.PRIMARY_BRAND_SOFT]: ColorAccent.PRIMARY_BRAND,
176
- [DashboardMetricCardStyle.SECONDARY_BRAND_FILLED]: ColorAccent.SECONDARY_BRAND,
177
- [DashboardMetricCardStyle.SECONDARY_BRAND_SOFT]: ColorAccent.SECONDARY_BRAND,
178
- [DashboardMetricCardStyle.NEUTRAL_FILLED]: ColorAccent.NEUTRAL,
179
- [DashboardMetricCardStyle.NEUTRAL_SOFT]: ColorAccent.NEUTRAL,
180
- [DashboardMetricCardStyle.CUSTOM_FILLED]: ColorAccent.NEUTRAL,
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 DashboardMetricCardStyle] || ColorAccent.NEUTRAL;
254
+ return variant[props.styleType as MetricCardStyle] || ColorAccent.NEUTRAL;
184
255
  });
185
256
 
186
257
  const textColorClass = computed(() => {
187
- const variant: Record<DashboardMetricCardStyle, string> = {
188
- [DashboardMetricCardStyle.DEFAULT]: "text-text-neutral-default",
189
- [DashboardMetricCardStyle.PRIMARY_BRAND_FILLED]: "text-text-neutral-on-filled",
190
- [DashboardMetricCardStyle.PRIMARY_BRAND_SOFT]: "text-text-primary-brand-on-soft-bg",
191
- [DashboardMetricCardStyle.SECONDARY_BRAND_FILLED]: "text-text-neutral-on-filled",
192
- [DashboardMetricCardStyle.SECONDARY_BRAND_SOFT]: "text-text-secondary-brand-on-soft-bg",
193
- [DashboardMetricCardStyle.NEUTRAL_FILLED]: "text-text-neutral-on-filled",
194
- [DashboardMetricCardStyle.NEUTRAL_SOFT]: "text-text-neutral-on-neutral-bg",
195
- [DashboardMetricCardStyle.CUSTOM_FILLED]: "text-text-neutral-on-filled",
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 DashboardMetricCardStyle] || "text-text-neutral-default";
269
+ return variant[props.styleType as MetricCardStyle] || "text-text-neutral-default";
199
270
  });
200
271
 
201
272
  const isFilledStyle = computed(() => {
202
273
  return [
203
- DashboardMetricCardStyle.PRIMARY_BRAND_FILLED,
204
- DashboardMetricCardStyle.SECONDARY_BRAND_FILLED,
205
- DashboardMetricCardStyle.NEUTRAL_FILLED,
206
- DashboardMetricCardStyle.CUSTOM_FILLED,
207
- ].includes(props.styleType as DashboardMetricCardStyle);
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<DashboardMetricTrendDirection, string> = {
224
- [DashboardMetricTrendDirection.UP]: trendUpClass.value,
225
- [DashboardMetricTrendDirection.DOWN]: trendDownClass.value,
226
- [DashboardMetricTrendDirection.NEUTRAL]: trendNeutralClass.value,
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 DashboardMetricTrendDirection] || trendUpClass.value;
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
- <h2 class="text-xl font-semibold">
2
+ <component :is="headingTag" class="text-xl font-semibold">
3
3
  {{ title }}
4
- </h2>
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,331 @@
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
+ limit: {
140
+ type: Number as PropType<number>,
141
+ default: 10,
142
+ },
143
+ isListIconContained: {
144
+ type: Boolean as PropType<boolean>,
145
+ default: true,
146
+ },
147
+ listIconSize: {
148
+ type: String as PropType<IconSize>,
149
+ default: IconSize.MD,
150
+ validator: (value: IconSize) => Object.values(IconSize).includes(value),
151
+ },
152
+ listContainedIconShape: {
153
+ type: String as PropType<IconContainerShape>,
154
+ default: IconContainerShape.CIRCLE,
155
+ validator: (value: IconContainerShape) => Object.values(IconContainerShape).includes(value),
156
+ },
157
+ listContainedIconSize: {
158
+ type: String as PropType<IconContainerSize>,
159
+ default: IconContainerSize.SM,
160
+ validator: (value: IconContainerSize) => Object.values(IconContainerSize).includes(value),
161
+ },
162
+ listContainedStyleType: {
163
+ type: String as PropType<IconContainerStyleType>,
164
+ default: IconContainerStyleType.FLAT,
165
+ validator: (value: IconContainerStyleType) => Object.values(IconContainerStyleType).includes(value),
166
+ },
167
+ listTimeAgoIcon: {
168
+ type: String as PropType<string>,
169
+ default: 'mdi:clock-time-four-outline',
170
+ },
171
+ listMaxHeightClass: {
172
+ type: String as PropType<string>,
173
+ default: 'max-h-[400px]',
174
+ },
175
+ listAuthorIcon: {
176
+ type: String as PropType<string>,
177
+ default: 'mdi:account-outline',
178
+ },
179
+ listRemoveItemIcon: {
180
+ type: String as PropType<string>,
181
+ default: 'mdi:close',
182
+ },
183
+ listRemoveItemAriaLabel: {
184
+ type: String as PropType<string>,
185
+ default: 'Remove notification',
186
+ },
187
+ listEmptyText: {
188
+ type: String as PropType<string>,
189
+ default: 'No notifications available.',
190
+ },
191
+ isLoading: {
192
+ type: Boolean as PropType<boolean>,
193
+ default: false,
194
+ },
195
+ loadingText: {
196
+ type: String as PropType<string>,
197
+ default: "Loading notifications",
198
+ },
199
+ errorText: {
200
+ type: String as PropType<string>,
201
+ default: "",
202
+ },
203
+ title: {
204
+ type: String as PropType<string>,
205
+ default: 'Notifications',
206
+ },
207
+ position: {
208
+ type: String as PropType<Position>,
209
+ default: Position.BOTTOM,
210
+ validator: (value: Position) => Object.values(Position).includes(value),
211
+ },
212
+ align: {
213
+ type: String as PropType<Align>,
214
+ default: Align.RIGHT,
215
+ validator: (value: Align) => Object.values(Align).includes(value),
216
+ },
217
+ trigger: {
218
+ type: String as PropType<Trigger>,
219
+ default: Trigger.CLICK,
220
+ validator: (value: Trigger) => Object.values(Trigger).includes(value),
221
+ },
222
+ popoverClass: {
223
+ type: String as PropType<string>,
224
+ default: 'min-w-[332px]'
225
+ },
226
+ badgeStyleType: {
227
+ type: String as PropType<BadgeStyle>,
228
+ default: BadgeStyle.FILLED,
229
+ validator: (value: BadgeStyle) => Object.values(BadgeStyle).includes(value),
230
+ },
231
+ badgeShape: {
232
+ type: String as PropType<BadgeShape>,
233
+ default: BadgeShape.PILL,
234
+ validator: (value: BadgeShape) => Object.values(BadgeShape).includes(value),
235
+ },
236
+ badgeColor: {
237
+ type: String as PropType<ColorAccent>,
238
+ default: ColorAccent.SECONDARY_BRAND,
239
+ validator: (value: ColorAccent) => Object.values(ColorAccent).includes(value),
240
+ },
241
+ viewAllText: {
242
+ type: String as PropType<string>,
243
+ default: 'View all',
244
+ },
245
+ viewAllLink: {
246
+ type: String as PropType<string>,
247
+ default: '',
248
+ },
249
+ buttonAllReadIcon: {
250
+ type: String as PropType<string>,
251
+ default: 'mdi:check-all',
252
+ },
253
+ buttonAllReadText: {
254
+ type: String as PropType<string>,
255
+ default: 'Mark all as read',
256
+ },
257
+ buttonClearAllIcon: {
258
+ type: String as PropType<string>,
259
+ default: 'mdi:close-circle-outline',
260
+ },
261
+ buttonClearAllText: {
262
+ type: String as PropType<string>,
263
+ default: 'Clear all',
264
+ },
265
+ filterAllButtonText: {
266
+ type: String as PropType<string>,
267
+ default: 'All',
268
+ },
269
+ filterUnreadButtonText: {
270
+ type: String as PropType<string>,
271
+ default: 'Unread',
272
+ },
273
+ filterGroupStyle: {
274
+ type: String as PropType<ToggleButtonGroupStyle>,
275
+ default: ToggleButtonGroupStyle.GROUPED,
276
+ validator: (value: ToggleButtonGroupStyle) => Object.values(ToggleButtonGroupStyle).includes(value),
277
+ },
278
+ })
279
+
280
+ // Emits
281
+ const emit = defineEmits(['markAllAsRead', 'clearAll', 'remove'])
282
+
283
+ // States
284
+ const selectedFilterValue = ref<string>('all')
285
+
286
+ const toggleFilterButtons = ref<ToggleButton[]>([
287
+ { text: props.filterAllButtonText, value: 'all', size: ButtonSize.SM },
288
+ { text: props.filterUnreadButtonText, value: 'unread', size: ButtonSize.SM },
289
+ ])
290
+
291
+ const internalList = ref<NotificationItem[]>([...props.list])
292
+
293
+ // Watchers
294
+ watch(() => props.list, (newList) => {
295
+ internalList.value = [...newList]
296
+ }, { deep: true })
297
+
298
+ // Computed
299
+ const hasError = computed(() => props.errorText.trim().length > 0)
300
+
301
+ const unreadCount = computed(() => internalList.value.filter(notification => !notification.read).length)
302
+
303
+ const filteredList = computed(() => {
304
+ const source = selectedFilterValue.value === 'unread'
305
+ ? internalList.value.filter(notification => !notification.read)
306
+ : internalList.value
307
+
308
+ return source.slice(0, props.limit)
309
+ })
310
+
311
+ // Handlers
312
+ const onRemoveNotification = (id: string) => {
313
+ internalList.value = internalList.value.filter(notification => notification.id !== id)
314
+ emit('remove', { id })
315
+ }
316
+
317
+ const onReadStatusChange = (id: string, read: boolean) => {
318
+ const item = internalList.value.find(notification => notification.id === id)
319
+ if (item) item.read = read
320
+ }
321
+
322
+ const onMarkAllAsRead = () => {
323
+ internalList.value.forEach(notification => notification.read = true)
324
+ emit('markAllAsRead')
325
+ }
326
+
327
+ const onClearAll = () => {
328
+ internalList.value = []
329
+ emit('clearAll')
330
+ }
331
+ </script>
@@ -65,7 +65,7 @@ const props = defineProps({
65
65
  validator: (value: Trigger) => Object.values(Trigger).includes(value),
66
66
  },
67
67
  popoverClass: {
68
- type: String as PropType<string>,
68
+ type: [String, Array] as PropType<string | string[]>,
69
69
  default: 'min-w-[300px]'
70
70
  }
71
71
  })
@@ -15,6 +15,8 @@ export enum IconContainerStyleType {
15
15
  }
16
16
 
17
17
  export enum IconContainerSize {
18
+ SM = 'sm',
19
+ MD = 'md',
18
20
  LG = 'lg',
19
21
  XL = 'xl',
20
22
  XXL = '2xl',
@@ -1,4 +1,4 @@
1
- export enum DashboardMetricCardStyle {
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 DashboardMetricTrendDirection {
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
+ }
@@ -0,0 +1,11 @@
1
+ export interface NotificationItem {
2
+ id: string;
3
+ read: boolean;
4
+ title: string;
5
+ description: string;
6
+ timeAgo: string;
7
+ author: string;
8
+ link: string;
9
+ icon?: string;
10
+ iconColor?: ColorAccent;
11
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imaginario27/air-ui-ds",
3
- "version": "1.14.2",
3
+ "version": "1.15.1",
4
4
  "author": "imaginario27",
5
5
  "type": "module",
6
6
  "homepage": "https://air-ui.netlify.app/",