@wishbone-media/spark 0.20.0 → 0.22.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.20.0",
3
+ "version": "0.22.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -9,6 +9,19 @@
9
9
  --ht-header-highlighted-background-color: transparent;
10
10
  }
11
11
 
12
+ .spark-table .handsontable td.current {
13
+ @apply relative;
14
+
15
+ &::before {
16
+ @apply absolute opacity-[0.14] content-[''] inset-0 bg-[var(--ht-cell-selection-background-color,#1a42e8)];
17
+ }
18
+ }
19
+
20
+ .spark-table .handsontable tr.ht__row_odd:hover td,
21
+ .spark-table .handsontable tr.ht__row_even:hover td {
22
+ @apply !bg-gray-100;
23
+ }
24
+
12
25
  .spark-table .ht_master .wtBorder {
13
26
  @apply !hidden;
14
27
  }
@@ -7,7 +7,7 @@
7
7
  <div class="ml-3">
8
8
  <slot />
9
9
  </div>
10
- <div class="ml-auto pl-3 pt-1 self-start">
10
+ <div v-if="closeable" class="ml-auto pl-3 pt-1 self-start">
11
11
  <div class="-mx-1.5 -my-1.5">
12
12
  <button
13
13
  type="button"
@@ -33,6 +33,10 @@ const props = defineProps({
33
33
  default: 'info',
34
34
  validator: (value) => ['success', 'warning', 'danger', 'info'].includes(value),
35
35
  },
36
+ closeable: {
37
+ type: Boolean,
38
+ default: true,
39
+ },
36
40
  })
37
41
 
38
42
  defineEmits(['close'])
@@ -13,6 +13,8 @@
13
13
  <script setup>
14
14
  import { computed, inject, ref } from 'vue'
15
15
 
16
+ defineEmits(['click'])
17
+
16
18
  const props = defineProps({
17
19
  size: {
18
20
  type: String,
@@ -0,0 +1,68 @@
1
+ <template>
2
+ <Transition
3
+ enter-active-class="transition-all duration-300 ease-out"
4
+ enter-from-class="opacity-0 -translate-y-2"
5
+ enter-to-class="opacity-100 translate-y-0"
6
+ leave-active-class="transition-all duration-200 ease-in"
7
+ leave-from-class="opacity-100 translate-y-0"
8
+ leave-to-class="opacity-0 -translate-y-2"
9
+ mode="out-in"
10
+ >
11
+ <SparkAlert
12
+ v-if="outlet.state.isVisible"
13
+ :key="notificationKey"
14
+ :type="outlet.state.type"
15
+ :closeable="outlet.state.closeable"
16
+ @close="handleClose"
17
+ @mouseenter="handleMouseEnter"
18
+ @mouseleave="handleMouseLeave"
19
+ >
20
+ <component
21
+ v-if="outlet.state.component"
22
+ :is="outlet.state.component"
23
+ v-bind="outlet.state.props"
24
+ />
25
+ <template v-else>
26
+ {{ outlet.state.message }}
27
+ </template>
28
+ </SparkAlert>
29
+ </Transition>
30
+ </template>
31
+
32
+ <script setup>
33
+ import { computed, ref, watch } from 'vue'
34
+ import { sparkNotificationService } from '@/composables/sparkNotificationService'
35
+ import SparkAlert from '@/components/SparkAlert.vue'
36
+
37
+ const props = defineProps({
38
+ name: {
39
+ type: String,
40
+ default: 'default',
41
+ },
42
+ })
43
+
44
+ const outlet = computed(() => sparkNotificationService.getOutlet(props.name))
45
+
46
+ // Generate unique key for each notification to trigger transition on content change
47
+ const notificationKey = ref(0)
48
+ watch(
49
+ () => [outlet.value.state.message, outlet.value.state.component, outlet.value.state.type],
50
+ () => {
51
+ if (outlet.value.state.isVisible) {
52
+ notificationKey.value++
53
+ }
54
+ },
55
+ )
56
+
57
+ const handleClose = () => {
58
+ sparkNotificationService.hide(props.name)
59
+ }
60
+
61
+ const handleMouseEnter = () => {
62
+ sparkNotificationService.pause(props.name)
63
+ }
64
+
65
+ const handleMouseLeave = () => {
66
+ sparkNotificationService.resume(props.name)
67
+ }
68
+ </script>
@@ -34,7 +34,8 @@
34
34
  <DialogPanel
35
35
  ref="panelRef"
36
36
  :class="[
37
- 'flex w-[400px] py-2.5',
37
+ 'flex py-2.5',
38
+ widthClass,
38
39
  position === 'left' ? 'relative left-[10px]' : 'absolute right-[10px] h-full',
39
40
  ]"
40
41
  >
@@ -52,11 +53,19 @@
52
53
  </template>
53
54
 
54
55
  <script setup>
55
- import { ref } from 'vue'
56
+ import { computed, ref } from 'vue'
56
57
  import { Dialog, DialogPanel, TransitionChild, TransitionRoot } from '@headlessui/vue'
57
58
 
58
59
  const panelRef = ref(null)
59
60
 
61
+ const sizeClasses = {
62
+ xs: 'w-[250px]',
63
+ sm: 'w-[300px]',
64
+ md: 'w-[450px]',
65
+ lg: 'w-[810px]',
66
+ xl: 'w-[1000px]',
67
+ }
68
+
60
69
  const props = defineProps({
61
70
  position: {
62
71
  type: String,
@@ -72,6 +81,10 @@ const props = defineProps({
72
81
 
73
82
  const emit = defineEmits(['close'])
74
83
 
84
+ const widthClass = computed(() => {
85
+ return sizeClasses[props.overlayInstance.state.size] || sizeClasses.md
86
+ })
87
+
75
88
  const handleClose = () => {
76
89
  props.overlayInstance.close()
77
90
  emit('close')
@@ -355,6 +355,29 @@ const sparkTable = reactive({
355
355
  }
356
356
  return stretchedWidth
357
357
  },
358
+ /**
359
+ * Copy displayed cell content instead of raw data values
360
+ * This ensures custom renderers copy their visual output, not the underlying data
361
+ */
362
+ beforeCopy: (data, coords) => {
363
+ const hot = table.value?.hotInstance
364
+ if (!hot) return
365
+
366
+ coords.forEach((range) => {
367
+ for (let row = range.startRow; row <= range.endRow; row++) {
368
+ for (let col = range.startCol; col <= range.endCol; col++) {
369
+ const td = hot.getCell(row, col)
370
+ if (td) {
371
+ const dataRow = row - coords[0].startRow
372
+ const dataCol = col - coords[0].startCol
373
+ // Prefer data-copy-value (for renderers like boolean with icons)
374
+ // Fall back to textContent for standard rendered content
375
+ data[dataRow][dataCol] = td.dataset.copyValue ?? td.textContent ?? ''
376
+ }
377
+ }
378
+ }
379
+ })
380
+ },
358
381
  },
359
382
  ...props.settings,
360
383
  })),
@@ -0,0 +1,184 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <!-- Top Left -->
4
+ <div class="fixed top-4 left-4 z-[2000] flex flex-col gap-3 max-w-sm w-full pointer-events-none">
5
+ <TransitionGroup
6
+ enter-active-class="transition-all duration-300 ease-out"
7
+ enter-from-class="opacity-0 -translate-x-4"
8
+ enter-to-class="opacity-100 translate-x-0"
9
+ leave-active-class="transition-all duration-200 ease-in"
10
+ leave-from-class="opacity-100 translate-x-0"
11
+ leave-to-class="opacity-0 -translate-x-4"
12
+ >
13
+ <div v-for="toast in topLeftToasts" :key="toast.id" class="pointer-events-auto">
14
+ <SparkAlert
15
+ :type="toast.type"
16
+ :closeable="toast.closeable"
17
+ @close="handleClose(toast.id)"
18
+ @mouseenter="handleMouseEnter(toast.id)"
19
+ @mouseleave="handleMouseLeave(toast.id)"
20
+ >
21
+ <component
22
+ v-if="toast.component"
23
+ :is="toast.component"
24
+ v-bind="toast.props"
25
+ />
26
+ <template v-else>{{ toast.message }}</template>
27
+ </SparkAlert>
28
+ </div>
29
+ </TransitionGroup>
30
+ </div>
31
+
32
+ <!-- Top Right -->
33
+ <div class="fixed top-4 right-4 z-[2000] flex flex-col gap-3 max-w-sm w-full pointer-events-none">
34
+ <TransitionGroup
35
+ enter-active-class="transition-all duration-300 ease-out"
36
+ enter-from-class="opacity-0 translate-x-4"
37
+ enter-to-class="opacity-100 translate-x-0"
38
+ leave-active-class="transition-all duration-200 ease-in"
39
+ leave-from-class="opacity-100 translate-x-0"
40
+ leave-to-class="opacity-0 translate-x-4"
41
+ >
42
+ <div v-for="toast in topRightToasts" :key="toast.id" class="pointer-events-auto">
43
+ <SparkAlert
44
+ :type="toast.type"
45
+ :closeable="toast.closeable"
46
+ @close="handleClose(toast.id)"
47
+ @mouseenter="handleMouseEnter(toast.id)"
48
+ @mouseleave="handleMouseLeave(toast.id)"
49
+ >
50
+ <component
51
+ v-if="toast.component"
52
+ :is="toast.component"
53
+ v-bind="toast.props"
54
+ />
55
+ <template v-else>{{ toast.message }}</template>
56
+ </SparkAlert>
57
+ </div>
58
+ </TransitionGroup>
59
+ </div>
60
+
61
+ <!-- Center (slightly above center) -->
62
+ <div class="fixed top-[40%] left-1/2 -translate-x-1/2 -translate-y-1/2 z-[2000] flex flex-col gap-3 max-w-sm w-full pointer-events-none">
63
+ <TransitionGroup
64
+ enter-active-class="transition-all duration-300 ease-out"
65
+ enter-from-class="opacity-0 scale-95"
66
+ enter-to-class="opacity-100 scale-100"
67
+ leave-active-class="transition-all duration-200 ease-in"
68
+ leave-from-class="opacity-100 scale-100"
69
+ leave-to-class="opacity-0 scale-95"
70
+ >
71
+ <div v-for="toast in centerToasts" :key="toast.id" class="pointer-events-auto">
72
+ <SparkAlert
73
+ :type="toast.type"
74
+ :closeable="toast.closeable"
75
+ @close="handleClose(toast.id)"
76
+ @mouseenter="handleMouseEnter(toast.id)"
77
+ @mouseleave="handleMouseLeave(toast.id)"
78
+ >
79
+ <component
80
+ v-if="toast.component"
81
+ :is="toast.component"
82
+ v-bind="toast.props"
83
+ />
84
+ <template v-else>{{ toast.message }}</template>
85
+ </SparkAlert>
86
+ </div>
87
+ </TransitionGroup>
88
+ </div>
89
+
90
+ <!-- Bottom Left -->
91
+ <div class="fixed bottom-4 left-4 z-[2000] flex flex-col-reverse gap-3 max-w-sm w-full pointer-events-none">
92
+ <TransitionGroup
93
+ enter-active-class="transition-all duration-300 ease-out"
94
+ enter-from-class="opacity-0 -translate-x-4"
95
+ enter-to-class="opacity-100 translate-x-0"
96
+ leave-active-class="transition-all duration-200 ease-in"
97
+ leave-from-class="opacity-100 translate-x-0"
98
+ leave-to-class="opacity-0 -translate-x-4"
99
+ >
100
+ <div v-for="toast in bottomLeftToasts" :key="toast.id" class="pointer-events-auto">
101
+ <SparkAlert
102
+ :type="toast.type"
103
+ :closeable="toast.closeable"
104
+ @close="handleClose(toast.id)"
105
+ @mouseenter="handleMouseEnter(toast.id)"
106
+ @mouseleave="handleMouseLeave(toast.id)"
107
+ >
108
+ <component
109
+ v-if="toast.component"
110
+ :is="toast.component"
111
+ v-bind="toast.props"
112
+ />
113
+ <template v-else>{{ toast.message }}</template>
114
+ </SparkAlert>
115
+ </div>
116
+ </TransitionGroup>
117
+ </div>
118
+
119
+ <!-- Bottom Right -->
120
+ <div class="fixed bottom-4 right-4 z-[2000] flex flex-col-reverse gap-3 max-w-sm w-full pointer-events-none">
121
+ <TransitionGroup
122
+ enter-active-class="transition-all duration-300 ease-out"
123
+ enter-from-class="opacity-0 translate-x-4"
124
+ enter-to-class="opacity-100 translate-x-0"
125
+ leave-active-class="transition-all duration-200 ease-in"
126
+ leave-from-class="opacity-100 translate-x-0"
127
+ leave-to-class="opacity-0 translate-x-4"
128
+ >
129
+ <div v-for="toast in bottomRightToasts" :key="toast.id" class="pointer-events-auto">
130
+ <SparkAlert
131
+ :type="toast.type"
132
+ :closeable="toast.closeable"
133
+ @close="handleClose(toast.id)"
134
+ @mouseenter="handleMouseEnter(toast.id)"
135
+ @mouseleave="handleMouseLeave(toast.id)"
136
+ >
137
+ <component
138
+ v-if="toast.component"
139
+ :is="toast.component"
140
+ v-bind="toast.props"
141
+ />
142
+ <template v-else>{{ toast.message }}</template>
143
+ </SparkAlert>
144
+ </div>
145
+ </TransitionGroup>
146
+ </div>
147
+ </Teleport>
148
+ </template>
149
+
150
+ <script setup>
151
+ import { computed } from 'vue'
152
+ import { sparkNotificationService } from '@/composables/sparkNotificationService'
153
+ import SparkAlert from '@/components/SparkAlert.vue'
154
+
155
+ const toastState = sparkNotificationService.toastState
156
+
157
+ const topLeftToasts = computed(() =>
158
+ toastState.toasts.filter(t => t.position === 'top-left')
159
+ )
160
+ const topRightToasts = computed(() =>
161
+ toastState.toasts.filter(t => t.position === 'top-right')
162
+ )
163
+ const centerToasts = computed(() =>
164
+ toastState.toasts.filter(t => t.position === 'center')
165
+ )
166
+ const bottomLeftToasts = computed(() =>
167
+ toastState.toasts.filter(t => t.position === 'bottom-left')
168
+ )
169
+ const bottomRightToasts = computed(() =>
170
+ toastState.toasts.filter(t => t.position === 'bottom-right')
171
+ )
172
+
173
+ const handleClose = (toastId) => {
174
+ sparkNotificationService.hideToast(toastId)
175
+ }
176
+
177
+ const handleMouseEnter = (toastId) => {
178
+ sparkNotificationService.pauseToast(toastId)
179
+ }
180
+
181
+ const handleMouseLeave = (toastId) => {
182
+ sparkNotificationService.resumeToast(toastId)
183
+ }
184
+ </script>
@@ -1,5 +1,7 @@
1
1
  export { default as SparkAlert } from './SparkAlert.vue'
2
2
  export { default as SparkAppSelector } from './SparkAppSelector.vue'
3
+ export { default as SparkNotificationOutlet } from './SparkNotificationOutlet.vue'
4
+ export { default as SparkToastContainer } from './SparkToastContainer.vue'
3
5
  export { default as SparkBrandSelector } from './SparkBrandSelector.vue'
4
6
  export { default as SparkButton } from './SparkButton.vue'
5
7
  export { default as SparkButtonGroup } from './SparkButtonGroup.vue'
@@ -1,4 +1,5 @@
1
1
  export { sparkModalService } from './sparkModalService.js'
2
+ export { sparkNotificationService } from './sparkNotificationService.js'
2
3
  export { sparkOverlayService } from './sparkOverlayService.js'
3
4
  export { useSparkOverlay } from './useSparkOverlay.js'
4
5
  export { useSparkTableRouteSync } from './useSparkTableRouteSync.js'