import { useState } from 'react' import type { TimeRange, TimeRangePresetId } from '../lib/timeRange' /// Shared look for a primary section's title bar: shaded so it reads as a /// distinct band rather than blending into the section body below it. /// The bar itself is the collapse/expand click target (see `sectionHeaderInteractiveClassName`) — /// this className alone (no click handling) is for the rare header that stays static. export const sectionHeaderClassName = 'flex items-center justify-between gap-2 border-b border-border-soft bg-surface-1 px-3 py-1.5' /// `sectionHeaderClassName` plus the affordance for a header row that toggles collapse on click: /// pointer cursor and a hover tint so the whole bar reads as clickable, not just the chevron. export const sectionHeaderInteractiveClassName = `${sectionHeaderClassName} cursor-pointer select-none hover:bg-surface-2` /// Left-hand cluster of a section header: title text, help icon, collapse/pop-out controls. export const sectionHeaderLeftClassName = 'flex items-center gap-1.5 text-sm font-medium text-text-secondary' /// localStorage key holding a record of section id -> collapsed, mirroring LogPreview's VISIBLE_PARAM_COLUMNS_STORAGE_KEY pattern. const COLLAPSED_SECTIONS_STORAGE_KEY = 'ottyl.collapsedSections' function loadCollapsedSections(): Record { 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) { 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. } } /// Returns only the boolean + setter, not a conditional-render helper — callers keep rendering their subtree and 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] } /// Chevron indicator for a section's collapsed/expanded state. For the title-bar sections /// (Alerts/Metrics/Params/Dashboards), the whole header row is the click target (see /// `sectionHeaderInteractiveClassName`) and `onToggle` is omitted — this renders as a /// non-interactive, decorative `span` rather than a nested ` ) } /// Keyboard handler for a header row using `sectionHeaderInteractiveClassName`: Enter/Space /// activate it like a button, matching the `role="button"` semantics on the row. export function handleSectionHeaderKeyDown(onToggle: () => void) { return (e: React.KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault() onToggle() } } } /// The four primary sections poppable into their own tab; matches the `?section=` query param a standalone page reads. export type PopOutSectionId = 'log' | 'params' | 'metrics' | 'dashboards' /// A rolling preset (anything but a manually dragged/fixed range) is carried over by id so the popped-out tab can resolve and keep re-resolving its own independent window of the same size, instead of freezing whatever absolute bounds happened to be active at click time. A fixed/dragged range has no such id, so it's carried over as absolute bounds, same as before. function popOutUrl(section: PopOutSectionId, options?: { range?: TimeRange | null; presetId?: TimeRangePresetId | null }): string { const query = new URLSearchParams({ section }) if (options?.presetId != null) { query.set('preset', options.presetId) } else if (options?.range) { query.set('startMs', String(options.range.startMs)) query.set('endMs', String(options.range.endMs)) } return `?${query.toString()}` } /// `range`/`activePreset` are only needed for sections without their own LogTimeline — the popped-out page can't pick a range itself until it mounts, so the current one is carried over via query params. export function PopOutButton({ section, range, activePreset, }: { section: PopOutSectionId range?: TimeRange | null activePreset?: TimeRangePresetId | null }) { return ( ) }