|
1 | 1 | <script setup lang="ts"> |
2 | 2 | import { useHotkey, useStack } from '@vuetify/v0' |
3 | | - import { computed } from 'vue' |
| 3 | + import { computed, watch } from 'vue' |
4 | 4 | import { useOverlays } from './context' |
5 | 5 |
|
6 | 6 | const { overlays, activeCount, open, close, closeAll } = useOverlays() |
|
12 | 12 | { accent: 'text-amber-500', bg: 'bg-amber-500', badge: 'bg-amber-500/10 text-amber-500' }, |
13 | 13 | ] |
14 | 14 |
|
15 | | - // Create stack entries for each overlay |
16 | | - const stacks = overlays.map((overlay, index) => ({ |
17 | | - overlay, |
18 | | - color: colors[index % colors.length]!, |
19 | | - ...useStack( |
20 | | - overlay.isOpen, |
21 | | - () => close(overlay.id), |
22 | | - { blocking: overlay.blocking }, |
23 | | - ), |
24 | | - })) |
| 15 | + const stack = useStack() |
| 16 | +
|
| 17 | + // Register each overlay with the stack and sync selection with isOpen |
| 18 | + const tickets = overlays.map((overlay, index) => { |
| 19 | + const ticket = stack.register({ |
| 20 | + id: overlay.id, |
| 21 | + onDismiss: () => close(overlay.id), |
| 22 | + blocking: overlay.blocking, |
| 23 | + }) |
| 24 | +
|
| 25 | + // Sync selection state with isOpen |
| 26 | + watch(overlay.isOpen, isOpen => { |
| 27 | + if (isOpen) { |
| 28 | + ticket.select() |
| 29 | + } else { |
| 30 | + ticket.unselect() |
| 31 | + } |
| 32 | + }, { immediate: true }) |
| 33 | +
|
| 34 | + return { |
| 35 | + overlay, |
| 36 | + ticket, |
| 37 | + color: colors[index % colors.length]!, |
| 38 | + } |
| 39 | + }) |
25 | 40 |
|
26 | 41 | // Find next closed overlay to open from within an overlay |
27 | 42 | const nextClosed = computed(() => |
|
30 | 45 |
|
31 | 46 | // Escape key dismisses the topmost non-blocking overlay |
32 | 47 | useHotkey('Escape', () => { |
33 | | - const topStack = stacks.find(s => s.globalTop.value) |
34 | | - if (topStack && !topStack.overlay.blocking) { |
35 | | - close(topStack.overlay.id) |
| 48 | + const topTicket = tickets.find(t => t.ticket.globalTop.value) |
| 49 | + if (topTicket && !topTicket.overlay.blocking) { |
| 50 | + close(topTicket.overlay.id) |
36 | 51 | } |
37 | 52 | }) |
38 | 53 | </script> |
|
43 | 58 | <div class="flex flex-col gap-4"> |
44 | 59 | <div class="grid grid-cols-3 gap-3 max-w-md mx-auto"> |
45 | 60 | <button |
46 | | - v-for="(stack, index) in stacks" |
47 | | - :key="stack.overlay.id" |
| 61 | + v-for="({ overlay }, index) in tickets" |
| 62 | + :key="overlay.id" |
48 | 63 | class="px-4 py-2 text-sm font-medium rounded-md transition-colors text-center" |
49 | 64 | :class="[ |
50 | | - stack.overlay.isOpen.value |
| 65 | + overlay.isOpen.value |
51 | 66 | ? `${colors[index]!.bg} text-white border border-transparent` |
52 | 67 | : 'border border-divider hover:bg-surface-tint' |
53 | 68 | ]" |
54 | | - @click="stack.overlay.isOpen.value ? close(stack.overlay.id) : open(stack.overlay.id)" |
| 69 | + @click="overlay.isOpen.value ? close(overlay.id) : open(overlay.id)" |
55 | 70 | > |
56 | | - {{ stack.overlay.title }} |
| 71 | + {{ overlay.title }} |
57 | 72 | </button> |
58 | 73 | </div> |
59 | 74 |
|
|
62 | 77 | <span class="text-on-surface-variant">Stack:</span> |
63 | 78 | <div class="flex items-center gap-1"> |
64 | 79 | <template v-if="activeCount > 0"> |
65 | | - <template v-for="(stack, index) in stacks" :key="stack.overlay.id"> |
| 80 | + <template v-for="({ overlay, ticket }, index) in tickets" :key="overlay.id"> |
66 | 81 | <span |
67 | | - v-if="stack.overlay.isOpen.value" |
| 82 | + v-if="overlay.isOpen.value" |
68 | 83 | class="px-2 py-0.5 rounded text-xs font-mono" |
69 | 84 | :class="colors[index]!.badge" |
70 | 85 | > |
71 | | - {{ stack.zIndex.value }} |
| 86 | + {{ ticket.zIndex.value }} |
72 | 87 | </span> |
73 | 88 | </template> |
74 | 89 | </template> |
|
80 | 95 | <!-- Overlays --> |
81 | 96 | <Teleport to="body"> |
82 | 97 | <TransitionGroup name="modal"> |
83 | | - <template v-for="({ overlay, color, styles, globalTop, zIndex, id }, index) in stacks" :key="overlay.id"> |
| 98 | + <template v-for="({ overlay, ticket, color }, index) in tickets" :key="overlay.id"> |
84 | 99 | <div |
85 | 100 | v-if="overlay.isOpen.value" |
86 | | - :aria-describedby="`${id}-desc`" |
87 | | - :aria-labelledby="`${id}-title`" |
| 101 | + :aria-describedby="`${ticket.id}-desc`" |
| 102 | + :aria-labelledby="`${ticket.id}-title`" |
88 | 103 | aria-modal="true" |
89 | 104 | class="fixed inset-0 flex items-center justify-center pointer-events-none p-4" |
90 | 105 | role="dialog" |
91 | 106 | :style="{ |
92 | | - ...styles.value, |
| 107 | + zIndex: ticket.zIndex.value, |
93 | 108 | transform: `translate(${index * 16}px, ${index * 16}px)`, |
94 | 109 | }" |
95 | 110 | > |
96 | 111 | <div |
97 | 112 | class="m-auto rounded-xl bg-surface border border-divider max-w-md w-full pointer-events-auto transition-all duration-200" |
98 | | - :class="globalTop ? 'shadow-xl' : 'shadow-lg opacity-95'" |
| 113 | + :class="ticket.globalTop.value ? 'shadow-xl' : 'shadow-lg opacity-95'" |
99 | 114 | > |
100 | 115 | <!-- Header --> |
101 | 116 | <div class="px-4 py-3 border-b border-divider"> |
102 | 117 | <div class="flex items-center justify-between"> |
103 | | - <h3 :id="`${id}-title`" class="text-lg font-semibold text-on-surface"> |
| 118 | + <h3 :id="`${ticket.id}-title`" class="text-lg font-semibold text-on-surface"> |
104 | 119 | {{ overlay.title }} |
105 | 120 | </h3> |
106 | 121 | <div class="flex items-center gap-2"> |
107 | 122 | <span class="px-2 py-0.5 rounded text-xs font-mono" :class="color.badge"> |
108 | | - z:{{ zIndex.value }} |
| 123 | + z:{{ ticket.zIndex.value }} |
109 | 124 | </span> |
110 | 125 | <span |
111 | 126 | class="px-2 py-0.5 rounded text-xs" |
112 | | - :class="globalTop ? 'bg-success/10 text-success' : 'bg-surface-variant text-on-surface-variant'" |
| 127 | + :class="ticket.globalTop.value ? 'bg-success/10 text-success' : 'bg-surface-variant text-on-surface-variant'" |
113 | 128 | > |
114 | | - {{ globalTop ? 'top' : 'behind' }} |
| 129 | + {{ ticket.globalTop.value ? 'top' : 'behind' }} |
115 | 130 | </span> |
116 | 131 | </div> |
117 | 132 | </div> |
|
122 | 137 |
|
123 | 138 | <!-- Content --> |
124 | 139 | <div class="p-4 space-y-4"> |
125 | | - <p :id="`${id}-desc`" class="text-sm text-on-surface leading-relaxed"> |
| 140 | + <p :id="`${ticket.id}-desc`" class="text-sm text-on-surface leading-relaxed"> |
126 | 141 | This overlay demonstrates z-index stacking. Open multiple overlays |
127 | 142 | to see how they layer. The topmost overlay receives focus and |
128 | 143 | handles the escape key. |
|
0 commit comments