diff --git a/web/src/app/globals.css b/web/src/app/globals.css index e0dbada15..6ba5972b8 100644 --- a/web/src/app/globals.css +++ b/web/src/app/globals.css @@ -1141,7 +1141,6 @@ .journal-lockup, .journal-featured-meta, .journal-archive-label, - .journal-list-number, .journal-list-meta, .journal-index-footer, .journal-back-link, @@ -1239,14 +1238,6 @@ rgba(20, 20, 20, 0.035) 15px ); } - .journal-index-thumb span { - position: relative; - font-family: var(--mono); - font-size: 10px; - letter-spacing: 0.1em; - text-transform: uppercase; - color: var(--ink-muted); - } .journal-index-thumb img { position: relative; z-index: 1; @@ -1265,17 +1256,6 @@ border: 1px solid var(--rule); background: var(--cream-2); } - .journal-cover-slot::before { - content: none; - } - .journal-cover-slot span { - position: relative; - font-family: var(--mono); - font-size: 10px; - letter-spacing: 0.1em; - text-transform: uppercase; - color: var(--ink-muted); - } .journal-cover-slot img { position: relative; z-index: 1; @@ -1318,9 +1298,6 @@ .journal-post-list a .pf-ghost { right: 0; } - .journal-list-number { - color: var(--ink-faint); - } .journal-list-body { display: grid; gap: 4px; @@ -1455,10 +1432,6 @@ overflow: visible; text-align: center; } - .journal-hero-cover .journal-cover-slot::before { - content: none; - display: none; - } .journal-hero-cover .journal-cover-slot img { display: block; width: auto; @@ -1635,27 +1608,6 @@ border: 0; background: var(--rule); } - .journal-figure { - margin: 36px 0; - } - .journal-figure-wide { - width: min(82vw, 1312px); - margin-left: 50%; - transform: translateX(-50%); - } - .journal-figure-full { - width: 100vw; - margin-left: 50%; - transform: translateX(-50%); - } - .journal-figure img, - .journal-figure video { - width: 100%; - height: auto; - display: block; - border: 1px solid var(--rule); - background: var(--cream-2); - } .journal-lightbox { position: fixed; inset: 0; @@ -1704,13 +1656,6 @@ text-transform: uppercase; cursor: pointer; } - .journal-figure figcaption { - margin-top: 10px; - font-family: var(--mono); - font-size: var(--pf-fs-mono); - letter-spacing: 0.06em; - color: var(--ink-muted); - } .journal-end-mark { margin-top: 80px; display: flex; @@ -1995,12 +1940,6 @@ margin-right: 12px; color: var(--pf-ink-muted); } - .journal-figure-wide, - .journal-figure-full { - width: 100%; - margin-left: 0; - transform: none; - } .journal-md-figure, .journal-md-figure:has(+ .journal-md-figure), .journal-md-figure:has(+ .journal-md-figure) + .journal-md-figure { diff --git a/web/src/components/journal/journalCss.test.ts b/web/src/components/journal/journalCss.test.ts index f02ee67b9..e100fb0c2 100644 --- a/web/src/components/journal/journalCss.test.ts +++ b/web/src/components/journal/journalCss.test.ts @@ -4,9 +4,12 @@ // ("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 { readdirSync, readFileSync } from "node:fs"; import path from "node:path"; +import { createElement } from "react"; +import { render } from "@testing-library/react"; import { describe, expect, it } from "vitest"; +import JournalImage from "./JournalImage"; const css = readFileSync(path.resolve(__dirname, "../../app/globals.css"), "utf8"); @@ -240,3 +243,65 @@ describe("journal footers", () => { expect(sides(phone, ".journal-index-footer")).toEqual([0, 0]); }); }); + +// Rules that matched nothing on a journal page. No component and none of the +// 74 posts wrote .journal-list-number, .journal-figure, -wide or -full (the +// posts' figures are journal-md-figure and journal-figure-row). The two cover +// slots hold a JournalImage, and the span rules inside them waited for a span +// it never draws. The cover slot's ::before was switched off twice, and no +// rule ever switched it on. On 27 October that was 61 lines of globals.css. + +/** Every .ts and .tsx under src/ that is not a test, and every post. */ +function written() { + const out: string[] = []; + const walk = (dir: string) => { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const at = path.join(dir, entry.name); + if (entry.isDirectory()) walk(at); + else if (/\.(tsx?|mdx?)$/.test(entry.name) && !/\.test\./.test(entry.name)) out.push(readFileSync(at, "utf8")); + } + }; + walk(path.resolve(__dirname, "../..")); + walk(path.resolve(__dirname, "../../../content/journal")); + return out.join("\n"); +} + +describe("journal rules", () => { + const all = rules(css); + const selectors = all.flatMap((r) => r.selectors); + + it("name only classes a journal component or post writes", () => { + const source = written(); + const named = new Set(selectors.flatMap((s) => [...s.matchAll(/\.(journal-[\w-]+)/g)].map((m) => m[1]))); + const unwritten = [...named].filter((c) => !new RegExp(`(^|[^\\w-])${c}(?![\\w-])`).test(source)); + expect(named.size).toBeGreaterThan(30); + expect(unwritten).toEqual([]); + }); + + it("style nothing in a cover slot but what JournalImage draws", () => { + const { container } = render(createElement(JournalImage, { src: "/journal/good-but/funding-success.png" })); + const drawn = new Set([...container.querySelectorAll("*")].map((el) => el.tagName.toLowerCase())); + const inSlot = selectors + .map((s) => s.match(/\.journal-(?:index-thumb|cover-slot) ([a-z]+)$/)?.[1]) + .filter((tag): tag is string => Boolean(tag)); + expect(inSlot.length).toBeGreaterThan(0); + for (const tag of inSlot) expect(drawn, `.journal-… ${tag}`).toContain(tag); + }); + + it("switch off no ::before that no rule switches on", () => { + const last = (selector: string) => selector.split(" ").at(-1); + const content = (body: string) => declaration(body, "content"); + for (const rule of all) { + for (const selector of rule.selectors) { + if (!selector.startsWith(".journal-") || !selector.endsWith("::before") || content(rule.body) !== "none") continue; + const on = all.some( + (other) => + other.selectors.some((s) => last(s) === last(selector)) && + content(other.body) !== undefined && + content(other.body) !== "none", + ); + expect(on, selector).toBe(true); + } + } + }); +});