Files
tolaria/src/components/aiWorkspaceSizing.ts
2026-05-30 21:10:41 +02:00

132 lines
4.5 KiB
TypeScript

import { useCallback, useEffect, useRef, useState, type CSSProperties } from 'react'
import { cn } from '@/lib/utils'
export type AiWorkspaceMode = 'docked' | 'side' | 'window'
export interface AiWorkspaceSizing {
onSidebarResize: (delta: number) => void
onWorkspaceResize: (deltaWidth: number, deltaHeight: number) => void
sidebarWidth: number
workspaceSize: { height: number; width: number }
}
const DEFAULT_DOCKED_WORKSPACE_SIZE = { height: 540, width: 560 }
const MIN_DOCKED_WORKSPACE_SIZE = { height: 360, width: 460 }
const DEFAULT_SIDE_WORKSPACE_WIDTH = 320
const MIN_SIDE_WORKSPACE_WIDTH = 320
const SIDE_WORKSPACE_WIDTH_STORAGE_KEY = 'tolaria:ai-workspace-side-width'
const DEFAULT_SIDEBAR_WIDTH = 168
const MIN_SIDEBAR_WIDTH = 132
const MAX_SIDEBAR_WIDTH = 240
function clampNumber(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max)
}
function maxDockedWorkspaceSize(): { height: number; width: number } {
if (typeof window === 'undefined') return { height: 680, width: 880 }
return {
height: Math.max(MIN_DOCKED_WORKSPACE_SIZE.height, window.innerHeight - 88),
width: Math.max(MIN_DOCKED_WORKSPACE_SIZE.width, window.innerWidth - 32),
}
}
function readStoredSideWorkspaceWidth(): number {
if (typeof localStorage === 'undefined') return DEFAULT_SIDE_WORKSPACE_WIDTH
try {
const parsed = Number(localStorage.getItem(SIDE_WORKSPACE_WIDTH_STORAGE_KEY))
if (!Number.isFinite(parsed)) return DEFAULT_SIDE_WORKSPACE_WIDTH
return clampNumber(parsed, MIN_SIDE_WORKSPACE_WIDTH, maxDockedWorkspaceSize().width)
} catch {
return DEFAULT_SIDE_WORKSPACE_WIDTH
}
}
function writeStoredSideWorkspaceWidth(width: number): void {
if (typeof localStorage === 'undefined') return
try {
localStorage.setItem(SIDE_WORKSPACE_WIDTH_STORAGE_KEY, String(width))
} catch {
// Ignore unavailable or restricted localStorage implementations.
}
}
export function workspaceClassName(mode: AiWorkspaceMode, expanded = false): string {
if (mode === 'side') {
return cn(
'z-20 flex h-full min-h-0 overflow-hidden border-l border-sidebar-border bg-sidebar text-sidebar-foreground',
expanded ? 'absolute inset-0 border-l-0' : 'relative shrink-0',
)
}
if (mode === 'window') {
return 'flex h-full w-full overflow-hidden bg-background text-foreground'
}
return 'fixed right-4 bottom-[30px] z-40 flex overflow-hidden rounded-lg border border-border bg-background text-foreground'
}
export function workspaceStyle(
mode: AiWorkspaceMode,
size: AiWorkspaceSizing['workspaceSize'],
expanded = false,
): CSSProperties | undefined {
if (mode === 'window') return undefined
if (mode === 'side') {
if (expanded) return undefined
return {
minWidth: MIN_SIDE_WORKSPACE_WIDTH,
width: size.width,
}
}
return {
height: size.height,
maxHeight: 'calc(100vh - 62px)',
maxWidth: 'calc(100vw - 32px)',
minHeight: MIN_DOCKED_WORKSPACE_SIZE.height,
minWidth: MIN_DOCKED_WORKSPACE_SIZE.width,
width: size.width,
}
}
export function useAiWorkspaceSizing(mode: AiWorkspaceMode): AiWorkspaceSizing {
const [workspaceSize, setWorkspaceSize] = useState(() => (
mode === 'side'
? { height: DEFAULT_DOCKED_WORKSPACE_SIZE.height, width: readStoredSideWorkspaceWidth() }
: DEFAULT_DOCKED_WORKSPACE_SIZE
))
const workspaceSizeRef = useRef(workspaceSize)
const [sidebarWidth, setSidebarWidth] = useState(DEFAULT_SIDEBAR_WIDTH)
const onWorkspaceResize = useCallback((deltaWidth: number, deltaHeight: number) => {
if (mode === 'window') return
const current = workspaceSizeRef.current
const max = maxDockedWorkspaceSize()
const minWidth = mode === 'side' ? MIN_SIDE_WORKSPACE_WIDTH : MIN_DOCKED_WORKSPACE_SIZE.width
const next = {
height: clampNumber(current.height + deltaHeight, MIN_DOCKED_WORKSPACE_SIZE.height, max.height),
width: clampNumber(current.width + deltaWidth, minWidth, max.width),
}
workspaceSizeRef.current = next
if (mode === 'side') writeStoredSideWorkspaceWidth(next.width)
setWorkspaceSize(next)
}, [mode])
const onSidebarResize = useCallback((delta: number) => {
setSidebarWidth((current) => clampNumber(current + delta, MIN_SIDEBAR_WIDTH, MAX_SIDEBAR_WIDTH))
}, [])
useEffect(() => {
workspaceSizeRef.current = workspaceSize
}, [workspaceSize])
useEffect(() => {
if (mode === 'side') writeStoredSideWorkspaceWidth(workspaceSize.width)
}, [mode, workspaceSize.width])
return { onSidebarResize, onWorkspaceResize, sidebarWidth, workspaceSize }
}