/** * A named record, one click from wherever it was named. * * Extracted from the approval card, where it was the escape hatch on a pending * write: before deciding, open the account and check the note is not already * there. It is here because Piggy's answers need the same thing and do not have * it — an answer naming "DEMO — 128× H100 reserved, 6 months" and "$399,972.42" * is inert text, while the Overview renders that same record as a row with * "Match →". Closing that gap is what turns the transcript from an island that * knows your data into a surface you can act from. * * Two honesty rules, both load-bearing: * * The title never promises a record the link cannot open. `/accounts/:id` is * the only per-record route PIG has, so everything else lands on the list * that contains the row. "Open the list containing Northwind Robotics" is a * worse sentence than "Open Northwind Robotics" and a true one. * * A record with no route renders as plain text rather than as a dead link. * Losing the name would be worse — an answer would silently stop mentioning * the thing it reasoned about — and a chip that goes nowhere is the failure * `tool.tsx` already names: proving nothing is worse than claiming nothing. * * `newTab` exists because the approval card's own escape hatch was destroying * the proposal it existed to help verify: a same-tab navigation unmounts the * transcript, and the pending card with it. */ import { ArrowUpRight } from 'lucide-react'; import { Link } from 'react-router-dom'; import { cn } from '@/components/ui'; /** * Where a record of each kind can be opened. * * When the other detail routes land, each of these becomes a one-line edit and * `opensRecord` grows an entry; this is the only place a record id becomes a * URL. The keys are wider than `PiggyRecordType` on purpose — a proposed change * carries `record.type` as free text, and `allocation` and `task` are both * things a write tool can produce. */ export const RECORD_ROUTES: Record = { account: '/accounts', contact: '/accounts', demand_deal: '/demand', supply_deal: '/supply', contract: '/contracts', commitment: '/capacity', allocation: '/capacity', task: '/calendar', }; /** The URL for a record, or null when PIG has nowhere to send the reader. */ export function recordHref(type: string, id: string): string | null { const base = RECORD_ROUTES[type]; if (!base) return null; return type === 'account' ? `${base}/${id}` : base; } /** Whether the link opens the record itself or merely the list holding it. */ export function opensRecord(type: string): boolean { return type === 'account'; } export interface RecordLinkProps { /** Record type as the payload reported it — free text, not a closed union. */ type: string; id: string; /** The record's name. Falls back to a neutral noun rather than showing a uuid. */ label?: string; /** * Open in a new tab. Off by default: this is the extracted behaviour, and a * link inside a page that steals the tab is normal. Turn it on where leaving * would destroy unsubmitted state — the pending approval card, and any link * inside a streaming transcript. */ newTab?: boolean; /** * A word before the name: "Check", "Open". Omitted by default so a link * inside a sentence reads as the record's name and nothing else. */ verb?: string; className?: string; } export function RecordLink({ type, id, label, newTab = false, verb, className }: RecordLinkProps) { const href = recordHref(type, id); const name = label?.trim() || 'the record'; /* * `min-h-11` even though this is a text link: it sits in the approval card's * footer beside two 44px buttons and inside transcript prose, and both are * places a thumb lands. The 44px floor is the primitive set's oldest rule and * an inline link is not an exemption from it. * * `text-xs` is the approval footer's size and the default here; a caller * inside 14px prose passes `className="text-sm"` and `cn()` resolves it, * rather than this growing a size prop for two values. */ const shared = 'inline-flex min-h-11 w-fit max-w-full items-center gap-1 rounded-lg text-xs ' + 'text-muted underline-offset-4 transition-colors duration-1 ease-enter ' + 'hover:text-fg hover:underline focus-visible:text-fg focus-visible:outline-none ' + 'focus-visible:ring-2 focus-visible:ring-brand focus-visible:ring-offset-2 ' + 'focus-visible:ring-offset-bg'; if (!href) { return ( {name} ); } const destination = opensRecord(type) ? `Open ${name}` : `Open the list containing ${name}`; return ( {verb ? `${verb} ` : ''} {name} {/* The title attribute is not announced reliably, and where this link lands is the one thing a screen-reader user must be told before they follow it out of a pending approval: that it opens a new tab, and — for every kind but `account`, which is the only per-record route PIG has — that it opens the list rather than the row. The honest wording existed already but lived only in `title`, which is neither visible nor announced. */} {opensRecord(type) && !newTab ? null : ( {opensRecord(type) ? '' : ' (opens the list containing it)'} {newTab ? ' (opens in a new tab)' : ''} )} ); }