refactor: replace session tabs with sectioned list
This commit is contained in:
74
src/App.tsx
74
src/App.tsx
@@ -5,7 +5,7 @@ import FolderSelectionView from "./components/folder-selection-view"
|
||||
import InstanceWelcomeView from "./components/instance-welcome-view"
|
||||
import CommandPalette from "./components/command-palette"
|
||||
import InstanceTabs from "./components/instance-tabs"
|
||||
import SessionTabs from "./components/session-tabs"
|
||||
import SessionList from "./components/session-list"
|
||||
import MessageStream from "./components/message-stream"
|
||||
import PromptInput from "./components/prompt-input"
|
||||
import InfoView from "./components/info-view"
|
||||
@@ -813,42 +813,46 @@ const App: Component = () => {
|
||||
{(instance) => (
|
||||
<>
|
||||
<Show when={activeSessions().size > 0} fallback={<InstanceWelcomeView instance={instance()} />}>
|
||||
<SessionTabs
|
||||
instanceId={instance().id}
|
||||
sessions={activeSessions()}
|
||||
activeSessionId={activeSessionIdForInstance()}
|
||||
onSelect={(id) => setActiveSession(instance().id, id)}
|
||||
onClose={(id) => handleCloseSession(instance().id, id)}
|
||||
onNew={() => handleNewSession(instance().id)}
|
||||
/>
|
||||
<div class="flex h-full">
|
||||
{/* Session List Sidebar */}
|
||||
<SessionList
|
||||
instanceId={instance().id}
|
||||
sessions={activeSessions()}
|
||||
activeSessionId={activeSessionIdForInstance()}
|
||||
onSelect={(id) => setActiveSession(instance().id, id)}
|
||||
onClose={(id) => handleCloseSession(instance().id, id)}
|
||||
onNew={() => handleNewSession(instance().id)}
|
||||
/>
|
||||
|
||||
<div class="content-area flex-1 overflow-hidden flex flex-col">
|
||||
<Show
|
||||
when={activeSessionIdForInstance() === "info"}
|
||||
fallback={
|
||||
<Show
|
||||
when={activeSessionIdForInstance()}
|
||||
fallback={
|
||||
<div class="flex items-center justify-center h-full">
|
||||
<div class="text-center text-gray-500 dark:text-gray-400">
|
||||
<p class="mb-2">No session selected</p>
|
||||
<p class="text-sm">Select a session to view messages</p>
|
||||
{/* Main Content Area */}
|
||||
<div class="content-area flex-1 overflow-hidden flex flex-col">
|
||||
<Show
|
||||
when={activeSessionIdForInstance() === "info"}
|
||||
fallback={
|
||||
<Show
|
||||
when={activeSessionIdForInstance()}
|
||||
fallback={
|
||||
<div class="flex items-center justify-center h-full">
|
||||
<div class="text-center text-gray-500 dark:text-gray-400">
|
||||
<p class="mb-2">No session selected</p>
|
||||
<p class="text-sm">Select a session to view messages</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<SessionView
|
||||
sessionId={activeSessionIdForInstance()!}
|
||||
activeSessions={activeSessions()}
|
||||
instanceId={activeInstance()!.id}
|
||||
instanceFolder={activeInstance()!.folder}
|
||||
escapeInDebounce={escapeInDebounce()}
|
||||
/>
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<InfoView instanceId={instance().id} />
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<SessionView
|
||||
sessionId={activeSessionIdForInstance()!}
|
||||
activeSessions={activeSessions()}
|
||||
instanceId={activeInstance()!.id}
|
||||
instanceFolder={activeInstance()!.folder}
|
||||
escapeInDebounce={escapeInDebounce()}
|
||||
/>
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<InfoView instanceId={instance().id} />
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</>
|
||||
|
||||
307
src/components/session-list.tsx
Normal file
307
src/components/session-list.tsx
Normal file
@@ -0,0 +1,307 @@
|
||||
import { Component, For, Show, createSignal, createEffect, onCleanup, onMount, createMemo } from "solid-js"
|
||||
import type { Session } from "../types/session"
|
||||
import { MessageSquare, Info, Plus, X } from "lucide-solid"
|
||||
import KeyboardHint from "./keyboard-hint"
|
||||
import { keyboardRegistry } from "../lib/keyboard-registry"
|
||||
|
||||
interface SessionListItem {
|
||||
id: string
|
||||
title: string
|
||||
isSpecial?: boolean
|
||||
isActive: boolean
|
||||
isParent?: boolean
|
||||
onSelect: () => void
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
interface SessionListProps {
|
||||
instanceId: string
|
||||
sessions: Map<string, Session>
|
||||
activeSessionId: string | null
|
||||
onSelect: (sessionId: string) => void
|
||||
onClose: (sessionId: string) => void
|
||||
onNew: () => void
|
||||
}
|
||||
|
||||
const MIN_WIDTH = 200
|
||||
const MAX_WIDTH = 500
|
||||
const DEFAULT_WIDTH = 280
|
||||
const STORAGE_KEY = "opencode-session-sidebar-width"
|
||||
|
||||
const SessionList: Component<SessionListProps> = (props) => {
|
||||
const [sidebarWidth, setSidebarWidth] = createSignal(DEFAULT_WIDTH)
|
||||
const [isResizing, setIsResizing] = createSignal(false)
|
||||
const [startX, setStartX] = createSignal(0)
|
||||
const [startWidth, setStartWidth] = createSignal(DEFAULT_WIDTH)
|
||||
|
||||
let mouseMoveHandler: ((event: MouseEvent) => void) | null = null
|
||||
let mouseUpHandler: (() => void) | null = null
|
||||
let touchMoveHandler: ((event: TouchEvent) => void) | null = null
|
||||
let touchEndHandler: (() => void) | null = null
|
||||
|
||||
onMount(() => {
|
||||
if (typeof window === "undefined") return
|
||||
const saved = window.localStorage.getItem(STORAGE_KEY)
|
||||
if (!saved) return
|
||||
|
||||
const width = Number.parseInt(saved, 10)
|
||||
if (Number.isFinite(width) && width >= MIN_WIDTH && width <= MAX_WIDTH) {
|
||||
setSidebarWidth(width)
|
||||
setStartWidth(width)
|
||||
}
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (typeof window === "undefined") return
|
||||
const width = sidebarWidth()
|
||||
window.localStorage.setItem(STORAGE_KEY, width.toString())
|
||||
})
|
||||
|
||||
const clampWidth = (width: number) => Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, width))
|
||||
|
||||
const removeMouseListeners = () => {
|
||||
if (mouseMoveHandler) {
|
||||
document.removeEventListener("mousemove", mouseMoveHandler)
|
||||
mouseMoveHandler = null
|
||||
}
|
||||
if (mouseUpHandler) {
|
||||
document.removeEventListener("mouseup", mouseUpHandler)
|
||||
mouseUpHandler = null
|
||||
}
|
||||
}
|
||||
|
||||
const removeTouchListeners = () => {
|
||||
if (touchMoveHandler) {
|
||||
document.removeEventListener("touchmove", touchMoveHandler)
|
||||
touchMoveHandler = null
|
||||
}
|
||||
if (touchEndHandler) {
|
||||
document.removeEventListener("touchend", touchEndHandler)
|
||||
touchEndHandler = null
|
||||
}
|
||||
}
|
||||
|
||||
const stopResizing = () => {
|
||||
setIsResizing(false)
|
||||
removeMouseListeners()
|
||||
removeTouchListeners()
|
||||
}
|
||||
|
||||
const handleMouseMove = (event: MouseEvent) => {
|
||||
if (!isResizing()) return
|
||||
const diff = event.clientX - startX()
|
||||
const newWidth = clampWidth(startWidth() + diff)
|
||||
setSidebarWidth(newWidth)
|
||||
}
|
||||
|
||||
const handleMouseUp = () => {
|
||||
stopResizing()
|
||||
}
|
||||
|
||||
const handleTouchMove = (event: TouchEvent) => {
|
||||
if (!isResizing()) return
|
||||
const touch = event.touches[0]
|
||||
const diff = touch.clientX - startX()
|
||||
const newWidth = clampWidth(startWidth() + diff)
|
||||
setSidebarWidth(newWidth)
|
||||
}
|
||||
|
||||
const handleTouchEnd = () => {
|
||||
stopResizing()
|
||||
}
|
||||
|
||||
const handleMouseDown = (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
setIsResizing(true)
|
||||
setStartX(event.clientX)
|
||||
setStartWidth(sidebarWidth())
|
||||
|
||||
mouseMoveHandler = handleMouseMove
|
||||
mouseUpHandler = handleMouseUp
|
||||
|
||||
document.addEventListener("mousemove", handleMouseMove)
|
||||
document.addEventListener("mouseup", handleMouseUp)
|
||||
}
|
||||
|
||||
const handleTouchStart = (event: TouchEvent) => {
|
||||
event.preventDefault()
|
||||
const touch = event.touches[0]
|
||||
setIsResizing(true)
|
||||
setStartX(touch.clientX)
|
||||
setStartWidth(sidebarWidth())
|
||||
|
||||
touchMoveHandler = handleTouchMove
|
||||
touchEndHandler = handleTouchEnd
|
||||
|
||||
document.addEventListener("touchmove", handleTouchMove)
|
||||
document.addEventListener("touchend", handleTouchEnd)
|
||||
}
|
||||
|
||||
onCleanup(() => {
|
||||
removeMouseListeners()
|
||||
removeTouchListeners()
|
||||
})
|
||||
|
||||
const sessionSections = createMemo(() => {
|
||||
const parentItems: SessionListItem[] = []
|
||||
const childItems: SessionListItem[] = []
|
||||
|
||||
for (const [id, session] of props.sessions.entries()) {
|
||||
const item: SessionListItem = {
|
||||
id,
|
||||
title: session.title || "Untitled",
|
||||
isActive: id === props.activeSessionId,
|
||||
isParent: session.parentId === null,
|
||||
onSelect: () => props.onSelect(id),
|
||||
onClose: session.parentId === null ? () => props.onClose(id) : undefined,
|
||||
}
|
||||
|
||||
if (session.parentId === null) {
|
||||
parentItems.push(item)
|
||||
} else {
|
||||
childItems.push(item)
|
||||
}
|
||||
}
|
||||
|
||||
childItems.sort((a, b) => {
|
||||
const sessionA = props.sessions.get(a.id)
|
||||
const sessionB = props.sessions.get(b.id)
|
||||
if (!sessionA || !sessionB) return 0
|
||||
return sessionB.time.updated - sessionA.time.updated
|
||||
})
|
||||
|
||||
parentItems.push({
|
||||
id: "info",
|
||||
title: "Info",
|
||||
isSpecial: true,
|
||||
isActive: props.activeSessionId === "info",
|
||||
onSelect: () => props.onSelect("info"),
|
||||
})
|
||||
|
||||
return { parentItems, childItems }
|
||||
})
|
||||
|
||||
return (
|
||||
<div
|
||||
class="session-list-container bg-surface-secondary border-r border-base flex flex-col"
|
||||
style={{ width: `${sidebarWidth()}px` }}
|
||||
>
|
||||
<div
|
||||
class="session-resize-handle"
|
||||
onMouseDown={handleMouseDown}
|
||||
onTouchStart={handleTouchStart}
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
<div class="session-list-header p-3 border-b border-base">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<h3 class="text-sm font-semibold text-primary">Sessions</h3>
|
||||
<KeyboardHint
|
||||
shortcuts={[keyboardRegistry.get("session-prev")!, keyboardRegistry.get("session-next")!].filter(Boolean)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="session-list flex-1 overflow-y-auto">
|
||||
<div class="session-section">
|
||||
<div class="session-section-header px-3 py-2 text-xs font-semibold text-primary/70 uppercase tracking-wide">
|
||||
Parent & Info
|
||||
</div>
|
||||
<For each={sessionSections().parentItems}>
|
||||
{(item) => (
|
||||
<div class="session-list-item group">
|
||||
<button
|
||||
class={`session-item-base ${
|
||||
item.isActive ? "session-item-active" : "session-item-inactive"
|
||||
} ${item.isSpecial ? "session-item-special" : ""}`}
|
||||
onClick={item.onSelect}
|
||||
title={item.title}
|
||||
role="button"
|
||||
aria-selected={item.isActive}
|
||||
>
|
||||
<Show when={item.isSpecial} fallback={<MessageSquare class="w-4 h-4 flex-shrink-0" />}>
|
||||
<Info class="w-4 h-4 flex-shrink-0" />
|
||||
</Show>
|
||||
|
||||
<span class="session-item-title truncate">{item.title}</span>
|
||||
|
||||
<Show when={!item.isSpecial && item.onClose}>
|
||||
<span
|
||||
class="session-item-close opacity-0 group-hover:opacity-100 hover:bg-status-error hover:text-white rounded p-0.5 transition-all"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
item.onClose?.()
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="Close session"
|
||||
>
|
||||
<X class="w-3 h-3" />
|
||||
</span>
|
||||
</Show>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<Show when={sessionSections().childItems.length > 0}>
|
||||
<div class="session-section">
|
||||
<div class="session-section-header px-3 py-2 text-xs font-semibold text-primary/70 uppercase tracking-wide">
|
||||
Child Sessions
|
||||
</div>
|
||||
<For each={sessionSections().childItems}>
|
||||
{(item) => (
|
||||
<div class="session-list-item group">
|
||||
<button
|
||||
class={`session-item-base ${
|
||||
item.isActive ? "session-item-active" : "session-item-inactive"
|
||||
} ${item.isSpecial ? "session-item-special" : ""}`}
|
||||
onClick={item.onSelect}
|
||||
title={item.title}
|
||||
role="button"
|
||||
aria-selected={item.isActive}
|
||||
>
|
||||
<MessageSquare class="w-4 h-4 flex-shrink-0" />
|
||||
|
||||
<span class="session-item-title truncate">{item.title}</span>
|
||||
|
||||
<Show when={!item.isSpecial && item.onClose}>
|
||||
<span
|
||||
class="session-item-close opacity-0 group-hover:opacity-100 hover:bg-status-error hover:text-white rounded p-0.5 transition-all"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
item.onClose?.()
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="Close session"
|
||||
>
|
||||
<X class="w-3 h-3" />
|
||||
</span>
|
||||
</Show>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="session-list-footer p-3 border-t border-base">
|
||||
<button
|
||||
class="session-new-button w-full flex items-center gap-2 px-3 py-2 rounded-md transition-colors text-sm font-medium"
|
||||
onClick={props.onNew}
|
||||
title="New session (Cmd/Ctrl+T)"
|
||||
aria-label="New session"
|
||||
>
|
||||
<Plus class="w-4 h-4" />
|
||||
<span>New Session</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SessionList
|
||||
@@ -1,56 +0,0 @@
|
||||
import { Component, Show } from "solid-js"
|
||||
import type { Session } from "../types/session"
|
||||
import { MessageSquare, Info, X } from "lucide-solid"
|
||||
|
||||
interface SessionTabProps {
|
||||
session?: Session
|
||||
special?: "info"
|
||||
active: boolean
|
||||
isParent?: boolean
|
||||
onSelect: () => void
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
const SessionTab: Component<SessionTabProps> = (props) => {
|
||||
const label = () => {
|
||||
if (props.special === "info") return "Info"
|
||||
return props.session?.title || "Untitled"
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="group">
|
||||
<button
|
||||
class={`session-tab-base ${
|
||||
props.active
|
||||
? "session-tab-active"
|
||||
: "session-tab-inactive"
|
||||
} ${props.special === "info" ? "session-tab-special" : ""} ${props.isParent && !props.active ? "font-semibold" : ""}`}
|
||||
onClick={props.onSelect}
|
||||
title={label()}
|
||||
role="tab"
|
||||
aria-selected={props.active}
|
||||
>
|
||||
<Show when={props.special === "info"} fallback={<MessageSquare class="w-3.5 h-3.5 flex-shrink-0" />}>
|
||||
<Info class="w-3.5 h-3.5 flex-shrink-0" />
|
||||
</Show>
|
||||
<span class="tab-label">{label()}</span>
|
||||
<Show when={!props.special && props.onClose}>
|
||||
<span
|
||||
class="tab-close"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
props.onClose?.()
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label="Close session"
|
||||
>
|
||||
<X class="w-3 h-3" />
|
||||
</span>
|
||||
</Show>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SessionTab
|
||||
@@ -1,62 +0,0 @@
|
||||
import { Component, For, Show } from "solid-js"
|
||||
import type { Session } from "../types/session"
|
||||
import SessionTab from "./session-tab"
|
||||
import KeyboardHint from "./keyboard-hint"
|
||||
import { Plus } from "lucide-solid"
|
||||
import { keyboardRegistry } from "../lib/keyboard-registry"
|
||||
|
||||
interface SessionTabsProps {
|
||||
instanceId: string
|
||||
sessions: Map<string, Session>
|
||||
activeSessionId: string | null
|
||||
onSelect: (sessionId: string) => void
|
||||
onClose: (sessionId: string) => void
|
||||
onNew: () => void
|
||||
}
|
||||
|
||||
const SessionTabs: Component<SessionTabsProps> = (props) => {
|
||||
const sessionsList = () => Array.from(props.sessions.entries())
|
||||
const totalTabs = () => sessionsList().length + 1
|
||||
|
||||
return (
|
||||
<div class="tab-bar tab-bar-session">
|
||||
<div class="tab-container" role="tablist">
|
||||
<div class="flex items-center gap-1 overflow-x-auto">
|
||||
<For each={sessionsList()}>
|
||||
{([id, session]) => (
|
||||
<SessionTab
|
||||
session={session}
|
||||
active={id === props.activeSessionId}
|
||||
isParent={session.parentId === null}
|
||||
onSelect={() => props.onSelect(id)}
|
||||
onClose={session.parentId === null ? () => props.onClose(id) : undefined}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
<SessionTab
|
||||
special="info"
|
||||
active={props.activeSessionId === "info"}
|
||||
onSelect={() => props.onSelect("info")}
|
||||
/>
|
||||
<button
|
||||
class="new-tab-button"
|
||||
onClick={props.onNew}
|
||||
title="New parent session (Cmd/Ctrl+T)"
|
||||
aria-label="New parent session"
|
||||
>
|
||||
<Plus class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<Show when={totalTabs() > 1}>
|
||||
<div class="flex-shrink-0 ml-4">
|
||||
<KeyboardHint
|
||||
shortcuts={[keyboardRegistry.get("session-prev")!, keyboardRegistry.get("session-next")!].filter(Boolean)}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SessionTabs
|
||||
@@ -5,6 +5,21 @@ import { getSessionFamily, activeSessionId, setActiveSession, activeParentSessio
|
||||
export function registerNavigationShortcuts() {
|
||||
const isMac = () => navigator.platform.toLowerCase().includes("mac")
|
||||
|
||||
const buildNavigationOrder = (instanceId: string): string[] => {
|
||||
const parentId = activeParentSessionId().get(instanceId)
|
||||
if (!parentId) return []
|
||||
|
||||
const familySessions = getSessionFamily(instanceId, parentId)
|
||||
if (familySessions.length === 0) return []
|
||||
|
||||
const [parentSession, ...childSessions] = familySessions
|
||||
if (!parentSession) return []
|
||||
|
||||
const sortedChildren = childSessions.slice().sort((a, b) => b.time.updated - a.time.updated)
|
||||
|
||||
return [parentSession.id, "info", ...sortedChildren.map((session) => session.id)]
|
||||
}
|
||||
|
||||
keyboardRegistry.register({
|
||||
id: "instance-prev",
|
||||
key: "[",
|
||||
@@ -43,16 +58,20 @@ export function registerNavigationShortcuts() {
|
||||
const instanceId = activeInstanceId()
|
||||
if (!instanceId) return
|
||||
|
||||
const parentId = activeParentSessionId().get(instanceId)
|
||||
if (!parentId) return
|
||||
const navigationIds = buildNavigationOrder(instanceId)
|
||||
if (navigationIds.length === 0) return
|
||||
|
||||
const familySessions = getSessionFamily(instanceId, parentId)
|
||||
const ids = familySessions.map((s) => s.id).concat(["info"])
|
||||
if (ids.length <= 1) return
|
||||
const currentActiveId = activeSessionId().get(instanceId)
|
||||
let currentIndex = navigationIds.indexOf(currentActiveId || "")
|
||||
|
||||
const current = ids.indexOf(activeSessionId().get(instanceId) || "")
|
||||
const prev = current <= 0 ? ids.length - 1 : current - 1
|
||||
if (ids[prev]) setActiveSession(instanceId, ids[prev])
|
||||
if (currentIndex === -1) {
|
||||
currentIndex = navigationIds.length - 1
|
||||
}
|
||||
|
||||
const targetIndex = currentIndex <= 0 ? navigationIds.length - 1 : currentIndex - 1
|
||||
const targetSessionId = navigationIds[targetIndex]
|
||||
|
||||
setActiveSession(instanceId, targetSessionId)
|
||||
},
|
||||
description: "previous session",
|
||||
context: "global",
|
||||
@@ -66,16 +85,20 @@ export function registerNavigationShortcuts() {
|
||||
const instanceId = activeInstanceId()
|
||||
if (!instanceId) return
|
||||
|
||||
const parentId = activeParentSessionId().get(instanceId)
|
||||
if (!parentId) return
|
||||
const navigationIds = buildNavigationOrder(instanceId)
|
||||
if (navigationIds.length === 0) return
|
||||
|
||||
const familySessions = getSessionFamily(instanceId, parentId)
|
||||
const ids = familySessions.map((s) => s.id).concat(["info"])
|
||||
if (ids.length <= 1) return
|
||||
const currentActiveId = activeSessionId().get(instanceId)
|
||||
let currentIndex = navigationIds.indexOf(currentActiveId || "")
|
||||
|
||||
const current = ids.indexOf(activeSessionId().get(instanceId) || "")
|
||||
const next = (current + 1) % ids.length
|
||||
if (ids[next]) setActiveSession(instanceId, ids[next])
|
||||
if (currentIndex === -1) {
|
||||
currentIndex = 0
|
||||
}
|
||||
|
||||
const targetIndex = (currentIndex + 1) % navigationIds.length
|
||||
const targetSessionId = navigationIds[targetIndex]
|
||||
|
||||
setActiveSession(instanceId, targetSessionId)
|
||||
},
|
||||
description: "next session",
|
||||
context: "global",
|
||||
|
||||
@@ -1599,4 +1599,142 @@ button.button-primary {
|
||||
@apply flex flex-col h-full;
|
||||
background-color: var(--surface-base);
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* Session list component */
|
||||
.session-list-container {
|
||||
@apply flex flex-col h-full relative;
|
||||
background-color: var(--surface-secondary);
|
||||
min-width: 200px;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.session-resize-handle {
|
||||
@apply absolute top-0 right-0 w-1 h-full cursor-col-resize bg-transparent transition-colors;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.session-resize-handle:hover {
|
||||
background-color: var(--accent-primary);
|
||||
}
|
||||
|
||||
.session-resize-handle::before {
|
||||
content: "";
|
||||
@apply absolute top-0 left-0 w-2 h-full -translate-x-1/2;
|
||||
}
|
||||
|
||||
.session-list-header {
|
||||
@apply border-b relative;
|
||||
border-color: var(--border-base);
|
||||
}
|
||||
|
||||
.session-list-header h3 {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.session-header-hints {
|
||||
@apply flex-shrink-0;
|
||||
}
|
||||
|
||||
.session-list {
|
||||
@apply flex-1;
|
||||
}
|
||||
|
||||
.session-list-item {
|
||||
@apply border-b last:border-b-0;
|
||||
border-color: var(--border-base);
|
||||
}
|
||||
|
||||
.session-item-base {
|
||||
@apply w-full flex items-center gap-3 px-3 py-2.5 text-left transition-colors outline-none;
|
||||
font-family: var(--font-family-sans);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
.session-item-base:focus-visible {
|
||||
@apply ring-2 ring-offset-1;
|
||||
ring-color: var(--accent-primary);
|
||||
ring-offset-color: var(--surface-secondary);
|
||||
}
|
||||
|
||||
.session-item-active {
|
||||
background-color: var(--accent-primary);
|
||||
color: var(--text-inverted);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.session-item-inactive {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.session-item-inactive:hover {
|
||||
background-color: var(--surface-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.session-item-special {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.session-item-active .session-item-close:hover {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.session-item-title {
|
||||
@apply flex-1 min-w-0;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
.session-item-close {
|
||||
@apply flex-shrink-0 p-0.5 rounded transition-all;
|
||||
}
|
||||
|
||||
.session-item-close:focus-visible {
|
||||
@apply ring-2 ring-offset-1;
|
||||
ring-color: var(--accent-primary);
|
||||
ring-offset-color: inherit;
|
||||
}
|
||||
|
||||
.session-list-footer {
|
||||
@apply border-t;
|
||||
border-color: var(--border-base);
|
||||
}
|
||||
|
||||
.session-new-button {
|
||||
background-color: var(--surface-base);
|
||||
color: var(--text-primary);
|
||||
border: 1px solid var(--border-base);
|
||||
}
|
||||
|
||||
.session-new-button:hover {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
.session-new-button:focus-visible {
|
||||
@apply ring-2 ring-offset-1;
|
||||
ring-color: var(--accent-primary);
|
||||
ring-offset-color: var(--surface-secondary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .session-new-button {
|
||||
background-color: var(--surface-base);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .session-new-button:hover {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
/* Responsive behavior for session list */
|
||||
@media (max-width: 768px) {
|
||||
.session-list-container {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.session-item-base {
|
||||
@apply px-2 py-2;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user