@luanlu/mk-motion 1.1.0 → 1.2.1

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.
Files changed (131) hide show
  1. package/package.json +14 -2
  2. package/src/a11y/focus-trap.ts +64 -0
  3. package/src/a11y/keyboard.ts +43 -0
  4. package/src/components/alert/alert.css +111 -0
  5. package/src/components/alert/alert.ts +107 -0
  6. package/src/components/avatar/avatar.css +112 -0
  7. package/src/components/avatar/avatar.ts +175 -0
  8. package/src/components/breadcrumb/breadcrumb.css +31 -0
  9. package/src/components/breadcrumb/breadcrumb.ts +71 -0
  10. package/src/components/button/button.css +108 -0
  11. package/src/components/button/button.ts +140 -0
  12. package/src/components/card/card.css +52 -0
  13. package/src/components/card/card.ts +87 -0
  14. package/src/components/collapse/collapse.css +76 -0
  15. package/src/components/collapse/collapse.ts +168 -0
  16. package/src/components/dialog/dialog.css +78 -0
  17. package/src/components/dialog/dialog.ts +164 -0
  18. package/src/components/drawer/drawer.css +73 -0
  19. package/src/components/drawer/drawer.ts +131 -0
  20. package/src/components/empty/empty.css +36 -0
  21. package/src/components/empty/empty.ts +85 -0
  22. package/src/components/form/checkbox.css +56 -0
  23. package/src/components/form/checkbox.ts +119 -0
  24. package/src/components/form/radio.css +57 -0
  25. package/src/components/form/radio.ts +153 -0
  26. package/src/components/form/select.css +91 -0
  27. package/src/components/form/select.ts +174 -0
  28. package/src/components/form/slider.css +56 -0
  29. package/src/components/form/slider.ts +148 -0
  30. package/src/components/input/input.css +92 -0
  31. package/src/components/input/input.ts +162 -0
  32. package/src/components/layout/divider.css +32 -0
  33. package/src/components/layout/divider.ts +42 -0
  34. package/src/components/layout/row.css +64 -0
  35. package/src/components/layout/row.ts +57 -0
  36. package/src/components/layout/space.css +14 -0
  37. package/src/components/layout/space.ts +48 -0
  38. package/src/components/loading/loading.css +37 -0
  39. package/src/components/loading/loading.ts +46 -0
  40. package/src/components/menu/menu.css +121 -0
  41. package/src/components/menu/menu.ts +187 -0
  42. package/src/components/message/message.css +64 -0
  43. package/src/components/message/message.ts +96 -0
  44. package/src/components/popover/popover.css +73 -0
  45. package/src/components/popover/popover.ts +279 -0
  46. package/src/components/progress/progress.css +112 -0
  47. package/src/components/progress/progress.ts +171 -0
  48. package/src/components/steps/steps.css +127 -0
  49. package/src/components/steps/steps.ts +102 -0
  50. package/src/components/styles/components.css +28 -0
  51. package/src/components/styles/reset.css +24 -0
  52. package/src/components/styles/tokens.css +248 -0
  53. package/src/components/styles/variables.css +24 -0
  54. package/src/components/switch/switch.css +53 -0
  55. package/src/components/switch/switch.ts +103 -0
  56. package/src/components/table/table.css +192 -0
  57. package/src/components/table/table.ts +370 -0
  58. package/src/components/tabs/tabs.css +138 -0
  59. package/src/components/tabs/tabs.ts +211 -0
  60. package/src/components/tag/tag.css +123 -0
  61. package/src/components/tag/tag.ts +112 -0
  62. package/src/components/tooltip/tooltip.css +66 -0
  63. package/src/components/tooltip/tooltip.ts +185 -0
  64. package/src/core/animator.ts +124 -0
  65. package/src/core/timeline.ts +128 -0
  66. package/src/core/utils.ts +47 -0
  67. package/src/effects/glitch.ts +99 -0
  68. package/src/effects/particle.ts +134 -0
  69. package/src/effects/text-split.ts +95 -0
  70. package/src/effects/wave-text.ts +88 -0
  71. package/src/gesture/draggable.ts +130 -0
  72. package/src/gesture/spring.ts +152 -0
  73. package/src/index.ts +162 -0
  74. package/src/interactive/coverflow.ts +100 -0
  75. package/src/interactive/cursor-trail.ts +113 -0
  76. package/src/interactive/flip-card.ts +114 -0
  77. package/src/interactive/magnetic.ts +121 -0
  78. package/src/micro/hover-lift.ts +94 -0
  79. package/src/micro/ripple.ts +130 -0
  80. package/src/motion/component-motion.ts +177 -0
  81. package/src/nuxt/module.ts +46 -0
  82. package/src/presets/index.ts +69 -0
  83. package/src/scroll/scroll-trigger.ts +104 -0
  84. package/src/styles/animations.css +135 -0
  85. package/src/styles/element-plus.css +174 -0
  86. package/src/text/count-up.ts +108 -0
  87. package/src/text/typewriter.ts +109 -0
  88. package/src/theme/dark.css +19 -0
  89. package/src/theme/light.css +19 -0
  90. package/src/theme/theme.ts +65 -0
  91. package/src/transitions/blur-reveal.ts +92 -0
  92. package/src/transitions/collapse.ts +112 -0
  93. package/src/transitions/lazy-image.ts +87 -0
  94. package/src/transitions/list.ts +75 -0
  95. package/src/transitions/loading.ts +95 -0
  96. package/src/transitions/parallax.ts +60 -0
  97. package/src/transitions/shimmer.ts +105 -0
  98. package/src/transitions/toast.ts +151 -0
  99. package/src/types.d.ts +4 -0
  100. package/src/vite/plugin.ts +45 -0
  101. package/src/vue/button.ts +28 -9
  102. package/src/vue/card.ts +28 -8
  103. package/src/vue/composables/index.ts +4 -0
  104. package/src/vue/composables/useLoading.ts +12 -0
  105. package/src/vue/composables/useMessage.ts +16 -0
  106. package/src/vue/composables/useMotion.ts +19 -0
  107. package/src/vue/composables/useTheme.ts +12 -0
  108. package/src/vue/dialog.ts +69 -17
  109. package/src/vue/index.ts +4 -21
  110. package/src/vue/input.ts +35 -11
  111. package/src/vue/slider.ts +22 -4
  112. package/src/vue/switch.ts +16 -9
  113. package/src/vue/alert.ts +0 -32
  114. package/src/vue/avatar.ts +0 -34
  115. package/src/vue/breadcrumb.ts +0 -32
  116. package/src/vue/checkbox.ts +0 -32
  117. package/src/vue/collapse.ts +0 -33
  118. package/src/vue/divider.ts +0 -32
  119. package/src/vue/drawer.ts +0 -33
  120. package/src/vue/empty.ts +0 -33
  121. package/src/vue/menu.ts +0 -33
  122. package/src/vue/popover.ts +0 -34
  123. package/src/vue/progress.ts +0 -33
  124. package/src/vue/row.ts +0 -32
  125. package/src/vue/select.ts +0 -33
  126. package/src/vue/space.ts +0 -32
  127. package/src/vue/steps.ts +0 -33
  128. package/src/vue/table.ts +0 -33
  129. package/src/vue/tabs.ts +0 -33
  130. package/src/vue/tag.ts +0 -33
  131. package/src/vue/tooltip.ts +0 -34
