@imaginario27/air-ui-ds 1.2.3 → 1.2.5

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.
@@ -1,13 +1,20 @@
1
1
  <template>
2
2
  <div
3
- :class="[
4
- 'inline-flex items-center gap-2 font-semibold text-xs px-2 h-[24px] w-fit',
3
+ :class="[
4
+ 'inline-flex',
5
+ 'items-center',
6
+ 'gap-2',
7
+ 'font-semibold',
8
+ 'text-xs',
9
+ 'px-2',
10
+ 'h-[24px]',
11
+ 'w-fit',
5
12
  styleClass,
6
13
  shapeClass,
7
14
  colorClass,
8
15
  borderColorClass,
9
16
  isTransparent ? 'bg-transparent' : undefined,
10
- showDot ? 'pl-3' : undefined
17
+ showDot ? 'pl-3' : undefined,
11
18
  ]"
12
19
  >
13
20
  <!-- Dot -->
@@ -25,7 +32,14 @@
25
32
  />
26
33
 
27
34
  <!-- Text -->
28
- <span :class="textClass">{{ text }}</span>
35
+ <span
36
+ :class="[
37
+ 'pt-0.25',
38
+ textClass,
39
+ ]"
40
+ >
41
+ {{ text }}
42
+ </span>
29
43
 
30
44
  <!-- Close button -->
31
45
  <div
@@ -195,7 +209,7 @@ const iconColorClass = computed(() => {
195
209
  })
196
210
 
