Skip to content

Commit 0060b32

Browse files
committed
feat(gpt-runner-web): add reszie support for sidebar
1 parent c0f253b commit 0060b32

File tree

15 files changed

+470
-258
lines changed

15 files changed

+470
-258
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"OVSE",
3838
"ovsx",
3939
"rehype",
40+
"rubberband",
4041
"tablist",
4142
"tabpanel",
4243
"tagify",

packages/gpt-runner-web/client/src/components/chat-message-input/chat-message-input.styles.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { styled } from 'styled-components'
44
export const Wrapper = styled.div`
55
display: flex;
66
flex-direction: column;
7-
max-height: 250px;
7+
height: 100%;
88
width: 100%;
99
flex-shrink: 0;
1010
padding: 0.5rem;
@@ -21,8 +21,10 @@ export const ToolbarWrapper = styled.div`
2121
export const StyledVSCodeTextArea = styled(VSCodeTextArea)`
2222
margin-top: 0.5rem;
2323
overflow: hidden;
24+
height: 100%;
2425
2526
&::part(control) {
2627
border-radius: 0.25rem;
28+
height: 100%;
2729
}
2830
`

packages/gpt-runner-web/client/src/components/chat-message-item/chat-message-item.styles.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ export const MsgWrapper = styled.div<{ $isMe: boolean }>`
88
margin-bottom: 1rem;
99
`
1010

11-
export const MsgAvatarWrapper = styled.div<{ $isMe: boolean }>`
12-
display: none;
11+
export const MsgAvatarWrapper = styled.div<{ $isMe: boolean; $showAvatar: boolean }>`
12+
display: ${({ $showAvatar }) => $showAvatar ? 'flex' : 'none'};
1313
width: 2rem;
1414
height: 2rem;
1515
justify-content: center;
@@ -18,10 +18,6 @@ export const MsgAvatarWrapper = styled.div<{ $isMe: boolean }>`
1818
margin: 0 0.5rem;
1919
border: 1px solid var(--panel-view-border);
2020
align-self: flex-start;
21-
22-
${withBreakpoint('lg', css`
23-
display: flex;
24-
`)}
2521
`
2622

