mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-06-06 19:35:44 +02:00
Pane placement (#598)
* allow user to change pane placement * allow user to change starting pane size
This commit is contained in:
parent
97c8f9d787
commit
7f3c16cc33
4 changed files with 144 additions and 17 deletions
|
|
@ -5,7 +5,7 @@ import { RunEvent, ListRunsResponse } from '@x/shared/src/runs.js';
|
||||||
import type { LanguageModelUsage, ToolUIPart } from 'ai';
|
import type { LanguageModelUsage, ToolUIPart } from 'ai';
|
||||||
import './App.css'
|
import './App.css'
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
import { CheckIcon, LoaderIcon, PanelLeftIcon, ArrowRight, MessageSquare, ChevronLeftIcon, ChevronRightIcon, Plus, HistoryIcon } from 'lucide-react';
|
import { CheckIcon, LoaderIcon, PanelLeftIcon, ArrowLeft, ArrowRight, MessageSquare, ChevronLeftIcon, ChevronRightIcon, Plus, HistoryIcon } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { MarkdownEditor, type MarkdownEditorHandle } from './components/markdown-editor';
|
import { MarkdownEditor, type MarkdownEditorHandle } from './components/markdown-editor';
|
||||||
import { ChatSidebar } from './components/chat-sidebar';
|
import { ChatSidebar } from './components/chat-sidebar';
|
||||||
|
|
@ -117,6 +117,7 @@ import { useVoiceTTS } from '@/hooks/useVoiceTTS'
|
||||||
import { useMeetingTranscription, type CalendarEventMeta } from '@/hooks/useMeetingTranscription'
|
import { useMeetingTranscription, type CalendarEventMeta } from '@/hooks/useMeetingTranscription'
|
||||||
import { useAnalyticsIdentity } from '@/hooks/useAnalyticsIdentity'
|
import { useAnalyticsIdentity } from '@/hooks/useAnalyticsIdentity'
|
||||||
import * as analytics from '@/lib/analytics'
|
import * as analytics from '@/lib/analytics'
|
||||||
|
import { useTheme } from '@/contexts/theme-context'
|
||||||
|
|
||||||
type DirEntry = z.infer<typeof workspace.DirEntry>
|
type DirEntry = z.infer<typeof workspace.DirEntry>
|
||||||
type RunEventType = z.infer<typeof RunEvent>
|
type RunEventType = z.infer<typeof RunEvent>
|
||||||
|
|
@ -165,6 +166,7 @@ function AutoScrollPre({ className, children }: { className?: string; children:
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_SIDEBAR_WIDTH = 256
|
const DEFAULT_SIDEBAR_WIDTH = 256
|
||||||
|
const DEFAULT_CHAT_PANE_WIDTH = 460
|
||||||
const wikiLinkRegex = /\[\[([^[\]]+)\]\]/g
|
const wikiLinkRegex = /\[\[([^[\]]+)\]\]/g
|
||||||
const graphPalette = [
|
const graphPalette = [
|
||||||
{ hue: 210, sat: 72, light: 52 },
|
{ hue: 210, sat: 72, light: 52 },
|
||||||
|
|
@ -736,6 +738,9 @@ function ContentHeader({
|
||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const { chatPanePlacement, chatPaneSize } = useTheme()
|
||||||
|
const isChatPaneInMiddle = chatPanePlacement === 'middle'
|
||||||
|
|
||||||
type ShortcutPane = 'left' | 'right'
|
type ShortcutPane = 'left' | 'right'
|
||||||
type MarkdownHistoryHandlers = { undo: () => boolean; redo: () => boolean }
|
type MarkdownHistoryHandlers = { undo: () => boolean; redo: () => boolean }
|
||||||
|
|
||||||
|
|
@ -765,7 +770,7 @@ function App() {
|
||||||
// Lives in ViewState so folder drill-down participates in back/forward history.
|
// Lives in ViewState so folder drill-down participates in back/forward history.
|
||||||
const [knowledgeViewFolderPath, setKnowledgeViewFolderPath] = useState<string | null>(null)
|
const [knowledgeViewFolderPath, setKnowledgeViewFolderPath] = useState<string | null>(null)
|
||||||
const [isChatHistoryOpen, setIsChatHistoryOpen] = useState(false)
|
const [isChatHistoryOpen, setIsChatHistoryOpen] = useState(false)
|
||||||
// Default landing view: Home in the middle with the chat docked on the right.
|
// Default landing view: Home with the chat docked according to appearance settings.
|
||||||
const [isHomeOpen, setIsHomeOpen] = useState(true)
|
const [isHomeOpen, setIsHomeOpen] = useState(true)
|
||||||
const [emailInitialThreadId, setEmailInitialThreadId] = useState<string | null>(null)
|
const [emailInitialThreadId, setEmailInitialThreadId] = useState<string | null>(null)
|
||||||
const [emailThreadIdVersion, setEmailThreadIdVersion] = useState(0)
|
const [emailThreadIdVersion, setEmailThreadIdVersion] = useState(0)
|
||||||
|
|
@ -5246,6 +5251,17 @@ function App() {
|
||||||
const isRightPaneContext = Boolean(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen || isBrowserOpen)
|
const isRightPaneContext = Boolean(selectedPath || isGraphOpen || isSuggestedTopicsOpen || isMeetingsOpen || isLiveNotesOpen || isBgTasksOpen || isEmailOpen || isWorkspaceOpen || isKnowledgeViewOpen || isChatHistoryOpen || isHomeOpen || isBrowserOpen)
|
||||||
const isRightPaneOnlyMode = isRightPaneContext && isChatSidebarOpen && isRightPaneMaximized
|
const isRightPaneOnlyMode = isRightPaneContext && isChatSidebarOpen && isRightPaneMaximized
|
||||||
const shouldCollapseLeftPane = isRightPaneOnlyMode
|
const shouldCollapseLeftPane = isRightPaneOnlyMode
|
||||||
|
const nonChatPaneStyle = React.useMemo<React.CSSProperties>(() => {
|
||||||
|
const style: React.CSSProperties = { maxWidth: insetMaxWidth }
|
||||||
|
if (!isRightPaneContext || !isChatSidebarOpen || isRightPaneMaximized) return style
|
||||||
|
if (chatPaneSize === 'chat-equal') {
|
||||||
|
return { ...style, width: 0, flex: '1 1 0' }
|
||||||
|
}
|
||||||
|
if (chatPaneSize === 'chat-bigger') {
|
||||||
|
return { ...style, width: DEFAULT_CHAT_PANE_WIDTH, flex: '0 0 auto' }
|
||||||
|
}
|
||||||
|
return style
|
||||||
|
}, [chatPaneSize, insetMaxWidth, isChatSidebarOpen, isRightPaneContext, isRightPaneMaximized])
|
||||||
// Collapsing: pin max-width to the snapshot px (no transition) for one frame so it's
|
// Collapsing: pin max-width to the snapshot px (no transition) for one frame so it's
|
||||||
// binding immediately (no flex jump), then animate to 0. Expanding goes back to 100%
|
// binding immediately (no flex jump), then animate to 0. Expanding goes back to 100%
|
||||||
// — its non-binding range lands at the end of the range, where it isn't visible.
|
// — its non-binding range lands at the end of the range, where it isn't visible.
|
||||||
|
|
@ -5323,10 +5339,11 @@ function App() {
|
||||||
<SidebarInset
|
<SidebarInset
|
||||||
className={cn(
|
className={cn(
|
||||||
"overflow-hidden! min-h-0 min-w-0",
|
"overflow-hidden! min-h-0 min-w-0",
|
||||||
|
isRightPaneContext && isChatPaneInMiddle && "order-3",
|
||||||
insetAnimateMaxWidth && "transition-[max-width] duration-200 ease-linear",
|
insetAnimateMaxWidth && "transition-[max-width] duration-200 ease-linear",
|
||||||
shouldCollapseLeftPane && "pointer-events-none select-none"
|
shouldCollapseLeftPane && "pointer-events-none select-none"
|
||||||
)}
|
)}
|
||||||
style={{ maxWidth: insetMaxWidth }}
|
style={nonChatPaneStyle}
|
||||||
aria-hidden={shouldCollapseLeftPane}
|
aria-hidden={shouldCollapseLeftPane}
|
||||||
onMouseDownCapture={() => setActiveShortcutPane('left')}
|
onMouseDownCapture={() => setActiveShortcutPane('left')}
|
||||||
onFocusCapture={() => setActiveShortcutPane('left')}
|
onFocusCapture={() => setActiveShortcutPane('left')}
|
||||||
|
|
@ -5438,7 +5455,11 @@ function App() {
|
||||||
: (viewOpen && !isChatSidebarOpen)
|
: (viewOpen && !isChatSidebarOpen)
|
||||||
? { onClick: openChatSidePane, icon: <MessageSquare className="size-5" />, label: 'Open chat' }
|
? { onClick: openChatSidePane, icon: <MessageSquare className="size-5" />, label: 'Open chat' }
|
||||||
: (viewOpen && isChatSidebarOpen && !isRightPaneMaximized)
|
: (viewOpen && isChatSidebarOpen && !isRightPaneMaximized)
|
||||||
? { onClick: () => setIsChatSidebarOpen(false), icon: <ArrowRight className="size-5" />, label: 'Expand pane' }
|
? {
|
||||||
|
onClick: () => setIsChatSidebarOpen(false),
|
||||||
|
icon: isChatPaneInMiddle ? <ArrowLeft className="size-5" /> : <ArrowRight className="size-5" />,
|
||||||
|
label: 'Expand pane'
|
||||||
|
}
|
||||||
: null
|
: null
|
||||||
return (
|
return (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
|
|
@ -5989,10 +6010,13 @@ function App() {
|
||||||
)}
|
)}
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
|
|
||||||
{/* Chat sidebar - shown when viewing files/graph */}
|
{/* Chat pane - shown when viewing files/graph */}
|
||||||
{isRightPaneContext && (
|
{isRightPaneContext && (
|
||||||
<ChatSidebar
|
<ChatSidebar
|
||||||
defaultWidth={460}
|
placement={chatPanePlacement}
|
||||||
|
paneSize={chatPaneSize}
|
||||||
|
className={isChatPaneInMiddle ? "order-2" : undefined}
|
||||||
|
defaultWidth={DEFAULT_CHAT_PANE_WIDTH}
|
||||||
isOpen={isChatSidebarOpen}
|
isOpen={isChatSidebarOpen}
|
||||||
isMaximized={isRightPaneMaximized}
|
isMaximized={isRightPaneMaximized}
|
||||||
chatTabs={chatTabs}
|
chatTabs={chatTabs}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ import { ChatInputWithMentions, type PermissionMode, type StagedAttachment, type
|
||||||
import { ChatMessageAttachments } from '@/components/chat-message-attachments'
|
import { ChatMessageAttachments } from '@/components/chat-message-attachments'
|
||||||
import { useSidebar } from '@/components/ui/sidebar'
|
import { useSidebar } from '@/components/ui/sidebar'
|
||||||
import { wikiLabel } from '@/lib/wiki-links'
|
import { wikiLabel } from '@/lib/wiki-links'
|
||||||
|
import type { ChatPaneSize } from '@/contexts/theme-context'
|
||||||
import {
|
import {
|
||||||
type ChatViewportAnchorState,
|
type ChatViewportAnchorState,
|
||||||
type ChatTabViewState,
|
type ChatTabViewState,
|
||||||
|
|
@ -125,6 +126,9 @@ interface ChatSidebarProps {
|
||||||
defaultWidth?: number
|
defaultWidth?: number
|
||||||
isOpen?: boolean
|
isOpen?: boolean
|
||||||
isMaximized?: boolean
|
isMaximized?: boolean
|
||||||
|
placement?: 'middle' | 'right'
|
||||||
|
paneSize?: ChatPaneSize
|
||||||
|
className?: string
|
||||||
chatTabs: ChatTab[]
|
chatTabs: ChatTab[]
|
||||||
activeChatTabId: string
|
activeChatTabId: string
|
||||||
getChatTabTitle: (tab: ChatTab) => string
|
getChatTabTitle: (tab: ChatTab) => string
|
||||||
|
|
@ -183,6 +187,9 @@ export function ChatSidebar({
|
||||||
defaultWidth = DEFAULT_WIDTH,
|
defaultWidth = DEFAULT_WIDTH,
|
||||||
isOpen = true,
|
isOpen = true,
|
||||||
isMaximized = false,
|
isMaximized = false,
|
||||||
|
placement = 'right',
|
||||||
|
paneSize = 'chat-smaller',
|
||||||
|
className,
|
||||||
chatTabs,
|
chatTabs,
|
||||||
activeChatTabId,
|
activeChatTabId,
|
||||||
getChatTabTitle,
|
getChatTabTitle,
|
||||||
|
|
@ -246,6 +253,8 @@ export function ChatSidebar({
|
||||||
const startWidthRef = useRef(0)
|
const startWidthRef = useRef(0)
|
||||||
const prevIsMaximizedRef = useRef(isMaximized)
|
const prevIsMaximizedRef = useRef(isMaximized)
|
||||||
const justToggledMaximize = prevIsMaximizedRef.current !== isMaximized
|
const justToggledMaximize = prevIsMaximizedRef.current !== isMaximized
|
||||||
|
const isMiddlePlacement = placement === 'middle'
|
||||||
|
const isResizable = paneSize === 'chat-smaller'
|
||||||
|
|
||||||
const getMaxAllowedWidth = useCallback(() => {
|
const getMaxAllowedWidth = useCallback(() => {
|
||||||
if (typeof window === 'undefined') return MAX_WIDTH
|
if (typeof window === 'undefined') return MAX_WIDTH
|
||||||
|
|
@ -306,7 +315,9 @@ export function ChatSidebar({
|
||||||
setIsResizing(true)
|
setIsResizing(true)
|
||||||
|
|
||||||
const handleMouseMove = (event: MouseEvent) => {
|
const handleMouseMove = (event: MouseEvent) => {
|
||||||
const delta = startXRef.current - event.clientX
|
const delta = isMiddlePlacement
|
||||||
|
? event.clientX - startXRef.current
|
||||||
|
: startXRef.current - event.clientX
|
||||||
const maxAllowedWidth = getMaxAllowedWidth()
|
const maxAllowedWidth = getMaxAllowedWidth()
|
||||||
setWidth(clampPaneWidth(startWidthRef.current + delta, maxAllowedWidth))
|
setWidth(clampPaneWidth(startWidthRef.current + delta, maxAllowedWidth))
|
||||||
}
|
}
|
||||||
|
|
@ -319,7 +330,7 @@ export function ChatSidebar({
|
||||||
|
|
||||||
document.addEventListener('mousemove', handleMouseMove)
|
document.addEventListener('mousemove', handleMouseMove)
|
||||||
document.addEventListener('mouseup', handleMouseUp)
|
document.addEventListener('mouseup', handleMouseUp)
|
||||||
}, [width, getMaxAllowedWidth])
|
}, [width, getMaxAllowedWidth, isMiddlePlacement])
|
||||||
|
|
||||||
const activeTabState = useMemo<ChatTabViewState>(() => ({
|
const activeTabState = useMemo<ChatTabViewState>(() => ({
|
||||||
runId: runId ?? null,
|
runId: runId ?? null,
|
||||||
|
|
@ -501,8 +512,11 @@ export function ChatSidebar({
|
||||||
// not add extra width to the right and overflow the app viewport.
|
// not add extra width to the right and overflow the app viewport.
|
||||||
return { width: 0, flex: '1 1 auto' }
|
return { width: 0, flex: '1 1 auto' }
|
||||||
}
|
}
|
||||||
|
if (paneSize === 'chat-equal' || paneSize === 'chat-bigger') {
|
||||||
|
return { width: 0, flex: '1 1 0' }
|
||||||
|
}
|
||||||
return { width, flex: '0 0 auto' }
|
return { width, flex: '0 0 auto' }
|
||||||
}, [isOpen, isMaximized, width])
|
}, [isOpen, isMaximized, paneSize, width])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -511,16 +525,19 @@ export function ChatSidebar({
|
||||||
onMouseDownCapture={onActivate}
|
onMouseDownCapture={onActivate}
|
||||||
onFocusCapture={onActivate}
|
onFocusCapture={onActivate}
|
||||||
className={cn(
|
className={cn(
|
||||||
'relative flex min-w-0 flex-col overflow-hidden border-l border-border bg-background',
|
'relative flex min-w-0 flex-col overflow-hidden bg-background',
|
||||||
!isResizing && !justToggledMaximize && 'transition-[width] duration-200 ease-linear'
|
isMiddlePlacement ? 'border-r border-border' : 'border-l border-border',
|
||||||
|
!isResizing && !justToggledMaximize && 'transition-[width] duration-200 ease-linear',
|
||||||
|
className
|
||||||
)}
|
)}
|
||||||
style={paneStyle}
|
style={paneStyle}
|
||||||
>
|
>
|
||||||
{!isMaximized && (
|
{!isMaximized && isResizable && (
|
||||||
<div
|
<div
|
||||||
onMouseDown={handleMouseDown}
|
onMouseDown={handleMouseDown}
|
||||||
className={cn(
|
className={cn(
|
||||||
'absolute inset-y-0 left-0 z-20 w-4 -translate-x-1/2 cursor-col-resize',
|
'absolute inset-y-0 z-20 w-4 cursor-col-resize',
|
||||||
|
isMiddlePlacement ? 'right-0 translate-x-1/2' : 'left-0 -translate-x-1/2',
|
||||||
'after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] after:transition-colors',
|
'after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] after:transition-colors',
|
||||||
'hover:after:bg-sidebar-border',
|
'hover:after:bg-sidebar-border',
|
||||||
isResizing && 'after:bg-primary'
|
isResizing && 'after:bg-primary'
|
||||||
|
|
@ -587,7 +604,9 @@ export function ChatSidebar({
|
||||||
className="titlebar-no-drag my-1 mr-2 h-8 w-8 shrink-0 text-muted-foreground hover:text-foreground"
|
className="titlebar-no-drag my-1 mr-2 h-8 w-8 shrink-0 text-muted-foreground hover:text-foreground"
|
||||||
aria-label={isMaximized ? 'Dock chat to side pane' : 'Expand chat'}
|
aria-label={isMaximized ? 'Dock chat to side pane' : 'Expand chat'}
|
||||||
>
|
>
|
||||||
{isMaximized ? <ArrowRight className="size-5" /> : <ArrowLeft className="size-5" />}
|
{isMaximized
|
||||||
|
? (isMiddlePlacement ? <ArrowLeft className="size-5" /> : <ArrowRight className="size-5" />)
|
||||||
|
: (isMiddlePlacement ? <ArrowRight className="size-5" /> : <ArrowLeft className="size-5" />)}
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="bottom">{isMaximized ? 'Dock to side pane' : 'Expand chat'}</TooltipContent>
|
<TooltipContent side="bottom">{isMaximized ? 'Dock to side pane' : 'Expand chat'}</TooltipContent>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { useState, useEffect, useCallback, useMemo } from "react"
|
import { useState, useEffect, useCallback, useMemo } from "react"
|
||||||
import { Server, Key, Shield, Palette, Monitor, Sun, Moon, Loader2, CheckCircle2, Plus, X, Wrench, Search, ChevronRight, Link2, Tags, Mail, BookOpen, User, Plug, HelpCircle, MessageCircle, Bug, Terminal, AlertTriangle, RefreshCw } from "lucide-react"
|
import { Server, Key, Shield, Palette, Monitor, Sun, Moon, Loader2, CheckCircle2, Plus, X, Wrench, Search, ChevronRight, Link2, Tags, Mail, BookOpen, User, Plug, HelpCircle, MessageCircle, Bug, Terminal, AlertTriangle, RefreshCw, PanelRight } from "lucide-react"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
|
|
@ -210,7 +210,7 @@ function ThemeOption({
|
||||||
}
|
}
|
||||||
|
|
||||||
function AppearanceSettings() {
|
function AppearanceSettings() {
|
||||||
const { theme, setTheme } = useTheme()
|
const { theme, setTheme, chatPanePlacement, setChatPanePlacement, chatPaneSize, setChatPaneSize } = useTheme()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|
@ -240,6 +240,50 @@ function AppearanceSettings() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium mb-3">Chat</h4>
|
||||||
|
<p className="text-xs text-muted-foreground mb-4">
|
||||||
|
Choose where chat sits when another pane is open
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<ThemeOption
|
||||||
|
label="Chat right"
|
||||||
|
icon={PanelRight}
|
||||||
|
isSelected={chatPanePlacement === "right"}
|
||||||
|
onClick={() => setChatPanePlacement("right")}
|
||||||
|
/>
|
||||||
|
<ThemeOption
|
||||||
|
label="Chat middle"
|
||||||
|
icon={MessageCircle}
|
||||||
|
isSelected={chatPanePlacement === "middle"}
|
||||||
|
onClick={() => setChatPanePlacement("middle")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h4 className="mt-6 text-sm font-medium mb-3">Chat size</h4>
|
||||||
|
<p className="text-xs text-muted-foreground mb-4">
|
||||||
|
Choose how much width chat gets when another pane is open
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
<ThemeOption
|
||||||
|
label="Chat smaller"
|
||||||
|
icon={MessageCircle}
|
||||||
|
isSelected={chatPaneSize === "chat-smaller"}
|
||||||
|
onClick={() => setChatPaneSize("chat-smaller")}
|
||||||
|
/>
|
||||||
|
<ThemeOption
|
||||||
|
label="Chat equal"
|
||||||
|
icon={Monitor}
|
||||||
|
isSelected={chatPaneSize === "chat-equal"}
|
||||||
|
onClick={() => setChatPaneSize("chat-equal")}
|
||||||
|
/>
|
||||||
|
<ThemeOption
|
||||||
|
label="Chat bigger"
|
||||||
|
icon={PanelRight}
|
||||||
|
isSelected={chatPaneSize === "chat-bigger"}
|
||||||
|
onClick={() => setChatPaneSize("chat-bigger")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,16 +3,32 @@
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
export type Theme = "light" | "dark" | "system"
|
export type Theme = "light" | "dark" | "system"
|
||||||
|
export type ChatPanePlacement = "right" | "middle"
|
||||||
|
export type ChatPaneSize = "chat-smaller" | "chat-equal" | "chat-bigger"
|
||||||
|
|
||||||
type ThemeContextProps = {
|
type ThemeContextProps = {
|
||||||
theme: Theme
|
theme: Theme
|
||||||
resolvedTheme: "light" | "dark"
|
resolvedTheme: "light" | "dark"
|
||||||
setTheme: (theme: Theme) => void
|
setTheme: (theme: Theme) => void
|
||||||
|
chatPanePlacement: ChatPanePlacement
|
||||||
|
setChatPanePlacement: (placement: ChatPanePlacement) => void
|
||||||
|
chatPaneSize: ChatPaneSize
|
||||||
|
setChatPaneSize: (size: ChatPaneSize) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ThemeContext = React.createContext<ThemeContextProps | null>(null)
|
const ThemeContext = React.createContext<ThemeContextProps | null>(null)
|
||||||
|
|
||||||
const STORAGE_KEY = "rowboat-theme"
|
const STORAGE_KEY = "rowboat-theme"
|
||||||
|
const CHAT_PANE_PLACEMENT_STORAGE_KEY = "rowboat-chat-pane-placement"
|
||||||
|
const CHAT_PANE_SIZE_STORAGE_KEY = "rowboat-chat-pane-size"
|
||||||
|
|
||||||
|
function isChatPanePlacement(value: string | null): value is ChatPanePlacement {
|
||||||
|
return value === "right" || value === "middle"
|
||||||
|
}
|
||||||
|
|
||||||
|
function isChatPaneSize(value: string | null): value is ChatPaneSize {
|
||||||
|
return value === "chat-smaller" || value === "chat-equal" || value === "chat-bigger"
|
||||||
|
}
|
||||||
|
|
||||||
function getSystemTheme(): "light" | "dark" {
|
function getSystemTheme(): "light" | "dark" {
|
||||||
if (typeof window === "undefined") return "light"
|
if (typeof window === "undefined") return "light"
|
||||||
|
|
@ -39,6 +55,16 @@ export function ThemeProvider({
|
||||||
const stored = localStorage.getItem(STORAGE_KEY) as Theme | null
|
const stored = localStorage.getItem(STORAGE_KEY) as Theme | null
|
||||||
return stored || defaultTheme
|
return stored || defaultTheme
|
||||||
})
|
})
|
||||||
|
const [chatPanePlacement, setChatPanePlacementState] = React.useState<ChatPanePlacement>(() => {
|
||||||
|
if (typeof window === "undefined") return "right"
|
||||||
|
const stored = localStorage.getItem(CHAT_PANE_PLACEMENT_STORAGE_KEY)
|
||||||
|
return isChatPanePlacement(stored) ? stored : "right"
|
||||||
|
})
|
||||||
|
const [chatPaneSize, setChatPaneSizeState] = React.useState<ChatPaneSize>(() => {
|
||||||
|
if (typeof window === "undefined") return "chat-smaller"
|
||||||
|
const stored = localStorage.getItem(CHAT_PANE_SIZE_STORAGE_KEY)
|
||||||
|
return isChatPaneSize(stored) ? stored : "chat-smaller"
|
||||||
|
})
|
||||||
|
|
||||||
const [resolvedTheme, setResolvedTheme] = React.useState<"light" | "dark">(() => {
|
const [resolvedTheme, setResolvedTheme] = React.useState<"light" | "dark">(() => {
|
||||||
if (theme === "system") return getSystemTheme()
|
if (theme === "system") return getSystemTheme()
|
||||||
|
|
@ -76,13 +102,27 @@ export function ThemeProvider({
|
||||||
setThemeState(newTheme)
|
setThemeState(newTheme)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const setChatPanePlacement = React.useCallback((placement: ChatPanePlacement) => {
|
||||||
|
localStorage.setItem(CHAT_PANE_PLACEMENT_STORAGE_KEY, placement)
|
||||||
|
setChatPanePlacementState(placement)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const setChatPaneSize = React.useCallback((size: ChatPaneSize) => {
|
||||||
|
localStorage.setItem(CHAT_PANE_SIZE_STORAGE_KEY, size)
|
||||||
|
setChatPaneSizeState(size)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const contextValue = React.useMemo<ThemeContextProps>(
|
const contextValue = React.useMemo<ThemeContextProps>(
|
||||||
() => ({
|
() => ({
|
||||||
theme,
|
theme,
|
||||||
resolvedTheme,
|
resolvedTheme,
|
||||||
setTheme,
|
setTheme,
|
||||||
|
chatPanePlacement,
|
||||||
|
setChatPanePlacement,
|
||||||
|
chatPaneSize,
|
||||||
|
setChatPaneSize,
|
||||||
}),
|
}),
|
||||||
[theme, resolvedTheme, setTheme]
|
[theme, resolvedTheme, setTheme, chatPanePlacement, setChatPanePlacement, chatPaneSize, setChatPaneSize]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue