/** * TEXT INK, AS AN ALIAS TABLE — the semantic layer, addressable. * * Two layers, and only one of them is a decision: * * - The **primitive** layer is the palette (`colors.zinc[600]`). It holds values * and owns nothing; it is the same Tailwind ramp anyone could paste. * - The **semantic** layer is this table. It holds the actual decisions — "a line * that supports the thing above it is one step lighter than body" — and every * component addresses ink through it. * * This used to be a `switch` inside `getTextColor`, which made the semantic layer * real but not REFERENCEABLE: the roles existed as a prop union on `Text` and * nowhere else, so nothing outside a `.tsx` file could name them. That is why the * palette leaked into call sites and into design: a designer picking `zinc-500` in * Figma and an author typing `zinc-500` in code look like one source of truth, but * the thing they agree on is a THIRD-PARTY RAMP. The decision about supporting * text was written in neither, so it was re-made at every call site — which is * exactly how one role came to render in two inks. * * As a table it serializes: {@link textInkTokens} emits it in the W3C Design * Tokens format with the alias intact (`{color.zinc.900}` rather than a flattened * hex), which is the form Figma Variables import as ALIAS variables pointing at * primitive ones. One file, both tools, and the alias survives the trip — so * re-pointing a role moves Figma and the code together instead of starting a * find-and-replace. * * A table rather than a component, so an app that hand-rolls its own DOM can * name a role without rendering one of ours to get at the colour. */ export interface InkBlend { /** The two rungs the decision sits between, lighter first. */ readonly between: readonly [string, string]; /** How much of the FIRST, as a percentage — an sRGB mix, as CSS composites. */ readonly at: number; } /** * A role is a palette rung, or a point BETWEEN two of them. * * The ramp is a third party's and its rungs are 100 apart: supporting text has * to recede from body ink AND clear 4.5:1 on the darkest wash the kit paints it * on, and no zinc satisfies both (500 is 4.4:1 on a selected row, 600 is 7.7:1 * on the canvas and reads as a second body). A blend NAMES the two it sits * between rather than freezing a hex, so re-pointing either rung still moves it * and a reader can see which decision was made. */ export type InkRef = string | InkBlend; export declare const TEXT_INK: { /** Body copy and any value the reader is meant to land on. */ readonly default: "zinc.900"; /** ALL supporting text — under a value, under a heading, anywhere. One role. */ readonly muted: { readonly between: readonly ["zinc.500", "zinc.600"]; readonly at: 65; }; /** A disabled control's label, and a value that is not filled in ("Not set"). */ readonly inactive: "zinc.400"; /** Requires a dark ground — illegal on the canvas. */ readonly onInverse: "white"; readonly danger: "red.900"; readonly warning: "amber.700"; readonly success: "emerald.700"; }; /** * Derived from the table, so anything iterating the vocabulary is exhaustive by * construction. Declaring the union separately would let a new ink ship without * a contrast case and keep every test green. */ export type TextColor = keyof typeof TEXT_INK; export declare const TEXT_COLORS: ("danger" | "default" | "inactive" | "muted" | "onInverse" | "success" | "warning")[]; /** * The role as the CSS variable that carries it, never the resolved literal — so * a theme moving `--foreground` moves body ink everywhere in one paint, and the * alias above stays the thing that decides which primitive a role lands on. * `tokens_agreement.test.ts` asserts each variable resolves to what this table * names. */ export declare function getTextColor(color?: TextColor): string; /** * WHAT MAY PAINT A MARK — a glyph, a ring, a dot, a spinner — as the whole of * `constitution.md` §9: rule 3's ink ladder above, the accent, or the ink of the * surface the mark sits on. * * A ROLE rather than a colour, because a colour prop typed `string` is the same * repaint an app sheet is refused: a shade written at a call site is right in * light and arbitrary in dark, it is re-decided everywhere the mark appears, and * no theme can move it. Typing the prop is what makes the rule a compile error * instead of a review. * * The escape is DATA — a record option's colour, an app's theme colour, a chart * series — which stays a string on the entries whose registry row says so. */ export type InkColor = TextColor | "accent" | "white" | "currentColor"; /** The role as the value that carries it. `currentColor` is the CSS keyword, so * the mark inherits whatever painted the box around it. */ export declare function getInkColor(color: InkColor): string; /** * The vocabulary in the W3C Design Tokens Community Group format, primitives and * aliases in one document — the file a Figma Variables import (or Tokens Studio) * reads, so the design tool and the code resolve `muted` from the same place. * * The alias is emitted as a REFERENCE, never a resolved hex. A flattened export * would hand Figma seven unrelated colours and lose the only thing worth syncing: * that `default` IS `zinc.900`, and moves when it moves. * * A BLEND has no reference to emit — DTCG has no mix — so it emits the resolved * value with `$description` naming the two rungs and the share, which is the * most a design tool can be told and still be told the truth. */ export declare function textInkTokens(): string; /** A blend as a palette value, composited the way CSS's `in srgb` mix does. */ export declare function blendInk(ref: InkBlend): string;