Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion web/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -1294,9 +1294,12 @@
.journal-post-list li {
border-bottom: 0;
}
/* The date column is at least 120px and as wide as its date. At 120px a
two-digit Korean date (130px, "2026년 10월 29일") and a long English
one (159px, "SEPTEMBER 29, 2026") broke onto a second line. */
.journal-post-list a {
display: grid;
grid-template-columns: 1fr 120px 80px;
grid-template-columns: minmax(0, 1fr) minmax(120px, max-content) 80px;
gap: 24px;
align-items: baseline;
padding-right: 104px;
Expand Down Expand Up @@ -1346,6 +1349,7 @@
}
.journal-list-meta {
text-align: right;
white-space: nowrap;
}
.journal-list-body .pf-row-d {
max-width: none;
Expand Down
62 changes: 62 additions & 0 deletions web/src/components/journal/journalCss.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// jsdom applies no stylesheet, so the journal's rules are read from
// app/globals.css. Above 720px each archive row is a grid, and its date
// column was a fixed 120px. On 27 October, at 1440, 27 of the 37 Korean dates
// ("2026년 9월 29일" is 122px) and 17 of the 37 English ones ("SEPTEMBER 29,
// 2026" is 159px) broke onto a second line, leaving 일 or 2026 alone under it.

import { readFileSync } from "node:fs";
import path from "node:path";
import { describe, expect, it } from "vitest";

const css = readFileSync(path.resolve(__dirname, "../../app/globals.css"), "utf8");

/** Every rule with no braces inside it; an @media block's rules come out on their own. */
function rules(source: string) {
const bare = source.replace(/\/\*[\s\S]*?\*\//g, "");
return [...bare.matchAll(/([^{}]+)\{([^{}]*)\}/g)].map((m) => ({
selectors: m[1].split(",").map((s) => s.trim().replace(/\s+/g, " ")),
body: m[2],
}));
}

function declaration(body: string, property: string) {
return body.match(new RegExp(`(?:^|;)\\s*${property}:\\s*([^;]+)`))?.[1].trim();
}

/** "minmax(0, 1fr) 120px 80px" → ["minmax(0, 1fr)", "120px", "80px"]. */
function tracks(value: string) {
const out: string[] = [];
let depth = 0;
let current = "";
for (const ch of value) {
if (ch === "(") depth++;
if (ch === ")") depth--;
if (/\s/.test(ch) && depth === 0) {
if (current) out.push(current);
current = "";
} else {
current += ch;
}
}
if (current) out.push(current);
return out;
}

describe("journal archive dates", () => {
const all = rules(css);

it("sizes the date column to its date", () => {
const row = all.find(
(r) => r.selectors.includes(".journal-post-list a") && declaration(r.body, "display") === "grid",
);
expect(row, "no .journal-post-list a grid rule").toBeDefined();
const columns = tracks(declaration(row!.body, "grid-template-columns") ?? "");
expect(columns, "title, date, reading time").toHaveLength(3);
expect(columns[1], "the date track").toMatch(/max-content/);
});

it("keeps a date on one line", () => {
const own = all.filter((r) => r.selectors.includes(".journal-list-meta"));
expect(own.some((r) => declaration(r.body, "white-space") === "nowrap")).toBe(true);
});
});
Loading