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
60 lines
2.2 KiB
TypeScript
60 lines
2.2 KiB
TypeScript
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>
|
|
)
|
|
}
|