Skip to content

Commit c67a3ba

Browse files
committed
docs: integrate useStack and Scrim across overlay components
- Install createStackPlugin in plugins/zero.ts for SSR-safe stack context - Unify scroll lock in default.vue via stack.isActive watch - Add Scrim with teleport=false to share stacking context with overlays - Register AppNav, AppSettingsSheet, DocsSearch with stack for z-index coordination - Add conditional stack registration to DocsAsk for mobile modal mode - Use isolated createStack() in StackProvider example to avoid global interference - Fix use-stack.md API docs to show correct register/select/unselect pattern - Fix scrim.md custom stack example to use provide() instead of non-existent prop - Simplify StackConsumer escape handler to stack.top.value?.dismiss()
1 parent 89bfbc7 commit c67a3ba

File tree

11 files changed

+112
-76
lines changed

11 files changed

+112
-76
lines changed

apps/docs/src/components/app/AppNav.vue

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script setup lang="ts">
22
// Framework
3-
import { IN_BROWSER, useClickOutside, useFeatures, useWindowEventListener } from '@vuetify/v0'
3+
import { IN_BROWSER, useClickOutside, useFeatures, useStack, useWindowEventListener } from '@vuetify/v0'
44
55
// Components
66
import { Discovery } from '@/components/discovery'
@@ -24,6 +24,7 @@
2424
2525
const settings = useSettings()
2626
const devmode = useFeatures().get('devmode')!
27+
const stack = useStack()
2728
2829
const app = useAppStore()
2930
const navigation = useNavigation()
@@ -94,6 +95,16 @@
9495
onMounted(updateMobile)
9596
useWindowEventListener('resize', updateMobile, { passive: true })
9697
98+
// Register with stack for z-index coordination (mobile only)
99+
const ticket = stack.register({
100+
onDismiss: () => navigation.close(),
101+
})
102+
103+
watch(() => navigation.isOpen.value && isMobile.value, isOpen => {
104+
if (isOpen) ticket.select()
105+
else ticket.unselect()
106+
}, { immediate: true })
107+
97108
// Scroll active link into view after nav sections are ready
98109
watch(scrollEnabled, enabled => {
99110
if (!enabled || !IN_BROWSER) return
@@ -137,14 +148,15 @@
137148
as="nav"
138149
class="flex flex-col fixed w-[230px] py-4 top-0 md:top-[72px] bottom-0"
139150
:class="[
140-
'flex flex-col fixed w-[230px] overflow-y-auto py-4 top-0 md:top-[72px] bottom-0 translate-x-[-100%] md:translate-x-0 border-r border-solid border-divider z-50',
151+
'flex flex-col fixed w-[230px] overflow-y-auto py-4 top-0 md:top-[72px] bottom-0 translate-x-[-100%] md:translate-x-0 border-r border-solid border-divider',
141152
settings.showBgGlass.value ? 'bg-glass-surface' : 'bg-surface',
142153
navigation.isOpen.value && '!translate-x-0',
143154
!settings.prefersReducedMotion.value && 'transition-transform duration-200 ease-in-out',
144155
]"
145156
:inert="!navigation.isOpen.value && isMobile ? true : undefined"
146157
:padding="-4"
147158
step="navigation"
159+
:style="{ zIndex: ticket.zIndex.value }"
148160
>
149161
<!-- Mobile header -->
150162
<header class="md:hidden shrink-0 px-4 py-3 -mt-4 mb-4 border-b border-divider flex items-center justify-between bg-surface">

apps/docs/src/components/app/AppSettingsSheet.vue

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script setup lang="ts">
22
// Framework
3-
import { useFeatures, useStorage } from '@vuetify/v0'
3+
import { useFeatures, useStack, useStorage } from '@vuetify/v0'
44
55
// Composables
66
import { useCustomThemes } from '@/composables/useCustomThemes'
@@ -14,6 +14,17 @@
1414
const storage = useStorage()
1515
const settings = useSettings()
1616
const levelFilter = useLevelFilterContext()
17+
const stack = useStack()
18+
19+
// Register with stack for z-index coordination
20+
const ticket = stack.register({
21+
onDismiss: () => settings.close(),
22+
})
23+
24+
watch(settings.isOpen, isOpen => {
25+
if (isOpen) ticket.select()
26+
else ticket.unselect()
27+
}, { immediate: true })
1728
1829
const hasChanges = computed(() => settings.hasChanges.value || levelFilter.hasChanges.value)
1930
@@ -57,8 +68,9 @@
5768
ref="sheet"
5869
aria-labelledby="settings-title"
5970
aria-modal="true"
60-
:class="['fixed inset-y-0 right-0 flex flex-col z-50 w-[320px] max-w-full shadow-xl outline-none', settings.showBgGlass.value ? 'bg-glass-surface' : 'bg-surface']"
71+
:class="['fixed inset-y-0 right-0 flex flex-col w-[320px] max-w-full shadow-xl outline-none', settings.showBgGlass.value ? 'bg-glass-surface' : 'bg-surface']"
6172
role="dialog"
73+
:style="{ zIndex: ticket.zIndex.value }"
6274
tabindex="-1"
6375
@keydown="onKeydown"
6476
>

apps/docs/src/components/docs/DocsAsk.vue

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
<script setup lang="ts">
22
// Framework
3-
import { useBreakpoints, useDocumentEventListener } from '@vuetify/v0'
3+
import { useBreakpoints, useDocumentEventListener, useStack, useToggleScope } from '@vuetify/v0'
44
55
// Components
66
import DocsAskInput from './DocsAskInput.vue'
77
import DocsAskPanel from './DocsAskPanel.vue'
88
99
// Composables
1010
import { useAsk } from '@/composables/useAsk'
11-
import { useScrollLock } from '@/composables/useScrollLock'
1211
import { useSettings } from '@/composables/useSettings'
1312
1413
// Utilities
@@ -20,16 +19,32 @@
2019
const isDesktop = computed(() => breakpoints.lgAndUp.value)
2120
const fullscreen = shallowRef(false)
2221
const settings = useSettings()
22+
const stack = useStack()
2323
24-
const fadeTransition = toRef(() => settings.prefersReducedMotion.value ? undefined : 'fade')
2524
const panelTransition = toRef(() => {
2625
if (settings.prefersReducedMotion.value) return undefined
2726
return isDesktop.value ? 'fade' : 'slide'
2827
})
2928
3029
const ask = useAsk()
3130
32-
useScrollLock(() => ask.isOpen.value && fullscreen.value && isDesktop.value)
31+
// Register with stack only in mobile modal mode (not desktop floating panel)
32+
const stackZIndex = shallowRef<number | undefined>(undefined)
33+
34+
useToggleScope(() => !isDesktop.value, () => {
35+
const ticket = stack.register({
36+
onDismiss: () => ask.close(),
37+
})
38+
39+
watch(ask.isOpen, isOpen => {
40+
if (isOpen) ticket.select()
41+
else ticket.unselect()
42+
}, { immediate: true })
43+
44+
watch(ticket.zIndex, z => {
45+
stackZIndex.value = z
46+
}, { immediate: true })
47+
})
3348
3449
const panelRef = useTemplateRef<InstanceType<typeof DocsAskPanel>>('panel')
3550
const triggerRef = shallowRef<HTMLElement | null>(null)
@@ -91,15 +106,6 @@
91106
@submit="onSubmit"
92107
/>
93108

94-
<!-- Backdrop (mobile only) -->
95-
<Transition :name="fadeTransition">
96-
<div
97-
v-if="ask.isOpen.value && !isDesktop"
98-
class="fixed inset-0 bg-black/30 z-40"
99-
@click="ask.close"
100-
/>
101-
</Transition>
102-
103109
<!-- Chat panel -->
104110
<Transition :name="panelTransition">
105111
<DocsAskPanel
@@ -109,6 +115,7 @@
109115
:error="ask.error.value"
110116
:is-loading="ask.isLoading.value"
111117
:messages="ask.messages.value"
118+
:z-index="stackZIndex"
112119
@clear="ask.clear"
113120
@close="ask.close"
114121
@stop="ask.stop"

apps/docs/src/components/docs/DocsAskPanel.vue

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
isLoading: boolean
2525
error: string | null
2626
fullscreen?: boolean
27+
zIndex?: number
2728
}>()
2829
2930
const emit = defineEmits<{
@@ -146,7 +147,7 @@
146147
aria-labelledby="ask-title"
147148
:aria-modal="!isDesktop"
148149
:class="[
149-
'flex flex-col z-50',
150+
'flex flex-col',
150151
settings.showBgGlass.value ? 'bg-glass-surface' : 'bg-surface',
151152
isDesktop && fullscreen
152153
? 'fixed inset-4 rounded-lg border border-divider shadow-lg'
@@ -155,6 +156,7 @@
155156
: 'fixed inset-0',
156157
]"
157158
:role="isDesktop ? 'complementary' : 'dialog'"
159+
:style="{ zIndex: props.zIndex ?? 50 }"
158160
>
159161
<!-- Header -->
160162
<Discovery.Activator

