Skip to content

Commit 46019a6

Browse files
authored
feat: add edge mode for floating docks (#224)
1 parent 6e3de6b commit 46019a6

File tree

13 files changed

+444
-78
lines changed

13 files changed

+444
-78
lines changed

packages/core/src/client/inject/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export async function init(): Promise<void> {
1515
const state = useLocalStorage<DockPanelStorage>(
1616
'vite-devtools-dock-state',
1717
{
18+
mode: 'float',
1819
width: 80,
1920
height: 80,
2021
top: 0,

packages/core/src/client/webcomponents/.generated/css.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

packages/core/src/client/webcomponents/components/DockContextMenu.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ export function openDockContextMenu(options: {
6565
gap?: number
6666
}) {
6767
const { context, entry, el, gap = 6 } = options
68+
const isEdgeMode = context.panel.store.mode === 'edge'
6869
const items: DockMenuItem[] = [
6970
{
7071
label: 'Hide',
@@ -78,6 +79,40 @@ export function openDockContextMenu(options: {
7879
action: () => refreshDock(context, entry),
7980
visible: canRefresh(entry),
8081
},
82+
{
83+
label: isEdgeMode ? 'Float Mode' : 'Edge Mode',
84+
icon: isEdgeMode ? 'i-ph-arrows-out-duotone' : 'i-ph-square-half-bottom-duotone',
85+
action: () => {
86+
if (isEdgeMode) {
87+
// Reset float position defaults based on current edge position
88+
const store = context.panel.store
89+
switch (store.position) {
90+
case 'bottom':
91+
store.left = 50
92+
store.top = 100
93+
break
94+
case 'top':
95+
store.left = 50
96+
store.top = 0
97+
break
98+
case 'left':
99+
store.left = 0
100+
store.top = 50
101+
break
102+
case 'right':
103+
store.left = 100
104+
store.top = 50
105+
break
106+
}
107+
store.mode = 'float'
108+
}
109+
else {
110+
context.panel.store.mode = 'edge'
111+
}
112+
setDockContextMenu(null)
113+
},
114+
visible: context.clientType === 'embedded',
115+
},
81116
{
82117
label: 'Popup',
83118
icon: 'i-ph-arrow-square-out-duotone',
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
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>

packages/core/src/client/webcomponents/components/DockEmbedded.vue

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { onUnmounted } from 'vue'
55
import { sharedStateToRef } from '../state/docks'
66
import { closeDockPopup, useIsDockPopupOpen } from '../state/popup'
77
import Dock from './Dock.vue'
8+
import DockEdge from './DockEdge.vue'
89
import DockPanel from './DockPanel.vue'
910
import FloatingElements from './FloatingElements.vue'
1011
import ToastOverlay from './ToastOverlay.vue'
@@ -42,16 +43,23 @@ onUnmounted(() => {
4243
</script>
4344

4445
<template>
45-
<Dock v-if="!isDockPopupOpen" :context>
46-
<template #default="{ dockEl, panelMargins, selected }">
47-
<DockPanel
48-
:context
49-
:selected
50-
:dock-el="dockEl!"
51-
:panel-margins="panelMargins"
52-
/>
46+
<template v-if="!isDockPopupOpen">
47+
<template v-if="context.panel.store.mode === 'edge'">
48+
<DockEdge :context />
5349
</template>
54-
</Dock>
55-
<FloatingElements v-if="!isDockPopupOpen" />
50+
<template v-else>
51+
<Dock :context>
52+
<template #default="{ dockEl, panelMargins, selected }">
53+
<DockPanel
54+
:context
55+
:selected
56+
:dock-el="dockEl!"
57+
:panel-margins="panelMargins"
58+
/>
59+
</template>
60+
</Dock>
61+
</template>
62+
<FloatingElements />
63+
</template>
5664
<ToastOverlay :context />
5765
</template>

packages/core/src/client/webcomponents/components/DockEntriesWithCategories.vue

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ import type { DocksContext } from '@vitejs/devtools-kit/client'
44
import type { DevToolsDockEntriesGrouped } from '../state/dock-settings'
55
import DockEntries from './DockEntries.vue'
66
7-
defineProps<{
7+
withDefaults(defineProps<{
88
context: DocksContext
99
groups: DevToolsDockEntriesGrouped
1010
selected: DevToolsDockEntry | null
1111
isVertical: boolean
12-
}>()
12+
rotate?: boolean
13+
}>(), {
14+
rotate: true,
15+
})
1316
1417
const emit = defineEmits<{
1518
(e: 'select', entry: DevToolsDockEntry): void
@@ -19,12 +22,13 @@ const emit = defineEmits<{
1922
<template>
2023
<template v-for="[category, entries], idx of groups" :key="category">
2124
<slot v-if="idx > 0" name="separator" :category="category" :index="idx" :is-vertical="isVertical">
22-
<div class="border-base m1 h-20px w-px border-r-1.5" />
25+
<div v-if="isVertical" class="border-base m1 w-20px h-px border-b-1.5" />
26+
<div v-else class="border-base m1 h-20px w-px border-r-1.5" />
2327
</slot>
2428
<DockEntries
2529
:context="context"
2630
:entries="entries"
27-
:is-vertical="isVertical"
31+
:is-vertical="isVertical && rotate"
2832
:selected="selected"
2933
@select="(e) => emit('select', e)"
3034
/>

0 commit comments

Comments
 (0)