@@ -0,0 +1,211 @@
1
+ import '../../styles/element-plus.css'
2
+ import './tabs.css'
3
+ import { Keys } from '../../a11y/keyboard'
4
+
5
+ export interface TabItem {
6
+ label: string
7
+ content?: string | HTMLElement
8
+ disabled?: boolean
9
+ }
10
+
11
+ export interface TabsOptions {
12
+ type?: 'line' | 'card' | 'pill'
13
+ items?: TabItem[]
14
+ activeIndex?: number
15
+ onChange?: (index: number) => void
16
+ }
17
+
18
+ let tabsIdCounter = 0
19
+
20
+ export class MkTabs {
21
+ el: HTMLDivElement
22
+ private options: TabsOptions
23
+ private headerEl: HTMLDivElement
24
+ private contentEl: HTMLDivElement
25
+ private indicatorEl: HTMLDivElement | null = null
26
+ private tabItems: HTMLButtonElement[] = []
27
+ private panels: HTMLDivElement[] = []
28
+ private currentIndex: number
29
+ private tabIdPrefix: string
30
+
31
+ constructor(container: HTMLElement | string, options: TabsOptions = {}) {
32
+ const parent =
33
+ typeof container === 'string'
34
+ ? document.querySelector(container)!
35
+ : container
36
+
37
+ this.options = {
38
+ type: 'line',
39
+ items: [],
40
+ activeIndex: 0,
41
+ ...options,
42
+ }
43
+ this.currentIndex = this.options.activeIndex!
44
+ this.tabIdPrefix = `mk-tabs-${++tabsIdCounter}`
45
+
46
+ this.el = document.createElement('div')
47
+ this.el.className = `mk-tabs mk-tabs--${this.options.type}`
48
+
49
+ this.headerEl = document.createElement('div')
50
+ this.headerEl.className = 'mk-tabs__header'
51
+ this.headerEl.setAttribute('role', 'tablist')
52
+ this.el.appendChild(this.headerEl)
53
+
54
+ this.contentEl = document.createElement('div')
55
+ this.contentEl.className = 'mk-tabs__content'
56
+ this.el.appendChild(this.contentEl)
57
+
58
+ if (this.options.type === 'line') {
59
+ this.indicatorEl = document.createElement('div')
60
+ this.indicatorEl.className = 'mk-tabs__indicator'
61
+ this.headerEl.appendChild(this.indicatorEl)
62
+ }
63
+
64
+ this.renderTabs()
65
+ this.setActive(this.currentIndex, false)
66
+
67
+ parent.appendChild(this.el)
68
+ }
69
+
70
+ private renderTabs(): void {
71
+ this.options.items?.forEach((item, index) => {
72
+ const panelId = `${this.tabIdPrefix}-panel-${index}`
73
+ const tabId = `${this.tabIdPrefix}-tab-${index}`
74
+
75
+ const tabBtn = document.createElement('button')
76
+ tabBtn.className = 'mk-tabs__item'
77
+ tabBtn.type = 'button'
78
+ tabBtn.textContent = item.label
79
+ tabBtn.id = tabId
80
+ tabBtn.setAttribute('role', 'tab')
81
+ tabBtn.setAttribute('aria-controls', panelId)
82
+ if (item.disabled) {
83
+ tabBtn.disabled = true
84
+ tabBtn.classList.add('is-disabled')
85
+ }
86
+ tabBtn.addEventListener('click', () => {
87
+ if (item.disabled) return
88
+ this.setActive(index)
89
+ })
90
+ this.headerEl.appendChild(tabBtn)
91
+ this.tabItems.push(tabBtn)
92
+
93
+ const panel = document.createElement('div')
94
+ panel.className = 'mk-tabs__panel'
95
+ panel.id = panelId
96
+ panel.setAttribute('role', 'tabpanel')
97
+ panel.setAttribute('aria-labelledby', tabId)
98
+ if (item.content) {
99
+ if (typeof item.content === 'string') {
100
+ panel.textContent = item.content
101
+ } else {
102
+ panel.appendChild(item.content)
103
+ }
104
+ }
105
+ this.contentEl.appendChild(panel)
106
+ this.panels.push(panel)
107
+ })
108
+
109
+ this.headerEl.addEventListener('keydown', (e) => {
110
+ if (![Keys.ArrowLeft, Keys.ArrowRight, Keys.Home, Keys.End].includes(e.key as any)) return
111
+ const tabs = this.tabItems.filter((_, i) => !this.options.items?.[i]?.disabled)
112
+ const activeTab = this.tabItems[this.currentIndex]
113
+ let currentIdx = tabs.indexOf(activeTab)
114
+ if (currentIdx === -1) currentIdx = 0
115
+
116
+ let nextIdx = currentIdx
117
+ switch (e.key) {
118
+ case Keys.ArrowLeft:
119
+ e.preventDefault()
120
+ nextIdx = currentIdx > 0 ? currentIdx - 1 : tabs.length - 1
121
+ break
122
+ case Keys.ArrowRight:
123
+ e.preventDefault()
124
+ nextIdx = currentIdx < tabs.length - 1 ? currentIdx + 1 : 0
125
+ break
126
+ case Keys.Home:
127
+ e.preventDefault()
128
+ nextIdx = 0
129
+ break
130
+ case Keys.End:
131
+ e.preventDefault()
132
+ nextIdx = tabs.length - 1
133
+ break
134
+ }
135
+ const nextTab = tabs[nextIdx]
136
+ if (nextTab) {
137
+ const realIndex = this.tabItems.indexOf(nextTab)
138
+ this.setActive(realIndex)
139
+ nextTab.focus()
140
+ }
141
+ })
142
+ }
143
+
144
+ setActive(index: number, animate = true): void {
145
+ if (index < 0 || index >= this.tabItems.length) return
146
+ if (this.options.items?.[index]?.disabled) return
147
+
148
+ const prevIndex = this.currentIndex
149
+ this.currentIndex = index
150
+
151
+ this.tabItems.forEach((tab, i) => {
152
+ const isActive = i === index
153
+ tab.classList.toggle('is-active', isActive)
154
+ tab.setAttribute('aria-selected', String(isActive))
155
+ tab.setAttribute('tabindex', isActive ? '0' : '-1')
156
+ })
157
+
158
+ this.panels.forEach((panel, i) => {
159
+ const isActive = i === index
160
+ panel.classList.toggle('is-active', isActive)
161
+
162
+ if (isActive) {
163
+ panel.style.display = 'block'
164
+ if (animate) {
165
+ requestAnimationFrame(() => {
166
+ panel.classList.add('is-entering')
167
+ requestAnimationFrame(() => {
168
+ panel.classList.remove('is-entering')
169
+ })
170
+ })
171
+ }
172
+ } else {
173
+ panel.style.display = 'none'
174
+ panel.classList.remove('is-entering')
175
+ }
176
+ })
177
+
178
+ if (this.indicatorEl) {
179
+ this.updateIndicator()
180
+ }
181
+
182
+ if (prevIndex !== index) {
183
+ this.options.onChange?.(index)
184
+ }
185
+ }
186
+
187
+ private updateIndicator(): void {
188
+ if (!this.indicatorEl) return
189
+ const activeTab = this.tabItems[this.currentIndex]
190
+ if (!activeTab) return
191
+ const headerRect = this.headerEl.getBoundingClientRect()
192
+ const tabRect = activeTab.getBoundingClientRect()
193
+ this.indicatorEl.style.left = `${tabRect.left - headerRect.left + this.headerEl.scrollLeft}px`
194
+ this.indicatorEl.style.width = `${tabRect.width}px`
195
+ }
196
+
197
+ getActive(): number {
198
+ return this.currentIndex
199
+ }
200
+
201
+ destroy(): void {
202
+ this.el.remove()
203
+ }
204
+ }
205
+
206
+ export function createTabs(
207
+ container: HTMLElement | string,
208
+ options?: TabsOptions
209
+ ): MkTabs {
210
+ return new MkTabs(container, options)
211
+ }
@@ -0,0 +1,123 @@
1
+ .mk-tag {
2
+ display: inline-flex;
3
+ align-items: center;
4
+ gap: 4px;
5
+ padding: 0 8px;
6
+ height: 24px;
7
+ font-size: var(--mk-text-xs);
8
+ font-weight: var(--mk-font-medium);
9
+ line-height: 1;
10
+ color: var(--mk-text);
11
+ background: var(--mk-surface-raised);
12
+ border: 1px solid var(--mk-border);
13
+ border-radius: var(--mk-radius-sm);
14
+ white-space: nowrap;
15
+ user-select: none;
16
+ transition: var(--mk-transition-all);
17
+ vertical-align: middle;
18
+ }
19
+
20
+ .mk-tag.is-round {
21
+ border-radius: var(--mk-radius-full);
22
+ }
23
+
24
+ .mk-tag.is-closable {
25
+ padding-right: 4px;
26
+ }
27
+
28
+ /* Sizes */
29
+ .mk-tag--small {
30
+ padding: 0 6px;
31
+ height: 20px;
32
+ font-size: 11px;
33
+ }
34
+ .mk-tag--small.is-closable {
35
+ padding-right: 2px;
36
+ }
37
+
38
+ .mk-tag--large {
39
+ padding: 0 12px;
40
+ height: 32px;
41
+ font-size: var(--mk-text-sm);
42
+ }
43
+ .mk-tag--large.is-closable {
44
+ padding-right: 6px;
45
+ }
46
+
47
+ /* Types */
48
+ .mk-tag--primary {
49
+ background: var(--mk-primary-soft);
50
+ border-color: var(--mk-primary);
51
+ color: var(--mk-primary-hover);
52
+ }
53
+ .mk-tag--success {
54
+ background: var(--mk-success-soft);
55
+ border-color: var(--mk-success);
56
+ color: var(--mk-green-400);
57
+ }
58
+ .mk-tag--warning {
59
+ background: var(--mk-warning-soft);
60
+ border-color: var(--mk-warning);
61
+ color: var(--mk-amber-400);
62
+ }
63
+ .mk-tag--danger {
64
+ background: var(--mk-danger-soft);
65
+ border-color: var(--mk-danger);
66
+ color: var(--mk-red-400);
67
+ }
68
+ .mk-tag--info {
69
+ background: var(--mk-info-soft);
70
+ border-color: var(--mk-info);
71
+ color: var(--mk-sky-400);
72
+ }
73
+
74
+ /* Plain */
75
+ .mk-tag.is-plain {
76
+ background: transparent;
77
+ }
78
+ .mk-tag--primary.is-plain {
79
+ border-color: var(--mk-primary);
80
+ color: var(--mk-primary-hover);
81
+ }
82
+ .mk-tag--success.is-plain {
83
+ border-color: var(--mk-success);
84
+ color: var(--mk-green-400);
85
+ }
86
+ .mk-tag--warning.is-plain {
87
+ border-color: var(--mk-warning);
88
+ color: var(--mk-amber-400);
89
+ }
90
+ .mk-tag--danger.is-plain {
91
+ border-color: var(--mk-danger);
92
+ color: var(--mk-red-400);
93
+ }
94
+ .mk-tag--info.is-plain {
95
+ border-color: var(--mk-info);
96
+ color: var(--mk-sky-400);
97
+ }
98
+
99
+ /* Close button */
100
+ .mk-tag__close {
101
+ display: inline-flex;
102
+ align-items: center;
103
+ justify-content: center;
104
+ width: 16px;
105
+ height: 16px;
106
+ font-size: 14px;
107
+ line-height: 1;
108
+ color: inherit;
109
+ opacity: 0.6;
110
+ cursor: pointer;
111
+ border-radius: var(--mk-radius-sm);
112
+ transition: var(--mk-transition-all);
113
+ }
114
+ .mk-tag__close:hover {
115
+ opacity: 1;
116
+ background: rgba(255, 255, 255, 0.1);
117
+ }
118
+
119
+ /* Closing animation */
120
+ .mk-tag.is-closing {
121
+ transform: scale(0.85);
122
+ opacity: 0;
123
+ }
@@ -0,0 +1,112 @@
1
+ import '../../styles/element-plus.css'
2
+ import './tag.css'
3
+ import { withMotion, type MotionOptions } from '../../motion/component-motion.ts'
4
+ import { onKey, Keys } from '../../a11y/keyboard.ts'
5
+
6
+ export interface TagOptions {
7
+ type?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info'
8
+ size?: 'small' | 'default' | 'large'
9
+ closable?: boolean
10
+ round?: boolean
11
+ plain?: boolean
12
+ text?: string
13
+ onClose?: () => void
14
+ motion?: MotionOptions
15
+ }
16
+
17
+ export class MkTag {
18
+ el: HTMLSpanElement
19
+ private options: TagOptions
20
+ private motion: ReturnType<typeof withMotion> | null = null
21
+ private _cleanupCloseKey?: () => void
22
+
23
+ constructor(container: HTMLElement | string, options: TagOptions = {}) {
24
+ const parent =
25
+ typeof container === 'string'
26
+ ? document.querySelector(container)!
27
+ : container
28
+
29
+ this.options = {
30
+ type: 'default',
31
+ size: 'default',
32
+ ...options,
33
+ }
34
+
35
+ this.el = document.createElement('span')
36
+ this.el.className = this.buildClass()
37
+
38
+ if (this.options.text) {
39
+ this.el.textContent = this.options.text
40
+ }
41
+
42
+ if (this.options.closable) {
43
+ const closeBtn = document.createElement('span')
44
+ closeBtn.className = 'mk-tag__close'
45
+ closeBtn.innerHTML = '×'
46
+ closeBtn.setAttribute('role', 'button')
47
+ closeBtn.setAttribute('tabindex', '0')
48
+ closeBtn.setAttribute('aria-label', 'Close')
49
+ closeBtn.addEventListener('click', (e) => {
50
+ e.stopPropagation()
51
+ this.close()
52
+ })
53
+ this._cleanupCloseKey = onKey(closeBtn, [
54
+ { key: Keys.Enter, handler: () => this.close() },
55
+ { key: Keys.Space, handler: () => this.close() },
56
+ ])
57
+ this.el.appendChild(closeBtn)
58
+ }
59
+
60
+ parent.appendChild(this.el)
61
+
62
+ this.motion = withMotion(this.el, options.motion || { hover: 'scale', enter: 'zoomIn', duration: 200 })
63
+ }
64
+
65
+ private buildClass(): string {
66
+ const classes = ['mk-tag']
67
+ if (this.options.type && this.options.type !== 'default') {
68
+ classes.push(`mk-tag--${this.options.type}`)
69
+ }
70
+ if (this.options.size && this.options.size !== 'default') {
71
+ classes.push(`mk-tag--${this.options.size}`)
72
+ }
73
+ if (this.options.plain) classes.push('is-plain')
74
+ if (this.options.round) classes.push('is-round')
75
+ if (this.options.closable) classes.push('is-closable')
76
+ return classes.join(' ')
77
+ }
78
+
79
+ setText(text: string): void {
80
+ this.options.text = text
81
+ if (this.options.closable) {
82
+ const closeBtn = this.el.querySelector('.mk-tag__close')
83
+ this.el.childNodes.forEach((node) => {
84
+ if (node !== closeBtn) node.remove()
85
+ })
86
+ this.el.insertBefore(document.createTextNode(text), closeBtn)
87
+ } else {
88
+ this.el.textContent = text
89
+ }
90
+ }
91
+
92
+ private close(): void {
93
+ this.el.style.pointerEvents = 'none'
94
+ this.motion?.playExit().then(() => {
95
+ this.destroy()
96
+ })
97
+ this.options.onClose?.()
98
+ }
99
+
100
+ destroy(): void {
101
+ this._cleanupCloseKey?.()
102
+ this.motion?.destroy()
103
+ this.el.remove()
104
+ }
105
+ }
106
+
107
+ export function createTag(
108
+ container: HTMLElement | string,
109
+ options?: TagOptions
110
+ ): MkTag {
111
+ return new MkTag(container, options)
112
+ }
@@ -0,0 +1,66 @@
1
+ .mk-tooltip {
2
+ display: none;
3
+ pointer-events: none;
4
+ opacity: 0;
5
+ transform: scale(0.96) translateY(2px);
6
+ transition: opacity var(--mk-duration-fast) var(--mk-ease-default),
7
+ transform var(--mk-duration-fast) var(--mk-ease-out);
8
+ filter: drop-shadow(var(--mk-shadow-md));
9
+ max-width: 280px;
10
+ }
11
+
12
+ .mk-tooltip.is-visible {
13
+ opacity: 1;
14
+ transform: scale(1) translateY(0);
15
+ }
16
+
17
+ .mk-tooltip__content {
18
+ padding: 6px 10px;
19
+ font-size: var(--mk-text-xs);
20
+ font-weight: var(--mk-font-medium);
21
+ line-height: var(--mk-leading-snug);
22
+ color: var(--mk-text);
23
+ background: var(--mk-surface-raised);
24
+ border: 1px solid var(--mk-border-hover);
25
+ border-radius: var(--mk-radius-md);
26
+ }
27
+
28
+ .mk-tooltip__arrow {
29
+ position: absolute;
30
+ width: 8px;
31
+ height: 8px;
32
+ background: var(--mk-surface-raised);
33
+ border: 1px solid var(--mk-border-hover);
34
+ }
35
+
36
+ .mk-tooltip__arrow.is-top {
37
+ top: -4px;
38
+ left: 50%;
39
+ transform: translateX(-50%) rotate(45deg);
40
+ border-right: none;
41
+ border-bottom: none;
42
+ }
43
+
44
+ .mk-tooltip__arrow.is-bottom {
45
+ bottom: -4px;
46
+ left: 50%;
47
+ transform: translateX(-50%) rotate(45deg);
48
+ border-left: none;
49
+ border-top: none;
50
+ }
51
+
52
+ .mk-tooltip__arrow.is-left {
53
+ left: -4px;
54
+ top: 50%;
55
+ transform: translateY(-50%) rotate(45deg);
56
+ border-right: none;
57
+ border-top: none;
58
+ }
59
+
60
+ .mk-tooltip__arrow.is-right {
61
+ right: -4px;
62
+ top: 50%;
63
+ transform: translateY(-50%) rotate(45deg);
64
+ border-left: none;
65
+ border-bottom: none;
66
+ }
@@ -0,0 +1,185 @@
1
+ import '../../styles/element-plus.css'
2
+ import './tooltip.css'
3
+
4
+ export interface TooltipOptions {
5
+ content?: string | HTMLElement
6
+ placement?: 'top' | 'bottom' | 'left' | 'right'
7
+ delay?: number
8
+ offset?: number
9
+ }
10
+
11
+ let tooltipEl: HTMLDivElement | null = null
12
+ let arrowEl: HTMLDivElement | null = null
13
+ let contentEl: HTMLDivElement | null = null
14
+ let showTimer: ReturnType<typeof setTimeout> | null = null
15
+ let hideTimer: ReturnType<typeof setTimeout> | null = null
16
+ let activeTarget: HTMLElement | null = null
17
+ const TOOLTIP_ID = 'mk-tooltip'
18
+
19
+ function getTooltip(): HTMLDivElement {
20
+ if (!tooltipEl) {
21
+ tooltipEl = document.createElement('div')
22
+ tooltipEl.className = 'mk-tooltip'
23
+ tooltipEl.id = TOOLTIP_ID
24
+ tooltipEl.setAttribute('role', 'tooltip')
25
+ tooltipEl.style.position = 'absolute'
26
+ tooltipEl.style.zIndex = 'var(--mk-z-tooltip)'
27
+
28
+ arrowEl = document.createElement('div')
29
+ arrowEl.className = 'mk-tooltip__arrow'
30
+ tooltipEl.appendChild(arrowEl)
31
+
32
+ contentEl = document.createElement('div')
33
+ contentEl.className = 'mk-tooltip__content'
34
+ tooltipEl.appendChild(contentEl)
35
+
36
+ document.body.appendChild(tooltipEl)
37
+ }
38
+ return tooltipEl
39
+ }
40
+
41
+ function setTooltipContent(content: string | HTMLElement): void {
42
+ const tip = getTooltip()
43
+ const c = tip.querySelector('.mk-tooltip__content') as HTMLDivElement
44
+ c.innerHTML = ''
45
+ if (typeof content === 'string') {
46
+ c.textContent = content
47
+ } else {
48
+ c.appendChild(content)
49
+ }
50
+ }
51
+
52
+ function positionTooltip(target: HTMLElement, placement: string, offset: number): void {
53
+ const tip = getTooltip()
54
+ const arrow = tip.querySelector('.mk-tooltip__arrow') as HTMLDivElement
55
+ const rect = target.getBoundingClientRect()
56
+ const tipRect = tip.getBoundingClientRect()
57
+ const scrollX = window.scrollX
58
+ const scrollY = window.scrollY
59
+
60
+ let top = 0
61
+ let left = 0
62
+ let arrowClass = ''
63
+
64
+ switch (placement) {
65
+ case 'top':
66
+ top = rect.top + scrollY - tipRect.height - offset
67
+ left = rect.left + scrollX + rect.width / 2 - tipRect.width / 2
68
+ arrowClass = 'is-bottom'
69
+ break
70
+ case 'bottom':
71
+ top = rect.bottom + scrollY + offset
72
+ left = rect.left + scrollX + rect.width / 2 - tipRect.width / 2
73
+ arrowClass = 'is-top'
74
+ break
75
+ case 'left':
76
+ top = rect.top + scrollY + rect.height / 2 - tipRect.height / 2
77
+ left = rect.left + scrollX - tipRect.width - offset
78
+ arrowClass = 'is-right'
79
+ break
80
+ case 'right':
81
+ top = rect.top + scrollY + rect.height / 2 - tipRect.height / 2
82
+ left = rect.right + scrollX + offset
83
+ arrowClass = 'is-left'
84
+ break
85
+ default:
86
+ top = rect.top + scrollY - tipRect.height - offset
87
+ left = rect.left + scrollX + rect.width / 2 - tipRect.width / 2
88
+ arrowClass = 'is-bottom'
89
+ }
90
+
91
+ // Viewport boundary adjustments
92
+ const padding = 8
93
+ if (left < padding) left = padding
94
+ if (left + tipRect.width > window.innerWidth - padding) {
95
+ left = window.innerWidth - tipRect.width - padding
96
+ }
97
+ if (top < padding) top = padding
98
+
99
+ tip.style.top = `${top}px`
100
+ tip.style.left = `${left}px`
101
+
102
+ arrow.className = `mk-tooltip__arrow ${arrowClass}`
103
+ }
104
+
105
+ function showTooltip(target: HTMLElement, options: Required<TooltipOptions>): void {
106
+ if (hideTimer) {
107
+ clearTimeout(hideTimer)
108
+ hideTimer = null
109
+ }
110
+
111
+ activeTarget = target
112
+ target.setAttribute('aria-describedby', TOOLTIP_ID)
113
+ setTooltipContent(options.content)
114
+
115
+ const tip = getTooltip()
116
+ tip.classList.remove('is-visible')
117
+
118
+ // Force layout to measure before positioning
119
+ tip.style.visibility = 'hidden'
120
+ tip.style.display = 'block'
121
+
122
+ requestAnimationFrame(() => {
123
+ if (activeTarget !== target) return
124
+ positionTooltip(target, options.placement, options.offset)
125
+ tip.style.visibility = 'visible'
126
+ tip.classList.add('is-visible')
127
+ })
128
+ }
129
+
130
+ function hideTooltip(): void {
131
+ if (!tooltipEl) return
132
+ tooltipEl.classList.remove('is-visible')
133
+ hideTimer = setTimeout(() => {
134
+ if (!tooltipEl?.classList.contains('is-visible')) {
135
+ tooltipEl!.style.display = 'none'
136
+ }
137
+ }, 200)
138
+ if (activeTarget) {
139
+ activeTarget.removeAttribute('aria-describedby')
140
+ }
141
+ activeTarget = null
142
+ }
143
+
144
+ export function createTooltip(
145
+ target: HTMLElement,
146
+ options: TooltipOptions = {}
147
+ ): () => void {
148
+ const opts: Required<TooltipOptions> = {
149
+ content: '',
150
+ placement: 'top',
151
+ delay: 150,
152
+ offset: 8,
153
+ ...options,
154
+ }
155
+
156
+ const onEnter = () => {
157
+ if (showTimer) clearTimeout(showTimer)
158
+ showTimer = setTimeout(() => {
159
+ showTooltip(target, opts)
160
+ }, opts.delay)
161
+ }
162
+
163
+ const onLeave = () => {
164
+ if (showTimer) {
165
+ clearTimeout(showTimer)
166
+ showTimer = null
167
+ }
168
+ hideTooltip()
169
+ }
170
+
171
+ target.addEventListener('mouseenter', onEnter)
172
+ target.addEventListener('mouseleave', onLeave)
173
+ target.addEventListener('focus', onEnter)
174
+ target.addEventListener('blur', onLeave)
175
+
176
+ return () => {
177
+ target.removeEventListener('mouseenter', onEnter)
178
+ target.removeEventListener('mouseleave', onLeave)
179
+ target.removeEventListener('focus', onEnter)
180
+ target.removeEventListener('blur', onLeave)
181
+ if (activeTarget === target) {
182
+ hideTooltip()
183
+ }
184
+ }
185
+ }