'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) => (
{items.map((it) => (
-
{it.href ? (
{it.label}
) : (
{it.label}
)}
{it.meta && {it.meta}}
))}
);
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