@imaginario27/air-ui-ds 1.10.2 → 1.11.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 +24 -0
- package/components/dropdowns/DropdownMenu.vue +159 -37
- package/components/dropdowns/DropdownMenuItem.vue +10 -0
- package/components/dropdowns/DropdownSectionItem.vue +28 -0
- package/components/dropdowns/DropdownSelect.vue +33 -23
- package/components/navigation/nav-sidebar/NavSidebar.vue +167 -83
- package/components/navigation/nav-sidebar/NavSidebarMenuItem.vue +39 -5
- package/components/navigation/nav-sidebar/NavSidebarMenuItemsTree.vue +202 -0
- package/components/navigation/nav-sidebar/NavSidebarMenuSectionTitle.vue +75 -6
- package/models/types/dropdowns.ts +18 -0
- package/models/types/selects.ts +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,30 @@ 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.10.2 - 2026-04-14
|
|
9
|
+
|
|
10
|
+
Release type: patch.
|
|
11
|
+
Commits found in range: 3.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
1. fix typecheck errors in selects ([eee12f8](https://github.com/imaginario27/air-ui/commit/eee12f87045fcb12556f3f38e80f87e5078a0d5c))
|
|
16
|
+
2. remove duplicated imports ([a277959](https://github.com/imaginario27/air-ui/commit/a2779590034a360e1685193455e978391870a69f))
|
|
17
|
+
3. fix duplicated imports ([5a324a3](https://github.com/imaginario27/air-ui/commit/5a324a3ea43ae5fdf794e345b54c57207860efbd))
|
|
18
|
+
|
|
19
|
+
- Package: @imaginario27/air-ui-ds.
|
|
20
|
+
|
|
21
|
+
## 1.10.1 - 2026-04-09
|
|
22
|
+
|
|
23
|
+
Release type: patch.
|
|
24
|
+
Commits found in range: 1.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
1. fix TypeScript errors in TabBar.vue ([273925b](https://github.com/imaginario27/air-ui/commit/273925bdca00ac9ad58c1c43de5668ed78a85da1))
|
|
29
|
+
|
|
30
|
+
- Package: @imaginario27/air-ui-ds.
|
|
31
|
+
|
|
8
32
|
## 1.10.0 - 2026-04-09
|
|
9
33
|
|
|
10
34
|
Release type: minor.
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
<div
|
|
33
33
|
v-if="isOpen"
|
|
34
34
|
ref="dropdown"
|
|
35
|
+
data-dropdown-menu-panel
|
|
35
36
|
v-bind="$attrs"
|
|
36
37
|
:class="[
|
|
37
38
|
'bg-background-surface',
|
|
@@ -55,26 +56,70 @@
|
|
|
55
56
|
:onClose="close"
|
|
56
57
|
/>
|
|
57
58
|
<template v-else-if="items?.length">
|
|
58
|
-
<
|
|
59
|
+
<template
|
|
59
60
|
v-for="(item, index) in items"
|
|
60
61
|
:key="index"
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
62
|
+
>
|
|
63
|
+
<DropdownSectionItem
|
|
64
|
+
v-if="item.sectionTitle"
|
|
65
|
+
:text="item.text"
|
|
66
|
+
:icon="item.icon"
|
|
67
|
+
/>
|
|
68
|
+
|
|
69
|
+
<DropdownMenu
|
|
70
|
+
v-else-if="canOpenNested(item)"
|
|
71
|
+
:items="item.children"
|
|
72
|
+
:position="DropdownPosition.RIGHT_TOP"
|
|
73
|
+
:positionXOffset="nestedMenuGap"
|
|
74
|
+
:positionYOffset="0"
|
|
75
|
+
:trigger="trigger"
|
|
76
|
+
:hasShadow
|
|
77
|
+
:hasBorder
|
|
78
|
+
:disabled="disabled || item.disabled"
|
|
79
|
+
:prefetchOn
|
|
80
|
+
:level="level + 1"
|
|
81
|
+
>
|
|
82
|
+
<template #activator>
|
|
83
|
+
<DropdownMenuItem
|
|
84
|
+
:actionType="DropdownActionType.ACTION"
|
|
85
|
+
:text="item.text"
|
|
86
|
+
:icon="item.icon"
|
|
87
|
+
:size="item.size"
|
|
88
|
+
:type="item.type"
|
|
89
|
+
:userDisplayName="item.userDisplayName"
|
|
90
|
+
:userProfileImg="item.userProfileImg"
|
|
91
|
+
:imgUrl="item.imgUrl"
|
|
92
|
+
:alt="item.alt"
|
|
93
|
+
:helpText="item.helpText"
|
|
94
|
+
:hasSeparator="item.hasSeparator"
|
|
95
|
+
:disabled="disabled || item.disabled"
|
|
96
|
+
:hasNestedLevels="true"
|
|
97
|
+
:prefetchOn
|
|
98
|
+
/>
|
|
99
|
+
</template>
|
|
100
|
+
</DropdownMenu>
|
|
101
|
+
|
|
102
|
+
<DropdownMenuItem
|
|
103
|
+
v-else
|
|
104
|
+
:actionType="resolveItemActionType(item)"
|
|
105
|
+
:text="item.text"
|
|
106
|
+
:icon="item.icon"
|
|
107
|
+
:size="item.size"
|
|
108
|
+
:type="item.type"
|
|
109
|
+
:userDisplayName="item.userDisplayName"
|
|
110
|
+
:userProfileImg="item.userProfileImg"
|
|
111
|
+
:imgUrl="item.imgUrl"
|
|
112
|
+
:alt="item.alt"
|
|
113
|
+
:helpText="item.helpText"
|
|
114
|
+
:to="item.to"
|
|
115
|
+
:isExternal="item.isExternal"
|
|
116
|
+
:hasSeparator="item.hasSeparator"
|
|
117
|
+
:disabled="disabled || item.disabled"
|
|
118
|
+
:hasNestedLevels="hasNestedItems(item)"
|
|
119
|
+
:prefetchOn
|
|
120
|
+
@click="handleClick(item.callback)"
|
|
121
|
+
/>
|
|
122
|
+
</template>
|
|
78
123
|
</template>
|
|
79
124
|
</div>
|
|
80
125
|
</Transition>
|
|
@@ -91,6 +136,7 @@
|
|
|
91
136
|
<div
|
|
92
137
|
v-if="isOpen"
|
|
93
138
|
ref="dropdown"
|
|
139
|
+
data-dropdown-menu-panel
|
|
94
140
|
v-bind="$attrs"
|
|
95
141
|
:class="[
|
|
96
142
|
'bg-background-surface',
|
|
@@ -116,25 +162,70 @@
|
|
|
116
162
|
:onClose="toggle"
|
|
117
163
|
/>
|
|
118
164
|
<template v-else-if="items?.length && !$slots['items']">
|
|
119
|
-
<
|
|
120
|
-
v-for="(item, index) in items"
|
|
121
|
-
:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
165
|
+
<template
|
|
166
|
+
v-for="(item, index) in items"
|
|
167
|
+
:key="index"
|
|
168
|
+
>
|
|
169
|
+
<DropdownSectionItem
|
|
170
|
+
v-if="item.sectionTitle"
|
|
171
|
+
:text="item.text"
|
|
172
|
+
:icon="item.icon"
|
|
173
|
+
/>
|
|
174
|
+
|
|
175
|
+
<DropdownMenu
|
|
176
|
+
v-else-if="canOpenNested(item)"
|
|
177
|
+
:items="item.children"
|
|
178
|
+
:position="DropdownPosition.RIGHT_TOP"
|
|
179
|
+
:positionXOffset="nestedMenuGap"
|
|
180
|
+
:positionYOffset="0"
|
|
181
|
+
:trigger="trigger"
|
|
182
|
+
:hasShadow
|
|
183
|
+
:hasBorder
|
|
184
|
+
:disabled="disabled || item.disabled"
|
|
185
|
+
:prefetchOn
|
|
186
|
+
:level="level + 1"
|
|
187
|
+
>
|
|
188
|
+
<template #activator>
|
|
189
|
+
<DropdownMenuItem
|
|
190
|
+
:actionType="DropdownActionType.ACTION"
|
|
191
|
+
:text="item.text"
|
|
192
|
+
:icon="item.icon"
|
|
193
|
+
:size="item.size"
|
|
194
|
+
:type="item.type"
|
|
195
|
+
:userDisplayName="item.userDisplayName"
|
|
196
|
+
:userProfileImg="item.userProfileImg"
|
|
197
|
+
:imgUrl="item.imgUrl"
|
|
198
|
+
:alt="item.alt"
|
|
199
|
+
:helpText="item.helpText"
|
|
200
|
+
:hasSeparator="item.hasSeparator"
|
|
201
|
+
:disabled="disabled || item.disabled"
|
|
202
|
+
:hasNestedLevels="true"
|
|
203
|
+
:prefetchOn
|
|
204
|
+
/>
|
|
205
|
+
</template>
|
|
206
|
+
</DropdownMenu>
|
|
207
|
+
|
|
208
|
+
<DropdownMenuItem
|
|
209
|
+
v-else
|
|
210
|
+
:actionType="resolveItemActionType(item)"
|
|
211
|
+
:text="item.text"
|
|
212
|
+
:icon="item.icon"
|
|
213
|
+
:size="item.size"
|
|
214
|
+
:type="item.type"
|
|
215
|
+
:userDisplayName="item.userDisplayName"
|
|
216
|
+
:userProfileImg="item.userProfileImg"
|
|
217
|
+
:imgUrl="item.imgUrl"
|
|
218
|
+
:alt="item.alt"
|
|
219
|
+
:helpText="item.helpText"
|
|
220
|
+
:to="item.to"
|
|
221
|
+
:isExternal="item.isExternal"
|
|
222
|
+
:hasSeparator="item.hasSeparator"
|
|
223
|
+
:disabled="disabled || item.disabled"
|
|
224
|
+
:hasNestedLevels="hasNestedItems(item)"
|
|
225
|
+
:prefetchOn
|
|
226
|
+
@click="handleClick(item.callback)"
|
|
227
|
+
/>
|
|
228
|
+
</template>
|
|
138
229
|
</template>
|
|
139
230
|
</div>
|
|
140
231
|
</Transition>
|
|
@@ -197,6 +288,14 @@ const props = defineProps({
|
|
|
197
288
|
default: Trigger.CLICK,
|
|
198
289
|
validator: (value: Trigger) => Object.values(Trigger).includes(value),
|
|
199
290
|
},
|
|
291
|
+
level: {
|
|
292
|
+
type: Number as PropType<number>,
|
|
293
|
+
default: 1,
|
|
294
|
+
},
|
|
295
|
+
nestedMenuGap: {
|
|
296
|
+
type: Number as PropType<number>,
|
|
297
|
+
default: 8,
|
|
298
|
+
},
|
|
200
299
|
disabled: {
|
|
201
300
|
type: Boolean as PropType<boolean>,
|
|
202
301
|
default: false,
|
|
@@ -224,6 +323,24 @@ const close = () => {
|
|
|
224
323
|
isPositioned.value = false
|
|
225
324
|
}
|
|
226
325
|
|
|
326
|
+
const MAX_NESTED_LEVELS = 3
|
|
327
|
+
|
|
328
|
+
const hasNestedItems = (item: DropdownMenuItem) => {
|
|
329
|
+
return Array.isArray(item.children) && item.children.length > 0
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const canOpenNested = (item: DropdownMenuItem) => {
|
|
333
|
+
return props.level < MAX_NESTED_LEVELS && hasNestedItems(item)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const resolveItemActionType = (item: DropdownMenuItem) => {
|
|
337
|
+
// Items with children are always ACTION items (never links)
|
|
338
|
+
if (hasNestedItems(item)) {
|
|
339
|
+
return DropdownActionType.ACTION
|
|
340
|
+
}
|
|
341
|
+
return item.actionType ?? DropdownActionType.ACTION
|
|
342
|
+
}
|
|
343
|
+
|
|
227
344
|
const clearCloseTimer = () => {
|
|
228
345
|
if (closeTimer.value !== null) {
|
|
229
346
|
globalThis.clearTimeout(closeTimer.value)
|
|
@@ -317,6 +434,11 @@ const shouldCloseFromTarget = (target: Node) => {
|
|
|
317
434
|
|
|
318
435
|
if (!dropdownEl || !activatorEl) return false
|
|
319
436
|
|
|
437
|
+
const targetElement = target as HTMLElement | null
|
|
438
|
+
if (targetElement?.closest('[data-dropdown-menu-panel]')) {
|
|
439
|
+
return false
|
|
440
|
+
}
|
|
441
|
+
|
|
320
442
|
return !dropdownEl.contains(target) && !activatorEl.contains(target)
|
|
321
443
|
}
|
|
322
444
|
|
|
@@ -53,6 +53,12 @@
|
|
|
53
53
|
:imgUrl="userProfileImg"
|
|
54
54
|
:size="AvatarSize.XS"
|
|
55
55
|
/>
|
|
56
|
+
|
|
57
|
+
<Icon
|
|
58
|
+
v-if="hasNestedLevels"
|
|
59
|
+
name="mdi:chevron-right"
|
|
60
|
+
iconClass="text-icon-neutral-subtle ml-auto"
|
|
61
|
+
/>
|
|
56
62
|
</div>
|
|
57
63
|
<p
|
|
58
64
|
v-if="helpText"
|
|
@@ -125,6 +131,10 @@ const props = defineProps({
|
|
|
125
131
|
type: Boolean as PropType<boolean>,
|
|
126
132
|
default: false,
|
|
127
133
|
},
|
|
134
|
+
hasNestedLevels: {
|
|
135
|
+
type: Boolean as PropType<boolean>,
|
|
136
|
+
default: false,
|
|
137
|
+
},
|
|
128
138
|
prefetchOn: {
|
|
129
139
|
type: [String, Object] as PropType<PrefetchOnStrategy>,
|
|
130
140
|
default: PrefetchOn.VISIBILITY,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div
|
|
3
|
+
:class="[
|
|
4
|
+
'flex items-center gap-3 w-full',
|
|
5
|
+
'px-3 py-2',
|
|
6
|
+
'text-sm font-semibold text-text-neutral-subtle',
|
|
7
|
+
'border-b border-border-default',
|
|
8
|
+
'select-none',
|
|
9
|
+
]"
|
|
10
|
+
>
|
|
11
|
+
<Icon
|
|
12
|
+
v-if="icon"
|
|
13
|
+
:name="icon"
|
|
14
|
+
iconClass="!text-icon-neutral-subtle"
|
|
15
|
+
/>
|
|
16
|
+
|
|
17
|
+
<span>{{ text }}</span>
|
|
18
|
+
</div>
|
|
19
|
+
</template>
|
|
20
|
+
<script setup lang="ts">
|
|
21
|
+
defineProps({
|
|
22
|
+
text: {
|
|
23
|
+
type: String as PropType<string>,
|
|
24
|
+
default: 'Section title',
|
|
25
|
+
},
|
|
26
|
+
icon: String as PropType<string>,
|
|
27
|
+
})
|
|
28
|
+
</script>
|
|
@@ -151,31 +151,41 @@
|
|
|
151
151
|
|
|
152
152
|
<!-- Filtered options -->
|
|
153
153
|
<div v-if="filteredOptions.length > 0">
|
|
154
|
-
<
|
|
154
|
+
<template
|
|
155
155
|
v-for="(option, index) in filteredOptions"
|
|
156
156
|
:key="index"
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
157
|
+
>
|
|
158
|
+
<DropdownSectionItem
|
|
159
|
+
v-if="option.sectionTitle"
|
|
160
|
+
:text="option.text"
|
|
161
|
+
:icon="option.icon"
|
|
162
|
+
/>
|
|
163
|
+
|
|
164
|
+
<DropdownSelectItem
|
|
165
|
+
v-else
|
|
166
|
+
:type="type"
|
|
167
|
+
:text="option.text"
|
|
168
|
+
:icon="option.icon"
|
|
169
|
+
:userDisplayName="option.userDisplayName"
|
|
170
|
+
:userProfileImg="option.userProfileImg"
|
|
171
|
+
:imgUrl="option.imgUrl"
|
|
172
|
+
:alt="option.alt"
|
|
173
|
+
:helpText="option.helpText"
|
|
174
|
+
:isSelected="isSelected(option)"
|
|
175
|
+
:activeStyle="activeStyle"
|
|
176
|
+
:to="option.to"
|
|
177
|
+
:isExternal="option.isExternal"
|
|
178
|
+
:class="[
|
|
179
|
+
hasSeparator && index !== filteredOptions.length - 1
|
|
180
|
+
? 'border-b border-border-default'
|
|
181
|
+
: undefined
|
|
182
|
+
]"
|
|
183
|
+
@click="() => {
|
|
184
|
+
handleOptionClick(option)
|
|
185
|
+
if (!multiple) onClose()
|
|
186
|
+
}"
|
|
187
|
+
/>
|
|
188
|
+
</template>
|
|
179
189
|
</div>
|
|
180
190
|
|
|
181
191
|
<!-- No Results Message -->
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
:isCollapsed
|
|
56
56
|
:class="[
|
|
57
57
|
!$slots['sidebar-footer'] && '80% lg:90%',
|
|
58
|
-
isCollapsed && '
|
|
58
|
+
isCollapsed && 'px-0! items-center',
|
|
59
59
|
]"
|
|
60
60
|
:style="{
|
|
61
61
|
height: computedMenuHeight
|
|
@@ -72,93 +72,79 @@
|
|
|
72
72
|
|
|
73
73
|
<slot name="sidebar-menu-prefix-content" />
|
|
74
74
|
|
|
75
|
-
<template v-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
:text="item.text"
|
|
80
|
-
:icon="item.icon"
|
|
81
|
-
:styleType="itemsStyleType"
|
|
82
|
-
:isCollapsed
|
|
83
|
-
:showCollapseDivider
|
|
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
|
-
:trigger="collapsedSubmenuTrigger"
|
|
93
|
-
:style="{ minWidth: `${collapsedSubmenuWidth}px` }"
|
|
94
|
-
>
|
|
95
|
-
<!-- Use NavSidebarMenuItem as activator -->
|
|
96
|
-
<template #activator>
|
|
97
|
-
<NavSidebarMenuItem
|
|
98
|
-
:text="item.text"
|
|
99
|
-
:icon="item.icon"
|
|
100
|
-
:styleType="itemsStyleType"
|
|
101
|
-
:textClass="itemsTextClass"
|
|
102
|
-
:iconClass="itemsIconClass"
|
|
103
|
-
isCollapsed
|
|
104
|
-
:showDropdownArrow="false"
|
|
105
|
-
:class="itemsCustomClass"
|
|
106
|
-
/>
|
|
107
|
-
</template>
|
|
108
|
-
|
|
109
|
-
<template #items>
|
|
110
|
-
<DropdownMenuItem
|
|
111
|
-
v-for="(child, childIndex) in item.children"
|
|
112
|
-
:key="`${item.text}-${childIndex}`"
|
|
113
|
-
:text="child.text"
|
|
114
|
-
:type="child.icon ? DropdownItemType.ICON : DropdownItemType.TEXT"
|
|
115
|
-
:icon="child.icon"
|
|
116
|
-
:to="child.to"
|
|
117
|
-
/>
|
|
118
|
-
</template>
|
|
119
|
-
</DropdownMenu>
|
|
120
|
-
</template>
|
|
121
|
-
|
|
122
|
-
<!-- Regular item if not collapsed or no children -->
|
|
123
|
-
<template v-else>
|
|
124
|
-
<NavSidebarMenuItem
|
|
75
|
+
<template v-if="isCollapsed">
|
|
76
|
+
<template v-for="(item, index) in menuItems" :key="`${item.text}-${index}`">
|
|
77
|
+
<NavSidebarMenuSectionTitle
|
|
78
|
+
v-if="item.isSectionTitle"
|
|
125
79
|
:text="item.text"
|
|
126
80
|
:icon="item.icon"
|
|
127
|
-
:to="item.to"
|
|
128
|
-
:disabled="item.disabled"
|
|
129
|
-
:prefetchOn
|
|
130
81
|
:styleType="itemsStyleType"
|
|
131
|
-
:textClass="itemsTextClass"
|
|
132
|
-
:iconClass="itemsIconClass"
|
|
133
|
-
:showDropdownArrow="!!item.children"
|
|
134
|
-
:isOpen="openItems[index]"
|
|
135
82
|
:isCollapsed
|
|
136
|
-
:
|
|
137
|
-
@click="item.children ? toggleItem(index) : undefined"
|
|
83
|
+
:showCollapseDivider
|
|
138
84
|
/>
|
|
139
85
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
:
|
|
145
|
-
:
|
|
146
|
-
:
|
|
147
|
-
:
|
|
148
|
-
|
|
86
|
+
<template v-else-if="hasChildren(item)">
|
|
87
|
+
<DropdownMenu
|
|
88
|
+
:items="getCollapsedDropdownItems(item.children ?? [])"
|
|
89
|
+
:position="index < props.collapsedFlipLimit ? DropdownPosition.RIGHT_TOP : DropdownPosition.RIGHT_BOTTOM"
|
|
90
|
+
:positionXOffset="collapsedSubmenuOffset"
|
|
91
|
+
:trigger="collapsedSubmenuTrigger"
|
|
92
|
+
:style="{ minWidth: `${collapsedSubmenuWidth}px` }"
|
|
93
|
+
:prefetchOn
|
|
94
|
+
>
|
|
95
|
+
<template #activator>
|
|
96
|
+
<NavSidebarMenuItem
|
|
97
|
+
:text="item.text"
|
|
98
|
+
:icon="item.icon"
|
|
99
|
+
:styleType="itemsStyleType"
|
|
100
|
+
:textClass="itemsTextClass"
|
|
101
|
+
:iconClass="itemsIconClass"
|
|
102
|
+
isCollapsed
|
|
103
|
+
:showDropdownArrow="false"
|
|
104
|
+
:class="itemsCustomClass"
|
|
105
|
+
/>
|
|
106
|
+
</template>
|
|
107
|
+
</DropdownMenu>
|
|
108
|
+
</template>
|
|
109
|
+
|
|
110
|
+
<template v-else>
|
|
111
|
+
<NavSidebarMenuItem
|
|
112
|
+
:text="item.text"
|
|
113
|
+
:icon="item.icon"
|
|
114
|
+
:to="item.to"
|
|
115
|
+
:disabled="item.disabled"
|
|
149
116
|
:prefetchOn
|
|
150
117
|
:styleType="itemsStyleType"
|
|
151
|
-
:textClass="
|
|
152
|
-
:iconClass="
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
]"
|
|
118
|
+
:textClass="itemsTextClass"
|
|
119
|
+
:iconClass="itemsIconClass"
|
|
120
|
+
isCollapsed
|
|
121
|
+
:showDropdownArrow="false"
|
|
122
|
+
:class="itemsCustomClass"
|
|
157
123
|
/>
|
|
158
124
|
</template>
|
|
159
125
|
</template>
|
|
160
126
|
</template>
|
|
161
127
|
|
|
128
|
+
<template v-else>
|
|
129
|
+
<NavSidebarMenuItemsTree
|
|
130
|
+
:items="menuItems"
|
|
131
|
+
:isCollapsed
|
|
132
|
+
:openItems
|
|
133
|
+
:itemsStyleType
|
|
134
|
+
:itemsCustomClass
|
|
135
|
+
:itemsTextClass
|
|
136
|
+
:itemsIconClass
|
|
137
|
+
:subItemsCustomClass
|
|
138
|
+
:subItemsTextClass
|
|
139
|
+
:subItemsIconClass
|
|
140
|
+
:thirdLevelItemsCustomClass
|
|
141
|
+
:prefetchOn
|
|
142
|
+
:showCollapseDivider
|
|
143
|
+
:showNestedLevelGuide="showNestedSectionLevelGuide"
|
|
144
|
+
@toggle="toggleItem"
|
|
145
|
+
/>
|
|
146
|
+
</template>
|
|
147
|
+
|
|
162
148
|
<slot name="sidebar-menu-suffix-content" />
|
|
163
149
|
</NavSidebarMenu>
|
|
164
150
|
|
|
@@ -216,10 +202,26 @@ const props = defineProps({
|
|
|
216
202
|
text: 'Item 3',
|
|
217
203
|
icon: 'mdi:help',
|
|
218
204
|
children: [
|
|
205
|
+
{
|
|
206
|
+
isSectionTitle: true,
|
|
207
|
+
text: 'Subsection title',
|
|
208
|
+
icon: 'mdi:shape-outline',
|
|
209
|
+
},
|
|
219
210
|
{
|
|
220
211
|
text: 'Subitem 1',
|
|
221
212
|
icon: 'mdi:help',
|
|
222
|
-
|
|
213
|
+
children: [
|
|
214
|
+
{
|
|
215
|
+
isSectionTitle: true,
|
|
216
|
+
text: 'Nested subsection title',
|
|
217
|
+
icon: 'mdi:format-list-bulleted-square',
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
text: 'Third level item',
|
|
221
|
+
icon: 'mdi:help-circle-outline',
|
|
222
|
+
to: '/',
|
|
223
|
+
},
|
|
224
|
+
],
|
|
223
225
|
},
|
|
224
226
|
{
|
|
225
227
|
text: 'Subitem 2',
|
|
@@ -324,6 +326,11 @@ const props = defineProps({
|
|
|
324
326
|
subItemsCustomClass: String as PropType<string>,
|
|
325
327
|
subItemsTextClass: String as PropType<string>,
|
|
326
328
|
subItemsIconClass: String as PropType<string>,
|
|
329
|
+
thirdLevelItemsCustomClass: String as PropType<string>,
|
|
330
|
+
showNestedSectionLevelGuide: {
|
|
331
|
+
type: Boolean as PropType<boolean>,
|
|
332
|
+
default: true,
|
|
333
|
+
},
|
|
327
334
|
prefetchOn: {
|
|
328
335
|
type: [String, Object] as PropType<PrefetchOnStrategy>,
|
|
329
336
|
default: PrefetchOn.VISIBILITY,
|
|
@@ -332,7 +339,7 @@ const props = defineProps({
|
|
|
332
339
|
|
|
333
340
|
// States
|
|
334
341
|
const isSticky = ref(false)
|
|
335
|
-
const openItems = ref<Record<
|
|
342
|
+
const openItems = ref<Record<string, boolean>>({})
|
|
336
343
|
|
|
337
344
|
// Emits
|
|
338
345
|
const emit = defineEmits<(e: 'update:isCollapsed', value: boolean) => void>()
|
|
@@ -349,29 +356,106 @@ const {
|
|
|
349
356
|
} = useSidebar()
|
|
350
357
|
const { isMobile } = useIsMobile()
|
|
351
358
|
|
|
359
|
+
const MAX_NESTING_LEVEL = 3
|
|
360
|
+
|
|
352
361
|
// Methods
|
|
362
|
+
const hasChildren = (item: SidebarMenuItem) => {
|
|
363
|
+
return Array.isArray(item.children) && item.children.length > 0
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const getNodeKey = (path: number[]) => {
|
|
367
|
+
return path.join('-')
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const parseNodeKey = (key: string) => {
|
|
371
|
+
return key.split('-').map(Number)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const isSameParent = (a: number[], b: number[]) => {
|
|
375
|
+
if (a.length !== b.length) return false
|
|
376
|
+
|
|
377
|
+
const aParent = a.slice(0, -1)
|
|
378
|
+
const bParent = b.slice(0, -1)
|
|
379
|
+
|
|
380
|
+
return aParent.length === bParent.length && aParent.every((value, index) => value === bParent[index])
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const isPrefixPath = (prefix: number[], path: number[]) => {
|
|
384
|
+
if (prefix.length > path.length) return false
|
|
385
|
+
|
|
386
|
+
return prefix.every((value, index) => value === path[index])
|
|
387
|
+
}
|
|
388
|
+
|
|
353
389
|
const handleScroll = () => {
|
|
354
390
|
if(props.stickOnScroll) {
|
|
355
391
|
isSticky.value = window.scrollY > props.stickyScrollHeight
|
|
356
392
|
}
|
|
357
393
|
}
|
|
358
394
|
|
|
359
|
-
const toggleItem = (
|
|
395
|
+
const toggleItem = (path: number[]) => {
|
|
396
|
+
const nodeKey = getNodeKey(path)
|
|
397
|
+
|
|
360
398
|
if (props.multipleSubmenusOpen) {
|
|
361
|
-
openItems.value[
|
|
399
|
+
openItems.value[nodeKey] = !openItems.value[nodeKey]
|
|
362
400
|
} else {
|
|
363
|
-
const wasOpen = openItems.value[
|
|
401
|
+
const wasOpen = openItems.value[nodeKey]
|
|
402
|
+
|
|
403
|
+
const nextOpenItems = Object.fromEntries(
|
|
404
|
+
Object.entries(openItems.value).filter(([key]) => {
|
|
405
|
+
const keyPath = parseNodeKey(key)
|
|
406
|
+
|
|
407
|
+
// Always close the toggled branch descendants and the branch itself.
|
|
408
|
+
if (isPrefixPath(path, keyPath)) {
|
|
409
|
+
return false
|
|
410
|
+
}
|
|
364
411
|
|
|
365
|
-
|
|
366
|
-
|
|
412
|
+
// Close siblings at the same level to preserve single-open behavior per level.
|
|
413
|
+
if (isSameParent(keyPath, path)) {
|
|
414
|
+
return false
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return true
|
|
418
|
+
})
|
|
419
|
+
)
|
|
367
420
|
|
|
368
421
|
// Reopen the one that was toggled if it wasn't already open
|
|
369
422
|
if (!wasOpen) {
|
|
370
|
-
|
|
423
|
+
nextOpenItems[nodeKey] = true
|
|
371
424
|
}
|
|
425
|
+
|
|
426
|
+
openItems.value = nextOpenItems
|
|
372
427
|
}
|
|
373
428
|
}
|
|
374
429
|
|
|
430
|
+
const getCollapsedDropdownItems = (
|
|
431
|
+
items: SidebarMenuItem[],
|
|
432
|
+
level = 2
|
|
433
|
+
): CollapsedDropdownItem[] => {
|
|
434
|
+
if (level > MAX_NESTING_LEVEL) {
|
|
435
|
+
return []
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return items.flatMap((item) => {
|
|
439
|
+
const hasItemChildren = hasChildren(item)
|
|
440
|
+
const dropdownItem: CollapsedDropdownItem = {
|
|
441
|
+
sectionTitle: item.isSectionTitle,
|
|
442
|
+
text: item.text,
|
|
443
|
+
to: item.to,
|
|
444
|
+
icon: item.icon,
|
|
445
|
+
type: item.icon ? DropdownItemType.ICON : DropdownItemType.TEXT,
|
|
446
|
+
disabled: Boolean(item.disabled),
|
|
447
|
+
actionType: item.isSectionTitle
|
|
448
|
+
? DropdownActionType.ACTION
|
|
449
|
+
: DropdownActionType.LINK,
|
|
450
|
+
children: hasItemChildren && level < MAX_NESTING_LEVEL
|
|
451
|
+
? getCollapsedDropdownItems(item.children ?? [], level + 1)
|
|
452
|
+
: undefined,
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return [dropdownItem]
|
|
456
|
+
})
|
|
457
|
+
}
|
|
458
|
+
|
|
375
459
|
// Computed
|
|
376
460
|
const computedMenuHeight = computed(() => {
|
|
377
461
|
if (slots['sidebar-footer']) {
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
:is="componentTag"
|
|
4
4
|
v-bind="componentProps"
|
|
5
5
|
:class="[
|
|
6
|
+
!isCollapsed && 'w-full',
|
|
6
7
|
'flex',
|
|
7
8
|
'items-center',
|
|
8
|
-
'text-sm',
|
|
9
9
|
'text-left',
|
|
10
|
-
'font-semibold',
|
|
11
10
|
'rounded-lg',
|
|
12
11
|
'hover:bg-background-neutral-hover',
|
|
13
12
|
'justify-between',
|
|
13
|
+
levelTextClass,
|
|
14
14
|
spacingClass,
|
|
15
15
|
!isActive && 'text-text-default',
|
|
16
16
|
isActive && 'text-text-primary-brand-on-neutral-hover-bg bg-background-neutral-hover',
|
|
@@ -82,6 +82,14 @@ const props = defineProps({
|
|
|
82
82
|
type: Boolean as PropType<boolean>,
|
|
83
83
|
default: false,
|
|
84
84
|
},
|
|
85
|
+
level: {
|
|
86
|
+
type: Number as PropType<number>,
|
|
87
|
+
default: 1,
|
|
88
|
+
},
|
|
89
|
+
showNestedLevelGuide: {
|
|
90
|
+
type: Boolean as PropType<boolean>,
|
|
91
|
+
default: true,
|
|
92
|
+
},
|
|
85
93
|
textClass: String as PropType<string>,
|
|
86
94
|
iconClass: String as PropType<string>,
|
|
87
95
|
disabled: {
|
|
@@ -101,6 +109,20 @@ defineEmits(['click'])
|
|
|
101
109
|
const route = useRoute()
|
|
102
110
|
|
|
103
111
|
// Computed classes
|
|
112
|
+
const resolvedLevel = computed(() => {
|
|
113
|
+
return Math.min(Math.max(props.level, 1), 3)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const levelTextClass = computed(() => {
|
|
117
|
+
const variant = {
|
|
118
|
+
1: 'text-sm font-semibold',
|
|
119
|
+
2: 'text-sm font-medium',
|
|
120
|
+
3: 'text-xs font-medium',
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return variant[resolvedLevel.value as 1 | 2 | 3]
|
|
124
|
+
})
|
|
125
|
+
|
|
104
126
|
const spacingClass = computed(() => {
|
|
105
127
|
if (props.isCollapsed) {
|
|
106
128
|
const collapsedVariant = {
|
|
@@ -111,11 +133,23 @@ const spacingClass = computed(() => {
|
|
|
111
133
|
return collapsedVariant[props.styleType as SidebarNavMenuItemStyleType] || 'p-2'
|
|
112
134
|
}
|
|
113
135
|
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
136
|
+
const levelVariant = {
|
|
137
|
+
1: {
|
|
138
|
+
[SidebarNavMenuItemStyleType.SPACED]: 'min-h-[40px] py-2 pl-3 pr-2',
|
|
139
|
+
[SidebarNavMenuItemStyleType.COMPACT]: 'py-1 pl-2 pr-1',
|
|
140
|
+
},
|
|
141
|
+
2: {
|
|
142
|
+
[SidebarNavMenuItemStyleType.SPACED]: 'min-h-[36px] py-1.5 pl-3 pr-2',
|
|
143
|
+
[SidebarNavMenuItemStyleType.COMPACT]: 'py-1 pl-2 pr-1',
|
|
144
|
+
},
|
|
145
|
+
3: {
|
|
146
|
+
[SidebarNavMenuItemStyleType.SPACED]: 'min-h-[32px] py-1 pl-3 pr-2',
|
|
147
|
+
[SidebarNavMenuItemStyleType.COMPACT]: 'py-0.5 pl-2 pr-1',
|
|
148
|
+
},
|
|
117
149
|
}
|
|
118
150
|
|
|
151
|
+
const variant = levelVariant[resolvedLevel.value as 1 | 2 | 3]
|
|
152
|
+
|
|
119
153
|
return variant[props.styleType as SidebarNavMenuItemStyleType] || 'min-h-[40px] py-2 pl-3 pr-2'
|
|
120
154
|
})
|
|
121
155
|
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div :class="levelWrapperClass">
|
|
3
|
+
<div
|
|
4
|
+
v-if="showGuideLine"
|
|
5
|
+
:class="guideLineClass"
|
|
6
|
+
/>
|
|
7
|
+
|
|
8
|
+
<template v-for="(item, index) in items" :key="getNodeKey(getNodePath(index))">
|
|
9
|
+
<NavSidebarMenuSectionTitle
|
|
10
|
+
v-if="item.isSectionTitle"
|
|
11
|
+
:text="item.text"
|
|
12
|
+
:icon="item.icon"
|
|
13
|
+
:styleType="itemsStyleType"
|
|
14
|
+
:isCollapsed
|
|
15
|
+
:level
|
|
16
|
+
:showNestedLevelGuide
|
|
17
|
+
:showCollapseDivider
|
|
18
|
+
:class="[
|
|
19
|
+
level > 1 && 'mt-2',
|
|
20
|
+
]"
|
|
21
|
+
/>
|
|
22
|
+
|
|
23
|
+
<template v-else>
|
|
24
|
+
<NavSidebarMenuItem
|
|
25
|
+
:text="item.text"
|
|
26
|
+
:icon="item.icon"
|
|
27
|
+
:to="item.to"
|
|
28
|
+
:disabled="item.disabled"
|
|
29
|
+
:prefetchOn
|
|
30
|
+
:styleType="itemsStyleType"
|
|
31
|
+
:textClass="getTextClass(level)"
|
|
32
|
+
:iconClass="getIconClass(level)"
|
|
33
|
+
:showDropdownArrow="canRenderChildren(item)"
|
|
34
|
+
:isOpen="isNodeOpen(getNodePath(index))"
|
|
35
|
+
:isCollapsed
|
|
36
|
+
:level
|
|
37
|
+
:showNestedLevelGuide
|
|
38
|
+
:class="[
|
|
39
|
+
getItemCustomClass(level),
|
|
40
|
+
]"
|
|
41
|
+
@click="handleItemClick(item, getNodePath(index))"
|
|
42
|
+
/>
|
|
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
|
+
/>
|
|
63
|
+
</template>
|
|
64
|
+
</template>
|
|
65
|
+
</div>
|
|
66
|
+
</template>
|
|
67
|
+
<script setup lang="ts">
|
|
68
|
+
defineOptions({
|
|
69
|
+
name: 'NavSidebarMenuItemsTree',
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const props = defineProps({
|
|
73
|
+
items: {
|
|
74
|
+
type: Array as PropType<SidebarMenuItem[]>,
|
|
75
|
+
default: () => [],
|
|
76
|
+
},
|
|
77
|
+
level: {
|
|
78
|
+
type: Number as PropType<number>,
|
|
79
|
+
default: 1,
|
|
80
|
+
},
|
|
81
|
+
isCollapsed: {
|
|
82
|
+
type: Boolean as PropType<boolean>,
|
|
83
|
+
default: false,
|
|
84
|
+
},
|
|
85
|
+
openItems: {
|
|
86
|
+
type: Object as PropType<Record<string, boolean>>,
|
|
87
|
+
default: () => ({}),
|
|
88
|
+
},
|
|
89
|
+
itemsStyleType: {
|
|
90
|
+
type: String as PropType<SidebarNavMenuItemStyleType>,
|
|
91
|
+
default: SidebarNavMenuItemStyleType.COMPACT,
|
|
92
|
+
validator: (value: SidebarNavMenuItemStyleType) => Object.values(SidebarNavMenuItemStyleType).includes(value),
|
|
93
|
+
},
|
|
94
|
+
itemsCustomClass: String as PropType<string>,
|
|
95
|
+
itemsTextClass: String as PropType<string>,
|
|
96
|
+
itemsIconClass: String as PropType<string>,
|
|
97
|
+
subItemsCustomClass: String as PropType<string>,
|
|
98
|
+
subItemsTextClass: String as PropType<string>,
|
|
99
|
+
subItemsIconClass: String as PropType<string>,
|
|
100
|
+
thirdLevelItemsCustomClass: String as PropType<string>,
|
|
101
|
+
prefetchOn: {
|
|
102
|
+
type: [String, Object] as PropType<PrefetchOnStrategy>,
|
|
103
|
+
default: PrefetchOn.VISIBILITY,
|
|
104
|
+
},
|
|
105
|
+
showCollapseDivider: {
|
|
106
|
+
type: Boolean as PropType<boolean>,
|
|
107
|
+
default: false,
|
|
108
|
+
},
|
|
109
|
+
showNestedLevelGuide: {
|
|
110
|
+
type: Boolean as PropType<boolean>,
|
|
111
|
+
default: true,
|
|
112
|
+
},
|
|
113
|
+
pathPrefix: {
|
|
114
|
+
type: Array as PropType<number[]>,
|
|
115
|
+
default: () => [],
|
|
116
|
+
},
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
const emit = defineEmits<{
|
|
120
|
+
(e: 'toggle', path: number[]): void
|
|
121
|
+
}>()
|
|
122
|
+
|
|
123
|
+
const MAX_NESTING_LEVEL = 3
|
|
124
|
+
|
|
125
|
+
const getNodePath = (index: number) => {
|
|
126
|
+
return [...props.pathPrefix, index]
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const getNodeKey = (path: number[]) => {
|
|
130
|
+
return path.join('-')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const hasChildren = (item: SidebarMenuItem) => {
|
|
134
|
+
return Array.isArray(item.children) && item.children.length > 0
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const canRenderChildren = (item: SidebarMenuItem) => {
|
|
138
|
+
return !props.isCollapsed && !item.isSectionTitle && hasChildren(item) && props.level < MAX_NESTING_LEVEL
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const isNodeOpen = (path: number[]) => {
|
|
142
|
+
return !!props.openItems[getNodeKey(path)]
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const showGuideLine = computed(() => {
|
|
146
|
+
return !props.isCollapsed && props.showNestedLevelGuide && props.level > 1
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
const levelWrapperClass = computed(() => {
|
|
150
|
+
if (props.level <= 1) return ''
|
|
151
|
+
|
|
152
|
+
const variant = {
|
|
153
|
+
2: 'relative ml-4 pl-3',
|
|
154
|
+
3: 'relative ml-8 pl-3',
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return variant[props.level as 2 | 3] || 'relative ml-4 pl-3'
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
const guideLineClass = computed(() => {
|
|
161
|
+
const variant = {
|
|
162
|
+
2: 'pointer-events-none absolute left-0 top-0 bottom-0 w-[2px] bg-border-neutral-subtle/70',
|
|
163
|
+
3: 'pointer-events-none absolute left-0 top-0 bottom-0 w-[2px] bg-border-neutral-subtle/50',
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return variant[props.level as 2 | 3] || 'pointer-events-none absolute left-0 top-0 bottom-0 w-[2px] bg-border-neutral-subtle/70'
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const getItemCustomClass = (level: number) => {
|
|
170
|
+
if (level === 1) {
|
|
171
|
+
return props.itemsCustomClass
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (level === 2) {
|
|
175
|
+
return props.subItemsCustomClass ?? '!font-medium'
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return props.thirdLevelItemsCustomClass ?? props.subItemsCustomClass ?? '!font-medium'
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const getTextClass = (level: number) => {
|
|
182
|
+
if (level === 1) {
|
|
183
|
+
return props.itemsTextClass
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return props.subItemsTextClass
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const getIconClass = (level: number) => {
|
|
190
|
+
if (level === 1) {
|
|
191
|
+
return props.itemsIconClass
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return props.subItemsIconClass
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const handleItemClick = (item: SidebarMenuItem, path: number[]) => {
|
|
198
|
+
if (!canRenderChildren(item)) return
|
|
199
|
+
|
|
200
|
+
emit('toggle', path)
|
|
201
|
+
}
|
|
202
|
+
</script>
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
3
|
v-if="!isCollapsed"
|
|
4
|
+
v-bind="attrs"
|
|
4
5
|
:class="[
|
|
5
6
|
'flex',
|
|
6
7
|
'items-center',
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
'text-text-neutral-subtle',
|
|
10
|
-
'border-b',
|
|
11
|
-
'border-border-neutral-subtle',
|
|
8
|
+
textClass,
|
|
9
|
+
borderClass,
|
|
12
10
|
spacingClass,
|
|
11
|
+
expandedLevelOneBottomMarginClass,
|
|
13
12
|
]"
|
|
14
13
|
>
|
|
15
14
|
<Icon
|
|
@@ -22,9 +21,17 @@
|
|
|
22
21
|
{{ text }}
|
|
23
22
|
</span>
|
|
24
23
|
</div>
|
|
25
|
-
|
|
24
|
+
|
|
25
|
+
<Divider
|
|
26
|
+
v-if="isCollapsed && showCollapseDivider"
|
|
27
|
+
v-bind="attrs"
|
|
28
|
+
/>
|
|
26
29
|
</template>
|
|
27
30
|
<script setup lang="ts">
|
|
31
|
+
defineOptions({
|
|
32
|
+
inheritAttrs: false,
|
|
33
|
+
})
|
|
34
|
+
|
|
28
35
|
// Props
|
|
29
36
|
const props = defineProps({
|
|
30
37
|
text: {
|
|
@@ -41,14 +48,63 @@ const props = defineProps({
|
|
|
41
48
|
type: Boolean as PropType<boolean>,
|
|
42
49
|
default: false,
|
|
43
50
|
},
|
|
51
|
+
level: {
|
|
52
|
+
type: Number as PropType<number>,
|
|
53
|
+
default: 1,
|
|
54
|
+
},
|
|
55
|
+
showNestedLevelGuide: {
|
|
56
|
+
type: Boolean as PropType<boolean>,
|
|
57
|
+
default: true,
|
|
58
|
+
},
|
|
44
59
|
showCollapseDivider: {
|
|
45
60
|
type: Boolean as PropType<boolean>,
|
|
46
61
|
default: true,
|
|
47
62
|
},
|
|
48
63
|
})
|
|
49
64
|
|
|
65
|
+
const attrs = useAttrs()
|
|
66
|
+
|
|
50
67
|
// Computed classes
|
|
68
|
+
const resolvedLevel = computed(() => {
|
|
69
|
+
return Math.min(Math.max(props.level, 1), 3)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const textClass = computed(() => {
|
|
73
|
+
const variant = {
|
|
74
|
+
1: 'text-sm font-semibold text-text-neutral-subtle',
|
|
75
|
+
2: 'text-sm font-bold text-text-neutral-subtle',
|
|
76
|
+
3: 'text-xs font-bold text-text-neutral-subtle',
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return variant[resolvedLevel.value as 1 | 2 | 3]
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const borderClass = computed(() => {
|
|
83
|
+
if (resolvedLevel.value === 1) {
|
|
84
|
+
return 'border-b border-border-neutral-subtle'
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return ''
|
|
88
|
+
})
|
|
89
|
+
|
|
51
90
|
const spacingClass = computed(() => {
|
|
91
|
+
if (resolvedLevel.value > 1) {
|
|
92
|
+
const nestedVariant = {
|
|
93
|
+
2: {
|
|
94
|
+
[SidebarNavMenuItemStyleType.SPACED]: 'min-h-[32px] gap-2 py-2 pl-3 pr-2',
|
|
95
|
+
[SidebarNavMenuItemStyleType.COMPACT]: 'min-h-[28px] gap-2 py-1.5 pl-2 pr-2',
|
|
96
|
+
},
|
|
97
|
+
3: {
|
|
98
|
+
[SidebarNavMenuItemStyleType.SPACED]: 'min-h-[30px] gap-2 py-1.5 pl-3 pr-2',
|
|
99
|
+
[SidebarNavMenuItemStyleType.COMPACT]: 'min-h-[26px] gap-2 py-1 pl-2 pr-2',
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const levelVariant = nestedVariant[resolvedLevel.value as 2 | 3]
|
|
104
|
+
|
|
105
|
+
return levelVariant[props.styleType as SidebarNavMenuItemStyleType] || 'min-h-[28px] gap-2 py-1.5 pl-3 pr-2'
|
|
106
|
+
}
|
|
107
|
+
|
|
52
108
|
const variant = {
|
|
53
109
|
[SidebarNavMenuItemStyleType.SPACED]: 'min-h-[40px] gap-3 pt-4 pb-2 px-3',
|
|
54
110
|
[SidebarNavMenuItemStyleType.COMPACT]: 'gap-2 pt-3 pb-1 px-2',
|
|
@@ -63,4 +119,17 @@ const iconSizeClass = computed(() => {
|
|
|
63
119
|
}
|
|
64
120
|
return variant[props.styleType as SidebarNavMenuItemStyleType] || 'w-[20px] h-[20px] min-w-[20px] min-h-[20px]'
|
|
65
121
|
})
|
|
122
|
+
|
|
123
|
+
const expandedLevelOneBottomMarginClass = computed(() => {
|
|
124
|
+
if (props.isCollapsed || resolvedLevel.value !== 1) {
|
|
125
|
+
return ''
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const variant = {
|
|
129
|
+
[SidebarNavMenuItemStyleType.SPACED]: 'mb-2',
|
|
130
|
+
[SidebarNavMenuItemStyleType.COMPACT]: 'mb-1',
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return variant[props.styleType as SidebarNavMenuItemStyleType] || 'mb-2'
|
|
134
|
+
})
|
|
66
135
|
</script>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export interface DropdownMenuItem {
|
|
2
2
|
actionType?: DropdownActionType
|
|
3
|
+
sectionTitle?: boolean
|
|
3
4
|
text?: string
|
|
4
5
|
icon?: string
|
|
5
6
|
size?: DropdownItemSize
|
|
@@ -14,4 +15,21 @@ export interface DropdownMenuItem {
|
|
|
14
15
|
hasSeparator?: boolean
|
|
15
16
|
disabled?: boolean
|
|
16
17
|
callback?: () => void
|
|
18
|
+
children?: DropdownMenuItem[]
|
|
17
19
|
}
|
|
20
|
+
|
|
21
|
+
export interface CollapsedDropdownItem {
|
|
22
|
+
sectionTitle?: boolean
|
|
23
|
+
text: string
|
|
24
|
+
to?: string
|
|
25
|
+
icon?: string
|
|
26
|
+
type?: DropdownItemType
|
|
27
|
+
size?: DropdownItemSize
|
|
28
|
+
helpText?: string
|
|
29
|
+
isExternal?: boolean
|
|
30
|
+
disabled?: boolean
|
|
31
|
+
hasSeparator?: boolean
|
|
32
|
+
actionType: DropdownActionType
|
|
33
|
+
children?: CollapsedDropdownItem[]
|
|
34
|
+
}
|
|
35
|
+
|
package/models/types/selects.ts
CHANGED