@wishbone-media/spark 0.19.0 → 0.20.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wishbone-media/spark",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -1,4 +1,5 @@
1
1
  @import './fonts.css';
2
+ @import './nprogress.css';
2
3
 
3
4
  :root {
4
5
  font-family:
@@ -0,0 +1,6 @@
1
+ @import 'nprogress/nprogress.css';
2
+
3
+ #nprogress .bar {
4
+ @apply bg-primary-300 drop-shadow-md;
5
+ height: 2px;
6
+ }
@@ -1,4 +1,5 @@
1
1
  import { reactive, markRaw } from 'vue'
2
+ import SparkModalDialog from '@/components/SparkModalDialog.vue'
2
3
 
3
4
  class SparkModalService {
4
5
  constructor() {
@@ -21,6 +22,54 @@ class SparkModalService {
21
22
  this.state.isVisible = false
22
23
  this.state.eventHandlers = {}
23
24
  }
25
+
26
+ /**
27
+ * Show a confirmation dialog and return a Promise
28
+ *
29
+ * @param {Object} options - Confirmation options
30
+ * @param {string} [options.title='Confirm'] - Dialog title
31
+ * @param {string} [options.message='Are you sure?'] - Dialog message
32
+ * @param {string} [options.type='warning'] - Dialog type (info, success, warning, danger)
33
+ * @param {string} [options.confirmText='Confirm'] - Confirm button text
34
+ * @param {string} [options.cancelText='Cancel'] - Cancel button text
35
+ * @param {string} [options.confirmVariant='primary'] - Confirm button variant
36
+ * @returns {Promise<boolean>} - Resolves to true if confirmed, false if cancelled
37
+ */
38
+ confirm = (options = {}) => {
39
+ return new Promise((resolve) => {
40
+ const {
41
+ title = 'Confirm',
42
+ message = 'Are you sure?',
43
+ type = 'warning',
44
+ confirmText = 'Confirm',
45
+ cancelText = 'Cancel',
46
+ confirmVariant = 'primary',
47
+ } = options
48
+
49
+ this.show(
50
+ SparkModalDialog,
51
+ {
52
+ title,
53
+ message,
54
+ type,
55
+ buttons: [
56
+ { text: confirmText, variant: confirmVariant, event: 'confirm' },
57
+ { text: cancelText, variant: 'secondary', event: 'cancel' },
58
+ ],
59
+ },
60
+ {
61
+ confirm: () => {
62
+ this.hide()
63
+ resolve(true)
64
+ },
65
+ cancel: () => {
66
+ this.hide()
67
+ resolve(false)
68
+ },
69
+ },
70
+ )
71
+ })
72
+ }
24
73
  }
25
74
 
26
75
  export const sparkModalService = new SparkModalService()
@@ -203,6 +203,19 @@ export const useSparkTableRouteSync = (sparkTable, options = {}) => {
203
203
  return false
204
204
  }
205
205
 
206
+ /**
207
+ * Coerce URL query param values to their proper types
208
+ * URL params are always strings, but page/limit should be integers
209
+ */
210
+ const coerceParamValue = (key, value) => {
211
+ const numericParams = ['page', 'limit']
212
+ if (numericParams.includes(key) && value !== null && value !== undefined) {
213
+ const parsed = parseInt(value, 10)
214
+ return isNaN(parsed) ? value : parsed
215
+ }
216
+ return value
217
+ }
218
+
206
219
  /**
207
220
  * Restore params from route query to sparkTable
208
221
  */
@@ -211,7 +224,7 @@ export const useSparkTableRouteSync = (sparkTable, options = {}) => {
211
224
  // Flat mode: read any query param that looks like a table param
212
225
  Object.keys(route.query).forEach((key) => {
213
226
  if (isTableParam(key)) {
214
- sparkTable.params[key] = route.query[key]
227
+ sparkTable.params[key] = coerceParamValue(key, route.query[key])
215
228
  }
216
229
  })
217
230
  } else {
@@ -224,7 +237,7 @@ export const useSparkTableRouteSync = (sparkTable, options = {}) => {
224
237
  // e.g., "table[selectFilters[is_quote]]" => "selectFilters[is_quote]"
225
238
  // e.g., "table[page]" => "page"
226
239
  const paramKey = key.slice(prefix.length, -1)
227
- sparkTable.params[paramKey] = route.query[key]
240
+ sparkTable.params[paramKey] = coerceParamValue(paramKey, route.query[key])
228
241
  }
229
242
  })
230
243
  }
