Skip to content

Commit 23b0d10

Browse files
molinlamoninlaJustineo
authored
feat: add resize functionality for panel (#34)
Co-authored-by: moninla <[email protected]> Co-authored-by: Justineo <[email protected]>
1 parent 3714748 commit 23b0d10

File tree

5 files changed

+169
-22
lines changed

5 files changed

+169
-22
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## 0.14.0
44

55
- Added MCP server support to let agents/IDEs pull code, structure, and screenshots from your current Figma selection.
6+
- The inspect panel can be resized horizontally by dragging either its left or right edge. (Suggested and implemented by @molinla at [#34](https://github.com/ecomfe/tempad-dev/pull/34))
67

78
## 0.13.1
89

components/Panel.vue

Lines changed: 154 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { useDraggable, useWindowSize, watchDebounced } from '@vueuse/core'
2+
import { useDraggable, useEventListener, useWindowSize, watchDebounced } from '@vueuse/core'
33
44
import { useScrollbar } from '@/composables/scrollbar'
55
import { ui } from '@/ui/figma'
@@ -21,7 +21,7 @@ useScrollbar(main, {
2121
})
2222
2323
const position = options.value.panelPosition
24-
const { x, y } = useDraggable(panel, {
24+
const { x, y, isDragging } = useDraggable(panel, {
2525
initialValue: {
2626
x: position ? position.left : 0,
2727
y: position ? position.top : 0
@@ -31,16 +31,107 @@ const { x, y } = useDraggable(panel, {
3131
3232
const { width: windowWidth, height: windowHeight } = useWindowSize()
3333
34+
const panelWidth = ref(position?.width ?? ui.tempadPanelWidth)
35+
36+
let resizeState: {
37+
direction: 'left' | 'right'
38+
startX: number
39+
startWidth: number
40+
target: HTMLElement
41+
pointerId: number
42+
} | null = null
43+
44+
function clampWidth(value: number): number {
45+
return Math.max(ui.tempadPanelWidth, Math.min(ui.tempadPanelMaxWidth, value))
46+
}
47+
48+
function startResize(e: PointerEvent, direction: 'left' | 'right') {
49+
e.preventDefault()
50+
e.stopPropagation()
51+
52+
const target = e.currentTarget as HTMLElement
53+
if (!target) return
54+
55+
target.setPointerCapture(e.pointerId)
56+
57+
resizeState = {
58+
direction,
59+
startX: e.clientX,
60+
startWidth: panelWidth.value,
61+
target,
62+
pointerId: e.pointerId
63+
}
64+
}
65+
66+
function onPointerMove(e: PointerEvent) {
67+
if (!resizeState) return
68+
69+
if (e.pointerId !== resizeState.pointerId) return
70+
71+
if (e.buttons === 0) {
72+
endResize(e)
73+
return
74+
}
75+
76+
const deltaX = e.clientX - resizeState.startX
77+
const newWidth =
78+
resizeState.direction === 'right'
79+
? clampWidth(resizeState.startWidth + deltaX)
80+
: clampWidth(resizeState.startWidth - deltaX)
81+
82+
if (resizeState.direction === 'left') {
83+
const positionDelta = panelWidth.value - newWidth
84+
x.value += positionDelta
85+
}
86+
87+
panelWidth.value = newWidth
88+
}
89+
90+
function endResize(e: PointerEvent) {
91+
if (!resizeState || e.pointerId !== resizeState.pointerId) return
92+
93+
resizeState.target.releasePointerCapture(resizeState.pointerId)
94+
95+
if (position) {
96+
position.width = panelWidth.value
97+
}
98+
99+
resizeState = null
100+
}
101+
102+
function resetWidth(direction: 'left' | 'right') {
103+
const newWidth = ui.tempadPanelWidth
104+
const positionDelta = panelWidth.value - newWidth
105+
106+
// Keep the edge under the double-clicked handle stationary
107+
if (direction === 'left' && positionDelta !== 0) {
108+
x.value += positionDelta
109+
}
110+
111+
panelWidth.value = newWidth
112+
113+
if (position) {
114+
delete position.width
115+
}
116+
}
117+
118+
useEventListener('pointermove', onPointerMove)
119+
useEventListener('pointerup', endResize)
120+
useEventListener('pointercancel', endResize)
121+
122+
const isAtMinWidth = computed(() => panelWidth.value <= ui.tempadPanelWidth)
123+
const isAtMaxWidth = computed(() => panelWidth.value >= ui.tempadPanelMaxWidth)
124+
34125
const restrictedPosition = computed(() => {
35-
if (!panel.value || !header.value) {
126+
if (!header.value) {
36127
return { top: x.value, left: y.value }
37128
}
38129
39-
const { offsetWidth: panelWidth } = panel.value
130+
const panelPixelWidth = panelWidth.value
40131
const { offsetHeight: headerHeight } = header.value
41132
42-
const xMin = -panelWidth / 2
43-
const xMax = windowWidth.value - panelWidth / 2
133+
const xMin = -panelPixelWidth / 2
134+
const xMax = windowWidth.value - panelPixelWidth / 2
44135
const yMin = ui.topBoundary
45136
const yMax = windowHeight.value - headerHeight
46137
@@ -54,6 +145,8 @@ const panelMaxHeight = computed(
54145
() => `${windowHeight.value - restrictedPosition.value.top - ui.bottomBoundary}px`
55146
)
56147
148+
const panelWidthPx = computed(() => `${panelWidth.value}px`)
149+
57150
const positionStyle = computed(() => {
58151
const p = restrictedPosition.value
59152
return `top: ${p.top}px; left: ${p.left}px`
@@ -73,15 +166,45 @@ if (position) {
73166
function toggleMinimized() {
74167
options.value.minimized = !options.value.minimized
75168
}
169+
170+
function getResizeCursor(direction: 'left' | 'right'): 'e-resize' | 'w-resize' | 'ew-resize' {
171+
const atMin = isAtMinWidth.value
172+
const atMax = isAtMaxWidth.value
173+
174+
if (direction === 'left') {
175+
if (atMax) return 'e-resize'
176+
if (atMin) return 'w-resize'
177+
} else {
178+
if (atMax) return 'w-resize'
179+
if (atMin) return 'e-resize'
180+
}
181+
return 'ew-resize'
182+
}
183+
184+
const leftHandleCursor = computed(() => getResizeCursor('left'))
185+
const rightHandleCursor = computed(() => getResizeCursor('right'))
76186
</script>
77187

78188
<template>
79189
<article
80190
ref="panel"
81191
class="tp-panel"
82-
:class="{ 'tp-panel-minimized': options.minimized }"
192+
:class="{
193+
'tp-panel-minimized': options.minimized,
194+
'tp-panel-dragging': isDragging
195+
}"
83196
:style="positionStyle"
84197
>
198+
<div
199+
class="tp-panel-resize-handle tp-panel-resize-handle-left"
200+
@pointerdown="startResize($event, 'left')"
201+
@dblclick="resetWidth('left')"
202+
/>
203+
<div
204+
class="tp-panel-resize-handle tp-panel-resize-handle-right"
205+
@pointerdown="startResize($event, 'right')"
206+
@dblclick="resetWidth('right')"
207+
/>
85208
<header ref="header" class="tp-row tp-row-justify tp-panel-header" @dblclick="toggleMinimized">
86209
<slot name="header" />
87210
</header>
@@ -97,10 +220,32 @@ function toggleMinimized() {
97220
z-index: 6;
98221
display: flex;
99222
flex-direction: column;
223+
width: v-bind(panelWidthPx);
100224
max-height: v-bind(panelMaxHeight);
101225
background-color: var(--color-bg);
102-
border-radius: 2px;
103-
box-shadow: var(--elevation-500-modal-window);
226+
border-radius: var(--radius-large);
227+
box-shadow: var(--elevation-100);
228+
}
229+
230+
.tp-panel-resize-handle {
231+
position: absolute;
232+
top: 0;
233+
bottom: 0;
234+
width: 8px;
235+
z-index: 10;
236+
transition: background-color 0.2s ease;
237+
touch-action: none;
238+
user-select: none;
239+
}
240+
241+
.tp-panel-resize-handle-left {
242+
left: -8px;
243+
cursor: v-bind(leftHandleCursor);
244+
}
245+
246+
.tp-panel-resize-handle-right {
247+
right: -8px;
248+
cursor: v-bind(rightHandleCursor);
104249
}
105250
106251
.tp-panel-header {
@@ -126,9 +271,4 @@ function toggleMinimized() {
126271
width: auto;
127272
height: 32px;
128273
}
129-
130-
[data-fpl-version='ui3'] .tp-panel {
131-
box-shadow: var(--elevation-100);
132-
border-radius: var(--radius-large);
133-
}
134274
</style>

entrypoints/ui/App.vue

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ function toggleMinimized() {
2121
options.value.minimized = !options.value.minimized
2222
}
2323
24-
const panelWidth = `${ui.tempadPanelWidth}px`
25-
2624
const { status, selfActive, count, activate } = useMcp()
2725
2826
const isMcpConnected = computed(() => status.value === 'connected')
@@ -111,18 +109,19 @@ function activateMcp() {
111109

112110
<style scoped>
113111
.tp-main {
114-
width: v-bind(panelWidth);
115-
transition: width, height;
116-
transition-duration: 0.2s;
117-
transition-timing-function: cubic-bezier(0.87, 0, 0.13, 1);
118-
overflow: hidden;
112+
transition: height 0.2s cubic-bezier(0.87, 0, 0.13, 1);
119113
}
120114
121115
.tp-main-minimized {
122116
height: 41px;
123117
border-bottom-width: 0;
124118
}
125119
120+
.tp-main.tp-panel-dragging,
121+
.tp-main.tp-panel-resizing {
122+
transition: none;
123+
}
124+
126125
.tp-mcp-badge {
127126
gap: 4px;
128127
}

ui/figma.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const NATIVE_PANEL_WIDTH = 241
22
const TEMPAD_PANEL_WIDTH = 240
3+
const TEMPAD_PANEL_MAX_WIDTH = 500
34

45
const ui = reactive({
56
isUi3: false,
@@ -12,6 +13,10 @@ const ui = reactive({
1213
return TEMPAD_PANEL_WIDTH
1314
},
1415

16+
get tempadPanelMaxWidth() {
17+
return TEMPAD_PANEL_MAX_WIDTH
18+
},
19+
1520
get topBoundary() {
1621
return sumLength(this.isUi3 ? 12 : '--toolbar-height', '--editor-banner-height')
1722
},

ui/state.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export type Options = {
1515
panelPosition: {
1616
left: number
1717
top: number
18+
width?: number
1819
}
1920
prefOpen: boolean
2021
deepSelectOn: boolean
@@ -33,7 +34,8 @@ export const options = useStorage<Options>('tempad-dev', {
3334
minimized: false,
3435
panelPosition: {
3536
left: window.innerWidth - ui.nativePanelWidth - ui.tempadPanelWidth,
36-
top: ui.topBoundary
37+
top: ui.topBoundary,
38+
width: ui.tempadPanelWidth
3739
},
3840
prefOpen: false,
3941
deepSelectOn: false,

0 commit comments

Comments
 (0)