Two-pane list and detail Working through a queue with the list and the item visible at once. Keyboard movement through the list is what makes it a queue rather than a pair of panels. npx shadcn@latest add button Tokens this needs: --card, --card-foreground, --muted, --muted-foreground, --border, --ring, --chart-1 ──────────────────────────────────────────────────────────────────────── // ListDetail.tsx ──────────────────────────────────────────────────────────────────────── import { useState, type KeyboardEvent } from "react"; import { Button } from "@/components/ui/button"; import { cn } from "@/lib/utils"; import { Sparkline } from "../sparkline/Sparkline"; export type Item = { id: string; title: string; /** The second line in the list: count and age. */ meta: string; /** Events over the window, drawn in the detail pane. */ events: number[]; stats: [label: string, value: string][]; body: string; /** The full-width view this pane hands over to when the content is wider * than the pane. */ href: string; }; type Props = { items: Item[]; /** Arrived since the list was drawn. Held behind the "n new" chip until * asked for, because a queue that inserts under someone makes them skip. */ incoming: Item[]; onReveal: () => void; defaultSelectedId?: string; onSelect?: (id: string) => void; }; export function ListDetail({ items, incoming, onReveal, defaultSelectedId, onSelect }: Props) { const [selectedId, setSelectedId] = useState(defaultSelectedId ?? items[0]?.id); const index = Math.max(0, items.findIndex((i) => i.id === selectedId)); const item = items[index]; const select = (i: number) => { const next = items[Math.min(items.length - 1, Math.max(0, i))]; if (!next) return; setSelectedId(next.id); onSelect?.(next.id); }; // Up and down through the list, detail following. This is the whole // productivity argument, so it lives on the list rather than on a shortcut. const onKeyDown = (e: KeyboardEvent) => { if (e.key === "ArrowDown") { e.preventDefault(); select(index + 1); } if (e.key === "ArrowUp") { e.preventDefault(); select(index - 1); } }; return (
{incoming.length > 0 && ( )}
    {items.map((i, n) => (
  • select(n)} className={cn("cursor-pointer px-3 py-2 text-[11px]", n === index ? "bg-muted/60 text-card-foreground" : "text-muted-foreground")} >

    {i.title}

    {i.meta}

  • ))}
{item && (

{item.title}

events, last hour

{item.stats.map(([label, value]) => (
{label}
{value}
))}

{item.body}

move the selection full page
)}
); } ──────────────────────────────────────────────────────────────────────── // demo.tsx ──────────────────────────────────────────────────────────────────────── import { useState } from "react"; import { ListDetail, type Item } from "./ListDetail"; const alert = (id: string, title: string, meta: string, events: number[], users: number, body: string): Item => ({ id, title, meta, events, body, href: `/issues/${id}`, stats: [["first seen", "3d ago"], ["last seen", meta.split(" · ")[1]], ["users", String(users)]], }); const QUEUE: Item[] = [ alert("i-4182", "ConnectionReset · payments-worker", "8 events · 11m ago", [1, 0, 2, 1, 3, 1], 6, "ConnectionResetError: [Errno 104] Connection reset by peer at psycopg2.connect (worker.py:88)"), alert("i-4183", "TimeoutError · checkout-api", "142 events · 2m ago", [6, 9, 8, 19, 24, 31, 45], 412, "TimeoutError: request to inventory-svc exceeded 3000ms at fetchStock (checkout/cart.ts:214), retried 2 times, gave up"), alert("i-4179", "NullPointerException · catalog-svc", "3 events · 26m ago", [1, 1, 0, 0, 1, 0], 3, "java.lang.NullPointerException: product.variants is null at CatalogMapper.toDto (CatalogMapper.java:57)"), alert("i-4175", "RateLimitExceeded · search-api", "51 events · 40m ago", [12, 14, 9, 8, 5, 3], 51, "RateLimitExceeded: 429 from provider after 1,000 req/min at SearchClient.query (search.ts:41)"), alert("i-4171", "DeadlineExceeded · notifications", "12 events · 1h ago", [4, 3, 2, 2, 1, 0], 12, "DeadlineExceeded: push delivery took 12.4s, budget 10s at Dispatcher.send (dispatch.go:133)"), ]; const INCOMING: Item[] = [ alert("i-4184", "TimeoutError · checkout-api", "9 events · 1m ago", [2, 3, 4], 9, "TimeoutError: request to pricing-svc exceeded 3000ms at fetchQuote (checkout/quote.ts:72)"), alert("i-4185", "ECONNREFUSED · image-resizer", "2 events · 1m ago", [1, 1], 2, "Error: connect ECONNREFUSED 10.0.4.12:9000 at TCPConnectWrap.afterConnect (net.js:1148)"), alert("i-4186", "ValidationError · signup-api", "1 event · now", [1], 1, "ValidationError: email must be a valid address at SignupSchema.parse (signup.ts:19)"), ]; /** * Five issues with the second selected and three more waiting behind the * chip. Arrow keys walk the list once it has focus; the chip merges the * three in at the top, and only then. */ export default function Demo() { const [items, setItems] = useState(QUEUE); const [incoming, setIncoming] = useState(INCOMING); const reveal = () => { setItems([...incoming, ...items]); setIncoming([]); }; return ; }