@imaginario27/air-ui-ds 1.0.20 → 1.0.22

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.
@@ -2,10 +2,12 @@
2
2
  <!-- Nav Sidebar -->
3
3
  <aside
4
4
  ref="sidebarRef"
5
- :style="stickOnScroll && { top: isSticky ? '0px' : `${stickyScrollHeight}px` }"
5
+ :style="{
6
+ top: stickOnScroll ? (isSticky ? '0px' : `${stickyScrollHeight}px`) : undefined,
7
+ width: `${isCollapsed ? collapsedWidth : expandedWidth}px`,
8
+ }"
6
9
  :class="[
7
10
  isFixed && 'fixed',
8
- 'w-[240px]',
9
11
  'h-screen',
10
12
  'bg-background-surface',
11
13
  'flex flex-col items-center gap-6',
@@ -13,52 +15,155 @@
13
15
  'border-r border-border-default',
14
16
  'transition-transform duration-300',
15
17
  isMobileSidebarOpen ? 'translate-x-0' : '-translate-x-full',
16
- 'lg:translate-x-0' // Always visible on large screens
18
+ 'lg:translate-x-0', // Always visible on large screens
17
19
  ]"
18
20
  >
19
- <!-- Only for small screen width -->
20
- <ActionIconButton
21
- v-if="hasCloseButton"
22
- icon="mdiClose"
23
- class="absolute top-3 right-3 sm:hidden"
24
- :size="ButtonSize.SM"
25
- @click="toggleMobileSidebar()"
26
- />
27
-
21
+ <!-- Collapse & Close Buttons -->
22
+ <div
23
+ v-if="showMobileSidebarClose || (showCollapseToggle && collapseTogglePosition === Position.TOP)"
24
+ :class="[
25
+ 'absolute',
26
+ 'top-3',
27
+ 'right-3',
28
+ ]"
29
+ >
30
+ <div class="flex gap-2">
31
+ <!-- Mobile sidebar toggler -->
32
+ <ActionIconButton
33
+ v-if="showMobileSidebarClose && isMobile && !isCollapsed"
34
+ :icon="mobileSidebarCloseIcon"
35
+ class="flex sm:hidden"
36
+ :size="ButtonSize.SM"
37
+ @click="toggleMobileSidebar()"
38
+ />
39
+
40
+ <!-- Collapse toggle button -->
41
+ <ActionIconButton
42
+ v-if="showCollapseToggle && collapseTogglePosition === Position.TOP && !isCollapsed"
43
+ :icon="expandedStateIcon"
44
+ :size="ButtonSize.SM"
45
+ @click="toggleSidebarState(sidebarId)"
46
+ />
47
+ </div>
48
+ </div>
49
+
28
50
  <!-- Header -->
29
51
  <slot name="sidebar-header" />
30
52
 
31
53
  <!-- Menu -->
32
-
33
54
  <NavSidebarMenu
34
- :class="[!$slots['sidebar-footer'] && '80% lg:90%']"
55
+ :isCollapsed
56
+ :class="[
57
+ !$slots['sidebar-footer'] && '80% lg:90%',
58
+ isCollapsed && '!px-0 items-center',
59
+ ]"
35
60
  :style="{
36
61
  height: computedMenuHeight
37
62
  }"
38
63
  >
64
+ <!-- Collapse toggle item shown only in collapsed state -->
65
+ <ActionIconButton
66
+ v-if="showCollapseToggle && collapseTogglePosition === Position.TOP && isCollapsed"
67
+ :icon="collapsedStateIcon"
68
+ :size="ButtonSize.SM"
69
+ class="mb-2"
70
+ @click="toggleSidebarState(sidebarId)"
71
+ />
72
+
39
73
  <slot name="sidebar-menu-prefix-content" />
40
- <template
41
- v-for="(item, index) in menuItems"
42
- :key="index"
43
- >
74
+
75
+ <template v-for="(item, index) in menuItems" :key="index">
76
+ <!-- Section Title -->
44
77
  <NavSidebarMenuSectionTitle
