blog.git

blog

~ Personal research notes. blog.sqzer.com
git clone https://github.com/debuman/blog.git

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>