src/pages/code/index.astro (8276B)
1 ---
2 import Base from '../../layouts/Base.astro';
3 import { getCollection, render } from 'astro:content';
4 import { hasHangul, langOf } from '../../lib/hangul';
5 import { assertRendered } from '../../lib/rendered';
6
7 /*
8 * Authorial order, and nothing else. `order` ascending is the whole sort: the reference's
9 * own code page is hand-ranked, and the author reorders this page by changing one number
10 * in front matter. Name and id break a tie only so the output is stable rather than
11 * filesystem-ordered — the same reason src/lib/order.ts refuses to delegate that. There
12 * is no date on this collection to sort by and no star count to let GitHub sort it.
13 */
14 const collator = new Intl.Collator('en');
15 const projects = (await getCollection('code'))
16 .filter((p) => !p.data.draft)
17 .sort(
18 (a, b) =>
19 a.data.order - b.data.order ||
20 collator.compare(a.data.name, b.data.name) ||
21 (a.id < b.id ? -1 : a.id > b.id ? 1 : 0),
22 );
23
24 /* The description is each file's markdown body, so every entry has to be rendered before
25 the template can lay it out. One `render()` call per entry, resolved together. */
26 projects.forEach(assertRendered);
27 const entries = await Promise.all(
28 projects.map(async (p) => ({ entry: p, Content: (await render(p)).Content })),
29 );
30
31 /* The same question src/pages/index.astro asks of about.md, and for the same reason: the
32 metadata on this route is English whatever the content says, so only the content can
33 answer whether the 118KB Korean face blocks first paint. Every name and description is
34 English today, so the answer is false and /code/ fetches no Korean font, as the `ko`
35 note in Base.astro says; the day one of them is written in Korean, it becomes true. */
36 const ko = projects.some((p) => hasHangul(p.data.name, p.body));
37 ---
38 <Base
39 title="Code :: debuman"
40 description="Tools and small programs I have written and published."
41 ko={ko}
42 >
43 <div class="code-index">
44 {/* The reference carries no page title on its section pages: the bar's left slot
45 already prints the page name, so an <h1> repeating it is the same word twice.
46 Kept in the document as .sr-only rather than deleted — the page still needs one
47 top-level heading for assistive tech and for the document outline, and the bar's
48 slot is a <span> precisely because it is not one. */}
49 <h1 class="sr-only">Code</h1>
50
51 {entries.length === 0 ? (
52 <p class="code-index__empty">Nothing here yet.</p>
53 ) : (
54 <ul class="projects">
55 {entries.map(({ entry, Content }) => (
56 <li class="project">
57 <h2 class="project__name" lang={langOf(entry.data.name, 'en')}>
58 <a href={entry.data.url}>{entry.data.name}</a>
59 </h2>
60 <div class="project__about" lang={hasHangul(entry.body) ? 'ko' : undefined}>
61 <Content />
62 </div>
63 </li>
64 ))}
65 </ul>
66 )}
67 </div>
68 </Base>
69
70 <style>
71 /* Capped at the site's measure, the same 928px an article's prose and the Writing
72 index's entry column get.
73
74 Capped WITHOUT `margin-inline: auto`, and that is the load-bearing half. `main` caps
75 at --w-page (1024px) and centres itself; centring a narrower box inside it again
76 would step this page's list ~48px right of `main`'s left edge, which is where the
77 header's page name and the text of /papers/ and /talk/ start. With margin-inline at
78 its 0 default the box keeps `main`'s left edge and gives the 96px back on the right —
79 the mirror of /writing/, where that same 96px is the date rail on the left. */
80 .code-index {
81 max-width: var(--w-content);
82 }
83
84 .code-index__empty {
85 font-size: var(--fs-body);
86 }
87
88 .projects {
89 list-style: none;
90 margin: 0;
91 padding: 0;
92 }
93
94 /* The reference separates its entries with a single blank line and nothing else: no
95 rule, no wash, no row-sized hit area. Copied as it stands rather than borrowing the
96 Writing index's row treatment, because that treatment is answering a different
97 question — a row on /writing/ is one whole link to a page of this site, so the row
98 can be the control. A row here is a name that leaves the site, so the name is the
99 control, and there is nothing else on the row to make clickable. Hairlines between
100 entries would be furniture the reference does not have and this page does not
101 need. */
102 .project + .project {
103 margin-top: var(--sp-32);
104 }
105
106 /* h2 is the right element — this is a list of named things directly under the page's
107 h1, and a screen reader should be able to walk it — but not the right size. --fs-h2
108 reaches 28.5px, which reads as a run of section headings; --fs-h5 is what the
109 Writing index sets its entry titles at. Family, weight and colour come from
110 base.css's heading rule, and the link keeps the heading's 500 through the global
111 `a { font-weight: inherit }`. */
112 .project__name {
113 font-size: var(--fs-h5);
114 line-height: var(--lh-medium);
115 text-wrap: balance;
116 }
117
118 /* Crimson, no underline: the colour-only treatment base.css gives <a>, and here
119 the link IS the entry's headline, so the colour is the whole affordance. --crimson
120 measures 7.25:1 against --page-bg, over the 4.5:1 normal text needs, so the colour is
121 not carrying meaning at a contrast that fails — and it lands within 0.23 of the
122 7.48:1 the reference sets its own outbound links at.
123 Hover and keyboard focus step the colour to --crimson-lift. The hover half sits
124 inside the pointer gate: a bare :hover latches after a tap on a touch screen and would
125 leave the project marked until the next tap elsewhere. :focus-visible stays outside
126 the gate -- it is the keyboard path, and the global focus ring comes with it.
127 The 0.05em of padding-block-end draws nothing. The link is inline, so it costs no
128 layout; it only lowers the bottom of the link's box, and with it the focus ring and
129 the hit area, by 0.05em: a pixel at 20px, 0.9px at the 18px floor. Check the ring
130 before removing it. */
131 .project__name a { color: var(--crimson); text-decoration: none; padding-block-end: 0.05em; }
132 /* Hover is a colour step, not a rule. The reference draws its animated underline in one
133 place only - the header nav - and every link in the body of a page, its writing index,
134 its code list and even its year rail, simply lifts from rgb(160,32,32) to
135 rgb(192,48,48) with no decoration at all. Measured over CDP on all three. */
136 .project__name a:focus-visible { color: var(--crimson-lift); }
137 @media (hover: hover) {
138 .project__name a:hover { color: var(--crimson-lift); }
139 }
140
141 /* Forced colours force `color` to the system link colour at rest and on hover alike, so
142 the colour step above does not show in that mode. An underline is what the mode can
143 still draw, so hover and focus get one here and only here. Written in this file
144 rather than in base.css because Astro scopes a page rule on every compound, which
145 outranks the same selector written globally. */
146 @media (forced-colors: active) {
147 .project__name a:focus-visible { text-decoration: underline; }
148 }
149 @media (forced-colors: active) and (hover: hover) {
150 .project__name a:hover { text-decoration: underline; }
151 }
152
153 /* margin-top is 0.375rem, matching .entry__deck on /writing/ — a description belongs
154 to the name above it, so the gap has to read as smaller than the --sp-32 between
155 entries.
156
157 The other two are set here and not on a descendant selector because <Content />
158 emits elements with no scope id, so `.project__about p` would miss every one of
159 them. Both properties inherit, which is what lets the wrapper carry them.
160
161 keep-all is a no-op on the English descriptions here and is the house rule
162 everywhere Korean can appear (.prose, .about, .entry__title); `ko` above already
163 anticipates a Korean description, so this does too. break-word is the guard against
164 a long unbroken token such as a bare URL, which would otherwise set the page's
165 minimum width and scroll it sideways on a phone. */
166 .project__about {
167 margin-top: 0.375rem;
168 word-break: keep-all;
169 overflow-wrap: break-word;
170 }
171 </style>