@imaginario27/air-ui-ds 1.15.3 → 1.16.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 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.15.3 - 2026-07-03
9
+
10
+ Release type: patch.
11
+ Commits found in range: 1.
12
+
13
+ ### Fixed
14
+
15
+ 1. add accessible aria-labels to pagination buttons ([58b1770](https://github.com/imaginario27/air-ui/commit/58b1770522a70786df5a02e7ad32ed8646b363a9))
16
+
17
+ - Package: @imaginario27/air-ui-ds.
18
+
8
19
  ## 1.15.2 - 2026-07-02
9
20
 
10
21
  Release type: patch.
@@ -13,7 +13,7 @@
13
13
  ],
14
14
  isInteractive && [
15
15
  hoverBorderSizeClass,
16
- 'border-border-primary-brand-hover cursor-pointer'
16
+ 'border-border-primary-brand-hover cursor-pointer transition-[border-width] duration-200 ease-out'
17
17
  ],
18
18
  ]"
19
19
  >
@@ -7,6 +7,7 @@
7
7
  'flex items-center justify-center',
8
8
  isRounded ? 'rounded-full' : 'rounded-button',
9
9
  'text-nowrap',
10
+ 'transition-colors duration-200 ease-out',
10
11
  ...buttonStyleClass,
11
12
  buttonSizeClass,
12
13
  horizontalPaddingClass,
@@ -26,12 +27,7 @@
26
27
  >
27
28
  <!-- Loading State -->
28
29
  <template v-if="isLoading">
29
- <div class="animate-spin">
30
- <Icon
31
- name="mdi:loading"
32
- :iconClass="[iconSizeClass, iconColorClass]"
33
- />
34
- </div>
30
+ <Spinner :class="[iconSizeClass, spinnerColorClass]" />
35
31
 
36
32
  <span :class="['font-semibold', 'leading-none', 'select-none', textSizeClass, textClass]">
37
33
  {{ loadingText }}
@@ -330,6 +326,24 @@ const iconColorClass = computed(() => {
330
326
  return variant[props.styleType as ButtonStyleType] || '!text-text-default'
331
327
  })
332
328
 