@@ -275,6 +288,10 @@ export const useSparkTableRouteSync = (sparkTable, options = {}) => {
275
288
  if (!hasUrlParams && persistToStorage) {
276
289
  const storedParams = loadFromStorage()
277
290
  if (storedParams && Object.keys(storedParams).length > 0) {
291
+ // Coerce numeric params in case localStorage has string values from previous buggy saves
292
+ Object.keys(storedParams).forEach((key) => {
293
+ storedParams[key] = coerceParamValue(key, storedParams[key])
294
+ })
278
295
  Object.assign(sparkTable.params, storedParams)
279
296
  }
280
297
  }
@@ -1,4 +1,5 @@
1
1
  import { icon } from '@fortawesome/fontawesome-svg-core'
2
+ import { sparkModalService } from '@/composables/sparkModalService'
2
3
 
3
4
  /**
4
5
  * Spark Actions Renderer
@@ -25,6 +26,16 @@ import { icon } from '@fortawesome/fontawesome-svg-core'
25
26
  * {
26
27
  * icon: 'download',
27
28
  * handler: (row) => downloadFile(row.file_url)
29
+ * },
30
+ * {
31
+ * icon: 'trash',
32
+ * label: 'Delete',
33
+ * event: 'deleteRow',
34
+ * confirm: true,
35
+ * confirmTitle: 'Delete Item',
36
+ * confirmType: 'danger',
37
+ * confirmText: 'Delete',
38
+ * cancelText: 'Keep'
28
39
  * }
29
40
  * ]
30
41
  * }
@@ -37,6 +48,11 @@ import { icon } from '@fortawesome/fontawesome-svg-core'
37
48
  * @property {string} [event] - Event name to emit (e.g., 'editRow')
38
49
  * @property {Function} [handler] - Custom handler function (row) => void
39
50
  * @property {boolean|string} [confirm] - Show confirmation dialog (true or custom message)
51
+ * @property {string} [confirmTitle] - Custom confirmation dialog title (default: 'Confirm')
52
+ * @property {string} [confirmType] - Confirmation dialog type: 'info', 'success', 'warning', 'danger' (default: 'warning')
53
+ * @property {string} [confirmText] - Confirm button text (default: 'Confirm')
54
+ * @property {string} [cancelText] - Cancel button text (default: 'Cancel')
55
+ * @property {string} [confirmVariant] - Confirm button variant (default: 'primary')
40
56
  * @property {Function} [condition] - Conditional visibility (row) => boolean
41
57
  */
42
58
 
@@ -89,7 +105,7 @@ export const actionsRenderer = (sparkTable) => {
89
105
  }
90
106
 
91
107
  // Add click handler
92
- button.addEventListener('click', (e) => {
108
+ button.addEventListener('click', async (e) => {
93
109
  e.preventDefault()
94
110
  e.stopPropagation()
95
111
 
@@ -100,7 +116,16 @@ export const actionsRenderer = (sparkTable) => {
100
116
  ? action.confirm
101
117
  : `Are you sure you want to ${action.label?.toLowerCase() || 'perform this action'}?`
102
118
 
103
- if (!window.confirm(confirmMessage)) {
119
+ const confirmed = await sparkModalService.confirm({
120
+ title: action.confirmTitle,
121
+ message: confirmMessage,
122
+ type: action.confirmType,
123
+ confirmText: action.confirmText,
124
+ cancelText: action.cancelText,
125
+ confirmVariant: action.confirmVariant,
126
+ })
127
+
128
+ if (!confirmed) {
104
129
  return
105
130
  }
106
131
  }
@@ -0,0 +1,97 @@
1
+ import { icon } from '@fortawesome/fontawesome-svg-core'
2
+
3
+ /**
4
+ * Spark Boolean Renderer
5
+ *
6
+ * Renders boolean-like values as check/times icons in a circular container
7
+ *
8
+ * Truthy values: true, 1, '1', 'yes', 'true'
9
+ * Falsy values: false, 0, '0', 'no', 'false', null, undefined, ''
10
+ *
11
+ * Usage:
12
+ * {
13
+ * data: 'is_active',
14
+ * renderer: 'spark.boolean',
15
+ * rendererConfig: {
16
+ * trueIcon: 'check', // Default: 'check'
17
+ * falseIcon: 'xmark', // Default: 'xmark'
18
+ * trueColor: 'green', // Default: 'green'
19
+ * falseColor: 'red', // Default: 'red'
20
+ * size: 32, // Default: 32 (px)
21
+ * iconPrefix: 'far' // Default: 'far' (solid)
22
+ * }
23
+ * }
24
+ */
25
+
26
+ const colorClasses = {
27
+ green: { bg: 'bg-green-100', text: 'text-green-500' },
28
+ red: { bg: 'bg-red-100', text: 'text-red-500' },
29
+ yellow: { bg: 'bg-yellow-100', text: 'text-yellow-500' },
30
+ blue: { bg: 'bg-blue-100', text: 'text-blue-500' },
31
+ gray: { bg: 'bg-gray-100', text: 'text-gray-500' },
32
+ purple: { bg: 'bg-purple-100', text: 'text-purple-500' },
33
+ }
34
+
35
+ /**
36
+ * Determines if a value is truthy in the context of boolean display
37
+ * @param {*} value - The value to check
38
+ * @returns {boolean}
39
+ */
40
+ const isTruthy = (value) => {
41
+ if (value === null || value === undefined || value === '') {
42
+ return false
43
+ }
44
+ if (typeof value === 'boolean') {
45
+ return value
46
+ }
47
+ if (typeof value === 'number') {
48
+ return value === 1
49
+ }
50
+ if (typeof value === 'string') {
51
+ const normalized = value.toLowerCase().trim()
52
+ return normalized === '1' || normalized === 'yes' || normalized === 'true'
53
+ }
54
+ return false
55
+ }
56
+
57
+ export const booleanRenderer = (sparkTable) => {
58
+ return (instance, td, row, col, prop, value, cellProperties) => {
59
+ // Clear cell
60
+ td.innerHTML = ''
61
+ td.classList.add('spark-table-cell-boolean')
62
+
63
+ const config = cellProperties.rendererConfig || {}
64
+
65
+ const truthy = isTruthy(value)
66
+ const iconName = truthy ? (config.trueIcon || 'check') : (config.falseIcon || 'xmark')
67
+ const colorName = truthy ? (config.trueColor || 'green') : (config.falseColor || 'red')
68
+ const size = config.size || 32
69
+ const iconPrefix = config.iconPrefix || 'far'
70
+
71
+ const colors = colorClasses[colorName] || colorClasses.gray
72
+
73
+ // Create circular container
74
+ const container = document.createElement('div')
75
+ container.classList.add(
76
+ 'inline-flex',
77
+ 'items-center',
78
+ 'justify-center',
79
+ 'rounded-full',
80
+ colors.bg,
81
+ colors.text,
82
+ )
83
+ container.style.width = `${size}px`
84
+ container.style.height = `${size}px`
85
+
86
+ // Create icon
87
+ const iconElement = document.createElement('span')
88
+ iconElement.innerHTML = icon({ prefix: iconPrefix, iconName }).html
89
+ iconElement.classList.add('flex', 'items-center', 'justify-center')
90
+ // Icon size is roughly 50% of container
91
+ const iconSize = Math.round(size * 0.5)
92
+ iconElement.style.fontSize = `${iconSize}px`
93
+
94
+ container.appendChild(iconElement)
95
+ td.appendChild(container)
96
+ }
97
+ }
@@ -1,6 +1,7 @@
1
1
  import { baseRenderer, registerRenderer } from 'handsontable/renderers'
2
2
  import { actionsRenderer } from './actions.js'
3
3
  import { badgeRenderer } from './badge.js'
4
+ import { booleanRenderer } from './boolean.js'
4
5
  import { linkRenderer } from './link.js'
5
6
  import { imageRenderer } from './image.js'
6
7
  import { dateRenderer } from './date.js'
@@ -50,6 +51,7 @@ export const getRenderer = (key) => {
50
51
  * Registers the following renderers:
51
52
  * - spark.actions: Inline action buttons
52
53
  * - spark.badge: Colored status badges
54
+ * - spark.boolean: Boolean indicators with check/times icons
53
55
  * - spark.link: Clickable links (email, phone, custom)
54
56
  * - spark.image: Image thumbnails
55
57
  * - spark.date: Formatted dates
@@ -61,6 +63,7 @@ export const registerSparkRenderers = (sparkTable) => {
61
63
  // Register Spark renderers
62
64
  register('spark.actions', actionsRenderer(sparkTable))
63
65
  register('spark.badge', badgeRenderer(sparkTable))
66
+ register('spark.boolean', booleanRenderer(sparkTable))
64
67
  register('spark.link', linkRenderer(sparkTable))
65
68
  register('spark.image', imageRenderer(sparkTable))
66
69
  register('spark.date', dateRenderer(sparkTable))