@wishbone-media/spark 0.14.0 → 0.15.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.
Files changed (32) hide show
  1. package/dist/index.css +1 -0
  2. package/dist/index.js +1735 -2669
  3. package/formkit.theme.mjs +53 -53
  4. package/package.json +8 -1
  5. package/src/assets/css/spark-table.css +69 -0
  6. package/src/components/SparkButton.vue +1 -1
  7. package/src/components/SparkTable.vue +368 -0
  8. package/src/components/SparkTablePaginationDetails.vue +26 -0
  9. package/src/components/SparkTablePaginationPaging.vue +102 -0
  10. package/src/components/SparkTablePaginationPerPage.vue +47 -0
  11. package/src/components/SparkTableToolbar.vue +33 -0
  12. package/src/components/index.js +7 -0
  13. package/src/components/plugins/SparkTableDatePicker.vue +122 -0
  14. package/src/components/plugins/SparkTableFilterButtons.vue +123 -0
  15. package/src/components/plugins/SparkTableFilterSelect.vue +116 -0
  16. package/src/components/plugins/SparkTableReset.vue +123 -0
  17. package/src/components/plugins/SparkTableSearch.vue +107 -0
  18. package/src/components/plugins/index.js +5 -0
  19. package/src/containers/SparkDefaultContainer.vue +6 -1
  20. package/src/index.js +1 -0
  21. package/src/plugins/fontawesome.js +6 -0
  22. package/src/utils/index.js +4 -0
  23. package/src/utils/sparkTable/header-title.js +10 -0
  24. package/src/utils/sparkTable/header.js +137 -0
  25. package/src/utils/sparkTable/plugin-manager.js +67 -0
  26. package/src/utils/sparkTable/renderers/actions.js +125 -0
  27. package/src/utils/sparkTable/renderers/badge.js +66 -0
  28. package/src/utils/sparkTable/renderers/date.js +102 -0
  29. package/src/utils/sparkTable/renderers/image.js +61 -0
  30. package/src/utils/sparkTable/renderers/index.js +82 -0
  31. package/src/utils/sparkTable/renderers/link.js +69 -0
  32. package/src/utils/sparkTable/update-row.js +5 -0
