|
| 1 | +<script setup lang="ts"> |
| 2 | +import type { DocksContext } from '@vitejs/devtools-kit/client' |
| 3 | +import type { CSSProperties } from 'vue' |
| 4 | +import { computed, h, markRaw, useTemplateRef } from 'vue' |
| 5 | +import { setEdgePositionDropdown, setFloatingTooltip, useEdgePositionDropdown } from '../state/floating-tooltip' |
| 6 | +import { PersistedDomViewsManager } from '../utils/PersistedDomViewsManager' |
| 7 | +import DockEntriesWithCategories from './DockEntriesWithCategories.vue' |
| 8 | +import DockPanelResizer from './DockPanelResizer.vue' |
| 9 | +import ViewEntry from './ViewEntry.vue' |
| 10 | +
|
| 11 | +// @unocss-include |
| 12 | +
|
| 13 | +const props = defineProps<{ |
| 14 | + context: DocksContext |
| 15 | +}>() |
| 16 | +
|
| 17 | +const context = props.context |
| 18 | +const store = context.panel.store |
| 19 | +
|
| 20 | +const viewsContainer = useTemplateRef<HTMLElement>('viewsContainer') |
| 21 | +const persistedDoms = markRaw(new PersistedDomViewsManager(viewsContainer)) |
| 22 | +
|
| 23 | +const isVertical = computed(() => store.position === 'left' || store.position === 'right') |
| 24 | +
|
| 25 | +const groupedEntries = computed(() => context.docks.groupedEntries) |
| 26 | +const selectedEntry = computed(() => context.docks.selected) |
| 27 | +
|
| 28 | +const positions = ['top', 'right', 'bottom', 'left'] as const |
| 29 | +const positionIcons: Record<string, string> = { |
| 30 | + top: 'i-ph-square-half-bottom-duotone rotate-180', |
| 31 | + right: 'i-ph-square-half-bottom-duotone rotate-270', |
| 32 | + bottom: 'i-ph-square-half-bottom-duotone', |
| 33 | + left: 'i-ph-square-half-bottom-duotone rotate-90', |
| 34 | +} |
| 35 | +const positionLabels: Record<string, string> = { |
| 36 | + top: 'Top', |
| 37 | + right: 'Right', |
| 38 | + bottom: 'Bottom', |
| 39 | + left: 'Left', |
| 40 | +} |
| 41 | +
|
| 42 | +function switchPosition(pos: 'top' | 'right' | 'bottom' | 'left') { |
| 43 | + store.position = pos |
| 44 | + setEdgePositionDropdown(null) |
| 45 | +} |
| 46 | +
|
| 47 | +const positionButton = useTemplateRef<HTMLButtonElement>('positionButton') |
| 48 | +const floatButton = useTemplateRef<HTMLButtonElement>('floatButton') |
| 49 | +const edgePositionDropdown = useEdgePositionDropdown() |
| 50 | +
|
| 51 | +function showTooltip(el: HTMLElement | null, text: string) { |
| 52 | + if (!el) |
| 53 | + return |
| 54 | + setFloatingTooltip({ content: text, el }) |
| 55 | +} |
| 56 | +function hideTooltip() { |
| 57 | + setFloatingTooltip(null) |
| 58 | +} |
| 59 | +
|
| 60 | +function togglePositionDropdown() { |
| 61 | + if (!positionButton.value) |
| 62 | + return |
| 63 | + if (edgePositionDropdown.value) { |
| 64 | + setEdgePositionDropdown(null) |
| 65 | + return |
| 66 | + } |
| 67 | + setEdgePositionDropdown({ |
| 68 | + el: positionButton.value, |
| 69 | + gap: 6, |
| 70 | + content: () => h('div', { class: 'flex flex-col gap-0.5 min-w-28' }, positions.map(pos => |
| 71 | + h('button', { |
| 72 | + class: [ |
| 73 | + 'flex items-center gap-2 w-full px2 py1 rounded hover:bg-active transition text-sm', |
| 74 | + store.position === pos ? 'text-primary bg-active' : 'op75 hover:op100', |
| 75 | + ], |
| 76 | + onClick: () => switchPosition(pos), |
| 77 | + }, [ |
| 78 | + h('div', { class: `${positionIcons[pos]} w-4.5 h-4.5` }), |
| 79 | + h('span', positionLabels[pos]), |
| 80 | + ]), |
| 81 | + )), |
| 82 | + }) |
| 83 | +} |
| 84 | +
|
| 85 | +function switchToFloat() { |
| 86 | + // Set sensible defaults for float position based on current edge position |
| 87 | + switch (store.position) { |
| 88 | + case 'bottom': |
| 89 | + store.left = 50 |
| 90 | + store.top = 100 |
| 91 | + break |
| 92 | + case 'top': |
| 93 | + store.left = 50 |
| 94 | + store.top = 0 |
| 95 | + break |
| 96 | + case 'left': |
| 97 | + store.left = 0 |
| 98 | + store.top = 50 |
| 99 | + break |
| 100 | + case 'right': |
| 101 | + store.left = 100 |
| 102 | + store.top = 50 |
| 103 | + break |
| 104 | + } |
| 105 | + store.mode = 'float' |
| 106 | +} |
| 107 | +
|
| 108 | +const panelStyle = computed<CSSProperties>(() => { |
| 109 | + const style: CSSProperties = { |
| 110 | + position: 'fixed', |
| 111 | + pointerEvents: context.panel.isResizing ? 'none' : 'auto', |
| 112 | + } |
| 113 | +
|
| 114 | + switch (store.position) { |
| 115 | + case 'bottom': |
| 116 | + style.left = '0' |
| 117 | + style.right = '0' |
| 118 | + style.bottom = '0' |
| 119 | + style.height = `${store.height}vh` |
| 120 | + style.minHeight = '150px' |
| 121 | + style.borderRadius = '8px 8px 0 0' |
| 122 | + break |
| 123 | + case 'top': |
| 124 | + style.left = '0' |
| 125 | + style.right = '0' |
| 126 | + style.top = '0' |
| 127 | + style.height = `${store.height}vh` |
| 128 | + style.minHeight = '150px' |
| 129 | + style.borderRadius = '0 0 8px 8px' |
| 130 | + break |
| 131 | + case 'left': |
| 132 | + style.top = '0' |
| 133 | + style.bottom = '0' |
| 134 | + style.left = '0' |
| 135 | + style.width = `${store.width}vw` |
| 136 | + style.minWidth = '200px' |
| 137 | + style.borderRadius = '0 8px 8px 0' |
| 138 | + break |
| 139 | + case 'right': |
| 140 | + style.top = '0' |
| 141 | + style.bottom = '0' |
| 142 | + style.right = '0' |
| 143 | + style.width = `${store.width}vw` |
| 144 | + style.minWidth = '200px' |
| 145 | + style.borderRadius = '8px 0 0 8px' |
| 146 | + break |
| 147 | + } |
| 148 | +
|
| 149 | + return style |
| 150 | +}) |
| 151 | +
|
| 152 | +const toolbarClass = computed(() => { |
| 153 | + return isVertical.value |
| 154 | + ? 'flex-col h-full w-[40px] border-r border-base' |
| 155 | + : 'flex-row w-full border-b border-base' |
| 156 | +}) |
| 157 | +
|
| 158 | +const contentClass = computed(() => { |
| 159 | + return isVertical.value |
| 160 | + ? 'flex-1 h-full overflow-hidden' |
| 161 | + : 'flex-1 w-full overflow-hidden' |
| 162 | +}) |
| 163 | +</script> |
| 164 | + |
| 165 | +<template> |
| 166 | + <div |
| 167 | + id="vite-devtools-edge-panel" |
| 168 | + class="bg-glass:75 border border-base color-base shadow overflow-hidden z-floating-anchor font-sans text-[15px] box-border" |
| 169 | + :class="`flex ${isVertical ? 'flex-row' : 'flex-col'}`" |
| 170 | + :style="panelStyle" |
| 171 | + > |
| 172 | + <DockPanelResizer :panel="context.panel" edge-mode /> |
| 173 | + |
| 174 | + <!-- Toolbar --> |
| 175 | + <div class="flex items-center shrink-0 select-none py1" :class="toolbarClass"> |
| 176 | + <div |
| 177 | + class="flex items-center flex-1 flex-wrap gap-0.5 px1" |
| 178 | + :class="isVertical ? 'flex-col py1' : 'flex-row px1'" |
| 179 | + > |
| 180 | + <DockEntriesWithCategories |
| 181 | + :context="context" |
| 182 | + :groups="groupedEntries" |
| 183 | + :is-vertical="isVertical" |
| 184 | + :rotate="false" |
| 185 | + :selected="selectedEntry" |
| 186 | + @select="(e) => context.docks.switchEntry(e?.id)" |
| 187 | + /> |
| 188 | + </div> |
| 189 | + |
| 190 | + <!-- Position dropdown & float toggle --> |
| 191 | + <div |
| 192 | + class="flex items-center gap-0.5 shrink-0 px1" |
| 193 | + :class="isVertical ? 'flex-col py1 border-t border-base' : 'flex-row px1 border-l border-base'" |
| 194 | + > |
| 195 | + <button |
| 196 | + ref="positionButton" |
| 197 | + class="p1.5 rounded hover:bg-active transition op75 hover:op100" |
| 198 | + @pointerenter="showTooltip(positionButton, 'Edge position')" |
| 199 | + @pointerleave="hideTooltip" |
| 200 | + @pointerdown="hideTooltip" |
| 201 | + @click="togglePositionDropdown" |
| 202 | + > |
| 203 | + <div :class="positionIcons[store.position]" class="w-4.5 h-4.5" /> |
| 204 | + </button> |
| 205 | + <button |
| 206 | + ref="floatButton" |
| 207 | + class="p1.5 rounded hover:bg-active transition op50 hover:op100" |
| 208 | + @pointerenter="showTooltip(floatButton, 'Float mode')" |
| 209 | + @pointerleave="hideTooltip" |
| 210 | + @pointerdown="hideTooltip" |
| 211 | + @click="switchToFloat" |
| 212 | + > |
| 213 | + <div class="i-ph-cards-three-duotone w-4.5 h-4.5" /> |
| 214 | + </button> |
| 215 | + </div> |
| 216 | + </div> |
| 217 | + |
| 218 | + <!-- Content --> |
| 219 | + <div class="relative" :class="contentClass"> |
| 220 | + <template v-if="selectedEntry && selectedEntry.type !== 'action'"> |
| 221 | + <ViewEntry |
| 222 | + v-if="viewsContainer" |
| 223 | + :key="selectedEntry.id" |
| 224 | + :context |
| 225 | + :entry="selectedEntry" |
| 226 | + :persisted-doms="persistedDoms" |
| 227 | + /> |
| 228 | + </template> |
| 229 | + <div |
| 230 | + v-else |
| 231 | + class="absolute inset-0 flex items-center justify-center op40 select-none" |
| 232 | + > |
| 233 | + <div class="flex flex-col items-center gap-2"> |
| 234 | + <div class="i-ph-layout-duotone w-8 h-8" /> |
| 235 | + <span class="text-sm">{{ selectedEntry ? 'Action executed' : 'Select a dock entry' }}</span> |
| 236 | + </div> |
| 237 | + </div> |
| 238 | + <div |
| 239 | + id="vite-devtools-views-container" |
| 240 | + ref="viewsContainer" |
| 241 | + class="absolute inset-0 pointer-events-none" |
| 242 | + /> |
| 243 | + </div> |
| 244 | + </div> |
| 245 | +</template> |
0 commit comments