@wishbone-media/spark 0.36.0 → 0.38.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
+ }
@@ -1,16 +1,18 @@
1
1
  /**
2
2
  * Spark Image Renderer
3
3
  *
4
- * Renders image thumbnails
4
+ * Renders image thumbnails with optional Fancybox lightbox support.
5
5
  *
6
6
  * Usage:
7
7
  * {
8
8
  * data: 'avatar',
9
9
  * renderer: 'spark.image',
10
10
  * rendererConfig: {
11
- * size: 'md', // 'sm' | 'md' | 'lg'
12
- * rounded: true, // true for circular, false for square
13
- * alt: 'User avatar' // Alt text (or use row property name)
11
+ * size: 'md', // 'sm' | 'md' | 'lg'
12
+ * rounded: true, // true for circular, false for square
13
+ * alt: 'User avatar', // Alt text (or use row property name)
14
+ * lightbox: true, // Enable click-to-enlarge via Fancybox (requires SparkFancybox wrapper)
15
+ * group: 'gallery', // Fancybox gallery group name (default: 'table-images')
14
16
  * }
15
17
  * }
16
18
  */
@@ -37,6 +39,8 @@ export const imageRenderer = (sparkTable) => {
37
39
  const size = config.size || 'md'
38
40
  const sizeClass = sizes[size] || sizes.md
39
41
  const rounded = config.rounded !== false // Default to true
42
+ const lightbox = config.lightbox || false
43
+ const group = config.group || 'table-images'
40
44
 
41
45
  // Determine alt text
42
46
  let alt = config.alt || ''
@@ -56,6 +60,17 @@ export const imageRenderer = (sparkTable) => {
56
60
  img.classList.add('rounded')
57
61
  }
58
62
 
59
- td.appendChild(img)
63
+ if (lightbox) {
64
+ // Wrap in <a> with data-fancybox for lightbox support
65
+ const link = document.createElement('a')
66
+ link.href = value
67
+ link.setAttribute('data-fancybox', group)
68
+ if (alt) link.setAttribute('data-caption', alt)
69
+ img.classList.add('cursor-pointer', 'hover:opacity-80', 'transition-opacity')
70
+ link.appendChild(img)
71
+ td.appendChild(link)
72
+ } else {
73
+ td.appendChild(img)
74
+ }
60
75
  }
61
76
  }