197
211
  const dotColorClass = computed(() => {
198
- if (props.styleType === BadgeStyle.FILLED) return "bg-text-on-filled"
212
+ if (props.styleType === BadgeStyle.FILLED) return "bg-text-neutral-on-filled"
199
213
 
200
214
  const dotVariant: Record<ColorAccent, string> = {
201
215
  [ColorAccent.NEUTRAL]: "bg-icon-neutral-subtle",
@@ -75,6 +75,10 @@ const props = defineProps({
75
75
  type: Boolean as PropType<boolean>,
76
76
  default: false,
77
77
  },
78
+ customRoute: {
79
+ type: String as PropType<string | undefined | null>,
80
+ default: null,
81
+ },
78
82
  homeIconClass: {
79
83
  type: String as PropType<string>,
80
84
  default: '',
@@ -96,9 +100,16 @@ const props = defineProps({
96
100
  // Route
97
101
  const route = useRoute()
98
102
 
99
- // Generate all breadcrumbs from route segments
103
+ // Decide which path to use
104
+ const basePath = computed(() => {
105
+ return props.customRoute ?? route.path
106
+ })
107
+
108
+ // Generate all breadcrumbs from selected path
100
109
  const allCrumbs = computed(() => {
101
- const pathSegments = route.path.split('/').filter(Boolean)
110
+ const pathSegments = basePath.value
111
+ .split('/')
112
+ .filter(Boolean)
102
113
 
103
114
  return pathSegments.map((segment, index) => {
104
115
  return {
@@ -32,7 +32,7 @@
32
32
  />
33
33
  </div>
34
34
 
35
- <span :class="['font-semibold', textSizeClass, textClass]">
35
+ <span :class="['font-semibold', textSizeClass, textClass, textTopSpacingClass]">
36
36
  {{ loadingText }}
37
37
  </span>
38
38
  </template>
@@ -52,7 +52,7 @@
52
52
  />
53
53
  </template>
54
54
 
55
- <span :class="['font-semibold', textSizeClass, textClass]">
55
+ <span :class="['font-semibold', textSizeClass, textClass, textTopSpacingClass]">
56
56
  {{ text }}
57
57
  </span>
58
58
 
@@ -330,14 +330,37 @@ const iconColorClass = computed(() => {
330
330
  })
331
331
 
332
332
  const horizontalPaddingClass = computed(() => {
333
- const variant = {
334
- [ButtonSize.XS]: 'px-2',
335
- [ButtonSize.SM]: 'px-2',
336
- [ButtonSize.MD]: 'px-2.5',
337
- [ButtonSize.LG]: 'px-3',
338
- [ButtonSize.XL]: 'px-3.5',
339
- [ButtonSize.XXL]: 'px-4',
333
+ let variant
334
+
335
+ if (props.iconPosition === IconPosition.LEFT) {
336
+ variant = {
337
+ [ButtonSize.XS]: 'pl-3 pr-4',
338
+ [ButtonSize.SM]: 'pl-3 pr-4',
339
+ [ButtonSize.MD]: 'pl-4 pr-5',
340
+ [ButtonSize.LG]: 'pl-4 pr-5',
341
+ [ButtonSize.XL]: 'pl-4 pr-5',
342
+ [ButtonSize.XXL]: 'pl-4 pr-5',
343
+ }
344
+ } else if (props.iconPosition === IconPosition.RIGHT) {
345
+ variant = {
346
+ [ButtonSize.XS]: 'pl-4 pr-3',
347
+ [ButtonSize.SM]: 'pl-4 pr-3',
348
+ [ButtonSize.MD]: 'pl-5 pr-4',
349
+ [ButtonSize.LG]: 'pl-5 pr-4',
350
+ [ButtonSize.XL]: 'pl-5 pr-4',
351
+ [ButtonSize.XXL]: 'pl-5 pr-4',
352
+ }
353
+ } else {
354
+ variant = {
355
+ [ButtonSize.XS]: 'px-3',
356
+ [ButtonSize.SM]: 'px-3',
357
+ [ButtonSize.MD]: 'px-4',
358
+ [ButtonSize.LG]: 'px-4',
359
+ [ButtonSize.XL]: 'px-4',
360
+ [ButtonSize.XXL]: 'px-4',
361
+ }
340
362
  }
363
+
341
364
  return variant[props.size as ButtonSize] || 'px-3'
342
365
  })
343
366
 
@@ -350,9 +373,17 @@ const gapClass = computed(() => {
350
373
  [ButtonSize.XL]: 'gap-2',
351
374
  [ButtonSize.XXL]: 'gap-2',
352
375
  }
376
+
353
377
  return variant[props.size as ButtonSize] || 'gap-2'
354
378
  })
355
379
 
380
+
381
+ const textTopSpacingClass = computed(() => {
382
+ return props.size === ButtonSize.XXL
383
+ ? undefined
384
+ : 'pt-0.25'
385
+ })
386
+
356
387
  // Props for the dynamic component
357
388
  const componentProps = computed(() => {
358
389
  if (props.actionType === ButtonActionType.LINK) {
@@ -0,0 +1,244 @@
1
+ <template>
2
+ <!-- Overlay -->
3
+ <Transition
4
+ enter-active-class="transition-opacity duration-200 ease-out"
5
+ enter-from-class="opacity-0"
6
+ enter-to-class="opacity-100"
7
+ leave-active-class="transition-opacity duration-200 ease-in"
8
+ leave-from-class="opacity-100"
9
+ leave-to-class="opacity-0"
10
+ >
11
+ <div
12
+ v-if="isOpen && hasOverlay"
13
+ :class="[
14
+ 'fixed',
15
+ 'inset-0',
16
+ 'bg-background-overlay',
17
+ 'backdrop-blur-sm',
18
+ 'z-[10000]',
19
+ overlayClass,
20
+ ]"
21
+ @click="closeOnOverlayClick ? close() : null"
22
+ />
23
+ </Transition>
24
+
25
+ <!-- Drawer -->
26
+ <Transition
27
+ :enter-active-class="transitionClasses.enterActive"
28
+ :enter-from-class="transitionClasses.enterFrom"
29
+ :enter-to-class="transitionClasses.enterTo"
30
+ :leave-active-class="transitionClasses.leaveActive"
31
+ :leave-from-class="transitionClasses.leaveFrom"
32
+ :leave-to-class="transitionClasses.leaveTo"
33
+ >
34
+ <aside
35
+ v-if="isOpen"
36
+ :class="[
37
+ 'fixed',
38
+ 'bg-background-container-surface',
39
+ 'shadow-xl',
40
+ 'z-[10000]',
41
+ 'p-4',
42
+ 'flex',
43
+ 'flex-col',
44
+ 'gap-4',
45
+ positionClasses,
46
+ sizeClasses,
47
+ drawerClass,
48
+ borderClass,
49
+ ]"
50
+ :style="drawerInlineStyle"
51
+ >
52
+ <!-- Header -->
53
+ <div
54
+ v-if="hasHeader"
55
+ :class="[
56
+ 'flex',
57
+ 'items-center',
58
+ 'justify-between',
59
+ ]"
60
+ >
61
+ <component
62
+ :is="titleHeadingTag"
63
+ class="text-lg font-semibold"
64
+ >
65
+ {{ title }}
66
+ </component>
67
+
68
+ <ActionIconButton
69
+ v-if="hasCloseButton"
70
+ :icon="buttonCloseIcon"
71
+ :styleType="ButtonStyleType.NEUTRAL_TRANSPARENT"
72
+ @click="close"
73
+ />
74
+ </div>
75
+
76
+ <!-- Content -->
77
+ <div class="flex-1 overflow-y-auto">
78
+ <slot />
79
+ </div>
80
+ </aside>
81
+ </Transition>
82
+ </template>
83
+
84
+ <script setup lang="ts">
85
+ // Props
86
+ const props = defineProps({
87
+ modelValue: {
88
+ type: Boolean as PropType<boolean>,
89
+ required: true,
90
+ },
91
+ direction: {
92
+ type: String as PropType<Direction>,
93
+ default: Direction.RIGHT,
94
+ validator: (value: Direction) => Object.values(Direction).includes(value),
95
+ },
96
+ maxSize: {
97
+ type: Number as PropType<number>,
98
+ default: 320,
99
+ },
100
+ hasHeader: {
101
+ type: Boolean as PropType<boolean>,
102
+ default: true,
103
+ },
104
+ hasCloseButton: {
105
+ type: Boolean as PropType<boolean>,
106
+ default: true,
107
+ },
108
+ hasOverlay: {
109
+ type: Boolean as PropType<boolean>,
110
+ default: true,
111
+ },
112
+ closeOnOverlayClick: {
113
+ type: Boolean as PropType<boolean>,
114
+ default: true,
115
+ },
116
+ title: {
117
+ type: String as PropType<string>,
118
+ default: 'Drawer',
119
+ },
120
+ titleHeadingTag: {
121
+ type: String as PropType<'h2' | 'h3' | 'h4' | 'h5' | 'h6'>,
122
+ default: 'h2',
123
+ },
124
+ buttonCloseIcon: {
125
+ type: String as PropType<string>,
126
+ default: 'mdi:close',
127
+ },
128
+ hasBorder: {
129
+ type: Boolean as PropType<boolean>,
130
+ default: true,
131
+ },
132
+ drawerClass: String as PropType<string>,
133
+ overlayClass: String as PropType<string>,
134
+ })
135
+
136
+ // Emits
137
+ const emit = defineEmits(['update:modelValue'])
138
+
139
+ const isOpen = computed(() => props.modelValue)
140
+
141
+ // Computed classes
142
+ const positionClasses = computed(() => {
143
+ const map: Record<Direction, string[]> = {
144
+ [Direction.RIGHT]: ['top-0', 'right-0', 'h-full'],
145
+ [Direction.LEFT]: ['top-0', 'left-0', 'h-full'],
146
+ [Direction.TOP]: ['top-0', 'left-0', 'w-full'],
147
+ [Direction.BOTTOM]: ['bottom-0', 'left-0', 'w-full'],
148
+ }
149
+
150
+ return map[props.direction]
151
+ })
152
+
153
+ const sizeClasses = computed(() => {
154
+ if (props.direction === Direction.LEFT || props.direction === Direction.RIGHT) {
155
+ return ['w-full']
156
+ }
157
+
158
+ return []
159
+ })
160
+
161
+ const drawerInlineStyle = computed(() => {
162
+ if (props.direction === Direction.LEFT || props.direction === Direction.RIGHT) {
163
+ return { maxWidth: props.maxSize + 'px' }
164
+ }
165
+
166
+ return { maxHeight: props.maxSize + 'px' }
167
+ })
168
+
169
+ const transitionClasses = computed(() => {
170
+ const base = 'transform transition-transform duration-300 ease-out'
171
+ const leaveBase = 'transform transition-transform duration-300 ease-in'
172
+
173
+ const map: Record<
174
+ Direction,
175
+ {
176
+ enterActive: string
177
+ enterFrom: string
178
+ enterTo: string
179
+ leaveActive: string
180
+ leaveFrom: string
181
+ leaveTo: string
182
+ }
183
+ > = {
184
+ [Direction.RIGHT]: {
185
+ enterActive: base,
186
+ enterFrom: 'translate-x-full',
187
+ enterTo: 'translate-x-0',
188
+ leaveActive: leaveBase,
189
+ leaveFrom: 'translate-x-0',
190
+ leaveTo: 'translate-x-full',
191
+ },
192
+ [Direction.LEFT]: {
193
+ enterActive: base,
194
+ enterFrom: '-translate-x-full',
195
+ enterTo: 'translate-x-0',
196
+ leaveActive: leaveBase,
197
+ leaveFrom: 'translate-x-0',
198
+ leaveTo: '-translate-x-full',
199
+ },
200
+ [Direction.TOP]: {
201
+ enterActive: base,
202
+ enterFrom: '-translate-y-full',
203
+ enterTo: 'translate-y-0',
204
+ leaveActive: leaveBase,
205
+ leaveFrom: 'translate-y-0',
206
+ leaveTo: '-translate-y-full',
207
+ },
208
+ [Direction.BOTTOM]: {
209
+ enterActive: base,
210
+ enterFrom: 'translate-y-full',
211
+ enterTo: 'translate-y-0',
212
+ leaveActive: leaveBase,
213
+ leaveFrom: 'translate-y-0',
214
+ leaveTo: 'translate-y-full',
215
+ },
216
+ }
217
+
218
+ return map[props.direction]
219
+ })
220
+
221
+ const borderClass = computed(() => {
222
+ if (!props.hasBorder) return ''
223
+
224
+ const colorClass = 'border-border-default'
225
+
226
+ switch (props.direction) {
227
+ case Direction.RIGHT:
228
+ return `border-l ${colorClass}`
229
+ case Direction.LEFT:
230
+ return `border-r ${colorClass}`
231
+ case Direction.TOP:
232
+ return `border-b ${colorClass}`
233
+ case Direction.BOTTOM:
234
+ return `border-t ${colorClass}`
235
+ default:
236
+ return ''
237
+ }
238
+ })
239
+
240
+ // Handlers
241
+ const close = () => {
242
+ emit('update:modelValue', false)
243
+ }
244
+ </script>
@@ -60,7 +60,10 @@
60
60
  </p>
61
61
  </template>
62
62
  <template #button="{ fileInput }">
63
- <div class="w-full flex justify-center my-4">
63
+ <div
64
+ class="w-full flex justify-center my-4"
65
+ @click.stop
66
+ >
64
67
  <ActionButton
65
68
  :text="computedButtonText"
66
69
  :styleType="ButtonStyleType.PRIMARY_BRAND_SOFT"
@@ -201,25 +204,41 @@ const acceptedFileTypes = computed(() => {
201
204
  })
202
205
 
203
206
  const computedTitleText = computed(() => {
204
- if (props.showPreview && props.previewImageUrl && selectedFiles.value.length === 0) {
205
- return props.title || 'Upload a new file to replace current one'
207
+ const isReplacingExisting =
208
+ props.showPreview &&
209
+ props.previewImageUrl &&
210
+ selectedFiles.value.length === 0
211
+
212
+ if (isReplacingExisting) {
213
+ return props.title ?? 'Upload a new file to replace current one'
214
+ }
215
+
216
+ if (props.title) {
217
+ return props.title
206
218
  }
207
219
 
208
- if (!props.title) {
209
- if (props.multiple) return 'Drag & drop files here or click to upload'
210
- else return 'Drag & drop a file here or click to upload'
211
- } else return props.title
220
+ return props.multiple
221
+ ? 'Drag & drop files here or click to upload'
222
+ : 'Drag & drop a file here or click to upload'
212
223
  })
213
224
 
214
225
  const computedButtonText = computed(() => {
215
- if (props.showPreview && props.previewImageUrl && selectedFiles.value.length === 0) {
216
- return props.buttonText || 'Replace file'
226
+ const isReplacingExisting =
227
+ props.showPreview &&
228
+ props.previewImageUrl &&
229
+ selectedFiles.value.length === 0
230
+
231
+ if (isReplacingExisting) {
232
+ return props.buttonText ?? 'Replace file'
217
233
  }
218
234
 
219
- if (!props.buttonText) {
220
- if (props.multiple) return 'Upload files'
221
- else return 'Upload a file'
222
- } else return props.buttonText
235
+ if (props.buttonText) {
236
+ return props.buttonText
237
+ }
238
+
239
+ return props.multiple
240
+ ? 'Upload files'
241
+ : 'Upload a file'
223
242
  })
224
243
 
225
244
  const selectedFiles = computed({
@@ -230,10 +249,6 @@ const selectedFiles = computed({
230
249
  if (props.required) {
231
250
  runValidation()
232
251
  }
233
-
234
- if (newFiles.length === 0) {
235
- dropzoneKey.value++ // This forces Vue3Dropzone to re-render
236
- }
237
252
  },
238
253
  })
239
254
 
@@ -1,20 +1,16 @@
1
1
  <template>
2
2
  <th
3
+ :scope
3
4
  :class="[
4
- 'px-3',
5
- 'py-3.5',
6
- 'border-b',
7
- 'border-border-neutral-subtle',
8
- 'font-semibold',
9
- 'text-sm',
10
- 'whitespace-nowrap',
5
+ ...headerClass
11
6
  ]"
7
+ @click="scope === TableHeaderCellScope.ROW && handleNavigation()"
12
8
  >
13
9
  <div class="w-full flex items-center gap-2">
14
10
  <slot />
15
11
 
16
12
  <ActionIconButton
17
- v-if="sorteable"
13
+ v-if="sorteable && scope === TableHeaderCellScope.COL"
18
14
  :size="ButtonSize.XS"
19
15
  :icon="sortAsc ? 'mdi:arrow-up' : 'mdi:arrow-down'"
20
16
  :styleType="sortKey === columnKey ? ButtonStyleType.NEUTRAL_FILLED : ButtonStyleType.NEUTRAL_OUTLINED"
@@ -25,7 +21,12 @@
25
21
  </template>
26
22
  <script setup lang="ts">
27
23
  // Props
28
- defineProps({
24
+ const props = defineProps({
25
+ scope : {
26
+ type: String as PropType<TableHeaderCellScope>,
27
+ default: TableHeaderCellScope.COL,
28
+ validator: (value: TableHeaderCellScope) => Object.values(TableHeaderCellScope).includes(value),
29
+ },
29
30
  sorteable: {
30
31
  type: Boolean as PropType<boolean>,
31
32
  default: false,
@@ -46,5 +47,42 @@ defineProps({
46
47
  type: Function as PropType<() => void>,
47
48
  default: undefined,
48
49
  },
50
+ fitToContent: {
51
+ type: Boolean as PropType<boolean>,
52
+ default: false,
53
+ },
54
+ to: String as PropType<string>,
49
55
  })
56
+
57
+ // Computed classes
58
+ const headerClass = computed(() => {
59
+ const variants = {
60
+ [TableHeaderCellScope.COL]: [
61
+ 'px-3',
62
+ 'py-3.5',
63
+ 'border-b',
64
+ 'border-border-neutral-subtle',
65
+ 'font-semibold',
66
+ 'text-sm',
67
+ 'whitespace-nowrap',
68
+ ],
69
+ [TableHeaderCellScope.ROW]: [
70
+ 'px-3',
71
+ 'py-3.5',
72
+ 'border-t',
73
+ 'border-border-neutral-subtle',
74
+ 'text-sm',
75
+ props.fitToContent ? 'w-[1%]' : 'w-auto',
76
+ props.to ? 'hover:cursor-pointer' : undefined
77
+ ],
78
+ }
79
+ return variants[props.scope as TableHeaderCellScope] || 'rounded'
80
+ })
81
+
82
+ // Navigation handler
83
+ const handleNavigation = () => {
84
+ if (props.to) {
85
+ navigateTo(props.to)
86
+ }
87
+ }
50
88
  </script>
@@ -0,0 +1,6 @@
1
+ export enum Direction {
2
+ LEFT = 'left',
3
+ RIGHT = 'right',
4
+ TOP = 'top',
5
+ BOTTOM = 'bottom'
6
+ }
@@ -0,0 +1,4 @@
1
+ export enum TableHeaderCellScope {
2
+ COL = 'col',
3
+ ROW = 'row',
4
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imaginario27/air-ui-ds",
3
- "version": "1.2.3",
3
+ "version": "1.2.5",
4
4
  "author": "imaginario27",
5
5
  "type": "module",
6
6
  "homepage": "https://air-ui.netlify.app/",