132 lines
4.5 KiB
TypeScript
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 }
|
||
|
|
}
|