@wishbone-media/spark 0.2.0 → 0.2.2

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.2.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -13,7 +13,7 @@
13
13
  </div>
14
14
  </div>
15
15
  <div
16
- v-for="brand in brandFilterStore.allBrands"
16
+ v-for="brand in sparkBrandFilterStore.allBrands"
17
17
  :key="brand.name"
18
18
  :class="brand.current ? 'bg-gray-50' : 'hover:bg-gray-50'"
19
19
  class="flex px-[22px] py-[15px] cursor-pointer"
@@ -47,12 +47,12 @@
47
47
  </template>
48
48
 
49
49
  <script setup>
50
- import { useBrandFilterStore } from '@/stores/brand-filter'
50
+ import { useSparkBrandFilterStore } from '@/stores/brand-filter'
51
51
  import { Icons } from '@/plugins/fontawesome'
52
52
 
53
53
  const emit = defineEmits(['close', 'select'])
54
54
 
55
- const brandFilterStore = useBrandFilterStore()
55
+ const sparkBrandFilterStore = useSparkBrandFilterStore()
56
56
 
57
57
  const selectBrand = (brand) => {
58
58
  emit('select', brand)
@@ -0,0 +1,45 @@
1
+ <template>
2
+ <TransitionRoot as="template" :show="sparkModalService.state.isVisible">
3
+ <Dialog class="relative z-50" @close="sparkModalService.hide">
4
+ <TransitionChild
5
+ as="template"
6
+ enter="ease-out duration-300"
7
+ enter-from="opacity-0"
8
+ enter-to="opacity-100"
9
+ leave="ease-in duration-200"
10
+ leave-from="opacity-100"
11
+ leave-to="opacity-0"
12
+ >
13
+ <div class="fixed inset-0 bg-gray-500/75 transition-opacity" />
14
+ </TransitionChild>
15
+
16
+ <div class="fixed inset-0 z-10 w-screen overflow-y-auto">
17
+ <div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
18
+ <TransitionChild
19
+ as="template"
20
+ enter="ease-out duration-300"
21
+ enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
22
+ enter-to="opacity-100 translate-y-0 sm:scale-100"
23
+ leave="ease-in duration-200"
24
+ leave-from="opacity-100 translate-y-0 sm:scale-100"
25
+ leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
26
+ >
27
+ <DialogPanel class="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
28
+ <!-- Render dynamic component -->
29
+ <component
30
+ :is="sparkModalService.state.content"
31
+ v-bind="sparkModalService.state.props"
32
+ v-on="sparkModalService.state.eventHandlers"
33
+ />
34
+ </DialogPanel>
35
+ </TransitionChild>
36
+ </div>
37
+ </div>
38
+ </Dialog>
39
+ </TransitionRoot>
40
+ </template>
41
+
42
+ <script setup>
43
+ import { Dialog, DialogPanel, TransitionChild, TransitionRoot } from '@headlessui/vue'
44
+ import { sparkModalService } from '@/composables/sparkModalService'
45
+ </script>
@@ -0,0 +1,145 @@
1
+ <template>
2
+ <div class="px-4 pt-5 pb-4 sm:p-6">
3
+ <!-- Icon -->
4
+ <div v-if="iconToUse" class="mx-auto flex size-12 items-center justify-center rounded-full" :class="iconBgClass">
5
+ <font-awesome-icon :icon="Icons[iconToUse]" class="h-5 w-5" :class="iconTextClass" />
6
+ </div>
7
+
8
+ <!-- Content -->
9
+ <div class="text-center" :class="{ 'mt-3 sm:mt-5': iconToUse }">
10
+ <!-- Title -->
11
+ <h3 v-if="title" class="text-lg font-medium text-gray-900">
12
+ {{ title }}
13
+ </h3>
14
+
15
+ <!-- Message -->
16
+ <div v-if="message" :class="{ 'mt-2': title }" class="text-sm text-gray-500">
17
+ {{ message }}
18
+ </div>
19
+ </div>
20
+
21
+ <!-- Actions -->
22
+ <div class="mt-5 sm:mt-6" :class="buttonContainerClass">
23
+ <button
24
+ v-for="(button, index) in buttonsToShow"
25
+ :key="index"
26
+ type="button"
27
+ :class="getButtonClass(button, index)"
28
+ class="inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-semibold shadow-xs focus-visible:outline-2 focus-visible:outline-offset-2"
29
+ @click="$emit(button.event, button)"
30
+ >
31
+ {{ button.text }}
32
+ </button>
33
+ </div>
34
+ </div>
35
+ </template>
36
+
37
+ <script setup>
38
+ import { computed } from 'vue'
39
+ import { Icons } from '@/plugins/fontawesome'
40
+
41
+ const props = defineProps({
42
+ title: {
43
+ type: String,
44
+ required: true,
45
+ },
46
+ message: {
47
+ type: String,
48
+ default: '',
49
+ },
50
+ type: {
51
+ type: String,
52
+ default: 'info',
53
+ validator: (value) => ['info', 'success', 'warning', 'danger'].includes(value),
54
+ },
55
+ icon: {
56
+ type: String,
57
+ default: null,
58
+ },
59
+ buttons: {
60
+ type: Array,
61
+ default: () => [
62
+ { text: 'OK', variant: 'primary', event: 'ok' }
63
+ ],
64
+ },
65
+ })
66
+
67
+ defineEmits([
68
+ 'ok', 'confirm', 'cancel', 'close', 'save', 'discard', 'delete',
69
+ 'approve', 'reject', 'submit', 'reset', 'continue', 'retry',
70
+ 'edit', 'view', 'download', 'upload', 'share', 'copy', 'input'
71
+ ])
72
+
73
+ const buttonsToShow = computed(() => {
74
+ // Default to single OK button if no buttons provided
75
+ if (!props.buttons || props.buttons.length === 0) {
76
+ return [{ text: 'OK', variant: 'primary', event: 'ok' }]
77
+ }
78
+ return props.buttons
79
+ })
80
+
81
+ const buttonContainerClass = computed(() => {
82
+ const buttonCount = buttonsToShow.value.length
83
+ if (buttonCount === 2) {
84
+ return 'sm:grid sm:grid-flow-row-dense sm:grid-cols-2 sm:gap-3'
85
+ } else if (buttonCount > 2) {
86
+ return 'flex flex-col gap-3'
87
+ }
88
+ return ''
89
+ })
90
+
91
+ const getButtonClass = (button, index) => {
92
+ const isPrimary = button.variant === 'primary'
93
+ const buttonCount = buttonsToShow.value.length
94
+
95
+ let baseClass = ''
96
+
97
+ // Position classes for 2-button layout
98
+ if (buttonCount === 2) {
99
+ if (isPrimary) {
100
+ baseClass = 'sm:col-start-2'
101
+ } else {
102
+ baseClass = 'mt-3 sm:col-start-1 sm:mt-0'
103
+ }
104
+ } else if (buttonCount > 2 && index > 0) {
105
+ baseClass = 'mt-3'
106
+ }
107
+
108
+ // Style classes
109
+ if (isPrimary) {
110
+ return `bg-primary-600 text-white hover:bg-primary-500 focus-visible:outline-primary-600 ${baseClass}`
111
+ } else {
112
+ // Secondary button style
113
+ return `bg-white text-gray-900 ring-1 ring-gray-300 ring-inset hover:bg-gray-50 ${baseClass}`
114
+ }
115
+ }
116
+
117
+ const defaultIcons = {
118
+ info: 'farInfoCircle',
119
+ success: 'farCheckCircle',
120
+ warning: 'farExclamationTriangle',
121
+ danger: 'farCircleXmark',
122
+ }
123
+
124
+ const iconToUse = computed(() => props.icon || defaultIcons[props.type])
125
+
126
+ const iconBgClass = computed(() => {
127
+ const colors = {
128
+ info: 'bg-blue-100',
129
+ success: 'bg-green-100',
130
+ warning: 'bg-yellow-100',
131
+ danger: 'bg-red-100',
132
+ }
133
+ return colors[props.type]
134
+ })
135
+
136
+ const iconTextClass = computed(() => {
137
+ const colors = {
138
+ info: 'text-blue-400',
139
+ success: 'text-green-400',
140
+ warning: 'text-yellow-400',
141
+ danger: 'text-red-400',
142
+ }
143
+ return colors[props.type]
144
+ })
145
+ </script>
@@ -1,42 +1,48 @@
1
1
  <template>
2
2
  <TransitionRoot :show="overlayInstance.state.isVisible" as="template">
3
- <Dialog :initialFocus="panelRef" class="relative z-200" @close="overlayInstance.close">
3
+ <Dialog
4
+ :initialFocus="panelRef"
5
+ class="relative z-200"
6
+ @close="overlayInstance.close"
7
+ >
4
8
  <TransitionChild
5
- as="template"
6
- enter="transition-opacity ease-linear duration-150"
7
- enter-from="opacity-0"
8
- enter-to="opacity-100"
9
- leave="transition-opacity ease-linear duration-150"
10
- leave-from="opacity-100"
11
- leave-to="opacity-0"
9
+ as="template"
10
+ enter="transition-opacity ease-linear duration-150"
11
+ enter-from="opacity-0"
12
+ enter-to="opacity-100"
13
+ leave="transition-opacity ease-linear duration-150"
14
+ leave-from="opacity-100"
15
+ leave-to="opacity-0"
12
16
  >
13
17
  <div class="fixed inset-0 bg-gray-600/30" />
14
18
  </TransitionChild>
15
19
 
16
20
  <div class="fixed inset-0 flex">
17
21
  <TransitionChild
18
- as="template"
19
- enter="transition ease-in-out duration-150 transform"
20
- :enter-from="
22
+ as="template"
23
+ enter="transition ease-in-out duration-150 transform"
24
+ :enter-from="
21
25
  position === 'left' ? '-translate-x-full opacity-0' : 'translate-x-full opacity-0'
22
26
  "
23
- enter-to="translate-x-0 opacity-100"
24
- leave="transition ease-in-out duration-150 transform"
25
- leave-from="translate-x-0 opacity-100"
26
- :leave-to="
27
+ enter-to="translate-x-0 opacity-100"
28
+ leave="transition ease-in-out duration-150 transform"
29
+ leave-from="translate-x-0 opacity-100"
30
+ :leave-to="
27
31
  position === 'left' ? '-translate-x-full opacity-0' : 'translate-x-full opacity-0'
28
32
  "
29
33
  >
30
34
  <DialogPanel
31
- ref="panelRef"
32
- :class="[
35
+ ref="panelRef"
36
+ :class="[
33
37
  'flex w-[400px] py-2.5',
34
38
  position === 'left' ? 'relative left-[10px]' : 'absolute right-[10px] h-full',
35
39
  ]"
