@nexxtmove/ui 0.1.57 → 0.1.59

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.
@@ -0,0 +1,143 @@
1
+ <script lang="ts" setup>
2
+ import { ref, computed, onMounted, onUnmounted } from 'vue'
3
+ import { useFloating, autoUpdate, flip, shift, offset } from '@floating-ui/vue'
4
+ import type { Placement } from '@floating-ui/vue'
5
+
6
+ export interface NexxtFloatingPanelProps {
7
+ placement?: Placement
8
+ offsetDistance?: number
9
+ }
10
+
11
+ defineOptions({ name: 'NexxtFloatingPanel' })
12
+
13
+ const props = withDefaults(defineProps<NexxtFloatingPanelProps>(), {
14
+ placement: 'bottom-start',
15
+ offsetDistance: 8,
16
+ })
17
+
18
+ const emit = defineEmits<{
19
+ open: []
20
+ close: []
21
+ }>()
22
+
23
+ const isOpen = ref(false)
24
+ // Keeps the positioning container alive while the leave transition plays.
25
+ const isLeaving = ref(false)
26
+ const triggerRef = ref<HTMLElement | null>(null)
27
+ const floatingRef = ref<HTMLElement | null>(null)
28
+
29
+ const {
30
+ floatingStyles,
31
+ placement: computedPlacement,
32
+ isPositioned,
33
+ } = useFloating(triggerRef, floatingRef, {
34
+ placement: computed(() => props.placement),
35
+ whileElementsMounted: autoUpdate,
36
+ middleware: [offset(props.offsetDistance), flip(), shift({ padding: 8 })],
37
+ // fixed strategy escapes overflow clipping (e.g. inside a Modal) without
38
+ // needing a body teleport that Reka UI's dialog would block with pointer-events:none.
39
+ strategy: 'fixed',
40
+ // Use top/left positioning so `transform` is free for Tailwind transition classes.
41
+ transform: false,
42
+ })
43
+
44
+ const open = () => {
45
+ isOpen.value = true
46
+ emit('open')
47
+ }
48
+
49
+ const close = () => {
50
+ if (!isOpen.value) return
51
+ // Only keep the container alive if content was actually shown (isPositioned),
52
+ // otherwise there is no leave transition to wait for.
53
+ if (isPositioned.value) isLeaving.value = true
54
+ isOpen.value = false
55
+ emit('close')
56
+ }
57
+
58
+ const toggle = () => (isOpen.value ? close() : open())
59
+
60
+ const handleMousedown = (event: MouseEvent) => {
61
+ const target = event.target as Node
62
+ if (triggerRef.value?.contains(target) || floatingRef.value?.contains(target)) return
63
+ close()
64
+ }
65
+
66
+ const handleKeydown = (event: KeyboardEvent) => {
67
+ if (event.key === 'Escape' && isOpen.value) close()
68
+ }
69
+
70
+ const transitionClasses = computed(() => {
71
+ const placement = computedPlacement.value
72
+ const [side, align] = placement.split('-') as [string, string | undefined]
73
+
74
+ const originMap: Record<string, string> = {
75
+ 'bottom-start': 'origin-top-left',
76
+ 'bottom-end': 'origin-top-right',
77
+ bottom: 'origin-top',
78
+ 'top-start': 'origin-bottom-left',
79
+ 'top-end': 'origin-bottom-right',
80
+ top: 'origin-bottom',
81
+ 'left-start': 'origin-top-right',
82
+ 'left-end': 'origin-bottom-right',
83
+ left: 'origin-right',
84
+ 'right-start': 'origin-top-left',
85
+ 'right-end': 'origin-bottom-left',
86
+ right: 'origin-left',
87
+ }
88
+ const translates: Record<string, string> = {
89
+ bottom: '-translate-y-2',
90
+ top: 'translate-y-2',
91
+ left: 'translate-x-2',
92
+ right: '-translate-x-2',
93
+ }
94
+ const origin = originMap[align ? `${side}-${align}` : side] ?? 'origin-top'
95
+ const translate = translates[side] ?? '-translate-y-2'
96
+ return {
97
+ enterActiveClass: `transition-all duration-200 ease-out ${origin}`,
98
+ leaveActiveClass: `transition-all duration-150 ease-in ${origin}`,
99
+ enterFromClass: `opacity-0 scale-95 ${translate}`,
100
+ leaveToClass: `opacity-0 scale-95 ${translate}`,
101
+ enterToClass: 'opacity-100 scale-100 translate-x-0 translate-y-0',
102
+ leaveFromClass: 'opacity-100 scale-100 translate-x-0 translate-y-0',
103
+ }
104
+ })
105
+
106
+ defineExpose({ isOpen, open, close, toggle, triggerRef, floatingRef })
107
+
108
+ onMounted(() => {
109
+ document.addEventListener('mousedown', handleMousedown)
110
+ document.addEventListener('keydown', handleKeydown)
111
+ })
112
+
113
+ onUnmounted(() => {
114
+ document.removeEventListener('mousedown', handleMousedown)
115
+ document.removeEventListener('keydown', handleKeydown)
116
+ })
117
+ </script>
118
+
119
+ <template>
120
+ <span ref="triggerRef" v-bind="$attrs" class="inline-flex">
121
+ <slot name="trigger" :is-open="isOpen" :toggle="toggle" :open="open" :close="close" />
122
+ </span>
123
+ <!--
124
+ floatingRef is the pure positioning container. It mounts immediately when
125
+ open so useFloating can compute the correct top/left before anything is visible.
126
+ isLeaving keeps it alive while the leave transition plays so the position
127
+ stays anchored to the trigger during the outgoing animation.
128
+ strategy:'fixed' means position:fixed is used, so this escapes any overflow
129
+ clipping without needing a body teleport.
130
+ -->
131
+ <div v-if="isOpen || isLeaving" ref="floatingRef" class="z-50" :style="floatingStyles">
132
+ <!--
133
+ Content only renders once isPositioned is true, meaning floatingRef is
134
+ already at the correct position. The enter transition therefore always
135
+ originates from the trigger — never from the page origin.
136
+ -->
137
+ <Transition v-bind="transitionClasses" @after-leave="isLeaving = false">
138
+ <div v-if="isOpen && isPositioned">
139
+ <slot name="content" :is-open="isOpen" :close="close" :toggle="toggle" />
140
+ </div>
141
+ </Transition>
142
+ </div>
143
+ </template>
@@ -55,7 +55,7 @@ const sizeClass = {
55
55
  <template>
56
56
  <DialogRoot :open="open" @update:open="handleOpenChange">
57
57
  <DialogPortal>
58
- <div class="pointer-events-none fixed inset-0 z-50 grid place-items-center px-8">
58
+ <div class="pointer-events-none fixed inset-0 z-40 grid place-items-center px-8">
59
59
  <Transition
60
60
  enter-active-class="transition-opacity duration-200 ease-out"
61
61
  enter-from-class="opacity-0"
@@ -14,9 +14,19 @@ defineOptions({
14
14
 
15
15
  const props = defineProps<NexxtTabbedContent>()
16
16
 
17
- const selected = ref(props.tabs[0] ?? '')
17
+ const selected = defineModel<string>({ default: '' })
18
18
  const direction = ref<'forward' | 'backward'>('forward')
19
19
 
20
+ watch(
21
+ () => props.tabs,
22
+ (newTabs) => {
23
+ if (!selected.value && newTabs?.length > 0) {
24
+ selected.value = newTabs[0]
25
+ }
26
+ },
27
+ { immediate: true },
28
+ )
29
+
20
30
  watch(selected, (next, prev) => {
21
31
  direction.value = props.tabs.indexOf(next) > props.tabs.indexOf(prev) ? 'forward' : 'backward'
22
32
  })
@@ -13,6 +13,7 @@
13
13
  "NexxtDatePicker": "components/DatePicker/DatePicker.vue",
14
14
  "NexxtDoughnutChart": "components/DoughnutChart/DoughnutChart.vue",
15
15
  "NexxtDropdownButton": "components/DropdownButton/DropdownButton.vue",
16
+ "NexxtFloatingPanel": "components/FloatingPanel/FloatingPanel.vue",
16
17
  "NexxtHeader": "components/Header/Header.vue",
17
18
  "NexxtIcon": "components/Icon/Icon.vue",
18
19
  "NexxtInfoBlock": "components/InfoBlock/InfoBlock.vue",
package/src/index.ts CHANGED
@@ -13,6 +13,7 @@ export { default as NexxtCustomerJourneyTile } from './components/CustomerJourne
13
13
  export { default as NexxtDatePicker } from './components/DatePicker/DatePicker.vue'
14
14
  export { default as NexxtDoughnutChart } from './components/DoughnutChart/DoughnutChart.vue'
15
15
  export { default as NexxtDropdownButton } from './components/DropdownButton/DropdownButton.vue'
16
+ export { default as NexxtFloatingPanel } from './components/FloatingPanel/FloatingPanel.vue'
16
17
  export { default as NexxtHeader } from './components/Header/Header.vue'
17
18
  export { default as NexxtIcon } from './components/Icon/Icon.vue'
18
19
  export { default as NexxtInfoBlock } from './components/InfoBlock/InfoBlock.vue'