@wishbone-media/spark 0.2.2 → 0.4.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.2.2",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -9,16 +9,16 @@
9
9
  "src"
10
10
  ],
11
11
  "peerDependencies": {
12
- "@headlessui/vue": "^1.7.0",
13
- "@fortawesome/fontawesome-svg-core": "^6.7.2",
14
- "@fortawesome/pro-regular-svg-icons": "^6.7.2",
15
- "pinia": "^3.0.3",
16
- "vue": "^3.5.16"
12
+ "@fortawesome/fontawesome-svg-core": "^7.1.0",
13
+ "@fortawesome/pro-regular-svg-icons": "^7.1.0",
14
+ "@headlessui/vue": "^1.7.23",
15
+ "pinia": "^3.0.4",
16
+ "vue": "^3.5.24"
17
17
  },
18
18
  "devDependencies": {
19
- "@vitejs/plugin-vue": "^5.2.4",
20
- "vite": "^6.3.5",
21
- "vue": "^3.5.16"
19
+ "@vitejs/plugin-vue": "^6.0.2",
20
+ "vite": "^7.2.2",
21
+ "vue": "^3.5.24"
22
22
  },
23
23
  "packageManager": "pnpm@10.11.1",
24
24
  "scripts": {
@@ -33,5 +33,8 @@
33
33
  },
34
34
  "publishConfig": {
35
35
  "access": "public"
36
+ },
37
+ "dependencies": {
38
+ "@wishbone-media/spark": "file:"
36
39
  }
37
40
  }
@@ -0,0 +1,162 @@
1
+ <template>
2
+ <button
3
+ type="button"
4
+ ref="buttonRef"
5
+ :class="computedButtonClass"
6
+ :disabled="disabled"
7
+ @click="$emit('click')"
8
+ >
9
+ <slot></slot>
10
+ </button>
11
+ </template>
12
+
13
+ <script setup>
14
+ import { computed, inject, ref } from 'vue'
15
+
16
+ const props = defineProps({
17
+ size: {
18
+ type: String,
19
+ default: 'md',
20
+ validator: (value) => ['xs', 'sm', 'md', 'lg', 'xl'].includes(value),
21
+ },
22
+ variant: {
23
+ type: String,
24
+ default: 'primary',
25
+ },
26
+ buttonClass: {
27
+ type: String,
28
+ default: '',
29
+ },
30
+ disabled: {
31
+ type: Boolean,
32
+ default: false,
33
+ },
34
+ })
35
+
36
+ const buttonRef = ref(null)
37
+ const buttonGroup = inject('buttonGroup', null)
38
+
39
+ const groupPosition = computed(() => {
40
+ if (!buttonGroup?.isInGroup || !buttonRef.value) return null
41
+
42
+ const index = buttonGroup.getButtonIndex(buttonRef.value)
43
+ const total = buttonGroup.getButtonCount()
44
+
45
+ return {
46
+ isFirst: index === 0,
47
+ isLast: index === total - 1,
48
+ index,
49
+ total,
50
+ }
51
+ })
52
+
53
+ const CONFLICT_GROUPS = {
54
+ paddingX: /^px-/,
55
+ paddingY: /^py-/,
56
+ paddingAll: /^p-/,
57
+ marginX: /^mx-/,
58
+ marginY: /^my-/,
59
+ marginAll: /^m-/,
60
+ borderRadius: /^rounded-/,
61
+ background: /^bg-/,
62
+ text: /^text-(?!white|black)/,
63
+ }
64
+
65
+ const mergeClasses = (baseClasses, overrideClasses) => {
66
+ if (!overrideClasses) return baseClasses
67
+
68
+ const base = baseClasses.split(' ').filter(Boolean)
69
+ const overrides = overrideClasses.split(' ').filter(Boolean)
70
+
71
+ const filtered = base.filter((baseClass) => {
72
+ return !overrides.some((override) => {
73
+ const baseGroup = Object.entries(CONFLICT_GROUPS).find(([_, regex]) =>
74
+ regex.test(baseClass),
75
+ )?.[0]
76
+
77
+ const overrideGroup = Object.entries(CONFLICT_GROUPS).find(([_, regex]) =>
78
+ regex.test(override),
79
+ )?.[0]
80
+
81
+ return baseGroup && baseGroup === overrideGroup
82
+ })
83
+ })
84
+
85
+ return [...filtered, ...overrides].join(' ')
86
+ }
87
+
88
+ const computedButtonClass = computed(() => {
89
+ let classes = ''
90
+ let roundingSize = ''
91
+
92
+ switch (props.size) {
93
+ case 'xs':
94
+ classes += ' px-2 py-1 text-xs'
95
+ roundingSize = 'sm'
96
+ break
97
+ case 'sm':
98
+ classes += ' px-2 py-1 text-sm'
99
+ roundingSize = 'sm'
100
+ break
101
+ case 'md':
102
+ classes += ' px-2.5 py-1.5 text-sm'
103
+ roundingSize = 'md'
104
+ break
105
+ case 'lg':
106
+ classes += ' px-3 py-2 text-sm'
107
+ roundingSize = 'md'
108
+ break
109
+ case 'xl':
110
+ classes += ' px-3.5 py-2.5 text-sm'
111
+ roundingSize = 'md'
112
+ break
113
+ }
114
+
115
+ if (buttonGroup?.isInGroup && groupPosition.value) {
116
+ const { isFirst, isLast } = groupPosition.value
117
+
118
+ classes += ` relative inline-flex items-center focus:z-10`
119
+
120
+ if (isFirst && isLast) {
121
+ // Single button in group
122
+ classes += ` rounded-${roundingSize}`
123
+ } else if (isFirst) {
124
+ classes += ` rounded-l-${roundingSize} rounded-r-none`
125
+ } else if (isLast) {
126
+ classes += ` rounded-r-${roundingSize} rounded-l-none -ml-px`
127
+ } else {
128
+ classes += ' rounded-none -ml-px'
129
+ }
130
+ } else {
131
+ classes += ` shadow-xs rounded-${roundingSize}`
132
+ }
133
+
134
+ switch (props.variant) {
135
+ case 'primary':
136
+ classes += ' bg-primary-600 hover:bg-primary-500 text-white'
137
+ break
138
+ case 'secondary':
139
+ classes += ' ring-1 ring-gray-300 ring-inset bg-white hover:bg-gray-50 text-gray-900'
140
+ break
141
+ case 'success':
142
+ classes += ' bg-green-600 hover:bg-green-500 text-white'
143
+ break
144
+ case 'warning':
145
+ classes += ' bg-amber-600 hover:bg-amber-500 text-white'
146
+ break
147
+ case 'danger':
148
+ classes += ' bg-red-600 hover:bg-red-500 text-white'
149
+ break
150
+ case 'info':
151
+ classes += ' bg-cyan-600 hover:bg-cyan-500 text-white'
152
+ break
153
+
154
+ default:
155
+ classes += ` bg-${props.variant}-600 hover:bg-${props.variant}-500 text-white`
156
+ }
157
+
158
+ classes = mergeClasses(classes, props.buttonClass)
159
+
160
+ return classes
161
+ })
162
+ </script>
@@ -0,0 +1,27 @@
1
+ <template>
2
+ <div class="inline-flex rounded-md shadow-xs" ref="groupRef">
3
+ <slot></slot>
4
+ </div>
5
+ </template>
6
+
7
+ <script setup>
8
+ import { provide, ref } from 'vue'
9
+
10
+ const groupRef = ref(null)
11
+
12
+ const getButtonIndex = (buttonElement) => {
13
+ if (!groupRef.value) return -1
14
+ const buttons = Array.from(groupRef.value.children)
15
+ return buttons.indexOf(buttonElement)
16
+ }
17
+
18
+ const getButtonCount = () => {
19
+ return groupRef.value?.children.length || 0
20
+ }
21
+
22
+ provide('buttonGroup', {
23
+ isInGroup: true,
24
+ getButtonIndex,
25
+ getButtonCount,
26
+ })
27
+ </script>
@@ -20,16 +20,14 @@
20
20
 
