@imaginario27/air-ui-ds 1.1.2 → 1.1.4

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.
@@ -138,7 +138,7 @@ const iconColorClass = computed(() => {
138
138
  [AlertType.WARNING]: '!text-icon-warning-on-bg',
139
139
  [AlertType.DANGER]: '!text-icon-danger',
140
140
  [AlertType.SUCCESS]: '!text-icon-success',
141
- [AlertType.INFO]: 'text-icon-info',
141
+ [AlertType.INFO]: '!text-icon-info',
142
142
  }
143
143
  return variant[props.type as AlertType] || '!text-icon-warning-on-bg'
144
144
  })
@@ -114,7 +114,7 @@ const handleCopy = useThrottleFn(
114
114
  const success = await copyToClipboard(props.textToCopy)
115
115
 
116
116
  if (success) {
117
- currentCopyButtonIcon.value = 'mdi:check-circle-outline'
117
+ currentCopyButtonIcon.value = 'mdi:check'
118
118
  currentCopyButtonText.value = props.copySuccessText
119
119
 
120
120
  if (
@@ -1,67 +1,131 @@
1
+
1
2
  <template>
2
- <div
3
- ref="dropdownContainer"
3
+ <div
4
+ ref="dropdownContainer"
4
5
  data-test="dropdown-container"
5
6
  :class="[isRelative && 'relative']"
6
7
  >
7
- <div
8
- v-if="isOpen"
9
- v-bind="$attrs"
10
- :class="[
11
- 'bg-background-surface',
12
- 'py-1',
13
- 'rounded',
14
- hasShadow && 'shadow-lg',
15
- 'w-full',
16
- 'flex flex-col',
17
- 'z-50',
18
- hasBorder && 'border border-border-default',
19
- positionClass ? positionClass : dropdownPositionClass,
20
- dropdownClass,
21
- ]"
22
- :style="!positionClass && positionOffsetStyle"
8
+ <!-- Activator Wrapper -->
9
+ <div
10
+ ref="activatorWrapper"
11
+ class="dropdown-activator"
12
+ @click="onActivatorClick"
23
13
  >
24
- <slot
25
- v-if="$slots['items']"
26
- name="items"
27
- :onClose="toggle"
14
+ <slot
15
+ name="activator"
16
+ :isOpen="isOpen"
28
17
  />
29
- <template v-else-if="items?.length && !$slots['items']">
30
- <DropdownMenuItem
31
- v-for="(item, index) in items" :key="index"
32
- :actionType="item.actionType"
33
- :text="item.text"
34
- :icon="item.icon"
35
- :size="item.size"
36
- :type="item.type"
37
- :userDisplayName="item.userDisplayName"
38
- :userProfileImg="item.userProfileImg"
39
- :imgUrl="item.imgUrl"
40
- :alt="item.alt"
41
- :helpText="item.helpText"
42
- :to="item.to"
43
- :isExternal="item.isExternal"
44
- :exportData="item.exportData"
45
- :exportFields="item.exportFields"
46
- :exportType="item.exportType"
47
- :exportFileName="item.exportFileName"
48
- :hasSeparator="item.hasSeparator"
49
- @click="handleClick(item.callback)"
50
- />
51
- </template>
52
18
  </div>
53
- <!-- Activator Slot -->
54
- <slot
55
- name="activator"
56
- :onClick="toggle"
57
- :isOpen
58
- />
19
+
20
+ <template v-if="shouldTeleport">
21
+ <!-- Teleported Dropdown Menu -->
22
+ <teleport to="body">
23
+ <div
24
+ v-if="isOpen"
25
+ ref="dropdown"
26
+ v-bind="$attrs"
27
+ :class="[
28
+ 'bg-background-surface',
29
+ 'py-1',
30
+ 'rounded',
31
+ hasShadow && 'shadow-lg',
32
+ 'flex flex-col',
33
+ 'z-50',
34
+ hasBorder && 'border border-border-default',
35
+ dropdownClass,
36
+ ]"
37
+ :style="computedTeleportStyle"
38
+ >
39
+ <slot
40
+ v-if="$slots.items"
41
+ name="items"
42
+ :onClose="close"
43
+ />
44
+ <template v-else-if="items?.length">
45
+ <DropdownMenuItem
46
+ v-for="(item, index) in items"
47
+ :key="index"
48
+ :actionType="item.actionType"
49
+ :text="item.text"
50
+ :icon="item.icon"
51
+ :size="item.size"
52
+ :type="item.type"
53
+ :userDisplayName="item.userDisplayName"
54
+ :userProfileImg="item.userProfileImg"
55
+ :imgUrl="item.imgUrl"
56
+ :alt="item.alt"
57
+ :helpText="item.helpText"
58
+ :to="item.to"
59
+ :isExternal="item.isExternal"
60
+ :exportData="item.exportData"
61
+ :exportFields="item.exportFields"
62
+ :exportType="item.exportType"
63
+ :exportFileName="item.exportFileName"
64
+ :hasSeparator="item.hasSeparator"
65
+ @click="handleClick(item.callback)"
66
+ />
67
+ </template>
68
+ </div>
69
+ </teleport>
70
+ </template>
71
+ <template v-else>
72
+ <div
73
+ v-if="isOpen"
74
+ ref="dropdown"
75
+ v-bind="$attrs"
76
+ :class="[
77
+ 'bg-background-surface',
78
+ 'py-1',
79
+ 'rounded',
80
+ hasShadow && 'shadow-lg',
81
+ 'w-full',
82
+ 'flex flex-col',
83
+ 'z-50',
84
+ hasBorder && 'border border-border-default',
85
+ positionClass ? positionClass : dropdownPositionClass,
86
+ dropdownClass,
87
+ ]"
88
+ :style="!positionClass && positionOffsetStyle"
89
+ >
90
+ <slot
91
+ v-if="$slots['items']"
92
+ name="items"
93
+ :onClose="toggle"
94
+ />
95
+ <template v-else-if="items?.length && !$slots['items']">
96
+ <DropdownMenuItem
97
+ v-for="(item, index) in items" :key="index"
98
+ :actionType="item.actionType"
99
+ :text="item.text"
100
+ :icon="item.icon"
101
+ :size="item.size"
102
+ :type="item.type"
103
+ :userDisplayName="item.userDisplayName"
104
+ :userProfileImg="item.userProfileImg"
105
+ :imgUrl="item.imgUrl"
106
+ :alt="item.alt"
107
+ :helpText="item.helpText"
108
+ :to="item.to"
109
+ :isExternal="item.isExternal"
110
+ :exportData="item.exportData"
111
+ :exportFields="item.exportFields"
112
+ :exportType="item.exportType"
113
+ :exportFileName="item.exportFileName"
114
+ :hasSeparator="item.hasSeparator"
115
+ @click="handleClick(item.callback)"
116
+ />
117
+ </template>
118
+ </div>
119
+ </template>
59
120
  </div>