2723
export const MsgContentWrapper = styled.div<{ $isMe: boolean }>`

packages/gpt-runner-web/client/src/components/chat-message-item/index.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export interface BuildMessageToolbarState extends SingleChatMessage {
1414
export interface MessageItemProps extends SingleChatMessage, Partial<MessageCodeBlockProps> {
1515
status: ChatMessageStatus
1616
showToolbar?: 'always' | 'hover' | 'never'
17+
showAvatar?: boolean
1718
style?: React.CSSProperties
1819
buildMessageToolbar?: (state: BuildMessageToolbarState) => React.ReactNode
1920
}
@@ -24,6 +25,7 @@ export const MessageItem: FC<MessageItemProps> = (props) => {
2425
text,
2526
status,
2627
style,
28+
showAvatar = false,
2729
showToolbar = 'hover',
2830
buildCodeToolbar,
2931
buildMessageToolbar,
@@ -54,7 +56,7 @@ export const MessageItem: FC<MessageItemProps> = (props) => {
5456

5557
return (
5658
<MsgWrapper style={style} $isMe={name === ChatRole.User}>
57-
<MsgAvatarWrapper $isMe={name === ChatRole.User}>
59+
<MsgAvatarWrapper $showAvatar={showAvatar} $isMe={name === ChatRole.User}>
5860
<Icon className={clsx(name === ChatRole.User ? 'codicon-account' : 'codicon-github')} />
5961
</MsgAvatarWrapper>
6062
<MsgContentWrapper ref={hoverContentRef} $isMe={name === ChatRole.User}>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { styled } from 'styled-components'
2+
3+
export const DragLine = styled.div<{ $dragLineColor: string; $dragLineActiveColor: string; $dragLineWidth: string }>`
4+
position: absolute;
5+
background: ${({ $dragLineColor }) => $dragLineColor};
6+
/* z-index: 2; */
7+
touch-action: none;
8+
9+
&:active {
10+
background: ${({ $dragLineActiveColor }) => $dragLineActiveColor};
11+
}
12+
13+
&[data-direction='left'] {
14+
cursor: col-resize;
15+
width: ${({ $dragLineWidth }) => $dragLineWidth};
16+
left: 0;
17+
top: 0;
18+
bottom: 0;
19+
}
20+
21+
&[data-direction='right'] {
22+
cursor: col-resize;
23+
width: ${({ $dragLineWidth }) => $dragLineWidth};
24+
right: 0;
25+
top: 0;
26+
bottom: 0;
27+
}
28+
29+
&[data-direction='top'] {
30+
cursor: row-resize;
31+
height: ${({ $dragLineWidth }) => $dragLineWidth};
32+
top: 0;
33+
left: 0;
34+
right: 0;
35+
}
36+
37+
&[data-direction='bottom'] {
38+
cursor: row-resize;
39+
height: ${({ $dragLineWidth }) => $dragLineWidth};
40+
bottom: 0;
41+
left: 0;
42+
right: 0;
43+
}
44+
`
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import { useEffect, useMemo, useRef } from 'react'
2+
import type { UserDragConfig } from '@use-gesture/react'
3+
import { useDrag } from '@use-gesture/react'
4+
import { motion, useMotionValue } from 'framer-motion'
5+
import { DragLine } from './drag-resize-view.styles'
6+
7+
export type DragDirection = 'left' | 'right' | 'top' | 'bottom'
8+
export interface DragDirectionConfig {
9+
direction: DragDirection
10+
/**
11+
* The boundary of the drag line.
12+
* The first element is the minimum value, the second element is the maximum value.
13+
*/
14+
boundary: number[]
15+
}
16+
export interface DragResizeViewProps {
17+
initWidth: number
18+
initHeight: number
19+
dragConfig?: Omit<UserDragConfig, 'axis' | 'bounds'>
20+
dragDirectionConfigs: DragDirectionConfig[]
21+
style?: React.CSSProperties
22+
className?: string
23+
dragLineStyle?: React.CSSProperties
24+
dragLineClassName?: string
25+
dragLineColor?: string
26+
dragLineActiveColor?: string
27+
dragLineWidth?: string
28+
children: React.ReactNode
29+
}
30+
31+
export function DragResizeView(props: DragResizeViewProps) {
32+
const {
33+
initWidth,
34+
initHeight,
35+
dragConfig,
36+
dragDirectionConfigs,
37+
style,
38+
className,
39+
dragLineClassName,
40+
dragLineStyle,
41+
dragLineColor = 'var(--panel-view-border)',
42+
dragLineActiveColor = 'var(--focus-border)',
43+
dragLineWidth = '1px',
44+
children,
45+
} = props
46+
47+
const ref = useRef<HTMLDivElement>(null)
48+
const finalWidth = useMotionValue(initWidth)
49+
const finalHeight = useMotionValue(initHeight)
50+
51+
useEffect(() => {
52+
finalWidth.set(initWidth)
53+
}, [initWidth])
54+
55+
useEffect(() => {
56+
finalHeight.set(initHeight)
57+
}, [initHeight])
58+
59+
const dragDirectionConfigMap = useMemo(() => {
60+
return dragDirectionConfigs.reduce((acc, config) => {
61+
acc[config.direction] = config
62+
return acc
63+
}, {} as Record<DragDirection, DragDirectionConfig>)
64+
}, [dragDirectionConfigs])
65+
66+
useEffect(() => {
67+
const handleWindowResize = () => {
68+
if (ref.current) {
69+
finalWidth.set(ref.current.offsetWidth)
70+
finalHeight.set(ref.current.offsetHeight)
71+
}
72+
}
73+
74+
window.addEventListener('resize', handleWindowResize)
75+
return () => {
76+
window.removeEventListener('resize', handleWindowResize)
77+
}
78+
}, [])
79+
80+
const leftDragLingBind = useDrag(
81+
({ offset }) => {
82+
finalWidth.set(initWidth - offset[0])
83+
},
84+
{
85+
rubberband: true,
86+
...dragConfig,
87+
axis: 'x',
88+
bounds: {
89+
left: dragDirectionConfigMap?.left?.boundary?.[0] ?? 0,
90+
right: dragDirectionConfigMap?.left?.boundary?.[1] ?? 0,
91+
}, // Adjust the bounds as needed
92+
},
93+
)
94+
95+
const rightDragLingBind = useDrag(
96+
({ offset }) => {
97+
finalWidth.set(initWidth + offset[0])
98+
},
99+
{
100+
rubberband: true,
101+
...dragConfig,
102+
axis: 'x',
103+
bounds: {
104+
left: dragDirectionConfigMap?.right?.boundary?.[0] ?? 0,
105+
right: dragDirectionConfigMap?.right?.boundary?.[1] ?? 0,
106+
}, // Adjust the bounds as needed
107+
},
108+
)
109+
110+
const topDragLingBind = useDrag(
111+
({ offset }) => {
112+
finalHeight.set(initHeight - offset[1])
113+
},
114+
{
115+
rubberband: true,
116+
...dragConfig,
117+
axis: 'y',
118+
bounds: {
119+
top: dragDirectionConfigMap?.top?.boundary?.[0] ?? 0,
120+
bottom: dragDirectionConfigMap?.top?.boundary?.[1] ?? 0,
121+
}, // Adjust the bounds as needed
122+
},
123+
)
124+
125+
const bottomDragLingBind = useDrag(
126+
({ offset }) => {
127+
finalHeight.set(initHeight + offset[1])
128+
},
129+
{
130+
rubberband: true,
131+
...dragConfig,
132+
axis: 'y',
133+
bounds: {
134+
top: dragDirectionConfigMap?.bottom?.boundary?.[0] ?? 0,
135+
bottom: dragDirectionConfigMap?.bottom?.boundary?.[1] ?? 0,
136+
}, // Adjust the bounds as needed
137+
},
138+
)
139+
140+
const dragLineProps: React.HTMLAttributes<HTMLDivElement> = {
141+
style: dragLineStyle,
142+
className: dragLineClassName,
143+
}
144+
145+
const dragLineBindMap: Record<DragDirection, ReturnType<typeof useDrag>> = {
146+
left: leftDragLingBind,
147+
right: rightDragLingBind,
148+
top: topDragLingBind,
149+
bottom: bottomDragLingBind,
150+
}
151+
152+
return <motion.div
153+
className={className}
154+
style={{
155+
...style,
156+
width: finalWidth,
157+
height: finalHeight,
158+
position: 'relative',
159+
}} ref={ref}>
160+
{children}
161+
162+
{Object.entries(dragLineBindMap).map(([direction, bind], index) => {
163+
return dragDirectionConfigMap[direction as DragDirection]
164+
? <DragLine
165+
{...{
166+
...dragLineProps,
167+
...bind(),
168+
}}
169+
key={index}
170+
$dragLineColor={dragLineColor}
171+
$dragLineActiveColor={dragLineActiveColor}
172+
$dragLineWidth={dragLineWidth}
173+
data-direction={direction}
174+
></DragLine>
175+
: null
176+
})}
177+
</motion.div>
178+
}

packages/gpt-runner-web/client/src/components/sidebar/index.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useDebounce } from 'react-use'
44
import type { TreeProps } from '../tree'
55
import { Tree } from '../tree'
66
import type { TreeItemBaseStateOtherInfo, TreeItemProps } from '../tree-item'
7-
import { SidebarHeader, SidebarSearch, SidebarSearchRightWrapper, SidebarSearchWrapper, SidebarTreeWrapper, SidebarWrapper } from './sidebar.styles'
7+
import { SidebarHeader, SidebarSearch, SidebarSearchRightWrapper, SidebarSearchWrapper, SidebarTreeWrapper, SidebarUnderSearchWrapper, SidebarWrapper } from './sidebar.styles'
88

99
export interface SidebarProps<OtherInfo extends TreeItemBaseStateOtherInfo = TreeItemBaseStateOtherInfo> {
1010
defaultSearchKeyword?: string
@@ -14,6 +14,7 @@ export interface SidebarProps<OtherInfo extends TreeItemBaseStateOtherInfo = Tre
1414
sortTreeItems?: (items: TreeItemProps<OtherInfo>[]) => TreeItemProps<OtherInfo>[]
1515
buildTopToolbarSlot?: () => React.ReactNode
1616
buildSearchRightSlot?: () => React.ReactNode
17+
buildUnderSearchSlot?: () => React.ReactNode
1718
}
1819

1920
export function Sidebar<OtherInfo extends TreeItemBaseStateOtherInfo = TreeItemBaseStateOtherInfo>(props: SidebarProps<OtherInfo>) {
@@ -25,6 +26,7 @@ export function Sidebar<OtherInfo extends TreeItemBaseStateOtherInfo = TreeItemB
2526
sortTreeItems,
2627
buildTopToolbarSlot,
2728
buildSearchRightSlot,
29+
buildUnderSearchSlot,
2830
} = props
2931

3032
const [searchKeyword, setSearchKeyword] = useState(defaultSearchKeyword)
@@ -80,6 +82,9 @@ export function Sidebar<OtherInfo extends TreeItemBaseStateOtherInfo = TreeItemB
8082
{buildSearchRightSlot?.()}
8183
</SidebarSearchRightWrapper>
8284
</SidebarSearchWrapper>
85+
<SidebarUnderSearchWrapper>
86+
{buildUnderSearchSlot?.()}
87+
</SidebarUnderSearchWrapper>
8388
<SidebarTreeWrapper>
8489
<Tree
8590
{...tree}

packages/gpt-runner-web/client/src/components/sidebar/sidebar.styles.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ export const SidebarSearchWrapper = styled.div`
2323
height: var(--my-input-height);
2424
`
2525

26+
export const SidebarUnderSearchWrapper = styled.div`
27+
font-size: var(--type-ramp-base-font-size);
28+
`
29+
2630
export const SidebarSearch = styled(VSCodeTextField)`
2731
flex: 1;
2832
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { useCallback, useMemo, useRef, useState } from 'react'
2+
import { useIsomorphicLayoutEffect } from 'react-use'
3+
4+
export interface ElementSize {
5+
width: number
6+
height: number
7+
}
8+
9+
export type UseElementSizeResult<E extends Element = Element> = [
10+
React.MutableRefObject<E | null>,
11+
ElementSize,
12+
]
13+
14+
const defaultState: ElementSize = {
15+
width: 0,
16+
height: 0,
17+
}
18+
19+
export function useElementSizeRealTime<
20+
E extends Element = Element,
21+
>(): UseElementSizeResult<E> {
22+
const elementRef = useRef<E | null>(null)
23+
const [size, setSize] = useState<ElementSize>(defaultState)
24+
25+
const updateSize = useCallback(() => {
26+
if (elementRef.current) {
27+
const { width, height } = elementRef.current.getBoundingClientRect()
28+
setSize({ width, height })
29+
}
30+
}, [])
31+
32+
const observer = useMemo(
33+
() =>
34+
new window.ResizeObserver(() => {
35+
updateSize()
36+
}),
37+
[],
38+
)
39+
40+
useIsomorphicLayoutEffect(() => {
41+
if (!elementRef.current)
42+
return
43+
observer.observe(elementRef.current)
44+
return () => {
45+
observer.disconnect()
46+
}
47+
}, [elementRef.current])
48+
49+
return [elementRef, size]
50+
}

0 commit comments

Comments
 (0)