diff --git a/app/llms-full.txt/route.test.ts b/app/llms-full.txt/route.test.ts index 5a8beed..186c9d3 100644 --- a/app/llms-full.txt/route.test.ts +++ b/app/llms-full.txt/route.test.ts @@ -147,8 +147,10 @@ describe("homepage mirror", () => { expect(body).toContain(`${figure.label}: ${figure.value}`); }); - it("mirrors the stack chips in order", () => { - expect(lines).toContain(`Stack: ${STACK.join(" · ")}`); + it("mirrors the stack strip in order", () => { + expect(lines).toContain( + `Stack: ${STACK.map((s) => `${s.name} (${s.role})`).join(" · ")}`, + ); }); // The header prose says "the three legal documents" in words, the way diff --git a/app/llms-full.txt/route.ts b/app/llms-full.txt/route.ts index 35e77d7..7a448a2 100644 --- a/app/llms-full.txt/route.ts +++ b/app/llms-full.txt/route.ts @@ -11,7 +11,7 @@ import { HERO_LEAD, METHOD_STEPS, SITE_DESCRIPTION, - STACK, + stackLine, statusBlock, } from "@/lib/copy"; import { FAQ_ITEMS } from "@/lib/faq"; @@ -215,7 +215,7 @@ ${compareTable} ### How it works -Stack: ${STACK.join(" · ")} +Stack: ${stackLine()} ${methodSteps} diff --git a/components/site/Method.test.ts b/components/site/Method.test.ts new file mode 100644 index 0000000..2b78277 --- /dev/null +++ b/components/site/Method.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from "vitest"; +import { Method } from "./Method"; +import { STACK } from "@/lib/copy"; +import { + asElement, + attrs, + childElements, + findAll, + findOne, + textOf, +} from "@/test-utils/react-tree"; + +const section = asElement(Method()); +const list = findOne(section, "ul"); + +describe("Method stack strip", () => { + // The page and the llms-full.txt mirror both read STACK; this is what keeps + // the rendered side from dropping a role or reordering without a failure. + it("renders one cell per STACK item, name over role, in order", () => { + const cells = childElements(list); + expect(cells.map((c) => c.type)).toEqual(STACK.map(() => "li")); + cells.forEach((cell, i) => { + const [name, role] = childElements(cell); + expect(textOf(name)).toBe(STACK[i].name); + expect(textOf(role)).toBe(STACK[i].role); + }); + }); + + it("hides the square marker and names the list by its eyebrow", () => { + for (const cell of childElements(list)) { + const [, role] = childElements(cell); + const [marker] = childElements(role); + expect(attrs(marker)["aria-hidden"]).toBe(true); + } + const id = attrs(list)["aria-labelledby"]; + const eyebrow = findAll(section, "span").find((s) => attrs(s).id === id); + expect(eyebrow && textOf(eyebrow)).toBe("stack"); + }); +}); diff --git a/components/site/Method.tsx b/components/site/Method.tsx index 5aebaa6..fac2748 100644 --- a/components/site/Method.tsx +++ b/components/site/Method.tsx @@ -1,4 +1,3 @@ -import { Fragment } from "react"; import { METHOD_STEPS, STACK } from "@/lib/copy"; import { Frame } from "@/components/ui/Frame"; import { MethodRow } from "@/components/ui/MethodRow"; @@ -25,21 +24,39 @@ export function Method() {
- stack -
- {/* The `·` is a separator between chips, not part of one, so it is - aria-hidden and rendered only between items. */} - {STACK.map((name, i) => ( - - {i > 0 && ( - - · - - )} - {name} - + + stack + + {/* Hairlines come from the list's top/left edge plus each cell's + right/bottom edge, so the grid stays single-ruled at any column + count — and closed while STACK.length divides evenly by it. In one + row of six the longest name (currently "BLAKE3") overflows + --text-h3, so names size off the frame's container width instead. + `.meta` and `.hug` sit outside any cascade layer, so a utility + beside either can't override it; the role label therefore spells + `.meta` out at 12px rather than adding a size to it. */} +
+
); diff --git a/lib/copy.test.ts b/lib/copy.test.ts index 4999cee..651a264 100644 --- a/lib/copy.test.ts +++ b/lib/copy.test.ts @@ -54,7 +54,10 @@ const ALL_STRINGS: readonly [string, string][] = [ [`METHOD_STEPS.${s.n}.word`, s.word], [`METHOD_STEPS.${s.n}.body`, s.body], ]), - ...STACK.map((s, i): [string, string] => [`STACK[${i}]`, s]), + ...STACK.flatMap((s): [string, string][] => [ + [`STACK.${s.name}.name`, s.name], + [`STACK.${s.name}.role`, s.role], + ]), ["DEMO_CAPTIONS.terminal", DEMO_CAPTIONS.terminal], ["DEMO_CAPTIONS.fleet", DEMO_CAPTIONS.fleet], ...CONTACT_HEADLINE.map((s, i): [string, string] => [ @@ -77,6 +80,20 @@ describe("copy strings", () => { ); }); +describe("STACK", () => { + // components/site/Method.tsx lays the strip out 2 / 3 / 6 across, and its + // hairlines only close when the count divides evenly by the column count. + it("has exactly six items", () => { + expect(STACK).toHaveLength(6); + }); + + // `name` is the React key in Method.tsx and the label in ALL_STRINGS above. + it("names each item once", () => { + const names = STACK.map((s) => s.name); + expect(new Set(names).size).toBe(names.length); + }); +}); + describe("COMPARE_ROWS", () => { // Hardcoded, not `COMPARE_ROWS.length`: the section header says "seven // axes" in prose (components/site/Compare.tsx), so the count is a published diff --git a/lib/copy.ts b/lib/copy.ts index 270bec7..f23bf8f 100644 --- a/lib/copy.ts +++ b/lib/copy.ts @@ -149,10 +149,27 @@ export const METHOD_STEPS: readonly MethodStep[] = [ }, ]; -/** The stack chips under the method steps, rendered with an aria-hidden `·` - * between each. Order is the render order. This is the section's only stack - * summary — a figure strip beside it used to restate QUIC, iroh and USDC. */ -export const STACK = ["Rust", "BLAKE3", "QUIC", "iroh", "USDC", "EVM"] as const; +export type StackItem = { + name: string; + /** What the piece does in the protocol — the small label under the name. */ + role: string; +}; + +/** The stack under the method steps, in render order. Rendered by `Method`; + * serialised by `stackLine()`. This is the section's only stack summary: put + * per-piece detail in `role`, not in a separate strip beside it. */ +export const STACK: readonly StackItem[] = [ + { name: "Rust", role: "node & client" }, + { name: "BLAKE3", role: "verification" }, + { name: "QUIC", role: "transport" }, + { name: "iroh", role: "connectivity" }, + { name: "USDC", role: "payment" }, + { name: "EVM", role: "settlement" }, +]; + +/** The stack as one line of text, for the machine mirror. */ +export const stackLine = (): string => + STACK.map(({ name, role }) => `${name} (${role})`).join(" · "); /** The target price, as the page and both machine surfaces quote it. */ export const TARGET_RATE = "$0.01/GB";