21
21
  <!-- Actions -->
22
22
  <div class="mt-5 sm:mt-6" :class="buttonContainerClass">
23
- <button
23
+ <spark-button
24
24
  v-for="(button, index) in buttonsToShow"
25
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"
26
+ :variant="button.variant"
29
27
  @click="$emit(button.event, button)"
30
28
  >
31
29
  {{ button.text }}
32
- </button>
30
+ </spark-button>
33
31
  </div>
34
32
  </div>
35
33
  </template>
@@ -37,6 +35,7 @@
37
35
  <script setup>
38
36
  import { computed } from 'vue'
39
37
  import { Icons } from '@/plugins/fontawesome'
38
+ import { SparkButton } from "./index.js";
40
39
 
41
40
  const props = defineProps({
42
41
  title: {
@@ -79,41 +78,18 @@ const buttonsToShow = computed(() => {
79
78
  })
80
79
 
81
80
  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'
81
+ switch (buttonsToShow.value.length) {
82
+ case 1:
83
+ return 'sm:grid sm:grid-flow-row-dense'
84
+ case 2:
85
+ return 'sm:grid sm:grid-cols-2 sm:gap-3';
86
+ case 3:
87
+ return 'sm:grid sm:grid-cols-3 sm:gap-3';
88
+ default:
89
+ return 'flex flex-col gap-3';
87
90
  }
88
- return ''
89
91
  })
90
92
 
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
93
  const defaultIcons = {
118
94
  info: 'farInfoCircle',
119
95
  success: 'farCheckCircle',
@@ -1,6 +1,8 @@
1
1
  export { default as SparkAlert } from './SparkAlert.vue'
2
2
  export { default as SparkAppSelector } from './SparkAppSelector.vue'
3
3
  export { default as SparkBrandSelector } from './SparkBrandSelector.vue'
4
+ export { default as SparkButton } from './SparkButton.vue'
5
+ export { default as SparkButtonGroup } from './SparkButtonGroup.vue'
4
6
  export { default as SparkModalContainer } from './SparkModalContainer.vue'
5
7
  export { default as SparkModalDialog } from './SparkModalDialog.vue'
6
8
  export { default as SparkOverlay } from './SparkOverlay.vue'