+
+
apps/web/app/terminal.tsx · canonical /options routing and nav labels
+
+
- export const NAV_ITEMS = [
+- { href: "/", label: "Home" },
+- { href: "/tape", label: "Tape" },
+- { href: "/news", label: "News" }
+- ] as const;
++ const CANONICAL_OPTIONS_PATH = "/options";
++ const TAPE_COMPAT_PATH = "/tape";
++ export const normalizeTerminalPathname = (pathname: string): string => {
++ if (pathname === TAPE_COMPAT_PATH) return CANONICAL_OPTIONS_PATH;
++ return KNOWN_TERMINAL_PATHS.has(pathname) ? pathname : "/";
++ };
++ export const NAV_ITEMS = [
++ { href: "/", label: "Home" },
++ { href: "/options", label: "Options" },
++ { href: "/news", label: "News" }
++ ] as const;
+
+
+
+
apps/web/app/terminal.tsx · sticky header, overlay drawer, and Options page layout
+
+
- <aside className="terminal-rail">...</aside>
++ const [drawerOpen, setDrawerOpen] = useState(false);
++ useEffect(() => { setDrawerOpen(false); }, [pathname]);
++ useEffect(() => {
++ if (!drawerOpen) return;
++ const handleKeyDown = (event: KeyboardEvent) => {
++ if (event.key === "Escape") setDrawerOpen(false);
++ };
++ document.addEventListener("keydown", handleKeyDown);
++ return () => document.removeEventListener("keydown", handleKeyDown);
++ }, [drawerOpen]);
++ <header className="terminal-topbar">
++ <button className="terminal-button terminal-menu-trigger">Menu</button>
++ ...
++ </header>
++ {drawerOpen ? <>...overlay drawer...</> : null}
+- export function TapeRoute() {
+- return <PageFrame title="Tape">...<EquitiesPane />...</PageFrame>;
+- }
++ export function OptionsRoute() {
++ return <PageFrame title="Options">...<FlowPane title="Packets" />...</PageFrame>;
++ }
+
+
+
+
apps/web/app/globals.css · full-width shell and drawer styling
+
+
- --rail-width: 236px;
+- .terminal-shell { display: grid; grid-template-columns: var(--rail-width) minmax(0, 1fr); }
+- .terminal-rail { ... }
+- .page-grid-tape { grid-template-columns: minmax(0, 1.5fr) minmax(320px, 1fr); }
++ --drawer-width: min(320px, calc(100vw - 28px));
++ .terminal-shell { position: relative; min-height: 100vh; }
++ .terminal-nav-drawer { position: fixed; inset: 0 auto 0 0; width: var(--drawer-width); ... }
++ .terminal-drawer-backdrop { position: fixed; inset: 0; ... }
++ .terminal-topbar { justify-content: space-between; backdrop-filter: blur(12px); }
++ .page-grid-options { grid-template-columns: minmax(0, 1fr); }
+
+
+
+
route + desktop compatibility · redirect and trust coverage
+
+
+ // apps/web/app/options/page.tsx
++ import { OptionsRoute } from "../terminal";
++ export default function Page() {
++ return <OptionsRoute />;
++ }
+
+- // apps/web/app/tape/page.tsx
+- return <TapeRoute />;
++ redirect("/options");
+
++ expect(isTrustedAppUrl("https://flow.deltaisland.io/options?symbol=SPY")).toBe(true);
++ expect(isTrustedAppUrl("https://flow.deltaisland.io/tape?symbol=SPY")).toBe(true);
+
+
+ Snippets are rendered client-side with Diffs (diffs.com project) and include inline fallback text for offline viewing.
+