@@ -0,0 +1,368 @@
1
+ <template>
2
+ <div class="spark-table">
3
+ <!-- Header Toolbar: All plugins flow left to right -->
4
+ <spark-table-toolbar v-if="sparkTable.computed.ready" position="header">
5
+ <component
6
+ v-for="plugin in headerPlugins"
7
+ :key="plugin.name"
8
+ :is="pluginComponentsByType[plugin.config.type]"
9
+ :class="plugin.config.align ? `self-${plugin.config.align}` : ''"
10
+ :spark-table="sparkTable"
11
+ :config="plugin.config"
12
+ />
13
+ <slot name="header" :spark-table="sparkTable" :loading="loading" :error="error"></slot>
14
+ </spark-table-toolbar>
15
+
16
+ <!-- Table Grid -->
17
+ <HotTable theme-name="ht-theme-classic" ref="table" :settings="sparkTable.tableSettings" />
18
+
19
+ <!-- Footer Toolbar: Pagination always on the right -->
20
+ <spark-table-toolbar v-if="sparkTable.computed.ready" position="footer">
21
+ <spark-table-pagination-details
22
+ :spark-table="sparkTable"
23
+ />
24
+ <div class="flex items-center gap-x-3">
25
+ <spark-table-pagination-paging
26
+ :spark-table="sparkTable"
27
+ @paginate="sparkTable.methods.applyParams"
28
+ />
29
+ <spark-table-pagination-per-page
30
+ :spark-table="sparkTable"
31
+ @paginate="sparkTable.methods.applyParams"
32
+ />
33
+ <slot name="footer" :spark-table="sparkTable" :loading="loading" :error="error"></slot>
34
+ </div>
35
+ </spark-table-toolbar>
36
+ </div>
37
+ </template>
38
+
39
+ <script>
40
+ const defaultOptions = {
41
+ perPages: [100, 200, 500],
42
+ limit: 100,
43
+ }
44
+
45
+ const defaultParams = {
46
+ page: 1,
47
+ limit: defaultOptions.limit,
48
+ }
49
+
50
+ const defaultTableSettings = {
51
+ colHeaders: true,
52
+ filters: false,
53
+ stretchH: 'all',
54
+ colWidths: 50,
55
+ renderAllRows: true,
56
+ readOnly: true,
57
+ tableClassName: 'spark-table-table',
58
+ readOnlyCellClassName: 'read-only',
59
+ licenseKey: 'non-commercial-and-evaluation',
60
+ }
61
+ </script>
62
+ <script setup>
63
+ import { computed, onMounted, ref, reactive, inject } from 'vue'
64
+ import nprogress from 'nprogress'
65
+ import { has, get, find } from 'lodash'
66
+ import { HotTable } from '@handsontable/vue3'
67
+ import 'handsontable/styles/handsontable.css'
68
+ import 'handsontable/styles/ht-theme-classic.css'
69
+ import {
70
+ registerPlugin,
71
+ AutoColumnSize,
72
+ CopyPaste,
73
+ StretchColumns,
74
+ NestedHeaders,
75
+ } from 'handsontable/plugins'
76
+ import { registerAllCellTypes } from 'handsontable/cellTypes'
77
+ import { watchDebounced } from '@vueuse/core'
78
+ import { customiseHeader } from '@/utils/sparkTable/header.js'
79
+ import { registerSparkRenderers } from '@/utils/sparkTable/renderers'
80
+ import { updateRow } from '@/utils/sparkTable/update-row.js'
81
+ import SparkTablePaginationDetails from './SparkTablePaginationDetails.vue'
82
+ import SparkTablePaginationPaging from './SparkTablePaginationPaging.vue'
83
+ import SparkTablePaginationPerPage from './SparkTablePaginationPerPage.vue'
84
+ import SparkTableToolbar from './SparkTableToolbar.vue'
85
+ import { SparkTableSearch, SparkTableFilterSelect, SparkTableFilterButtons, SparkTableDatePicker, SparkTableReset } from './plugins'
86
+
87
+ const props = defineProps({
88
+ url: {
89
+ type: String,
90
+ },
91
+
92
+ dataProvider: {
93
+ type: Function,
94
+ },
95
+
96
+ dataTransformer: {
97
+ type: Function,
98
+
99
+ default: (data, props) => {
100
+ const hasNestedHeaders = get(props.settings, 'nestedHeaders')
101
+
102
+ return {
103
+ data: hasNestedHeaders
104
+ ? data.data.data.map((row) => {
105
+ const columnKeys = props.settings.columnKeys || []
106
+ return columnKeys.map((key) => row[key])
107
+ })
108
+ : data.data.data,
109
+ meta: {
110
+ last_page: data.data.last_page,
111
+ from: data.data.from,
112
+ to: data.data.to,
113
+ total: data.data.total,
114
+ },
115
+ }
116
+ },
117
+ },
118
+
119
+ params: {
120
+ type: Object,
121
+ default() {
122
+ return defaultParams
123
+ },
124
+ },
125
+
126
+ options: {
127
+ type: Object,
128
+ default() {
129
+ return defaultOptions
130
+ },
131
+ },
132
+
133
+ settings: {
134
+ type: Object,
135
+ default() {
136
+ return defaultTableSettings
137
+ },
138
+ },
139
+
140
+ plugins: {
141
+ type: Object,
142
+ default: () => ({}),
143
+ },
144
+ })
145
+
146
+ registerAllCellTypes()
147
+ registerPlugin(AutoColumnSize)
148
+ registerPlugin(CopyPaste)
149
+ registerPlugin(StretchColumns)
150
+ if (get(props, 'settings.nestedHeaders')) {
151
+ registerPlugin(NestedHeaders)
152
+ }
153
+
154
+ const emit = defineEmits([
155
+ 'viewRow',
156
+ 'editRow',
157
+ 'deleteRow',
158
+ 'ready',
159
+ 'loading',
160
+ 'load',
161
+ 'error',
162
+ 'page-change',
163
+ ])
164
+
165
+ // Inject configured axios instance
166
+ const axios = inject('axios')
167
+
168
+ const table = ref(null)
169
+ const loading = ref(false)
170
+ const error = ref(null)
171
+
172
+ // Plugin component mapping by type
173
+ const pluginComponentsByType = {
174
+ search: SparkTableSearch,
175
+ filterSelect: SparkTableFilterSelect,
176
+ filterButtons: SparkTableFilterButtons,
177
+ datePicker: SparkTableDatePicker,
178
+ reset: SparkTableReset,
179
+ }
180
+
181
+ // Header plugins: all enabled plugins flow left to right
182
+ const headerPlugins = computed(() => {
183
+ return Object.entries(props.plugins)
184
+ .filter(([_, config]) => config && config.enabled)
185
+ .map(([name, config]) => ({ name, config }))
186
+ })
187
+
188
+ const sparkTable = reactive({
189
+ hotInstance: null,
190
+ url: props.url,
191
+ plugins: props.plugins,
192
+ response: {},
193
+ params: {
194
+ page: 1,
195
+ limit: props.options.limit,
196
+ },
197
+ methods: {
198
+ applyParams: (payload) => {
199
+ if (payload) {
200
+ sparkTable.params = {
201
+ ...sparkTable.params,
202
+ ...payload,
203
+ }
204
+ emit('page-change', sparkTable.params)
205
+ }
206
+ },
207
+
208
+ removeParam: async (prop) => {
209
+ if (sparkTable.params[prop]) {
210
+ delete sparkTable.params[prop]
211
+ await sparkTable.methods.loadTable()
212
+ }
213
+ },
214
+
215
+ loadTable: async (callback) => {
216
+ if (!table.value || !table.value.hotInstance) {
217
+ return
218
+ }
219
+
220
+ loading.value = true
221
+ error.value = null
222
+ emit('loading', true)
223
+
224
+ nprogress.start()
225
+
226
+ sparkTable.hotInstance = table.value.hotInstance
227
+
228
+ try {
229
+ if (props.dataProvider) {
230
+ const response = await props.dataProvider(sparkTable.computed.params)
231
+ sparkTable.response = props.dataTransformer(response, props)
232
+ } else if (props.url) {
233
+ const response = await axios.get(props.url, {
234
+ params: sparkTable.computed.params,
235
+ })
236
+ sparkTable.response = props.dataTransformer(response, props)
237
+ } else {
238
+ console.error('No data provider or URL provided')
239
+ loading.value = false
240
+ emit('loading', false)
241
+ nprogress.done()
242
+ return
243
+ }
244
+
245
+ sparkTable.hotInstance.updateData(sparkTable.response.data)
246
+
247
+ if (sparkTable.options.callback && typeof sparkTable.options.callback === 'function') {
248
+ await sparkTable.options.callback()
249
+ }
250
+
251
+ // @see: https://forum.handsontable.com/t/table-not-maintaning-width/3116/11
252
+ const autoColumnSize = sparkTable.hotInstance.getPlugin('autoColumnSize')
253
+ autoColumnSize.recalculateAllColumnsWidth()
254
+
255
+ emit('load', {
256
+ data: sparkTable.response.data,
257
+ meta: sparkTable.response.meta,
258
+ })
259
+
260
+ if (callback && typeof callback === 'function') {
261
+ callback()
262
+ }
263
+ } catch (e) {
264
+ error.value = e
265
+ emit('error', e)
266
+ console.error('Error loading table data:', e)
267
+ } finally {
268
+ loading.value = false
269
+ emit('loading', false)
270
+ nprogress.done()
271
+ }
272
+ },
273
+
274
+ // can't use sparkTable.hotInstance here because the ref isn't ready
275
+ colToProp: (col) => table.value.hotInstance.colToProp(col),
276
+
277
+ fireEvent: (type, payload) => {
278
+ emit(type, payload)
279
+ },
280
+
281
+ getSettingsForProp: (prop) => find(get(sparkTable.tableSettings, `columns`), { data: prop }),
282
+
283
+ // Helper methods for easier param access
284
+ getParams: () => sparkTable.computed.params,
285
+ getParam: (key) => sparkTable.computed.params[key],
286
+ setParam: (key, value) => {
287
+ sparkTable.methods.applyParams({ [key]: value })
288
+ },
289
+ clearParam: (key) => sparkTable.methods.removeParam(key),
290
+ clearParams: (keys) => {
291
+ // Clear multiple params at once without triggering multiple reloads
292
+ const keysArray = Array.isArray(keys) ? keys : [keys]
293
+ keysArray.forEach(key => {
294
+ if (sparkTable.params[key] !== undefined) {
295
+ delete sparkTable.params[key]
296
+ }
297
+ })
298
+ // Trigger a single reload
299
+ sparkTable.methods.applyParams({ page: 1 })
300
+ },
301
+ },
302
+
303
+ computed: {
304
+ params: computed(() => ({
305
+ ...props.params,
306
+ ...sparkTable.params,
307
+ })),
308
+
309
+ ready: computed(() => has(sparkTable, 'response.meta.last_page')),
310
+ },
311
+
312
+ options: computed(() => ({
313
+ ...defaultOptions,
314
+ ...props.options,
315
+ })),
316
+
317
+ tableSettings: computed(() => ({
318
+ ...defaultTableSettings,
319
+ ...{
320
+ ...{
321
+ nestedHeaders: get(props.settings, 'nestedHeaders', []),
322
+ },
323
+ ...(!get(props.settings, 'nestedHeaders') && {
324
+ afterGetColHeader: (col, th) => customiseHeader(col, th, sparkTable),
325
+ }),
326
+ afterChange: (changes, source) => updateRow(changes, source, sparkTable),
327
+ },
328
+ ...props.settings,
329
+ })),
330
+ })
331
+
332
+ watchDebounced(
333
+ () => props.params,
334
+ async () => {
335
+ sparkTable.params.page = 1
336
+ await sparkTable.methods.loadTable()
337
+ },
338
+ { debounce: 50, maxWait: 1000 },
339
+ )
340
+ watchDebounced(
341
+ () => sparkTable.params,
342
+ async () => {
343
+ await sparkTable.methods.loadTable()
344
+ },
345
+ { debounce: 50, maxWait: 1000 },
346
+ )
347
+
348
+ onMounted(async () => {
349
+ await sparkTable.methods.loadTable()
350
+ emit('ready')
351
+ })
352
+
353
+ registerSparkRenderers(sparkTable)
354
+
355
+ defineExpose({
356
+ refresh: () => sparkTable.methods.loadTable(),
357
+ getParams: () => sparkTable.methods.getParams(),
358
+ getParam: (key) => sparkTable.methods.getParam(key),
359
+ setParam: (key, value) => sparkTable.methods.setParam(key, value),
360
+ clearParam: (key) => sparkTable.methods.clearParam(key),
361
+ clearParams: (keys) => sparkTable.methods.clearParams(keys),
362
+ applyParams: (params) => sparkTable.methods.applyParams(params),
363
+ loading,
364
+ error,
365
+ sparkTable,
366
+ table,
367
+ })
368
+ </script>
@@ -0,0 +1,26 @@
1
+ <template>
2
+ <div>
3
+ <div class="flex items-center gap-4 px-4 py-3">
4
+ <div class="shrink-0">
5
+ <div class="text-sm text-gray-700">
6
+ Showing
7
+ <span class="font-medium">{{ props.sparkTable.response.meta.from }}</span>
8
+ to
9
+ <span class="font-medium">{{ props.sparkTable.response.meta.to }}</span>
10
+ of
11
+ <span class="font-medium">{{ props.sparkTable.response.meta.total }}</span>
12
+ results
13
+ </div>
14
+ </div>
15
+ </div>
16
+ </div>
17
+ </template>
18
+
19
+ <script setup>
20
+ const props = defineProps({
21
+ sparkTable: {
22
+ type: Object,
23
+ required: true,
24
+ },
25
+ })
26
+ </script>
@@ -0,0 +1,102 @@
1
+ <template>
2
+ <div>
3
+ <div class="flex items-center gap-4 px-4 py-3">
4
+ <div class="shrink-0 ml-auto" v-if="pageRange.length > 1">
5
+ <div>
6
+ <nav
7
+ class="isolate inline-flex -space-x-px rounded-md shadow-xs bg-white"
8
+ aria-label="Pagination"
9
+ >
10
+ <a
11
+ :class="canPrev ? '' : 'disabled'"
12
+ href="#"
13
+ class="relative inline-flex items-center rounded-l-md px-2 py-[9px] text-gray-400 ring-1 ring-gray-300 ring-inset hover:bg-gray-50 focus:z-20 focus:outline-offset-0"
14
+ @click.prevent="pageChanged(-1)"
15
+ >
16
+ <font-awesome-icon :icon="Icons.farChevronLeft" class="size-5" />
17
+ </a>
18
+
19
+ <a
20
+ v-for="page in pageRange"
21
+ :key="`page-${page}`"
22
+ href="#"
23
+ class="relative inline-flex items-center px-4 py-[9px] text-sm font-semibold ring-1 ring-inset"
24
+ :class="
25
+ sparkTable.params.page === page
26
+ ? 'z-10 text-white bg-primary-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 hover:bg-primary-600'
27
+ : 'text-gray-900 hover:bg-gray-50 ring-gray-300 focus:z-20 focus:outline-offset-0'
28
+ "
29
+ @click="gotoPage(page)"
30
+ >
31
+ {{ page }}
32
+ </a>
33
+
34
+ <a
35
+ :class="canNext ? '' : 'disabled'"
36
+ @click.prevent="pageChanged(1)"
37
+ href="#"
38
+ class="relative inline-flex items-center rounded-r-md px-2 py-[9px] text-gray-400 ring-1 ring-gray-300 ring-inset hover:bg-gray-50 focus:z-20 focus:outline-offset-0"
39
+ >
40
+ <font-awesome-icon :icon="Icons.farChevronRight" class="size-5" />
41
+ </a>
42
+ </nav>
43
+ </div>
44
+ </div>
45
+ </div>
46
+ </div>
47
+ </template>
48
+
49
+ <script setup>
50
+ import { computed } from 'vue'
51
+ import { get, range } from 'lodash'
52
+ import { Icons } from '@/plugins/fontawesome.js'
53
+
54
+ const props = defineProps({
55
+ sparkTable: {
56
+ type: Object,
57
+ required: true,
58
+ },
59
+ })
60
+
61
+ const emit = defineEmits(['paginate'])
62
+
63
+ const pageChanged = (change) => {
64
+ gotoPage(props.sparkTable.params.page + change)
65
+ }
66
+ const gotoPage = (targetPage) => {
67
+ if (targetPage < 1 || targetPage > props.sparkTable.response.meta.last_page) {
68
+ return
69
+ }
70
+
71
+ emit('paginate', {
72
+ page: targetPage,
73
+ })
74
+ }
75
+
76
+ // Range
77
+ const lastPage = computed(() => {
78
+ if (!props.sparkTable.params.page) {
79
+ return 1
80
+ }
81
+
82
+ return Math.ceil(props.sparkTable.params.page / 10) * 10
83
+ })
84
+ const pageRange = computed(() => {
85
+ if (!props.sparkTable.computed.ready) {
86
+ return []
87
+ }
88
+
89
+ return range(
90
+ Math.floor((lastPage.value - 1) / 10) * 10 + 1,
91
+ lastPage.value > props.sparkTable.response.meta.last_page
92
+ ? props.sparkTable.response.meta.last_page + 1
93
+ : lastPage.value + 1,
94
+ )
95
+ })
96
+
97
+ // Status
98
+ const canNext = computed(
99
+ () => props.sparkTable.params.page < get(props.sparkTable.response.meta, 'last_page', 1),
100
+ )
101
+ const canPrev = computed(() => props.sparkTable.params.page > 1)
102
+ </script>
@@ -0,0 +1,47 @@
1
+ <template>
2
+ <div class="spark-table-pagination-per-page">
3
+ <FormKit
4
+ :model-value="sparkTable.params.limit"
5
+ type="select"
6
+ :options="perPageOptions"
7
+ outer-class="!mb-0"
8
+ wrapper-class="!mb-0"
9
+ input-class="!py-2"
10
+ @input="limitChanged"
11
+ />
12
+ </div>
13
+ </template>
14
+
15
+ <script setup>
16
+ import { computed } from 'vue'
17
+
18
+ const props = defineProps({
19
+ sparkTable: {
20
+ type: Object,
21
+ required: true,
22
+ },
23
+ })
24
+
25
+ const emit = defineEmits(['paginate'])
26
+
27
+ // Format options for FormKit select
28
+ const perPageOptions = computed(() => {
29
+ return props.sparkTable.options.perPages.map((option) => ({
30
+ label: String(option),
31
+ value: option,
32
+ }))
33
+ })
34
+
35
+ const limitChanged = (value) => {
36
+ emit('paginate', {
37
+ page: 1,
38
+ limit: parseInt(value),
39
+ })
40
+ }
41
+ </script>
42
+
43
+ <style scoped>
44
+ .spark-table-pagination-per-page {
45
+ @apply flex items-center;
46
+ }
47
+ </style>
@@ -0,0 +1,33 @@
1
+ <template>
2
+ <div v-if="hasContent" :class="toolbarClass">
3
+ <slot></slot>
4
+ </div>
5
+ </template>
6
+
7
+ <script setup>
8
+ import { computed, useSlots } from 'vue'
9
+
10
+ const props = defineProps({
11
+ position: {
12
+ type: String,
13
+ required: true,
14
+ validator: (value) => ['header', 'footer'].includes(value),
15
+ },
16
+ })
17
+
18
+ const slots = useSlots()
19
+
20
+ const hasContent = computed(() => !!slots.default)
21
+
22
+ const toolbarClass = computed(() => {
23
+ const baseClasses = 'spark-table-toolbar flex flex-wrap items-center gap-x-5 px-5 w-full'
24
+
25
+ // Footer has pagination details on left, controls on right
26
+ if (props.position === 'footer') {
27
+ return `${baseClasses} spark-table-toolbar-footer justify-between`
28
+ }
29
+
30
+ // Header flows left to right
31
+ return `${baseClasses} spark-table-toolbar-header gap-y-5 py-5`
32
+ })
33
+ </script>
@@ -7,3 +7,10 @@ export { default as SparkCard } from './SparkCard.vue'
7
7
  export { default as SparkModalContainer } from './SparkModalContainer.vue'