329
+ const spinnerColorClass = computed(() => {
330
+ const variant = {
331
+ [ButtonStyleType.PRIMARY_BRAND_FILLED]: 'border-x-border-neutral-on-filled! border-b-border-neutral-on-filled!',
332
+ [ButtonStyleType.PRIMARY_BRAND_SOFT]: 'border-x-border-primary-brand-default! border-b-border-primary-brand-default!',
333
+ [ButtonStyleType.PRIMARY_BRAND_TRANSPARENT]: 'border-x-border-primary-brand-default! border-b-border-primary-brand-default!',
334
+ [ButtonStyleType.SECONDARY_BRAND_FILLED]: 'border-x-border-neutral-on-filled! border-b-border-neutral-on-filled!',
335
+ [ButtonStyleType.NEUTRAL_FILLED]: 'border-x-border-neutral-on-filled! border-b-border-neutral-on-filled!',
336
+ [ButtonStyleType.NEUTRAL_OUTLINED]: 'border-x-border-primary-brand-default! border-b-border-primary-brand-default!',
337
+ [ButtonStyleType.NEUTRAL_TRANSPARENT]: 'border-x-border-primary-brand-default! border-b-border-primary-brand-default!',
338
+ [ButtonStyleType.NEUTRAL_TRANSPARENT_SUBTLE]: 'border-x-border-primary-brand-default! border-b-border-primary-brand-default!',
339
+ [ButtonStyleType.DELETE_FILLED]: 'border-x-border-neutral-on-filled! border-b-border-neutral-on-filled!',
340
+ [ButtonStyleType.DELETE_SOFT]: 'border-x-border-delete-default! border-b-border-delete-default!',
341
+ [ButtonStyleType.DELETE_OUTLINED]: 'border-x-border-delete-default! border-b-border-delete-default!',
342
+ [ButtonStyleType.DELETE_TRANSPARENT]: 'border-x-border-delete-default! border-b-border-delete-default!',
343
+ }
344
+ return variant[props.styleType as ButtonStyleType] || 'border-border-primary-brand-default'
345
+ })
346
+
333
347
  const horizontalPaddingClass = computed(() => {
334
348
  let variant
335
349
 
@@ -7,6 +7,7 @@
7
7
  'flex items-center justify-center rounded-button',
8
8
  isRounded ? 'rounded-full' : 'rounded-button',
9
9
  'aspect-square',
10
+ 'transition-colors duration-200 ease-out',
10
11
  ...buttonStyleClass,
11
12
  buttonSizeClass,
12
13
  disabled && 'opacity-disabled cursor-not-allowed pointer-events-none',
@@ -11,6 +11,7 @@
11
11
  'text-sm',
12
12
  'text-text-default',
13
13
  'font-semibold',
14
+ 'transition-colors duration-200 ease-out',
14
15
  'hover:bg-background-neutral-hover',
15
16
  props.disabled ? 'opacity-50 hover:cursor-not-allowed hover:bg-transparent' : 'hover:cursor-pointer'
16
17
  ]"
@@ -7,6 +7,7 @@
7
7
  :class="[
8
8
  'flex items-center justify-center',
9
9
  'text-nowrap',
10
+ 'transition-colors duration-200 ease-out',
10
11
  ...buttonStyleClass,
11
12
  buttonSizeClass,
12
13
  horizontalPaddingClass,
@@ -9,7 +9,7 @@
9
9
  :class="[
10
10
  'lg:p-5',
11
11
  selectMode === CardSelectionMode.CARD && isHoverable &&
12
- 'hover:border-border-neutral-hover cursor-pointer transition-shadow duration-300',
12
+ 'hover:border-border-neutral-hover cursor-pointer transition-[box-shadow,border-color] duration-300 ease-out',
13
13
  selectMode === CardSelectionMode.CARD && 'outline-none focus-visible:ring-2 focus-visible:ring-border-primary-brand-default',
14
14
  modelValue && '!border-border-primary-brand-active',
15
15
  disabled && 'opacity-disabled cursor-not-allowed',
@@ -0,0 +1,135 @@
1
+ <template>
2
+ <div>
3
+ <!-- Visually hidden native checkbox (remains accessible to screen readers) -->
4
+ <input
5
+ :id="id"
6
+ type="checkbox"
7
+ :checked="modelValue"
8
+ class="sr-only"
9
+ :aria-label="ariaLabel || 'Toggle'"
10
+ :disabled="disabled"
11
+ @change="handleNativeChange"
12
+ @keydown.space.prevent="toggleSwitch"
13
+ >
14
+
15
+ <!-- Custom Switch -->
16
+ <div
17
+ role="switch"
18
+ :aria-checked="modelValue"
19
+ :aria-label="ariaLabel || 'Toggle'"
20
+ :class="[
21
+ 'relative flex items-center',
22
+ controlFieldSizeClass,
23
+ 'rounded-full transition-colors',
24
+ 'border border-border-default',
25
+ modelValue ? checkedBackgroundClass : 'bg-background-neutral-subtle',
26
+ disabled ? 'bg-background-neutral-disabled cursor-not-allowed opacity-disabled' : 'cursor-pointer'
27
+ ]"
28
+ @click="toggleSwitch"
29
+ >
30
+ <div
31
+ :class="[
32
+ 'absolute bg-icon-on-filled rounded-full shadow-md transform transition-transform',
33
+ 'aspect-square',
34
+ controlFieldHandlerSizeClass,
35
+ modelValue ? controlFieldHandlerCheckedOffsetClass : controlFieldHandlerUncheckedOffsetClass,
36
+ 'bg-icon-neutral-on-filled-bg'
37
+ ]"
38
+ />
39
+ </div>
40
+ </div>
41
+ </template>
42
+
43
+ <script setup lang="ts">
44
+ // Props
45
+ const props = defineProps({
46
+ id: {
47
+ type: String as PropType<string>,
48
+ required: true,
49
+ },
50
+ ariaLabel: String as PropType<string>,
51
+ modelValue: {
52
+ type: Boolean as PropType<boolean>,
53
+ default: false,
54
+ },
55
+ disabled: {
56
+ type: Boolean as PropType<boolean>,
57
+ default: false,
58
+ },
59
+ size: {
60
+ type: String as PropType<ControlFieldSize>,
61
+ default: ControlFieldSize.MD,
62
+ validator: (value: ControlFieldSize) => Object.values(ControlFieldSize).includes(value),
63
+ },
64
+ styleType: {
65
+ type: String as PropType<SwitchStyle>,
66
+ default: SwitchStyle.BRAND,
67
+ validator: (value: SwitchStyle) => Object.values(SwitchStyle).includes(value),
68
+ },
69
+ })
70
+
71
+ // Emits
72
+ const emit = defineEmits(['update:modelValue'])
73
+
74
+ // Computed classes
75
+ const controlFieldSizeClass = computed(() => {
76
+ const sizeVariant = {
77
+ [ControlFieldSize.XS]: 'w-[32px] h-[16px] min-w-[32px] min-h-[16px]',
78
+ [ControlFieldSize.SM]: 'w-[40px] h-[20px] min-w-[40px] min-h-[20px]',
79
+ [ControlFieldSize.MD]: 'w-[44px] h-[24px] min-w-[44px] min-h-[24px]',
80
+ [ControlFieldSize.LG]: 'w-[56px] h-[32px] min-w-[56px] min-h-[32px]',
81
+ }
82
+ return sizeVariant[props.size as ControlFieldSize] || 'w-[44px] h-[24px] min-w-[44px] min-h-[24px]'
83
+ })
84
+
85
+ const controlFieldHandlerSizeClass = computed(() => {
86
+ const sizeVariant = {
87
+ [ControlFieldSize.XS]: 'w-[12px] h-[12px]',
88
+ [ControlFieldSize.SM]: 'w-[16px] h-[16px]',
89
+ [ControlFieldSize.MD]: 'w-[16px] h-[16px]',
90
+ [ControlFieldSize.LG]: 'w-[24px] h-[24px]',
91
+ }
92
+ return sizeVariant[props.size as ControlFieldSize] || 'w-[16px] h-[16px]'
93
+ })
94
+
95
+ const controlFieldHandlerUncheckedOffsetClass = computed(() => {
96
+ const sizeVariant = {
97
+ [ControlFieldSize.XS]: 'translate-x-0.5',
98
+ [ControlFieldSize.SM]: 'translate-x-0.5',
99
+ [ControlFieldSize.MD]: 'translate-x-1',
100
+ [ControlFieldSize.LG]: 'translate-x-1',
101
+ }
102
+ return sizeVariant[props.size as ControlFieldSize] || 'translate-x-1'
103
+ })
104
+
105
+ const controlFieldHandlerCheckedOffsetClass = computed(() => {
106
+ const sizeVariant = {
107
+ [ControlFieldSize.XS]: 'translate-x-[18px]',
108
+ [ControlFieldSize.SM]: 'translate-x-[22px]',
109
+ [ControlFieldSize.MD]: 'translate-x-6',
110
+ [ControlFieldSize.LG]: 'translate-x-7',
111
+ }
112
+ return sizeVariant[props.size as ControlFieldSize] || 'translate-x-6'
113
+ })
114
+
115
+ const checkedBackgroundClass = computed(() => {
116
+ const backgroundVariant = {
117
+ [SwitchStyle.BRAND]: 'bg-background-primary-brand-checked',
118
+ [SwitchStyle.SUCCESS]: 'bg-background-success-bold',
119
+ }
120
+ return backgroundVariant[props.styleType as SwitchStyle] || 'bg-background-primary-brand-checked'
121
+ })
122
+
123
+ // Handlers
124
+ const toggleSwitch = () => {
125
+ if (props.disabled) return
126
+
127
+ emit('update:modelValue', !props.modelValue)
128
+ }
129
+
130
+ const handleNativeChange = () => {
131
+ if (props.disabled) return
132
+
133
+ emit('update:modelValue', !props.modelValue)
134
+ }
135
+ </script>
@@ -54,43 +54,15 @@
54
54
  />
