chore: app -> desktop
This commit is contained in:
217
packages/desktop/src/components/resizeable-pane.tsx
Normal file
217
packages/desktop/src/components/resizeable-pane.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
import { batch, createContext, createMemo, createSignal, onCleanup, Show, useContext } from "solid-js"
|
||||
import type { ComponentProps, JSX } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
import { useLocal } from "@/context"
|
||||
|
||||
type PaneDefault = number | { size: number; visible?: boolean }
|
||||
|
||||
type LayoutContextValue = {
|
||||
id: string
|
||||
register: (pane: string, options: { min?: number | string; max?: number | string }) => void
|
||||
size: (pane: string) => number
|
||||
visible: (pane: string) => boolean
|
||||
percent: (pane: string) => number
|
||||
next: (pane: string) => string | undefined
|
||||
startDrag: (left: string, right: string | undefined, event: MouseEvent) => void
|
||||
dragging: () => string | undefined
|
||||
}
|
||||
|
||||
const LayoutContext = createContext<LayoutContextValue | undefined>(undefined)
|
||||
|
||||
export interface ResizeableLayoutProps {
|
||||
id: string
|
||||
defaults: Record<string, PaneDefault>
|
||||
class?: ComponentProps<"div">["class"]
|
||||
classList?: ComponentProps<"div">["classList"]
|
||||
children: JSX.Element
|
||||
}
|
||||
|
||||
export interface ResizeablePaneProps {
|
||||
id: string
|
||||
minSize?: number | string
|
||||
maxSize?: number | string
|
||||
class?: ComponentProps<"div">["class"]
|
||||
classList?: ComponentProps<"div">["classList"]
|
||||
children: JSX.Element
|
||||
}
|
||||
|
||||
export function ResizeableLayout(props: ResizeableLayoutProps) {
|
||||
const local = useLocal()
|
||||
const [meta, setMeta] = createStore<Record<string, { min: number; max: number; minPx?: number; maxPx?: number }>>({})
|
||||
const [dragging, setDragging] = createSignal<string>()
|
||||
let container: HTMLDivElement | undefined
|
||||
|
||||
local.layout.ensure(props.id, props.defaults)
|
||||
|
||||
const order = createMemo(() => local.layout.order(props.id))
|
||||
const visibleOrder = createMemo(() => order().filter((pane) => local.layout.visible(props.id, pane)))
|
||||
const totalVisible = createMemo(() => {
|
||||
const panes = visibleOrder()
|
||||
if (!panes.length) return 0
|
||||
return panes.reduce((total, pane) => total + local.layout.size(props.id, pane), 0)
|
||||
})
|
||||
|
||||
const percent = (pane: string) => {
|
||||
const panes = visibleOrder()
|
||||
if (!panes.length) return 0
|
||||
const total = totalVisible()
|
||||
if (!total) return 100 / panes.length
|
||||
return (local.layout.size(props.id, pane) / total) * 100
|
||||
}
|
||||
|
||||
const nextPane = (pane: string) => {
|
||||
const panes = visibleOrder()
|
||||
const index = panes.indexOf(pane)
|
||||
if (index === -1) return undefined
|
||||
return panes[index + 1]
|
||||
}
|
||||
|
||||
const minMax = (pane: string) => meta[pane] ?? { min: 5, max: 95 }
|
||||
|
||||
const pxToPercent = (px: number, total: number) => (px / total) * 100
|
||||
|
||||
const boundsForPair = (left: string, right: string, total: number) => {
|
||||
const leftMeta = minMax(left)
|
||||
const rightMeta = minMax(right)
|
||||
const containerWidth = container?.getBoundingClientRect().width ?? 0
|
||||
|
||||
let minLeft = leftMeta.min
|
||||
let maxLeft = leftMeta.max
|
||||
let minRight = rightMeta.min
|
||||
let maxRight = rightMeta.max
|
||||
|
||||
if (containerWidth && leftMeta.minPx !== undefined) minLeft = pxToPercent(leftMeta.minPx, containerWidth)
|
||||
if (containerWidth && leftMeta.maxPx !== undefined) maxLeft = pxToPercent(leftMeta.maxPx, containerWidth)
|
||||
if (containerWidth && rightMeta.minPx !== undefined) minRight = pxToPercent(rightMeta.minPx, containerWidth)
|
||||
if (containerWidth && rightMeta.maxPx !== undefined) maxRight = pxToPercent(rightMeta.maxPx, containerWidth)
|
||||
|
||||
const finalMinLeft = Math.max(minLeft, total - maxRight)
|
||||
const finalMaxLeft = Math.min(maxLeft, total - minRight)
|
||||
return {
|
||||
min: Math.min(finalMinLeft, finalMaxLeft),
|
||||
max: Math.max(finalMinLeft, finalMaxLeft),
|
||||
}
|
||||
}
|
||||
|
||||
const setPair = (left: string, right: string, leftSize: number, rightSize: number) => {
|
||||
batch(() => {
|
||||
local.layout.setSize(props.id, left, leftSize)
|
||||
local.layout.setSize(props.id, right, rightSize)
|
||||
})
|
||||
}
|
||||
|
||||
const startDrag = (left: string, right: string | undefined, event: MouseEvent) => {
|
||||
if (!right) return
|
||||
if (!container) return
|
||||
const rect = container.getBoundingClientRect()
|
||||
if (!rect.width) return
|
||||
event.preventDefault()
|
||||
const startX = event.clientX
|
||||
const startLeft = local.layout.size(props.id, left)
|
||||
const startRight = local.layout.size(props.id, right)
|
||||
const total = startLeft + startRight
|
||||
const bounds = boundsForPair(left, right, total)
|
||||
const move = (moveEvent: MouseEvent) => {
|
||||
const delta = ((moveEvent.clientX - startX) / rect.width) * 100
|
||||
const nextLeft = Math.max(bounds.min, Math.min(bounds.max, startLeft + delta))
|
||||
const nextRight = total - nextLeft
|
||||
setPair(left, right, nextLeft, nextRight)
|
||||
}
|
||||
const stop = () => {
|
||||
setDragging()
|
||||
document.removeEventListener("mousemove", move)
|
||||
document.removeEventListener("mouseup", stop)
|
||||
}
|
||||
setDragging(left)
|
||||
document.addEventListener("mousemove", move)
|
||||
document.addEventListener("mouseup", stop)
|
||||
onCleanup(() => stop())
|
||||
}
|
||||
|
||||
const register = (pane: string, options: { min?: number | string; max?: number | string }) => {
|
||||
let min = 5
|
||||
let max = 95
|
||||
let minPx: number | undefined
|
||||
let maxPx: number | undefined
|
||||
|
||||
if (typeof options.min === "string" && options.min.endsWith("px")) {
|
||||
minPx = parseInt(options.min)
|
||||
min = 0
|
||||
} else if (typeof options.min === "number") {
|
||||
min = options.min
|
||||
}
|
||||
|
||||
if (typeof options.max === "string" && options.max.endsWith("px")) {
|
||||
maxPx = parseInt(options.max)
|
||||
max = 100
|
||||
} else if (typeof options.max === "number") {
|
||||
max = options.max
|
||||
}
|
||||
|
||||
setMeta(pane, () => ({ min, max, minPx, maxPx }))
|
||||
const fallback = props.defaults[pane]
|
||||
local.layout.ensurePane(props.id, pane, fallback ?? { size: min, visible: true })
|
||||
}
|
||||
|
||||
const contextValue: LayoutContextValue = {
|
||||
id: props.id,
|
||||
register,
|
||||
size: (pane) => local.layout.size(props.id, pane),
|
||||
visible: (pane) => local.layout.visible(props.id, pane),
|
||||
percent,
|
||||
next: nextPane,
|
||||
startDrag,
|
||||
dragging,
|
||||
}
|
||||
|
||||
return (
|
||||
<LayoutContext.Provider value={contextValue}>
|
||||
<div
|
||||
ref={(node) => {
|
||||
container = node ?? undefined
|
||||
}}
|
||||
class={props.class ? `relative flex h-full w-full ${props.class}` : "relative flex h-full w-full"}
|
||||
classList={props.classList}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
</LayoutContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function ResizeablePane(props: ResizeablePaneProps) {
|
||||
const context = useContext(LayoutContext)!
|
||||
context.register(props.id, { min: props.minSize, max: props.maxSize })
|
||||
const visible = () => context.visible(props.id)
|
||||
const width = () => context.percent(props.id)
|
||||
const next = () => context.next(props.id)
|
||||
const dragging = () => context.dragging() === props.id
|
||||
|
||||
return (
|
||||
<Show when={visible()}>
|
||||
<div
|
||||
class={props.class ? `relative flex h-full flex-col ${props.class}` : "relative flex h-full flex-col"}
|
||||
classList={props.classList}
|
||||
style={{
|
||||
width: `${width()}%`,
|
||||
flex: `0 0 ${width()}%`,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
<Show when={next()}>
|
||||
<div
|
||||
class="absolute top-0 -right-1 h-full w-1.5 cursor-col-resize z-50 group"
|
||||
onMouseDown={(event) => context.startDrag(props.id, next(), event)}
|
||||
>
|
||||
<div
|
||||
classList={{
|
||||
"w-0.5 h-full bg-transparent transition-colors group-hover:bg-border-active": true,
|
||||
"bg-border-active!": dragging(),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user