website

#astro#js#html#css

git clone https://git.pyrossh.dev/website

木 Personal website of pyrossh. Built with astrojs, shiki, vite.


18b36a2 — pyrossh 2026-11-06T15:29:20+05:30
Improve markdown readability in light and dark themes
assets/css/markdown.css CHANGED
@@ -10,9 +10,32 @@
10
10
  between markdown content and the file-viewer, so it stays global
11
11
  rather than duplicated here). */
12
12
  .markdown-body {
13
+ /* Reading colors, redefined here so long documents get them without changing
14
+ the rest of the site. Targets are about 12:1 for body text, since the
15
+ site-wide near-white on near-black (18:1) is harsh for long reading, and at
16
+ least 4.5:1 for everything else, including syntax tokens in light mode. */
17
+ --color-text: light-dark(hsl(0, 0%, 20%), hsl(210, 14%, 86%));
18
+ --color-heading: light-dark(hsl(0, 0%, 12%), hsl(210, 17%, 95%));
19
+ --color-link: light-dark(hsl(211, 100%, 36%), hsl(211, 100%, 72%));
20
+ --color-pre-bg: light-dark(hsl(0, 0%, 98%), hsl(210, 13%, 6%));
21
+ --color-code-fg: light-dark(hsl(348, 80%, 34%), hsl(348, 75%, 80%));
22
+ --color-code-bg: light-dark(hsl(0, 0%, 90%), hsl(210, 11%, 17%));
23
+ --color-markdown-border: light-dark(hsl(0, 0%, 80%), hsl(210, 10%, 26%));
24
+
25
+ color: var(--color-text);
13
26
  font-size: 1.1rem;
14
27
  line-height: 1.5;
15
28
 
29
+ h1,
30
+ h2,
31
+ h3,
32
+ h4,
33
+ h5,
34
+ h6,
35
+ strong {
36
+ color: var(--color-heading);
37
+ }
38
+
16
39
  h1 {
17
40
  font-size: var(--fs-heading-md);
18
41
  font-weight: 700;
@@ -74,25 +97,28 @@
74
97
  margin-bottom: 0;
75
98
  }
76
99
 
77
- p code,
100
+ /* Inline code anywhere (paragraphs, lists, tables, headings), not fenced
101
+ blocks. Sized relative to its surroundings so it stays in proportion. */
78
- li code {
102
+ :not(pre) > code {
79
- color: var(--color-code-fg) !important;
103
+ color: var(--color-code-fg);
80
- background: var(--color-code-bg) !important;
104
+ background: var(--color-code-bg);
81
105
  word-wrap: break-word;
82
106
  box-decoration-break: clone;
83
107
  padding: 2px 4px;
84
108
  border-radius: 0.2rem;
85
109
  font-weight: 400;
86
- font-size: 0.8rem;
110
+ font-size: 0.85em;
87
111
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
88
112
  }
89
113
 
90
- /* Fenced code blocks in a README (markdown-it output — no language-
91
- class on the <pre> itself, unlike Prism's blog-post output, which is
92
- how this targets only the README case) get a subtle border + corner
93
- radius, matching the framed-code-block look. */
114
+ /* Code inside a link has to still read as a link. */
115
+ a[href] > code {
94
- pre:not([class*="language-"]) {
116
+ color: var(--color-link);
117
+ }
118
+
119
+ pre {
120
+ font-size: 0.9rem;
95
- border: 1px solid var(--color-box-bg);
121
+ border: 1px solid var(--color-markdown-border);
96
122
  border-radius: 0.4rem;
97
123
  }
98
124
 
@@ -113,7 +139,7 @@
113
139
 
114
140
  th,
115
141
  td {
116
- border: 1px solid var(--color-box-bg);
142
+ border: 1px solid var(--color-markdown-border);
117
143
  padding: 0.4rem 0.8rem;
118
144
  text-align: left;
119
145
  }
@@ -128,10 +154,8 @@
128
154
  }
129
155
 
130
156
  blockquote {
131
- font-size: 1.2rem;
132
- font-weight: 600;
133
- background: var(--color-highlight-bg);
157
+ border-left: 3px solid var(--color-markdown-border);
134
- padding: 0.4rem;
158
+ padding: 0.2rem 0 0.2rem 0.9rem;
135
159
  margin: 16px 0px;
136
160
 
137
161
  p {
assets/css/shared.css CHANGED
@@ -77,15 +77,15 @@ table { border-collapse: collapse; }
77
77
  --color-code-bg: light-dark(hsl(0, 0%, 89%), hsl(210, 13%, 6%));
78
78
  --color-pre-bg: light-dark(hsl(0, 0%, 89%), hsl(210, 13%, 6%));
79
79
  --color-tag: light-dark(hsl(152, 96%, 35%), hsl(152, 96%, 44%));
80
- --token-comment: light-dark(hsl(220, 10%, 45%), hsl(220, 10%, 60%));
80
+ --token-comment: light-dark(hsl(220, 10%, 38%), hsl(220, 10%, 60%));
81
- --token-keyword: light-dark(hsl(310, 60%, 45%), hsl(310, 70%, 72%));
81
+ --token-keyword: light-dark(hsl(310, 60%, 36%), hsl(310, 70%, 72%));
82
- --token-string: light-dark(hsl(95, 45%, 35%), hsl(95, 55%, 65%));
82
+ --token-string: light-dark(hsl(95, 50%, 25%), hsl(95, 55%, 65%));
83
- --token-function: light-dark(hsl(210, 70%, 45%), hsl(210, 80%, 72%));
83
+ --token-function: light-dark(hsl(210, 70%, 36%), hsl(210, 80%, 72%));
84
- --token-number: light-dark(hsl(25, 80%, 42%), hsl(25, 90%, 65%));
84
+ --token-number: light-dark(hsl(25, 85%, 30%), hsl(25, 90%, 65%));
85
- --token-builtin: light-dark(hsl(175, 55%, 32%), hsl(175, 60%, 60%));
85
+ --token-builtin: light-dark(hsl(175, 60%, 24%), hsl(175, 60%, 60%));
86
- --token-punctuation: light-dark(hsl(220, 8%, 40%), hsl(220, 8%, 70%));
86
+ --token-punctuation: light-dark(hsl(220, 8%, 36%), hsl(220, 8%, 70%));
87
- --token-tag: light-dark(hsl(0, 60%, 45%), hsl(0, 70%, 72%));
87
+ --token-tag: light-dark(hsl(0, 60%, 38%), hsl(0, 70%, 72%));
88
- --token-attr-name: light-dark(hsl(35, 70%, 38%), hsl(35, 80%, 65%));
88
+ --token-attr-name: light-dark(hsl(35, 80%, 26%), hsl(35, 80%, 65%));
89
89
  color-scheme: light dark;
90
90
  --btn-light: none;
91
91
  --btn-dark: block;
tests/pages.test.js CHANGED
@@ -251,6 +251,59 @@ describe("repo pages", () => {
251
251
  assert.notEqual(style.color, style.parentColor);
252
252
  });
253
253
 
254
+ for (const colorScheme of ["dark", "light"]) {
255
+ test(`markdown text is comfortably readable in ${colorScheme} mode`, async () => {
256
+ await page.emulateMedia({ colorScheme });
257
+ try {
258
+ await goto(`/repos/${repoId}/files/README.md/`);
259
+ await page.locator("#file-content > .markdown-body").waitFor();
260
+ const ratios = await page.evaluate(() => {
261
+ const channels = (color) => color.match(/[\d.]+/g).slice(0, 3).map(Number);
262
+ const luminance = (color) => {
263
+ const [r, g, b] = channels(color).map((v) => {
264
+ const c = v / 255;
265
+ return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
266
+ });
267
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
268
+ };
269
+ const contrast = (a, b) => {
270
+ const [hi, lo] = [luminance(a), luminance(b)].sort((x, y) => y - x);
271
+ return (hi + 0.05) / (lo + 0.05);
272
+ };
273
+ const style = (selector) => getComputedStyle(document.querySelector(selector));
274
+ const article = style("#file-content .markdown-body");
275
+ const pre = style("#file-content .markdown-body pre");
276
+ const inlineCode = style("#file-content .markdown-body :not(pre) > code");
277
+ const tokenColor = (name) => {
278
+ const probe = document.createElement("span");
279
+ probe.style.color = `var(--token-${name})`;
280
+ document.querySelector("#file-content .markdown-body pre").append(probe);
281
+ const { color } = getComputedStyle(probe);
282
+ probe.remove();
283
+ return color;
284
+ };
285
+ return {
286
+ body: contrast(article.color, article.backgroundColor),
287
+ link: contrast(style("#file-content .markdown-body a[href]").color, article.backgroundColor),
288
+ inlineCode: contrast(inlineCode.color, inlineCode.backgroundColor),
289
+ codeBlock: contrast(pre.color, pre.backgroundColor),
290
+ tokens: ["comment", "keyword", "string", "function", "number", "builtin", "punctuation", "tag", "attr-name"].map(
291
+ (name) => contrast(tokenColor(name), pre.backgroundColor),
292
+ ),
293
+ };
294
+ });
295
+
296
+ // Not just a floor: near-white on near-black is hard to read for long.
297
+ assert.ok(ratios.body >= 7 && ratios.body <= 14, `body text is ${ratios.body}:1`);
298
+ for (const [part, ratio] of Object.entries({ ...ratios, tokens: Math.min(...ratios.tokens) })) {
299
+ assert.ok(ratio >= 4.5, `${part} is ${ratio}:1`);
300
+ }
301
+ } finally {
302
+ await page.emulateMedia({ colorScheme: null });
303
+ }
304
+ });
305
+ }
306
+
254
307
  test("repo files index renders the tree and a default file", async () => {
255
308
  const response = await goto(`/repos/${repoId}/files/`);
256
309
  assert.equal(response.status(), 200);