55
55
  </div>
56
56
 
57
- <!-- Visually hidden native checkbox (remains accessible to screen readers) -->
58
- <input
59
- :id="id"
60
- type="checkbox"
61
- :checked="modelValue"
62
- class="sr-only"
63
- :aria-label="ariaLabel || label || legend || 'Toggle'"
64
- :disabled="disabled"
65
- @change="handleChange"
66
- @keydown.space.prevent="toggleCheckbox"
67
- >
68
-
69
- <!-- Custom Switch -->
70
- <div
71
- role="switch"
72
- :aria-checked="modelValue"
73
- :aria-label="ariaLabel || label || legend || 'Toggle'"
74
- :class="[
75
- 'relative flex items-center',
76
- controlFieldSizeClass,
77
- 'rounded-full transition-colors',
78
- 'border border-border-default',
79
- modelValue ? checkedBackgroundClass : 'bg-background-neutral-subtle',
80
- disabled ? 'bg-background-neutral-disabled cursor-not-allowed opacity-disabled' : 'cursor-pointer'
81
- ]"
82
- @click="toggleCheckbox"
83
- >
84
- <div
85
- :class="[
86
- 'absolute bg-icon-on-filled rounded-full shadow-md transform transition-transform',
87
- 'aspect-square',
88
- controlFieldHandlerSizeClass,
89
- modelValue ? 'translate-x-6' : 'translate-x-1',
90
- 'bg-icon-neutral-on-filled-bg'
91
- ]"
92
- />
93
- </div>
57
+ <Switch
58
+ :id
59
+ :ariaLabel="ariaLabel || label || legend || 'Toggle'"
60
+ :modelValue
61
+ :disabled
62
+ :size
63
+ :styleType
64
+ @update:modelValue="handleSwitchUpdate"
65
+ />
94
66
  </div>