apps/docs/src/components/docs/DocsSearch.vue

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script setup lang="ts">
22
// Framework
3-
import { useDocumentEventListener } from '@vuetify/v0'
3+
import { useDocumentEventListener, useStack } from '@vuetify/v0'
44
55
// Components
66
import { Discovery } from '@/components/discovery'
@@ -20,6 +20,17 @@
2020
2121
const search = useSearch()
2222
const discovery = useDiscovery()
23+
const stack = useStack()
24+
25+
// Register with stack for z-index coordination
26+
const ticket = stack.register({
27+
onDismiss: () => search.close(),
28+
})
29+
30+
watch(search.isOpen, isOpen => {
31+
if (isOpen) ticket.select()
32+
else ticket.unselect()
33+
}, { immediate: true })
2334
2435
const router = useRouter()
2536
const inputRef = useTemplateRef<HTMLInputElement>('input')
@@ -138,21 +149,14 @@
138149
</script>
139150

140151
<template>
141-
<Transition :name="transition">
142-
<div
143-
v-if="search.isOpen.value"
144-
class="fixed inset-0 bg-black/30 z-50"
145-
@click="search.close"
146-
/>
147-
</Transition>
148-
149152
<Transition :name="transition">
150153
<div
151154
v-if="search.isOpen.value"
152155
aria-label="Search Documentation"
153156
aria-modal="true"
154-
class="fixed inset-x-0 top-[20%] mx-auto w-full max-w-2xl z-50 px-4"
157+
class="fixed inset-x-0 top-[20%] mx-auto w-full max-w-2xl px-4"
155158
role="dialog"
159+
:style="{ zIndex: ticket.zIndex.value }"
156160
>
157161
<div :class="['rounded-lg shadow-xl border border-divider overflow-hidden', settings.showBgGlass.value ? 'bg-glass-surface' : 'bg-surface']">
158162
<Discovery.Activator

