1
0
Fork 0
forked from Mirrors/gomuks

web/roomlist: make room list panel resizable

This commit is contained in:
Tulir Asokan 2024-10-30 22:50:45 +02:00
parent 336f0aa100
commit 39bfa7d084
16 changed files with 345 additions and 75 deletions

1
web/src/icons/group.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M40-160v-112q0-34 17.5-62.5T104-378q62-31 126-46.5T360-440q66 0 130 15.5T616-378q29 15 46.5 43.5T680-272v112H40Zm720 0v-120q0-44-24.5-84.5T666-434q51 6 96 20.5t84 35.5q36 20 55 44.5t19 53.5v120H760ZM360-480q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 66-47 113t-113 47Zm400-160q0 66-47 113t-113 47q-11 0-28-2.5t-28-5.5q27-32 41.5-71t14.5-81q0-42-14.5-81T544-792q14-5 28-6.5t28-1.5q66 0 113 47t47 113ZM120-240h480v-32q0-11-5.5-20T580-306q-54-27-109-40.5T360-360q-56 0-111 13.5T140-306q-9 5-14.5 14t-5.5 20v32Zm240-320q33 0 56.5-23.5T440-640q0-33-23.5-56.5T360-720q-33 0-56.5 23.5T280-640q0 33 23.5 56.5T360-560Zm0 320Zm0-400Z"/></svg>

After

Width:  |  Height:  |  Size: 767 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="m370-80-16-128q-13-5-24.5-12T307-235l-119 50L78-375l103-78q-1-7-1-13.5v-27q0-6.5 1-13.5L78-585l110-190 119 50q11-8 23-15t24-12l16-128h220l16 128q13 5 24.5 12t22.5 15l119-50 110 190-103 78q1 7 1 13.5v27q0 6.5-2 13.5l103 78-110 190-118-50q-11 8-23 15t-24 12L590-80H370Zm70-80h79l14-106q31-8 57.5-23.5T639-327l99 41 39-68-86-65q5-14 7-29.5t2-31.5q0-16-2-31.5t-7-29.5l86-65-39-68-99 42q-22-23-48.5-38.5T533-694l-13-106h-79l-14 106q-31 8-57.5 23.5T321-633l-99-41-39 68 86 64q-5 15-7 30t-2 32q0 16 2 31t7 30l-86 65 39 68 99-42q22 23 48.5 38.5T427-266l13 106Zm42-180q58 0 99-41t41-99q0-58-41-99t-99-41q-59 0-99.5 41T342-480q0 58 40.5 99t99.5 41Zm-2-140Z"/></svg>

After

Width:  |  Height:  |  Size: 772 B

View file

@ -1,31 +1,49 @@
main.matrix-main { main.matrix-main {
--room-list-width: 300px;
--right-panel-width: 300px;
position: fixed; position: fixed;
inset: 0; inset: 0;
display: grid; display: grid;
grid-template: "roomlist roomview" 1fr / 300px 1fr; grid-template:
" roomlist rh1 roomview" 1fr
/ var(--room-list-width) 0 1fr;
@media screen and (max-width: 1000px) { &.right-panel-open {
grid-template: "roomlist roomview" 1fr / 250px 1fr; grid-template:
} " roomlist rh1 roomview rh2 rightpanel " 1fr
/ var(--room-list-width) 0 1fr 0 var(--right-panel-width);
@media screen and (max-width: 900px) {
grid-template: "roomlist roomview" 1fr / 200px 1fr;
} }
@media screen and (max-width: 750px) { @media screen and (max-width: 750px) {
&.room-selected { &.right-panel-open {
grid-template: "rightpanel" 1fr / 1fr;
> div.room-list-wrapper {
display: none;
}
> div.room-view {
display: none;
}
}
&.room-selected:not(.right-panel-open) {
grid-template: "roomview" 1fr / 1fr; grid-template: "roomview" 1fr / 1fr;
> div.room-list-wrapper { > div.room-list-wrapper {
display: none; display: none;
} }
} }
&:not(.room-selected) { &:not(.room-selected):not(.right-panel-open) {
grid-template: "roomlist" 1fr / 1fr; grid-template: "roomlist" 1fr / 1fr;
> div.room-view {
display: none;
}
} }
} }
> div.room-list-resizer {
grid-area: rh1;
}
> div.right-panel-resizer {
grid-area: rh2;
}
} }

