diff --git a/apps/web/src/no-external-fonts.test.ts b/apps/web/src/no-external-fonts.test.ts index b1797a03..1a4d163c 100644 --- a/apps/web/src/no-external-fonts.test.ts +++ b/apps/web/src/no-external-fonts.test.ts @@ -3,6 +3,21 @@ import { readFileSync } from "node:fs"; import document from "../index.html?raw"; const hearthTokens = readFileSync(new URL("./hearth-tokens.css", import.meta.url), "utf8"); +const styles = readFileSync(new URL("./styles.css", import.meta.url), "utf8"); + +function cssBlock(selector: string) { + const start = styles.indexOf(`${selector} {`); + if (start < 0) throw new Error(`Missing CSS rule for ${selector}`); + const end = styles.indexOf("}", start); + if (end < 0) throw new Error(`Unclosed CSS rule for ${selector}`); + return styles.slice(start, end + 1); +} + +function customProperty(block: string, name: string) { + const match = block.match(new RegExp(`\\s${name}:\\s*([^;]+);`)); + if (!match) throw new Error(`Missing ${name}`); + return match[1].trim(); +} describe("production typography boundary", () => { it("does not require an external font CDN", () => { @@ -15,4 +30,35 @@ describe("production typography boundary", () => { expect(hearthTokens).toContain("--hearth-azure"); expect(hearthTokens).not.toMatch(/url\(|https?:\/\/|@import/i); }); + + it("keeps neutral metadata capsules on the public Hearth scale", () => { + const root = cssBlock(":root"); + const tag = cssBlock(".tag"); + const tagWrap = cssBlock(".tag-wrap"); + const reference = cssBlock(".ref-chip"); + + for (const rule of [tag, reference]) { + expect(rule).toContain("gap: var(--s1)"); + expect(rule).toContain("font-size: var(--type-xs)"); + } + expect(tag).toContain("padding: 2px var(--s2)"); + expect(tag).toContain("border-radius: var(--r-sm)"); + expect(tagWrap).toContain("gap: var(--s1)"); + expect(reference).toContain("padding: var(--s1) var(--s2)"); + expect(reference).toContain("border-radius: 999px"); + expect(customProperty(root, "--s1")).toBe("4px"); + expect(customProperty(root, "--s2")).toBe("8px"); + expect(customProperty(root, "--r-sm")).toBe("7px"); + expect(customProperty(root, "--type-xs")).toBe("11px"); + }); + + it("keeps state, topology selection, and AI suggestion capsules distinct", () => { + const state = cssBlock(".state-pill"); + const filter = cssBlock(".filter-chip"); + const suggestion = cssBlock(".suggest-chip"); + + expect(state).toContain("color: var(--c, var(--s-unknown))"); + expect(filter).toContain("text-transform: capitalize"); + expect(suggestion).toContain("border-radius: 999px"); + }); }); diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index fe9e0816..0af1dd8b 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -44,6 +44,10 @@ --s5: 24px; --s6: 32px; + /* Hearth's compact metadata type role. Geometry continues through the + established --s* and --r-* aliases until #222's wider export lands. */ + --type-xs: 11px; + --shadow: var(--hearth-shadow); --t: 140ms cubic-bezier(0.22, 0.61, 0.36, 1); @@ -592,10 +596,12 @@ kbd { .tag { display: inline-flex; align-items: center; - gap: 5px; - padding: 2px 8px; - border-radius: 6px; - font-size: 11.5px; + /* The 2px block padding is a DockerMap density exception; the shared + type, horizontal rhythm, and corner geometry come from Hearth. */ + gap: var(--s1); + padding: 2px var(--s2); + border-radius: var(--r-sm); + font-size: var(--type-xs); color: var(--muted); background: var(--surface-2); border: 1px solid var(--border); @@ -622,7 +628,7 @@ kbd { .tag-wrap { display: flex; flex-wrap: wrap; - gap: 6px; + gap: var(--s1); } .kv { @@ -1953,12 +1959,12 @@ kbd { .ref-chip { display: inline-flex; align-items: center; - gap: 6px; - padding: 4px 10px; + gap: var(--s1); + padding: var(--s1) var(--s2); border-radius: 999px; background: var(--surface-2); border: 1px solid var(--border); - font-size: 12px; + font-size: var(--type-xs); font-weight: 600; }