@wishbone-media/spark 0.35.0 → 0.37.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.
@@ -0,0 +1,228 @@
1
+ import { computePosition, offset, flip, shift, arrow } from '@floating-ui/vue'
2
+
3
+ /**
4
+ * v-spark-tooltip directive for adding tooltips to any element.
5
+ *
6
+ * Usage:
7
+ * v-spark-tooltip="'Simple text'"
8
+ * v-spark-tooltip="{ content: 'HTML <b>bold</b>', html: true }"
9
+ * v-spark-tooltip="{ content: 'Tip', placement: 'bottom', offset: 12, delay: { show: 500, hide: 100 } }"
10
+ *
11
+ * Empty/falsy content = no tooltip.
12
+ */
13
+
14
+ const TOOLTIP_CLASS = 'spark-tooltip'
15
+ const ARROW_CLASS = 'spark-tooltip-arrow'
16
+
17
+ function normalizeConfig(value) {
18
+ if (!value) return { content: '' }
19
+ if (typeof value === 'string') return { content: value }
20
+ return {
21
+ content: '',
22
+ placement: 'top',
23
+ offset: 8,
24
+ html: false,
25
+ delay: { show: 200, hide: 0 },
26
+ arrow: true,
27
+ ...value,
28
+ }
29
+ }
30
+
31
+ function createTooltipElement(config) {
32
+ const tooltip = document.createElement('div')
33
+ tooltip.className = TOOLTIP_CLASS
34
+ tooltip.setAttribute('role', 'tooltip')
35
+
36
+ if (config.html) {
37
+ tooltip.innerHTML = config.content
38
+ } else {
39
+ tooltip.textContent = config.content
40
+ }
41
+
42
+ if (config.arrow) {
43
+ const arrowEl = document.createElement('div')
44
+ arrowEl.className = ARROW_CLASS
45
+ tooltip.appendChild(arrowEl)
46
+ }
47
+
48
+ return tooltip
49
+ }
50
+
51
+ async function updatePosition(el, tooltip, config) {
52
+ const arrowEl = tooltip.querySelector(`.${ARROW_CLASS}`)
53
+
54
+ const middleware = [
55
+ offset(config.offset ?? 8),
56
+ flip(),
57
+ shift({ padding: 8 }),
58
+ ]
59
+
60
+ if (arrowEl) {
61
+ middleware.push(arrow({ element: arrowEl, padding: 5 }))
62
+ }
63
+
64
+ const { x, y, placement, middlewareData } = await computePosition(el, tooltip, {
65
+ placement: config.placement || 'top',
66
+ strategy: 'fixed',
67
+ middleware,
68
+ })
69
+
70
+ Object.assign(tooltip.style, {
71
+ left: `${x}px`,
72
+ top: `${y}px`,
73
+ })
74
+
75
+ // Position the arrow
76
+ if (arrowEl && middlewareData.arrow) {
77
+ const { x: arrowX, y: arrowY } = middlewareData.arrow
78
+ const side = placement.split('-')[0]
79
+ const staticSide = { top: 'bottom', right: 'left', bottom: 'top', left: 'right' }[side]
80
+
81
+ Object.assign(arrowEl.style, {
82
+ left: arrowX != null ? `${arrowX}px` : '',
83
+ top: arrowY != null ? `${arrowY}px` : '',
84
+ right: '',
85
+ bottom: '',
86
+ [staticSide]: '-4px',
87
+ })
88
+ }
89
+ }
90
+
91
+ function showTooltip(el) {
92
+ const state = el.__sparkTooltip
93
+ if (!state || !state.config.content) return
94
+
95
+ clearTimeout(state.hideTimeout)
96
+
97
+ const delay = typeof state.config.delay === 'number'
98
+ ? state.config.delay
99
+ : state.config.delay?.show ?? 200
100
+
101
+ state.showTimeout = setTimeout(() => {
102
+ if (state.tooltipEl) return // Already visible
103
+
104
+ const tooltip = createTooltipElement(state.config)
105
+ tooltip.style.position = 'fixed'
106
+ tooltip.style.top = '0'
107
+ tooltip.style.left = '0'
108
+ document.body.appendChild(tooltip)
109
+ state.tooltipEl = tooltip
110
+
111
+ updatePosition(el, tooltip, state.config)
112
+ }, delay)
113
+ }
114
+
115
+ function hideTooltip(el) {
116
+ const state = el.__sparkTooltip
117
+ if (!state) return
118
+
119
+ clearTimeout(state.showTimeout)
120
+
121
+ const delay = typeof state.config.delay === 'number'
122
+ ? 0
123
+ : state.config.delay?.hide ?? 0
124
+
125
+ state.hideTimeout = setTimeout(() => {
126
+ if (state.tooltipEl) {
127
+ state.tooltipEl.remove()
128
+ state.tooltipEl = null
129
+ }
130
+ }, delay)
131
+ }
132
+
133
+ function setupListeners(el) {
134
+ const onShow = () => showTooltip(el)
135
+ const onHide = () => hideTooltip(el)
136
+
137
+ el.addEventListener('mouseenter', onShow)
138
+ el.addEventListener('mouseleave', onHide)
139
+ el.addEventListener('focus', onShow)
140
+ el.addEventListener('blur', onHide)
141
+
142
+ el.__sparkTooltip.listeners = { onShow, onHide }
143
+ }
144
+
145
+ function removeListeners(el) {
146
+ const state = el.__sparkTooltip
147
+ if (!state?.listeners) return
148
+
149
+ el.removeEventListener('mouseenter', state.listeners.onShow)
150
+ el.removeEventListener('mouseleave', state.listeners.onHide)
151
+ el.removeEventListener('focus', state.listeners.onShow)
152
+ el.removeEventListener('blur', state.listeners.onHide)
153
+ }
154
+
155
+ function cleanup(el) {
156
+ const state = el.__sparkTooltip
157
+ if (!state) return
158
+
159
+ clearTimeout(state.showTimeout)
160
+ clearTimeout(state.hideTimeout)
161
+ removeListeners(el)
162
+
163
+ if (state.tooltipEl) {
164
+ state.tooltipEl.remove()
165
+ state.tooltipEl = null
166
+ }
167
+
168
+ delete el.__sparkTooltip
169
+ }
170
+
171
+ export const sparkTooltipDirective = {
172
+ mounted(el, binding) {
173
+ const config = normalizeConfig(binding.value)
174
+ el.__sparkTooltip = { config, tooltipEl: null, showTimeout: null, hideTimeout: null }
175
+
176
+ if (config.content) {
177
+ setupListeners(el)
178
+ }
179
+ },
180
+
181
+ updated(el, binding) {
182
+ const config = normalizeConfig(binding.value)
183
+ const state = el.__sparkTooltip
184
+
185
+ if (!state) {
186
+ // Was never initialized (content was empty on mount)
187
+ el.__sparkTooltip = { config, tooltipEl: null, showTimeout: null, hideTimeout: null }
188
+ if (config.content) setupListeners(el)
189
+ return
190
+ }
191
+
192
+ state.config = config
193
+
194
+ // Update visible tooltip content
195
+ if (state.tooltipEl) {
196
+ const arrowEl = state.tooltipEl.querySelector(`.${ARROW_CLASS}`)
197
+ if (config.html) {
198
+ state.tooltipEl.innerHTML = config.content
199
+ } else {
200
+ state.tooltipEl.textContent = config.content
201
+ }
202
+ // Re-append arrow if it was removed by innerHTML/textContent
203
+ if (config.arrow) {
204
+ if (arrowEl) {
205
+ state.tooltipEl.appendChild(arrowEl)
206
+ } else {
207
+ const newArrow = document.createElement('div')
208
+ newArrow.className = ARROW_CLASS
209
+ state.tooltipEl.appendChild(newArrow)
210
+ }
211
+ }
212
+ updatePosition(el, state.tooltipEl, config)
213
+ }
214
+
215
+ // Add/remove listeners if content became available/empty
216
+ if (config.content && !state.listeners) {
217
+ setupListeners(el)
218
+ } else if (!config.content && state.listeners) {
219
+ hideTooltip(el)
220
+ removeListeners(el)
221
+ state.listeners = null
222
+ }
223
+ },
224
+
225
+ unmounted(el) {
226
+ cleanup(el)
227
+ },
228
+ }
@@ -1,5 +1,6 @@
1
1
  import { library, icon as faIconToSvg } from '@fortawesome/fontawesome-svg-core'