95
67
 
96
68
  <!-- Help Text -->
@@ -158,26 +130,6 @@ const validationMode = useInjectedValidationMode()
158
130
  const hasError = computed(() => props.error !== '')
159
131
 
160
132
  // Computed classes
161
- const controlFieldSizeClass = computed(() => {
162
- const sizeVariant = {
163
- [ControlFieldSize.XS]: 'w-[32px] h-[16px] min-w-[32px] min-h-[16px]',
164
- [ControlFieldSize.SM]: 'w-[40px] h-[20px] min-w-[40px] min-h-[20px]',
165
- [ControlFieldSize.MD]: 'w-[44px] h-[24px] min-w-[44px] min-h-[24px]',
166
- [ControlFieldSize.LG]: 'w-[56px] h-[32px] min-w-[56px] min-h-[32px]',
167
- }
168
- return sizeVariant[props.size as ControlFieldSize] || 'w-[44px] h-[24px] min-w-[44px] min-h-[24px]'
169
- })
170
-
171
- const controlFieldHandlerSizeClass = computed(() => {
172
- const sizeVariant = {
173
- [ControlFieldSize.XS]: 'w-[12px] h-[12px]',
174
- [ControlFieldSize.SM]: 'w-[16px] h-[16px]',
175
- [ControlFieldSize.MD]: 'w-[16px] h-[16px]',
176
- [ControlFieldSize.LG]: 'w-[24px] h-[24px]',
177
- }
178
- return sizeVariant[props.size as ControlFieldSize] || 'w-[16px] h-[16px]'
179
- })
180
-
181
133
  const labelSizeClass = computed(() => {
182
134
  const sizeVariant = {
183
135
  [ControlFieldSize.XS]: 'text-xs',
@@ -198,29 +150,15 @@ const iconSizeClass = computed(() => {
198
150
  return sizeVariant[props.size as ControlFieldSize] || 'w-[20px] h-[20px] min-w-[20px] min-h-[20px]'
199
151
  })
200
152
 
201
- const checkedBackgroundClass = computed(() => {
202
- const backgroundVariant = {
203
- [SwitchStyle.BRAND]: 'bg-background-primary-brand-checked',
204
- [SwitchStyle.SUCCESS]: 'bg-background-success-bold',
205
- }
206
- return backgroundVariant[props.styleType as SwitchStyle] || 'bg-background-primary-brand-checked'
207
- })
208
-
209
153
  // Handlers
210
- const handleChange = () => {
211
- if (validationMode.value === FormValidationMode.BLUR) {
212
- runValidation()
213
- }
214
- }
215
-
216
- const toggleCheckbox = () => {
154
+ const handleSwitchUpdate = (value: boolean) => {
217
155
  if (props.disabled) return
218
156
 
219
157
  if (validationMode.value === FormValidationMode.BLUR) {
220
158
  runValidation()
221
159
  }
222
160
 
223
- emit('update:modelValue', !props.modelValue)
161
+ emit('update:modelValue', value)
224
162
  }
225
163
 
226
164
  const runValidation = () => {
@@ -11,6 +11,7 @@
11
11
  'py-4',
12
12
  'rounded-lg',
13
13
  'hover:cursor-pointer',
14
+ 'transition-colors duration-200 ease-out',
14
15
  modelValue === value && selectedBackgroundColorClass,
15
16
  disabled && 'opacity-disabled'
16
17
  ]"
@@ -81,7 +82,7 @@
81
82
  'border',
82
83
  'rounded-full',
83
84
  'flex items-center justify-center',
84
- 'transition-colors',
85
+ 'transition-colors duration-200 ease-out',
85
86
  modelValue === value ? selectedCheckboxBackgroundColorClass : 'bg-neutral-white border-border-default',
86
87
  modelValue === value && 'border-0',
87
88
  disabled ? 'cursor-not-allowed' : 'cursor-pointer'
@@ -40,7 +40,7 @@
40
40
  'border',
41
41
  'rounded-full',
42
42
  'flex items-center justify-center',
43
- 'transition-colors',
43
+ 'transition-colors duration-200 ease-out',
44
44
  modelValue === value ? 'bg-background-primary-brand-checked border-0' : 'bg-neutral-white border-border-default',
45
45
  modelValue === value && disabled && '!bg-background-neutral-disabled !border-border-neutral-disabled',
46
46
  disabled ? 'bg-background-neutral-disabled cursor-not-allowed' : 'cursor-pointer'
@@ -10,6 +10,7 @@
10
10
  :class="[
11
11
  'group flex items-center justify-center',
12
12
  'text-text-primary-brand-default',
13
+ 'transition-colors duration-200 ease-out',
13
14
  !props.disabled ? 'hover:text-text-primary-brand-hover' : 'opacity-disabled cursor-not-allowed',
14
15
  gapClass,
15
16
  ]">
@@ -8,6 +8,7 @@
8
8
  'items-center',
9
9
  'text-left',
10
10
  'rounded-lg',
11
+ 'transition-colors duration-200 ease-out',
11
12
  'hover:bg-background-neutral-hover',
12
13
  'justify-between',
13
14
  levelTextClass,
@@ -41,25 +41,26 @@
41
41
  @click="handleItemClick(item, getNodePath(index))"
42
42
  />
43
43
 
44
- <NavSidebarMenuItemsTree
45
- v-if="canRenderChildren(item) && isNodeOpen(getNodePath(index))"
46
- :items="item.children ?? []"
47
- :level="level + 1"
48
- :isCollapsed
49
- :openItems
50
- :itemsStyleType
51
- :itemsTextClass
52
- :itemsIconClass
53
- :subItemsCustomClass
54
- :subItemsTextClass
55
- :subItemsIconClass
56
- :thirdLevelItemsCustomClass
57
- :prefetchOn
58
- :showCollapseDivider
59
- :showNestedLevelGuide
60
- :pathPrefix="getNodePath(index)"
61
- @toggle="emit('toggle', $event)"
62
- />
44
+ <VerticalExpansionTransition v-if="canRenderChildren(item)" v-show="isNodeOpen(getNodePath(index))">
45
+ <NavSidebarMenuItemsTree
46
+ :items="item.children ?? []"
47
+ :level="level + 1"
48
+ :isCollapsed
49
+ :openItems
50
+ :itemsStyleType
51
+ :itemsTextClass
52
+ :itemsIconClass
53
+ :subItemsCustomClass
54
+ :subItemsTextClass
55
+ :subItemsIconClass
56
+ :thirdLevelItemsCustomClass
57
+ :prefetchOn
58
+ :showCollapseDivider
59
+ :showNestedLevelGuide
60
+ :pathPrefix="getNodePath(index)"
61
+ @toggle="emit('toggle', $event)"
62
+ />
63
+ </VerticalExpansionTransition>
63
64
  </template>
64
65
  </template>
65
66
  </div>
@@ -6,7 +6,7 @@
6
6
  :disabled="modelValue === 1"
7
7
  :ariaLabel="previousPageAriaLabel"
8
8
  :class="[
9
- 'bg-transparent',
9
+ 'bg-transparent transition-colors duration-200 ease-out',
10
10
  modelValue > 1 && 'hover:border border-border-default',
11
11
  ]"
12
12
  @click="goToPreviousPage"
@@ -20,7 +20,7 @@
20
20
  :disabled="modelValue === totalPages"
21
21
  :ariaLabel="nextPageAriaLabel"
22
22
  :class="[
23
- 'bg-transparent',
23
+ 'bg-transparent transition-colors duration-200 ease-out',
24
24
  modelValue < totalPages && 'hover:border border-border-default',
25
25
  ]"
26
26
  @click="goToNextPage"
@@ -4,7 +4,7 @@
4
4
  :class="[
5
5
  iconSizeClass,
6
6
  colorClass,
7
- isInteractive && 'hover:cursor-pointer'
7
+ isInteractive && 'hover:cursor-pointer hover:scale-110 transition-transform duration-150 ease-out'
8
8
  ]"