45
- v-if="item.isSectionTitle"
46
- :text="item.text"
47
- :icon="item.icon"
48
- :styleType="itemsStyleType"
49
- />
50
- <NavSidebarMenuItem
51
- v-else
78
+ v-if="item.isSectionTitle"
52
79
  :text="item.text"
53
80
  :icon="item.icon"
54
- :to="item.to"
55
81
  :styleType="itemsStyleType"
56
- :class="itemsCustomClass"
82
+ :isCollapsed
83
+ :showCollapseDivider
57
84
  />
85
+
86
+ <!-- Collapsed Dropdown using NavSidebarMenuItem as activator -->
87
+ <template v-else-if="isCollapsed && item.children">
88
+ <DropdownMenu
89
+ :key="item.text"
90
+ :position="index < props.collapsedFlipLimit ? DropdownPosition.RIGHT_TOP : DropdownPosition.RIGHT_BOTTOM"
91
+ :positionXOffset="collapsedSubmenuOffset"
92
+ :style="{ minWidth: `${collapsedSubmenuWidth}px` }"
93
+ >
94
+ <!-- Use NavSidebarMenuItem as activator -->
95
+ <template #activator="{ onClick }">
96
+ <NavSidebarMenuItem
97
+ :text="item.text"
98
+ :icon="item.icon"
99
+ :styleType="itemsStyleType"
100
+ isCollapsed
101
+ :showDropdownArrow="false"
102
+ :class="itemsCustomClass"
103
+ @click="onClick"
104
+ />
105
+ </template>
106
+
107
+ <template #items>
108
+ <DropdownMenuItem
109
+ v-for="(child, childIndex) in item.children"
110
+ :key="`${item.text}-${childIndex}`"
111
+ :text="child.text"
112
+ :type="child.icon ? DropdownItemType.ICON : DropdownItemType.TEXT"
113
+ :icon="child.icon"
114
+ :to="child.to"
115
+ />
116
+ </template>
117
+ </DropdownMenu>
118
+ </template>
119
+
120
+ <!-- Regular item if not collapsed or no children -->
121
+ <template v-else>
122
+ <NavSidebarMenuItem
123
+ :text="item.text"
124
+ :icon="item.icon"
125
+ :to="item.to"
126
+ :styleType="itemsStyleType"
127
+ :showDropdownArrow="!!item.children"
128
+ :isOpen="openItems[index]"
129
+ :isCollapsed
130
+ :class="itemsCustomClass"
131
+ @click="item.children ? toggleItem(index) : undefined"
132
+ />
133
+
134
+ <!-- Render nested children only when expanded -->
135
+ <template v-if="item.children && openItems[index] && !isCollapsed">
136
+ <NavSidebarMenuItem
137
+ v-for="(child, childIndex) in item.children"
138
+ :key="`${index}-${childIndex}`"
139
+ :text="child.text"
140
+ :icon="child.icon"
141
+ :to="child.to"
142
+ :styleType="itemsStyleType"
143
+ :class="[
144
+ itemsCustomClass,
145
+ 'ml-4',
146
+ '!font-medium',
147
+ ]"
148
+ />
149
+ </template>
150
+ </template>
58
151
  </template>
152
+
59
153
  <slot name="sidebar-menu-suffix-content" />
60
154
  </NavSidebarMenu>
61
155
 
156
+ <!-- Collapse toggle button at bottom -->
157
+ <ActionIconButton
158
+ v-if="showCollapseToggle && collapseTogglePosition === Position.BOTTOM"
159
+ :icon="isCollapsed ? collapsedStateIcon : expandedStateIcon"
160
+ :size="ButtonSize.SM"
161
+ :class="[
162
+ isCollapsed ? '' : 'absolute right-3 bottom-3'
163
+ ]"
164
+ @click="toggleSidebarState(sidebarId)"
165
+ />
166
+
62
167
  <!-- Footer -->
63
168
  <div
64
169
  v-if="$slots['sidebar-footer']"
@@ -76,6 +181,10 @@
76
181
  <script setup lang="ts">
77
182
  // Props