apps/docs/src/examples/composables/use-stack/StackConsumer.vue

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,7 @@
4545
4646
// Escape key dismisses the topmost non-blocking overlay
4747
useHotkey('Escape', () => {
48-
const topTicket = tickets.find(t => t.ticket.globalTop.value)
49-
if (topTicket && !topTicket.overlay.blocking) {
50-
close(topTicket.overlay.id)
51-
}
48+
stack.top.value?.dismiss()
5249
})
5350
</script>
5451

apps/docs/src/examples/composables/use-stack/StackProvider.vue

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@
22
// Note: For SSR applications, install the plugin in main.ts:
33
// import { createStackPlugin } from '@vuetify/v0'
44
// app.use(createStackPlugin())
5-
import { Scrim, useStack } from '@vuetify/v0'
6-
import { computed, onScopeDispose, shallowRef, watch } from 'vue'
5+
import { createStack, Scrim } from '@vuetify/v0'
6+
import { computed, onScopeDispose, provide, shallowRef, watch } from 'vue'
77
import { provideOverlays } from './context'
88
import type { Overlay } from './context'
99
10-
const stack = useStack()
10+
// Create isolated stack for this example (doesn't interfere with app's global stack)
11+
const stack = createStack()
12+
provide('v0:stack', stack)
1113
1214
// Block body scroll when overlays are active
1315
watch(() => stack.isActive.value, active => {

apps/docs/src/layouts/default.vue

Lines changed: 14 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
11
<script setup lang="ts">
22
// Framework
3-
import { useBreakpoints } from '@vuetify/v0'
3+
import { IN_BROWSER, Scrim, useBreakpoints, useStack } from '@vuetify/v0'
44
55
// Composables
66
import { useAsk } from '@/composables/useAsk'
77
import { useDiscovery } from '@/composables/useDiscovery'
88
import { createLevelFilter } from '@/composables/useLevelFilter'
99
import { createNavConfig } from '@/composables/useNavConfig'
10-
import { useNavigation } from '@/composables/useNavigation'
11-
import { useScrollLock } from '@/composables/useScrollLock'
1210
import { useSearch } from '@/composables/useSearch'
1311
import { useSettings } from '@/composables/useSettings'
1412
1513
// Utilities
16-
import { computed, defineAsyncComponent, toRef, watch } from 'vue'
14+
import { computed, defineAsyncComponent, onScopeDispose, toRef, watch } from 'vue'
1715
1816
// Stores
1917
import { useAppStore } from '@/stores/app'
@@ -33,16 +31,15 @@
3331
const breakpoints = useBreakpoints()
3432
const discovery = useDiscovery()
3533
const ask = useAsk()
36-
const navigation = useNavigation()
3734
const search = useSearch()
3835
const settings = useSettings()
36+
const stack = useStack()
3937
4038
// Force reduced motion during tours so elements don't animate
4139
watch(() => discovery.isActive.value, active => {
4240
settings.forceReducedMotion.value = active
4341
})
4442
45-
const fadeTransition = toRef(() => settings.prefersReducedMotion.value ? undefined : 'fade')
4643
const slideTransition = toRef(() => settings.prefersReducedMotion.value ? undefined : 'slide')
4744
4845
const isModalOpen = computed(() => {
@@ -52,10 +49,16 @@
5249
return false
5350
})
5451
55-
const isMobileNavOpen = toRef(() => navigation.isOpen.value && !breakpoints.mdAndUp.value)
52+
// Unified scroll lock via stack
53+
watch(() => stack.isActive.value, active => {
54+
if (!IN_BROWSER) return
55+
document.body.style.overflow = active ? 'hidden' : ''
56+
}, { immediate: true })
5657
57-
useScrollLock(settings.isOpen)
58-
useScrollLock(isMobileNavOpen)
58+
onScopeDispose(() => {
59+
if (!IN_BROWSER) return
60+
document.body.style.overflow = ''
61+
})
5962
</script>
6063

6164
<template>
@@ -79,23 +82,8 @@
7982

8083
<DocsSearch />
8184

82-
<!-- Mobile nav backdrop -->
83-
<Transition :name="fadeTransition">
84-
<div
85-
v-if="isMobileNavOpen"
86-
class="fixed inset-0 bg-black/30 z-9"
87-
@click="navigation.close()"
88-
/>
89-
</Transition>
90-
91-
<!-- Settings backdrop -->
92-
<Transition :name="fadeTransition">
93-
<div
94-
v-if="settings.isOpen.value"
95-
class="fixed inset-0 bg-black/30 z-40"
96-
@click="settings.close"
97-
/>
98-
</Transition>
85+
<!-- Unified scrim for all overlays (teleport disabled to share stacking context) -->
86+
<Scrim class="fixed inset-0 bg-black/30 transition-opacity" :teleport="false" />
9987

10088
<!-- Settings sheet -->
10189
<Transition :name="slideTransition">

apps/docs/src/pages/components/providers/scrim.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -121,17 +121,20 @@ By default, Scrim teleports to `body`. Disable teleport for inline rendering:
121121

122122
## Custom Stack Context
123123

124-
For isolated overlay systems, provide a custom stack context:
124+
For isolated overlay systems, create a custom stack and provide it via Vue's injection system:
125125

126126
```vue
127127
<script setup lang="ts">
128-
import { Scrim, createStackContext } from '@vuetify/v0'
128+
import { provide } from 'vue'
129+
import { createStack, Scrim } from '@vuetify/v0'
129130
130-
const customStack = createStackContext()
131+
// Create isolated stack (doesn't interfere with global stack)
132+
const stack = createStack()
133+
provide('v0:stack', stack)
131134
</script>
132135
133136
<template>
134-
<Scrim :stack="customStack" class="fixed inset-0 bg-black/50" />
137+
<Scrim class="fixed inset-0 bg-black/50" />
135138
</template>
136139
```
137140

0 commit comments

Comments
 (0)