View file

@ -13,32 +13,64 @@
// //
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import { use, useCallback, useLayoutEffect, useState } from "react" import { use, useCallback, useLayoutEffect, useMemo, useState } from "react"
import type { RoomID } from "@/api/types" import type { RoomID } from "@/api/types"
import ClientContext from "./ClientContext.ts" import ClientContext from "./ClientContext.ts"
import MainScreenContext, { MainScreenContextFields } from "./MainScreenContext.ts"
import RoomView from "./RoomView.tsx" import RoomView from "./RoomView.tsx"
import RightPanel, { RightPanelProps } from "./rightpanel/RightPanel.tsx"
import RoomList from "./roomlist/RoomList.tsx" import RoomList from "./roomlist/RoomList.tsx"
import { useResizeHandle } from "./useResizeHandle.tsx"
import "./MainScreen.css" import "./MainScreen.css"
const MainScreen = () => { const MainScreen = () => {
const [activeRoomID, setActiveRoomID] = useState<RoomID | null>(null) const [activeRoomID, setActiveRoomID] = useState<RoomID | null>(null)
const [rightPanel, setRightPanel] = useState<RightPanelProps | null>(null)
const client = use(ClientContext)! const client = use(ClientContext)!
const activeRoom = activeRoomID && client.store.rooms.get(activeRoomID) const activeRoom = activeRoomID && client.store.rooms.get(activeRoomID)
const setActiveRoom = useCallback((roomID: RoomID) => { const setActiveRoom = useCallback((roomID: RoomID) => {
console.log("Switching to room", roomID) console.log("Switching to room", roomID)
setActiveRoomID(roomID) setActiveRoomID(roomID)
setRightPanel(null)
if (client.store.rooms.get(roomID)?.stateLoaded === false) { if (client.store.rooms.get(roomID)?.stateLoaded === false) {
client.loadRoomState(roomID) client.loadRoomState(roomID)
.catch(err => console.error("Failed to load room state", err)) .catch(err => console.error("Failed to load room state", err))
} }
}, [client]) }, [client])
const context: MainScreenContextFields = useMemo(() => ({
setActiveRoom,
clickRoom: (evt: React.MouseEvent) => {
const roomID = evt.currentTarget.getAttribute("data-room-id")
if (roomID) {
setActiveRoom(roomID)
} else {
console.warn("No room ID :(", evt.currentTarget)
}
},
clearActiveRoom: () => setActiveRoomID(null),
setRightPanel,
}), [setRightPanel, setActiveRoom])
useLayoutEffect(() => { useLayoutEffect(() => {
client.store.switchRoom = setActiveRoom client.store.switchRoom = setActiveRoom
}, [client, setActiveRoom]) }, [client, setActiveRoom])
const clearActiveRoom = useCallback(() => setActiveRoomID(null), []) const [roomListWidth, resizeHandle1] = useResizeHandle(
return <main className={`matrix-main ${activeRoom ? "room-selected" : ""}`}> 300, 48, 900, "roomListWidth", { className: "room-list-resizer" },
<RoomList setActiveRoom={setActiveRoom} activeRoomID={activeRoomID} /> )
{activeRoom && <RoomView key={activeRoomID} clearActiveRoom={clearActiveRoom} room={activeRoom} />} const [rightPanelWidth, resizeHandle2] = useResizeHandle(
300, 100, 900, "rightPanelWidth", { className: "right-panel-resizer" },
)
const extraStyle = {
["--room-list-width" as string]: `${roomListWidth}px`,
["--right-panel-width" as string]: `${rightPanelWidth}px`,
}
return <main className={`matrix-main ${activeRoom ? "room-selected" : ""}`} style={extraStyle}>
<MainScreenContext value={context}>
<RoomList activeRoomID={activeRoomID}/>
{resizeHandle1}
{activeRoom && <RoomView key={activeRoomID} room={activeRoom}/>}
{resizeHandle2}
{rightPanel && <RightPanel {...rightPanel}/>}
</MainScreenContext>
</main> </main>
} }

View file

