@imaginario27/air-ui-ds 1.10.2 → 1.12.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 +35 -0
- package/assets/css/defaults.css +2 -1
- package/components/dropdowns/ContextMenu.vue +130 -0
- package/components/dropdowns/ContextMenuDropdown.vue +281 -0
- package/components/dropdowns/DropdownMenu.vue +159 -37
- package/components/dropdowns/DropdownMenuContextItem.vue +239 -0
- package/components/dropdowns/DropdownMenuContextItemsTree.vue +153 -0
- package/components/dropdowns/DropdownMenuItem.vue +10 -0
- package/components/dropdowns/DropdownSectionItem.vue +28 -0
- package/components/dropdowns/DropdownSelect.vue +33 -23
- package/components/kbds/Kbd.vue +32 -0
- package/components/modals/ModalDialog.vue +51 -28
- 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 +24 -0
- package/models/types/selects.ts +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,41 @@ 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.11.0 - 2026-04-15
|
|
9
|
+
|
|
10
|
+
Release type: minor.
|
|
11
|
+
Commits found in range: 1.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
1. add nested dropdown menus, sidebar menu items and section titles ([2c49dbf](https://github.com/imaginario27/air-ui/commit/2c49dbf1fae116b377dea988f80a3fdf28ef4c4f))
|
|
16
|
+
|
|
17
|
+
- Package: @imaginario27/air-ui-ds.
|
|
18
|
+
|
|
19
|
+
## 1.10.2 - 2026-04-14
|
|
20
|
+
|
|
21
|
+
Release type: patch.
|
|
22
|
+
Commits found in range: 3.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
1. fix typecheck errors in selects ([eee12f8](https://github.com/imaginario27/air-ui/commit/eee12f87045fcb12556f3f38e80f87e5078a0d5c))
|
|
27
|
+
2. remove duplicated imports ([a277959](https://github.com/imaginario27/air-ui/commit/a2779590034a360e1685193455e978391870a69f))
|
|
28
|
+
3. fix duplicated imports ([5a324a3](https://github.com/imaginario27/air-ui/commit/5a324a3ea43ae5fdf794e345b54c57207860efbd))
|
|
29
|
+
|
|
30
|
+
- Package: @imaginario27/air-ui-ds.
|
|
31
|
+
|
|
32
|
+
## 1.10.1 - 2026-04-09
|
|
33
|
+
|
|
34
|
+
Release type: patch.
|
|
35
|
+
Commits found in range: 1.
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
1. fix TypeScript errors in TabBar.vue ([273925b](https://github.com/imaginario27/air-ui/commit/273925bdca00ac9ad58c1c43de5668ed78a85da1))
|
|
40
|
+
|
|
41
|
+
- Package: @imaginario27/air-ui-ds.
|
|
42
|
+
|
|
8
43
|
## 1.10.0 - 2026-04-09
|
|
9
44
|
|
|
10
45
|
Release type: minor.
|
package/assets/css/defaults.css
CHANGED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div data-test="context-menu-target" @contextmenu="onContextMenu">
|
|
3
|
+
<slot />
|
|
4
|
+
</div>
|
|
5
|
+
|
|
6
|
+
<ContextMenuDropdown
|
|
7
|
+
v-model="isOpen"
|
|
8
|
+
:x="anchorPoint.x"
|
|
9
|
+
:y="anchorPoint.y"
|
|
10
|
+
:width="width"
|
|
11
|
+
:hasShadow="hasShadow"
|
|
12
|
+
:hasBorder="hasBorder"
|
|
13
|
+
:positionXOffset="positionXOffset"
|
|
14
|
+
:positionYOffset="positionYOffset"
|
|
15
|
+
:dropdownClass="dropdownClass"
|
|
16
|
+
:shouldTeleport="shouldTeleport"
|
|
17
|
+
:teleportTo="teleportTo"
|
|
18
|
+
:zIndex="zIndex"
|
|
19
|
+
v-bind="$attrs"
|
|
20
|
+
>
|
|
21
|
+
<template #items="{ onClose }">
|
|
22
|
+
<slot v-if="$slots.items" name="items" :onClose="onClose" />
|
|
23
|
+
|
|
24
|
+
<DropdownMenuContextItemsTree
|
|
25
|
+
v-else
|
|
26
|
+
:items="items"
|
|
27
|
+
:trigger="Trigger.HOVER"
|
|
28
|
+
:hasShadow="hasShadow"
|
|
29
|
+
:hasBorder="hasBorder"
|
|
30
|
+
:disabled="disabled"
|
|
31
|
+
:prefetchOn="prefetchOn"
|
|
32
|
+
:level="level"
|
|
33
|
+
:nestedMenuGap="nestedMenuGap"
|
|
34
|
+
:onClose="onClose"
|
|
35
|
+
/>
|
|
36
|
+
</template>
|
|
37
|
+
</ContextMenuDropdown>
|
|
38
|
+
</template>
|
|
39
|
+
|
|
40
|
+
<script setup lang="ts">
|
|
41
|
+
defineOptions({
|
|
42
|
+
inheritAttrs: false,
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// Props
|
|
46
|
+
const props = defineProps({
|
|
47
|
+
items: {
|
|
48
|
+
type: Array as PropType<ContextMenuItem[]>,
|
|
49
|
+
default: () => [],
|
|
50
|
+
},
|
|
51
|
+
hasShadow: {
|
|
52
|
+
type: Boolean as PropType<boolean>,
|
|
53
|
+
default: true,
|
|
54
|
+
},
|
|
55
|
+
hasBorder: {
|
|
56
|
+
type: Boolean as PropType<boolean>,
|
|
57
|
+
default: true,
|
|
58
|
+
},
|
|
59
|
+
width: {
|
|
60
|
+
type: Number as PropType<number>,
|
|
61
|
+
default: 240,
|
|
62
|
+
},
|
|
63
|
+
positionXOffset: {
|
|
64
|
+
type: [Number, String] as PropType<number | string>,
|
|
65
|
+
default: 0,
|
|
66
|
+
},
|
|
67
|
+
positionYOffset: {
|
|
68
|
+
type: [Number, String] as PropType<number | string>,
|
|
69
|
+
default: 0,
|
|
70
|
+
},
|
|
71
|
+
dropdownClass: String as PropType<string>,
|
|
72
|
+
shouldTeleport: {
|
|
73
|
+
type: Boolean as PropType<boolean>,
|
|
74
|
+
default: true,
|
|
75
|
+
},
|
|
76
|
+
teleportTo: {
|
|
77
|
+
type: String as PropType<string>,
|
|
78
|
+
default: 'body',
|
|
79
|
+
},
|
|
80
|
+
zIndex: {
|
|
81
|
+
type: String as PropType<string>,
|
|
82
|
+
default: '50',
|
|
83
|
+
},
|
|
84
|
+
level: {
|
|
85
|
+
type: Number as PropType<number>,
|
|
86
|
+
default: 1,
|
|
87
|
+
},
|
|
88
|
+
nestedMenuGap: {
|
|
89
|
+
type: Number as PropType<number>,
|
|
90
|
+
default: 8,
|
|
91
|
+
},
|
|
92
|
+
disabled: {
|
|
93
|
+
type: Boolean as PropType<boolean>,
|
|
94
|
+
default: false,
|
|
95
|
+
},
|
|
96
|
+
prefetchOn: {
|
|
97
|
+
type: [String, Object] as PropType<PrefetchOnStrategy>,
|
|
98
|
+
default: PrefetchOn.VISIBILITY,
|
|
99
|
+
},
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// Composables
|
|
103
|
+
const slots = useSlots()
|
|
104
|
+
|
|
105
|
+
// States
|
|
106
|
+
const isOpen = ref(false)
|
|
107
|
+
const anchorPoint = ref({ x: 0, y: 0 })
|
|
108
|
+
|
|
109
|
+
// Computed
|
|
110
|
+
const hasMenuContent = computed(() => {
|
|
111
|
+
return props.items.length > 0 || Boolean(slots.items)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// Methods
|
|
115
|
+
const openAtCursor = (event: MouseEvent) => {
|
|
116
|
+
anchorPoint.value = {
|
|
117
|
+
x: event.clientX,
|
|
118
|
+
y: event.clientY,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
isOpen.value = true
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const onContextMenu = async (event: MouseEvent) => {
|
|
125
|
+
if (props.disabled || !hasMenuContent.value) return
|
|
126
|
+
|
|
127
|
+
event.preventDefault()
|
|
128
|
+
openAtCursor(event)
|
|
129
|
+
}
|
|
130
|
+
</script>
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<template v-if="shouldTeleport">
|
|
3
|
+
<teleport :to="teleportTo">
|
|
4
|
+
<Transition
|
|
5
|
+
appear
|
|
6
|
+
enter-active-class="transition-opacity duration-200 ease-out"
|
|
7
|
+
enter-from-class="opacity-0"
|
|
8
|
+
leave-active-class="transition-opacity duration-150 ease-in"
|
|
9
|
+
leave-to-class="opacity-0"
|
|
10
|
+
>
|
|
11
|
+
<div
|
|
12
|
+
v-if="modelValue"
|
|
13
|
+
ref="menuPanel"
|
|
14
|
+
data-context-menu-panel
|
|
15
|
+
data-dropdown-menu-panel
|
|
16
|
+
:class="[
|
|
17
|
+
'bg-background-surface',
|
|
18
|
+
'py-1',
|
|
19
|
+
'rounded',
|
|
20
|
+
hasShadow && 'shadow-lg',
|
|
21
|
+
'flex flex-col',
|
|
22
|
+
hasBorder && 'border border-border-default',
|
|
23
|
+
dropdownClass,
|
|
24
|
+
]"
|
|
25
|
+
:style="panelStyle"
|
|
26
|
+
@contextmenu.prevent
|
|
27
|
+
>
|
|
28
|
+
<slot
|
|
29
|
+
v-if="$slots.items"
|
|
30
|
+
name="items"
|
|
31
|
+
:onClose="close"
|
|
32
|
+
/>
|
|
33
|
+
<slot
|
|
34
|
+
v-else
|
|
35
|
+
:onClose="close"
|
|
36
|
+
/>
|
|
37
|
+
</div>
|
|
38
|
+
</Transition>
|
|
39
|
+
</teleport>
|
|
40
|
+
</template>
|
|
41
|
+
|
|
42
|
+
<Transition
|
|
43
|
+
v-else
|
|
44
|
+
appear
|
|
45
|
+
enter-active-class="transition-opacity duration-200 ease-out"
|
|
46
|
+
enter-from-class="opacity-0"
|
|
47
|
+
leave-active-class="transition-opacity duration-150 ease-in"
|
|
48
|
+
leave-to-class="opacity-0"
|
|
49
|
+
>
|
|
50
|
+
<div
|
|
51
|
+
v-if="modelValue"
|
|
52
|
+
ref="menuPanel"
|
|
53
|
+
data-context-menu-panel
|
|
54
|
+
data-dropdown-menu-panel
|
|
55
|
+
:class="[
|
|
56
|
+
'bg-background-surface',
|
|
57
|
+
'py-1',
|
|
58
|
+
'rounded',
|
|
59
|
+
hasShadow && 'shadow-lg',
|
|
60
|
+
'flex flex-col',
|
|
61
|
+
hasBorder && 'border border-border-default',
|
|
62
|
+
dropdownClass,
|
|
63
|
+
]"
|
|
64
|
+
:style="panelStyle"
|
|
65
|
+
@contextmenu.prevent
|
|
66
|
+
>
|
|
67
|
+
<slot
|
|
68
|
+
v-if="$slots.items"
|
|
69
|
+
name="items"
|
|
70
|
+
:onClose="close"
|
|
71
|
+
/>
|
|
72
|
+
<slot
|
|
73
|
+
v-else
|
|
74
|
+
:onClose="close"
|
|
75
|
+
/>
|
|
76
|
+
</div>
|
|
77
|
+
</Transition>
|
|
78
|
+
</template>
|
|
79
|
+
|
|
80
|
+
<script setup lang="ts">
|
|
81
|
+
// Imports
|
|
82
|
+
import type { CSSProperties } from 'vue'
|
|
83
|
+
|
|
84
|
+
// Props
|
|
85
|
+
const props = defineProps({
|
|
86
|
+
modelValue: {
|
|
87
|
+
type: Boolean as PropType<boolean>,
|
|
88
|
+
default: false,
|
|
89
|
+
},
|
|
90
|
+
width: {
|
|
91
|
+
type: Number as PropType<number>,
|
|
92
|
+
default: 240,
|
|
93
|
+
},
|
|
94
|
+
x: {
|
|
95
|
+
type: Number as PropType<number>,
|
|
96
|
+
default: 0,
|
|
97
|
+
},
|
|
98
|
+
y: {
|
|
99
|
+
type: Number as PropType<number>,
|
|
100
|
+
default: 0,
|
|
101
|
+
},
|
|
102
|
+
positionXOffset: {
|
|
103
|
+
type: [Number, String] as PropType<number | string>,
|
|
104
|
+
default: 0,
|
|
105
|
+
},
|
|
106
|
+
positionYOffset: {
|
|
107
|
+
type: [Number, String] as PropType<number | string>,
|
|
108
|
+
default: 0,
|
|
109
|
+
},
|
|
110
|
+
hasShadow: {
|
|
111
|
+
type: Boolean as PropType<boolean>,
|
|
112
|
+
default: true,
|
|
113
|
+
},
|
|
114
|
+
hasBorder: {
|
|
115
|
+
type: Boolean as PropType<boolean>,
|
|
116
|
+
default: true,
|
|
117
|
+
},
|
|
118
|
+
dropdownClass: String as PropType<string>,
|
|
119
|
+
shouldTeleport: {
|
|
120
|
+
type: Boolean as PropType<boolean>,
|
|
121
|
+
default: true,
|
|
122
|
+
},
|
|
123
|
+
teleportTo: {
|
|
124
|
+
type: String as PropType<string>,
|
|
125
|
+
default: 'body',
|
|
126
|
+
},
|
|
127
|
+
zIndex: {
|
|
128
|
+
type: String as PropType<string>,
|
|
129
|
+
default: '50',
|
|
130
|
+
},
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
// Emits
|
|
134
|
+
const emit = defineEmits<{
|
|
135
|
+
(e: 'update:modelValue', value: boolean): void
|
|
136
|
+
}>()
|
|
137
|
+
|
|
138
|
+
// States
|
|
139
|
+
const menuPanel = ref<HTMLElement | null>(null)
|
|
140
|
+
const resolvedPosition = ref({ left: 0, top: 0 })
|
|
141
|
+
const previousBodyOverflow = ref<string | null>(null)
|
|
142
|
+
const previousBodyPaddingRight = ref<string | null>(null)
|
|
143
|
+
|
|
144
|
+
// Methods
|
|
145
|
+
const close = () => {
|
|
146
|
+
emit('update:modelValue', false)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const lockScroll = () => {
|
|
150
|
+
if (previousBodyOverflow.value === null) {
|
|
151
|
+
previousBodyOverflow.value = document.body.style.overflow
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (previousBodyPaddingRight.value === null) {
|
|
155
|
+
previousBodyPaddingRight.value = document.body.style.paddingRight
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const previousViewportWidth = document.documentElement.clientWidth
|
|
159
|
+
document.body.style.overflow = 'hidden'
|
|
160
|
+
const currentViewportWidth = document.documentElement.clientWidth
|
|
161
|
+
const layoutShiftWidth = Math.max(0, currentViewportWidth - previousViewportWidth)
|
|
162
|
+
|
|
163
|
+
const computedPaddingRight = Number.parseFloat(getComputedStyle(document.body).paddingRight)
|
|
164
|
+
const currentPaddingRight = Number.isNaN(computedPaddingRight) ? 0 : computedPaddingRight
|
|
165
|
+
|
|
166
|
+
if (layoutShiftWidth > 0) {
|
|
167
|
+
document.body.style.paddingRight = `${currentPaddingRight + layoutShiftWidth}px`
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const unlockScroll = () => {
|
|
172
|
+
document.body.style.overflow = previousBodyOverflow.value ?? ''
|
|
173
|
+
document.body.style.paddingRight = previousBodyPaddingRight.value ?? ''
|
|
174
|
+
|
|
175
|
+
previousBodyOverflow.value = null
|
|
176
|
+
previousBodyPaddingRight.value = null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const toOffsetNumber = (value: number | string) => {
|
|
180
|
+
if (typeof value === 'number') return value
|
|
181
|
+
const parsed = Number.parseFloat(value)
|
|
182
|
+
return Number.isNaN(parsed) ? 0 : parsed
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const updatePosition = () => {
|
|
186
|
+
const panel = menuPanel.value
|
|
187
|
+
if (!panel) return
|
|
188
|
+
|
|
189
|
+
const x = props.x + toOffsetNumber(props.positionXOffset)
|
|
190
|
+
const y = props.y + toOffsetNumber(props.positionYOffset)
|
|
191
|
+
const rect = panel.getBoundingClientRect()
|
|
192
|
+
|
|
193
|
+
const maxLeft = Math.max(0, window.innerWidth - rect.width)
|
|
194
|
+
const maxTop = Math.max(0, window.innerHeight - rect.height)
|
|
195
|
+
|
|
196
|
+
resolvedPosition.value = {
|
|
197
|
+
left: Math.min(Math.max(0, x), maxLeft),
|
|
198
|
+
top: Math.min(Math.max(0, y), maxTop),
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const schedulePositionUpdate = () => {
|
|
203
|
+
nextTick(() => {
|
|
204
|
+
requestAnimationFrame(updatePosition)
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const handlePointerDown = (event: MouseEvent) => {
|
|
209
|
+
if (!props.modelValue || !menuPanel.value) return
|
|
210
|
+
|
|
211
|
+
const target = event.target as Node | null
|
|
212
|
+
if (!target) return
|
|
213
|
+
|
|
214
|
+
if (!menuPanel.value.contains(target)) {
|
|
215
|
+
close()
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const handleWindowResize = () => {
|
|
220
|
+
if (!props.modelValue) return
|
|
221
|
+
|
|
222
|
+
updatePosition()
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const handleKeydown = (event: KeyboardEvent) => {
|
|
226
|
+
if (!props.modelValue) return
|
|
227
|
+
|
|
228
|
+
if (event.key === 'Escape') {
|
|
229
|
+
close()
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Watchers
|
|
234
|
+
watch(
|
|
235
|
+
() => props.modelValue,
|
|
236
|
+
(isOpen) => {
|
|
237
|
+
if (isOpen) {
|
|
238
|
+
schedulePositionUpdate()
|
|
239
|
+
lockScroll()
|
|
240
|
+
} else {
|
|
241
|
+
unlockScroll()
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
watch(
|
|
247
|
+
() => [props.x, props.y, props.positionXOffset, props.positionYOffset],
|
|
248
|
+
() => {
|
|
249
|
+
if (props.modelValue) {
|
|
250
|
+
schedulePositionUpdate()
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
const panelStyle = computed<CSSProperties>(() => {
|
|
256
|
+
return {
|
|
257
|
+
position: 'fixed',
|
|
258
|
+
left: `${resolvedPosition.value.left}px`,
|
|
259
|
+
top: `${resolvedPosition.value.top}px`,
|
|
260
|
+
width: `${props.width}px`,
|
|
261
|
+
zIndex: props.zIndex,
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
onMounted(() => {
|
|
266
|
+
document.addEventListener('mousedown', handlePointerDown)
|
|
267
|
+
window.addEventListener('resize', handleWindowResize)
|
|
268
|
+
document.addEventListener('keydown', handleKeydown)
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
onBeforeUnmount(() => {
|
|
272
|
+
unlockScroll()
|
|
273
|
+
document.removeEventListener('mousedown', handlePointerDown)
|
|
274
|
+
window.removeEventListener('resize', handleWindowResize)
|
|
275
|
+
document.removeEventListener('keydown', handleKeydown)
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
defineExpose({
|
|
279
|
+
close,
|
|
280
|
+
})
|
|
281
|
+
</script>
|