From 01797a3c178eb3b8587f15a16d1c8d022ca5f4f9 Mon Sep 17 00:00:00 2001 From: Will Manning Date: Wed, 4 Nov 2026 06:52:33 -0500 Subject: [PATCH 1/3] Match the design system in post figures, bench's hovers, the site nav, the results source line, and blog excerpts Signed-off-by: Will Manning --- src/app/(site)/globals.css | 19 ++++ src/bench/styles/globals.css | 119 ++++++++-------------- src/bench/styles/tokens.test.ts | 36 ++++++- src/components/blog/figures.tsx | 3 +- src/components/results-table/index.tsx | 2 +- src/content/blog/btrblocks-compressor.mdx | 2 +- src/lib/figures.ts | 10 +- tests/unit/figures.test.ts | 13 +++ tests/unit/results-table.test.tsx | 17 +++- 9 files changed, 134 insertions(+), 87 deletions(-) diff --git a/src/app/(site)/globals.css b/src/app/(site)/globals.css index 86f0e7e8..f1cee738 100644 --- a/src/app/(site)/globals.css +++ b/src/app/(site)/globals.css @@ -17,6 +17,17 @@ margin-inline: auto; } +/* The header's links start just after the wordmark, and the end group takes + the slack. The system pushes the links to the end group, so they moved + whenever its width did: only the blog's header carries the ThemeSwitch. */ +.vx-header .vx-header-nav { + margin-left: 0; +} + +.vx-header .vx-header-nav + .vx-header-end { + margin-left: auto; +} + /* main fills the height the header and footer leave, and hands it on as a flex column, so the homepage's hero and the Status card can fill it. */ .site-main { @@ -122,6 +133,14 @@ margin-bottom: var(--space-16); } +/* An excerpt keeps a reading measure of about 72 characters. The row's + column is as wide as a post (68ch at 17px), so at 14px an excerpt ran past + 100 characters a line. A ch is Geist's zero, wider than an average letter, + so 48ch holds about 72 characters of prose. */ +.vx-post-row-excerpt { + max-width: 48ch; +} + /* The byline's date and reading time each stay on one line. */ .blog-nowrap { white-space: nowrap; diff --git a/src/bench/styles/globals.css b/src/bench/styles/globals.css index 2f84020d..b714559c 100644 --- a/src/bench/styles/globals.css +++ b/src/bench/styles/globals.css @@ -71,11 +71,15 @@ pre { font-family: var(--font-mono); } +/* A link is ink at rest and Blue only under the pointer, as the system's are: bench's Blue names + * Vortex, its line, and a pressed chip. A link in prose also takes the system's quiet underline + * (the methodology page, below), so it reads as a link without the Blue. */ a { - color: var(--accent-ink); + color: var(--ink); text-decoration: none; } a:hover { + color: var(--accent-ink); text-decoration: underline; } @@ -135,36 +139,34 @@ main { } } +/* The toolbar buttons (the Groups button and the "Filter series" trigger) are outlined controls: + * 4px corners, a mono label in sentence case, and a border that turns ink under the pointer. */ .control-btn { display: inline-flex; align-items: center; justify-content: center; gap: 0.45rem; min-height: 36px; + padding: var(--space-8) var(--space-20); border: 1px solid var(--line); - border-radius: var(--radius-card); + border-radius: var(--radius-control); background: var(--surface); color: var(--ink); font: inherit; - font-size: 0.875rem; + font-family: var(--font-mono); + font-size: 11px; font-weight: 500; line-height: 1; white-space: nowrap; + cursor: pointer; transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease; } -.control-btn { - padding: var(--space-8) var(--space-20); - cursor: pointer; -} - .control-btn:hover { - border-color: var(--accent-ink); - background: var(--ground); - color: var(--accent-ink); + border-color: var(--ink); } /* While it shows its word, the toggle keeps one width for "Light" and "Dark". */ @@ -215,6 +217,17 @@ main { line-height: 1.6; } +.methodology a:not([class]) { + text-decoration: underline; + text-decoration-thickness: 1px; + text-decoration-color: color-mix(in srgb, var(--ink) 30%, transparent); + text-underline-offset: 3px; +} + +.methodology a:not([class]):hover { + text-decoration-color: currentColor; +} + .methodology h2 { margin: 0 0 var(--space-16); } @@ -321,7 +334,7 @@ main { transform: rotate(90deg); } .group-summary:hover .group-summary-row { - background: var(--surface); + background: var(--wash); } /* The groups speak in the system's mono voice, as the header's label does, and heavy enough to lead their cards: 400 at 13px read thin, Light most of all. */ @@ -337,7 +350,9 @@ main { font-size: 17px; } } +/* Square, on purpose, as the filter badge is. */ .group-count { + border-radius: 2px; color: var(--ink-muted); font-family: var(--font-mono); font-size: 0.7rem; @@ -371,7 +386,7 @@ main { height: 1.5rem; padding: 0; border: none; - border-radius: var(--radius-card); + border-radius: var(--radius-control); background: transparent; color: var(--ink-muted); text-decoration: none; @@ -405,7 +420,7 @@ main { width: 1.5rem; height: 1.5rem; padding: 0; - border-radius: var(--radius-card); + border-radius: var(--radius-control); color: var(--ink-muted); text-decoration: none; flex-shrink: 0; @@ -485,7 +500,8 @@ main { visibility 0.15s ease; pointer-events: none; z-index: 10; - box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18); + /* A hairline, not a shadow: bench keeps shadows for its two chart tooltips only. */ + border: 1px solid var(--edge); } [data-tooltip].group-info-icon:hover::after, [data-tooltip].group-info-icon:focus-visible::after { @@ -719,9 +735,10 @@ main { break-inside: avoid; } +/* Ranks are muted, so Blue marks only Vortex's rows. */ .score-rank { min-width: 2.25rem; - color: var(--accent-ink); + color: var(--ink-muted); font-size: 0.75rem; font-weight: 600; font-variant-numeric: tabular-nums; @@ -799,7 +816,8 @@ main { min-width: 1.25rem; height: 1.25rem; padding: 0 0.4rem; - border-radius: var(--radius-full); + /* Square, on purpose: 2px, under the control radius. */ + border-radius: 2px; background: var(--accent); color: var(--on-accent); font-size: 0.7rem; @@ -819,7 +837,7 @@ main { max-width: min(90vw, 36rem); padding: var(--space-12) 0.85rem; border: 1px solid var(--edge); - border-radius: var(--radius-card); + border-radius: var(--radius-control); background: var(--surface); } .filter-panel[hidden] { @@ -844,7 +862,7 @@ main { min-height: 24px; padding: 0.1rem 0.6rem; border: 1px solid var(--line); - border-radius: var(--radius-full); + border-radius: var(--radius-control); background: var(--surface); color: var(--ink); font-size: 0.78rem; @@ -852,7 +870,7 @@ main { cursor: pointer; } .filter-chip:hover { - border-color: var(--accent-ink); + border-color: var(--ink); } .filter-chip--active, .filter-chip--active:hover { @@ -953,7 +971,6 @@ main { .group-filter-trigger { min-height: 28px; padding: var(--space-4) 0.85rem; - font-size: 0.78rem; } .group-filter-panel { position: absolute; @@ -968,7 +985,7 @@ main { max-width: min(90vw, 42rem); padding: var(--space-12) 0.85rem; border: 1px solid var(--edge); - border-radius: var(--radius-card); + border-radius: var(--radius-control); background: var(--surface); } .group-filter-panel[hidden] { @@ -1002,8 +1019,8 @@ main { } .group-toolbar-reset:hover, .group-toolbar-empty-toggle:hover { - border-color: var(--accent-ink); - color: var(--accent-ink); + border-color: var(--ink); + color: var(--ink); } /* While the empty charts are revealed the toggle reads as engaged. */ .group-toolbar-empty-toggle[aria-pressed='true'] { @@ -1075,11 +1092,6 @@ main { .chart-badge[hidden] { display: none; } -.chart-badge--downsampled { - background: color-mix(in srgb, var(--accent-ink) 12%, transparent); - color: var(--accent-ink); - border-color: color-mix(in srgb, var(--accent-ink) 35%, transparent); -} .chart-window-chip { font-family: inherit; @@ -1456,7 +1468,6 @@ main > .chart-card:not(.chart-grid .chart-card) .chart-wrap { .control-btn { min-height: 32px; padding: 0.4rem 0.55rem; - font-size: 0.75rem; } main { padding: var(--space-16) var(--page-pad); @@ -1646,7 +1657,7 @@ a.site-footer-sha:hover code { padding: var(--space-12); overflow-y: auto; border: 1px solid var(--edge); - border-radius: var(--radius-card); + border-radius: var(--radius-control); background: var(--surface); /* Hidden until toggled; fade + slide up from the bottom-left toggle. */ visibility: hidden; @@ -1686,7 +1697,7 @@ a.site-footer-sha:hover code { width: 100%; padding: 0.4rem var(--space-8); border: 0; - border-radius: var(--radius-card); + border-radius: var(--radius-control); background: transparent; color: var(--ink); font: inherit; @@ -1703,8 +1714,8 @@ a.site-footer-sha:hover code { .group-nav-link:hover, .group-nav-link:focus-visible { - background: var(--ground); - color: var(--accent-ink); + background: var(--wash); + color: var(--ink); } @media (prefers-reduced-motion: reduce) { @@ -1713,46 +1724,6 @@ a.site-footer-sha:hover code { } } -/* The system's corners and label voice. Cards take 6px corners and controls 4px; circles - * (the info icon, spinners, swatches) keep their 50%. The page title and the toolbar buttons read - * as mono labels in sentence case, as every label does (no capitals, no tracking), ranks are muted so Blue marks only ratios, and the count badges - * are square. */ -.group-details, -.summary-loading-panel, -.chart-card { - border-radius: var(--radius-card); -} - -.control-btn, -.toolbar-btn, -.group-toolbar-reset, -.group-toolbar-empty-toggle, -.group-filter-trigger, -.filter-panel, -.group-filter-panel, -.group-nav-panel, -.group-nav-link, -.group-permalink, -.group-docs-link, -.filter-chip { - border-radius: var(--radius-control); -} -.control-btn { - font-family: var(--font-mono); - font-weight: 500; -} -.control-btn { - font-size: 11px; -} -.score-rank { - color: var(--ink-muted); -} -.filter-badge, -.group-count { - /* Square badges, on purpose: 2px, under the control radius. */ - border-radius: 2px; -} - /* A press shows, as on the system's buttons: an outlined control fills one step up, a filled one * darkens toward the ground. A press never moves a control, so it also cancels the hover lift. * These rules come after each control's pressed or active rule, so a press shows on those too. */ diff --git a/src/bench/styles/tokens.test.ts b/src/bench/styles/tokens.test.ts index 26eb5956..9beb31d9 100644 --- a/src/bench/styles/tokens.test.ts +++ b/src/bench/styles/tokens.test.ts @@ -104,7 +104,34 @@ describe("bench's stylesheet on the system's tokens", () => { expect(lastValue('.filter-chip--active:hover', 'border-color')).toBe('var(--accent)'); expect(lastValue('.toolbar-slider', 'accent-color')).toBe('var(--accent)'); expect(lastValue('.chart-range-strip-handle', 'background')).toBe('var(--accent)'); - expect(lastValue('a', 'color')).toBe('var(--accent-ink)'); + }); + + // Bench's Blue names Vortex, its line, and a pressed chip. A link is ink at rest and Blue only + // under the pointer. Under the pointer an outlined control's border turns ink, and a row takes + // the wash, as the system's States rule says. + it('keeps Blue off links at rest and off hovers', () => { + expect(lastValue('a', 'color')).toBe('var(--ink)'); + expect(lastValue('a:hover', 'color')).toBe('var(--accent-ink)'); + for (const sel of [ + '.control-btn:hover', + '.filter-chip:hover', + '.group-toolbar-reset:hover', + '.group-toolbar-empty-toggle:hover', + ]) { + expect(lastValue(sel, 'border-color'), sel).toBe('var(--ink)'); + } + expect(lastValue('.control-btn:hover', 'color')).toBeUndefined(); + expect(lastValue('.group-summary:hover .group-summary-row', 'background')).toBe('var(--wash)'); + expect(lastValue('.group-nav-link:hover', 'background')).toBe('var(--wash)'); + expect(lastValue('.group-nav-link:hover', 'color')).toBe('var(--ink)'); + expect(lastValue('.score-rank', 'color')).toBe('var(--ink-muted)'); + expect(decls.some((d) => selectorsOf(d).includes('.chart-badge--downsampled'))).toBe(false); + }); + + // Each control's look is in its own rules, once: no late block restates it. + it('defines the toolbar button in one rule', () => { + expect(decls.filter((d) => d.sel === '.control-btn' && d.prop === 'font-size')).toHaveLength(1); + expect(lastValue('.control-btn', 'border-radius')).toBe('var(--radius-control)'); }); // The system's States rule: a pressed switch sits on surface in ink inside a raised group, and @@ -163,11 +190,10 @@ describe("bench's stylesheet on the system's tokens", () => { } }); - // Cards and popovers stand on their borders, as the system's do. Only the tooltips, which sit - // over a chart's lines, keep a shadow. - it('draws no shadows but on the tooltips', () => { + // Cards, popovers, and the group info tooltip stand on their borders, as the system's do. Only + // the chart tooltip, which sits over a chart's lines, keeps a shadow. + it('draws no shadows but on the chart tooltip', () => { expect(decls.filter((d) => d.prop === 'box-shadow').map((d) => d.sel)).toEqual([ - '[data-tooltip].group-info-icon::after', '.chart-tooltip-host', ]); }); diff --git a/src/components/blog/figures.tsx b/src/components/blog/figures.tsx index 4a58ce6d..f9beb554 100644 --- a/src/components/blog/figures.tsx +++ b/src/components/blog/figures.tsx @@ -1,6 +1,7 @@ import { type CSSProperties, Fragment, type ReactNode } from "react"; import { OpenInNewIcon } from "@/components/icons"; import { type Bar, barRows, barsLabel, chainLabel } from "@/lib/figures"; +import { barClass } from "@/vendor/vortex-design/results.js"; /** * The parts a post draws with the design system's cards. Each takes the @@ -55,7 +56,7 @@ export function Bars({
{rows.map((r) => ( -
+
{r.name}

File format benchmarks - + bench.vortex.dev diff --git a/src/content/blog/btrblocks-compressor.mdx b/src/content/blog/btrblocks-compressor.mdx index 3dda014c..3c8c3ccd 100644 --- a/src/content/blog/btrblocks-compressor.mdx +++ b/src/content/blog/btrblocks-compressor.mdx @@ -163,7 +163,7 @@ On TPC-H at scale factor 10: unit="MB" better="↓" bars={[ - { name: "Parquet + zstd", value: 4465 }, + { name: "Parquet + zstd", system: "Parquet", value: 4465 }, { name: "Vortex default", value: 2776, ours: true, extra: "0.62×" }, { name: "Vortex compact", value: 2016, ours: true, extra: "0.45×" } ]} diff --git a/src/lib/figures.ts b/src/lib/figures.ts index 36fe1681..1cdf7459 100644 --- a/src/lib/figures.ts +++ b/src/lib/figures.ts @@ -2,8 +2,15 @@ export interface Bar { name: string; value: number; - /** Vortex's own bar, in `chart-vortex`. People and other systems are grey. */ + /** Vortex's own bar, in `chart-vortex`. */ ours?: boolean; + /** + * The system the bar measures, when its name says more ("Parquet" for + * "Parquet + zstd"). Parquet, Lance, and Arrow take their muted tints, as + * the design system's `barClass` maps them. People and other systems are + * grey. + */ + system?: string; /** A ratio after the value, such as "0.62×". */ extra?: string; } @@ -22,6 +29,7 @@ export function barRows(bars: readonly Bar[]) { width: Math.round((b.value / max) * 10_000) / 100, value: NUMBER.format(b.value), ours: b.ours ?? false, + system: b.system ?? b.name, extra: b.extra })); } diff --git a/tests/unit/figures.test.ts b/tests/unit/figures.test.ts index af639e66..53b82fd6 100644 --- a/tests/unit/figures.test.ts +++ b/tests/unit/figures.test.ts @@ -18,6 +18,19 @@ describe("barRows", () => { }); }); +describe("barRows system", () => { + it("names each bar's system, so Parquet takes its tint", () => { + const rows = barRows([ + { name: "Parquet + zstd", system: "Parquet", value: 4465 }, + { name: "Connor Tsui", value: 41 } + ]); + assert.deepEqual( + rows.map((r) => r.system), + ["Parquet", "Connor Tsui"] + ); + }); +}); + describe("barsLabel", () => { it("states every figure for a screen reader", () => { assert.equal( diff --git a/tests/unit/results-table.test.tsx b/tests/unit/results-table.test.tsx index accca133..45a6dec5 100644 --- a/tests/unit/results-table.test.tsx +++ b/tests/unit/results-table.test.tsx @@ -25,12 +25,21 @@ describe("ResultsTable", () => { const html = renderToStaticMarkup( ); + // One difference, until the system's renderer catches up: the README + // gives every source line `vx-label-sm`, and renderResults leaves it off + // the head's source line. When the copy in src/vendor adds it, the first + // assertion fails: drop it and the replace then. + const system = renderResults(PANELS, { + read: formatReadDate(READ), + icon: renderToStaticMarkup() + }); + assert.ok(system.includes('')); assert.equal( html, - renderResults(PANELS, { - read: formatReadDate(READ), - icon: renderToStaticMarkup() - }) + system.replace( + '', + '' + ) ); }); From cce442f3bbbab9fe8e4ebdff8cb4b8d099a4c6bb Mon Sep 17 00:00:00 2001 From: Will Manning Date: Wed, 4 Nov 2026 09:14:18 -0500 Subject: [PATCH 2/3] Cap blog excerpts at 54ch, the design system's measure At 54ch the excerpt's full lines run 70 to 76 characters (median 72) at 1440 and 1024. At 48ch they held about 64. Signed-off-by: Will Manning --- src/app/(site)/globals.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/(site)/globals.css b/src/app/(site)/globals.css index f1cee738..26336cc5 100644 --- a/src/app/(site)/globals.css +++ b/src/app/(site)/globals.css @@ -136,9 +136,9 @@ /* An excerpt keeps a reading measure of about 72 characters. The row's column is as wide as a post (68ch at 17px), so at 14px an excerpt ran past 100 characters a line. A ch is Geist's zero, wider than an average letter, - so 48ch holds about 72 characters of prose. */ + so 54ch, the design system's measure, holds about 72 characters of prose. */ .vx-post-row-excerpt { - max-width: 48ch; + max-width: 54ch; } /* The byline's date and reading time each stay on one line. */ From eb4654f7ea37248b136f65f37097734bb9f70ba7 Mon Sep 17 00:00:00 2001 From: Will Manning Date: Wed, 4 Nov 2026 09:33:27 -0500 Subject: [PATCH 3/3] Take the header and excerpt rules from the design system main's copy of the system's styles now anchors the header links after the wordmark and caps the post-row excerpt at 54ch, so the site's overrides go. Signed-off-by: Will Manning --- src/app/(site)/globals.css | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/app/(site)/globals.css b/src/app/(site)/globals.css index 26336cc5..86f0e7e8 100644 --- a/src/app/(site)/globals.css +++ b/src/app/(site)/globals.css @@ -17,17 +17,6 @@ margin-inline: auto; } -/* The header's links start just after the wordmark, and the end group takes - the slack. The system pushes the links to the end group, so they moved - whenever its width did: only the blog's header carries the ThemeSwitch. */ -.vx-header .vx-header-nav { - margin-left: 0; -} - -.vx-header .vx-header-nav + .vx-header-end { - margin-left: auto; -} - /* main fills the height the header and footer leave, and hands it on as a flex column, so the homepage's hero and the Status card can fill it. */ .site-main { @@ -133,14 +122,6 @@ margin-bottom: var(--space-16); } -/* An excerpt keeps a reading measure of about 72 characters. The row's - column is as wide as a post (68ch at 17px), so at 14px an excerpt ran past - 100 characters a line. A ch is Geist's zero, wider than an average letter, - so 54ch, the design system's measure, holds about 72 characters of prose. */ -.vx-post-row-excerpt { - max-width: 54ch; -} - /* The byline's date and reading time each stay on one line. */ .blog-nowrap { white-space: nowrap;