11<script setup lang="ts">
2- import { useDraggable , useWindowSize , watchDebounced } from ' @vueuse/core'
2+ import { useDraggable , useEventListener , useWindowSize , watchDebounced } from ' @vueuse/core'
33
44import { useScrollbar } from ' @/composables/scrollbar'
55import { ui } from ' @/ui/figma'
@@ -21,7 +21,7 @@ useScrollbar(main, {
2121})
2222
2323const 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
3232const { 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+
34125const 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+
57150const positionStyle = computed (() => {
58151 const p = restrictedPosition .value
59152 return ` top: ${p .top }px; left: ${p .left }px `
@@ -73,15 +166,45 @@ if (position) {
73166function 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 >
0 commit comments