@ -0,0 +1,43 @@
// gomuks - A Matrix client written in Go.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import { createContext } from "react"
import type { RoomID } from "@/api/types"
import type { RightPanelProps } from "./rightpanel/RightPanel.tsx"
export interface MainScreenContextFields {
setActiveRoom: (roomID: RoomID) => void
clickRoom: (evt: React.MouseEvent) => void
clearActiveRoom: () => void
setRightPanel: (props: RightPanelProps) => void
}
const MainScreenContext = createContext<MainScreenContextFields>({
get setActiveRoom(): never {
throw new Error("MainScreenContext used outside main screen")
},
get clickRoom(): never {
throw new Error("MainScreenContext used outside main screen")
},
get clearActiveRoom(): never {
throw new Error("MainScreenContext used outside main screen")
},
get setRightPanel(): never {
throw new Error("MainScreenContext used outside main screen")
},
})
export default MainScreenContext

View file

@ -0,0 +1,12 @@
div.resize-handle-outer {
position: relative;
> div.resize-handle-inner {
position: absolute;
top: 0;
bottom: 0;
left: -.25rem;
right: -.25rem;
cursor: col-resize;
}
}

View file

@ -0,0 +1,50 @@
// gomuks - A Matrix client written in Go.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import React, { CSSProperties } from "react"
import useEvent from "@/util/useEvent.ts"
import "./ResizeHandle.css"
export interface ResizeHandleProps {
width: number
minWidth: number
maxWidth: number
setWidth: (width: number) => void
className?: string
style?: CSSProperties
}
const ResizeHandle = ({ width, minWidth, maxWidth, setWidth, style, className }: ResizeHandleProps) => {
const onMouseDown = useEvent((evt: React.MouseEvent<HTMLDivElement>) => {
const origWidth = width
const startPos = evt.clientX
const onMouseMove = (evt: MouseEvent) => {
setWidth(Math.max(minWidth, Math.min(maxWidth, origWidth + evt.clientX - startPos)))
evt.preventDefault()
}
const onMouseUp = () => {
document.removeEventListener("mousemove", onMouseMove)
document.removeEventListener("mouseup", onMouseUp)
}
document.addEventListener("mousemove", onMouseMove)
document.addEventListener("mouseup", onMouseUp)
evt.preventDefault()
})
return <div className={`resize-handle-outer ${className ?? ""}`} style={style}>
<div className="resize-handle-inner" onMouseDown={onMouseDown}/>
</div>
}
export default ResizeHandle

View file