60
121
  </template>
122
+
61
123
  <script setup lang="ts">
62
- // Component options
124
+ // Imports
125
+ import type { CSSProperties } from 'vue'
126
+
63
127
  defineOptions({
64
- inheritAttrs: false, // Prevents Vue from automatically applying attributes incorrectly
128
+ inheritAttrs: false,
65
129
  })
66
130
 
67
131
  // Props
@@ -94,27 +158,149 @@ const props = defineProps({
94
158
  type: Boolean as PropType<boolean>,
95
159
  default: true,
96
160
  },
161
+ shouldTeleport: {
162
+ type: Boolean as PropType<boolean>,
163
+ default: true,
164
+ },
97
165
  })
98
166
 
99
- // Ref
100
- const dropdownContainer = ref(null)
167
+ // Refs
168
+ const activatorWrapper = ref<HTMLElement | null>(null)
169
+ const dropdown = ref<HTMLElement | null>(null)
170
+ const isOpen = ref(false)
101
171
 
102
- // Composables
103
- const [isOpen, toggle] = useToggle(false)
172
+ // States
173
+ const isPositioned = ref(false)
174
+ const activatorRect = ref<DOMRect | null>(null)
175
+ const dropdownRect = ref<DOMRect | null>(null)
104
176
 
105
- onClickOutside(dropdownContainer, () => {
177
+ // Methods
178
+ const close = () => {
106
179
  isOpen.value = false
107
- })
180
+ isPositioned.value = false
181
+ }
182
+
183
+ const open = () => {
184
+ isOpen.value = true
185
+ isPositioned.value = false
186
+
187
+ nextTick(() => {
188
+ requestAnimationFrame(() => {
189
+ updateRects()
190
+ isPositioned.value = true
191
+ })
192
+ })
193
+ }
194
+
195
+ const toggle = () => {
196
+ if (isOpen.value) {
197
+ close()
198
+ } else {
199
+ open()
200
+ }
201
+ }
202
+
203
+ const getActivatorElement = () => {
204
+ return activatorWrapper.value
205
+ }
206
+
207
+ const onActivatorClick = (event: MouseEvent) => {
208
+ if (!activatorWrapper.value) return
209
+
210
+ if (activatorWrapper.value.contains(event.target as Node)) {
211
+ toggle()
212
+ }
213
+ }
214
+
215
+ const updateRects = () => {
216
+ const activatorEl = getActivatorElement()
217
+ if (!activatorEl || !dropdown.value) return
218
+
219
+ activatorRect.value = activatorEl.getBoundingClientRect()
220
+ dropdownRect.value = dropdown.value.getBoundingClientRect()
221
+ }
108
222
 