2
2
  import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
3
+ import { setupTooltip } from './tooltip.js'
3
4
 
4
5
  import {
5
6
  faArrowLeftToLine,
@@ -58,6 +59,8 @@ import {
58
59
  faAnglesRight,
59
60
  faAnglesLeft,
60
61
  faEyeDropper,
62
+ faCopy,
63
+ faArrowUpRightFromSquare,
61
64
  } from '@fortawesome/pro-regular-svg-icons'
62
65
 
63
66
  import {
@@ -128,6 +131,8 @@ export const Icons = {
128
131
  farAnglesLeft: faAnglesLeft,
129
132
  farEyeDropper: faEyeDropper,
130
133
  farCircleDot: faCircleDot,
134
+ farCopy: faCopy,
135
+ farArrowUpRightFromSquare: faArrowUpRightFromSquare,
131
136
  }
132
137
 
133
138
  /**
@@ -200,4 +205,7 @@ export function addIcons(newIcons) {
200
205
  export function setupFontAwesome(app) {
201
206
  library.add(...Object.values(Icons))
202
207
  app.component('FontAwesomeIcon', FontAwesomeIcon)
208
+
209
+ // Auto-register tooltip directive (used by SparkEntityBadge and available to all consumers)
210
+ setupTooltip(app)
203
211
  }
@@ -1,4 +1,5 @@
1
1
  export { Icons, addIcons, setupFontAwesome, formKitIcons, formKitIconLoader, formKitGenesisOverride } from './fontawesome.js'
2
2
  export { createAuthRoutes, setupAuthGuards, create403Route, create404Route, setupBootstrapGuard, setupDirtyFormGuard } from './router.js'
3
3
  export { createAxiosInstance, setupAxios, getAxiosInstance } from './axios.js'
4
- export { createBootstrapService } from './app-bootstrap.js'
4
+ export { createBootstrapService } from './app-bootstrap.js'
5
+ export { setupTooltip } from './tooltip.js'
@@ -0,0 +1,11 @@
1
+ import { sparkTooltipDirective } from '../directives/sparkTooltip.js'
2
+
3
+ /**
4
+ * Register the v-spark-tooltip directive globally.
5
+ * Call in your app's main.js: setupTooltip(app)
6
+ *
7
+ * @param {import('vue').App} app - Vue app instance
8
+ */
9
+ export function setupTooltip(app) {
10
+ app.directive('spark-tooltip', sparkTooltipDirective)
11
+ }