8
8
  export { default as SparkModalDialog } from './SparkModalDialog.vue'
9
9
  export { default as SparkOverlay } from './SparkOverlay.vue'
10
+ export { default as SparkTable } from './SparkTable.vue'
11
+ export { default as SparkTablePaginationPaging } from './SparkTablePaginationPaging.vue'
12
+ export { default as SparkTablePaginationPerPage } from './SparkTablePaginationPerPage.vue'
13
+ export { default as SparkTableToolbar } from './SparkTableToolbar.vue'
14
+
15
+ // Table plugins
16
+ export * from './plugins'
@@ -0,0 +1,122 @@
1
+ <template>
2
+ <div class="spark-table-date-picker">
3
+ <FormKit
4
+ v-model="selectedDate"
5
+ type="datepicker"
6
+ :label="label"
7
+ :placeholder="config.placeholder || 'Select date'"
8
+ :overlay="true"
9
+ value-format="YYYY-MM-DD"
10
+ format="YYYY-MM-DD"
11
+ outer-class="!mb-0"
12
+ wrapper-class="!mb-0"
13
+ v-bind="config.formkitProps || {}"
14
+ />
15
+ </div>
16
+ </template>
17
+
18
+ <script setup>
19
+ /**
20
+ * SparkTableDatePicker Plugin Component
21
+ *
22
+ * Provides a single date picker that applies date filter parameters to the SparkTable.
23
+ * For date ranges, use two separate datePicker plugin instances (startDate and endDate).
24
+ *
25
+ * @component
26
+ * @example
27
+ * // Single date filter
28
+ * const plugins = {
29
+ * createdDate: {
30
+ * type: 'datePicker',
31
+ * enabled: true,
32
+ * key: 'created_at',
33
+ * label: 'Created Date',
34
+ * placeholder: 'Select date',
35
+ * position: 'header-right',
36
+ * },
37
+ * }
38
+ *
39
+ * @example
40
+ * // Date range using two pickers
41
+ * const plugins = {
42
+ * startDate: {
43
+ * type: 'datePicker',
44
+ * enabled: true,
45
+ * key: 'created_at_start',
46
+ * label: 'Start Date',
47
+ * position: 'header-right',
48
+ * },
49
+ * endDate: {
50
+ * type: 'datePicker',
51
+ * enabled: true,
52
+ * key: 'created_at_end',
53
+ * label: 'End Date',
54
+ * position: 'header-right',
55
+ * },
56
+ * }
57
+ */
58
+ import { ref, watch } from 'vue'
59
+
60
+ /**
61
+ * @typedef {Object} SparkTableDatePickerConfig
62
+ * @property {string} type - Plugin type: 'datePicker'
63
+ * @property {boolean} enabled - Enable the plugin
64
+ * @property {string} key - Date filter key (used in query params)
65
+ * @property {string} [label] - Label displayed above the date picker
66
+ * @property {string} [placeholder='Select date'] - Placeholder text
67
+ * @property {string} [param] - Custom query param (defaults to filter[key])
68
+ * @property {string} [position='header-left'] - Toolbar position
69
+ * @property {Object} [formkitProps] - Additional FormKit props to pass through
70
+ */
71
+
72
+ const props = defineProps({
73
+ /** SparkTable instance object */
74
+ sparkTable: {
75
+ type: Object,
76
+ required: true,
77
+ },
78
+ /** Date picker plugin configuration */
79
+ config: {
80
+ type: Object,
81
+ required: true,
82
+ validator: (config) => {
83
+ return config.key
84
+ },
85
+ },
86
+ })
87
+
88
+ const selectedDate = ref(null)
89
+ const label = props.config.label || null
90
+ const param = props.config.param || `filter[${props.config.key}]`
91
+
92
+ // Initialize from existing params
93
+ selectedDate.value = props.sparkTable.params[param] || null
94
+
95
+ // Watch for date changes
96
+ watch(selectedDate, (newValue) => {
97
+ if (newValue) {
98
+ props.sparkTable.methods.applyParams({
99
+ page: 1,
100
+ [param]: newValue,
101
+ })
102
+ } else {
103
+ props.sparkTable.methods.removeParam(param)
104
+ }
105
+ })
106
+
107
+ // Watch for external param changes
108
+ watch(
109
+ () => props.sparkTable.params[param],
110
+ (newValue) => {
111
+ if (!newValue && selectedDate.value) {
112
+ selectedDate.value = null
113
+ }
114
+ },
115
+ )
116
+ </script>
117
+
118
+ <style scoped>
119
+ .spark-table-date-picker {
120
+ @apply flex items-center gap-2;
121
+ }
122
+ </style>