'use client'; import { PanelRightClose, PanelRightOpen, Pause, Play, SlidersHorizontal } from 'lucide-react'; import Link from 'next/link'; import { type ReactNode, useCallback, useEffect, useRef, useState } from 'react'; import { Hint } from '@/components/ui/hint'; import { Sheet } from '@/components/ui/sheet'; import { cn } from '@/lib/cn'; /* Terminal layout primitives (density-aware through the CSS variables in globals.css): - TerminalLayout: desktop 3-pane (filter rail 15rem sticky · main · inspector 22rem sticky, collapsible, state in localStorage); mobile: filters and inspector in bottom sheets. - FilterSheet: the mobile bottom sheet used for filters (also usable standalone). - DataStrip: horizontal live counters with definition tooltips. - Ticker: scrolling strip of recent events, pausable, reduced-motion aware. - SectionNav: sticky in-page anchor nav with the active section highlighted. */ const INSPECTOR_KEY = 'aia-inspector'; export function TerminalLayout({ filters, children, inspector, filtersTitle = 'Filters', inspectorTitle = 'Inspector', storageKey = INSPECTOR_KEY, className, filterCount, wide = true, }: { filters?: ReactNode; children: ReactNode; inspector?: ReactNode; filtersTitle?: string; inspectorTitle?: string; /** localStorage key for the inspector collapsed state (per page when needed). */ storageKey?: string; className?: string; /** Number of active filters, shown on the mobile filter button. */ filterCount?: number; wide?: boolean; }) { const [collapsed, setCollapsed] = useState(false); const [ready, setReady] = useState(false); const [filtersOpen, setFiltersOpen] = useState(false); const [inspectorOpen, setInspectorOpen] = useState(false); useEffect(() => { try { setCollapsed(localStorage.getItem(storageKey) === 'collapsed'); } catch { /* ignore */ } setReady(true); }, [storageKey]); const toggle = useCallback(() => { setCollapsed((c) => { try { localStorage.setItem(storageKey, c ? 'open' : 'collapsed'); } catch { /* ignore */ } return !c; }); }, [storageKey]); const showInspector = !!inspector && (!ready || !collapsed); return (
{/* mobile toolbar */} {(filters || inspector) && (
{filters && ( )} {inspector && ( )}
)}
{filters && ( )}
{inspector && (
)} {children}
{showInspector && ( )}
{filters && ( setFiltersOpen(false)} title={filtersTitle}> {filters} )} {inspector && ( setInspectorOpen(false)} side="bottom" eyebrow="Inspector" title={inspectorTitle}> {inspector} )}
); } /** Mobile bottom sheet for filters; the filter form is server-rendered by the page and simply moved inside. */ export function FilterSheet({ open, onClose, title = 'Filters', children }: { open: boolean; onClose: () => void; title?: string; children: ReactNode }) { return ( Done}> {children} ); } /* ---------------------------------------------------------------------------------------------------------- DataStrip */ export type StripItem = { label: string; value: ReactNode; hint?: ReactNode; definition?: string; href?: string; delta?: { value: string; tone?: 'positive' | 'negative' | 'neutral' }; live?: boolean }; /** Horizontal live counters strip: label + big tabular number + optional delta and a "How counted" definition tooltip. */ export function DataStrip({ items, className, dense = false }: { items: StripItem[]; className?: string; dense?: boolean }) { return (
4 ? 'md:grid-cols-4 xl:grid-cols-[repeat(var(--n),minmax(0,1fr))]' : 'md:grid-cols-[repeat(var(--n),minmax(0,1fr))]')} style={{ ['--n' as string]: items.length }}> {items.map((it, i) => { const body = ( <>

{it.live && } {it.label} {it.definition && = items.length - 2 ? 'right' : 'left'} />}

{it.value}

{(it.delta || it.hint) && (

{it.delta && {it.delta.value}} {it.hint}

)} ); const cls = cn('block min-w-[9rem] border-r border-rule px-3 py-2.5 last:border-r-0 md:min-w-0 md:[&:nth-child(4n)]:border-r-0 md:[&:nth-child(4n+1)]:pl-0 md:[&:nth-child(n+5)]:border-t xl:[&:nth-child(4n)]:border-r xl:[&:nth-child(4n+1)]:pl-3 xl:[&:nth-child(n+5)]:border-t-0 xl:first:pl-0 xl:last:border-r-0', items.length <= 4 && 'md:first:pl-0', it.href && 'hover:bg-surface-2'); return it.href ? ( {body} ) : (
{body}
); })}
); } /* ------------------------------------------------------------------------------------------------------------- Ticker */ export type TickerItem = { id: string; label: ReactNode; href?: string; tone?: 'new' | 'price' | 'warn' | 'danger' | 'bench' | 'neutral'; meta?: ReactNode }; const TICK_TONE: Record, string> = { new: 'text-positive', price: 'text-accent-2', warn: 'text-warning', danger: 'text-danger', bench: 'text-type-benchmark', neutral: 'text-ink-3' }; /** Horizontal scrolling strip of recent events (duplicated track for a seamless loop). Pausable; static + scrollable under reduced motion. */ export function Ticker({ items, className, speed = 60, label = 'Live' }: { items: TickerItem[]; className?: string; /** seconds per loop */ speed?: number; label?: string }) { const [paused, setPaused] = useState(false); const [reduced, setReduced] = useState(false); useEffect(() => { const m = window.matchMedia('(prefers-reduced-motion: reduce)'); setReduced(m.matches); const on = () => setReduced(m.matches); m.addEventListener('change', on); return () => m.removeEventListener('change', on); }, []); if (!items.length) return null; const track = (dup: boolean) => ( ); return (
{label}
{track(false)} {!reduced && track(true)}
{!reduced && ( )}
); } /* --------------------------------------------------------------------------------------------------------- SectionNav */ /** Sticky in-page anchor nav; the section currently in view is highlighted (IntersectionObserver). */ export function SectionNav({ items, className }: { items: { id: string; label: string }[]; className?: string }) { const [activeId, setActiveId] = useState(null); const seen = useRef>(new Map()); useEffect(() => { const els = items.map((i) => document.getElementById(i.id)).filter((x): x is HTMLElement => !!x); if (!els.length) return; const io = new IntersectionObserver( (entries) => { for (const e of entries) seen.current.set(e.target.id, e.isIntersecting); const first = items.find((i) => seen.current.get(i.id)); if (first) setActiveId(first.id); }, { rootMargin: '-30% 0px -60% 0px', threshold: 0 }, ); els.forEach((el) => io.observe(el)); return () => io.disconnect(); }, [items]); return ( ); }