@wishbone-media/spark 0.41.0 → 0.43.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/dist/index.js +2877 -2847
- package/package.json +1 -1
- package/src/components/SparkButtonGroup.vue +12 -2
- package/src/components/index.js +1 -0
- package/src/composables/useSubNavigation.js +37 -2
- package/src/containers/SparkDefaultContainer.vue +108 -82
package/package.json
CHANGED
|
@@ -5,9 +5,17 @@
|
|
|
5
5
|
</template>
|
|
6
6
|
|
|
7
7
|
<script setup>
|
|
8
|
-
import { provide, ref } from 'vue'
|
|
8
|
+
import { provide, ref, onMounted, onUpdated } from 'vue'
|
|
9
9
|
|
|
10
10
|
const groupRef = ref(null)
|
|
11
|
+
const childCount = ref(0)
|
|
12
|
+
|
|
13
|
+
function syncChildCount() {
|
|
14
|
+
childCount.value = groupRef.value?.children.length || 0
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
onMounted(syncChildCount)
|
|
18
|
+
onUpdated(syncChildCount)
|
|
11
19
|
|
|
12
20
|
const getButtonIndex = (buttonElement) => {
|
|
13
21
|
if (!groupRef.value) return -1
|
|
@@ -16,7 +24,9 @@ const getButtonIndex = (buttonElement) => {
|
|
|
16
24
|
}
|
|
17
25
|
|
|
18
26
|
const getButtonCount = () => {
|
|
19
|
-
|
|
27
|
+
// Reading childCount.value creates a reactive dependency so SparkButton's
|
|
28
|
+
// computed re-evaluates when items are added/removed
|
|
29
|
+
return childCount.value
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
provide('buttonGroup', {
|
package/src/components/index.js
CHANGED
|
@@ -17,6 +17,7 @@ export { default as SparkModalDialog } from './SparkModalDialog.vue'
|
|
|
17
17
|
export { default as SparkOverlay } from './SparkOverlay.vue'
|
|
18
18
|
export { default as SparkSubNav } from './SparkSubNav.vue'
|
|
19
19
|
export { default as SparkTable } from './SparkTable.vue'
|
|
20
|
+
export { default as SparkTablePaginationDetails } from './SparkTablePaginationDetails.vue'
|
|
20
21
|
export { default as SparkTablePaginationPaging } from './SparkTablePaginationPaging.vue'
|
|
21
22
|
export { default as SparkTablePaginationPerPage } from './SparkTablePaginationPerPage.vue'
|
|
22
23
|
export { default as SparkTableToolbar } from './SparkTableToolbar.vue'
|
|
@@ -9,9 +9,13 @@ import { sparkOverlayService } from './sparkOverlayService.js'
|
|
|
9
9
|
* @param {Object} options - Configuration options
|
|
10
10
|
* @param {Array} options.items - Navigation items array
|
|
11
11
|
* @param {string} [options.defaultId] - Default active item ID (falls back to first visible item)
|
|
12
|
+
* @param {boolean|string} [options.syncToQuery=false] - Sync active tab to URL query param.
|
|
13
|
+
* `true` uses 'tab' as the param name, a string uses that as the param name.
|
|
14
|
+
* When enabled, items without a route/overlay/modal/action will update the query param on navigate.
|
|
12
15
|
* @returns {Object} Sub-navigation utilities
|
|
13
16
|
*
|
|
14
17
|
* @example
|
|
18
|
+
* // Route-based tabs
|
|
15
19
|
* const subNav = useSubNavigation({
|
|
16
20
|
* items: [
|
|
17
21
|
* { id: 'general', label: 'General', route: { name: 'brands.general' } },
|
|
@@ -21,6 +25,17 @@ import { sparkOverlayService } from './sparkOverlayService.js'
|
|
|
21
25
|
* ]
|
|
22
26
|
* })
|
|
23
27
|
*
|
|
28
|
+
* @example
|
|
29
|
+
* // Local tabs with URL query sync (e.g. ?tab=addresses)
|
|
30
|
+
* const subNav = useSubNavigation({
|
|
31
|
+
* items: [
|
|
32
|
+
* { id: 'contacts', label: 'Contacts' },
|
|
33
|
+
* { id: 'addresses', label: 'Addresses' },
|
|
34
|
+
* ],
|
|
35
|
+
* syncToQuery: true,
|
|
36
|
+
* defaultId: 'contacts',
|
|
37
|
+
* })
|
|
38
|
+
*
|
|
24
39
|
* // Item structure:
|
|
25
40
|
* // {
|
|
26
41
|
* // id: 'general', // Unique identifier (required)
|
|
@@ -41,7 +56,10 @@ import { sparkOverlayService } from './sparkOverlayService.js'
|
|
|
41
56
|
* // }
|
|
42
57
|
*/
|
|
43
58
|
export function useSubNavigation(options = {}) {
|
|
44
|
-
const { items: initialItems = [], defaultId = null } = options
|
|
59
|
+
const { items: initialItems = [], defaultId = null, syncToQuery = false } = options
|
|
60
|
+
|
|
61
|
+
// Resolve syncToQuery: true → 'tab', string → custom param name, false → null
|
|
62
|
+
const syncToQueryParam = syncToQuery === true ? 'tab' : syncToQuery || null
|
|
45
63
|
|
|
46
64
|
const router = useRouter()
|
|
47
65
|
const route = useRoute()
|
|
@@ -95,7 +113,18 @@ export function useSubNavigation(options = {}) {
|
|
|
95
113
|
*/
|
|
96
114
|
function isActive(item) {
|
|
97
115
|
const itemObj = typeof item === 'string' ? items.value.find((i) => i.id === item) : item
|
|
98
|
-
if (!itemObj
|
|
116
|
+
if (!itemObj) return false
|
|
117
|
+
|
|
118
|
+
// syncToQuery mode: match against query param value for items without a route
|
|
119
|
+
if (syncToQueryParam && !itemObj.route) {
|
|
120
|
+
const queryValue = route.query[syncToQueryParam]
|
|
121
|
+
if (queryValue) return itemObj.id === queryValue
|
|
122
|
+
// No query param → active if this is the default/first visible item
|
|
123
|
+
const fallbackId = defaultId || visibleItems.value[0]?.id
|
|
124
|
+
return itemObj.id === fallbackId
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!itemObj.route) return false
|
|
99
128
|
|
|
100
129
|
// Handle route as string (route name) or object
|
|
101
130
|
const itemRoute = typeof itemObj.route === 'string' ? { name: itemObj.route } : itemObj.route
|
|
@@ -191,6 +220,12 @@ export function useSubNavigation(options = {}) {
|
|
|
191
220
|
return true
|
|
192
221
|
}
|
|
193
222
|
|
|
223
|
+
// Handle syncToQuery for items without explicit navigation type
|
|
224
|
+
if (syncToQueryParam) {
|
|
225
|
+
await router.replace({ query: { ...route.query, [syncToQueryParam]: itemObj.id } })
|
|
226
|
+
return true
|
|
227
|
+
}
|
|
228
|
+
|
|
194
229
|
return false
|
|
195
230
|
}
|
|
196
231
|
|
|
@@ -1,102 +1,118 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div :class="sidebarClass" class="fixed inset-y-0 flex transition-all z-100">
|
|
3
3
|
<div class="flex grow m-2.5 p-[10px] rounded-lg">
|
|
4
|
-
<nav class="flex flex-1 flex-col">
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
4
|
+
<nav class="flex flex-1 flex-col min-h-0">
|
|
5
|
+
<!-- Logo (fixed at top) -->
|
|
6
|
+
<div class="flex-shrink-0 flex items-center pb-8">
|
|
7
|
+
<a
|
|
8
|
+
class="grid w-[40px] h-[40px] place-items-center rounded-md bg-primary-600 text-white text-[13px] cursor-pointer"
|
|
9
|
+
@click.prevent="mainNavStore.goto(appStore.state.homeRoute)"
|
|
10
|
+
>
|
|
11
|
+
<font-awesome-icon :icon="Icons[appIcon]" class="size-5" />
|
|
12
|
+
</a>
|
|
13
|
+
<a
|
|
14
|
+
v-if="!mainNavStore.state.collapsed"
|
|
15
|
+
class="font-medium text-gray-800 ml-[10px] cursor-pointer"
|
|
16
|
+
@click.prevent="mainNavStore.goto(appStore.state.homeRoute)"
|
|
17
|
+
>
|
|
18
|
+
{{ appStore.state.app }}
|
|
19
|
+
</a>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<!-- Menu items (scrollable) -->
|
|
23
|
+
<ul role="list" class="flex-1 overflow-y-auto">
|
|
24
|
+
<template v-for="item in menuItems" :key="item.name">
|
|
25
|
+
<li :class="{ 'mt-[10px]': item.children }">
|
|
26
|
+
<a
|
|
27
|
+
:class="{
|
|
28
|
+
'bg-gray-100': isCurrentRoute(item),
|
|
29
|
+
'hover:bg-gray-100': item?.href,
|
|
30
|
+
}"
|
|
31
|
+
:href="item?.href"
|
|
32
|
+
class="h-[37px] sgroup flex items-center gap-x-2 rounded-md p-3 text-gray-800 leading-5 transition-all duration-300 ease-in-out"
|
|
33
|
+
@click.prevent="mainNavStore.goto(item.href)"
|
|
34
|
+
>
|
|
35
|
+
<font-awesome-icon
|
|
36
|
+
v-if="item.icon"
|
|
37
|
+
:icon="Icons[item.icon]"
|
|
38
|
+
:class="[isCurrentRoute(item) ? 'text-gray-400' : 'text-gray-400']"
|
|
39
|
+
class="size-4"
|
|
40
|
+
/>
|
|
41
|
+
<span
|
|
16
42
|
v-if="!mainNavStore.state.collapsed"
|
|
17
|
-
class="font-medium text-gray-800 ml-[10px] cursor-pointer"
|
|
18
|
-
@click.prevent="mainNavStore.goto(appStore.state.homeRoute)"
|
|
19
|
-
>
|
|
20
|
-
{{ appStore.state.app }}
|
|
21
|
-
</a>
|
|
22
|
-
</li>
|
|
23
|
-
<template v-for="item in mainNavStore.state.menu" :key="item.name">
|
|
24
|
-
<li
|
|
25
43
|
:class="{
|
|
26
|
-
'
|
|
27
|
-
'
|
|
44
|
+
'text-[11px]': item?.children,
|
|
45
|
+
'text-[13px]': !item?.children,
|
|
46
|
+
'font-semibold': item?.children,
|
|
47
|
+
'text-gray-500': item?.children,
|
|
28
48
|
}"
|
|
29
49
|
>
|
|
50
|
+
{{ item.name }}
|
|
51
|
+
</span>
|
|
52
|
+
<div v-else-if="item?.children" class="w-full flex justify-center">
|
|
53
|
+
<div class="w-[10px] h-px bg-gray-400" />
|
|
54
|
+
</div>
|
|
55
|
+
</a>
|
|
56
|
+
<ul v-if="item.children" class="mt-[5px] flex flex-col gap-[5px]">
|
|
57
|
+
<li v-for="child in item.children" :key="child.name">
|
|
30
58
|
<a
|
|
31
|
-
:class="
|
|
32
|
-
|
|
33
|
-
'hover:bg-gray-100': item?.href,
|
|
34
|
-
}"
|
|
35
|
-
:href="item?.href"
|
|
59
|
+
:class="[isCurrentRoute(child) ? 'bg-gray-100' : '', 'hover:bg-gray-100']"
|
|
60
|
+
:href="child.href"
|
|
36
61
|
class="h-[37px] sgroup flex items-center gap-x-2 rounded-md p-3 text-gray-800 leading-5 transition-all duration-300 ease-in-out"
|
|
37
|
-
@click.prevent="mainNavStore.goto(
|
|
62
|
+
@click.prevent="mainNavStore.goto(child.href)"
|
|
38
63
|
>
|
|
39
64
|
<font-awesome-icon
|
|
40
|
-
v-if="
|
|
41
|
-
:icon="Icons[
|
|
42
|
-
:class="[isCurrentRoute(
|
|
65
|
+
v-if="child.icon"
|
|
66
|
+
:icon="Icons[child.icon]"
|
|
67
|
+
:class="[isCurrentRoute(child) ? 'text-gray-400' : 'text-gray-400']"
|
|
43
68
|
class="size-4"
|
|
44
69
|
/>
|
|
45
|
-
<span
|
|
46
|
-
|
|
47
|
-
:class="{
|
|
48
|
-
'text-[11px]': item?.children,
|
|
49
|
-
'text-[13px]': !item?.children,
|
|
50
|
-
'font-semibold': item?.children,
|
|
51
|
-
'text-gray-500': item?.children,
|
|
52
|
-
}"
|
|
53
|
-
>
|
|
54
|
-
{{ item.name }}
|
|
70
|
+
<span v-if="!mainNavStore.state.collapsed" class="text-[13px]">
|
|
71
|
+
{{ child.name }}
|
|
55
72
|
</span>
|
|
56
|
-
<div v-else-if="item?.children" class="w-full flex justify-center">
|
|
57
|
-
<div class="w-[10px] h-px bg-gray-400" />
|
|
58
|
-
</div>
|
|
59
73
|
</a>
|
|
60
|
-
<ul v-if="item.children" class="mt-[5px] flex flex-col gap-[5px]">
|
|
61
|
-
<li v-for="child in item.children" :key="child.name">
|
|
62
|
-
<a
|
|
63
|
-
:class="[isCurrentRoute(child) ? 'bg-gray-100' : '', 'hover:bg-gray-100']"
|
|
64
|
-
:href="child.href"
|
|
65
|
-
class="h-[37px] sgroup flex items-center gap-x-2 rounded-md p-3 text-gray-800 leading-5 transition-all duration-300 ease-in-out"
|
|
66
|
-
@click.prevent="mainNavStore.goto(child.href)"
|
|
67
|
-
>
|
|
68
|
-
<font-awesome-icon
|
|
69
|
-
v-if="child.icon"
|
|
70
|
-
:icon="Icons[child.icon]"
|
|
71
|
-
:class="[isCurrentRoute(child) ? 'text-gray-400' : 'text-gray-400']"
|
|
72
|
-
class="size-4"
|
|
73
|
-
/>
|
|
74
|
-
<span v-if="!mainNavStore.state.collapsed" class="text-[13px]">
|
|
75
|
-
{{ child.name }}
|
|
76
|
-
</span>
|
|
77
|
-
</a>
|
|
78
|
-
</li>
|
|
79
|
-
</ul>
|
|
80
74
|
</li>
|
|
81
|
-
</
|
|
82
|
-
</
|
|
83
|
-
</
|
|
84
|
-
<li class="mt-auto">
|
|
85
|
-
<slot name="sidebar-footer" />
|
|
86
|
-
<a
|
|
87
|
-
class="font-medium grid place-content-center gap-x-3 rounded-md h-10 p-2.5 text-gray-800 text-[13px] hover:bg-gray-100 transition-all duration-300 ease-in-out"
|
|
88
|
-
href="#"
|
|
89
|
-
@click.prevent="mainNavStore.toggleCollapsed()"
|
|
90
|
-
>
|
|
91
|
-
<font-awesome-icon
|
|
92
|
-
:icon="
|
|
93
|
-
Icons[mainNavStore.state.collapsed ? 'farArrowRightToLine' : 'farArrowLeftToLine']
|
|
94
|
-
"
|
|
95
|
-
class="class-5"
|
|
96
|
-
/>
|
|
97
|
-
</a>
|
|
98
|
-
</li>
|
|
75
|
+
</ul>
|
|
76
|
+
</li>
|
|
77
|
+
</template>
|
|
99
78
|
</ul>
|
|
79
|
+
|
|
80
|
+
<!-- Footer items (fixed at bottom) -->
|
|
81
|
+
<div class="flex-shrink-0 mt-auto">
|
|
82
|
+
<ul v-if="footerItems.length" role="list" class="flex flex-col gap-[5px]">
|
|
83
|
+
<li v-for="child in footerItems" :key="child.name">
|
|
84
|
+
<a
|
|
85
|
+
:class="[isCurrentRoute(child) ? 'bg-gray-100' : '', 'hover:bg-gray-100']"
|
|
86
|
+
:href="child.href"
|
|
87
|
+
class="h-[37px] sgroup flex items-center gap-x-2 rounded-md p-3 text-gray-800 leading-5 transition-all duration-300 ease-in-out"
|
|
88
|
+
@click.prevent="mainNavStore.goto(child.href)"
|
|
89
|
+
>
|
|
90
|
+
<font-awesome-icon
|
|
91
|
+
v-if="child.icon"
|
|
92
|
+
:icon="Icons[child.icon]"
|
|
93
|
+
:class="[isCurrentRoute(child) ? 'text-gray-400' : 'text-gray-400']"
|
|
94
|
+
class="size-4"
|
|
95
|
+
/>
|
|
96
|
+
<span v-if="!mainNavStore.state.collapsed" class="text-[13px]">
|
|
97
|
+
{{ child.name }}
|
|
98
|
+
</span>
|
|
99
|
+
</a>
|
|
100
|
+
</li>
|
|
101
|
+
</ul>
|
|
102
|
+
<slot name="sidebar-footer" />
|
|
103
|
+
<a
|
|
104
|
+
class="font-medium grid place-content-center gap-x-3 rounded-md h-10 p-2.5 text-gray-800 text-[13px] hover:bg-gray-100 transition-all duration-300 ease-in-out"
|
|
105
|
+
href="#"
|
|
106
|
+
@click.prevent="mainNavStore.toggleCollapsed()"
|
|
107
|
+
>
|
|
108
|
+
<font-awesome-icon
|
|
109
|
+
:icon="
|
|
110
|
+
Icons[mainNavStore.state.collapsed ? 'farArrowRightToLine' : 'farArrowLeftToLine']
|
|
111
|
+
"
|
|
112
|
+
class="class-5"
|
|
113
|
+
/>
|
|
114
|
+
</a>
|
|
115
|
+
</div>
|
|
100
116
|
</nav>
|
|
101
117
|
</div>
|
|
102
118
|
</div>
|
|
@@ -213,6 +229,16 @@ const isCurrentRoute = (item) => {
|
|
|
213
229
|
return false
|
|
214
230
|
}
|
|
215
231
|
|
|
232
|
+
// Split menu items: scrollable items vs footer items (sticky at bottom)
|
|
233
|
+
const menuItems = computed(() =>
|
|
234
|
+
props.mainNavStore.state.menu.filter((item) => !item.footerSection),
|
|
235
|
+
)
|
|
236
|
+
const footerItems = computed(() =>
|
|
237
|
+
props.mainNavStore.state.menu
|
|
238
|
+
.filter((item) => item.footerSection)
|
|
239
|
+
.flatMap((item) => item.children || []),
|
|
240
|
+
)
|
|
241
|
+
|
|
216
242
|
const sparkBrandFilterStore = useSparkBrandFilterStore()
|
|
217
243
|
const sparkAppSelectorStore = useSparkAppSelectorStore()
|
|
218
244
|
|