Skip to content

Commit 2d8855d

Browse files
committed
feat(core): overflow docks panel
1 parent 0508e09 commit 2d8855d

File tree

8 files changed

+208
-92
lines changed

8 files changed

+208
-92
lines changed

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/Dock.vue

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { useEventListener, useScreenSafeArea } from '@vueuse/core'
44
import { computed, onMounted, reactive, ref, useTemplateRef, watchEffect } from 'vue'
55
import { BUILTIN_ENTRY_CLIENT_AUTH_NOTICE } from '../constants'
66
import DockEntriesWithCategories from './DockEntriesWithCategories.vue'
7+
import DockOverflowButton from './DockOverflowButton.vue'
78
import BracketLeft from './icons/BracketLeft.vue'
89
import BracketRight from './icons/BracketRight.vue'
910
import VitePlusCore from './icons/VitePlusCore.vue'
@@ -316,7 +317,18 @@ onMounted(() => {
316317
:is-vertical="context.panel.isVertical"
317318
:selected="context.docks.selected"
318319
@select="(e) => context.docks.switchEntry(e?.id)"
319-
/>
320+
>
321+
<template #overflow="{ entries }">
322+
<div class="border-base m1 h-20px w-px border-r-1.5" />
323+
<DockOverflowButton
324+
:context="context"
325+
:is-vertical="context.panel.isVertical"
326+
:entries="entries.flatMap(([_, entries]) => entries)"
327+
:selected="context.docks.selected"
328+
@select="(e) => context.docks.switchEntry(e?.id)"
329+
/>
330+
</template>
331+
</DockEntriesWithCategories>
320332
</div>
321333
</div>
322334
</div>

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

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import type { DocksContext } from '@vitejs/devtools-kit/client'
44
import { computed, toRefs } from 'vue'
55
import { DEFAULT_CATEGORIES_ORDER } from '../constants'
66
import DockEntries from './DockEntries.vue'
7-
import DockEntry from './DockEntry.vue'
87
98
const props = defineProps<{
109
context: DocksContext
@@ -75,13 +74,6 @@ const groups = computed(() => {
7574
overflow,
7675
}
7776
})
78-
79-
const overflowBadge = computed(() => {
80-
const count = groups.value.overflow.reduce((acc, [_, entries]) => acc + entries.length, 0)
81-
if (count > 9)
82-
return '9+'
83-
return count.toString()
84-
})
8577
</script>
8678

8779
<template>
@@ -96,20 +88,6 @@ const overflowBadge = computed(() => {
9688
:selected="selected"
9789
@select="(e) => emit('select', e)"
9890
/>
99-
<slot v-if="groups.overflow.length > 0" name="overflow" :entries="groups.overflow">
100-
<div class="border-base m1 h-20px w-px border-r-1.5" />
101-
<DockEntry
102-
:dock="{
103-
id: 'overflow',
104-
title: 'Overflow',
105-
icon: 'ph:dots-three-circle-duotone',
106-
}"
107-
:badge="overflowBadge"
108-
:is-vertical="isVertical"
109-
:is-selected="false"
110-
:is-dimmed="false"
111-
/>
112-
<!-- TODO: panel -->
113-
</slot>
91+
<slot v-if="groups.overflow.length > 0" name="overflow" :entries="groups.overflow" />
11492
</template>
11593
</template>
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<script setup lang="ts">
2+
import type { DevToolsDockEntry } from '@vitejs/devtools-kit'
3+
import type { DocksContext } from '@vitejs/devtools-kit/client'
4+
import { watchDebounced } from '@vueuse/core'
5+
import { computed, h, ref, useTemplateRef } from 'vue'
6+
import { setDocksOverflowPanel, useDocksOverflowPanel } from '../state/floating-tooltip'
7+
import DockEntriesWithCategories from './DockEntriesWithCategories.vue'
8+
import DockEntry from './DockEntry.vue'
9+
10+
const props = defineProps<{
11+
context: DocksContext
12+
isVertical: boolean
13+
entries: DevToolsDockEntry[]
14+
selected: DevToolsDockEntry | null
15+
}>()
16+
17+
const emit = defineEmits<{
18+
(e: 'select', entry: DevToolsDockEntry): void
19+
}>()
20+
21+
const overflowButton = useTemplateRef<HTMLButtonElement>('overflowButton')
22+
const overflowBadge = computed(() => {
23+
const count = props.entries.length
24+
if (count > 9)
25+
return '9+'
26+
return count.toString()
27+
})
28+
29+
const isOverflowPanelVisible = ref(false)
30+
const docksOverflowPanel = useDocksOverflowPanel()
31+
32+
function showOverflowPanel() {
33+
if (!overflowButton.value)
34+
return
35+
isOverflowPanelVisible.value = true
36+
setDocksOverflowPanel({
37+
content: () => h('div', {
38+
class: 'flex gap-0 flex-wrap max-w-200px',
39+
}, [
40+
h(DockEntriesWithCategories, {
41+
context: props.context,
42+
entries: props.entries,
43+
isVertical: false,
44+
selected: props.selected,
45+
onSelect: e => emit('select', e),
46+
}),
47+
]),
48+
el: overflowButton.value,
49+
})
50+
}
51+
52+
// We have an internal state and delay the update to the DOM to conflicts with the "onClickOutside" logic
53+
watchDebounced(
54+
() => docksOverflowPanel.value,
55+
(value) => {
56+
isOverflowPanelVisible.value = !!value
57+
},
58+
{ debounce: 1000 },
59+
)
60+
61+
function toggleOverflowPanel() {
62+
if (isOverflowPanelVisible.value)
63+
hideOverflowPanel()
64+
else
65+
showOverflowPanel()
66+
}
67+
68+
function hideOverflowPanel() {
69+
isOverflowPanelVisible.value = false
70+
setDocksOverflowPanel(null)
71+
}
72+
</script>
73+
74+
<template>
75+
<div ref="overflowButton">
76+
<DockEntry
77+
:dock="{
78+
id: 'overflow',
79+
title: 'Overflow',
80+
icon: 'ph:dots-three-circle-duotone',
81+
}"
82+
class="overflow-button"
83+
:tooltip="false"
84+
:badge="overflowBadge"
85+
:is-vertical="isVertical"
86+
:is-selected="false"
87+
:is-dimmed="false"
88+
@click="toggleOverflowPanel"
89+
/>
90+
</div>
91+
</template>
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
<script setup lang="ts">
2-
import { useFloatingTooltip } from '../state/floating-tooltip'
2+
import { setDocksOverflowPanel, useDocksOverflowPanel, useFloatingTooltip } from '../state/floating-tooltip'
33
import FloatingPopover from './FloatingPopover'
44
55
const tooltip = useFloatingTooltip()
6+
const docksOverflowPanel = useDocksOverflowPanel()
67
</script>
78

89
<template>
10+
<FloatingPopover :item="docksOverflowPanel" @dismiss="() => setDocksOverflowPanel(null)" />
911
<FloatingPopover :item="tooltip" />
1012
</template>
Lines changed: 88 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,119 +1,143 @@
1-
import type { PropType } from 'vue'
1+
import type { PropType, VNode } from 'vue'
22
import type { FloatingPopoverProps } from '../state/floating-tooltip'
3-
import { useDebounceFn, useElementBounding } from '@vueuse/core'
4-
import { computed, defineComponent, h, reactive, ref, watch } from 'vue'
3+
import { onClickOutside, useDebounceFn } from '@vueuse/core'
4+
import { defineComponent, h, ref, useTemplateRef, watch } from 'vue'
55

66
// @unocss-include
77

88
const DETECT_MARGIN = 100
9-
const GAP = 10
9+
const DEFAULT_GAP = 10
1010

11-
const FloatingTooltipComponent = defineComponent({
12-
name: 'FloatingTooltip',
11+
const FloatingPopoverComponent = defineComponent({
12+
name: 'FloatingPopover',
1313
props: {
1414
item: {
1515
type: Object as PropType<FloatingPopoverProps | null | undefined>,
1616
required: false,
1717
},
18+
dismissOnClickOutside: {
19+
type: Boolean,
20+
default: true,
21+
},
1822
},
19-
setup(props) {
23+
emits: ['dismiss'],
24+
setup(props, { emit }) {
25+
const panel = useTemplateRef<HTMLDivElement>('panel')
2026
const el = ref(props.item?.el)
21-
const rect = reactive(useElementBounding(el))
27+
const renderCounter = ref(0)
28+
29+
const clearThrottled = useDebounceFn(() => {
30+
if (props.item?.el == null)
31+
el.value = undefined
32+
}, 800)
33+
34+
if (props.dismissOnClickOutside) {
35+
onClickOutside(panel, () => {
36+
emit('dismiss')
37+
})
38+
}
39+
40+
watch(
41+
() => props.item,
42+
(value) => {
43+
if (value) {
44+
if (el.value !== value.el)
45+
el.value = value.el
46+
else
47+
renderCounter.value++
48+
}
49+
else {
50+
clearThrottled()
51+
}
52+
},
53+
)
54+
55+
let previousContent: VNode | undefined
56+
57+
return () => {
58+
// Force re-render to update the position
59+
// eslint-disable-next-line ts/no-unused-expressions
60+
renderCounter.value
61+
62+
if (!el.value)
63+
return null
64+
65+
const rect = el.value.getBoundingClientRect()
66+
67+
// guess alignment of the tooltip based on viewport position
68+
let align: 'bottom' | 'left' | 'right' | 'top' = 'bottom'
2269

23-
// guess alignment of the tooltip based on viewport position
24-
const align = computed<'bottom' | 'left' | 'right' | 'top'>(() => {
25-
if (!props.item?.el)
26-
return 'bottom'
2770
const vw = window.innerWidth
2871
const vh = window.innerHeight
2972
if (rect.left < DETECT_MARGIN)
30-
return 'right'
31-
if (rect.left + rect.width > vw - DETECT_MARGIN)
32-
return 'left'
33-
if (rect.top < DETECT_MARGIN)
34-
return 'bottom'
35-
if (rect.top + rect.height > vh - DETECT_MARGIN)
36-
return 'top'
37-
return 'bottom'
38-
})
39-
40-
const style = computed(() => {
41-
if (!props.item?.el)
42-
return {}
43-
switch (align.value) {
73+
align = 'right'
74+
else if (rect.left + rect.width > vw - DETECT_MARGIN)
75+
align = 'left'
76+
else if (rect.top < DETECT_MARGIN)
77+
align = 'bottom'
78+
else if (rect.top + rect.height > vh - DETECT_MARGIN)
79+
align = 'top'
80+
81+
let style: Record<string, string> = {}
82+
const gap = props.item?.gap ?? DEFAULT_GAP
83+
84+
switch (align) {
4485
case 'bottom': {
45-
return {
86+
style = {
4687
left: `${rect.left + rect.width / 2}px`,
47-
top: `${rect.top + rect.height + GAP}px`,
88+
top: `${rect.top + rect.height + gap}px`,
4889
transform: 'translateX(-50%)',
4990
}
91+
break
5092
}
5193
case 'top': {
52-
return {
94+
style = {
5395
left: `${rect.left + rect.width / 2}px`,
54-
bottom: `${window.innerHeight - rect.top + GAP}px`,
96+
bottom: `${vh - rect.top + gap}px`,
5597
transform: 'translateX(-50%)',
5698
}
99+
break
57100
}
58101
case 'left': {
59-
return {
60-
right: `${window.innerWidth - rect.left + GAP}px`,
102+
style = {
103+
right: `${vw - rect.left + gap}px`,
61104
top: `${rect.top + rect.height / 2}px`,
62105
transform: 'translateY(-50%)',
63106
}
107+
break
64108
}
65109
case 'right': {
66-
return {
67-
left: `${rect.left + rect.width + GAP}px`,
110+
style = {
111+
left: `${rect.left + rect.width + gap}px`,
68112
top: `${rect.top + rect.height / 2}px`,
69113
transform: 'translateY(-50%)',
70114
}
71-
}
72-
default: {
73-
throw new Error('Unreachable')
115+
break
74116
}
75117
}
76-
})
77-
78-
const clearThrottled = useDebounceFn(() => {
79-
if (props.item?.el == null)
80-
el.value = undefined
81-
}, 800)
82-
83-
watch(
84-
() => props.item,
85-
(value) => {
86-
if (value) {
87-
if (el.value !== value.el)
88-
el.value = value.el
89-
else
90-
rect.update()
91-
}
92-
else {
93-
clearThrottled()
94-
}
95-
},
96-
)
97118

98-
return () => {
99-
if (!props.item?.content)
100-
return null
119+
const content = (
120+
typeof props.item?.content === 'string'
121+
? h('span', props.item?.content)
122+
: props.item?.content()
123+
) ?? previousContent
101124

102-
const content = typeof props.item.content === 'string' ? h('span', props.item.content) : props.item.content()
125+
previousContent = content
103126

104127
return h(
105128
'div',
106129
{
130+
ref: 'panel',
107131
class: [
108132
'fixed z-floating-tooltip text-xs transition-all duration-300 w-max bg-glass border border-base rounded px2 p1',
109133
props.item ? 'op100' : 'op0 pointer-events-none',
110134
],
111-
style: style.value,
135+
style,
112136
},
113137
content,
114138
)
115139
}
116140
},
117141
})
118142

119-
export default FloatingTooltipComponent
143+
export default FloatingPopoverComponent

0 commit comments

Comments
 (0)