adata-ui 2.0.80 → 2.0.82

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.
@@ -439,7 +439,7 @@ onUnmounted(() => cleanup?.())
439
439
  }
440
440
  button {
441
441
  user-select: text !important;
442
- cursor: text;
442
+ cursor: pointer;
443
443
  }
444
444
  }
445
445
  </style>
@@ -15,8 +15,8 @@ const errorAlert = ref(false)
15
15
  const sumArr = ref([
16
16
  {
17
17
  id: 1,
18
- value: 1000,
19
- label: '1 000 ₸',
18
+ value: 2000,
19
+ label: '2 000 ₸',
20
20
  },
21
21
  {
22
22
  id: 2,
@@ -1,74 +1,5 @@
1
1
  <script setup lang="ts">
2
-
3
- import {twJoin, twMerge} from "tailwind-merge"
4
-
5
- const uiConfig = {
6
- wrapper: 'relative',
7
- container: {
8
- base: 'group absolute',
9
- zIndex: 'z-[999]',
10
- placement: {
11
- top: '-top-1 left-1/2',
12
- bottom: '-bottom-1 left-1/2',
13
- left: '-left-1 top-1/2',
14
- right: '-right-1 top-1/2'
15
- }
16
- },
17
- width: 'max-w-xs',
18
- background: 'bg-deepblue-900 dark:bg-gray-50',
19
- backgroundError: 'bg-[#FAD9D7] dark:bg-[#4F393A]',
20
- color: 'text-white dark:text-deepblue-900',
21
- colorError: 'text-[#E74135] dark:text-[#F47E75]',
22
- rounded: 'rounded-md',
23
- base: 'text-xs font-normal absolute',
24
- padding: {
25
- sm: 'px-4 py-2',
26
- md: 'px-4 py-4'
27
- },
28
- placement: {
29
- top: {
30
- start: 'bottom-[1px] left-[-13px]',
31
- center: 'bottom-[1px] left-1/2 transform translate-x-[-50%]',
32
- end: 'bottom-[1px] right-[-13px]'
33
- },
34
- bottom: {
35
- start: 'top-[1px] left-[-13px]',
36
- center: 'top-[1px] left-1/2 transform translate-x-[-50%]',
37
- end: 'top-[1px] right-[-13px]'
38
- },
39
- left: {
40
- start: 'right-[1px] top-[-13px]',
41
- center: 'right-[1px] top-1/2 transform translate-y-[-50%]',
42
- end: 'right-[1px] bottom-[-13px]'
43
- },
44
- right: {
45
- start: 'left-[1px] top-[-13px]',
46
- center: 'left-[1px] top-1/2 transform translate-y-[-50%]',
47
- end: 'left-[1px] bottom-[-13px]'
48
- }
49
- },
50
- transition: {
51
- enterActiveClass: 'transition ease-out duration-200',
52
- enterFromClass: 'opacity-0 translate-y-1',
53
- enterToClass: 'opacity-100 translate-y-0',
54
- leaveActiveClass: 'transition ease-in duration-150',
55
- leaveFromClass: 'opacity-100 translate-y-0',
56
- leaveToClass: 'opacity-0 translate-y-1'
57
- },
58
- popper: {
59
- strategy: 'absolute',
60
- base: 'invisible before:visible before:block before:absolute before:rotate-45 before:z-19 before:w-3 before:h-3',
61
- rounded: 'before:rounded-sm',
62
- background: 'before:bg-deepblue-900 dark:before:bg-gray-50',
63
- backgroundError: 'before:bg-[#FAD9D7] dark:before:dark:bg-[#4F393A]',
64
- placement: {
65
- top: 'bottom-0 before:bottom-[-2px] before:left-[-6px]',
66
- bottom: 'top-0 before:top-[-2px] before:left-[-6px]',
67
- left: 'right-0 before:right-[-2px] before:top-[-6px]',
68
- right: 'left-0 before:left-[-2px] before:top-[-6px]'
69
- }
70
- },
71
- }
2
+ import { ref, computed, nextTick, onMounted, onBeforeUnmount, watch } from 'vue'
72
3
 
73
4
  interface Props {
74
5
  text?: string
@@ -100,143 +31,207 @@ const props = withDefaults(defineProps<Props>(), {
100
31
  errorParsing: false
101
32
  })
102
33
 
103
- const slots = useSlots()
104
- const body = ref<HTMLElement | null>(null)
105
-
34
+ /* --- refs --- */
35
+ const wrapper = ref<HTMLElement | null>(null) // root wrapper
36
+ const body = ref<HTMLElement | null>(null) // tooltip element
106
37
  const open = ref(false)
107
- let openTimeout: NodeJS.Timeout | null = null
108
- let closeTimeout: NodeJS.Timeout | null = null
38
+ let openTimeout: ReturnType<typeof setTimeout> | null = null
39
+ let closeTimeout: ReturnType<typeof setTimeout> | null = null
40
+
41
+ // inline styles for tooltip positioning
42
+ const tooltipStyle = ref<Record<string, string>>({
43
+ left: '0px',
44
+ top: '0px',
45
+ transform: 'translateX(0) translateY(0)'
46
+ })
109
47
 
48
+ // visual placement (may be flipped if not enough space)
49
+ const visualPlacement = ref(props.placement)
110
50
  const popperPosition = ref(props.popperPosition)
111
51
 
112
- const wrapperClasses = uiConfig.wrapper
113
- const containerClasses = computed(() => twMerge(twJoin(
114
- uiConfig.container.base,
115
- uiConfig.container.placement[props.placement],
116
- uiConfig.width,
117
- props.zIndex ? '' : uiConfig.container.zIndex
118
- )))
119
- const poperClasses = computed(() => twMerge(twJoin(
120
- uiConfig.popper.strategy,
121
- uiConfig.popper.base,
122
- uiConfig.popper.rounded,
123
- props.errorParsing ? uiConfig.popper.backgroundError : uiConfig.popper.background,
124
- uiConfig.popper.placement[props.placement]
125
- )))
126
- const baseClasses = computed(() => twMerge(twJoin(
127
- uiConfig.base,
128
- props.errorParsing ? uiConfig.backgroundError : uiConfig.background,
129
- props.errorParsing ? uiConfig.colorError : uiConfig.color,
130
- uiConfig.rounded,
131
- uiConfig.placement[props.placement][popperPosition.value],
132
- uiConfig.padding[props.size],
133
- props.truncate ? 'truncate' : 'max-w-[530px] w-max'
134
- )))
135
- const transitionClasses = uiConfig.transition
136
-
137
- const containerStyles = computed(() => {
138
- return {
139
- zIndex: props.zIndex
140
- }
52
+ /* --- helpers: compute classes (you can keep your uiConfig) --- */
53
+ const baseClass = computed(() => {
54
+ // keep your Tailwind classes if you like, but we position via inline styles
55
+ const bg = props.errorParsing ? 'bg-[#FAD9D7] dark:bg-[#4F393A]' : 'bg-deepblue-900 dark:bg-gray-50'
56
+ const color = props.errorParsing ? 'text-[#E74135] dark:text-[#F47E75]' : 'text-white dark:text-deepblue-900'
57
+ const pad = props.size === 'sm' ? 'px-4 py-2' : 'px-4 py-4'
58
+ const truncated = props.truncate ? 'truncate' : 'max-w-[530px] w-max'
59
+ return ['text-xs font-normal absolute', bg, color, 'rounded-md', pad, truncated].join(' ')
141
60
  })
142
61
 
62
+ /* --- positioning logic --- */
63
+ function updatePosition(targetEl: HTMLElement | null) {
64
+ if (!targetEl || !body.value) return
65
+
66
+ const targetRect = targetEl.getBoundingClientRect()
67
+ const tooltipRect = body.value.getBoundingClientRect()
68
+ const vw = window.innerWidth
69
+ const vh = window.innerHeight
70
+
71
+ // default placement = requested prop
72
+ let place: Props['placement'] = props.placement
73
+
74
+ // decide flip if not enough space
75
+ if (place === 'top' && targetRect.top < tooltipRect.height + 8 && (vh - targetRect.bottom) >= tooltipRect.height + 8) {
76
+ place = 'bottom'
77
+ } else if (place === 'bottom' && (vh - targetRect.bottom) < tooltipRect.height + 8 && targetRect.top >= tooltipRect.height + 8) {
78
+ place = 'top'
79
+ } else if (place === 'left' && targetRect.left < tooltipRect.width + 8 && (vw - targetRect.right) >= tooltipRect.width + 8) {
80
+ place = 'right'
81
+ } else if (place === 'right' && (vw - targetRect.right) < tooltipRect.width + 8 && targetRect.left >= tooltipRect.width + 8) {
82
+ place = 'left'
83
+ }
143
84
 
144
- const hasSlot = (name: string): boolean => {
145
- return Boolean(slots[name])
146
- }
85
+ visualPlacement.value = place
86
+
87
+ // compute left/top depending on placement and prefer 'start' unless end/center chosen by popperPosition
88
+ let left = 0
89
+ let top = 0
90
+
91
+ if (place === 'top' || place === 'bottom') {
92
+ // horizontal alignment depends on popperPosition
93
+ if (popperPosition.value === 'center') {
94
+ left = targetRect.left + targetRect.width / 2 - tooltipRect.width / 2
95
+ } else if (popperPosition.value === 'end') {
96
+ left = targetRect.right - tooltipRect.width
97
+ } else {
98
+ // start
99
+ left = targetRect.left
100
+ }
147
101
 
148
- const hasHeader = computed(() => hasSlot('header') || Boolean(props.header))
102
+ // vertical
103
+ if (place === 'top') {
104
+ top = targetRect.top - tooltipRect.height - 8
105
+ } else {
106
+ top = targetRect.bottom + 8
107
+ }
108
+ } else {
109
+ // left or right: vertical alignment
110
+ if (popperPosition.value === 'center') {
111
+ top = targetRect.top + targetRect.height / 2 - tooltipRect.height / 2
112
+ } else if (popperPosition.value === 'end') {
113
+ top = targetRect.bottom - tooltipRect.height
114
+ } else {
115
+ top = targetRect.top
116
+ }
149
117
 
150
- function onMouseEnter(e) {
151
- if (props.parent) {
152
- const parent = props.parent.getBoundingClientRect()
153
- const target = e.target
118
+ if (place === 'left') {
119
+ left = targetRect.left - tooltipRect.width - 8
120
+ } else {
121
+ left = targetRect.right + 8
122
+ }
123
+ }
154
124
 
155
- setTimeout(() => {
156
- if (body.value) {
157
- let left = parent.width - (target.offsetLeft + (target.clientWidth / 2) + body.value.offsetWidth)
125
+ // keep tooltip inside the viewport (small clamp)
126
+ const minGap = 6
127
+ if (left < minGap) left = minGap
128
+ if (left + tooltipRect.width > vw - minGap) left = vw - tooltipRect.width - minGap
129
+ if (top < minGap) top = minGap
130
+ if (top + tooltipRect.height > vh - minGap) top = vh - tooltipRect.height - minGap
158
131
 
159
- if (left < 0) {
160
- popperPosition.value = 'end'
161
- }
162
- }
163
- })
164
- }
132
+ tooltipStyle.value.left = `${Math.round(left)}px`
133
+ tooltipStyle.value.top = `${Math.round(top)}px`
134
+ tooltipStyle.value.transform = 'none'
165
135
  }
166
136
 
167
- const onMouseOver = (e) => {
137
+ /* --- events --- */
138
+ function handleMouseEnter(e: MouseEvent) {
139
+ // clear close timeout
168
140
  if (closeTimeout) {
169
141
  clearTimeout(closeTimeout)
170
142
  closeTimeout = null
171
143
  }
172
- if (open.value) {
173
- return
174
- }
175
144
 
176
- openTimeout = openTimeout || setTimeout(() => {
145
+ const targetEl = (e.currentTarget ?? e.target) as HTMLElement
146
+
147
+ // schedule open
148
+ openTimeout = openTimeout || setTimeout(async () => {
177
149
  open.value = true
150
+ await nextTick()
151
+ // measure and position after tooltip is rendered
152
+ updatePosition(targetEl)
178
153
  openTimeout = null
179
154
  }, props.openDelay)
180
155
  }
181
156
 
182
- const onMouseLeave = () => {
157
+ function handleMouseLeave(e?: MouseEvent) {
183
158
  if (openTimeout) {
184
159
  clearTimeout(openTimeout)
185
160
  openTimeout = null
186
161
  }
187
- if (!open.value) {
188
- return
189
- }
162
+ if (!open.value) return
163
+
190
164
  closeTimeout = closeTimeout || setTimeout(() => {
191
165
  open.value = false
192
166
  closeTimeout = null
193
167
  }, props.closeDelay)
194
168
  }
169
+
170
+ // if tooltip is open and user scrolls/resizes, recalc
171
+ function onWindowChange() {
172
+ // try to recalc against the wrapper's slot element if present
173
+ // we assume wrapper contains the hovered child
174
+ const hovered = wrapper.value?.querySelector(':hover') as HTMLElement | null
175
+ const target = hovered ?? wrapper.value
176
+ updatePosition(target)
177
+ }
178
+
179
+ onMounted(() => {
180
+ window.addEventListener('resize', onWindowChange)
181
+ window.addEventListener('scroll', onWindowChange, true)
182
+ })
183
+
184
+ onBeforeUnmount(() => {
185
+ window.removeEventListener('resize', onWindowChange)
186
+ window.removeEventListener('scroll', onWindowChange, true)
187
+ })
188
+
195
189
  </script>
196
190
 
197
191
  <template>
198
192
  <div
193
+ ref="wrapper"
199
194
  v-bind="$attrs"
200
- :class="wrapperClasses"
201
- @mouseover="onMouseOver"
202
- @mouseleave="onMouseLeave"
203
- @mouseenter="onMouseEnter"
195
+ class="relative"
196
+ @mouseenter="handleMouseEnter"
197
+ @mouseleave="handleMouseLeave"
204
198
  >
205
199
  <slot :open="open">
206
200
  Hover
207
201
  </slot>
208
- <Transition
209
- appear
210
- v-bind="transitionClasses"
211
- >
202
+
203
+ <!-- tooltip rendered absolutely on body of component, positioned by inline styles -->
204
+ <transition name="fade" appear>
212
205
  <div
213
- v-if="open && !prevent"
214
- :class="containerClasses"
215
- :style="containerStyles"
206
+ v-if="open && !props.prevent"
207
+ class="pointer-events-none"
208
+ :style="{ position: 'fixed', left: tooltipStyle.left, top: tooltipStyle.top, zIndex: props.zIndex ?? 9999 }"
216
209
  >
217
- <div
218
- v-if="popper"
219
- :class="poperClasses"
220
- />
221
- <div :class="baseClasses" ref="body">
222
- <div
223
- v-if="hasHeader"
224
- class="font-semibold mb-1"
225
- >
210
+ <div ref="body" :class="baseClass">
211
+ <div v-if="$slots.header || props.header" class="font-semibold mb-1">
226
212
  <slot name="header">
227
- <p>{{ header }}</p>
213
+ <p>{{ props.header }}</p>
228
214
  </slot>
229
215
  </div>
230
216
 
231
217
  <slot name="content">
232
- {{ text }}
218
+ {{ props.text }}
233
219
  </slot>
234
220
  </div>
235
221
  </div>
236
- </Transition>
222
+ </transition>
237
223
  </div>
238
224
  </template>
239
225
 
240
226
  <style scoped>
241
-
227
+ /* simple fade (you can adapt to your transition) */
228
+ .fade-enter-active, .fade-leave-active {
229
+ transition: opacity .18s ease;
230
+ }
231
+ .fade-enter-from, .fade-leave-to {
232
+ opacity: 0;
233
+ }
234
+ .fade-enter-to, .fade-leave-from {
235
+ opacity: 1;
236
+ }
242
237
  </style>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "adata-ui",
3
3
  "type": "module",
4
- "version": "2.0.80",
4
+ "version": "2.0.82",
5
5
  "main": "./nuxt.config.ts",
6
6
  "scripts": {
7
7
  "dev": "nuxi dev .playground",