feat(ui): add right panel Changes/Status tabs
This commit is contained in:
@@ -93,20 +93,26 @@ const MIN_SESSION_SIDEBAR_WIDTH = 220
|
||||
const MAX_SESSION_SIDEBAR_WIDTH = 400
|
||||
const RIGHT_DRAWER_WIDTH = 260
|
||||
const MIN_RIGHT_DRAWER_WIDTH = 200
|
||||
const MAX_RIGHT_DRAWER_WIDTH = 380
|
||||
const MAX_RIGHT_DRAWER_WIDTH = 1200
|
||||
const SESSION_CACHE_LIMIT = 5
|
||||
const LEFT_DRAWER_STORAGE_KEY = "opencode-session-sidebar-width-v8"
|
||||
const RIGHT_DRAWER_STORAGE_KEY = "opencode-session-right-drawer-width-v1"
|
||||
const LEFT_PIN_STORAGE_KEY = "opencode-session-left-drawer-pinned-v1"
|
||||
const RIGHT_PIN_STORAGE_KEY = "opencode-session-right-drawer-pinned-v1"
|
||||
const RIGHT_PANEL_TAB_STORAGE_KEY = "opencode-session-right-panel-tab-v1"
|
||||
|
||||
|
||||
|
||||
|
||||
type LayoutMode = "desktop" | "tablet" | "phone"
|
||||
type RightPanelTab = "files" | "status"
|
||||
|
||||
const clampWidth = (value: number) => Math.min(MAX_SESSION_SIDEBAR_WIDTH, Math.max(MIN_SESSION_SIDEBAR_WIDTH, value))
|
||||
const clampRightWidth = (value: number) => Math.min(MAX_RIGHT_DRAWER_WIDTH, Math.max(MIN_RIGHT_DRAWER_WIDTH, value))
|
||||
const clampRightWidth = (value: number) => {
|
||||
const windowMax = typeof window !== "undefined" ? Math.floor(window.innerWidth * 0.7) : MAX_RIGHT_DRAWER_WIDTH
|
||||
const max = Math.max(MIN_RIGHT_DRAWER_WIDTH, windowMax)
|
||||
return Math.min(max, Math.max(MIN_RIGHT_DRAWER_WIDTH, value))
|
||||
}
|
||||
const getPinStorageKey = (side: "left" | "right") => (side === "left" ? LEFT_PIN_STORAGE_KEY : RIGHT_PIN_STORAGE_KEY)
|
||||
function readStoredPinState(side: "left" | "right", defaultValue: boolean) {
|
||||
if (typeof window === "undefined") return defaultValue
|
||||
@@ -120,11 +126,19 @@ function persistPinState(side: "left" | "right", value: boolean) {
|
||||
window.localStorage.setItem(getPinStorageKey(side), value ? "true" : "false")
|
||||
}
|
||||
|
||||
function readStoredRightPanelTab(defaultValue: RightPanelTab): RightPanelTab {
|
||||
if (typeof window === "undefined") return defaultValue
|
||||
const stored = window.localStorage.getItem(RIGHT_PANEL_TAB_STORAGE_KEY)
|
||||
return stored === "status" ? "status" : defaultValue
|
||||
}
|
||||
|
||||
const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
const { t } = useI18n()
|
||||
|
||||
const [sessionSidebarWidth, setSessionSidebarWidth] = createSignal(DEFAULT_SESSION_SIDEBAR_WIDTH)
|
||||
const [rightDrawerWidth, setRightDrawerWidth] = createSignal(RIGHT_DRAWER_WIDTH)
|
||||
const [rightDrawerWidth, setRightDrawerWidth] = createSignal(
|
||||
typeof window !== "undefined" ? clampRightWidth(window.innerWidth * 0.35) : RIGHT_DRAWER_WIDTH,
|
||||
)
|
||||
const [leftPinned, setLeftPinned] = createSignal(true)
|
||||
const [leftOpen, setLeftOpen] = createSignal(true)
|
||||
const [rightPinned, setRightPinned] = createSignal(true)
|
||||
@@ -141,6 +155,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
const [activeResizeSide, setActiveResizeSide] = createSignal<"left" | "right" | null>(null)
|
||||
const [resizeStartX, setResizeStartX] = createSignal(0)
|
||||
const [resizeStartWidth, setResizeStartWidth] = createSignal(0)
|
||||
const [rightPanelTab, setRightPanelTab] = createSignal<RightPanelTab>(readStoredRightPanelTab("files"))
|
||||
const [rightPanelExpandedItems, setRightPanelExpandedItems] = createSignal<string[]>([
|
||||
"plan",
|
||||
"background-processes",
|
||||
@@ -148,6 +163,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
"lsp",
|
||||
"plugins",
|
||||
])
|
||||
const [selectedFile, setSelectedFile] = createSignal<string | null>(null)
|
||||
const [selectedBackgroundProcess, setSelectedBackgroundProcess] = createSignal<BackgroundProcess | null>(null)
|
||||
const [showBackgroundOutput, setShowBackgroundOutput] = createSignal(false)
|
||||
const [permissionModalOpen, setPermissionModalOpen] = createSignal(false)
|
||||
@@ -168,7 +184,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
})
|
||||
|
||||
const isPhoneLayout = createMemo(() => layoutMode() === "phone")
|
||||
const leftPinningSupported = createMemo(() => layoutMode() === "desktop")
|
||||
const leftPinningSupported = createMemo(() => layoutMode() !== "phone")
|
||||
const rightPinningSupported = createMemo(() => layoutMode() !== "phone")
|
||||
|
||||
const persistPinIfSupported = (side: "left" | "right", value: boolean) => {
|
||||
@@ -196,11 +212,10 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
break
|
||||
}
|
||||
case "tablet": {
|
||||
const rightSaved = readStoredPinState("right", true)
|
||||
setLeftPinned(false)
|
||||
setLeftOpen(false)
|
||||
setRightPinned(rightSaved)
|
||||
setRightOpen(rightSaved)
|
||||
setLeftPinned(true)
|
||||
setLeftOpen(true)
|
||||
setRightPinned(false)
|
||||
setRightOpen(false)
|
||||
break
|
||||
}
|
||||
default:
|
||||
@@ -232,17 +247,25 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
let didLoadRightWidth = false
|
||||
const savedRight = window.localStorage.getItem(RIGHT_DRAWER_STORAGE_KEY)
|
||||
if (savedRight) {
|
||||
const parsed = Number.parseInt(savedRight, 10)
|
||||
if (Number.isFinite(parsed)) {
|
||||
setRightDrawerWidth(clampRightWidth(parsed))
|
||||
didLoadRightWidth = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!didLoadRightWidth) {
|
||||
setRightDrawerWidth(clampRightWidth(window.innerWidth * 0.35))
|
||||
}
|
||||
|
||||
const handleResize = () => {
|
||||
const width = clampWidth(window.innerWidth * 0.3)
|
||||
setSessionSidebarWidth((current) => clampWidth(current || width))
|
||||
const fallbackRight = window.innerWidth * 0.35
|
||||
setRightDrawerWidth((current) => clampRightWidth(current || fallbackRight))
|
||||
measureDrawerHost()
|
||||
}
|
||||
|
||||
@@ -272,6 +295,11 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
window.localStorage.setItem(RIGHT_DRAWER_STORAGE_KEY, rightDrawerWidth().toString())
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (typeof window === "undefined") return
|
||||
window.localStorage.setItem(RIGHT_PANEL_TAB_STORAGE_KEY, rightPanelTab())
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
props.tabBarOffset
|
||||
requestAnimationFrame(() => measureDrawerHost())
|
||||
@@ -965,19 +993,31 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
)
|
||||
|
||||
const RightDrawerContent = () => {
|
||||
const renderSessionChanges = () => {
|
||||
const renderFilesTabContent = () => {
|
||||
const sessionId = activeSessionIdForInstance()
|
||||
if (!sessionId || sessionId === "info") {
|
||||
return <p class="text-xs text-secondary">{t("instanceShell.sessionChanges.noSessionSelected")}</p>
|
||||
return (
|
||||
<div class="right-panel-empty">
|
||||
<span class="text-xs">{t("instanceShell.sessionChanges.noSessionSelected")}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const diffs = activeSessionDiffs()
|
||||
if (diffs === undefined) {
|
||||
return <p class="text-xs text-secondary">{t("instanceShell.sessionChanges.loading")}</p>
|
||||
return (
|
||||
<div class="right-panel-empty">
|
||||
<span class="text-xs">{t("instanceShell.sessionChanges.loading")}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!Array.isArray(diffs) || diffs.length === 0) {
|
||||
return <p class="text-xs text-secondary">{t("instanceShell.sessionChanges.empty")}</p>
|
||||
return (
|
||||
<div class="right-panel-empty">
|
||||
<span class="text-xs">{t("instanceShell.sessionChanges.empty")}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const sorted = [...diffs].sort((a, b) => String(a.file || "").localeCompare(String(b.file || "")))
|
||||
@@ -990,47 +1030,119 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
{ additions: 0, deletions: 0 },
|
||||
)
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-3 min-h-0">
|
||||
<div class="flex items-center justify-between gap-2 text-[11px] text-secondary">
|
||||
<span>{t("instanceShell.sessionChanges.filesChanged", { count: sorted.length })}</span>
|
||||
<span class="flex items-center gap-2">
|
||||
<span style={{ color: "var(--session-status-idle-fg)" }}>{`+${totals.additions}`}</span>
|
||||
<span style={{ color: "var(--session-status-working-fg)" }}>{`-${totals.deletions}`}</span>
|
||||
</span>
|
||||
</div>
|
||||
// Select first file by default if none selected
|
||||
const currentSelected = selectedFile()
|
||||
const selectedFileData = sorted.find((f) => f.file === currentSelected) || sorted[0]
|
||||
|
||||
<div class="rounded-md border border-base bg-surface-secondary p-2 max-h-[40vh] overflow-y-auto">
|
||||
<div class="flex flex-col">
|
||||
<For each={sorted}>
|
||||
{(item) => (
|
||||
<div class="py-2 border-b border-base last:border-b-0">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div
|
||||
class="text-xs font-mono text-primary min-w-0 flex-1 overflow-hidden whitespace-nowrap"
|
||||
title={item.file}
|
||||
style="text-overflow: ellipsis; direction: rtl; text-align: left; unicode-bidi: plaintext;"
|
||||
>
|
||||
{item.file}
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-[11px] flex-shrink-0">
|
||||
<span style={{ color: "var(--session-status-idle-fg)" }}>{`+${item.additions}`}</span>
|
||||
<span style={{ color: "var(--session-status-working-fg)" }}>{`-${item.deletions}`}</span>
|
||||
</div>
|
||||
if (isPhoneLayout()) {
|
||||
return (
|
||||
<div class="files-tab-container">
|
||||
<div class="mobile-file-selector">
|
||||
<span class="mobile-file-selector-label">{t("instanceShell.filesShell.mobileSelectorLabel")}</span>
|
||||
<button type="button" class="selector-trigger mobile-file-selector-trigger" disabled>
|
||||
<span class="selector-trigger-label selector-trigger-primary selector-trigger-primary--align-left truncate">
|
||||
{selectedFileData?.file || t("instanceShell.filesShell.mobileSelectorEmpty")}
|
||||
</span>
|
||||
<span class="selector-trigger-icon" aria-hidden="true">
|
||||
<ChevronDown class="w-3 h-3" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mobile-file-viewer">
|
||||
<div class="file-viewer-header">
|
||||
<span class="file-viewer-title">{t("instanceShell.filesShell.viewerTitle")}</span>
|
||||
</div>
|
||||
<div class="file-viewer-content">
|
||||
<Show
|
||||
when={selectedFileData}
|
||||
fallback={
|
||||
<div class="file-viewer-empty">
|
||||
<span class="file-viewer-empty-text">{t("instanceShell.filesShell.viewerEmpty")}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
}
|
||||
>
|
||||
{(file) => (
|
||||
<div class="file-viewer-selected-file">
|
||||
<span class="file-viewer-file-name">{file().file}</span>
|
||||
<p class="file-viewer-placeholder">{t("instanceShell.filesShell.viewerPlaceholder")}</p>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="files-tab-container">
|
||||
<div class="files-tab-header">
|
||||
<div class="files-tab-stats">
|
||||
<span class="files-tab-stat">
|
||||
<span class="files-tab-stat-value">{sorted.length}</span>
|
||||
<span>files</span>
|
||||
</span>
|
||||
<span class="files-tab-stat files-tab-stat-additions">
|
||||
<span class="files-tab-stat-value">+{totals.additions}</span>
|
||||
<span>additions</span>
|
||||
</span>
|
||||
<span class="files-tab-stat files-tab-stat-deletions">
|
||||
<span class="files-tab-stat-value">-{totals.deletions}</span>
|
||||
<span>deletions</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="button-tertiary w-full p-1.5 inline-flex items-center justify-center"
|
||||
onClick={() => undefined}
|
||||
>
|
||||
{t("instanceShell.sessionChanges.actions.show")}
|
||||
</button>
|
||||
<div class="flex min-h-0 gap-3 flex-1">
|
||||
<div class="file-list-panel">
|
||||
<div class="file-list-header">
|
||||
<span class="file-list-title">{t("instanceShell.filesShell.fileListTitle")}</span>
|
||||
<span class="file-list-count">{sorted.length}</span>
|
||||
</div>
|
||||
<div class="file-list-scroll">
|
||||
<For each={sorted}>
|
||||
{(item) => (
|
||||
<div
|
||||
class={`file-list-item ${selectedFileData?.file === item.file ? "file-list-item-active" : ""}`}
|
||||
onClick={() => setSelectedFile(item.file)}
|
||||
>
|
||||
<div class="file-list-item-content">
|
||||
<div class="file-list-item-path" title={item.file}>
|
||||
{item.file}
|
||||
</div>
|
||||
<div class="file-list-item-stats">
|
||||
<span class="file-list-item-additions">+{item.additions}</span>
|
||||
<span class="file-list-item-deletions">-{item.deletions}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-viewer-panel flex-1">
|
||||
<div class="file-viewer-header">
|
||||
<span class="file-viewer-title">{t("instanceShell.filesShell.viewerTitle")}</span>
|
||||
</div>
|
||||
<div class="file-viewer-content">
|
||||
<Show
|
||||
when={selectedFileData}
|
||||
fallback={
|
||||
<div class="file-viewer-empty">
|
||||
<span class="file-viewer-empty-text">{t("instanceShell.filesShell.viewerEmpty")}</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{(file) => (
|
||||
<div class="file-viewer-selected-file">
|
||||
<span class="file-viewer-file-name">{file().file}</span>
|
||||
<p class="file-viewer-placeholder">{t("instanceShell.filesShell.viewerPlaceholder")}</p>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1038,11 +1150,19 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
const renderPlanSectionContent = () => {
|
||||
const sessionId = activeSessionIdForInstance()
|
||||
if (!sessionId || sessionId === "info") {
|
||||
return <p class="text-xs text-secondary">{t("instanceShell.plan.noSessionSelected")}</p>
|
||||
return (
|
||||
<div class="right-panel-empty right-panel-empty--left">
|
||||
<span class="text-xs">{t("instanceShell.plan.noSessionSelected")}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const todoState = latestTodoState()
|
||||
if (!todoState) {
|
||||
return <p class="text-xs text-secondary">{t("instanceShell.plan.empty")}</p>
|
||||
return (
|
||||
<div class="right-panel-empty right-panel-empty--left">
|
||||
<span class="text-xs">{t("instanceShell.plan.empty")}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <TodoListView state={todoState} emptyLabel={t("instanceShell.plan.empty")} showStatusLabel={false} />
|
||||
}
|
||||
@@ -1050,17 +1170,21 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
const renderBackgroundProcesses = () => {
|
||||
const processes = backgroundProcessList()
|
||||
if (processes.length === 0) {
|
||||
return <p class="text-xs text-secondary">{t("instanceShell.backgroundProcesses.empty")}</p>
|
||||
return (
|
||||
<div class="right-panel-empty right-panel-empty--left">
|
||||
<span class="text-xs">{t("instanceShell.backgroundProcesses.empty")}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-2">
|
||||
<For each={processes}>
|
||||
{(process) => (
|
||||
<div class="rounded-md border border-base bg-surface-secondary p-2 flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-xs font-semibold text-primary">{process.title}</span>
|
||||
<div class="flex flex-wrap gap-2 text-[11px] text-secondary">
|
||||
<div class="status-process-card">
|
||||
<div class="status-process-header">
|
||||
<span class="status-process-title">{process.title}</span>
|
||||
<div class="status-process-meta">
|
||||
<span>{t("instanceShell.backgroundProcesses.status", { status: process.status })}</span>
|
||||
<Show when={typeof process.outputSizeBytes === "number"}>
|
||||
<span>
|
||||
@@ -1071,7 +1195,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div class="status-process-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="button-tertiary w-full p-1 inline-flex items-center justify-center"
|
||||
@@ -1108,12 +1232,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
const sections = [
|
||||
{
|
||||
id: "session-changes",
|
||||
labelKey: "instanceShell.rightPanel.sections.sessionChanges",
|
||||
render: renderSessionChanges,
|
||||
},
|
||||
const statusSections = [
|
||||
{
|
||||
id: "plan",
|
||||
labelKey: "instanceShell.rightPanel.sections.plan",
|
||||
@@ -1164,8 +1283,8 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
|
||||
createEffect(() => {
|
||||
const currentExpanded = new Set(rightPanelExpandedItems())
|
||||
if (sections.every((section) => currentExpanded.has(section.id))) return
|
||||
setRightPanelExpandedItems(sections.map((section) => section.id))
|
||||
if (statusSections.every((section) => currentExpanded.has(section.id))) return
|
||||
setRightPanelExpandedItems(statusSections.map((section) => section.id))
|
||||
})
|
||||
|
||||
const handleAccordionChange = (values: string[]) => {
|
||||
@@ -1174,78 +1293,112 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
|
||||
const isSectionExpanded = (id: string) => rightPanelExpandedItems().includes(id)
|
||||
|
||||
const renderStatusTabContent = () => (
|
||||
<div class="status-tab-container">
|
||||
<Show when={activeSessionForInstance()}>
|
||||
{(activeSession) => (
|
||||
<ContextUsagePanel
|
||||
instanceId={props.instance.id}
|
||||
sessionId={activeSession().id}
|
||||
class="status-tab-context-panel"
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<Accordion.Root
|
||||
class="right-panel-accordion"
|
||||
collapsible
|
||||
multiple
|
||||
value={rightPanelExpandedItems()}
|
||||
onChange={handleAccordionChange}
|
||||
>
|
||||
<For each={statusSections}>
|
||||
{(section) => (
|
||||
<Accordion.Item
|
||||
value={section.id}
|
||||
class="right-panel-accordion-item"
|
||||
>
|
||||
<Accordion.Header>
|
||||
<Accordion.Trigger class="right-panel-accordion-trigger">
|
||||
<span>{t(section.labelKey)}</span>
|
||||
<ChevronDown
|
||||
class={`right-panel-accordion-chevron ${isSectionExpanded(section.id) ? "right-panel-accordion-chevron-expanded" : ""}`}
|
||||
/>
|
||||
</Accordion.Trigger>
|
||||
</Accordion.Header>
|
||||
<Accordion.Content class="right-panel-accordion-content">
|
||||
{section.render()}
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
)}
|
||||
</For>
|
||||
</Accordion.Root>
|
||||
</div>
|
||||
)
|
||||
|
||||
const tabClass = (tab: RightPanelTab) =>
|
||||
`right-panel-tab ${rightPanelTab() === tab ? "right-panel-tab-active" : "right-panel-tab-inactive"}`
|
||||
|
||||
return (
|
||||
<div class="flex flex-col h-full" ref={setRightDrawerContentEl}>
|
||||
<div class="border-b border-base text-primary">
|
||||
<div class="relative flex items-center px-4 py-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<Show when={rightDrawerState() === "floating-open"}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
aria-label={t("instanceShell.rightDrawer.toggle.close")}
|
||||
title={t("instanceShell.rightDrawer.toggle.close")}
|
||||
onClick={closeRightDrawer}
|
||||
>
|
||||
<MenuOpenIcon fontSize="small" sx={{ transform: "scaleX(-1)" }} />
|
||||
</IconButton>
|
||||
</Show>
|
||||
<Show when={!isPhoneLayout()}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
aria-label={rightPinned() ? t("instanceShell.rightDrawer.unpin") : t("instanceShell.rightDrawer.pin")}
|
||||
onClick={() => (rightPinned() ? unpinRightDrawer() : pinRightDrawer())}
|
||||
>
|
||||
{rightPinned() ? <PushPinIcon fontSize="small" /> : <PushPinOutlinedIcon fontSize="small" />}
|
||||
</IconButton>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<span class="session-sidebar-title text-sm font-semibold uppercase text-primary">
|
||||
{t("instanceShell.rightPanel.title")}
|
||||
</span>
|
||||
<div class="right-panel-tab-bar">
|
||||
<div class="tab-container">
|
||||
<div class="tab-scroll">
|
||||
<div class="tab-strip">
|
||||
<div class="tab-strip-shortcuts text-primary">
|
||||
<Show when={rightDrawerState() === "floating-open"}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
aria-label={t("instanceShell.rightDrawer.toggle.close")}
|
||||
title={t("instanceShell.rightDrawer.toggle.close")}
|
||||
onClick={closeRightDrawer}
|
||||
>
|
||||
<MenuOpenIcon fontSize="small" sx={{ transform: "scaleX(-1)" }} />
|
||||
</IconButton>
|
||||
</Show>
|
||||
<Show when={!isPhoneLayout()}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
aria-label={rightPinned() ? t("instanceShell.rightDrawer.unpin") : t("instanceShell.rightDrawer.pin")}
|
||||
onClick={() => (rightPinned() ? unpinRightDrawer() : pinRightDrawer())}
|
||||
>
|
||||
{rightPinned() ? <PushPinIcon fontSize="small" /> : <PushPinOutlinedIcon fontSize="small" />}
|
||||
</IconButton>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="tab-strip-tabs" role="tablist" aria-label={t("instanceShell.rightPanel.tabs.ariaLabel")}>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class={tabClass("files")}
|
||||
aria-selected={rightPanelTab() === "files"}
|
||||
onClick={() => setRightPanelTab("files")}
|
||||
>
|
||||
<span class="tab-label">{t("instanceShell.rightPanel.tabs.changes")}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class={tabClass("status")}
|
||||
aria-selected={rightPanelTab() === "status"}
|
||||
onClick={() => setRightPanelTab("status")}
|
||||
>
|
||||
<span class="tab-label">{t("instanceShell.rightPanel.tabs.status")}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-strip-spacer" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={activeSessionForInstance()}>
|
||||
{(activeSession) => (
|
||||
<ContextUsagePanel
|
||||
instanceId={props.instance.id}
|
||||
sessionId={activeSession().id}
|
||||
class="border-t border-base"
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<Accordion.Root
|
||||
class="flex flex-col"
|
||||
collapsible
|
||||
multiple
|
||||
value={rightPanelExpandedItems()}
|
||||
onChange={handleAccordionChange}
|
||||
>
|
||||
<For each={sections}>
|
||||
{(section) => (
|
||||
<Accordion.Item
|
||||
value={section.id}
|
||||
class="w-full border border-base bg-surface-secondary text-primary"
|
||||
>
|
||||
<Accordion.Header>
|
||||
<Accordion.Trigger class="w-full flex items-center justify-between gap-3 px-3 py-2 text-[11px] font-semibold uppercase tracking-wide">
|
||||
<span>{t(section.labelKey)}</span>
|
||||
<ChevronDown
|
||||
class={`h-4 w-4 transition-transform duration-150 ${isSectionExpanded(section.id) ? "rotate-180" : ""}`}
|
||||
/>
|
||||
</Accordion.Trigger>
|
||||
</Accordion.Header>
|
||||
<Accordion.Content class="w-full px-3 pb-3 text-sm text-primary">
|
||||
{section.render()}
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
)}
|
||||
</For>
|
||||
</Accordion.Root>
|
||||
<Show when={rightPanelTab() === "files"}>{renderFilesTabContent()}</Show>
|
||||
<Show when={rightPanelTab() === "status"}>{renderStatusTabContent()}</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -1305,6 +1458,15 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Show when={!isPhoneLayout()}>
|
||||
<div
|
||||
class="session-resize-handle session-resize-handle--left"
|
||||
onMouseDown={handleDrawerResizeMouseDown("left")}
|
||||
onTouchStart={handleDrawerResizeTouchStart("left")}
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Show>
|
||||
<LeftDrawerContent />
|
||||
</Drawer>
|
||||
)
|
||||
@@ -1364,6 +1526,15 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Show when={!isPhoneLayout()}>
|
||||
<div
|
||||
class="session-resize-handle session-resize-handle--right"
|
||||
onMouseDown={handleDrawerResizeMouseDown("right")}
|
||||
onTouchStart={handleDrawerResizeTouchStart("right")}
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Show>
|
||||
<RightDrawerContent />
|
||||
</Drawer>
|
||||
|
||||
|
||||
@@ -86,6 +86,9 @@ export const instanceMessages = {
|
||||
"instanceShell.empty.description": "Select a session to view messages",
|
||||
|
||||
"instanceShell.rightPanel.title": "Status Panel",
|
||||
"instanceShell.rightPanel.tabs.changes": "Changes",
|
||||
"instanceShell.rightPanel.tabs.status": "Status",
|
||||
"instanceShell.rightPanel.tabs.ariaLabel": "Right panel tabs",
|
||||
"instanceShell.rightPanel.sections.sessionChanges": "Session Changes",
|
||||
"instanceShell.rightPanel.sections.plan": "Plan",
|
||||
"instanceShell.rightPanel.sections.backgroundProcesses": "Background Shells",
|
||||
@@ -99,6 +102,13 @@ export const instanceMessages = {
|
||||
"instanceShell.sessionChanges.filesChanged": "{count} files changed",
|
||||
"instanceShell.sessionChanges.actions.show": "Show changes",
|
||||
|
||||
"instanceShell.filesShell.fileListTitle": "File list",
|
||||
"instanceShell.filesShell.mobileSelectorLabel": "Select file",
|
||||
"instanceShell.filesShell.mobileSelectorEmpty": "Select a file",
|
||||
"instanceShell.filesShell.viewerTitle": "Change viewer",
|
||||
"instanceShell.filesShell.viewerPlaceholder": "Detailed change rendering will be added in the next step.",
|
||||
"instanceShell.filesShell.viewerEmpty": "No file selected.",
|
||||
|
||||
"instanceShell.plan.noSessionSelected": "Select a session to view plan.",
|
||||
"instanceShell.plan.empty": "Nothing planned yet.",
|
||||
|
||||
|
||||
@@ -86,6 +86,9 @@ export const instanceMessages = {
|
||||
"instanceShell.empty.description": "Selecciona una sesión para ver mensajes",
|
||||
|
||||
"instanceShell.rightPanel.title": "Panel de estado",
|
||||
"instanceShell.rightPanel.tabs.changes": "Cambios",
|
||||
"instanceShell.rightPanel.tabs.status": "Estado",
|
||||
"instanceShell.rightPanel.tabs.ariaLabel": "Pestañas del panel derecho",
|
||||
"instanceShell.rightPanel.sections.sessionChanges": "Cambios de sesion",
|
||||
"instanceShell.rightPanel.sections.plan": "Plan",
|
||||
"instanceShell.rightPanel.sections.backgroundProcesses": "Shells en segundo plano",
|
||||
@@ -99,6 +102,13 @@ export const instanceMessages = {
|
||||
"instanceShell.sessionChanges.filesChanged": "{count} archivos cambiados",
|
||||
"instanceShell.sessionChanges.actions.show": "Mostrar cambios",
|
||||
|
||||
"instanceShell.filesShell.fileListTitle": "Lista de archivos",
|
||||
"instanceShell.filesShell.mobileSelectorLabel": "Seleccionar archivo",
|
||||
"instanceShell.filesShell.mobileSelectorEmpty": "Selecciona un archivo",
|
||||
"instanceShell.filesShell.viewerTitle": "Visor de cambios",
|
||||
"instanceShell.filesShell.viewerPlaceholder": "La vista detallada se agregará en el siguiente paso.",
|
||||
"instanceShell.filesShell.viewerEmpty": "Ningún archivo seleccionado.",
|
||||
|
||||
"instanceShell.plan.noSessionSelected": "Selecciona una sesión para ver el plan.",
|
||||
"instanceShell.plan.empty": "Aún no hay nada planificado.",
|
||||
|
||||
|
||||
@@ -86,6 +86,9 @@ export const instanceMessages = {
|
||||
"instanceShell.empty.description": "Sélectionnez une session pour voir les messages",
|
||||
|
||||
"instanceShell.rightPanel.title": "Panneau d'état",
|
||||
"instanceShell.rightPanel.tabs.changes": "Modifications",
|
||||
"instanceShell.rightPanel.tabs.status": "Statut",
|
||||
"instanceShell.rightPanel.tabs.ariaLabel": "Onglets du panneau droit",
|
||||
"instanceShell.rightPanel.sections.sessionChanges": "Changements de session",
|
||||
"instanceShell.rightPanel.sections.plan": "Plan",
|
||||
"instanceShell.rightPanel.sections.backgroundProcesses": "Shells en arrière-plan",
|
||||
@@ -99,6 +102,13 @@ export const instanceMessages = {
|
||||
"instanceShell.sessionChanges.filesChanged": "{count} fichiers modifiés",
|
||||
"instanceShell.sessionChanges.actions.show": "Afficher les changements",
|
||||
|
||||
"instanceShell.filesShell.fileListTitle": "Liste des fichiers",
|
||||
"instanceShell.filesShell.mobileSelectorLabel": "Sélectionner un fichier",
|
||||
"instanceShell.filesShell.mobileSelectorEmpty": "Sélectionnez un fichier",
|
||||
"instanceShell.filesShell.viewerTitle": "Visionneuse de changements",
|
||||
"instanceShell.filesShell.viewerPlaceholder": "Le rendu détaillé sera ajouté à l'étape suivante.",
|
||||
"instanceShell.filesShell.viewerEmpty": "Aucun fichier sélectionné.",
|
||||
|
||||
"instanceShell.plan.noSessionSelected": "Sélectionnez une session pour voir le plan.",
|
||||
"instanceShell.plan.empty": "Aucun plan pour l'instant.",
|
||||
|
||||
|
||||
@@ -86,6 +86,9 @@ export const instanceMessages = {
|
||||
"instanceShell.empty.description": "メッセージを表示するにはセッションを選択してください",
|
||||
|
||||
"instanceShell.rightPanel.title": "ステータスパネル",
|
||||
"instanceShell.rightPanel.tabs.changes": "変更",
|
||||
"instanceShell.rightPanel.tabs.status": "ステータス",
|
||||
"instanceShell.rightPanel.tabs.ariaLabel": "右パネルのタブ",
|
||||
"instanceShell.rightPanel.sections.sessionChanges": "セッション変更",
|
||||
"instanceShell.rightPanel.sections.plan": "計画",
|
||||
"instanceShell.rightPanel.sections.backgroundProcesses": "バックグラウンドシェル",
|
||||
@@ -99,6 +102,13 @@ export const instanceMessages = {
|
||||
"instanceShell.sessionChanges.filesChanged": "{count} 個のファイルが変更されました",
|
||||
"instanceShell.sessionChanges.actions.show": "変更を表示",
|
||||
|
||||
"instanceShell.filesShell.fileListTitle": "ファイル一覧",
|
||||
"instanceShell.filesShell.mobileSelectorLabel": "ファイルを選択",
|
||||
"instanceShell.filesShell.mobileSelectorEmpty": "ファイルを選択してください",
|
||||
"instanceShell.filesShell.viewerTitle": "変更ビューア",
|
||||
"instanceShell.filesShell.viewerPlaceholder": "詳細な変更表示は次のステップで追加します。",
|
||||
"instanceShell.filesShell.viewerEmpty": "ファイルが選択されていません。",
|
||||
|
||||
"instanceShell.plan.noSessionSelected": "計画を表示するにはセッションを選択してください。",
|
||||
"instanceShell.plan.empty": "まだ計画はありません。",
|
||||
|
||||
|
||||
@@ -86,6 +86,9 @@ export const instanceMessages = {
|
||||
"instanceShell.empty.description": "Выберите сессию, чтобы просмотреть сообщения",
|
||||
|
||||
"instanceShell.rightPanel.title": "Панель состояния",
|
||||
"instanceShell.rightPanel.tabs.changes": "Изменения",
|
||||
"instanceShell.rightPanel.tabs.status": "Статус",
|
||||
"instanceShell.rightPanel.tabs.ariaLabel": "Вкладки правой панели",
|
||||
"instanceShell.rightPanel.sections.sessionChanges": "Изменения сессии",
|
||||
"instanceShell.rightPanel.sections.plan": "План",
|
||||
"instanceShell.rightPanel.sections.backgroundProcesses": "Фоновые Shell",
|
||||
@@ -99,6 +102,13 @@ export const instanceMessages = {
|
||||
"instanceShell.sessionChanges.filesChanged": "Изменено файлов: {count}",
|
||||
"instanceShell.sessionChanges.actions.show": "Показать изменения",
|
||||
|
||||
"instanceShell.filesShell.fileListTitle": "Список файлов",
|
||||
"instanceShell.filesShell.mobileSelectorLabel": "Выбрать файл",
|
||||
"instanceShell.filesShell.mobileSelectorEmpty": "Выберите файл",
|
||||
"instanceShell.filesShell.viewerTitle": "Просмотр изменений",
|
||||
"instanceShell.filesShell.viewerPlaceholder": "Подробный рендер изменений будет добавлен на следующем этапе.",
|
||||
"instanceShell.filesShell.viewerEmpty": "Файл не выбран.",
|
||||
|
||||
"instanceShell.plan.noSessionSelected": "Выберите сессию, чтобы просмотреть план.",
|
||||
"instanceShell.plan.empty": "Пока ничего не запланировано.",
|
||||
|
||||
|
||||
@@ -86,6 +86,9 @@ export const instanceMessages = {
|
||||
"instanceShell.empty.description": "选择会话以查看消息",
|
||||
|
||||
"instanceShell.rightPanel.title": "状态面板",
|
||||
"instanceShell.rightPanel.tabs.changes": "更改",
|
||||
"instanceShell.rightPanel.tabs.status": "状态",
|
||||
"instanceShell.rightPanel.tabs.ariaLabel": "右侧面板标签页",
|
||||
"instanceShell.rightPanel.sections.sessionChanges": "会话更改",
|
||||
"instanceShell.rightPanel.sections.plan": "计划",
|
||||
"instanceShell.rightPanel.sections.backgroundProcesses": "后台 Shell",
|
||||
@@ -99,6 +102,13 @@ export const instanceMessages = {
|
||||
"instanceShell.sessionChanges.filesChanged": "已更改 {count} 个文件",
|
||||
"instanceShell.sessionChanges.actions.show": "显示更改",
|
||||
|
||||
"instanceShell.filesShell.fileListTitle": "文件列表",
|
||||
"instanceShell.filesShell.mobileSelectorLabel": "选择文件",
|
||||
"instanceShell.filesShell.mobileSelectorEmpty": "请选择文件",
|
||||
"instanceShell.filesShell.viewerTitle": "更改查看器",
|
||||
"instanceShell.filesShell.viewerPlaceholder": "详细更改渲染将在下一步中添加。",
|
||||
"instanceShell.filesShell.viewerEmpty": "未选择文件。",
|
||||
|
||||
"instanceShell.plan.noSessionSelected": "选择会话以查看计划。",
|
||||
"instanceShell.plan.empty": "暂无计划。",
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@import "./panels/modal.css";
|
||||
@import "./panels/panel-shell.css";
|
||||
@import "./panels/session-layout.css";
|
||||
@import "./panels/right-panel.css";
|
||||
|
||||
|
||||
.tab-bar-instance {
|
||||
@@ -32,11 +33,13 @@
|
||||
.tab-active {
|
||||
background-color: var(--tab-active-bg);
|
||||
color: var(--tab-active-text);
|
||||
border-bottom: 2px solid var(--accent-primary);
|
||||
}
|
||||
|
||||
.tab-inactive {
|
||||
background-color: var(--tab-inactive-bg);
|
||||
color: var(--tab-inactive-text);
|
||||
border-bottom: 2px solid var(--tab-active-bg);
|
||||
}
|
||||
|
||||
.tab-inactive:hover {
|
||||
|
||||
358
packages/ui/src/styles/panels/right-panel.css
Normal file
358
packages/ui/src/styles/panels/right-panel.css
Normal file
@@ -0,0 +1,358 @@
|
||||
/* Right Panel / Drawer specific styles */
|
||||
|
||||
/* Right panel tab bar - browser-style tabs */
|
||||
.right-panel-tab-bar {
|
||||
background-color: var(--surface-secondary);
|
||||
border-bottom: 1px solid var(--border-base);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.right-panel-tab-bar::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 1px;
|
||||
background-color: var(--border-base);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.right-panel-tab-bar .tab-container {
|
||||
@apply flex items-center justify-between gap-1 px-2 pt-2 pb-0;
|
||||
}
|
||||
|
||||
/* Shortcuts on the left side - match left drawer icon colors */
|
||||
.tab-strip-shortcuts {
|
||||
@apply flex items-center gap-1 flex-shrink-0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Browser-style tabs - using INSTANCE tab color scheme */
|
||||
.right-panel-tab {
|
||||
@apply inline-flex items-center gap-2 px-4 py-2 text-xs font-medium transition-all duration-150 relative;
|
||||
font-family: var(--font-family-sans);
|
||||
outline: none;
|
||||
border: 1px solid transparent;
|
||||
border-bottom: none;
|
||||
border-radius: 8px 8px 0 0;
|
||||
margin-right: 2px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.right-panel-tab:focus-visible {
|
||||
@apply ring-2 ring-offset-1;
|
||||
ring-color: var(--accent-primary);
|
||||
ring-offset-color: var(--surface-secondary);
|
||||
}
|
||||
|
||||
.right-panel-tab-active {
|
||||
background-color: var(--tab-active-bg);
|
||||
border-color: transparent;
|
||||
border-bottom: 2px solid var(--accent-primary);
|
||||
color: var(--tab-active-text);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.right-panel-tab-inactive {
|
||||
background-color: var(--tab-inactive-bg);
|
||||
color: var(--tab-inactive-text);
|
||||
border-color: transparent;
|
||||
border-bottom: 2px solid var(--tab-active-bg);
|
||||
}
|
||||
|
||||
.right-panel-tab-inactive:hover {
|
||||
background-color: var(--tab-inactive-hover-bg);
|
||||
color: var(--text-secondary);
|
||||
border-color: var(--border-base);
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
/* Files tab layout */
|
||||
.files-tab-container {
|
||||
@apply flex flex-col h-full min-h-0 p-3 gap-3;
|
||||
}
|
||||
|
||||
.files-tab-header {
|
||||
@apply flex items-center justify-between gap-2 px-1;
|
||||
}
|
||||
|
||||
.files-tab-stats {
|
||||
@apply flex items-center gap-3 text-[11px];
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.files-tab-stat {
|
||||
@apply flex items-center gap-1.5;
|
||||
}
|
||||
|
||||
.files-tab-stat-value {
|
||||
@apply font-semibold;
|
||||
}
|
||||
|
||||
.files-tab-stat-additions {
|
||||
color: var(--session-status-idle-fg);
|
||||
}
|
||||
|
||||
.files-tab-stat-deletions {
|
||||
color: var(--session-status-working-fg);
|
||||
}
|
||||
|
||||
/* File list panel */
|
||||
.file-list-panel {
|
||||
@apply rounded-lg border flex flex-col min-h-0;
|
||||
background-color: var(--surface-secondary);
|
||||
border-color: var(--border-base);
|
||||
}
|
||||
|
||||
.file-list-header {
|
||||
@apply flex items-center justify-between gap-2 px-3 py-2 border-b;
|
||||
border-color: var(--border-base);
|
||||
background-color: var(--surface-secondary);
|
||||
}
|
||||
|
||||
.file-list-title {
|
||||
@apply text-[11px] font-semibold uppercase tracking-wide;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.file-list-count {
|
||||
@apply text-[10px] px-1.5 py-0.5 rounded-full;
|
||||
background-color: var(--surface-base);
|
||||
color: var(--text-muted);
|
||||
border: 1px solid var(--border-base);
|
||||
}
|
||||
|
||||
.file-list-scroll {
|
||||
@apply flex-1 overflow-y-auto min-h-0;
|
||||
}
|
||||
|
||||
.file-list-item {
|
||||
@apply px-3 py-2.5 border-b cursor-pointer transition-all duration-150;
|
||||
border-color: var(--border-base);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.file-list-item:last-child {
|
||||
@apply border-b-0;
|
||||
}
|
||||
|
||||
.file-list-item:hover {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
.file-list-item-active {
|
||||
background-color: var(--surface-base);
|
||||
box-shadow: inset 0 0 0 1px var(--accent-primary);
|
||||
}
|
||||
|
||||
.file-list-item-content {
|
||||
@apply flex items-center justify-between gap-3;
|
||||
}
|
||||
|
||||
.file-list-item-path {
|
||||
@apply text-xs font-mono min-w-0 flex-1 overflow-hidden whitespace-nowrap;
|
||||
color: var(--text-primary);
|
||||
text-overflow: ellipsis;
|
||||
direction: rtl;
|
||||
text-align: left;
|
||||
unicode-bidi: plaintext;
|
||||
}
|
||||
|
||||
.file-list-item-stats {
|
||||
@apply flex items-center gap-2 text-[11px] flex-shrink-0;
|
||||
}
|
||||
|
||||
.file-list-item-additions {
|
||||
color: var(--session-status-idle-fg);
|
||||
}
|
||||
|
||||
.file-list-item-deletions {
|
||||
color: var(--session-status-working-fg);
|
||||
}
|
||||
|
||||
/* File viewer panel */
|
||||
.file-viewer-panel {
|
||||
@apply rounded-lg border flex flex-col min-h-0;
|
||||
background-color: var(--surface-secondary);
|
||||
border-color: var(--border-base);
|
||||
}
|
||||
|
||||
.file-viewer-header {
|
||||
@apply flex items-center gap-2 px-3 py-2 border-b;
|
||||
border-color: var(--border-base);
|
||||
background-color: var(--surface-secondary);
|
||||
}
|
||||
|
||||
.file-viewer-title {
|
||||
@apply text-[11px] font-semibold uppercase tracking-wide;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.file-viewer-content {
|
||||
@apply flex-1 p-4 overflow-auto min-h-0;
|
||||
}
|
||||
|
||||
.file-viewer-empty {
|
||||
@apply flex flex-col items-center justify-center h-full gap-3 text-center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.file-viewer-empty-icon {
|
||||
@apply w-8 h-8 opacity-40;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.file-viewer-empty-text {
|
||||
@apply text-xs;
|
||||
}
|
||||
|
||||
.file-viewer-placeholder {
|
||||
@apply text-xs leading-relaxed;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.file-viewer-selected-file {
|
||||
@apply flex flex-col gap-2;
|
||||
}
|
||||
|
||||
.file-viewer-file-name {
|
||||
@apply text-xs font-mono px-2 py-1.5 rounded border;
|
||||
background-color: var(--surface-base);
|
||||
border-color: var(--border-base);
|
||||
color: var(--text-primary);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* Mobile file selector */
|
||||
.mobile-file-selector {
|
||||
@apply rounded-lg border flex flex-col gap-3 p-3;
|
||||
background-color: var(--surface-secondary);
|
||||
border-color: var(--border-base);
|
||||
}
|
||||
|
||||
.mobile-file-selector-label {
|
||||
@apply text-[11px] font-semibold uppercase tracking-wide;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.mobile-file-selector-trigger {
|
||||
@apply w-full;
|
||||
}
|
||||
|
||||
.mobile-file-viewer {
|
||||
@apply rounded-lg border flex flex-col min-h-[220px];
|
||||
background-color: var(--surface-secondary);
|
||||
border-color: var(--border-base);
|
||||
}
|
||||
|
||||
/* Status tab layout */
|
||||
.status-tab-container {
|
||||
@apply flex flex-col h-full min-h-0;
|
||||
}
|
||||
|
||||
.status-tab-context-panel {
|
||||
@apply border-b;
|
||||
border-color: var(--border-base);
|
||||
background-color: var(--surface-secondary);
|
||||
}
|
||||
|
||||
/* Accordion improvements for right panel */
|
||||
.right-panel-accordion {
|
||||
@apply flex flex-col;
|
||||
}
|
||||
|
||||
.right-panel-accordion-item {
|
||||
@apply border-b last:border-b-0;
|
||||
border-color: var(--border-base);
|
||||
background-color: var(--surface-secondary);
|
||||
}
|
||||
|
||||
.right-panel-accordion-trigger {
|
||||
@apply w-full flex items-center justify-between gap-3 px-3 py-2.5 text-[11px] font-semibold uppercase tracking-wide transition-colors duration-150;
|
||||
color: var(--text-secondary);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.right-panel-accordion-trigger:hover {
|
||||
background-color: var(--surface-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.right-panel-accordion-chevron {
|
||||
@apply h-4 w-4 transition-transform duration-200;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.right-panel-accordion-chevron-expanded {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.right-panel-accordion-content {
|
||||
@apply w-full px-3 pb-3 text-sm;
|
||||
color: var(--text-primary);
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.right-panel-accordion-content [data-accordion-content] {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Background process cards in status panel */
|
||||
.status-process-card {
|
||||
@apply rounded-lg border flex flex-col gap-2 p-3 transition-all duration-150;
|
||||
background-color: var(--surface-base);
|
||||
border-color: var(--border-base);
|
||||
}
|
||||
|
||||
.status-process-card:hover {
|
||||
border-color: var(--border-strong);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.status-process-header {
|
||||
@apply flex flex-col gap-1;
|
||||
}
|
||||
|
||||
.status-process-title {
|
||||
@apply text-xs font-semibold;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.status-process-meta {
|
||||
@apply flex flex-wrap gap-2 text-[11px];
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.status-process-actions {
|
||||
@apply grid grid-cols-3 gap-2;
|
||||
}
|
||||
|
||||
/* Empty states */
|
||||
.right-panel-empty {
|
||||
@apply flex flex-col items-center justify-center text-center gap-2;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.right-panel-empty--left {
|
||||
@apply items-start justify-start text-left w-full;
|
||||
}
|
||||
|
||||
.right-panel-empty-text {
|
||||
@apply text-xs;
|
||||
}
|
||||
|
||||
/* Dark mode adjustments */
|
||||
[data-theme="dark"] .right-panel-tab-active {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .file-list-item-active {
|
||||
box-shadow: inset 0 0 0 1px var(--accent-primary), 0 0 0 1px var(--surface-secondary);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme]) .right-panel-tab-active {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
}
|
||||
@@ -58,11 +58,13 @@
|
||||
.tab-active {
|
||||
background-color: var(--tab-active-bg);
|
||||
color: var(--tab-active-text);
|
||||
border-bottom: 2px solid var(--accent-primary);
|
||||
}
|
||||
|
||||
.tab-inactive {
|
||||
background-color: var(--tab-inactive-bg);
|
||||
color: var(--tab-inactive-text);
|
||||
border-bottom: 2px solid var(--tab-active-bg);
|
||||
}
|
||||
|
||||
.tab-inactive:hover {
|
||||
|
||||
Reference in New Issue
Block a user