109
- // Handlers
110
223
  const handleClick = (callback?: () => void) => {
111
- if(callback) {
112
- callback()
224
+ if (callback) callback()
225
+ close()
226
+ }
227
+
228
+ const handleClickOutside = (event: MouseEvent) => {
229
+ const dropdownEl = dropdown.value
230
+ const activatorEl = activatorWrapper.value
231
+
232
+ if (!dropdownEl || !activatorEl) return
233
+
234
+ const target = event.target as Node
235
+
236
+ if (
237
+ !dropdownEl.contains(target) &&
238
+ !activatorEl.contains(target)
239
+ ) {
240
+ close()
241
+ }
242
+ }
243
+
244
+ const handleScrollOutside = (event: Event) => {
245
+ const dropdownEl = dropdown.value
246
+ const activatorEl = activatorWrapper.value
247
+
248
+ if (!dropdownEl || !activatorEl) return
249
+
250
+ const target = event.target as Node
251
+
252
+ // If scroll happened on an element not containing the dropdown or activator
253
+ if (
254
+ !dropdownEl.contains(target) &&
255
+ !activatorEl.contains(target)
256
+ ) {
257
+ close()
113
258
  }
114
- toggle()
115
259
  }
116
260
 
117
- // Computed
261
+ const computedTeleportStyle = computed<CSSProperties>(() => {
262
+ if (!isPositioned.value || !activatorRect.value || !dropdownRect.value) {
263
+ return {
264
+ position: 'absolute',
265
+ top: '0px',
266
+ left: '0px',
267
+ visibility: 'hidden',
268
+ zIndex: '50',
269
+ width: '0px',
270
+ }
271
+ }
272
+
273
+ const a = activatorRect.value
274
+ const d = dropdownRect.value
275
+ const x = Number(props.positionXOffset) || 0
276
+ const y = Number(props.positionYOffset) || 0
277
+
278
+ const [primary, secondary] = props.position.toLowerCase().split('-')
279
+
280
+ let top = 0
281
+ let left = 0
282
+
283
+ if (primary === 'bottom') top = a.bottom + y
284
+ if (primary === 'top') top = a.top - d.height - y
285
+ if (primary === 'left') left = a.left - d.width - x
286
+ if (primary === 'right') left = a.right + x
287
+
288
+ if (secondary === 'left') left = a.left
289
+ if (secondary === 'right') left = a.right - d.width
290
+ if (secondary === 'top') top = a.top
291
+ if (secondary === 'bottom') top = a.bottom - d.height
292
+
293
+ return {
294
+ position: 'absolute',
295
+ top: `${top + window.scrollY}px`,
296
+ left: `${left + window.scrollX}px`,
297
+ width: `${a.width}px`,
298
+ visibility: 'visible',
299
+ zIndex: '50',
300
+ }
301
+ })
302
+
303
+ // Styles for non-teleported dropdown
118
304
  const dropdownPositionClass = computed(() => {
119
305
  const variant = {
120
306
  [DropdownPosition.LEFT_TOP]: 'absolute right-full top-0',
@@ -204,4 +390,15 @@ const positionOffsetStyle = computed(() => {
204
390
  return style
205
391
  })
206
392
 
207
- </script>
393
+ onMounted(() => {
394
+ document.addEventListener('click', handleClickOutside)
395
+ window.addEventListener('resize', close)
396
+ document.addEventListener('scroll', handleScrollOutside, true)
397
+ })
398
+
399
+ onBeforeUnmount(() => {
400
+ document.removeEventListener('click', handleClickOutside)
401
+ window.removeEventListener('resize', close)
402
+ document.removeEventListener('scroll', handleScrollOutside, true)
403
+ })
404
+ </script>
@@ -3,6 +3,7 @@
3
3
  <!-- Dropdown Menu -->
4
4
  <DropdownMenu
5
5
  ref="dropdownContainer"
6
+ :shouldTeleport="false"
6
7
  :positionClass="`absolute ${dropdownPositionClass}`"
7
8
  dropdownClass="max-w-full z-10"
8
9
  :class="[
@@ -12,7 +13,7 @@
12
13
  'border-border-default',
13
14
  ]"
14
15
  >
15
- <template #activator="{ onClick, isOpen }">
16
+ <template #activator="{ isOpen }">
16
17
  <!-- Select Box -->
17
18
  <div
18
19
  :class="[
@@ -29,7 +30,7 @@
29
30
  disabled ? 'text-text-neutral-disabled' : 'text-text-default',
30
31
  sizeClass,
31
32
  ]"
32
- @click="!disabled && onClick()"
33
+ @click="!disabled"
33
34
  >
34
35
  <div v-if="multiple">
35
36
  <template v-if="Array.isArray(selected) && selected.length">
@@ -65,7 +65,7 @@
65
65
  <Icon
66
66
  v-if="isSelected && activeStyle === SelectActiveStyle.CHECK"
67
67
  name="mdi:check"
68
- iconClass="text-icon-primary-brand-active"
68
+ iconClass="!text-icon-primary-brand-active"
69
69
  />
70
70
  </div>
71
71
  <p
@@ -123,10 +123,10 @@ const orientationClass = computed(() => {
123
123
 
124
124
  const iconSizeClass = computed(() => {
125
125
  const iconSize = {
126
- [Orientation.VERTICAL]: '!min-w-[40px] !max-w-[40px]',
127
- [Orientation.HORIZONTAL]: '!min-w-[32px] !max-w-[32px]',
126
+ [Orientation.VERTICAL]: 'w-[40px] h-[40px] min-w-[40px] min-h-[40px]',
127
+ [Orientation.HORIZONTAL]: 'w-[32px] h-[32px] min-w-[32px] min-h-[32px]',
128
128
  }
129
- return iconSize[props.orientation as Orientation] || '!min-w-[32px] max-w-[32px]'
129
+ return iconSize[props.orientation as Orientation] || 'w-[32px] h-[32px] min-w-[32px] min-h-[32px]'
130
130
  })
131
131
 
132
132
  const containerClasses = computed(() => {
@@ -3,13 +3,10 @@
3
3
  :name
4
4
  :mode
5
5
  :customize="svgCustomize"
6
- :class="[
7
- iconSizeClass,
8
- iconColorClass,
9
- ...normalizedIconClass,
10
- ]"
6
+ :class="finalIconClasses"
11
7
  />
12
8
  </template>
9
+
13
10
  <script setup lang="ts">
14
11
  // Props
15
12
  const props = defineProps({
@@ -24,23 +21,36 @@ const props = defineProps({
24
21
  },
25
22
  size: {
26
23
  type: String as PropType<IconSize>,
27
- default: IconSize.MD,
24
+ default: IconSize.MD,
28
25
  validator: (value: IconSize) => Object.values(IconSize).includes(value),
29
26
  },
30
27
  color: String as PropType<ColorAccent>,
31
- svgCustomize: Function as PropType<CollectionCustomizeCallback>,
32
- iconClass: [String, Array] as PropType<string | string[]>,
28
+ svgCustomize: Function as PropType<CollectionCustomizeCallback>,
29
+ iconClass: [String, Array] as PropType<string | string[]>,
33
30
  })
34
31
 
35
- // Computed function
32
+ // Normalize iconClass into array
36
33
  const normalizedIconClass = computed(() => {
37
34
  return Array.isArray(props.iconClass)
38
35
  ? props.iconClass
39
36
  : props.iconClass?.split(' ').filter(Boolean) || []
40
37
  })
41
38
 
42
- // Computed classes
39
+ // Detect overrides
40
+ const hasTextClass = computed(() =>
41
+ normalizedIconClass.value.some(cls => cls.startsWith('text-'))
42
+ )
43
+
44
+ const hasSizeClass = computed(() =>
45
+ normalizedIconClass.value.some(cls =>
46
+ /^(w-|h-|min-w-|min-h-)/.test(cls)
47
+ )
48
+ )
49
+
50
+ // Size classes (skipped if overridden)
43
51
  const iconSizeClass = computed(() => {
52
+ if (hasSizeClass.value) return null
53
+
44
54
  const variants = {
45
55
  [IconSize.XS]: 'w-[12px] h-[12px] min-w-[12px] min-h-[12px]',
46
56
  [IconSize.SM]: 'w-[16px] h-[16px] min-w-[16px] min-h-[16px]',
@@ -50,10 +60,13 @@ const iconSizeClass = computed(() => {
50
60
  [IconSize.XXL]: 'w-[40px] h-[40px] min-w-[40px] min-h-[40px]',
51
61
  }
52
62
 
53
- return variants[props.size as IconSize] || 'w-[20px] h-[20px] min-w-[20px] min-h-[20px]'
63
+ return variants[props.size as IconSize] || variants[IconSize.MD]
54
64
  })
55
65
 
66
+ // Color classes (skipped if overridden)
56
67
  const iconColorClass = computed(() => {
68
+ if (hasTextClass.value) return null
69
+
57
70
  if (!props.color) return 'text-inherit'
58
71
 
59
72
  const variants = {
@@ -68,4 +81,15 @@ const iconColorClass = computed(() => {
68
81
 
69
82
  return variants[props.color] || 'text-inherit'
70
83
  })
71
- </script>
84
+
85
+ // Final class list (deduplicated)
86
+ const finalIconClasses = computed(() => {
87
+ const classes = [
88
+ iconSizeClass.value,
89
+ iconColorClass.value,
90
+ ...normalizedIconClass.value,
91
+ ].filter(Boolean)
92
+
93
+ return [...new Set(classes)]
94
+ })
95
+ </script>
@@ -9,8 +9,8 @@
9
9
  <template v-if="items.length && !$slots.default">
10
10
  <ListItem
11
11
  v-for="(item, index) in items" :key="index"
12
- :icon="listItemIcon"
13
- :iconClass="listItemIconClass"
12
+ :markerIcon="listItemIcon"
13
+ :markerIconClass="listItemIconClass"
14
14
  :size="listItemSize"
15
15
  :spaced="spaced"
16
16
  >
@@ -6,9 +6,9 @@
6
6
  ]"
7
7
  >
8
8
  <Icon
9
- v-if="icon"
10
- :name="icon"
11
- :class="[iconClass, iconSizeClass]"
9
+ v-if="markerIcon"
10
+ :name="markerIcon"
11
+ :iconClass="[markerIconClass, markerIconSizeClass]"
12
12
  />
13
13
  <span :class="[contentSizeClass, 'w-full']">
14
14
  <slot />
@@ -18,8 +18,8 @@
18
18
  <script setup lang="ts">
19
19
  // Props
20
20
  const props = defineProps({
21
- icon: String as PropType<string>,
22
- iconClass: {
21
+ markerIcon: String as PropType<string>,
22
+ markerIconClass: {
23
23
  type: String as PropType<string>,
24
24
  default: 'text-icon-secondary-brand-default'
25
25
  },
@@ -35,13 +35,13 @@ const props = defineProps({
35
35
  })
36
36
 
37
37
  // Computed
38
- const iconSizeClass = computed(() => {
38
+ const markerIconSizeClass = computed(() => {
39
39
  const sizeVariant = {
40
- [ListItemSize.XS]: 'w-[16px] h-[16px] min-w-[16px] min-h-[16px]',
41
- [ListItemSize.SM]: 'w-[20px] h-[20px] min-w-[20px] min-h-[20px]',
42
- [ListItemSize.MD]: 'w-[24px] h-[24px] min-w-[24px] min-h-[24px]',
40
+ [ListItemSize.XS]: '!w-[16px] h-[16px] !min-w-[16px] !min-h-[16px]',
41
+ [ListItemSize.SM]: '!w-[20px] h-[20px] !min-w-[20px] !min-h-[20px]',
42
+ [ListItemSize.MD]: '!w-[24px] h-[24px] !min-w-[24px] !min-h-[24px]',
43
43
  }
44
- return sizeVariant[props.size as ListItemSize] || 'w-[20px] h-[20px] min-w-[20px] min-h-[20px]'
44
+ return sizeVariant[props.size as ListItemSize] || '!w-[20px] !h-[20px] !min-w-[20px] !min-h-[20px]'
45
45
  })
46
46
 
47
47
  const contentSizeClass = computed(() => {
@@ -45,9 +45,9 @@ const iconSizeClass = computed(() => {
45
45
 
46
46
  const colorClass = computed(() => {
47
47
  const colorVariants = {
48
- [RatingItemColor.GOLD]: 'text-icon-rating',
49
- [RatingItemColor.PRIMARY_BRAND]: 'text-icon-primary-brand-rating',
48
+ [RatingItemColor.GOLD]: '!text-icon-rating',
49
+ [RatingItemColor.PRIMARY_BRAND]: '!text-icon-primary-brand-rating',
50
50
  }
51
- return colorVariants[props.color as RatingItemColor] || 'text-icon-rating'
51
+ return colorVariants[props.color as RatingItemColor] || '!text-icon-rating'
52
52
  })
53
53
  </script>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imaginario27/air-ui-ds",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "author": "imaginario27",
5
5
  "type": "module",
6
6
  "homepage": "https://air-ui.netlify.app/",