@wishbone-media/spark 0.11.0 → 0.12.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.11.0",
3
+ "version": "0.12.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -12,12 +12,14 @@
12
12
  />
13
13
  </div>
14
14
  </div>
15
- <div
15
+ <a
16
16
  v-for="app in appsWithCurrent"
17
17
  :key="app.name"
18
+ :href="app.href"
19
+ target="_blank"
18
20
  :class="app.current ? 'bg-gray-50' : 'hover:bg-gray-50'"
19
21
  class="flex px-[22px] py-[15px] cursor-pointer"
20
- @click="openNewTab(app)"
22
+ @click="app.current ? $event.preventDefault() : handleAppClick(app, $event)"
21
23
  >
22
24
  <div class="gap-y-1">
23
25
  <div class="text-base text-gray-800 flex items-center">
@@ -38,12 +40,21 @@
38
40
  class="h-5 w-5 shrink-0"
39
41
  />
40
42
  </div>
41
- </div>
43
+ </a>
42
44
  <div></div>
43
45
  </div>
44
46
  <div class="mt-auto">
45
- <div class="p-6">Learn More</div>
46
- <div class="bg-gray-50 p-6">Footer</div>
47
+ <div v-if="$slots.bottom || bottomSlot" class="p-6">
48
+ <slot name="bottom">
49
+ <component v-if="bottomSlot" :is="bottomSlot" />
50
+ </slot>
51
+ </div>
52
+
53
+ <div v-if="$slots.footer || footerSlot" class="bg-gray-50 p-6">
54
+ <slot name="footer">
55
+ <component v-if="footerSlot" :is="footerSlot" />
56
+ </slot>
57
+ </div>
47
58
  </div>
48
59
  </div>
49
60
  </div>
@@ -52,69 +63,34 @@
52
63
  <script setup>
53
64
  import { computed } from 'vue'
54
65
  import { Icons } from '@/plugins/fontawesome'
66
+ import { useSparkAppSelectorStore } from '@/stores/app-selector'
67
+ import { useSparkAppStore } from '@/stores/app'
55
68
 