@ -1,4 +1,5 @@
div.room-view { div.room-view {
grid-area: roomview;
overflow: hidden; overflow: hidden;
height: 100%; height: 100%;
display: grid; display: grid;
@ -8,21 +9,4 @@ div.room-view {
"messageview" 1fr "messageview" 1fr
"input" auto "input" auto
/ 1fr; / 1fr;
> div.room-header {
display: flex;
align-items: center;
gap: .5rem;
padding-left: .5rem;
border-bottom: 1px solid var(--border-color);
> span.room-name {
font-weight: bold;
}
> button.back {
height: 2.5rem;
width: 2.5rem;
}
}
} }

View file

@ -13,39 +13,16 @@
// //
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import { use, useRef } from "react" import { useRef } from "react"
import { getAvatarURL } from "@/api/media.ts"
import { RoomStateStore } from "@/api/statestore" import { RoomStateStore } from "@/api/statestore"
import { useEventAsState } from "@/util/eventdispatcher.ts" import RoomViewHeader from "./RoomViewHeader.tsx"
import MessageComposer from "./composer/MessageComposer.tsx" import MessageComposer from "./composer/MessageComposer.tsx"
import { LightboxContext } from "./modal/Lightbox.tsx"
import { RoomContext, RoomContextData } from "./roomcontext.ts" import { RoomContext, RoomContextData } from "./roomcontext.ts"
import TimelineView from "./timeline/TimelineView.tsx" import TimelineView from "./timeline/TimelineView.tsx"
import BackIcon from "@/icons/back.svg?react"
import "./RoomView.css" import "./RoomView.css"
interface RoomViewProps { interface RoomViewProps {
room: RoomStateStore room: RoomStateStore
clearActiveRoom: () => void
}
const RoomHeader = ({ room, clearActiveRoom }: RoomViewProps) => {
const roomMeta = useEventAsState(room.meta)
const avatarSourceID = roomMeta.lazy_load_summary?.heroes?.length === 1
? roomMeta.lazy_load_summary.heroes[0] : room.roomID
return <div className="room-header">
<button className="back" onClick={clearActiveRoom}><BackIcon/></button>
<img
className="avatar"
loading="lazy"
src={getAvatarURL(avatarSourceID, { avatar_url: roomMeta.avatar, displayname: roomMeta.name })}
onClick={use(LightboxContext)!}
alt=""
/>
<span className="room-name">
{roomMeta.name ?? roomMeta.room_id}
</span>
</div>
} }
const onKeyDownRoomView = (evt: React.KeyboardEvent) => { const onKeyDownRoomView = (evt: React.KeyboardEvent) => {
@ -54,14 +31,14 @@ const onKeyDownRoomView = (evt: React.KeyboardEvent) => {
} }
} }
const RoomView = ({ room, clearActiveRoom }: RoomViewProps) => { const RoomView = ({ room }: RoomViewProps) => {
const roomContextDataRef = useRef<RoomContextData | undefined>(undefined) const roomContextDataRef = useRef<RoomContextData | undefined>(undefined)
if (roomContextDataRef.current === undefined) { if (roomContextDataRef.current === undefined) {
roomContextDataRef.current = new RoomContextData(room) roomContextDataRef.current = new RoomContextData(room)
} }
return <div className="room-view" onKeyDown={onKeyDownRoomView} tabIndex={-1}> return <div className="room-view" onKeyDown={onKeyDownRoomView} tabIndex={-1}>
<RoomContext value={roomContextDataRef.current}> <RoomContext value={roomContextDataRef.current}>
<RoomHeader room={room} clearActiveRoom={clearActiveRoom}/> <RoomViewHeader room={room}/>
<TimelineView/> <TimelineView/>
<MessageComposer/> <MessageComposer/>
</RoomContext> </RoomContext>

View file

@ -0,0 +1,31 @@
div.room-header {
display: flex;
align-items: center;
gap: .5rem;
padding-left: .5rem;
border-bottom: 1px solid var(--border-color);
> span.room-name {
font-weight: bold;
}
> div.divider {
flex: 1;
}
> button.back {
height: 2.5rem;
width: 2.5rem;
}
> div.right-buttons {
display: flex;
align-items: center;
margin-right: .5rem;
> button {
width: 2.5rem;
height: 2.5rem;
}
}
}

View file

@ -0,0 +1,58 @@
// gomuks - A Matrix client written in Go.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import { use } from "react"
import { getAvatarURL } from "@/api/media.ts"
import { RoomStateStore } from "@/api/statestore"
import { useEventAsState } from "@/util/eventdispatcher.ts"
import MainScreenContext from "./MainScreenContext.ts"
import { LightboxContext } from "./modal/Lightbox.tsx"
import BackIcon from "@/icons/back.svg?react"
// import PeopleIcon from "@/icons/group.svg?react"
// import PinIcon from "@/icons/pin.svg?react"
// import SettingsIcon from "@/icons/settings.svg?react"
import "./RoomViewHeader.css"
interface RoomViewHeaderProps {
room: RoomStateStore
}
const RoomViewHeader = ({ room }: RoomViewHeaderProps) => {
const roomMeta = useEventAsState(room.meta)
const avatarSourceID = roomMeta.lazy_load_summary?.heroes?.length === 1
? roomMeta.lazy_load_summary.heroes[0] : room.roomID
const mainScreen = use(MainScreenContext)
return <div className="room-header">
<button className="back" onClick={mainScreen.clearActiveRoom}><BackIcon/></button>
<img
className="avatar"
loading="lazy"
src={getAvatarURL(avatarSourceID, { avatar_url: roomMeta.avatar, displayname: roomMeta.name })}
onClick={use(LightboxContext)!}
alt=""
/>
<span className="room-name">
{roomMeta.name ?? roomMeta.room_id}
</span>
<div className="divider"/>
{/*<div className="right-buttons">
<button><PinIcon/></button>
<button><PeopleIcon/></button>
<button><SettingsIcon/></button>
</div>*/}
</div>
}
export default RoomViewHeader

View file

@ -0,0 +1,3 @@
div.right-panel {
}

View file

@ -0,0 +1,27 @@
// gomuks - A Matrix client written in Go.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
export interface RightPanelProps {
meow?: never
}
const RightPanel = ({ meow }: RightPanelProps) => {
return <div className="right-panel">
{meow}
</div>
}
export default RightPanel

View file

@ -19,10 +19,10 @@ import type { RoomListEntry } from "@/api/statestore"
import type { MemDBEvent, MemberEventContent } from "@/api/types" import type { MemDBEvent, MemberEventContent } from "@/api/types"
import useContentVisibility from "@/util/contentvisibility.ts" import useContentVisibility from "@/util/contentvisibility.ts"
import ClientContext from "../ClientContext.ts" import ClientContext from "../ClientContext.ts"
import MainScreenContext from "../MainScreenContext.ts"
export interface RoomListEntryProps { export interface RoomListEntryProps {
room: RoomListEntry room: RoomListEntry
setActiveRoom: (evt: React.MouseEvent) => void
isActive: boolean isActive: boolean
hidden: boolean hidden: boolean
} }
@ -77,12 +77,12 @@ const EntryInner = ({ room }: InnerProps) => {
</> </>
} }
const Entry = ({ room, setActiveRoom, isActive, hidden }: RoomListEntryProps) => { const Entry = ({ room, isActive, hidden }: RoomListEntryProps) => {
const [isVisible, divRef] = useContentVisibility<HTMLDivElement>() const [isVisible, divRef] = useContentVisibility<HTMLDivElement>()
return <div return <div
ref={divRef} ref={divRef}
className={`room-entry ${isActive ? "active" : ""} ${hidden ? "hidden" : ""}`} className={`room-entry ${isActive ? "active" : ""} ${hidden ? "hidden" : ""}`}
onClick={setActiveRoom} onClick={use(MainScreenContext).clickRoom}
data-room-id={room.room_id} data-room-id={room.room_id}
> >
{isVisible ? <EntryInner room={room}/> : null} {isVisible ? <EntryInner room={room}/> : null}

View file

@ -22,23 +22,14 @@ import Entry from "./Entry.tsx"
import "./RoomList.css" import "./RoomList.css"
interface RoomListProps { interface RoomListProps {
setActiveRoom: (room_id: RoomID) => void
activeRoomID: RoomID | null activeRoomID: RoomID | null
} }
const RoomList = ({ setActiveRoom, activeRoomID }: RoomListProps) => { const RoomList = ({ activeRoomID }: RoomListProps) => {
const roomList = useEventAsState(use(ClientContext)!.store.roomList) const roomList = useEventAsState(use(ClientContext)!.store.roomList)
const roomFilterRef = useRef<HTMLInputElement>(null) const roomFilterRef = useRef<HTMLInputElement>(null)
const [roomFilter, setRoomFilter] = useState("") const [roomFilter, setRoomFilter] = useState("")
const [realRoomFilter, setRealRoomFilter] = useState("") const [realRoomFilter, setRealRoomFilter] = useState("")
const clickRoom = useCallback((evt: React.MouseEvent) => {
const roomID = evt.currentTarget.getAttribute("data-room-id")
if (roomID) {
setActiveRoom(roomID)
} else {
console.warn("No room ID :(", evt.currentTarget)
}
}, [setActiveRoom])
const updateRoomFilter = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => { const updateRoomFilter = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => {
setRoomFilter(evt.target.value) setRoomFilter(evt.target.value)
@ -61,7 +52,6 @@ const RoomList = ({ setActiveRoom, activeRoomID }: RoomListProps) => {
isActive={room.room_id === activeRoomID} isActive={room.room_id === activeRoomID}
hidden={roomFilter ? !room.search_name.includes(realRoomFilter) : false} hidden={roomFilter ? !room.search_name.includes(realRoomFilter) : false}
room={room} room={room}
setActiveRoom={clickRoom}
/>, />,
)} )}
</div> </div>

View file

@ -0,0 +1,43 @@
// gomuks - A Matrix client written in Go.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import { useEffect, useState } from "react"
import ResizeHandle, { ResizeHandleProps } from "./ResizeHandle.tsx"
export const useResizeHandle = (
defaultSize: number,
minWidth: number,
maxWidth: number,
localStorageKey: string,
extraProps: Partial<ResizeHandleProps>,
) => {
const [width, setWidth] = useState(() => {
const savedWidth = localStorage.getItem(localStorageKey)
if (savedWidth) {
return Number(savedWidth)
}
return defaultSize
})
useEffect(() => {
localStorage.setItem(localStorageKey, width.toString())
}, [localStorageKey, width])
return [width, <ResizeHandle
width={width}
minWidth={minWidth}
maxWidth={maxWidth}
setWidth={setWidth}
{...extraProps}
/>] as const
}