Add collapsible primary UI sections (ticket 035)
Log viewer (FilterBar+LogTimeline+LogPreview), Params, Metrics, and Dashboards can each be collapsed/expanded via a toggle next to their existing help icon. Collapsed state persists per-section in localStorage (ottyl.collapsedSections) and content is hidden with the `hidden` attribute rather than unmounted, so in-progress form state (a half-filled Metrics/Params form, in-progress Dashboards panel edits) survives a collapse/expand cycle. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
dd1a0a234d
commit
7db1e0f055
@@ -0,0 +1,59 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
/// localStorage key for which of the four primary sections (log viewer,
|
||||
/// params, metrics, dashboards) are collapsed (ticket 035). Mirrors
|
||||
/// LogPreview's VISIBLE_PARAM_COLUMNS_STORAGE_KEY pattern — one key holding
|
||||
/// a record of section id -> collapsed.
|
||||
const COLLAPSED_SECTIONS_STORAGE_KEY = 'ottyl.collapsedSections'
|
||||
|
||||
function loadCollapsedSections(): Record<string, boolean> {
|
||||
try {
|
||||
const raw = localStorage.getItem(COLLAPSED_SECTIONS_STORAGE_KEY)
|
||||
if (!raw) return {}
|
||||
const parsed = JSON.parse(raw)
|
||||
return parsed && typeof parsed === 'object' ? parsed : {}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
function saveCollapsedSections(sections: Record<string, boolean>) {
|
||||
try {
|
||||
localStorage.setItem(COLLAPSED_SECTIONS_STORAGE_KEY, JSON.stringify(sections))
|
||||
} catch {
|
||||
// best-effort — a private browsing mode or full storage just means the
|
||||
// collapsed state won't persist across reloads.
|
||||
}
|
||||
}
|
||||
|
||||
/// Persists a section's collapsed state across reloads. Deliberately
|
||||
/// returns only the boolean + setter — callers keep rendering their
|
||||
/// subtree, just hide it with CSS (`hidden`), so collapsing never unmounts
|
||||
/// in-progress form state.
|
||||
export function useSectionCollapsed(sectionId: string): [boolean, (next: boolean) => void] {
|
||||
const [collapsed, setCollapsedState] = useState(() => loadCollapsedSections()[sectionId] ?? false)
|
||||
|
||||
function setCollapsed(next: boolean) {
|
||||
setCollapsedState(next)
|
||||
const all = loadCollapsedSections()
|
||||
all[sectionId] = next
|
||||
saveCollapsedSections(all)
|
||||
}
|
||||
|
||||
return [collapsed, setCollapsed]
|
||||
}
|
||||
|
||||
export function CollapseToggle({ collapsed, onToggle }: { collapsed: boolean; onToggle: () => void }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggle}
|
||||
title={collapsed ? 'Expand section' : 'Collapse section'}
|
||||
aria-label={collapsed ? 'Expand section' : 'Collapse section'}
|
||||
aria-expanded={!collapsed}
|
||||
className="flex h-4 w-4 shrink-0 items-center justify-center rounded-full border border-border-soft text-[10px] leading-none text-text-faint hover:border-border-strong hover:text-text-secondary"
|
||||
>
|
||||
{collapsed ? '▸' : '▾'}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user