agent-kit
Give a coding agent tradecn's rules, a way to find the item that fits, and a check it can run on the screen it built. The install puts a skill, an instructions file and a review prompt under .github/, where GitHub Copilot loads them on its own, and adds checkContract in lib/agent-kit.ts.
import { cn } from "cn"
import { useEffect, useRef, useState } from "react"
import { Button } from "@/components/ui/button"
import { CONTRACT_RULES, checkContract, type ContractReport } from "@/lib/agent-kit"
import { NUMERIC_CLASS } from "@/lib/format"
export default function AgentKitDemo() {
const [cue, setCue] = useState(true)
const [named, setNamed] = useState(true)
const [report, setReport] = useState<ContractReport | null>(null)
const row = useRef<HTMLDivElement>(null)
// Check the row once each change is on screen.
useEffect(() => {
const frame = requestAnimationFrame(() => {
if (row.current) setReport(checkContract({ root: row.current }))
})
return () => cancelAnimationFrame(frame)
}, [cue, named])
return (
<div className="flex w-full max-w-md flex-col gap-3 text-sm">
<div data-demo-controls className="flex flex-wrap gap-4 text-xs">
<label className="flex items-center gap-1.5">
<input type="checkbox" checked={cue} onChange={(event) => setCue(event.target.checked)} />
Direction cue
</label>
<label className="flex items-center gap-1.5">
<input type="checkbox" checked={named} onChange={(event) => setNamed(event.target.checked)} />
Refresh has a name
</label>
</div>
<div ref={row} className="grid grid-cols-[1fr_auto_auto_auto_auto] items-center gap-x-4 rounded-md border border-border px-3 py-2">
<span>UST 10Y</span>
<span data-numeric="" className={NUMERIC_CLASS}>99-16+</span>
<span data-numeric="" className={NUMERIC_CLASS}>99-17</span>
<span data-numeric="" data-direction={cue ? "up" : undefined} className={cn(NUMERIC_CLASS, "text-up")}>
{cue ? "+0-01" : "0-01"}
</span>
<Button type="button" variant="ghost" size="icon" aria-label={named ? "Refresh" : undefined}>
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
<path d="M20 11a8 8 0 1 0-2.3 5.7M20 5v6h-6" />
</svg>
</Button>
</div>
{report && (
<div className="flex flex-col gap-1 text-xs">
<p className={cn("text-muted-foreground", NUMERIC_CLASS)}>Checked {CONTRACT_RULES.map((rule) => `${rule} ${report.checked[rule]}`).join(", ")}</p>
{report.findings.length ? (
<ul className="flex flex-col gap-0.5">
{report.findings.map((finding) => (
<li key={`${finding.rule} ${finding.where}`}>
<span className="font-medium">{finding.rule}</span> {finding.detail}
</li>
))}
</ul>
) : (
<p>No findings.</p>
)}
</div>
)}
</div>
)
}
Installation
pnpm dlx shadcn@latest add tradecn/ui/agent-kit#v2.0.0
npx shadcn@latest add tradecn/ui/agent-kit#v2.0.0
yarn dlx shadcn@latest add tradecn/ui/agent-kit#v2.0.0
bunx --bun shadcn@latest add tradecn/ui/agent-kit#v2.0.0
-
Copy the files into your project:
lib/agent-kit.ts // The contract read off a rendered screen: what a coding agent, or an end-to-end test, checks on a page built // with tradecn before calling the work done. Text under the size floor, a number set in figures that are not // lining and tabular, a value colored by direction with nothing else saying which way, and a control with no // accessible name. The rules are 14 and 15 of the item contract plus the name every control needs. // // The whole check is one function with nothing outside it, so a test can hand it to the browser as it stands: // `page.evaluate(checkContract, { root: "main" })`. The helpers live inside it for that reason. /** The checks, by name. `floor` and `numeric` are rule 14, `direction` is rule 15, `name` is every control's accessible name. */ export type ContractRule = "floor" | "numeric" | "direction" | "name" /** Every rule, in the order the report counts them. */ export const CONTRACT_RULES: readonly ContractRule[] = ["floor", "numeric", "direction", "name"] export interface ContractOptions { /** The subtree to check: an element, a shadow root, or a selector for an element. The whole document when omitted. */ root?: ParentNode | string /** The rules to run. All four when omitted. */ rules?: readonly ContractRule[] /** The smallest text size in px. Omitted, it is `--tradecn-text-size-grid-min`, or 12 when that is unset. */ floorPx?: number /** A selector for subtrees to leave out. `[data-contract-ignore]` when omitted, and an empty string leaves nothing out. */ ignore?: string /** * Holds direction to what a reader sees: a leading sign, an arrow or a word in the visible text. Off by default, * when `data-direction`, `data-side` and an accessible label or description count too, as they do in the contract. */ visibleCue?: boolean } export interface ContractFinding { rule: ContractRule /** The nearest tradecn slot, then the element's tag and its data attributes. */ where: string /** The element's text, trimmed to 80 characters. */ text: string /** What is wrong, in words. */ detail: string } export interface ContractReport { findings: ContractFinding[] /** How many elements each rule looked at, so a check that looked at nothing shows as zero rather than as a pass. */ checked: Record<ContractRule, number> } /** * Check a rendered subtree against the contract and return what breaks it. Runs in the page: in a component, a * test's `page.evaluate`, or a browser console. Throws when `root` names nothing, so a mistyped selector fails. */ export function checkContract(options: ContractOptions = {}): ContractReport { const rules = new Set<ContractRule>(options.rules ?? ["floor", "numeric", "direction", "name"]) const scope: ParentNode | null = typeof options.root === "string" ? document.querySelector(options.root) : (options.root ?? document) if (!scope) throw new Error(`checkContract: nothing matches ${String(options.root)}`) const doc = (scope as Node).ownerDocument ?? (scope as Document) const view = doc.defaultView ?? window const style = (el: Element, pseudo?: string) => view.getComputedStyle(el, pseudo) const rootStyle = style(doc.documentElement) const floor = options.floorPx ?? (parseFloat(rootStyle.getPropertyValue("--tradecn-text-size-grid-min")) || 12) const ignore = options.ignore ?? "[data-contract-ignore]" const report: ContractReport = { findings: [], checked: { floor: 0, numeric: 0, direction: 0, name: 0 } } // A select's options show through the select, which the rules read as a field. const skip = new Set(["HEAD", "SCRIPT", "STYLE", "TEMPLATE", "NOSCRIPT", "TITLE", "META", "LINK", "OPTION", "OPTGROUP", "DATALIST"]) const all = [...("tagName" in scope ? [scope as Element] : []), ...scope.querySelectorAll("*")] const elements = all.filter((el) => !skip.has(el.tagName.toUpperCase()) && !(ignore && el.closest(ignore))) // An element's own text, apart from its children's, with the whitespace folded. const own = (el: Element) => [...el.childNodes] .filter((node) => node.nodeType === 3) .map((node) => node.textContent ?? "") .join("") .replace(/\s+/g, " ") .trim() // What a field shows: the value of an input or a text area, a drop-down's chosen option, or every option of a list // box. A checkbox, a radio, a slider or a color well shows no text of its own. A file picker shows the browser's // words and the chosen file's name, which no rule reads: the floor measures them by their styles instead. Read by // tag so an element from another window's realm still counts. const isField = (el: Element) => el.tagName === "INPUT" || el.tagName === "TEXTAREA" || el.tagName === "SELECT" const isFilePicker = (el: Element) => el.tagName === "INPUT" && (el as HTMLInputElement).type.toLowerCase() === "file" const field = (el: Element) => { if (el.tagName === "SELECT") { const select = el as HTMLSelectElement const showing = select.multiple || select.size > 1 ? [...select.options] : [select.options[select.selectedIndex]] return showing.map((option) => (option ? option.label || (option.textContent ?? "") : "")).join(" ").trim() } if (el.tagName === "INPUT") { const input = el as HTMLInputElement const type = input.type.toLowerCase() // A password shows as dots, so the check sees that it holds something and never what. if (type === "password") return input.value ? "••••" : "" // A submit or reset button with no value attribute wears the browser's own word; an empty one wears nothing. if (type === "submit" || type === "reset") return input.hasAttribute("value") ? input.value : type === "submit" ? "Submit" : "Reset" if (/^(checkbox|radio|range|color|file|image|hidden)$/.test(type)) return "" } return isField(el) ? (el as HTMLInputElement).value : "" } // The text an element shows of its own: a field's value, or its own text nodes. const textOf = (el: Element) => (isField(el) ? field(el) : own(el)) // Only a text field or a text area shows its placeholder. const placeholderOf = (el: Element) => el.tagName === "TEXTAREA" || (el.tagName === "INPUT" && /^(text|search|url|tel|email|password|number)$/.test((el as HTMLInputElement).type.toLowerCase())) ? (el.getAttribute("placeholder") ?? "") : "" const svgText = (el: Element) => el.namespaceURI === "http://www.w3.org/2000/svg" && /^(text|tspan|textPath)$/.test(el.tagName) const hidden = (el: Element): boolean => { if (el.closest("[hidden]")) return true // A display: contents element has no box of its own, and what it holds still draws, so its parent decides. if (style(el).display === "contents") return style(el).visibility === "hidden" || (el.parentElement ? hidden(el.parentElement) : false) if (typeof el.checkVisibility === "function") return !el.checkVisibility({ checkVisibilityCSS: true, visibilityProperty: true }) for (let node: Element | null = el; node; node = node.parentElement) { const s = style(node) if (s.display === "none" || s.visibility === "hidden") return true } return false } // A paint that shows nothing: none, transparent, or any color at zero alpha. A browser always resolves a paint, so an // empty one is unknown rather than clear. const transparent = (paint: string) => paint === "none" || paint === "transparent" || /^rgba\([^,]+,[^,]+,[^,]+,\s*0(?:\.0*)?\)$/.test(paint) || /\/\s*0(?:\.0*)?%?\s*\)$/.test(paint) // A box a reader never sees: faded to nothing, or clipped down to a pixel, the screen-reader-only pattern. const unseenBox = (el: Element) => { const s = style(el) if (s.display === "contents") return false if (parseFloat(s.opacity) === 0) return true const clips = [s.overflowX, s.overflowY].some((v) => v !== "" && v !== "visible") || /rect\(/.test(s.clip) || (s.clipPath !== "" && s.clipPath !== "none") const box = el.getBoundingClientRect() return clips && box.width <= 1 && box.height <= 1 } // A number a browser resolves; an empty one is unknown, so it counts as showing. const positive = (value: string) => value === "" || parseFloat(value) > 0 // SVG paint shows when it has a color, an opacity above zero and, for a stroke, a width. const fillShows = (s: CSSStyleDeclaration) => !transparent(s.fill) && positive(s.fillOpacity) const strokeShows = (s: CSSStyleDeclaration) => !transparent(s.stroke) && positive(s.strokeOpacity) && positive(s.strokeWidth) // Glyphs drawn in something other than transparent: a color for text, a fill or a stroke that shows for SVG text. const inked = (el: Element) => { const s = style(el) return svgText(el) ? fillShows(s) || strokeShows(s) : !transparent(s.color) } // Laid out, and inside no box that fades it or clips it away. const placed = (el: Element) => { if (hidden(el)) return false for (let node: Element | null = el; node; node = node.parentElement) if (unseenBox(node)) return false return true } // Text a reader sees: placed and inked. const drawn = (el: Element) => placed(el) && inked(el) // An element's role: the first of its role tokens that names a role the browser knows, WAI-ARIA's, Graphics ARIA's // or DPUB-ARIA's, in any case, as a browser reads a fallback list such as "switch checkbox". With none, the element // keeps its native role, which the rules read by tag. const ariaRoles = new Set([ ..."alert alertdialog application article banner blockquote button caption cell checkbox code columnheader combobox comment complementary contentinfo definition deletion dialog directory document emphasis feed figure form generic grid gridcell group heading image img insertion link list listbox listitem log main mark marquee math menu menubar menuitem menuitemcheckbox menuitemradio meter navigation none note option paragraph presentation progressbar radio radiogroup region row rowgroup rowheader scrollbar search searchbox sectionfooter sectionheader separator slider spinbutton status strong subscript suggestion superscript switch tab table tablist tabpanel term textbox time timer toolbar tooltip tree treegrid treeitem".split(" "), ..."document object symbol".split(" ").map((role) => `graphics-${role}`), ..."abstract acknowledgments afterword appendix backlink biblioentry bibliography biblioref chapter colophon conclusion cover credit credits dedication endnote endnotes epigraph epilogue errata example footnote foreword glossary glossref index introduction noteref notice pagebreak pagefooter pageheader pagelist part preface prologue pullquote qna subtitle tip toc".split(" ").map((role) => `doc-${role}`), ]) const roleOf = (el: Element) => (el.getAttribute("role") ?? "").toLowerCase().split(/\s+/).find((token) => ariaRoles.has(token)) // A node's text alternative, read the way the accessible name computation reads it: a hidden part says nothing // unless a reference points straight at it, an element's own label outranks its text, an image says its alt, a // field says what it holds, and a reference is followed one level deep. The control being named stays out of it. const textAlternative = (node: Node, walk: { referenced: boolean; hiddenOk: boolean; self?: Element }): string => { if (node.nodeType === 3) return node.textContent ?? "" if (node.nodeType !== 1) return "" const el = node as Element if (el === walk.self) return "" // An SVG's title names it though it is never drawn. if (el.namespaceURI === "http://www.w3.org/2000/svg" && el.tagName === "title") return el.textContent ?? "" if (!walk.hiddenOk && (el.getAttribute("aria-hidden") === "true" || hidden(el))) return "" const pointed = walk.referenced ? "" : refs(el, "aria-labelledby") if (pointed) return pointed const label = (el.getAttribute("aria-label") ?? "").trim() if (label) return label if (el.tagName === "IMG" || (el.tagName === "INPUT" && (el as HTMLInputElement).type.toLowerCase() === "image")) return el.getAttribute("alt") ?? "" if (isField(el)) return field(el) return [...el.childNodes].map((child) => textAlternative(child, walk)).join(" ") } // An ID names an element in the referring element's own tree, a shadow root's or the document's, never across a // shadow boundary. Read by node type so a tree from another window's realm still resolves; a detached element's root // is itself, which names nothing. const byId = (el: Element, id: string) => { const tree = el.getRootNode() as Document | DocumentFragment return tree.nodeType === 9 || tree.nodeType === 11 ? tree.getElementById(id) : null } // The text alternative of the elements an ID list points to, as aria-labelledby and aria-describedby read them. An // element pointed at counts even when hidden, and a reference to nothing reads as nothing. The referring element met // inside a target says nothing, so a field wrapped by the label it points to takes no name from its own value; pointed // at itself, it is read for itself, its value or its content, as Chromium reads it. // The cell a node sits in, by role, or by tag where it carries no known role. const cellHolding = (node: Element): Element | null => { for (let at: Element | null = node; at; at = at.parentElement) { const role = roleOf(at) if (role ? /^(gridcell|cell|columnheader|rowheader)$/.test(role) : /^(TD|TH)$/.test(at.tagName)) return at } return null } // `outside` leaves out the targets in the element's own cells, for a row named by its cells; a label of its own that is // no cell still counts. const refs = (el: Element, attribute: string, outside = false): string => (el.getAttribute(attribute) ?? "") .split(/\s+/) .filter(Boolean) .map((id) => { const target = byId(el, id) if (!target) return "" if (outside && target !== el) { const cell = cellHolding(target) if (cell && el.contains(cell)) return "" } const hiddenOk = target.getAttribute("aria-hidden") === "true" || hidden(target) return textAlternative(target, target === el ? { referenced: true, hiddenOk } : { referenced: true, hiddenOk, self: el }) }) .join(" ") .replace(/\s+/g, " ") .trim() // The text of the <label>s a form control has, wrapping it or pointing at it with for, the control left out. const nativeLabels = (el: Element) => "labels" in el ? [...((el as HTMLInputElement).labels ?? [])] .map((label) => textAlternative(label, { referenced: false, hiddenOk: false, self: el })) .join(" ") .replace(/\s+/g, " ") .trim() : "" const where = (el: Element) => { const slot = el.closest("[data-slot^='tradecn-']")?.getAttribute("data-slot") ?? "page" const attrs = [...el.attributes] .filter((a) => a.name.startsWith("data-") && a.name !== "data-slot") .map((a) => `[${a.name}${a.value ? `=${a.value.slice(0, 40)}` : ""}]`) .join("") return `${slot} ${el.tagName.toLowerCase()}${attrs}` } const find = (rule: ContractRule, el: Element, detail: string) => { const text = (isField(el) ? field(el) || placeholderOf(el) : own(el) || el.textContent || "").replace(/\s+/g, " ").trim().slice(0, 80) report.findings.push({ rule, where: where(el), text, detail }) } if (rules.has("floor")) { for (const el of elements) { // A file picker draws the chosen file's name, or the browser's word for none, in its own style, and its button's // word in the style ::file-selector-button gives it. Both are measured and neither is read, so no finding carries // a file's name. if (isFilePicker(el)) { if (!placed(el)) continue report.checked.floor++ const parts: [string, CSSStyleDeclaration][] = [["font-size", style(el)], ["the button's font-size", style(el, "::file-selector-button")]] const under = parts.filter(([, s]) => s.display !== "none" && s.visibility !== "hidden" && parseFloat(s.opacity) !== 0 && !transparent(s.color) && parseFloat(s.fontSize) < floor - 0.01) if (under.length) find("floor", el, `${under.map(([part, s]) => `${part} ${s.fontSize}`).join(" and ")} ${under.length > 1 ? "are" : "is"} under the ${floor} px floor`) continue } const value = textOf(el) const text = value || placeholderOf(el) if (!text) continue // An empty field draws its placeholder in the style ::placeholder gives it, with its own size, color and opacity. const pseudo = value ? undefined : "::placeholder" const s = style(el, pseudo) if (pseudo ? !placed(el) || transparent(s.color) || parseFloat(s.opacity) === 0 : !drawn(el)) continue report.checked.floor++ const size = s.fontSize // SVG text takes its size in the viewBox's units, so it is measured as drawn, through the scale above it. const matrix = svgText(el) ? (el as SVGGraphicsElement).getScreenCTM?.() : null const px = matrix ? parseFloat(size) * Math.hypot(matrix.c, matrix.d) : parseFloat(size) if (px < floor - 0.01) find("floor", el, `font-size ${size}${Math.abs(px - parseFloat(size)) > 0.01 ? ` draws at ${px.toFixed(1)}px, which` : ""} is under the ${floor} px floor`) } } // The site's smoke check, for a consumer's page: every element under a tradecn slot whose own text holds a // digit, every field under one holding a number, and every node marked data-numeric. if (rules.has("numeric")) { for (const el of elements) { const marked = el.hasAttribute("data-numeric") if (!marked && !el.closest("[data-slot^='tradecn-']")) continue if (!marked && !/\d/.test(textOf(el))) continue report.checked.numeric++ const variant = style(el).fontVariantNumeric if (!variant.includes("lining-nums") || !variant.includes("tabular-nums")) find("numeric", el, `font-variant-numeric is "${variant}", where lining-nums tabular-nums belongs`) } } // A value painted with a direction token, its text or the fill behind it, needs another channel. Text with no // number in it (Buy, Live, Filled) carries its own meaning, so it isn't held to it. The tokens are resolved // through a probe so the comparison uses the same serialization the page's own colors do. if (rules.has("direction")) { const probe = doc.createElement("span") probe.style.display = "none" ;(doc.body ?? doc.documentElement).append(probe) const resolve = (property: "color" | "backgroundColor" | "fill" | "stroke", token: string) => { if (!rootStyle.getPropertyValue(`--${token}`).trim()) return null probe.style[property] = `var(--${token})` const value = style(probe)[property] probe.style[property] = "" return value } // The colors a property resolves the direction tokens to, each with the directions it stands for: one, or both where // a theme draws up and down alike. const paints = (property: "color" | "backgroundColor" | "fill" | "stroke", tokens: readonly string[]) => { const found = new Map<string, Set<string>>() for (const token of tokens) { const value = resolve(property, token) if (value) found.set(value, new Set([...(found.get(value) ?? []), token.replace(/-soft$/, "")])) } return found } // Text is drawn in its color, and SVG text in its fill and stroke whatever its color says. const inks = { color: paints("color", ["up", "down"]), fill: paints("fill", ["up", "down"]), stroke: paints("stroke", ["up", "down"]) } const fills = paints("backgroundColor", ["up", "down", "up-soft", "down-soft"]) probe.remove() // The direction ink an element's glyphs are drawn in, the property that carries it, and the directions it stands for. const inkOf = (el: Element) => { const s = style(el) const shows = { color: true, fill: fillShows(s), stroke: strokeShows(s) } for (const property of svgText(el) ? (["fill", "stroke"] as const) : (["color"] as const)) { const directions = shows[property] ? inks[property].get(s[property]) : undefined if (directions) return { property, value: s[property], directions } } return null } // The first background behind the text, however far out, up to the checked root: a tinted cell, a flash, a badge. A // direction fill comes with the directions it stands for. const fillBehind = (el: Element) => { for (let node: Element | null = el; node; node = node.parentElement) { const background = style(node).backgroundColor if (background && !transparent(background)) { const directions = fills.get(background) return directions ? { node, color: background, directions } : null } if (node === scope) break } return null } // A value with a number in it is held to the rule whatever its units say. A word that states the direction is a // cue of its own, the way a sign or an arrow is. const saysDirection = /\b(?:up|down|buys?|sells?|bought|sold|bids?|asks?|offers?|offered|long|short|paid|given|gains?|loss(?:es)?|rises?|falls?|higher|lower)\b/i // A cue in marks or words: a leading sign, a sign on a number further in (a currency symbol may sit between), an // arrow, or a word that states the direction. A dash inside a number, as in 99-16, is no sign. const cued = (text: string) => /^[+\-−]/.test(text) || /[^\w.,][+\-−][$€£¥₹]?\d/.test(text) || /[▲▼△▽↑↓]/.test(text) || saysDirection.test(text) // The cell and the row that hold a value, by role, or by tag where the element has none. const holder = (el: Element, roles: RegExp, tags: RegExp) => { for (let node: Element | null = el; node; node = node.parentElement) { const role = roleOf(node) if (role ? roles.test(role) : tags.test(node.tagName)) return node } return null } const cellOf = (el: Element) => holder(el, /^(gridcell|cell|columnheader|rowheader)$/, /^(TD|TH)$/) const rowOf = (el: Element) => holder(el, /^row$/, /^TR$/) // A direction marker names a direction or a side: a data-direction of up or down, or a data-side of buy, sell, bid, // ask, offer, long or short. An empty or other value says nothing. It counts on the value, anywhere in its colored // run, or on the cell or the row that holds it, never on a container that has a side of its own, such as a ticket // for a buy, and never under aria-hidden, where only what is drawn counts. const marker = (node: Element | null) => Boolean(node && !unheard(node) && (/^(up|down)$/i.test((node.getAttribute("data-direction") ?? "").trim()) || /^(buy|sell|bid|ask|offer|long|short)$/i.test((node.getAttribute("data-side") ?? "").trim()))) const marked = (el: Element, run: Element) => { for (let node: Element | null = el; node; node = node.parentElement) { if (marker(node)) return true if (node === run) break } return [cellOf(el), rowOf(el)].some(marker) } // ARIA prohibits naming a generic element, a span or a div without a role, so a screen reader reading the text never // hears an aria-label there. A description reaches it: aria-description, aria-describedby, a title. const unnamed = /^(generic|presentation|none|paragraph|caption|code|deletion|emphasis|insertion|strong|subscript|superscript)$/ const genericTags = new Set(["SPAN", "DIV", "B", "I", "U", "S", "SMALL", "EM", "STRONG", "CODE", "DEL", "INS", "SUB", "SUP", "P", "Q", "SAMP", "VAR", "BDI", "BDO", "DATA"]) const nameable = (node: Element) => { const role = roleOf(node) return role ? !unnamed.test(role) : !genericTags.has(node.tagName) } // A screen reader skips everything under aria-hidden, its labels and descriptions with its text. const unheard = (node: Element) => Boolean(node.closest("[aria-hidden=true]")) // A row named by its own cells says only what each cell says, and the cells are read one by one; any other element // labelled by its own child, a button by its "Buy", says what that child says. // A row as rowOf finds one: by its role, or by its tag where no role it carries is known, an empty one included. const isRow = (node: Element) => { const role = roleOf(node) return role ? role === "row" : node.tagName === "TR" } // An accessible label or description that states the direction: its own, one it points to, or a native label. const says = (node: Element | null) => Boolean( node && !unheard(node) && [nameable(node) ? node.getAttribute("aria-label") : null, nameable(node) ? refs(node, "aria-labelledby", isRow(node)) : "", node.getAttribute("aria-description"), node.getAttribute("title"), refs(node, "aria-describedby"), nativeLabels(node)].some((text) => text && cued(text)), ) // A grid rule's tone is the rule's color, not a direction. The rule names itself in data-rule and data-tone and says // what it matched in its description, the channel scripts/color.test.ts records for it. Only the rule nearest the // value, on it or in its colored run, counts, and only when its tone is the direction color that was found. const ruled = (el: Element, run: Element, directions: Set<string>) => { for (let node: Element | null = el; node; node = node.parentElement) { if (node.matches("[data-rule]")) return !unheard(node) && directions.has(node.getAttribute("data-tone") ?? "") && Boolean((node.getAttribute("aria-description") ?? "").trim() || refs(node, "aria-describedby")) if (node === run) break } return false } // A label that states the direction counts on the value or anywhere in its colored run, and, read on their own, on // the cell and the row that hold it. const saysAlong = (el: Element, run: Element) => { for (let node: Element | null = el; node; node = node.parentElement) { if (says(node)) return true if (node === run) break } return false } // What a run shows, in order: its text, with a field's value where the field sits. Hidden parts reach nobody, so // they show nothing. Asked for what a sighted reader sees, it also leaves out a box that fades or clips away (the // screen-reader-only pattern) and glyphs inked in nothing, and reads through a boxless display: contents wrapper. // Under aria-hidden, which a screen reader skips, only what a sighted reader sees shows. // Its pieces join with gap: nothing, to read a sign as drawn, so a 99 and a -16 in two spans stay one price, or a // space, to read words, so an Up and a 10 in two spans stay two words, as a text alternative joins them. const shown = (node: Node, visible: boolean, gap: string): string => { if (node.nodeType === 3) return !visible || !node.parentElement || inked(node.parentElement) ? (node.textContent ?? "") : "" if (node.nodeType !== 1) return "" const el = node as Element const sighted = visible || el.getAttribute("aria-hidden") === "true" if (hidden(el) || (sighted && unseenBox(el))) return "" if (isField(el)) return !sighted || inked(el) ? field(el) : "" return [...el.childNodes].map((child) => shown(child, sighted, gap)).join(gap) } for (const el of elements) { const text = textOf(el) if (!/\d/.test(text) || !drawn(el)) continue const ink = inkOf(el) const tint = ink ? null : fillBehind(el) const paint = ink ?? tint if (!paint) continue const painted = ink?.value ?? tint?.color report.checked.direction++ // The colored run: a tinted box whole, or the element and up to three wrappers around it that share its ink, the // checked root among them, so a sign in a sibling span counts and a panel painted one color doesn't become one run. let run: Element = tint?.node ?? el if (ink) for (let depth = 0; depth < 3 && run !== scope && run.parentElement && style(run.parentElement)[ink.property] === ink.value; depth++) run = run.parentElement const seen = (gap: string) => shown(run, Boolean(options.visibleCue) || unheard(run), gap) if (cued(seen("").trim()) || saysDirection.test(seen(" "))) continue if (!options.visibleCue && (marked(el, run) || ruled(el, run, paint.directions) || saysAlong(el, run) || says(cellOf(el)) || says(rowOf(el)))) continue find("direction", el, `painted ${painted} with no sign, arrow, word${options.visibleCue ? "" : ", data-direction or label"} saying the direction`) } } if (rules.has("name")) { const editable = "[contenteditable]:not([contenteditable=false])" const control = (el: Element) => el.matches(`button, a[href], details > summary, input:not([type=hidden]), select, textarea, ${editable}`) || /^(button|link|checkbox|radio|switch|tab|treeitem|menuitem|menuitemcheckbox|menuitemradio|option|combobox|slider|spinbutton|textbox|searchbox|grid|treegrid|listbox|tree|radiogroup)$/.test(roleOf(el) ?? "") // A field's value is not its name, and neither is the text inside a combobox, a slider, a grid, a list box, a radio // group or an editable region: only the other controls take a name from their content, and only from the part of it // that isn't hidden. const authorNamed = (el: Element) => el.matches(`input, select, textarea, ${editable}`) || /^(combobox|slider|spinbutton|textbox|searchbox|grid|treegrid|listbox|tree|radiogroup)$/.test(roleOf(el) ?? "") for (const el of elements) { if (!control(el) || el.closest("[aria-hidden=true], [inert]") || hidden(el)) continue report.checked.name++ // A button-like input is named by the words it shows, and an image input by its alt. const type = el.tagName === "INPUT" ? (el as HTMLInputElement).type.toLowerCase() : "" const value = /^(submit|reset|button)$/.test(type) ? field(el) : type === "image" ? (el.getAttribute("alt") ?? "") : "" const name = [refs(el, "aria-labelledby"), el.getAttribute("aria-label"), nativeLabels(el), el.getAttribute("title"), placeholderOf(el), value, authorNamed(el) ? "" : textAlternative(el, { referenced: false, hiddenOk: false })].map((part) => (part ?? "").trim()).find(Boolean) if (!name) find("name", el, `${roleOf(el) ?? el.tagName.toLowerCase()} with no accessible name`) } } return report }.github/skills/ tradecn/ SKILL.md --- name: tradecn description: Build, change and review trading-desk UI made from tradecn/ui, a shadcn registry of trading components (grids, tickets, RFQ stacks, blotters, depth, charts, workspaces, hotkeys, themes). Use it when a task adds or changes a screen, panel, grid, ticket, chart or displayed number in an app that installs tradecn items, when you need the item that fits a need, and before you call UI work done, to run the contract check and the review. --- # tradecn tradecn/ui is a registry of trading-terminal components installed with the shadcn CLI, for React 19 applications. Each item is source in this repository: components in `components/ui/`, blocks in `components/`, hooks in `hooks/` and libraries in `lib/`. Compose them. A grid, ticket, stack or palette written by hand where an item already does the job is the most common way a trading screen goes wrong. [references/items.md](references/items.md) lists every item at this tag, with what it's for and what it exports. ## Before you build 1. Find the items. Read [references/items.md](references/items.md), then the item's page, before writing markup. 2. Write the screen down first: the panels, the items and parts in each, the data each reads (the store and the fields, in order), its states and its keys. The review in [references/review.md](references/review.md) checks each one. 3. Install at the app's pinned tag, one URL per argument. Preview first: `bun x shadcn@4.21.0 add --dry-run -c <app dir> https://tradecn.dev/r/<tag>/<item>.json` lists every file the install would create or overwrite, shared files such as `lib/format.ts` included. When it would overwrite no file the app has edited, install with `bun x shadcn@4.21.0 add -y -o -c <app dir> https://tradecn.dev/r/<tag>/<item>.json`. When it would, read that file's change with `--diff <file>` and keep the app's edit: for a shared file, install, then put the edit back, or port the change by hand; an edited block is always ported by hand, as step 4 says. Install a theme last and alone, because it replaces the stylesheet's variables. 4. Start from the item's Usage example and compose its public parts. Don't edit an installed file to change what it does. Wrap it, or compose around it. When one has to change, say so in the change, so the next `--diff` stays readable. The ticket blocks — `ticket` and `rfq-ticket` — are the exception: they install into the `components` alias as markup that is yours to edit for layout, and the whole block file is then the app's, its exported helpers included. Never re-add an edited block with `-o`, which overwrites it; port its fixes by hand from `--diff`. The shared files beside it update through the items that lead with them — `quote-field`, `format`, `limits`, `flash-cell`, `use-hotkeys`, and for `rfq-ticket` also `countdown`. The third block, `quote-panel`, is not edited: compose it through `quotePanelColumns`. ## The rules 1. **Numbers.** Set every number in lining tabular figures: `NUMERIC_CLASS`, `MONO_NUMERIC_CLASS` or `numericFontClass(convention)` from `@/lib/format`, or `data-numeric` on your own node once a theme is installed. Write `lining-nums tabular-nums` together, since `tabular-nums` alone drops the lining figures. Right-align numeric columns with `numeric: true` on the column. 2. **Formatting.** Format and parse through `format`: `createInstrumentFormatter(convention)` for prices in the instrument's own notation (32nds such as `99-16+`, decimals, discount), `formatNotional`, `formatBps`, `formatTicks`, `formatSigned` and `parsePrice`. Never format a price by hand. 3. **Size and type.** Nothing renders under 12 px, the floor `--tradecn-text-size-grid-min` names, and `text-xs` is the smallest size to use. A compact shadcn style draws `Badge`, `Kbd`, `CommandShortcut`, `ContextMenuShortcut`, `DropdownMenuShortcut`, `Button size="xs"` and `NativeSelect size="sm"` at 10 px, so set `text-xs` or larger on each one you compose. A small select takes it as `[&_select[data-size=sm]]:text-xs` on the component, whose `className` lands on a wrapper div, because the style's own rule sits on the select inside and outranks the wrapper's plain classes. A key inside an `InputGroupAddon` takes it on the addon as `[&_kbd[data-slot=kbd]]:text-xs`, because the addon's own rule for its keys outranks the key's class. SVG text takes its size in the viewBox's units, so a label on a chart drawn at its own size needs `fontSize={12}` or more. Reach fonts through the tokens, for example `font-(family-name:--tradecn-font-mono)`. Tailwind's own `font-sans`, `font-mono` and `font-serif` skip the app's `--tradecn-font-*` choices. `data-accessibility="hyperlegible"` on `<html>` swaps in the Atkinson Hyperlegible pair. 4. **Direction.** Color never carries direction alone. Print a sign (`formatSigned`), an arrow or a word (Buy, Sell, Bid, Ask), and mark the value `data-direction="up"` or `data-direction="down"`, the two values the check reads. A screen-reader word or an ARIA description helps a screen reader, and `data-direction` lets a test find the value, but a sighted reader needs the sign, the arrow or the word. Pick the theme for the desk's convention: `tradecn-slate` for green up, `tradecn-slate-east` for red up, `tradecn-amber` for blue up. Never recolor up and down inside one component. 5. **Streaming.** Data arrives through a row store (`createRowStore`, `applyDeltas`, and `createFrameBatcher` when messages come one at a time), and components subscribe per row with `useRow` and `useRowIds`. Never keep ticks in React state. Let the grid or `FlashCell` flash a change. Measure a busy screen with `perf-monitor` before and after a change. 6. **Keys and focus.** Every action has a key. Declare bindings once with `use-hotkeys` and attach each handler with `useHotkey` under the `HotkeyScope` of the panel it belongs to, so the focused panel's binding wins; `useHotkey` needs a `HotkeysProvider` above it and throws without one. Keep bare letters out of the `editing` scope, which also runs inside text fields. A dialog is a wall: while focus is inside it, only its own scopes run. An arrival never moves focus or the active row, and `RfqStack` keeps its `activeId` through arrivals, changing it only through `onActivate`. Focus is always visible. 7. **Server truth.** Status words and allowed actions come from the server. Show its status string, draw a button only when the row's allowed actions include it, and leave an edit pending until the row carries the new value. Never change a status locally. 8. **Layout.** Panels live in a `Workspace`, framed by `Panel`. A desktop shell whose windows are separate JavaScript contexts mounts one `Workspace` per window, with `window-set` for the set. A layout is data: `layout-manager` templates, and `preferences` slots with `template`, `user` and `session` boundaries. 9. **Density.** Use the grid presets in `DATA_GRID_PRESETS` (`blotter`, `watchlist`, `rfq`, `option-chain`, `tape`, `parameters`) rather than row heights of your own. A trading screen is dense and quiet: no cards inside cards, no gradients, no hero headers, no decorative icons, and no padding a trader scrolls past. Color is for meaning. 10. **States.** Every panel says what it's showing: empty, loading or resyncing, stale, disconnected, pending, refused, and a burst of arrivals. `feed-health` shows a feed's state and age, and `session-calendar` tells a quiet market from a stale feed. ## Check it `checkContract` in `lib/agent-kit.ts` reads a rendered screen for rules 1, 3 and 4 and for every control's accessible name. Run it in the project's end-to-end tests, with the import pointing at wherever `lib/agent-kit.ts` landed: ```ts import { expect, test } from "@playwright/test" import { checkContract } from "../src/lib/agent-kit" test("the desk keeps the contract", async ({ page }) => { await page.goto("/") const report = await page.evaluate(checkContract, { root: "main" }) expect(report.findings).toEqual([]) expect(report.checked.numeric).toBeGreaterThan(0) }) ``` Run it on every screen the change touched, in light, dark and hyperlegible mode. Each finding names the tradecn slot, the element and what's wrong. `visibleCue: true` holds direction to a sign, an arrow or a word a reader can see. Mark a subtree `data-contract-ignore` only for a sample that breaks the rules on purpose, and say why beside it. ## Review it Before calling UI work done, walk [references/review.md](references/review.md), or run the `/tradecn-review` prompt, which walks it and writes the findings as a change order. ## Report When you finish, list the items you used, the states and keys you walked, the check's findings and its `checked` counts, the screenshots you took, and anything you couldn't verify..github/skills/ tradecn/ references/ items.md # Items Every tradecn item at this tag, by kind. Install one as the skill's step 3 says: preview with `bun x shadcn@4.21.0 add --dry-run -c <app dir> https://tradecn.dev/r/<tag>/<name>.json`, then install with `add -y -o` only when it would overwrite no file the app has edited, one URL per argument. Read its page at `https://tradecn.dev/<tag>/docs/<name>/`. The list is generated from the registry, so it names what the tag has. ## Components ### Alerts (`alerts`) Composable notice containers, lists, headers, bodies, and actions. Supply your own markup and data, or use the included alert-store hooks for newest-first views, per-notice updates, expiry, and announcements. Add the optional history grid wherever it belongs in your application. Exports: `Alerts`, `AlertsList`, `AlertItem`, `AlertHeader`, `AlertTitle`, `AlertBody`, `AlertSeverity`, `AlertActions`, `AlertActionButton`, `AlertDismiss`, `AlertsEmpty`, `AlertsAnnouncer`, `useAlert`, `useAlertView`, `AlertHistory`, `alertColumns`, `useToastBridge`, `ALERT_TONE_BAR`, `ALERT_TONE_TEXT`, `DEFAULT_ALERT_HISTORY_LABELS`. ### Audit Trail (`audit-trail`) Composable event history with a tape grid, selected-event changes, cumulative comparisons and CSV export. Exports: `AuditTrail`, `AuditTrailGrid`, `AuditTrailChanges`, `AuditTrailExportButton`, `useAuditTrail`, `useAuditTrailChanges`, `auditTrailColumns`, `foldChanges`, `diffEvents`, `formatAuditValue`, `DEFAULT_AUDIT_TRAIL_LABELS`. ### Blotter (`blotter`) The data grid as an order blotter: time, symbol, side, quantity, filled, price, status, and account. The status is the server's word, printed as it arrives and never worked out. An action such as Cancel is offered only for the orders whose allowedActions the server put it in, the button says how many that is, and the check is made again against the store as the click lands. Exports: `Blotter`, `BlotterGrid`, `BlotterActionScope`, `BlotterNewButton`, `BlotterSelection`, `BlotterActionButton`, `BlotterActionMenuItem`, `useBlotter`, `useBlotterActions`, `blotterColumns`, `allowedRows`. ### Column Chooser (`column-chooser`) Compose column visibility, order, width readings, and reset controls over a grid’s controlled column state. Exports: `ColumnChooser`, `ColumnChooserItem`, `ColumnChooserSearch`, `ColumnChooserAnnouncer`, `ColumnChooserHiddenCount`, `ColumnChooserVisibility`, `ColumnChooserName`, `ColumnChooserFrozen`, `ColumnChooserRule`, `ColumnChooserWidth`, `ColumnChooserResetWidth`, `ColumnChooserMove`, `ColumnChooserResetAll`, `useColumnChooser`, `useColumnChooserItem`, `DEFAULT_COLUMN_CHOOSER_LABELS`, `chooserRows`, `moveColumnTo`, `moveColumnBy`, `setColumnVisible`, `resetColumnWidth`, `isDefaultColumnState`, `useColumnChooserCommand`. ### Command Palette (`command-palette`) Compose a command palette from public parts on your own shadcn command. Shared behavior supplies registered actions, Shift+Enter secondary actions, async symbol search, recents and live shortcut data. Use the optional dialog wrapper or compose an inline go-bar with your own grammar and result markup. Exports: `CommandPalette`, `CommandPaletteContent`, `CommandPaletteDialog`, `CommandPaletteInput`, `CommandPaletteList`, `CommandPaletteItem`, `CommandPaletteResults`, `CommandPaletteEmpty`, `CommandPaletteSecondary`, `CommandPaletteKeys`, `useCommandPalette`, `createActionRegistry`, `scorePaletteAction`. ### Countdown (`countdown`) Time left to a moment: digits that tick once a second on one shared clock, and a bar that shrinks to nothing on one animation the compositor runs, turning in the last seconds and stopping at zero. It rounds up, so it never says zero while time is left, and it decides nothing: the server's word says whether the thing it timed is over. Exports: `Countdown`, `countdownTier`, `formatRemaining`, `PROVISIONAL_COUNTDOWN_THRESHOLDS`. ### Data Grid (`data-grid`) Virtualized grid fed by a row store one row at a time: a delta re-renders one row, numeric cells flash by direction, frozen and resizable columns, keyboard selection by row id, a reorder hold, a right-click menu, footer totals, cells edited in place as commands the server answers, presets for blotter, watchlist, RFQ, option chain, tape, and parameters. Exports: `DataGrid`, `DATA_GRID_PRESETS`, `exportCsv`, `editProblem`, `isEditProblem`, `EMPTY_COLUMN_STATE`, `DEFAULT_DATA_GRID_LABELS`, `compareForSort`, `comparatorFor`, `resolveColumns`. ### Depth Ladder (`depth-ladder`) A composable virtual price ladder with public headers, rows, cells, size readings, and recenter controls. Mounted rows subscribe to their own price level; shared behavior preserves following, keyboard navigation, and order staging. Exports: `DepthLadder`, `DepthLadderHeader`, `DepthLadderColumnHeader`, `DepthLadderViewport`, `DepthLadderRows`, `DepthLadderRow`, `DepthLadderSizeCell`, `DepthLadderPriceCell`, `DepthLadderSize`, `DepthLadderOwnSize`, `DepthLadderEmpty`, `DepthLadderRecenter`, `useDepthLadder`, `useDepthLadderRow`, `levelId`, `tickIndexOf`, `priceAtTick`, `DEFAULT_DEPTH_LADDER_LABELS`. ### Feed Health (`feed-health`) Composable feed readings with connection state, data age, staleness tiers and lane metadata. Callers own rows, tooltips and controls; a per-feed action hook preserves permission checks and pending requests, and an explicit announcer reports tier changes. Exports: `FeedHealth`, `FeedHealthList`, `FeedHealthEmpty`, `FeedHealthItem`, `FeedHealthTooltipTrigger`, `FeedHealthTooltipContent`, `FeedHealthIndicator`, `FeedHealthTier`, `FeedAge`, `FeedHealthLane`, `FeedHealthDetails`, `FeedHealthPending`, `FeedHealthAnnouncer`, `useFeedActions`, `useFeedActionMenu`, `stalenessTier`, `createClock`, `feedActionsFor`, `PROVISIONAL_THRESHOLDS`, `alwaysOpen`, `formatAge`. ### Flash Cell (`flash-cell`) A cell that flashes on change, colored up, down, or flat, as a fill or an inset ring. Web Animations on the existing element, a shared memory so virtualized rows resume mid-flash, reduced-motion aware. Introduces the up/down/flat tokens. Exports: `FlashCell`. ### Hotkey Editor (`hotkey-editor`) Composable shortcut readings, capture and text fields, reset controls and conflict messages over the hotkey registry. Callers own grouping, row content, empty states and import/export actions. Exports: `HotkeyEditor`, `HotkeyEditorItem`, `HotkeyEditorSearch`, `HotkeyEditorKeys`, `HotkeyEditorChange`, `HotkeyEditorEdit`, `HotkeyEditorCapture`, `HotkeyEditorInput`, `HotkeyEditorReset`, `HotkeyEditorResetAll`, `HotkeyEditorProblem`, `HotkeyEditorConflicts`, `useHotkeyEditor`, `useHotkeyEditorItem`, `DEFAULT_HOTKEY_EDITOR_LABELS`, `matchesQuery`, `groupOf`, `scopeWord`. ### Instrument Search (`instrument-search`) Composable instrument search with shared query recognition, debounced requests and selection. Arrange the input, results, recognition hints and application controls through public parts; choose each result row and its order. Exports: `InstrumentSearch`, `InstrumentSearchContent`, `InstrumentSearchInput`, `InstrumentSearchList`, `InstrumentSearchItem`, `InstrumentSearchHint`, `useInstrumentSearchState`, `useInstrumentSearch`, `toSymbolAdapter`, `matchedIdentifier`, `DEFAULT_INSTRUMENT_SEARCH_LABELS`, `isCusip`, `isIsin`, `parseCoupon`, `parseMaturity`, `parseCouponMaturity`, `recognizeQuery`, `QUERY_KIND_LABELS`. ### Layout Manager (`layout-manager`) Composable saved workspace layouts with public template readings, save and import fields, rename controls, and confirmed actions. The caller owns the list, persistence, export, reset, and workspace loading. Exports: `LayoutManager`, `useLayoutManager`, `LayoutManagerItem`, `useLayoutManagerItem`, `LayoutManagerSaveName`, `LayoutManagerSave`, `LayoutManagerTaken`, `LayoutManagerImportTrigger`, `LayoutManagerImportContent`, `LayoutManagerImportText`, `LayoutManagerImportName`, `LayoutManagerImportSubmit`, `LayoutManagerImportProblem`, `LayoutManagerName`, `LayoutManagerRenameField`, `LayoutManagerActive`, `LayoutManagerPanelCount`, `LayoutManagerSavedAt`, `LayoutManagerUnknownKinds`, `LayoutManagerLoad`, `LayoutManagerRename`, `LayoutManagerDuplicate`, `LayoutManagerDelete`, `readLayoutTemplates`, `writeLayoutTemplates`, `parseLayoutTemplates`, `saveTemplate`, `importTemplate`, `exportTemplate`, `DEFAULT_LAYOUT_MANAGER_LABELS`, `LAYOUT_TEMPLATES_SLOT`, `LAYOUT_TEMPLATES_VERSION`, `renameTemplate`, `duplicateTemplate`, `deleteTemplate`. ### Panel (`panel`) The frame around a book, a chart, or a blotter. A panel is a hotkey scope, so its keys come with it, and palette actions registered with the scope and within answer from the focused panel. A header that is a drag handle, a symbol tag you click to retype, link groups that carry a symbol between panels and between windows, and a popout that moves the panel to its own window without remounting it. Exports: `Panel`, `PanelHeader`, `PanelTitle`, `PanelActions`, `PanelContent`, `SymbolTag`, `LinkGroupDot`, `PanelPopout`. ### Parameter Grid (`parameter-grid`) A parameter table on the data grid: one row per instrument, tier, or pair, a frozen name, an enable box that asks the server and never flips itself, the values typed in place and held pending until the server's row comes back with them, a rejection printed in the cell, when the server last changed each row, and a mark on the rows changed since a moment. Every value is the consumer's; nothing here decides one. Exports: `ParameterGrid`, `parameterColumns`, `parameterEdit`, `allowsAction`, `DEFAULT_PARAMETER_GRID_LABELS`. ### Perf Monitor (`perf-monitor`) Composable frame statistics, histograms, and store lane readings. One sampler and report subscription per monitor; callers own the markup, labels, controls, and lane order. Exports: `PerfMonitor`, `PerfMonitorValue`, `PerfMonitorHistogram`, `PerfMonitorLane`, `PerfMonitorLaneValue`, `usePerfReport`, `usePerfLane`, `percentile`, `histogram`, `summarize`, `formatMs`, `createFrameSampler`. ### Positions (`positions`) The data grid as a book of positions: instrument, the position signed in its unit (millions of notional or a count of contracts) with the side named, average, mark, the day's and the total P&L colored by their sign with the sign printed, a risk column the desk names, and totals of what is shown under the body. Every number is the server's; the grid prints and adds and never multiplies a position by a mark. One grid per book. Exports: `Positions`, `positionsColumns`, `positionsTotals`, `formatPosition`, `positionSide`, `withSign`. ### Price Chart (`price-chart`) A composable intraday chart on uPlot. Arrange the header, price readings, cursor readout and overlay legend around a line or candle plot. One store subscription feeds the readings and canvas; the plot owns resize, theme updates and the pointer and keyboard crosshair. Exports: `PriceChart`, `PriceChartHeader`, `PriceChartLast`, `PriceChartChange`, `PriceChartReadout`, `PriceChartPlot`, `PriceChartEmpty`, `PriceChartLegend`, `PriceChartOverlaySwatch`, `usePriceChart`, `CHART_TOKEN_CLASS`, `DEFAULT_PRICE_CHART_LABELS`, `barId`, `barStart`, `foldTick`, `foldTicks`, `EMPTY_COLUMNS`, `columnsOf`, `EMPTY_SUMMARY`, `directionBetween`, `summarize`, `priceOf`, `priceStep`, `priceIncrements`, `priceDecimals`, `formatChange`, `timeFormatter`, `dayFormatter`. ### Quote Field (`quote-field`) A field that types a quote the way the instrument quotes it: 99-16+ for a note on price, 4.253 for a bill on discount, 12.6 for credit on a spread. It parses on every keystroke and hands the number up, marks what is not a quote on blur and prints a good one back in the notation, and steps by the instrument's own step with the arrows and two buttons, ten with Shift. A held modifier leaves the arrows to the registry above it. Exports: `QuoteField`, `DEFAULT_QUOTE_FIELD_LABELS`, `DEFAULT_INVALID_QUOTE`. ### Quote Panel (`quote-panel`) A market maker's two-way panel: one row per instrument with the market's bid and ask, the desk's bid and ask, the skew and the width, a size per side, the server's status word, and the actions the server allows on the row. Every level, size, skew, and width is typed in place through the grid's editing contract, so an edit is a command the server answers and the cell shows it as pending until the row comes back with it; the limits table runs on every edit, a block refusing the value and a confirm asking in words above the grid until a fresh Enter on that value answers it. Pull all asks again. Everything the panel shows is the server's. Exports: `QuotePanel`, `quotePanelColumns`, `quoteEdit`, `allowsQuoteAction`, `DEFAULT_QUOTE_PANEL_LABELS`. ### RFQ Stack (`rfq-stack`) The stack of open inquiries: the data grid in its RFQ preset with an inquiry's columns, a compact countdown in every row on one shared clock, a threshold that hides the small auto-quoted ones, and a mark on the one in the ticket. The order is the desk's, held still under a hand; an arrival never moves the viewport, the focus, the mark, or the order. With it, useActiveInquiry: the one rule a stack and a ticket agree on, that the active inquiry stays until the trader acts or the server ends it, and an arrival never changes it. Exports: `RfqStack`, `useRfqStackView`, `rfqStackColumns`, `rfqThresholdFilter`, `stackOrder`, `bySize`, `byTimeLeft`, `byArrival`, `formatStackSize`, `useActiveInquiry`. ### RFQ Ticket (`rfq-ticket`) A dealer's ticket for a request for quote: the inquiry as it came (who, which way, how much, in what, with the venue's words on it), the market beside it, the levels already quoted, the auto price when there is one, a countdown to its end, and the fields to type a level in the instrument's own basis. The buttons are the actions the server allowed, checked again as the click lands; the status is the venue's word, printed as it is. One ticket is one inquiry, so a new one never lands in a ticket being worked. Quick sizes, the inquiry's own first, as buttons and mod+1 to mod+9, and the draft carries the size the quote is for. Exports: `RfqTicket`, `RFQ_TICKET_BINDINGS`, `checkQuote`, `describeQuote`, `quoteDistance`, `quotedSides`, `formatSize`, `DEFAULT_RFQ_TICKET_LABELS`, `QUICK_SIZE_KEYS`. ### Rules Editor (`rules-editor`) Compose an editor for grid highlights, filters, and sort order with public fields, actions, validation, and live match counts. Exports: `RulesEditor`, `RulesEditorItem`, `RulesEditorColumn`, `RulesEditorOperator`, `RulesEditorValue`, `RulesEditorTone`, `RulesEditorToneSwatch`, `RulesEditorTarget`, `RulesEditorLabel`, `RulesEditorDirection`, `RulesEditorProblem`, `RulesEditorMatchCount`, `RulesEditorFilterCount`, `RulesEditorRuleCount`, `RulesEditorAdd`, `RulesEditorMove`, `RulesEditorRemove`, `useRulesEditor`, `useRulesEditorItem`, `DEFAULT_RULES_EDITOR_LABELS`, `valueShape`, `withOp`, `withColumn`, `parseValues`, `valuesText`, `moveItem`, `newHighlight`, `newFilter`, `newSort`, `newRuleId`. ### Session Guard (`session-guard`) Compose session warnings, sign-in dialogs and custom controls around one expiry and one shared request. Keep surrounding drafts mounted while the application renews the session. SessionStatus provides an independent phase and countdown readout. Exports: `SessionGuardProvider`, `SessionGuardWarning`, `SessionGuardWarningText`, `SessionGuardRemaining`, `SessionGuardReauthenticate`, `SessionGuardActionLabel`, `SessionGuardError`, `SessionGuardDialog`, `useSessionGuard`, `SessionStatus`, `useSessionStatus`, `sessionStatus`, `DEFAULT_SESSION_GUARD_LABELS`, `DEFAULT_WARN_MS`. ### Sparkline (`sparkline`) A line small enough for a grid cell that still says which way, how far, and between what. Colored by direction against the first reading or a baseline, with the same direction in words for a screen reader. A missing reading is a gap, not a shift. Fixed size for a grid column, or it fills its box through one shared ResizeObserver. An optional crosshair that moves with the pointer and the arrow keys. Exports: `Sparkline`, `buildSparklineGeometry`, `nearestPointIndex`, `observeSize`, `resetSizeObserver`. ### Spread Matrix (`spread-matrix`) Composable spread tables for instruments, curves and butterflies, with signed readings in ticks or basis points and row/cell quote subscriptions. Exports: `SpreadMatrix`, `SpreadMatrixTable`, `SpreadMatrixHead`, `SpreadMatrixRow`, `SpreadMatrixCell`, `SpreadMatrixStructureRow`, `SpreadMatrixStructureCell`, `SpreadMatrixLegs`, `SpreadMatrixValue`, `useSpreadMatrixRow`, `useSpreadMatrixCell`, `useSpreadMatrixStructure`, `spreadBetween`, `structureSpread`, `formatSpread`, `defaultWeights`. ### Status Bar (`status-bar`) Composable environment, market clock and account readings for terminal bars and custom layouts. Exports: `StatusBar`, `StatusBarEnvironmentBadge`, `StatusBarClocks`, `StatusBarClockReadout`, `StatusBarUser`, `STATUS_TONE_CLASS`, `formatClock`, `clockFormat`, `DEFAULT_STATUS_BAR_LABELS`. ### Ticket (`ticket`) An order ticket that types a price the way the instrument quotes it (99-16+), steps it by the quote step with the arrows and the buttons, and hands a checked draft to the action you named. Buttons are the actions the server allowed and nothing else; the status is the server word, printed as it is; an acknowledgement rings the ticket once in primary. Quick sizes as buttons under the quantity and on mod+1 to mod+9. Its keys work while you type in it, and inside a dialog. Exports: `Ticket`, `TICKET_BINDINGS`, `describeDraft`, `checkDraft`, `parseQuantity`, `DEFAULT_TICKET_LABELS`, `DEFAULT_ORDER_TYPES`, `DEFAULT_TIME_IN_FORCES`, `QUICK_SIZE_KEYS`, `formatQuickSize`. ### Watchlist (`watchlist`) A watchlist with composable add controls, removal actions and a virtual grid. Reuse symbol normalization, validation and selection in your own layout; the list stays in your row store. Exports: `Watchlist`, `WatchlistGrid`, `WatchlistAddForm`, `WatchlistAddInput`, `WatchlistAddButton`, `WatchlistRemoveButton`, `WatchlistRemoveMenuItem`, `useWatchlist`, `useWatchlistAdd`, `watchlistColumns`, `watchlistRemoveColumn`, `WatchlistAddStatus`. ### Workspace (`workspace`) Panels that dock, tab, float, and pop out, on dockview. The dock owns where things sit; tradecn owns what each panel is: a kind, a title, and the small JSON it asked to keep. Every docked panel is a hotkey scope of its kind with the active border, and the dock decides which one is active. A layout is written out some milliseconds after the last change, carries its own persistence boundaries, and is read back through a parser that takes nothing on trust. Exports: `Workspace`, `useWorkspacePanel`, `WorkspaceTab`, `WorkspaceTabTitle`, `WorkspaceTabClose`, `WorkspaceTabActions`, `useWorkspaceTab`. ## Hooks ### Hotkeys (`use-hotkeys`) A hotkey registry: bindings declared as data with an id, keys, a scope, and a description; one keydown listener; scopes read from the DOM so a panel's keys beat global ones; chords with a timeout; conflict detection; remapping with the consumer's persistence. Typing is protected, menus are left alone, and a dialog is a wall while focus is inside it. Exports: `HotkeysProvider`, `HotkeyScope`, `useHotkey`, `useHotkeys`, `useHotkeyList`, `usePendingChord`, `useHotkeyScope`, `useMaybeHotkeys`, `useDeclaredHotkeyIds`. ## Utilities ### Agent Kit (`agent-kit`) What a coding agent needs to build with tradecn: a skill with the design rules and an index of every item at the tag, an instructions file for UI code, a review prompt that writes its findings as a change order, and checkContract, which reads a rendered screen for text under the size floor, numbers out of lining tabular figures, direction carried by color alone, and controls with no accessible name. Exports: `checkContract`, `CONTRACT_RULES`. ### Alert Store (`alert-store`) A store for notices over the row store's ordered lane: a notice with the same key replaces the last and grows its count instead of adding a row, a cap lets the oldest without an action go first, and the severity, tone, and allowed actions are the consumer's words and the server's ids, never decided here. Exports: `byNewest`, `createAlertStore`. ### Format (`format`) Number formatting for trading screens: tick-derived precision, 32nds and 64ths with + halves, yields, basis points, DV01, compact notional and millions, coupons in eighths, maturities, ticks between two prices, a quote basis per instrument (price, yield, discount, spread) with its own step, signed values, one null sentinel. Exports: `NULL_TOKEN`, `isInstant`, `numberFormat`, `decimalsFromTick`, `roundToTick`, `stepByTick`, `formatFraction`, `formatPrice`, `stripGrouping`, `parsePrice`, `formatYield`, `formatBps`, `formatDv01`, `formatNotional`, `formatSigned`, `formatPercent`, `formatQuantity`, `formatCoupon`, `formatMaturity`, `daysToMaturity`, `ticksBetween`, `formatTicks`, `NUMERIC_CLASS`, `MONO_NUMERIC_CLASS`, `numericFontClass`, `QUOTE_BASIS_LABELS`, `quoteBasisOf`, `quoteInvertedOf`, `quoteStepOf`, `formatQuote`, `parseQuote`, `stepQuote`, `createInstrumentFormatter`. ### Grid Rules (`grid-rules`) Rules as data for a grid: color a cell or a row when a value crosses a line, show only the rows that pass, order by a stack of columns, all as plain objects a desk writes without a build. A value is typed in the column's own format, a tone is a token name, and an applied rule says itself in words beside the color. Exports: `RULE_OPS`, `NUMBER_OPS`, `TEXT_OPS`, `RULE_OP_LABELS`, `DEFAULT_RULE_WORDS`, `ruleWords`, `RULE_TONES`, `ON_TINT_CLASS`, `RULE_TONE_CLASS`, `opsFor`, `columnName`, `normalizeValue`, `compareValues`, `compareDirected`, `readRuleValue`, `readCondition`, `readColumnRule`, `readFilterRule`, `readSortRule`, `readRules`, `compileCondition`, `ruleProblem`, `describeRule`, `compileFilter`, `compileComparator`, `ruleDecoration`, `applyRules`. ### Limits (`limits`) Fat-finger checks as data: a size above a line, a level too far from the market in the instrument's own unit, a side the book may not take, each a block or an ask-again, returned in the shape the tickets print. The numbers are the desk's, the words are the consumer's, and nothing here decides a limit. Exports: `DEFAULT_LIMITS_LABELS`, `marketSideFor`, `distanceFromMarket`, `checkLimits`, `blocks`, `confirms`, `problemsByField`. ### Preferences (`preferences`) The envelope everything a trader or a desk changes without a build travels in: slots with a version and a JSON value for a workspace layout, a grid's column state, hotkey overrides, rules, a threshold, and a payload that says which slots may leave as a desk template, which are one person's, and which never leave a session. Parse, set, read, diff, export, import, and migrate; not a store and not a provider. Exports: `PREFERENCES_MARK`, `PREFERENCES_VERSION`, `PREFERENCE_BOUNDARIES`, `DEFAULT_BOUNDARY`, `toJson`, `createPreferences`, `parsePreferences`, `getSlot`, `setSlot`, `removeSlot`, `boundaryOf`, `withBoundary`, `diffPreferences`, `exportableSlots`, `exportPreferences`, `importPreferences`, `migratePreferences`, `readSlot`. ### Row Store (`row-store`) Store-agnostic row store for high-rate feeds: apply one delta batch per frame, subscribe per row, sorted and filtered views with a reorder hold, and a frame batcher for message-driven feeds. Exports: `createRowStore`, `createFrameBatcher`. ### Session Calendar (`session-calendar`) A session calendar from sessions, holidays, and early closes in a venue's own time zone, the SessionCalendar feed-health reads plus the next transition, the time to the close, and whether a date is a trading day. Sessions may run across midnight. Time zones and daylight saving go through Intl.DateTimeFormat, no dependency, and the lib ships no venue's data. Exports: `SESSION_STATUSES`, `localTime`, `parseTime`, `addDays`, `weekdayOf`, `zonedInstant`, `createSessionCalendar`. ### Window Set (`window-set`) For shells whose windows are separate JavaScript contexts: the set of windows a desk has, which layout each shows, and where each sits, as data with its boundaries, and a controller that drives the consumer's adapter over the shell's own calls: open a window, close one, hear one close, ask where one is. restore() opens the main window first and the rest after it, stopping at the first that fails; a snapshot reads the bounds back. No shell package is imported. Exports: `createWindowSet`, `parseWindowSet`, `windowSetOf`, `mainWindow`, `readWindowSet`, `writeWindowSet`, `defaultWindowUrl`, `WINDOW_SET_BOUNDARIES`, `WINDOW_SET_KIND`, `WINDOW_SET_VERSION`, `WINDOW_SET_SLOT`, `parseWindowRecord`. ## Themes ### Amber (`tradecn-amber`) Warm paper in light mode, near-black in dark mode, and blue and vermilion for up and down. ### Slate (`tradecn-slate`) Cool neutral surfaces with a blue primary, bluish green for up and vermilion for down, in light and dark modes. ### Slate East (`tradecn-slate-east`) Slate with the direction convention reversed: vermilion for up and bluish green for down. ## Shared Files no item leads with, installed with every item that bundles them. ### `@/hooks/use-clock` Installed with `countdown`, `feed-health`, `rfq-stack`, `rfq-ticket`, `session-guard`, `status-bar`. Exports: `useNow`. ### `@/hooks/use-flash` Installed with `alerts`, `audit-trail`, `blotter`, `column-chooser`, `data-grid`, `depth-ladder`, `flash-cell`, `parameter-grid`, `positions`, `price-chart`, `quote-panel`, `rfq-stack`, `rfq-ticket`, `rules-editor`, `sparkline`, `spread-matrix`, `ticket`, `watchlist`. Exports: `createFlashMemory`, `compareValues`, `directionOf`, `FILL_COLORS`, `RING_COLORS`, `directionClass`, `resetReducedMotionCache`, `playFlash`, `useFlash`. ### `@/hooks/use-link-group` Installed with `panel`, `workspace`. Exports: `LinkGroupProvider`, `useLinkGroupStore`, `useLinkGroup`. ### `@/hooks/use-popout` Installed with `panel`, `workspace`. Exports: `mirrorRoot`, `usePopout`. ### `@/hooks/use-row-store` Installed with `alerts`, `audit-trail`, `blotter`, `column-chooser`, `data-grid`, `depth-ladder`, `parameter-grid`, `positions`, `price-chart`, `quote-panel`, `rfq-stack`, `row-store`, `rules-editor`, `spread-matrix`, `watchlist`. Exports: `useRow`, `useRowIds`, `useStoreMeta`, `useView`. ### `@/lib/clock` Installed with `countdown`, `feed-health`, `rfq-stack`, `rfq-ticket`, `session-guard`, `status-bar`. Exports: `createClock`, `sharedClock`. ### `@/lib/hotkeys` Installed with `command-palette`, `hotkey-editor`, `panel`, `rfq-ticket`, `ticket`, `use-hotkeys`, `workspace`. Exports: `detectPlatform`, `normalizeKeys`, `formatKeys`, `matchesKeys`, `keysFromEvent`, `isEditableTarget`, `isMenuTarget`, `scopeChain`, `createHotkeyRegistry`. ### `@/lib/link-group` Installed with `panel`, `workspace`. Exports: `LINK_GROUPS`, `cycleLinkGroup`, `normalizeSymbol`, `isLinkMessage`, `createLinkGroupStore`, `createCallbackTransport`, `createBroadcastChannelTransport`. ### `@/lib/workspace-layout` Installed with `layout-manager`, `workspace`. Exports: `WORKSPACE_LAYOUT_KIND`, `WORKSPACE_LAYOUT_VERSION`, `WORKSPACE_PERSISTENCE_BOUNDARIES`, `toPanelState`, `parseWorkspaceLayout`, `unknownPanelKinds`, `isReservedPanelId`, `nextPanelId`, `createWorkspacePanelStore`, `debounce`..github/skills/ tradecn/ references/ review.md # Review The walk to finish UI work with, and the change order to write when someone else makes the fixes. Run it on test data, never on production data. ## The walk 1. **The check.** Run `checkContract` on every screen the change touched, in light, dark and hyperlegible mode. It should report no findings, and a `checked` count above zero for each rule the screen has content for. 2. **Screenshots.** Take one of each touched panel in each mode, at the smallest window size the app supports and at a typical desk size. Look at every one before going on. 3. **States.** Force each state and look at it: empty, loading or resyncing, stale, disconnected, pending, refused, and a burst of arrivals. A state the screen can't show is a finding. 4. **Keys.** Do every action from the keyboard. The Tab order follows the reading order, focus is always visible, Escape closes what it opened, and a dialog blocks the shortcuts behind it. Start a burst while typing in a ticket: the caret, the focus and the active row stay where they were. 5. **Numbers.** Numeric columns are right-aligned and in the instrument's notation, a unit is said once in the header, a change carries its sign, and a value cut short shows whole in its title or tooltip. 6. **Color.** Every colored value says its direction or state another way too. Look at the screen in grayscale. The theme matches the desk's up and down convention. 7. **Density.** No empty bands, no scrolling to reach a primary action, nothing clipped at the smallest size, and no decoration that isn't information. 8. **Speed.** Watch `perf-monitor` through a burst. The change adds no long frames, and nothing renders per tick outside the rows that changed. ## The change order When the fixes go to someone who can't see what you saw, write them as a change order that stands on its own, since its reader may have nothing else. One finding per section, the most severe first: ```markdown # Change order: <screen or topic> Revision: <commit or tag reviewed>. Data: <the test data used>. Screens: <panels, modes and window sizes>. ## 1. <short title> - **Where:** <file, component, part or selector> - **Now:** <what it does, as observed> - **Change:** <exactly what to do: the item or part, the prop, the token, the value> - **Done when:** <the check, the test or the screenshot that proves it> ## Leave alone <what this order doesn't cover, so the fixes stay in scope> ``` Name values exactly. Write `text-up` beside a `formatSigned` value, not "make it green"..github/instructions/ tradecn.instructions.md --- name: tradecn description: Rules for UI code in an app built from tradecn/ui items. applyTo: "**/*.tsx" --- This app's UI is built from tradecn/ui. [The tradecn skill](../skills/tradecn/SKILL.md) has the detail, and [its item list](../skills/tradecn/references/items.md) names every item. - Use an installed tradecn item before writing a component of your own. - Set every number in lining tabular figures with `NUMERIC_CLASS`, `MONO_NUMERIC_CLASS` or `numericFontClass` from `@/lib/format`, and format prices, sizes, spreads and changes through `format`. - Keep text at 12 px or more. `text-xs` is the smallest size. Reach fonts through the `--tradecn-font-*` tokens. - Never let color carry direction alone. Pair `text-up` and `text-down` with a sign, an arrow or a word, and with `data-direction`. - Feed rows through a row store and subscribe per row. Never keep ticks in React state. - Give every action a key through `use-hotkeys`, under its panel's `HotkeyScope`. An arrival never moves focus or the active row. - Show the server's status words and allowed actions. An edit stays pending until the row carries the value. - Use the grid presets for density. No gradients, cards inside cards, hero headers or decorative icons. - Every panel shows its empty, loading, stale, disconnected, pending and refused states. - Before calling UI work done, run `checkContract` from `lib/agent-kit.ts` on the screens you changed, and walk [the review](../skills/tradecn/references/review.md)..github/prompts/ tradecn-review.prompt.md --- name: tradecn-review description: Review the screens a change touched against tradecn's contract and design rules, and write the findings as a change order. argument-hint: The screens, panels or files to review agent: agent --- Review ${input:scope:the screens this change touched} against the tradecn skill. 1. Read [the skill](../skills/tradecn/SKILL.md) and [the review](../skills/tradecn/references/review.md). 2. From the diff and the request, list the screens and panels in scope, and say which ones you'll cover. 3. Run the app on test data. Run `checkContract` on each screen in light, dark and hyperlegible mode, through the project's end-to-end tests where they exist. 4. Take the screenshots the review asks for, and look at each one. 5. Walk the states, the keys, the numbers, color, density and speed. 6. Write the findings as a change order in the review's format, the most severe first, to the file the request names or in your reply. Don't fix anything during the review unless asked. Report what you couldn't run or see as not verified. -
Add the tokens to your stylesheet:
:root { --tradecn-text-size-grid-min: 12px; } .dark { --tradecn-text-size-grid-min: 12px; }
Usage
The preview checks a quote row in the page. Turn off the direction cue and the check reports a value colored by direction with nothing else saying which way. Take the refresh button's name away and it reports a control with no accessible name.
Run the same check in an end-to-end test, on every screen a change touched:
import { expect, test } from "@playwright/test"
import { checkContract } from "../src/lib/agent-kit"
test("the blotter keeps the contract", async ({ page }) => {
await page.goto("/")
const report = await page.evaluate(checkContract, { root: "[data-slot='tradecn-blotter']" })
expect(report.findings).toEqual([])
expect(report.checked.numeric).toBeGreaterThan(0)
})
checkContract uses nothing outside its own body, so page.evaluate sends it to the page as it is. Point the import at wherever the CLI put lib/agent-kit.ts.
What the agent reads
| Installed at | For |
|---|---|
.github/skills/tradecn/SKILL.md |
The skill: find the item first, write the screen down, the ten rules, the check and the review. Copilot reads its description and loads the rest when a task matches. |
.github/skills/tradecn/references/items.md |
Every item at the tag by kind, with what it's for and what it exports. |
.github/skills/tradecn/references/review.md |
The walk to finish UI work with, and the change order to write when someone else makes the fixes. |
.github/instructions/tradecn.instructions.md |
Ten rules Copilot applies to every .tsx file it works on. |
.github/prompts/tradecn-review.prompt.md |
/tradecn-review: the walk, with the findings written as a change order. |
The CLI puts these at the project root, where components.json is. Copilot reads them from the repository root, so in a monorepo move .github/ there. They're plain Markdown in the Agent Skills layout, so an agent that reads skills from another folder can use them from there.
API Reference
checkContract
checkContract(options?: ContractOptions): ContractReport checks a rendered subtree and returns what breaks the contract. It throws when root names nothing, so a mistyped selector fails instead of passing.
| Option | Type | Default | Description |
|---|---|---|---|
root |
ParentNode | string |
document |
The subtree, as an element, a shadow root or a selector. |
rules |
readonly ContractRule[] |
All four | The rules to run. |
floorPx |
number |
--tradecn-text-size-grid-min, else 12 |
The smallest text size, in px. The install adds the token when it is missing, since the check reads it. |
ignore |
string |
"[data-contract-ignore]" |
A selector for subtrees to leave out. An empty string leaves nothing out. |
visibleCue |
boolean |
false |
Direction needs a leading sign, an arrow or a word a reader can see. |
The report has findings, each with its rule, where (the nearest tradecn slot, then the tag and its data attributes), text and detail. A password reads as ••••, and a file picker is measured without being read, so no finding carries a password or a file's name. It also has checked, how many elements each rule looked at. A rule that looked at nothing shows zero, so a test can tell an empty check from a pass.
The rules
| Rule | What it checks |
|---|---|
floor |
Drawn text, and fields showing a value or a placeholder, at the floor size or larger. Screen-reader-only, faded-out and transparent text isn't drawn, so it isn't held to it. An empty field's placeholder is measured in its own ::placeholder style, and SVG paint shows only with some opacity, a stroke only with some width. SVG text is measured as drawn, through its viewBox. A select shows its chosen option, or every option in a list box. A file picker draws the chosen file's name in its own style and its button's word in ::file-selector-button's, so both are measured. Rule 14 of the item contract. |
numeric |
Digits under a tradecn slot, fields holding a number there, and every [data-numeric] node, set in lining-nums tabular-nums. Rule 14, read the way the site and the browser matrix read it. |
direction |
A value with a number in it, in text or in a field, drawn in the up or down token or sitting on one of their fills, also carries a leading sign, an arrow, a data-direction of up or down or a data-side of buy, sell, bid, ask, offer, long or short on the value, its colored run, or the cell or row that holds it, or an accessible label or description that states it, on the value, its colored run, or the cell or row that holds it: its own, one it points to, or a native label. A row's aria-labelledby says nothing for the cells it points at inside that row, or for what those cells hold, since a screen reader reads those cells one by one; a label of the row's own that is no cell still counts, as does a target outside the row and any other element's own child that labels it. A label counts only where the element takes a name, never on a plain span or div, which ARIA doesn't let a screen reader announce; a description counts anywhere. The colored run is the value and up to three wrappers around it that share its color, the checked root among them. A grid rule's highlight in the up or down tone is the rule's color rather than a direction, so the description of the rule that paints the value counts for it when the rule's tone is the direction the value is painted in. A hidden sign says nothing, and under visibleCue neither does a screen-reader-only or transparent one. Under aria-hidden, which a screen reader skips, only a cue a sighted reader sees counts: a data-direction or data-side, a label or a description there says nothing. SVG text is drawn in its fill and stroke, so those are what count there. Text with no number in it isn't held to it, and a word that states the direction (up, down, buy, sell, bid, ask and their kin) is a cue of its own, and an element's edge ends a word, so Up and 10 in two spans read as a word and a number. A sign is read as drawn, so 99 and -16 in two spans stay one price, with no sign. Units excuse nothing: 10 lots in the up color needs a cue like any number. Rule 15. |
name |
Every visible control outside an aria-hidden or inert subtree, a disclosure's summary, a tree item and a radio group among them, has an accessible name: aria-labelledby, aria-label, a label, a title, a text field's placeholder, an image input's alt, or the text it shows, where a labelled icon inside names its button. A reference or a label is read the way a screen reader reads it: an image by its alt, a part by its own label, nothing hidden, and an ID only in the element's own tree, so a reference inside a shadow root never reaches the document. A field, a select, a combobox or an editable region takes no name from what it holds, even through a label or a reference that wraps it, unless its aria-labelledby points at itself, a grid or a list box none from its cells, a radio group none from its radios, and hidden text names nothing. A role is read the way a browser reads a list of them, by the first one it knows in any case, so role="switch checkbox" is a switch and a cell or a row is found by its role before its tag. |
CONTRACT_RULES lists them in that order.
What it does not do
It reads computed styles, so it runs in a browser or in a DOM that computes them. It doesn't check contrast, which scripts/themes.test.ts holds the themes to, or visible focus, layout and clipping, which the review walks by eye. It doesn't look at shapes without text for direction: a dot, a bar or a line needs a label of its own. A color inside another theme's scope, or mixed with transparency, isn't matched to a direction token, and a fill outside the checked root isn't read, even when the value sits on it. HTML text is measured at its computed size, so a CSS transform that shrinks it goes unseen, and text faded out mid-transition isn't read until it shows. Direction words are read in English, so a value that says its direction in another language wants a sign or data-direction as well. The skill and the review ask for what the check can't see.
The item index
references/items.md is written from registry.json by just agent-kit, and a test fails when a change to the registry leaves it stale.
This page is docs/agent-kit.md at v2.0.0.