56
69
  const props = defineProps({
57
- appItems: {
58
- type: Array,
59
- default() {
60
- return [
61
- {
62
- name: '3CX',
63
- description: 'VOIP Phone',
64
- href: 'https://3cx.letsbolt.com.au',
65
- icon: 'farLaptopMobile',
66
- },
67
- {
68
- name: 'Buzz',
69
- description: 'Communication on the go',
70
- href: 'https://buzz.letsbolt.com.au',
71
- icon: 'farSatelliteDish',
72
- },
73
- {
74
- name: 'Dash',
75
- description: 'Financial powerhouse',
76
- href: 'https://dash.letsbolt.com.au',
77
- icon: 'farScaleBalanced',
78
- },
79
- {
80
- name: 'MAPit',
81
- description: 'Geolocation everything',
82
- href: 'https://mapit.letsbolt.com.au',
83
- icon: 'farStreetView',
84
- },
85
- {
86
- name: 'ProspectR',
87
- description: 'Leads management',
88
- href: 'https://prospectr.letsbolt.com.au',
89
- icon: 'farFaceSmileRelaxed',
90
- },
91
- {
92
- name: 'ReVuze',
93
- description: 'Get Customer feedback',
94
- href: 'https://revuze.letsbolt.com.au',
95
- icon: 'farComments',
96
- },
97
- ]
98
- },
70
+ bottomSlot: {
71
+ type: [Object, Function],
72
+ default: null,
99
73
  },
100
- currentApp: {
101
- type: String,
102
- default: 'Buzz',
74
+ footerSlot: {
75
+ type: [Object, Function],
76
+ default: null,
103
77
  },
104
78
  })
105
79
 
106
80
  const emit = defineEmits(['close', 'select'])
107
81
 
82
+ const appSelectorStore = useSparkAppSelectorStore()
83
+ const appStore = useSparkAppStore()
84
+
108
85
  const appsWithCurrent = computed(() => {
109
- return props.appItems.map(app => ({
86
+ const currentAppName = appStore.state.app
87
+ return appSelectorStore.state.apps.map(app => ({
110
88
  ...app,
111
- current: app.name === props.currentApp
89
+ current: currentAppName && app.name.toLowerCase() === currentAppName.toLowerCase()
112
90
  }))
113
91
  })
114
92
 
115
- const openNewTab = (app) => {
116
- window.open(app.href, '_blank')
117
-
93
+ const handleAppClick = (app, event) => {
118
94
  emit('select', app)
119
95
  }
120
96
  </script>
@@ -10,7 +10,7 @@
10
10
  class="grid w-[40px] h-[40px] place-items-center rounded-md bg-primary-600 text-white text-[13px] cursor-pointer"
11
11
  @click.prevent="mainNavStore.goto(appStore.state.homeRoute)"
12
12
  >
13
- <font-awesome-icon :icon="Icons[appStore.state.icon]" class="size-5" />
13
+ <font-awesome-icon :icon="Icons[appIcon]" class="size-5" />
14
14
  </a>
15
15
  <a
16
16
  @click.prevent="mainNavStore.goto(appStore.state.homeRoute)"
@@ -165,11 +165,12 @@
165
165
  </template>
166
166
 
167
167
  <script setup>
168
- import { computed } from 'vue'
168
+ import { computed, h, useSlots } from 'vue'
169
169
  import {
170
170
  SparkOverlay,
171
171
  SparkBrandSelector,
172
172
  useSparkBrandFilterStore,
173
+ useSparkAppSelectorStore,
173
174
  SparkAppSelector,
174
175
  sparkOverlayService,
175
176
  SparkModalContainer,
@@ -185,14 +186,38 @@ const props = defineProps({
185
186
  type: Object,
186
187
  required: true,
187
188
  },
189
+ appSelectorSlots: {
190
+ type: Object,
191
+ default: () => ({}),
192
+ },
188
193
  })
189
194
 
195
+ const slots = useSlots()
196
+
190
197
  const sparkBrandFilterStore = useSparkBrandFilterStore()
198
+ const sparkAppSelectorStore = useSparkAppSelectorStore()
199
+
200
+ const appIcon = computed(() => {
201
+ return sparkAppSelectorStore.getAppIcon(props.appStore.state.app)
202
+ })
191
203
 
192
204
  const toggleAppSelector = () => {
193
- sparkOverlayService.showRight(SparkAppSelector, {
194
- currentApp: props.appStore.state.app,
195
- })
205
+ // Create component wrappers for slots if they exist
206
+ const slotProps = {}
207
+
208
+ if (slots['app-selector-bottom']) {
209
+ slotProps.bottomSlot = () => h('div', {}, slots['app-selector-bottom']())
210
+ } else if (props.appSelectorSlots.bottomSlot) {
211
+ slotProps.bottomSlot = props.appSelectorSlots.bottomSlot
212
+ }
213
+
214
+ if (slots['app-selector-footer']) {
215
+ slotProps.footerSlot = () => h('div', {}, slots['app-selector-footer']())
216
+ } else if (props.appSelectorSlots.footerSlot) {
217
+ slotProps.footerSlot = props.appSelectorSlots.footerSlot
218
+ }
219
+
220
+ sparkOverlayService.showRight(SparkAppSelector, slotProps)
196
221
  }
197
222
 
198
223
  const toggleBrandSelector = () => {
@@ -14,7 +14,7 @@ export function createAuthRoutes(options = {}) {
14
14
  loginPath = '/login',
15
15
  logoutPath = '/logout',
16
16
  forgotPasswordPath = '/forgot-password',
17
- resetPasswordPath = '/reset-password',
17
+ resetPasswordPath = '/password/reset',
18
18
  logo = '',
19
19
  defaultRedirect = '/dashboard',
20
20
  } = options
@@ -43,7 +43,7 @@ export function createAuthRoutes(options = {}) {
43
43
  },
44
44
  {
45
45
  path: resetPasswordPath,
46
- name: 'reset-password',
46
+ name: 'password-reset',
47
47
  component: SparkResetPasswordView,
48
48
  props: { logo, loginRoute: loginPath },
49
49
  meta: { auth: false },
@@ -0,0 +1,140 @@
1
+ import { defineStore } from 'pinia'
2
+ import { computed, reactive } from 'vue'
3
+
4
+ const DEFAULT_APPS = [
5
+ {
6
+ name: '3CX',
7
+ description: 'VOIP Phone',
8
+ href: 'https://tmrg.3cx.com.au:5001/webclient',
9
+ icon: 'farLaptopMobile',
10
+ },
11
+ {
12
+ name: 'Buzz',
13
+ description: 'Communication on the go',
14
+ href: 'https://buzz-next.letsbolt.io',
15
+ icon: 'farSatelliteDish',
16
+ },
17
+ {
18
+ name: 'Dash',
19
+ description: 'Financial powerhouse',
20
+ href: 'https://dash-next.letsbolt.io',
21
+ icon: 'farScaleBalanced',
22
+ },
23
+ {
24
+ name: 'MAPit',
25
+ description: 'Geolocation everything',
26
+ href: 'https://mapit-next.letsbolt.io',
27
+ icon: 'farStreetView',
28
+ },
29
+ {
30
+ name: 'ProspectR',
31
+ description: 'Leads management',
32
+ href: 'https://prospectr-next.letsbolt.io',
33
+ icon: 'farFaceSmileRelaxed',
34
+ },
35
+ {
36
+ name: 'ReVuze',
37
+ description: 'Get Customer feedback',
38
+ href: 'https://revuze-next.letsbolt.io',
39
+ icon: 'farComments',
40
+ },
41
+ {
42
+ name: 'Tabula',
43
+ description: 'Admin interface',
44
+ href: 'https://tabula.letsbolt.io',
45
+ icon: 'farCompass',
46
+ },
47
+ ]
48
+
49
+ export const useSparkAppSelectorStore = defineStore('sparkAppSelector', () => {
50
+ const state = reactive({
51
+ apps: [...DEFAULT_APPS],
52
+ })
53
+
54
+ /**
55
+ * Initialize app selector store with either partial href config or full app override
56
+ * @param {Object} config - Configuration object
57
+ * @param {Object} config.appHrefs - Object mapping app names to hrefs (case-insensitive)
58
+ * Example: { 'Buzz': 'https://buzz.staging.example.com' }
59
+ * @param {Array} config.apps - Complete array of app objects to override defaults
60
+ * Example: [{ name: 'Buzz', description: '...', icon: '...', href: '...' }]
61
+ */
62
+ const initialize = (config = {}) => {
63
+ // Full override: if apps array is provided, use it entirely
64
+ if (config.apps && Array.isArray(config.apps)) {
65
+ const validApps = config.apps.filter((app) => {
66
+ const isValid = app.name && app.description && app.icon && app.href
67
+ if (!isValid) {
68
+ console.warn('useSparkAppSelectorStore: Invalid app object', app)
69
+ }
70
+ return isValid
71
+ })
72
+
73
+ state.apps = validApps.map((app) => ({
74
+ name: app.name,
75
+ description: app.description,
76
+ icon: app.icon,
77
+ href: app.href,
78
+ }))
79
+
80
+ return
81
+ }
82
+
83
+ // Partial config: merge appHrefs into defaults
84
+ if (config.appHrefs && typeof config.appHrefs === 'object') {
85
+ // Create a case-insensitive lookup map
86
+ const hrefMap = new Map()
87
+ Object.keys(config.appHrefs).forEach((appName) => {
88
+ hrefMap.set(appName.toLowerCase(), config.appHrefs[appName])
89
+ })
90
+
91
+ // Update default apps with provided hrefs
92
+ state.apps = DEFAULT_APPS.map((app) => {
93
+ const customHref = hrefMap.get(app.name.toLowerCase())
94
+ return {
95
+ ...app,
96
+ href: customHref || app.href,
97
+ }
98
+ })
99
+
100
+ return
101
+ }
102
+
103
+ // No config provided: use defaults
104
+ state.apps = [...DEFAULT_APPS]
105
+ }
106
+
107
+ /**
108
+ * Get all available apps
109
+ */
110
+ const allApps = computed(() => state.apps)
111
+
112
+ /**
113
+ * Get app by name (case-insensitive)
114
+ * @param {string} appName - Name of the app to find
115
+ * @returns {Object|null} App object or null if not found
116
+ */
117
+ const getAppByName = (appName) => {
118
+ if (!appName) return null
119
+ const normalizedName = appName.toLowerCase()
120
+ return state.apps.find((app) => app.name.toLowerCase() === normalizedName) || null
121
+ }
122
+
123
+ /**
124
+ * Get icon for a specific app (case-insensitive)
125
+ * @param {string} appName - Name of the app
126
+ * @returns {string} Icon name or empty string if not found
127
+ */
128
+ const getAppIcon = (appName) => {
129
+ const app = getAppByName(appName)
130
+ return app ? app.icon : ''
131
+ }
132
+
133
+ return {
134
+ state,
135
+ initialize,
136
+ allApps,
137
+ getAppByName,
138
+ getAppIcon,
139
+ }
140
+ })
package/src/stores/app.js CHANGED
@@ -4,7 +4,6 @@ import { reactive } from 'vue'
4
4
  export const useSparkAppStore = defineStore('sparkApp', () => {
5
5
  const state = reactive({
6
6
  app: '',
7
- icon: '',
8
7
  homeRoute: 'dashboard',
9
8
  showBrandSelector: true,
10
9
  showAppSelector: true,
@@ -12,7 +11,6 @@ export const useSparkAppStore = defineStore('sparkApp', () => {
12
11
 
13
12
  const initialize = (config = {}) => {
14
13
  state.app = config.app || ''
15
- state.icon = config.icon || ''
16
14
  state.homeRoute = config.homeRoute ?? 'dashboard'
17
15
  state.showBrandSelector = config.showBrandSelector ?? true
18
16
  state.showAppSelector = config.showAppSelector ?? true
@@ -1,4 +1,5 @@
1
1
  export * from './app.js'
2
+ export * from './app-selector.js'
2
3
  export * from './auth.js'
3
4
  export * from './brand-filter.js'
4
5
  export * from './navigation.js'
@@ -50,7 +50,7 @@
50
50
  name="password_confirmation"
51
51
  placeholder="••••••••"
52
52
  type="password"
53
- validation="required|confirm"
53
+ validation="required|confirm:password"
54
54
  outer-class="max-w-full"
55
55
  />
56
56