Redesign home command deck

This commit is contained in:
dirtydishes 2026-05-28 05:10:21 -04:00
parent b075a0994c
commit a35a757622
4 changed files with 1325 additions and 24 deletions

View file

@ -769,6 +769,395 @@ h3 {
grid-column: 1 / -1;
}
.command-deck-shell {
display: grid;
gap: 12px;
}
.command-deck-header {
min-width: 0;
display: grid;
grid-template-columns: minmax(220px, 0.8fr) minmax(260px, 1fr) auto;
gap: 14px;
align-items: center;
padding: 13px 14px;
border: 1px solid var(--border);
border-radius: 12px;
background: linear-gradient(180deg, oklch(0.18 0.013 250 / 0.96), oklch(0.145 0.012 250 / 0.96));
}
.command-deck-brand {
min-width: 0;
display: flex;
align-items: center;
gap: 11px;
}
.command-deck-mark {
width: 34px;
height: 34px;
flex: 0 0 auto;
border: 1px solid var(--border-strong);
border-radius: 8px;
background:
linear-gradient(135deg, oklch(0.78 0.12 74 / 0.7), oklch(0.28 0.035 250)),
var(--accent-soft);
}
.command-deck-kicker,
.command-pane-meta,
.command-health-row,
.command-replay-strip,
.command-ticker-card {
font-family: var(--font-mono), monospace;
}
.command-deck-kicker {
display: block;
color: var(--text-faint);
font-size: 0.68rem;
letter-spacing: 0.12em;
text-transform: lowercase;
}
.command-deck-brand h2 {
margin: 1px 0 0;
font-family: var(--font-display), sans-serif;
font-size: 1.2rem;
line-height: 1.05;
letter-spacing: 0;
}
.command-deck-brief {
min-width: 0;
display: flex;
align-items: center;
gap: 9px;
flex-wrap: wrap;
color: var(--text-dim);
font-size: 0.82rem;
}
.command-deck-brief strong {
color: var(--text);
font-family: var(--font-mono), monospace;
font-size: 0.86rem;
}
.command-deck-controls {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 8px;
flex-wrap: wrap;
}
.command-chip {
min-height: 32px;
display: inline-flex;
align-items: center;
border: 1px solid var(--border);
border-radius: 999px;
padding: 5px 9px;
background: var(--bg-soft);
color: var(--text-dim);
font-family: var(--font-mono), monospace;
font-size: 0.68rem;
text-transform: uppercase;
}
.command-chip-connected {
color: var(--green);
background: var(--green-soft);
}
.command-chip-stale,
.command-chip-connecting {
color: var(--accent);
background: var(--accent-soft);
}
.command-chip-disconnected {
color: var(--red);
background: var(--red-soft);
}
.command-ticker-rail {
min-width: 0;
overflow: hidden;
border: 1px solid var(--border);
border-radius: 10px;
background: oklch(0.13 0.012 250 / 0.98);
}
.command-ticker-track {
display: grid;
grid-auto-columns: minmax(176px, 1fr);
grid-auto-flow: column;
gap: 8px;
overflow-x: auto;
padding: 7px;
}
.command-ticker-card {
min-width: 176px;
min-height: 64px;
display: grid;
grid-template-columns: 1fr auto;
gap: 4px 9px;
align-items: center;
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px 10px;
background: oklch(0.17 0.013 250);
color: inherit;
text-align: left;
cursor: pointer;
}
.command-ticker-card:hover,
.command-ticker-card:focus-visible {
border-color: var(--border-strong);
outline: none;
}
.command-ticker-symbol {
color: var(--text);
font-weight: 700;
}
.command-ticker-price,
.command-ticker-meta {
color: var(--text-dim);
font-size: 0.72rem;
}
.command-ticker-move {
justify-self: end;
color: var(--text-faint);
font-size: 0.68rem;
}
.command-ticker-card.is-up .command-ticker-move {
color: var(--green);
}
.command-ticker-card.is-down .command-ticker-move {
color: var(--red);
}
.command-ticker-meta {
grid-column: 1 / -1;
}
.command-deck-grid {
display: grid;
grid-template-columns: minmax(360px, 1.12fr) minmax(420px, 1.38fr) minmax(300px, 0.9fr);
grid-template-areas:
"tape chart signals"
"feed dark context"
"replay replay replay";
gap: 10px;
align-items: stretch;
}
.command-deck-grid > .terminal-pane {
border-radius: 10px;
}
.command-deck-grid > :nth-child(1) {
grid-area: tape;
min-height: 386px;
}
.command-deck-grid > :nth-child(2) {
grid-area: chart;
min-height: 386px;
}
.command-deck-grid > :nth-child(3) {
grid-area: signals;
min-height: 386px;
}
.command-deck-grid > :nth-child(4) {
grid-area: feed;
min-height: 278px;
}
.command-deck-grid > :nth-child(5) {
grid-area: dark;
min-height: 278px;
}
.command-deck-grid > :nth-child(6) {
grid-area: context;
min-height: 278px;
}
.command-deck-grid > :nth-child(7) {
grid-area: replay;
min-height: 116px;
}
.command-deck-grid .terminal-pane-head {
min-height: 42px;
padding: 10px 12px;
}
.command-deck-grid .terminal-pane-body {
padding: 10px 12px 12px;
}
.command-deck-grid .terminal-pane-title {
font-family: var(--font-mono), monospace;
font-size: 0.72rem;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.command-deck-grid .chart-surface {
height: 300px;
}
.command-pane-meta {
color: var(--text-faint);
font-size: 0.68rem;
text-transform: uppercase;
}
.command-health-list,
.command-context-list,
.command-replay-strip {
display: grid;
gap: 8px;
}
.command-health-row {
min-height: 34px;
display: grid;
grid-template-columns: minmax(96px, 1fr) 92px 76px 92px;
gap: 8px;
align-items: center;
border-bottom: 1px solid oklch(0.72 0.012 250 / 0.09);
color: var(--text-dim);
font-size: 0.72rem;
}
.command-health-row:last-child {
border-bottom: 0;
}
.command-health-status {
width: fit-content;
max-width: 100%;
display: inline-flex;
align-items: center;
min-height: 22px;
border: 1px solid var(--border);
border-radius: 999px;
padding: 3px 7px;
font-size: 0.64rem;
}
.command-health-connected {
color: var(--green);
background: var(--green-soft);
}
.command-health-stale,
.command-health-connecting {
color: var(--accent);
background: var(--accent-soft);
}
.command-health-disconnected {
color: var(--red);
background: var(--red-soft);
}
.command-context-row {
min-width: 0;
min-height: 42px;
display: grid;
grid-template-columns: 62px 52px minmax(0, 1fr);
gap: 4px 8px;
align-items: center;
border: 0;
border-bottom: 1px solid oklch(0.72 0.012 250 / 0.09);
padding: 6px 0;
background: transparent;
color: inherit;
text-align: left;
cursor: pointer;
}
.command-context-row:last-child {
border-bottom: 0;
}
.command-context-row time,
.command-context-row span {
color: var(--text-dim);
font-family: var(--font-mono), monospace;
font-size: 0.68rem;
}
.command-context-row strong {
min-width: 0;
overflow: hidden;
color: var(--text);
font-size: 0.78rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.command-context-row > span:last-child {
grid-column: 3;
overflow: hidden;
color: var(--text-faint);
text-overflow: ellipsis;
white-space: nowrap;
}
.command-context-kind {
width: fit-content;
border: 1px solid var(--border);
border-radius: 999px;
padding: 2px 6px;
background: var(--bg-soft);
text-transform: uppercase;
}
.command-replay-strip {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.command-replay-strip div {
min-width: 0;
display: grid;
gap: 3px;
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px 10px;
background: var(--bg-soft);
}
.command-replay-strip span {
color: var(--text-faint);
font-size: 0.66rem;
text-transform: uppercase;
}
.command-replay-strip strong {
min-width: 0;
overflow: hidden;
color: var(--text);
font-size: 0.78rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.terminal-pane {
min-width: 0;
height: 100%;
@ -2027,6 +2416,25 @@ h3 {
min-width: 136px;
padding: 10px 12px;
}
.command-deck-header {
grid-template-columns: minmax(220px, 0.8fr) minmax(240px, 1fr);
}
.command-deck-controls {
grid-column: 1 / -1;
justify-content: flex-start;
}
.command-deck-grid {
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
grid-template-areas:
"signals chart"
"tape chart"
"feed context"
"dark dark"
"replay replay";
}
}
@media (max-width: 980px) {
@ -2065,6 +2473,32 @@ h3 {
min-height: 0;
}
.command-deck-grid {
grid-template-columns: minmax(0, 1fr);
grid-template-areas:
"signals"
"chart"
"tape"
"context"
"replay"
"feed"
"dark";
}
.command-deck-grid > .terminal-pane {
min-height: 0;
}
.command-deck-grid > :nth-child(1),
.command-deck-grid > :nth-child(2),
.command-deck-grid > :nth-child(3),
.command-deck-grid > :nth-child(4),
.command-deck-grid > :nth-child(5),
.command-deck-grid > :nth-child(6),
.command-deck-grid > :nth-child(7) {
min-height: 0;
}
.terminal-topbar {
align-items: center;
justify-content: space-between;
@ -2129,11 +2563,32 @@ h3 {
.terminal-pane-head,
.chart-controls,
.card-controls,
.terminal-pane-actions {
.terminal-pane-actions,
.command-deck-header {
flex-direction: column;
align-items: flex-start;
}
.command-deck-header {
display: flex;
}
.command-deck-brief,
.command-deck-controls {
width: 100%;
justify-content: flex-start;
}
.command-deck-controls .terminal-button,
.command-chip {
width: 100%;
justify-content: center;
}
.command-ticker-track {
grid-auto-columns: minmax(164px, 78vw);
}
.terminal-pane-title-row {
flex-direction: column;
align-items: flex-start;
@ -2311,6 +2766,37 @@ h3 {
height: 48px;
}
.command-deck-grid .chart-surface {
height: 320px;
}
.command-health-row {
grid-template-columns: minmax(94px, 1fr) 92px;
}
.command-health-row span:nth-child(3),
.command-health-row span:nth-child(4) {
font-size: 0.68rem;
}
.command-context-row {
grid-template-columns: 58px minmax(0, 1fr);
}
.command-context-kind {
grid-column: 2;
grid-row: 1;
}
.command-context-row strong,
.command-context-row > span:last-child {
grid-column: 2;
}
.command-replay-strip {
grid-template-columns: minmax(0, 1fr);
}
.time {
text-align: left;
}

View file

@ -352,10 +352,10 @@ export const getRouteFeatures = (pathname: string): RouteFeatures => {
case "/":
default:
return {
options: false,
options: true,
nbbo: false,
equities: true,
flow: false,
flow: true,
news: true,
alerts: true,
smartMoney: true,
@ -364,17 +364,17 @@ export const getRouteFeatures = (pathname: string): RouteFeatures => {
equityJoins: true,
equityCandles: true,
equityOverlay: true,
showOptionsPane: false,
showOptionsPane: true,
showEquitiesPane: true,
showFlowPane: false,
showFlowPane: true,
showNewsPane: true,
showAlertsPane: true,
showClassifierPane: false,
showDarkPane: false,
showDarkPane: true,
showChartPane: true,
showFocusPane: false,
showReplayConsole: false,
needsClassifierDecor: false,
needsClassifierDecor: true,
needsAlertEvidencePrefetch: true,
needsDarkUnderlying: true
};
@ -4215,24 +4215,24 @@ const CandleChart = ({
width,
height,
layout: {
background: { color: "#fffdf7" },
textColor: "#4e3e25"
background: { color: "#0d141b" },
textColor: "#90a0b2"
},
grid: {
vertLines: { color: "rgba(82, 64, 36, 0.12)" },
horzLines: { color: "rgba(82, 64, 36, 0.12)" }
vertLines: { color: "rgba(144, 160, 178, 0.12)" },
horzLines: { color: "rgba(144, 160, 178, 0.12)" }
},
crosshair: {
vertLine: { color: "rgba(47, 109, 79, 0.35)" },
horzLine: { color: "rgba(47, 109, 79, 0.35)" }
vertLine: { color: "rgba(245, 166, 35, 0.32)" },
horzLine: { color: "rgba(245, 166, 35, 0.32)" }
},
timeScale: {
borderColor: "rgba(111, 91, 57, 0.35)",
borderColor: "rgba(144, 160, 178, 0.24)",
timeVisible: true,
secondsVisible: intervalMs < 60000
},
rightPriceScale: {
borderColor: "rgba(111, 91, 57, 0.35)"
borderColor: "rgba(144, 160, 178, 0.24)"
}
});
@ -4250,11 +4250,11 @@ const CandleChart = ({
overlayCtxRef.current = overlayCanvas.getContext("2d");
const series = chart.addCandlestickSeries({
upColor: "#2f6d4f",
downColor: "#c46f2a",
upColor: "#25c17a",
downColor: "#ff6b5f",
borderVisible: false,
wickUpColor: "#2f6d4f",
wickDownColor: "#c46f2a"
wickUpColor: "#25c17a",
wickDownColor: "#ff6b5f"
});
chartRef.current = chart;
@ -8397,6 +8397,278 @@ const ChartPane = memo(({ state, title = "Chart" }: ChartPaneProps) => {
);
});
type CommandDeckTicker = {
symbol: string;
price: number | null;
move: number | null;
options: number;
alerts: number;
};
const buildCommandDeckTickers = (state: TerminalState): CommandDeckTicker[] => {
const symbols = new Set<string>();
for (const symbol of state.activeTickers) {
symbols.add(symbol);
}
for (const print of state.filteredEquities.slice(0, 80)) {
symbols.add(print.underlying_id.toUpperCase());
}
for (const print of state.filteredOptions.slice(0, 80)) {
const parsed = parseOptionContractId(normalizeContractId(print.option_contract_id));
const symbol = (print.underlying_id ?? parsed?.root ?? extractUnderlying(print.option_contract_id))?.toUpperCase();
if (symbol) {
symbols.add(symbol);
}
}
for (const event of state.filteredSmartMoneyEvents.slice(0, 30)) {
symbols.add(event.underlying_id.toUpperCase());
}
for (const story of state.filteredNews.slice(0, 20)) {
for (const symbol of story.resolved_symbols) {
symbols.add(symbol.toUpperCase());
}
}
if (symbols.size === 0) {
symbols.add(state.chartTicker.toUpperCase());
}
return Array.from(symbols).slice(0, 10).map((symbol) => {
const equityPrints = state.filteredEquities
.filter((print) => print.underlying_id.toUpperCase() === symbol)
.slice(0, 2);
const price = equityPrints[0]?.price ?? null;
const previous = equityPrints[1]?.price ?? null;
const move = price !== null && previous !== null && previous !== 0 ? (price - previous) / previous : null;
const options = state.filteredOptions
.slice(0, 120)
.filter((print) => {
const parsed = parseOptionContractId(normalizeContractId(print.option_contract_id));
const underlying = (print.underlying_id ?? parsed?.root ?? extractUnderlying(print.option_contract_id))?.toUpperCase();
return underlying === symbol;
}).length;
const alerts = state.filteredAlerts
.slice(0, 80)
.filter((alert) => alert.trace_id.toUpperCase().includes(symbol)).length;
return { symbol, price, move, options, alerts };
});
};
const CommandDeckHeader = ({ state }: { state: TerminalState }) => {
const focus = state.activeTickers.length > 0 ? state.activeTickers.join(", ") : state.chartTicker;
const selected = state.selectedInstrumentLabel ?? "No contract lock";
const connectionLabel = state.mode === "live" ? statusLabel(state.liveSession.status, false, state.mode) : "Replay";
return (
<header className="command-deck-header" aria-label="Command deck context">
<div className="command-deck-brand">
<span className="command-deck-mark" aria-hidden="true" />
<div>
<span className="command-deck-kicker">islandflow</span>
<h2>Command Deck</h2>
</div>
</div>
<div className="command-deck-brief">
<span>Evidence console</span>
<strong>{focus}</strong>
<span>{selected}</span>
</div>
<div className="command-deck-controls" aria-label="Active command deck controls">
<span className={`command-chip command-chip-${state.liveSession.status}`}>
{state.mode === "live" ? "Live" : "Replay"}: {connectionLabel}
</span>
<span className="command-chip">Last {state.lastSeen ? formatTime(state.lastSeen) : "waiting"}</span>
<button className="terminal-button" type="button" onClick={state.toggleMode}>
{state.mode === "live" ? "Switch to Replay" : "Switch to Live"}
</button>
</div>
</header>
);
};
const TickerRail = ({ state }: { state: TerminalState }) => {
const tickers = useMemo(() => buildCommandDeckTickers(state), [state]);
return (
<div className="command-ticker-rail" aria-label="Live ticker focus rail">
<div className="command-ticker-track">
{tickers.map((ticker) => {
const direction = ticker.move === null ? "flat" : ticker.move >= 0 ? "up" : "down";
const equity = state.filteredEquities.find((print) => print.underlying_id.toUpperCase() === ticker.symbol);
return (
<button
className={`command-ticker-card is-${direction}`}
key={ticker.symbol}
type="button"
onClick={() => (equity ? state.focusEquityTicker(equity) : state.setFilterInput(ticker.symbol))}
>
<span className="command-ticker-symbol">{ticker.symbol}</span>
<span className="command-ticker-price">{ticker.price === null ? "--" : `$${formatPrice(ticker.price)}`}</span>
<span className="command-ticker-move">
{ticker.move === null
? "Move n/a"
: `${direction === "up" ? "Up" : "Down"} ${formatPct(Math.abs(ticker.move))}`}
</span>
<span className="command-ticker-meta">
{ticker.options} opt / {ticker.alerts} alerts
</span>
</button>
);
})}
</div>
</div>
);
};
const FeedHealthPane = ({ state }: { state: TerminalState }) => {
const rows = [
{ label: "Options", tape: state.options, subscribed: state.routeFeatures.options },
{ label: "Equities", tape: state.equities, subscribed: state.routeFeatures.equities },
{ label: "Flow", tape: state.flow, subscribed: state.routeFeatures.flow },
{ label: "Alerts", tape: state.alerts, subscribed: state.routeFeatures.alerts },
{ label: "News", tape: state.news, subscribed: state.routeFeatures.news },
{ label: "Dark", tape: state.inferredDark, subscribed: state.routeFeatures.inferredDark }
];
return (
<Pane
className="command-feed-pane"
title="Feed Health"
status={<span className="command-pane-meta">{state.liveSession.manifest.length} subscriptions</span>}
>
<div className="command-health-list">
{rows.map(({ label, tape, subscribed }) => (
<div className="command-health-row" key={label}>
<span>{label}</span>
<span className={`command-health-status command-health-${tape.status}`}>
{subscribed ? statusLabel(tape.status, tape.paused, state.mode) : "Idle"}
</span>
<span>{tape.lastUpdate ? formatTime(tape.lastUpdate) : "No update"}</span>
<span>{tape.dropped > 0 ? `${tape.dropped} dropped` : "Queue clear"}</span>
</div>
))}
</div>
</Pane>
);
};
const EventContextPane = ({ state }: { state: TerminalState }) => {
const events = [
...state.filteredAlerts.slice(0, 3).map((alert) => ({
key: `alert-${alert.trace_id}-${alert.seq}`,
ts: alert.source_ts,
label: "Alert",
title: alert.hits[0] ? humanizeClassifierId(alert.hits[0].classifier_id) : "Classifier alert",
detail: alert.hits[0]?.explanations?.[0] ?? `${alert.hits.length} linked hits`,
action: () => state.setSelectedAlert(alert)
})),
...state.filteredSmartMoneyEvents.slice(0, 3).map((event) => ({
key: `smart-${event.event_id}-${event.seq}`,
ts: event.source_ts,
label: "Smart",
title: smartMoneyProfileLabel(event.primary_profile_id),
detail: `${event.underlying_id} ${normalizeDirection(event.primary_direction)} / ${event.packet_ids.length} packets`,
action: () => state.openFromSmartMoneyEvent(event)
})),
...state.filteredInferredDark.slice(0, 3).map((event) => ({
key: `dark-${event.trace_id}-${event.seq}`,
ts: event.source_ts,
label: "Dark",
title: humanizeClassifierId(event.type),
detail: `${event.evidence_refs.length} evidence refs / confidence ${formatConfidence(event.confidence)}`,
action: () => state.setSelectedDarkEvent(event)
})),
...state.filteredNews.slice(0, 2).map((story) => ({
key: `news-${story.trace_id}-${story.seq}`,
ts: story.published_ts,
label: "News",
title: story.headline,
detail: story.resolved_symbols.length > 0 ? story.resolved_symbols.join(", ") : story.source,
action: () => state.setSelectedNewsStory(story)
}))
].sort((a, b) => b.ts - a.ts).slice(0, 6);
return (
<Pane
className="command-context-pane"
title="Event Context"
status={<span className="command-pane-meta">Focus evidence</span>}
>
{events.length === 0 ? (
<div className="empty">No linked evidence is available for this scope yet.</div>
) : (
<div className="command-context-list" role="list">
{events.map((event) => (
<button className="command-context-row" key={event.key} type="button" onClick={event.action}>
<time>{formatTime(event.ts)}</time>
<span className="command-context-kind">{event.label}</span>
<strong>{event.title}</strong>
<span>{event.detail}</span>
</button>
))}
</div>
)}
</Pane>
);
};
const HomeReplayRail = ({ state }: { state: TerminalState }) => {
const replayTime =
state.options.replayTime ??
state.equities.replayTime ??
state.flow.replayTime ??
state.alerts.replayTime ??
state.inferredDark.replayTime;
const replayComplete =
state.options.replayComplete ||
state.equities.replayComplete ||
state.flow.replayComplete ||
state.alerts.replayComplete ||
state.inferredDark.replayComplete;
const activeSource = state.replaySource ? state.replaySource.toUpperCase() : state.mode === "live" ? "LIVE HEAD" : "AUTO";
return (
<Pane
className="command-replay-pane"
title="Replay / Mode"
status={
<TapeStatus
status={state.mode === "live" ? state.liveSession.status : state.options.status}
lastUpdate={state.lastSeen}
replayTime={replayTime}
replayComplete={replayComplete}
paused={false}
dropped={state.options.dropped + state.equities.dropped + state.flow.dropped + state.alerts.dropped}
mode={state.mode}
/>
}
actions={
<button className="terminal-button" type="button" onClick={state.toggleMode}>
{state.mode === "live" ? "Replay" : "Live"}
</button>
}
>
<div className="command-replay-strip">
<div>
<span>Source</span>
<strong>{activeSource}</strong>
</div>
<div>
<span>Cursor</span>
<strong>{replayTime ? formatTime(replayTime) : state.lastSeen ? formatTime(state.lastSeen) : "waiting"}</strong>
</div>
<div>
<span>Chart</span>
<strong>{state.chartTicker} / {formatIntervalLabel(state.chartIntervalMs)}</strong>
</div>
<div>
<span>Scope</span>
<strong>{state.activeTickers.length > 0 ? state.activeTickers.join(", ") : "All symbols"}</strong>
</div>
</div>
</Pane>
);
};
const FocusPane = memo(({ state }: { state: TerminalState }) => {
const hits = state.chartSmartMoneyEvents.slice(-10).reverse();
const dark = state.chartInferredDark.slice(-10).reverse();
@ -9040,11 +9312,18 @@ export function OverviewRoute() {
const state = useTerminal();
return (
<PageFrame title="Home">
<div className="page-grid page-grid-home">
<ChartPane state={state} />
<EquitiesPane state={state} />
<NewsPane state={state} limit={6} />
<AlertsPane state={state} withStrip />
<div className="command-deck-shell">
<CommandDeckHeader state={state} />
<TickerRail state={state} />
<div className="command-deck-grid">
<OptionsPane state={state} limit={14} />
<ChartPane state={state} title="Price / Flow" />
<AlertsPane state={state} limit={8} withStrip className="command-signals-pane" />
<FeedHealthPane state={state} />
<DarkPane state={state} limit={8} className="command-dark-pane" />
<EventContextPane state={state} />
<HomeReplayRail state={state} />
</div>
</div>
</PageFrame>
);