@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 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.
@@ -1,6 +1,7 @@
1
1
 
2
2
  html {
3
- scroll-behavior: smooth;
3
+ scroll-behavior: smooth;
4
+ scrollbar-gutter: stable;
4
5
  }
5
6
 
6
7
  body {
@@ -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>