@wishbone-media/spark 1.3.1 → 1.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": "1.3.1",
3
+ "version": "1.4.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -57,6 +57,7 @@
57
57
  <script setup>
58
58
  import { computed, onUnmounted, ref } from 'vue'
59
59
  import { Dialog, DialogPanel, TransitionChild, TransitionRoot } from '@headlessui/vue'
60
+ import { sparkModalService } from '@/composables/sparkModalService'
60
61
 
61
62
  const panelRef = ref(null)
62
63
 
@@ -95,6 +96,20 @@ const widthClass = computed(() => {
95
96
  })
96
97
 
97
98
  const handleClose = () => {
99
+ // A spark modal stacked above overlay content (e.g. a confirm dialog) is a
100
+ // sibling Headless UI Dialog: its clicks and Escape register here as
101
+ // outside interactions. Ignore closes while one is open — the modal
102
+ // dismisses first, the overlay only on the next interaction.
103
+ //
104
+ // Two Headless UI behaviors this guard depends on (re-verify on upgrade):
105
+ // 1. Outside-click dismissal is dispatched on document `click` in the
106
+ // CAPTURE phase, so this runs before a modal button's bubble-phase
107
+ // handler calls hide() — isVisible is still true for the dismissing
108
+ // click itself.
109
+ // 2. Sibling Dialogs process document events in mount order; the overlay's
110
+ // Dialog mounts before any modal spawned from its content, so on a
111
+ // shared backdrop click this guard runs before the modal closes itself.
112
+ if (sparkModalService.state.isVisible) return
98
113
  props.overlayInstance.close()
99
114
  emit('close')
100
115
  }
@@ -67,6 +67,10 @@ export function useSubNavigation(options = {}) {
67
67
  // Reactive items list
68
68
  const items = ref(initialItems.map(normalizeItem))
69
69
 
70
+ // Local selection for route-less items without query sync (e.g. tabs inside
71
+ // an overlay panel, where writing to the host route's query is undesirable)
72
+ const manualActiveId = ref(null)
73
+
70
74
  /**
71
75
  * Normalize item to ensure consistent structure
72
76
  */
@@ -124,7 +128,11 @@ export function useSubNavigation(options = {}) {
124
128
  return itemObj.id === fallbackId
125
129
  }
126
130
 
127
- if (!itemObj.route) return false
131
+ // Local mode: route-less items without query sync track manual selection
132
+ if (!itemObj.route) {
133
+ const fallbackId = defaultId || visibleItems.value[0]?.id
134
+ return itemObj.id === (manualActiveId.value ?? fallbackId)
135
+ }
128
136
 
129
137
  // Handle route as string (route name) or object
130
138
  const itemRoute = typeof itemObj.route === 'string' ? { name: itemObj.route } : itemObj.route
@@ -226,7 +234,9 @@ export function useSubNavigation(options = {}) {
226
234
  return true
227
235
  }
228
236
 
229
- return false
237
+ // Local mode: remember the selection without touching the route
238
+ manualActiveId.value = itemObj.id
239
+ return true
230
240
  }
231
241
 
232
242
  /**
@@ -235,6 +245,12 @@ export function useSubNavigation(options = {}) {
235
245
  */
236
246
  function setItems(newItems) {
237
247
  items.value = newItems.map(normalizeItem)
248
+ // Drop a local-mode selection whose id no longer exists — a later items
249
+ // update reintroducing the id would otherwise silently re-activate it.
250
+ // Selections whose id survives the replacement are kept.
251
+ if (manualActiveId.value && !items.value.some((item) => item.id === manualActiveId.value)) {
252
+ manualActiveId.value = null
253
+ }
238
254
  }
239
255
 
240
256
  /**