9
9
  />
10
10
  </template>
@@ -1,6 +1,5 @@
1
1
  <template>
2
- <div
3
- role="status"
2
+ <output
4
3
  aria-label="Loading"
5
4
  :class="[
6
5
  'animate-spin',
@@ -14,6 +14,7 @@
14
14
  :stepIcon="item.icon"
15
15
  :completedIcon
16
16
  :isHovered="isInteractive && hoveredIndex === index"
17
+ :class="isInteractive && 'cursor-pointer'"
17
18
  @click="handleStepClick(index)"
18
19
  @mouseenter="handleMouseEnter(index)"
19
20
  @mouseleave="handleMouseLeave"
@@ -23,6 +23,7 @@
23
23
  <span
24
24
  :class="[
25
25
  'font-semibold',
26
+ 'transition-colors duration-200 ease-out',
26
27
  titleClass,
27
28
  ]"
28
29
  >
@@ -7,6 +7,7 @@
7
7
  'rounded-full',
8
8
  'select-none',
9
9
  'text-center',
10
+ 'transition-colors duration-200 ease-out',
10
11
  containerBackgroundClass,
11
12
  containerSizeClass,
12
13
  containerBorderClass,
@@ -18,6 +19,7 @@
18
19
  :class="[
19
20
  'flex items-center justify-center',
