Files
ottyl/webapp/src/components/CollapsibleSection.tsx
T
Hugh KaznowskiandClaude Sonnet 5 7db1e0f055 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
2026-09-05 16:46:22 +01:00

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>
)
}