36
40
  >
41
+ <!-- Bind props and event handlers dynamically -->
37
42
  <component
38
43
  :is="overlayInstance.state.content"
39
44
  v-bind="{ ...$attrs, ...overlayInstance.state.props }"
45
+ v-on="overlayInstance.state.eventHandlers"
40
46
  />
41
47
  </DialogPanel>
42
48
  </TransitionChild>
@@ -57,6 +63,7 @@ defineProps({
57
63
  required: true,
58
64
  validator: (value) => ['left', 'right'].includes(value),
59
65
  },
66
+
60
67
  overlayInstance: {
61
68
  type: Object,
62
69
  required: true,
@@ -1,7 +1,6 @@
1
- import SparkAlert from './SparkAlert.vue'
2
- import SparkAppSelector from './SparkAppSelector.vue'
3
- import SparkBrandSelector from './SparkBrandSelector.vue'
4
- import SparkModal from './SparkModal.vue'
5
- import SparkOverlay from './SparkOverlay.vue'
6
-
7
- export { SparkAlert, SparkAppSelector, SparkBrandSelector, SparkModal, SparkOverlay }
1
+ export { default as SparkAlert } from './SparkAlert.vue'
2
+ export { default as SparkAppSelector } from './SparkAppSelector.vue'
3
+ export { default as SparkBrandSelector } from './SparkBrandSelector.vue'
4
+ export { default as SparkModalContainer } from './SparkModalContainer.vue'
5
+ export { default as SparkModalDialog } from './SparkModalDialog.vue'
6
+ export { default as SparkOverlay } from './SparkOverlay.vue'
@@ -1 +1,3 @@
1
- export { useSparkOverlay } from './useSparkOverlay.js'
1
+ export { sparkModalService } from './sparkModalService.js'
2
+ export { sparkOverlayService } from './sparkOverlayService.js'
3
+ export { useSparkOverlay } from './useSparkOverlay.js'
@@ -0,0 +1,26 @@
1
+ import { reactive, markRaw } from 'vue'
2
+
3
+ class SparkModalService {
4
+ constructor() {
5
+ this.state = reactive({
6
+ isVisible: false,
7
+ content: null,
8
+ props: {},
9
+ eventHandlers: {},
10
+ })
11
+ }
12
+
13
+ show = (component, props = {}, eventHandlers = {}) => {
14
+ this.state.content = markRaw(component)
15
+ this.state.props = props
16
+ this.state.eventHandlers = eventHandlers
17
+ this.state.isVisible = true
18
+ }
19
+
20
+ hide = () => {
21
+ this.state.isVisible = false
22
+ this.state.eventHandlers = {}
23
+ }
24
+ }
25
+
26
+ export const sparkModalService = new SparkModalService()
@@ -0,0 +1,31 @@
1
+ import { useSparkOverlay } from '@/composables/useSparkOverlay'
2
+
3
+ class SparkOverlayService {
4
+ constructor() {
5
+ this.left = useSparkOverlay()
6
+ this.right = useSparkOverlay()
7
+ }
8
+
9
+ showLeft = (component, props = {}, eventHandlers = {}) => {
10
+ this.left.show(component, props, eventHandlers)
11
+ }
12
+
13
+ showRight = (component, props = {}, eventHandlers = {}) => {
14
+ this.right.show(component, props, eventHandlers)
15
+ }
16
+
17
+ closeLeft = () => {
18
+ this.left.close()
19
+ }
20
+
21
+ closeRight = () => {
22
+ this.right.close()
23
+ }
24
+
25
+ closeAll = () => {
26
+ this.left.close()
27
+ this.right.close()
28
+ }
29
+ }
30
+
31
+ export const sparkOverlayService = new SparkOverlayService()
@@ -5,6 +5,7 @@ export function useSparkOverlay() {
5
5
  isVisible: false,
6
6
  content: null,
7
7
  props: {},
8
+ eventHandlers: {},
8
9
  })
9
10
 
10
11
  const toggle = () => {
@@ -13,19 +14,21 @@ export function useSparkOverlay() {
13
14
 
14
15
  const close = () => {
15
16
  state.isVisible = false
17
+ state.eventHandlers = {}
16
18
  }
17
19
 
18
20
  const open = () => {
19
21
  state.isVisible = true
20
22
  }
21
23
 
22
- const setContent = (content, props = {}) => {
24
+ const setContent = (content, props = {}, eventHandlers = {}) => {
23
25
  state.content = markRaw(content)
24
26
  state.props = props
27
+ state.eventHandlers = eventHandlers
25
28
  }
26
29
 
27
- const show = (content, props = {}) => {
28
- if (content) setContent(content, props)
30
+ const show = (content, props = {}, eventHandlers = {}) => {
31
+ if (content) setContent(content, props, eventHandlers)
29
32
  open()
30
33
  }
31
34
 
@@ -37,4 +40,4 @@ export function useSparkOverlay() {
37
40
  setContent,
38
41
  show,
39
42
  }
40
- }
43
+ }
@@ -5,7 +5,7 @@ import mrPestControllerLogo from '@/assets/images/mr-pest-controller.png'
5
5
  import mrGutterCleaningLogo from '@/assets/images/mr-gutter-cleaning.png'
6
6
  import mrAntennaLogo from '@/assets/images/mr-antenna.png'
7
7
 
8
- export const useBrandFilterStore = defineStore('brandFilter', () => {
8
+ export const useSparkBrandFilterStore = defineStore('brandFilter', () => {
9
9
  const state = reactive({
10
10
  brands: [
11
11
  {
@@ -1,109 +0,0 @@
1
- <template>
2
- <TransitionRoot as="template" :show="open">
3
- <Dialog class="relative z-50" @close="$emit('close')">
4
- <TransitionChild
5
- as="template"
6
- enter="ease-out duration-300"
7
- enter-from="opacity-0"
8
- enter-to="opacity-100"
9
- leave="ease-in duration-200"
10
- leave-from="opacity-100"
11
- leave-to="opacity-0"
12
- >
13
- <div class="fixed inset-0 bg-gray-500/75 transition-opacity" />
14
- </TransitionChild>
15
-
16
- <div class="fixed inset-0 z-10 w-screen overflow-y-auto">
17
- <div
18
- class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0"
19
- >
20
- <TransitionChild
21
- as="template"
22
- enter="ease-out duration-300"
23
- enter-from="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
24
- enter-to="opacity-100 translate-y-0 sm:scale-100"
25
- leave="ease-in duration-200"
26
- leave-from="opacity-100 translate-y-0 sm:scale-100"
27
- leave-to="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
28
- >
29
- <DialogPanel
30
- :class="[
31
- 'relative transform overflow-hidden rounded-lg bg-white px-4 pt-5 pb-4 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:p-6',
32
- sizeClasses,
33
- ]"
34
- >
35
- <div>
36
- <!-- Icon -->
37
- <div v-if="$slots.icon">
38
- <slot name="icon" />
39
- </div>
40
-
41
- <div class="text-center sm:mt-5">
42
- <!-- Header with title -->
43
- <template v-if="$slots.title">
44
- <DialogTitle as="div" class="text-base font-semibold text-gray-900">
45
- <slot name="title" />
46
- </DialogTitle>
47
- </template>
48
-
49
- <!-- Main content -->
50
- <div :class="{ 'mt-2': $slots.title }">
51
- <slot />
52
- </div>
53
- </div>
54
- </div>
55
-
56
- <!-- Actions/Buttons -->
57
- <div
58
- v-if="$slots.actions"
59
- class="mt-5 sm:mt-6 sm:grid sm:grid-flow-row-dense sm:grid-cols-2 sm:gap-3"
60
- >
61
- <slot name="actions" />
62
- </div>
63
- </DialogPanel>
64
- </TransitionChild>
65
- </div>
66
- </div>
67
- </Dialog>
68
- </TransitionRoot>
69
- </template>
70
-
71
- <script setup>
72
- import { computed } from 'vue'
73
- import { Dialog, DialogPanel, DialogTitle, TransitionChild, TransitionRoot } from '@headlessui/vue'
74
-
75
- const props = defineProps({
76
- open: {
77
- type: Boolean,
78
- required: true,
79
- },
80
-
81
- size: {
82
- type: String,
83
- default: 'md',
84
- validator: (value) => ['sm', 'md', 'lg', 'xl', '2xl'].includes(value),
85
- },
86
-
87
- icon: {
88
- type: String,
89
- },
90
-
91
- iconColor: {
92
- type: String,
93
- },
94
- })
95
-
96
- defineEmits(['close'])
97
-
98
- const sizeClasses = computed(() => {
99
- const sizes = {
100
- sm: 'sm:max-w-sm',
101
- md: 'sm:max-w-lg',
102
- lg: 'sm:max-w-2xl',
103
- xl: 'sm:max-w-4xl',
104
- '2xl': 'sm:max-w-6xl',
105
- }
106
-
107
- return sizes[props.size]
108
- })
109
- </script>