@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.
- package/components/navigation/nav-sidebar/NavSidebar.vue +254 -33
- package/components/navigation/nav-sidebar/NavSidebarMenu.vue +11 -2
- package/components/navigation/nav-sidebar/NavSidebarMenuItem.vue +78 -15
- package/components/navigation/nav-sidebar/NavSidebarMenuSectionTitle.vue +14 -1
- package/components/progress/ProgressBar.vue +194 -0
- package/composables/useSidebar.ts +41 -0
- package/models/enums/navigation.ts +1 -1
- package/models/enums/progress.ts +7 -0
- package/models/types/navigation.ts +1 -0
- package/package.json +1 -1
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
<!-- Nav Sidebar -->
|
|
3
3
|
<aside
|
|
4
4
|
ref="sidebarRef"
|
|
5
|
-
:style="
|
|
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
|
-
<!--
|
|
20
|
-
<
|
|
21
|
-
v-if="
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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="
|
|
17
|
-
:icon
|
|
41
|
+
v-if="showDropdownArrow && !isCollapsed"
|
|
42
|
+
:icon="isOpen ? 'mdiChevronUp' : 'mdiChevronDown'"
|
|
18
43
|
preserveAspectRatio="xMidYMid meet"
|
|
19
|
-
:class="['text-icon-
|
|
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]: '
|
|
60
|
-
[SidebarNavMenuItemStyleType.COMPACT]: 'gap-2
|
|
110
|
+
[SidebarNavMenuItemStyleType.SPACED]: 'gap-3',
|
|
111
|
+
[SidebarNavMenuItemStyleType.COMPACT]: 'gap-2',
|
|
61
112
|
}
|
|
62
|
-
return variant[props.styleType as SidebarNavMenuItemStyleType] || '
|
|
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
|
-
|
|
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
|
+
}
|