78
183
  const props = defineProps({
184
+ sidebarId: {
185
+ type: String as PropType<string>,
186
+ required: true,
187
+ },
79
188
  menuItems: {
80
189
  type: Array as PropType<SidebarMenuItem[]>,
81
190
  default: () => [
@@ -97,15 +206,78 @@ const props = defineProps({
97
206
  {
98
207
  text: 'Item 3',
99
208
  icon: 'mdiHelp',
100
- to: '/',
209
+ children: [
210
+ {
211
+ text: 'Subitem 1',
212
+ icon: 'mdiHelp',
213
+ to: '/',
214
+ },
215
+ {
216
+ text: 'Subitem 2',
217
+ icon: 'mdiHelp',
218
+ to: '/',
219
+ },
220
+ ],
101
221
  },
102
222
  ],
103
-
104
223
  },
105
- hasCloseButton: {
224
+ expandedWidth: {
225
+ type: Number as PropType<number>,
226
+ default: 240
227
+ },
228
+ collapsedWidth: {
229
+ type: Number as PropType<number>,
230
+ default: 60
231
+ },
232
+ multipleSubmenusOpen: {
233
+ type: Boolean as PropType<boolean>,
234
+ default: false,
235
+ },
236
+ isCollapsed: {
237
+ type: Boolean as PropType<boolean>,
238
+ default: false,
239
+ },
240
+ showCollapseDivider: {
241
+ type: Boolean as PropType<boolean>,
242
+ default: false,
243
+ },
244
+ collapsedSubmenuOffset: {
245
+ type: Number as PropType<number>,
246
+ default: 20,
247
+ },
248
+ collapsedSubmenuWidth: {
249
+ type: Number as PropType<number>,
250
+ default: 200,
251
+ },
252
+ collapsedFlipLimit: {
253
+ type: Number as PropType<number>,
254
+ default: 8,
255
+ },
256
+ showCollapseToggle: {
257
+ type: Boolean as PropType<boolean>,
258
+ default: false,
259
+ },
260
+ collapseTogglePosition: {
261
+ type: String as PropType<Position>,
262
+ default: Position.BOTTOM,
263
+ validator: (value: Position) => Object.values(Position).includes(value),
264
+ },
265
+ collapsedStateIcon: {
266
+ type: String as PropType<string>,
267
+ default: 'mdiMenuClose',
268
+ },
269
+ expandedStateIcon: {
270
+ type: String as PropType<string>,
271
+ default: 'mdiMenuOpen',
272
+ },
273
+ showMobileSidebarClose: {
106
274
  type: Boolean as PropType<boolean>,
107
275
  default: false,
108
276
  },
277
+ mobileSidebarCloseIcon: {
278
+ type: String as PropType<string>,
279
+ default: 'mdiClose',
280
+ },
109
281
  isFixed: {
110
282
  type: Boolean as PropType<boolean>,
111
283
  default: true,
@@ -127,19 +299,32 @@ const props = defineProps({
127
299
  type: Number as PropType<number>,
128
300
  default: 180,
129
301
  },
130
- itemsStyleType: String as PropType<SidebarNavMenuItemStyleType>,
302
+ itemsStyleType: {
303
+ type: String as PropType<SidebarNavMenuItemStyleType>,
304
+ default: SidebarNavMenuItemStyleType.COMPACT,
305
+ validator: (value: SidebarNavMenuItemStyleType) => Object.values(SidebarNavMenuItemStyleType).includes(value),
306
+ },
131
307
  itemsCustomClass: String as PropType<string>
132
308
  })
133
309
 
134
310
  // States
135
- const sidebarRef = ref<HTMLElement | null>(null)
136
311
  const isSticky = ref(false)
312
+ const openItems = ref<Record<number, boolean>>({})
313
+
314
+ // Emits
315
+ const emit = defineEmits<(e: 'update:isCollapsed', value: boolean) => void>()
137
316
 
138
317
  // Slots
139
318
  const slots = defineSlots()
140
319
 
141
320
  // Composables
142
321
  const { isMobileSidebarOpen, toggleMobileSidebar } = useMobileSidebar()
322
+ const {
323
+ isSidebarCollapsed,
324
+ setSidebarCollapsed,
325
+ toggleSidebarState,
326
+ } = useSidebar()
327
+ const { isMobile } = useIsMobile()
143
328
 
144
329
  // Methods
145
330
  const handleScroll = () => {
@@ -148,7 +333,23 @@ const handleScroll = () => {
148
333
  }
149
334
  }
150
335
 
151
- // Dynamic height computation
336
+ const toggleItem = (index: number) => {
337
+ if (props.multipleSubmenusOpen) {
338
+ openItems.value[index] = !openItems.value[index]
339
+ } else {
340
+ const wasOpen = openItems.value[index]
341
+
342
+ // Close all
343
+ openItems.value = {}
344
+
345
+ // Reopen the one that was toggled if it wasn't already open
346
+ if (!wasOpen) {
347
+ openItems.value[index] = true
348
+ }
349
+ }
350
+ }
351
+
352
+ // Computed
152
353
  const computedMenuHeight = computed(() => {
153
354
  if (slots['sidebar-footer']) {
154
355
  return `calc(100% - ${props.footerSafeAreaHeight + props.headerHeight}px)`
@@ -157,6 +358,9 @@ const computedMenuHeight = computed(() => {
157
358
  }
158
359
  })
159
360
 
361
+ // Constants
362
+ const isCollapsed = isSidebarCollapsed(props.sidebarId)
363
+
160
364
  onMounted(() => {
161
365
  if(props.stickOnScroll) {
162
366
  window.addEventListener('scroll', handleScroll)
@@ -169,4 +373,21 @@ onUnmounted(() => {
169
373
  window.removeEventListener('scroll', handleScroll)
170
374
  }
171
375
  })
376
+
377
+ // Sync collapsed props with global sidebar state
378
+ watch(
379
+ () => props.isCollapsed,
380
+ (newVal) => {
381
+ setSidebarCollapsed(props.sidebarId, newVal)
382
+ },
383
+ { immediate: true }
384
+ )
385
+
386
+ // Update parent when local isCollapsed changes
387
+ watch(
388
+ isCollapsed,
389
+ (newVal) => {
390
+ emit('update:isCollapsed', newVal)
391
+ }
392
+ )
172
393
  </script>
@@ -6,9 +6,18 @@
6
6
  'gap-1',
7
7
  'w-full',
8
8
  'px-6',
9
- 'overflow-y-auto',
9
+ !isCollapsed && 'overflow-y-auto',
10
10
  ]"
11
11
  >
12
12
  <slot />
13
13
  </nav>
14
- </template>
14
+ </template>
15
+ <script setup lang="ts">
16
+ // Props
17
+ defineProps({
18
+ isCollapsed: {
19
+ type: Boolean as PropType<boolean>,
20
+ default: false,
21
+ },
22
+ })
23
+ </script>
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <NuxtLink
3
- :to="to"
3
+ :to
4
4
  :class="[
5
5
  'flex',
6
6
  'items-center',
@@ -8,19 +8,41 @@
8
8
  'font-semibold',
9
9
  'rounded-lg',
10
10
  'hover:bg-background-neutral-hover',
11
+ 'justify-between',
11
12
  spacingClass,
12
13
  isActive && 'text-text-primary-brand-on-neutral-hover-bg bg-background-neutral-hover',
13
14
  ]"
15
+ @click="$emit('click')"
14
16
  >
17
+ <div
18
+ :class="[
19
+ 'w-full',
20
+ 'flex',
21
+ 'items-center',
22
+ gapClass,
23
+ ]"
24
+ >
25
+ <MdiIcon
26
+ v-if="icon"
27
+ :icon
28
+ preserveAspectRatio="xMidYMid meet"
29
+ :class="['text-icon-neutral-subtler', iconSizeClass]"
30
+ />
31
+
32
+ <span
33
+ v-if="!isCollapsed"
34
+ :class="[!isActive && 'text-text-default']"
35
+ >
36
+ {{ text }}
37
+ </span>
38
+ </div>
39
+
15
40
  <MdiIcon
16
- v-if="icon"
17
- :icon
41
+ v-if="showDropdownArrow && !isCollapsed"
42
+ :icon="isOpen ? 'mdiChevronUp' : 'mdiChevronDown'"
18
43
  preserveAspectRatio="xMidYMid meet"
19
- :class="['text-icon-neutral-subtler', iconSizeClass]"
44
+ :class="['text-icon-default', iconSizeClass]"
20
45
  />
21
- <span :class="[!isActive && 'text-text-default']">
22
- {{ text }}
23
- </span>
24
46
  </NuxtLink>
25
47
  </template>
26
48
  <script setup lang="ts">
@@ -35,38 +57,79 @@ const props = defineProps({
35
57
  type: String as PropType<string>,
36
58
  default: null
37
59
  },
38
- isActive: {
39
- type: Boolean as PropType<boolean>,
40
- default: false
41
- },
42
60
  styleType: {
43
61
  type: String as PropType<SidebarNavMenuItemStyleType>,
44
62
  default: SidebarNavMenuItemStyleType.COMPACT,
45
63
  validator: (value: SidebarNavMenuItemStyleType) => Object.values(SidebarNavMenuItemStyleType).includes(value),
46
64
  },
47
65
  detectActive: {
48
- type: Boolean,
66
+ type: Boolean as PropType<boolean>,
49
67
  default: true,
50
68
  },
69
+ showDropdownArrow: {
70
+ type: Boolean as PropType<boolean>,
71
+ default: false,
72
+ },
73
+ isOpen: {
74
+ type: Boolean as PropType<boolean>,
75
+ default: false,
76
+ },
77
+ isCollapsed: {
78
+ type: Boolean as PropType<boolean>,
79
+ default: false,
80
+ },
51
81
  })
52
82
 
83
+ // Emits
84
+ defineEmits(['click'])
85
+
53
86
  // Composables
54
87
  const route = useRoute()
55
88
 
56
89
  // Computed classes
57
90
  const spacingClass = computed(() => {
91
+ if (props.isCollapsed) {
92
+ const collapsedVariant = {
93
+ [SidebarNavMenuItemStyleType.SPACED]: 'p-3',
94
+ [SidebarNavMenuItemStyleType.COMPACT]: 'p-2',
95
+ }
96
+
97
+ return collapsedVariant[props.styleType as SidebarNavMenuItemStyleType] || 'p-2'
98
+ }
99
+
100
+ const variant = {
101
+ [SidebarNavMenuItemStyleType.SPACED]: 'min-h-[40px] py-2 pl-3 pr-2',
102
+ [SidebarNavMenuItemStyleType.COMPACT]: 'py-1 pl-2 pr-1',
103
+ }
104
+
105
+ return variant[props.styleType as SidebarNavMenuItemStyleType] || 'min-h-[40px] py-2 pl-3 pr-2'
106
+ })
107
+
108
+ const gapClass = computed(() => {
58
109
  const variant = {
59
- [SidebarNavMenuItemStyleType.SPACED]: 'min-h-[40px] gap-3 py-2 pl-3 pr-2',
60
- [SidebarNavMenuItemStyleType.COMPACT]: 'gap-2 py-1 pl-2 pr-1',
110
+ [SidebarNavMenuItemStyleType.SPACED]: 'gap-3',
111
+ [SidebarNavMenuItemStyleType.COMPACT]: 'gap-2',
61
112
  }
62
- return variant[props.styleType as SidebarNavMenuItemStyleType] || 'min-h-[40px] gap-2 py-2 pl-3 pr-2'
113
+ return variant[props.styleType as SidebarNavMenuItemStyleType] || 'gap-3'
63
114
  })
64
115
 
116
+
65
117
  const iconSizeClass = computed(() => {
118
+ if (props.isCollapsed) {
119
+ const collapsedVariant = {
120
+ [SidebarNavMenuItemStyleType.SPACED]: 'w-[24px] h-[24px] min-w-[24px] min-h-[24px]',
121
+ [SidebarNavMenuItemStyleType.COMPACT]: 'w-[20px] h-[20px] min-w-[20px] min-h-[20px]',
122
+ }
123
+
124
+ return collapsedVariant[props.styleType as SidebarNavMenuItemStyleType] || 'w-[20px] h-[20px] min-w-[20px] min-h-[20px]'
125
+ }
126
+
127
+ // Regular size
66
128
  const variant = {
67
129
  [SidebarNavMenuItemStyleType.SPACED]: 'w-[20px] h-[20px] min-w-[20px] min-h-[20px]',
68
130
  [SidebarNavMenuItemStyleType.COMPACT]: 'w-[16px] h-[16px] min-w-[16px] min-h-[16px]',
69
131
  }
132
+
70
133
  return variant[props.styleType as SidebarNavMenuItemStyleType] || 'w-[20px] h-[20px] min-w-[20px] min-h-[20px]'
71
134
  })
72
135
 
@@ -1,5 +1,6 @@
1
1
  <template>
2
2
  <div
3
+ v-if="!isCollapsed"
3
4
  :class="[
4
5
  'flex',
5
6
  'items-center',
@@ -17,8 +18,12 @@
17
18
  preserveAspectRatio="xMidYMid meet"
18
19
  :class="['text-icon-neutral-subtle', iconSizeClass]"
19
20
  />
20
- {{ text }}
21
+
22
+ <span>
23
+ {{ text }}
24
+ </span>
21
25
  </div>
26
+ <Divider v-if="isCollapsed && showCollapseDivider" />
22
27
  </template>
23
28
  <script setup lang="ts">
24
29
  // Props
@@ -33,6 +38,14 @@ const props = defineProps({
33
38
  default: SidebarNavMenuItemStyleType.COMPACT,
34
39
  validator: (value: SidebarNavMenuItemStyleType) => Object.values(SidebarNavMenuItemStyleType).includes(value),
35
40
  },
41
+ isCollapsed: {
42
+ type: Boolean as PropType<boolean>,
43
+ default: false,
44
+ },
45
+ showCollapseDivider: {
46
+ type: Boolean as PropType<boolean>,
47
+ default: true,
48
+ },
36
49
  })
37
50
 
38
51
  // Computed classes
@@ -0,0 +1,194 @@
1
+ <template>
2
+ <div
3
+ class="w-full space-y-1.5"
4
+ >
5
+ <div
6
+ v-if="showProgressLabel && progressLabelPosition === Position.TOP"
7
+ :class="[
8
+ 'text-xs text-text-neutral-subtle',
9
+ labelAlignmentClass,
10
+ progressLabelClass,
11
+ ]"
12
+ >
13
+ {{ isIndeterminate ? loadingText : `${normalizedProgress}%` }}
14
+ </div>
15
+
16
+ <div
17
+ :class="[
18
+ 'w-full',
19
+ 'overflow-hidden',
20
+ barSizeClass,
21
+ incompleteBackgroundColorClass,
22
+ isRounded && 'rounded-full',
23
+ 'relative',
24
+ ]"
25
+ >
26
+ <div
27
+ v-if="!isIndeterminate && normalizedProgress > 0"
28
+ :class="[
29
+ 'h-full',
30
+ 'transition-all',
31
+ 'duration-300',
32
+ 'ease-in-out',
33
+ isRounded && 'rounded-full',
34
+ completedBackgroundColorClass,
35
+ progressClass,
36
+ ]"
37
+ :style="{ width: `${normalizedProgress}%` }"
38
+ />
39
+
40
+ <div
41
+ v-else-if="isIndeterminate"
42
+ :class="[
43
+ 'absolute top-0 left-0 h-full w-1/3',
44
+ 'indeterminate-bar',
45
+ isRounded && 'rounded-full',
46
+ completedBackgroundColorClass,
47
+ progressClass,
48
+ ]"
49
+ />
50
+ </div>
51
+
52
+ <div
53
+ v-if="showProgressLabel && progressLabelPosition === Position.BOTTOM"
54
+ :class="[
55
+ 'text-xs text-text-neutral-subtle',
56
+ labelAlignmentClass,
57
+ progressLabelClass,
58
+ ]"
59
+ >
60
+ {{ isIndeterminate ? loadingText : `${normalizedProgress}%` }}
61
+ </div>
62
+ </div>
63
+ </template>
64
+ <script setup lang="ts">
65
+ // Props
66
+ const props = defineProps({
67
+ progress: {
68
+ type: Number as PropType<number>,
69
+ default: 50,
70
+ },
71
+ color: {
72
+ type: String as PropType<ColorAccent>,
73
+ default: ColorAccent.PRIMARY_BRAND,
74
+ validator: (value: ColorAccent) => Object.values(ColorAccent).includes(value),
75
+ },
76
+ size: {
77
+ type: String as PropType<ProgressBarSize>,
78
+ default: ProgressBarSize.SM,
79
+ validator: (value: ProgressBarSize) => Object.values(ProgressBarSize).includes(value),
80
+ },
81
+ isRounded: {
82
+ type: Boolean as PropType<boolean>,
83
+ default: true,
84
+ },
85
+ showProgressLabel: {
86
+ type: Boolean as PropType<boolean>,
87
+ default: false,
88
+ },
89
+ progressLabelPosition: {
90
+ type: String as PropType<Position>,
91
+ default: Position.TOP,
92
+ validator: (value: Position) => Object.values(Position).includes(value),
93
+ },
94
+ progressLabelAlignment: {
95
+ type: String as PropType<Align>,
96
+ default: Align.CENTER,
97
+ validator: (value: Align) => Object.values(Align).includes(value),
98
+ },
99
+ min: {
100
+ type: Number as PropType<number>,
101
+ default: 0,
102
+ },
103
+ max: {
104
+ type: Number as PropType<number>,
105
+ default: 100,
106
+ },
107
+ isIndeterminate: {
108
+ type: Boolean as PropType<boolean>,
109
+ default: false,
110
+ },
111
+ loadingText: {
112
+ type: String as PropType<string>,
113
+ default: 'Loading...',
114
+ },
115
+ progressClass: String as PropType<string>,
116
+ progressLabelClass: String as PropType<string>,
117
+ })
118
+
119
+ // Computed
120
+ const normalizedProgress = computed(() => {
121
+ if (props.isIndeterminate) return 100
122
+
123
+ const clamped = Math.min(Math.max(props.progress, props.min), props.max)
124
+ const percent = ((clamped - props.min) / (props.max - props.min)) * 100
125
+
126
+ return Math.round(percent)
127
+ })
128
+
129
+ // Computed classes
130
+ const barSizeClass = computed(() => {
131
+ const variants = {
132
+ [ProgressBarSize.XS]: 'h-[4px]',
133
+ [ProgressBarSize.SM]: 'h-[8px]',
134
+ [ProgressBarSize.MD]: 'h-[12px]',
135
+ [ProgressBarSize.LG]: 'h-[16px]',
136
+ [ProgressBarSize.XL]: 'h-[20px]',
137
+ }
138
+
139
+ return variants[props.size as ProgressBarSize] || 'h-[8px]'
140
+ })
141
+
142
+ const incompleteBackgroundColorClass = computed(() => {
143
+ const variants = {
144
+ [ColorAccent.NEUTRAL]: 'bg-background-neutral-default/10',
145
+ [ColorAccent.SUCCESS]: 'bg-background-success-bold/10',
146
+ [ColorAccent.WARNING]: 'bg-background-warning-bold/10',
147
+ [ColorAccent.DANGER]: 'bg-background-danger-bold/10',
148
+ [ColorAccent.INFO]: 'bg-background-info-bold/10',
149
+ [ColorAccent.PRIMARY_BRAND]: 'bg-background-primary-brand-default/10',
150
+ [ColorAccent.SECONDARY_BRAND]: 'bg-background-secondary-brand-default/10',
151
+ }
152
+
153
+ return variants[props.color as ColorAccent] || 'bg-background-primary-brand-default/10'
154
+ })
155
+
156
+ const completedBackgroundColorClass = computed(() => {
157
+ const variants = {
158
+ [ColorAccent.NEUTRAL]: 'bg-background-neutral-default',
159
+ [ColorAccent.SUCCESS]: 'bg-background-success-bold',
160
+ [ColorAccent.WARNING]: 'bg-background-warning-bold',
161
+ [ColorAccent.DANGER]: 'bg-background-danger-bold',
162
+ [ColorAccent.INFO]: 'bg-background-info-bold',
163
+ [ColorAccent.PRIMARY_BRAND]: 'bg-background-primary-brand-default',
164
+ [ColorAccent.SECONDARY_BRAND]: 'bg-background-secondary-brand-default',
165
+ }
166
+
167
+ return variants[props.color as ColorAccent] || 'bg-background-primary-brand-default'
168
+ })
169
+
170
+ const labelAlignmentClass = computed(() => {
171
+ const variants = {
172
+ [Align.LEFT]: 'text-left',
173
+ [Align.CENTER]: 'text-center',
174
+ [Align.RIGHT]: 'text-right',
175
+ }
176
+
177
+ return variants[props.progressLabelAlignment as Align] || 'text-center'
178
+ })
179
+ </script>
180
+
181
+ <style scoped>
182
+ @keyframes indeterminate-slide {
183
+ 0% {
184
+ transform: translateX(-100%);
185
+ }
186
+ 100% {
187
+ transform: translateX(300%);
188
+ }
189
+ }
190
+
191
+ .indeterminate-bar {
192
+ animation: indeterminate-slide 2s ease-in-out infinite;
193
+ }
194
+ </style>
@@ -0,0 +1,41 @@
1
+ const sidebarStates = reactive<Record<string, boolean>>({})
2
+
3
+ export const useSidebar = () => {
4
+ const { isMobile } = useIsMobile()
5
+
6
+ const toggleSidebarState = (id: string) => {
7
+ sidebarStates[id] = !sidebarStates[id]
8
+ }
9
+
10
+ const expandSidebar = (id: string) => {
11
+ sidebarStates[id] = false
12
+ }
13
+
14
+ const collapseSidebar = (id: string) => {
15
+ sidebarStates[id] = true
16
+ }
17
+
18
+ const setSidebarCollapsed = (id: string, collapsed: boolean) => {
19
+ sidebarStates[id] = collapsed
20
+ }
21
+
22
+ const isSidebarCollapsed = (id: string) => {
23
+ return computed(() => sidebarStates[id] ?? false)
24
+ }
25
+
26
+ watch(isMobile, (newVal) => {
27
+ if (!newVal) {
28
+ for (const key in sidebarStates) {
29
+ sidebarStates[key] = false
30
+ }
31
+ }
32
+ })
33
+
34
+ return {
35
+ isSidebarCollapsed,
36
+ setSidebarCollapsed,
37
+ toggleSidebarState,
38
+ expandSidebar,
39
+ collapseSidebar,
40
+ }
41
+ }
@@ -9,5 +9,5 @@ export enum NavLinkSize {
9
9
 
10
10
  export enum SidebarNavMenuItemStyleType {
11
11
  SPACED = 'spaced',
12
- COMPACT = 'sidebar',
12
+ COMPACT = 'compact',
13
13
  }
@@ -0,0 +1,7 @@
1
+ export enum ProgressBarSize {
2
+ XS = 'xs',
3
+ SM = 'sm',
4
+ MD = 'md',
5
+ LG = 'lg',
6
+ XL = 'xl'
7
+ }
@@ -14,4 +14,5 @@ export interface SidebarMenuItem {
14
14
  icon?: string
15
15
  to?: string
16
16
  isSectionTitle?: boolean
17
+ children?: SidebarMenuItem[]
17
18
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imaginario27/air-ui-ds",
3
- "version": "1.0.20",
3
+ "version": "1.0.22",
4
4
  "author": "imaginario27",
5
5
  "type": "module",
6
6
  "homepage": "https://air-ui.netlify.app/",