20
21
  'font-bold',
22
+ 'transition-colors duration-200 ease-out',
21
23
  stepNumberSizeClass,
22
24
  textColorClass,
23
25
  ]"
@@ -39,6 +41,7 @@
39
41
  :class="[
40
42
  'flex items-center justify-center',
41
43
  'font-bold',
44
+ 'transition-colors duration-200 ease-out',
42
45
  stepNumberSizeClass,
43
46
  textColorClass,
44
47
  ]"
@@ -60,6 +63,7 @@
60
63
  :class="[
61
64
  'flex items-center justify-center',
62
65
  'font-bold text-text-primary-brand-default',
66
+ 'transition-colors duration-200 ease-out',
63
67
  stepNumberSizeClass,
64
68
  textColorClass,
65
69
  ]"
@@ -75,11 +79,12 @@
75
79
  </template>
76
80
  <template v-else-if="type === StepIndicatorType.EMPTY">
77
81
  <!-- Circle -->
78
- <div
82
+ <div
79
83
  :class="[
80
84
  'w-[10px] h-[10px] rounded-full bg-icon-primary-brand-default',
85
+ 'transition-colors duration-200 ease-out',
81
86
  circleColorClass,
82
- ]"
87
+ ]"
83
88
  />
84
89
  </template>
85
90
  </span>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imaginario27/air-ui-ds",
3
- "version": "1.15.3",
3
+ "version": "1.16.0",
4
4
  "author": "imaginario27",
5
5
  "type": "module",
6
6
  "homepage": "https://air-ui.netlify.app/",