diff --git a/content/posts/listings-that-cannot-show-their-own-dates.ko.md b/content/posts/listings-that-cannot-show-their-own-dates.ko.md
@@ -2,7 +2,7 @@
title = "자기 날짜를 못 보여 주는 목록"
description = "날짜순으로 세워 놓고 그 날짜를 숨기는 생성기. 고치는 데 든 값과, 일부러 빼놓은 것."
date = 2026-08-03
-toc = false
+toc = true
+++
[sqzass](@/projects/sqzass.md) 는 처음부터 섹션을 날짜순으로 정렬할 수 있었다.
diff --git a/content/posts/listings-that-cannot-show-their-own-dates.md b/content/posts/listings-that-cannot-show-their-own-dates.md
@@ -2,7 +2,7 @@
title = "Listings that cannot show their own dates"
description = "A generator that sorts by date and then hides it. What the fix cost, and what I left out."
date = 2026-08-03
-toc = false
+toc = true
+++
[sqzass](@/projects/sqzass.md) has always been able to sort a section by date.
diff --git a/content/posts/what-this-site-is-built-on.ko.md b/content/posts/what-this-site-is-built-on.ko.md
@@ -2,7 +2,7 @@
title = "이 사이트는 무엇으로 만들어졌나"
description = "바이너리 하나, 스타일시트 하나, 스크립트 둘, 그리고 생성기 말고는 빌드 단계가 없다."
date = 2026-08-02
-toc = false
+toc = true
+++
이 사이트는 직접 만든 [sqzass](@/projects/sqzass.md) 가 빌드하고, 바이너리를
diff --git a/content/posts/what-this-site-is-built-on.md b/content/posts/what-this-site-is-built-on.md
@@ -2,7 +2,7 @@
title = "What this site is built on"
description = "One binary, one stylesheet, two scripts, and no build step that is not the generator."
date = 2026-08-02
-toc = false
+toc = true
+++
This site is built by [sqzass](@/projects/sqzass.md), which I wrote, and
diff --git a/static/css/main.css b/static/css/main.css
@@ -276,13 +276,13 @@ main { display: block; padding-bottom: clamp(5rem, 12vw, 9rem); }
padding: 0; border: 0; background: none; cursor: pointer;
font: inherit; font-size: 0.7rem; letter-spacing: 0.12em;
text-transform: uppercase; color: var(--ink-3);
- /* 좁은 화면에서 "한국어"가 세 줄로 쪼개져 마스트헤드 높이를 밀어 올린다.
- 컨트롤 라벨은 낱말 하나이고, 낱말 하나는 줄바꿈하지 않는다. */
+ /* 컨트롤 라벨은 낱말 하나이고, 낱말 하나는 줄바꿈하지 않는다. body 가
+ overflow-wrap: anywhere 라 이걸 빼면 좁은 화면에서 "Search" 가 쪼개져
+ 마스트헤드 높이를 밀어 올린다. */
white-space: nowrap;
transition: color var(--tr-ink);
}
.chip:hover { color: var(--ink); }
-.chip-lang { font-size: 0.7rem; }
/* 키를 말하는 컨트롤 하나만 진짜 키캡이 된다 — 터미널에서 모는 도구라 얇은
테두리에 작은 반경. 네 칩에 전부 테두리를 두르면 테두리 없는 타일·원장·
@@ -366,12 +366,48 @@ main { display: block; padding-bottom: clamp(5rem, 12vw, 9rem); }
text-wrap: pretty;
}
+/* 표제 아래 한 줄. 날짜와 언어가 여기 함께 선다 — 둘 다 이 문서에 대한 사실이고,
+ 따로 세우면 제목과 본문 사이에 줄만 하나 더 생긴다. 비어 있으면 gap 도 여백도
+ 없다(빈 flex 컨테이너라 높이가 0 이다). */
.page-meta {
+ display: flex; flex-wrap: wrap; align-items: baseline;
+ justify-content: center; gap: 1.1rem;
margin: 1.2rem 0 0;
font-family: var(--mono); font-size: 0.74rem;
letter-spacing: 0.08em; font-variant-numeric: tabular-nums;
color: var(--ink-3);
}
+.page-meta:empty { margin: 0; }
+
+/* ---------- language switch ---------- */
+
+/* 두 글자 코드 둘. 사이에 구분자를 두지 않는다 — 간격이 이미 나누고 있고,
+ 가운뎃점이나 슬래시를 넣으면 두 낱말이 한 낱말처럼 붙어 보인다.
+
+ 현재 언어가 제일 밝다. 크롬의 현재 항목이 밝아지는 것과 같은 규칙이고,
+ 반대로 두면 "지금 어디에 있는지"를 알려고 두 회색을 견줘야 한다.
+ 갈 수 있는 쪽에는 밑줄 자리가 비어 있다가 손이 닿으면 액센트로 채워진다 —
+ 색만으로 뜻을 전하지 않는다는 이 사이트의 규칙 그대로다. */
+/* 자기 크기를 스스로 갖는다. .page-meta 안에도 서고 프로젝트 히어로의 배지 줄
+ 아래에도 서는데, 부모마다 크기가 달라지면 같은 컨트롤이 페이지마다 다른
+ 물건으로 보인다. */
+.langs {
+ display: inline-flex; gap: 0.55rem;
+ font-family: var(--mono); font-size: 0.7rem;
+ letter-spacing: 0.16em; text-transform: uppercase;
+}
+/* 히어로에서는 배지 줄 바로 아래 붙는다 — 둘 다 이 프로젝트에 대한 사실이다. */
+.hero-main > .langs { margin-top: 0.85rem; }
+.lang-cur { color: var(--ink); }
+/* 갈 수 있는 쪽은 이 사이트의 링크 단이다(--ink-3). --ink-4 는 날짜와 펜스의
+ 단이라 "있지만 누를 것은 아닌" 자리이고, 라이트 테마에서는 1.7:1 로 사실상
+ 안 보인다. 지금 판(--ink)과의 차이는 이 단에서도 충분히 크다. */
+.langs a {
+ color: var(--ink-3);
+ border-bottom: 1px solid transparent;
+ transition: color var(--tr-ink), border-color var(--tr-ink);
+}
+.langs a:hover { color: var(--ink-2); border-bottom-color: var(--accent); }
.eyebrow {
margin: 0 0 0.9rem;
@@ -436,9 +472,14 @@ main { display: block; padding-bottom: clamp(5rem, 12vw, 9rem); }
max-width: 44ch; color: var(--ink-2); text-wrap: pretty;
}
+/* 첫 화면에서는 언어 줄이 문 위에 선다. 문 뒤에 붙이면 가운데 정렬 스택에서
+ 한 줄이 통째로 접힌 자리 아래로 내려간다. 문의 margin-top 이 이 줄에 맞춰
+ 깎여 있으므로(clamp(0.85rem…)), 두 값은 함께 고쳐야 한다. */
+.hero-home .page-meta { margin-top: clamp(1.3rem, 3vw, 1.9rem); }
+
/* 문. 채운 알약이 아니라 밑줄 그은 라벨이다. */
.door {
- display: inline-block; margin-top: clamp(1.8rem, 4vw, 2.75rem);
+ display: inline-block; margin-top: clamp(0.85rem, 1.8vw, 1.15rem);
padding-bottom: 0.35rem;
font-size: 0.72rem; letter-spacing: 0.22em; text-transform: uppercase;
color: var(--ink-2);
@@ -925,7 +966,7 @@ html[lang="ko"] [data-tooltip]::after { letter-spacing: 0; text-transform: none;
/* ---------- toc & pager ---------- */
-.toc { margin: 2rem auto 0; max-width: var(--read); padding: 1rem 1.25rem; border-left: 1px solid var(--rule-2); }
+.toc { margin: 2rem auto 2.5rem; max-width: var(--read); padding: 1rem 1.25rem; border-left: 1px solid var(--rule-2); }
.toc-label {
margin: 0 0 0.5rem; font-size: 0.66rem;
letter-spacing: 0.22em; text-transform: uppercase; color: var(--ink-4);
@@ -936,6 +977,112 @@ html[lang="ko"] [data-tooltip]::after { letter-spacing: 0; text-transform: none;
.toc a { color: var(--ink-3); transition: color var(--tr-ink); }
.toc a:hover { color: var(--accent); }
+/* ---------- reading rail ---------- */
+
+/* 목차의 두 번째 모습. 넓은 화면에서 오른쪽 여백에 서고, 절마다 세로 헤어라인
+ 한 칸이며 칸의 높이가 그 절이 글에서 차지하는 몫이다. 목록은 절이 몇 개인지
+ 말하고 지도는 어느 절이 긴지 말한다 — 같은 자료의 다른 사실이라 둘 다 있어도
+ 중복이 아니다.
+
+ 레일이 실제로 만들어졌을 때만 목록이 물러난다(.has-rail 은 ui.js 가 붙인다).
+ 스크립트가 못 오면 목록이 그대로 남고, 그게 이 장치의 폴백이다. 문서 안에
+ 목차는 언제나 한 벌이고 접근성 트리에도 한 벌만 남는다. */
+
+.rail { display: none; }
+
+/* 이 시트의 첫 min-width 다. 나머지는 전부 max-width — 넓게 시작해서 좁아지면
+ 뺀다는 자세이고, 이것만 예외인 이유는 레일이 없던 자리를 요구하는 유일한
+ 요소이기 때문이다. 좁은 화면에서는 줄이는 게 아니라 뺀다. */
+@media (min-width: 80rem) {
+ .rail {
+ display: grid;
+ /* 왼쪽이 눈금, 오른쪽이 이름. 선을 먼저 긋고 그 옆에 글을 두는 것은 .toc 의
+ border-left 와 blockquote 가 이미 쓰는 이 집의 곁줄 어법이다. 눈금을 창
+ 끝에 붙이면 스크롤바 바로 옆에 세로줄이 하나 더 생긴다. */
+ grid-template-columns: 1px minmax(0, 1fr);
+ column-gap: 0.9rem;
+ row-gap: 0.4rem;
+ /* 행 높이는 ui.js 가 grid-template-rows 로 쓴다. 겹침 풀이를 격자에 맡기면
+ 참조 구현이 손으로 짠 두 번의 밀어내기 패스와 그 안의 상수 셋이 통째로
+ 없어진다. */
+
+ position: fixed;
+ /* 창이 아니라 본문에 붙는다. right:0 으로 매달면 2560px 에서 레일만 500px
+ 멀어져 무엇의 눈금인지 알 수 없게 된다. .wrap 은 --gutter 만큼 안쪽
+ 패딩이 있으므로 글자가 끝나는 자리는 50vw + --read/2 - --gutter 다.
+ 거기서 2.5rem 떨어뜨린다 — 이 부품의 유일한 임의값이고, 본문과 레일이
+ 서로 다른 것으로 읽히는 최소 거리다. 폭도 같은 값들에서 끌어내므로
+ --read 를 고치면 레일이 따라온다(.wrap-ledger 가 같은 방법을 쓴다). */
+ left: calc(50vw + var(--read) / 2 - var(--gutter) + 2.5rem);
+ width: min(14rem, calc(50vw - var(--read) / 2 + var(--gutter) - 4rem));
+ top: 50%; transform: translateY(-50%);
+ height: min(58vh, 30rem);
+ /* 마스트헤드(30) 아래. 올라온 이름이 크롬을 덮으면 안 된다. 검색은 modal
+ <dialog> 라 top layer 에 뜨므로 셈에 넣지 않는다. */
+ z-index: 20;
+ /* 레일 자체는 손을 받지 않는다. 여백 전체를 투명한 판으로 덮으면 나중에 그
+ 여백에 놓는 것이 전부 눌리지 않는다. */
+ pointer-events: none;
+ }
+
+ .rail-seg {
+ position: relative;
+ background: var(--rule-2);
+ pointer-events: auto;
+ transition: background-color var(--tr-ink);
+ }
+ /* 1px 은 겨눌 수 없다. 보이는 선은 그대로 두고 손이 닿는 자리만 넓힌다.
+ 오른쪽 1rem 은 column-gap(0.9rem)보다 넓다 — 눈금에서 이름으로 커서가
+ 건너가는 중간에서 반응이 끊기면 이름이 도로 사라진다. 세로 -0.2rem 은
+ row-gap 의 절반이라 칸 사이에서도 안 끊긴다. */
+ .rail-seg::before { content: ""; position: absolute; inset: -0.2rem -1rem; }
+
+ /* 읽은 만큼. 높이는 ui.js 가 스크롤에서 직접 쓴다 — 전환을 걸면 화면보다 한
+ 박자 늦게 따라와 거짓말을 한다. 이미 연속값에 매인 것은 애니메이션하지
+ 않는다. 색은 상태라서 전환한다. */
+ .rail-fill {
+ display: block; width: 100%; height: 0;
+ background: var(--ink-3);
+ transition: background-color var(--tr-ink);
+ }
+ /* 세 단이다: 헤어라인은 아직 안 읽은 것, 잉크는 읽은 것, 주황은 지금 읽는
+ 것. 주황이 색 하나로만 말하지 않는다 — 지금 칸은 이 열에서 유일하게 덜 찬
+ 칸이고(위는 다 차고 아래는 비어 있다), 그 이름은 유일하게 --ink 다.
+ 클래스로 표시하는 이유: 왼쪽이 눈금이라 DOM 에서 눈금이 이름보다 앞에 오고,
+ CSS 에는 앞 형제를 짚는 선택자가 없다. */
+ .rail-fill.is-now { background: var(--accent); }
+
+ .rail-label {
+ align-self: start;
+ /* 두 줄까지. 칸의 아래끝 2.2rem 이 정확히 두 줄 높이라, 넘치면 옆 칸을
+ 침범한다. 지금 글의 제일 긴 제목이 30자이고 1280px 에서 한 줄이 26자쯤이라
+ 두 줄에 들어온다.
+ ponytail: 여기 걸리는 제목이 잦아지면 breakpoint 를 84rem 으로 올려 이름
+ 열을 넓히는 쪽이 먼저다. 글자 크기를 줄이는 건 그다음. */
+ display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2;
+ overflow: hidden;
+ font-size: 0.72rem; line-height: 1.35;
+ color: var(--ink-3);
+ opacity: 0; pointer-events: none;
+ transition: opacity var(--tr-ink), color var(--tr-ink);
+ }
+ .rail-label[aria-current] { color: var(--ink); }
+
+ /* 드러나는 것은 숨어 있던 것이지 자리를 옮긴 것이 아니다. 이름은 제자리에서
+ 밝아지고 눈금은 색이 한 단 오른다 — 굵어지지도, 밀려나지도 않는다. 박자는
+ 이 시트의 유일한 박자 하나다. */
+ .rail:hover .rail-label,
+ .rail:focus-within .rail-label { opacity: 1; pointer-events: auto; }
+ .rail:hover .rail-seg,
+ .rail:focus-within .rail-seg { background: var(--rule-3); }
+
+ /* 지금 절 위에 손이 올라가도 현재 표시를 지우지 않는다. hover 와 현재를 같게
+ 칠하면 목차에 커서를 얹는 것만으로 위치 표시가 사라진다. */
+ .rail-label:not([aria-current]):hover { color: var(--accent); }
+
+ .has-rail .toc { display: none; }
+}
+
.pager {
display: flex; gap: 1.5rem;
margin-top: clamp(3.5rem, 8vw, 5.5rem); padding-top: 1.75rem;
diff --git a/static/fonts/display-kr.woff2 b/static/fonts/display-kr.woff2
Binary files differ.
diff --git a/static/js/ui.js b/static/js/ui.js
@@ -78,4 +78,179 @@
pre.appendChild(btn);
});
}
+
+ /* ---------- reading rail ---------- */
+
+ /* 넓은 화면에서 오른쪽 여백에 서는 목차. 항목마다 세로 헤어라인 한 칸이고,
+ 칸의 높이가 그 절이 글에서 차지하는 몫이다. 항목이 다 같은 크기인 목록은
+ "가운데 절이 이 글의 절반"이라는 사실을 말하지 못한다.
+
+ 자료는 본문에 이미 있는 .toc 다. 이 파일이 못 오면 그 목록이 그대로 남는다 —
+ 목록을 감추는 CSS 가 여기서 붙이는 .has-rail 뒤에 있다.
+
+ 넓은 화면인지는 여기서 묻지 않는다. 그 판단은 main.css 의 min-width 하나가
+ 갖고 있다. 두 곳에 두면 두 값이 조용히 어긋나고, 그때 나오는 증상은 "레일이
+ 보이는데 안 찬다"라 숫자 문제로 보이지 않는다. 숨어 있을 때 여기가 하는 일은
+ 짧은 배열 하나에 대한 산수뿐이라 값이 없다.
+
+ 이른 반환이 여러 번 필요해서 안쪽 IIFE 다. 바깥 IIFE 에서 return 하면 나중에
+ 이 아래에 절을 하나 더 붙이는 사람이 조용히 잃는다. */
+ (function () {
+ /* 읽는 줄. 마스트헤드에 가리지 않은 높이의 위에서 30%.
+ ponytail: 절이 이 거리보다 짧으면 그 절의 이름을 눌렀을 때 다음 절이
+ 현재가 된다. 짧은 절이 많은 글이 생기면 0.2 쪽으로 내린다. */
+ var LINE = 0.3;
+ /* 이만큼도 못 구르는 문서에는 진행 표시를 그리지 않는다. 못 움직이는 진행
+ 표시는 진행을 말하지 않는다. */
+ var FLOOR = 240;
+
+ var toc = document.querySelector(".toc");
+ var prose = document.querySelector(".prose");
+ if (!toc || !prose) return;
+ if (root.scrollHeight - window.innerHeight < FLOOR) return;
+
+ var host = document.createElement("nav");
+ host.className = "rail";
+ /* 이름은 목록이 이미 갖고 있다. JS 안에 문자열을 두면 번역도 안 되고
+ tools/fonts.py 의 눈에도 안 띈다. */
+ host.setAttribute("aria-label", toc.getAttribute("aria-label") || "");
+
+ /* 목차의 링크를 전부 쓴다. 깊이별로 고르지 않는다 — 레일이 그리는 것은
+ 개요가 아니라 분량이고, 분량에는 깊이가 없다. 구조는 좁은 화면의 목록이
+ 들여쓰기로 이미 말한다. */
+ var rows = [];
+ toc.querySelectorAll("a[href^='#']").forEach(function (a) {
+ var href = a.getAttribute("href");
+ /* 한국어 제목은 id 가 한글이다(#택하지-않은-우회로). "#"+id 를
+ querySelector 에 넘기면 CSS 선택자로 파싱돼 던진다. 소스에 적힌 값을
+ 그대로 쓰므로 디코딩도 필요 없다 — decodeURIComponent 는 제목에 % 가
+ 들어간 날 오히려 던진다. */
+ var head = document.getElementById(href.slice(1));
+ if (!head) return;
+
+ var seg = document.createElement("span");
+ seg.className = "rail-seg";
+ var fill = document.createElement("i");
+ fill.className = "rail-fill";
+ seg.appendChild(fill);
+
+ var label = document.createElement("a");
+ label.className = "rail-label";
+ label.setAttribute("href", href);
+ label.textContent = a.textContent;
+
+ /* 격자가 두 열이라 넣는 순서대로 눈금·이름이 한 행이 된다. 행 번호를 셀
+ 필요가 없고, 그래서 이름의 세로 위치가 곧 그 절의 위치다. */
+ host.appendChild(seg);
+ host.appendChild(label);
+ rows.push({ fill: fill, label: label, head: head, f: -1 });
+ });
+ /* 항목이 하나뿐인 목차는 목차가 아니라 링크 하나다. */
+ if (rows.length < 2) return;
+
+ /* 목차 바로 뒤에 꽂는다. body 끝에 붙이면 화면에서는 본문 옆이지만 탭
+ 순서에서는 본문 뒤가 되고, 다 읽은 뒤에야 닿는 목차는 아무도 안 쓴다. */
+ toc.insertAdjacentElement("afterend", host);
+ root.classList.add("has-rail");
+
+ var tops = [];
+ var lead = 0;
+ var at = -1;
+
+ function absTop(el) { return el.getBoundingClientRect().top + window.scrollY; }
+
+ /* 재는 곳은 여기 하나다. 스크롤 중에는 아무것도 재지 않고 산수만 한다. */
+ function layout() {
+ /* 읽는 줄이 마스트헤드에 가리는 만큼을 제목 자신의 scroll-margin-top 에서
+ 가져온다. main.css 가 앵커 점프를 위해 이미 선언해 둔 값이고, 브라우저가
+ 눌러서 뛸 때 쓰는 바로 그 값이다 — 한 군데서 나오므로 "여기 있다"와
+ "여기로 간다"가 어긋날 수 없다. --masthead-h 를 고쳐도 따라온다. */
+ var pad = parseFloat(getComputedStyle(rows[0].head).scrollMarginTop) || 0;
+ lead = pad + (window.innerHeight - pad) * LINE;
+
+ tops = [];
+ var prev = 0;
+ rows.forEach(function (r) {
+ /* 앞 경계보다 위로는 못 간다. 서브픽셀이나 sticky 때문에 뒤 제목이 앞
+ 제목보다 위로 재어지면 높이가 음수인 칸이 생긴다. */
+ prev = Math.max(absTop(r.head), prev);
+ tops.push(prev);
+ });
+ /* 읽기는 본문이 끝나는 데서 끝난다. 페이저와 콜로폰은 글이 아니다. 레일의
+ 좌표계는 첫 제목부터 본문 끝까지다 — 머리말은 목차에 없으므로 레일에도
+ 없다. */
+ tops.push(Math.max(absTop(prose) + prose.offsetHeight, prev + 1));
+
+ /* 칸의 높이가 그 절의 몫이다. 아래끝을 두면 아주 짧은 절도 사라지지 않고
+ 두 줄짜리 이름이 옆 칸을 침범하지도 않는다 — 겹침 풀이를 격자에 맡긴다.
+ ponytail: 절이 열한 개를 넘으면 아래끝의 합이 상자보다 커져 넘친다.
+ 그때 손댈 곳은 2.2rem 과 min(58vh, 30rem) 둘이다. */
+ var total = tops[tops.length - 1] - tops[0];
+ var track = "";
+ for (var i = 0; i < rows.length; i++) {
+ track += " minmax(2.2rem, " +
+ ((tops[i + 1] - tops[i]) / total * 100).toFixed(3) + "fr)";
+ }
+ host.style.gridTemplateRows = track.slice(1);
+ progress();
+ }
+
+ function progress() {
+ if (!tops.length) return;
+ var line = window.scrollY + lead;
+ /* 문서 끝에 닿으면 마지막 절은 끝난 것이다. 읽는 줄은 화면 아래 한 뼘을
+ 영영 못 지나가므로, 이게 없으면 마지막 칸은 절대 안 찬다. */
+ var done = window.scrollY + window.innerHeight >= root.scrollHeight - 2;
+ /* 첫 제목 위에서는 아무것도 현재가 아니다. 아직 시작도 안 한 사람에게
+ "1절에 있다"고 말하면 그 표시는 남은 페이지 내내 못 믿는다. */
+ var now = -1;
+
+ for (var i = 0; i < rows.length; i++) {
+ var f = done ? 1 : (line - tops[i]) / Math.max(1, tops[i + 1] - tops[i]);
+ f = f < 0 ? 0 : f > 1 ? 1 : f;
+ /* 값이 그대로면 손대지 않는다. 스크롤 한 번에 절 수만큼 쓰는 자리다. */
+ if (rows[i].f !== f) {
+ rows[i].fill.style.height = (f * 100).toFixed(2) + "%";
+ rows[i].f = f;
+ }
+ /* 화면 상자가 아니라 줄 하나로 고른다. 제목 여럿이 같이 보일 때 누가
+ 이기는지 따질 일이 없다 — 줄 위에 있는 마지막 절이다. 관찰자를 쓰지
+ 않으므로 부드러운 스크롤과 싸울 일도 없다: 눌러서 뛰는 동안 표시가
+ 지나가는 절들을 훑는 것은, 실제로 그 절들을 지나가고 있어서다. */
+ if (line >= tops[i]) now = i;
+ }
+ if (done) now = rows.length - 1;
+
+ if (now === at) return;
+ if (at >= 0) {
+ rows[at].label.removeAttribute("aria-current");
+ rows[at].fill.classList.remove("is-now");
+ }
+ if (now >= 0) {
+ rows[now].label.setAttribute("aria-current", "true");
+ rows[now].fill.classList.add("is-now");
+ }
+ at = now;
+ }
+
+ var ticking = false;
+ window.addEventListener("scroll", function () {
+ if (ticking) return;
+ ticking = true;
+ requestAnimationFrame(function () { ticking = false; progress(); });
+ }, { passive: true });
+
+ var timer;
+ window.addEventListener("resize", function () {
+ clearTimeout(timer);
+ timer = setTimeout(layout, 120);
+ });
+
+ /* 한글 서브셋은 unicode-range 뒤에 있어서 첫 페인트 뒤에 온다. 오면 본문
+ 높이가 바뀌고, 그 전에 잰 경계는 전부 틀린 값이 된다. */
+ window.addEventListener("load", layout);
+ if ("ResizeObserver" in window) new ResizeObserver(layout).observe(prose);
+
+ layout();
+ })();
})();
diff --git a/templates/about.html b/templates/about.html
@@ -4,6 +4,7 @@
흐름을 끊고, 폭과 비율을 문서마다 다시 정해야 한다. 머리에 두면 자리가
한 곳이고 규격도 하나다. #}
{% extends "base.html" %}
+{% from "partials/lang.html" import lang_switch %}
{% block content %}
<div class="wrap wrap-read">
@@ -19,6 +20,7 @@
{%- if page.description %}
<p class="page-lede">{{ page.description }}</p>
{%- endif %}
+ <div class="page-meta">{{ lang_switch(page) }}</div>
</header>
<div class="fence" aria-hidden="true">+++</div>
diff --git a/templates/base.html b/templates/base.html
@@ -103,9 +103,6 @@
<span class="chip-word">{{ t("search") }}</span>
</button>
{%- endif %}
- {%- for lang in page.translations %}
- <a class="chip chip-lang" href="{{ lang.url }}" hreflang="{{ lang.code }}">{{ lang.name }}</a>
- {%- endfor %}
<a class="chip chip-icon chip-github" href="https://github.com/sqzer-x" aria-label="GitHub" title="GitHub">
<svg viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" focusable="false"><path fill="currentColor" d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
</a>
diff --git a/templates/home.html b/templates/home.html
@@ -7,6 +7,7 @@
카피는 전부 [extra] 에 있고 이 파일은 구조만 안다. #}
{% extends "base.html" %}
{% from "partials/ledger.html" import ledger %}
+{% from "partials/lang.html" import lang_switch %}
{% block content %}
{%- set ko = page.language == "ko" %}
@@ -19,6 +20,11 @@
{%- if page.extra.hero_sub %}
<p class="statement-sub" data-rise="2">{{ page.extra.hero_sub }}</p>
{%- endif %}
+ {#- 홈에도 둔다. 참조 사이트는 홈에서 이걸 빠뜨렸는데, 영어 첫 화면에 도착한
+ 사람에게 한국어로 가는 길이 사이트 전체에 하나도 없다는 뜻이 된다.
+ 문보다 **앞에** 세우는 이유는 히어로가 가운데 정렬이라서다 — 뒤에 붙이면
+ 한 줄만큼이 통째로 접힌 자리 아래로 내려가고, 문까지 같이 밀린다. #}
+ <div class="page-meta" data-rise="3">{{ lang_switch(page) }}</div>
<a class="door" href="{{ home }}about/" data-rise="3">{{ page.extra.hero_door }}</a>
</div>
</section>
diff --git a/templates/page.html b/templates/page.html
@@ -1,5 +1,6 @@
{#- 산문 한 장. About 이 이걸 쓴다. #}
{% extends "base.html" %}
+{% from "partials/lang.html" import lang_switch %}
{% block content %}
<div class="wrap wrap-read">
@@ -8,6 +9,7 @@
{%- if page.description %}
<p class="page-lede">{{ page.description }}</p>
{%- endif %}
+ <div class="page-meta">{{ lang_switch(page) }}</div>
</header>
<div class="fence" aria-hidden="true">+++</div>
diff --git a/templates/partials/lang.html b/templates/partials/lang.html
@@ -0,0 +1,45 @@
+{#- 언어 전환. 크롬이 아니라 본문에 붙는다.
+
+ 마스트헤드에 있을 때는 "이 사이트를 한국어로 본다"는 전역 설정처럼 보였는데,
+ 실제로 하는 일은 **지금 이 문서의 다른 판으로 간다**는 것이다. 자리를 문서
+ 안으로 옮기면 그 뜻이 자리로 드러나고, 크롬에는 세 갈래와 검색만 남는다.
+
+ 라벨이 EN / KO 인 이유: 이 사이트의 크롬은 두 언어에서 영어다. 그리고 이건 두
+ 낱말이 나란히 서서 서로를 설명하는 자리다 — "한국어" 옆의 "EN" 은 무엇을
+ 고르는 것인지 말해 주지만, "English" 옆의 "한국어" 는 한쪽만 읽는 사람에게
+ 한쪽만 읽힌다. 두 글자짜리 코드는 어느 쪽에서도 같게 읽힌다.
+
+ 지금 보고 있는 언어가 **더 밝다**. 참조 사이트는 반대로 두었는데(현재가 제일
+ 어둡다) 그러면 "어디에 있는지"를 알려고 두 회색을 견줘 봐야 한다. 이 집의
+ 규칙은 이미 정해져 있다 — 내비의 현재 항목이 눈금과 함께 밝아진다.
+
+ 번역이 없는 페이지에서는 통째로 안 나온다. page.translations 가 비면 아무것도
+ 그리지 않는다. 없는 판으로 가는 링크나 회색 처리된 라벨을 남기지 않는 이유는,
+ 둘 다 "있는데 지금 못 쓴다"는 뜻이 되기 때문이다. 없는 것은 없다. #}
+
+{% macro lang_cur(page) -%}
+<span class="lang-cur" aria-current="true">{{ page.language }}</span>
+{%- endmacro %}
+
+{#- lang 은 이 요소의 글자가 무슨 말인지를 말하는 속성이다. 여기 글자는 라틴 두
+ 자라 lang="ko" 를 붙이면 스크린리더가 "ko" 를 한국어 음가로 읽는다.
+ 목적지의 언어는 hreflang 이 이미 지고 있다. #}
+{% macro lang_alt(l) -%}
+<a href="{{ l.url }}" hreflang="{{ l.code }}" title="{{ l.name }}">{{ l.code }}</a>
+{%- endmacro %}
+
+{#- 순서는 언제나 EN, KO 다. 현재 언어를 앞에 세우면 언어를 바꿀 때마다 두 낱말이
+ 자리를 맞바꿔서, 방금 누른 것이 커서 밑에서 도망간다. #}
+{% macro lang_switch(page) -%}
+{%- if page.translations %}
+<nav class="langs" aria-label="{{ t("language") }}">
+ {%- if page.language == "en" %}
+ {{ lang_cur(page) }}
+ {%- for l in page.translations %}{{ lang_alt(l) }}{% endfor %}
+ {%- else %}
+ {%- for l in page.translations %}{{ lang_alt(l) }}{% endfor %}
+ {{ lang_cur(page) }}
+ {%- endif %}
+</nav>
+{%- endif %}
+{%- endmacro %}
diff --git a/templates/post.html b/templates/post.html
@@ -2,6 +2,7 @@
필요한데, 숫자는 두 언어에서 같게 읽히고 tabular-nums 로 자리가 맞는다. #}
{% extends "base.html" %}
{% from "partials/toc.html" import toc_list %}
+{% from "partials/lang.html" import lang_switch %}
{% block content %}
<article class="wrap wrap-read">
@@ -15,10 +16,17 @@
{%- if page.description %}
<p class="page-lede">{{ page.description }}</p>
{%- endif %}
- {%- if page.date %}
- <p class="page-meta">
+ {#- 날짜와 언어는 같은 줄에 선다. 둘 다 "이 문서가 무엇인가"에 대한 사실이고,
+ 따로 세우면 제목과 본문 사이에 줄이 하나 더 생긴다.
+ 바깥 if 가 필요한 이유: 이 div 안에는 줄바꿈이 있어서 둘 다 없을 때
+ :empty 가 안 걸린다. 목록 템플릿은 한 줄로 붙어 있어 걸린다. #}
+ {%- if page.date or page.translations %}
+ <div class="page-meta">
+ {%- if page.date %}
<time datetime="{{ page.date.date }}">{{ page.date.year }}.{% if page.date.month < 10 %}0{% endif %}{{ page.date.month }}.{% if page.date.day < 10 %}0{% endif %}{{ page.date.day }}</time>
- </p>
+ {%- endif %}
+ {{ lang_switch(page) }}
+ </div>
{%- endif %}
</header>
diff --git a/templates/posts.html b/templates/posts.html
@@ -1,6 +1,7 @@
{#- 글 목록. 섹션이 `sort_by = "date"` 라 최신순이고, 이제 목록 항목이 자기
날짜를 실어 오므로 그 순서의 근거가 화면에 보인다. #}
{% extends "base.html" %}
+{% from "partials/lang.html" import lang_switch %}
{% from "partials/ledger.html" import ledger %}
{% block content %}
@@ -10,6 +11,7 @@
{%- if page.description %}
<p class="page-lede">{{ page.description }}</p>
{%- endif %}
+ <div class="page-meta">{{ lang_switch(page) }}</div>
</header>
<div class="fence" aria-hidden="true">+++</div>
diff --git a/templates/project.html b/templates/project.html
@@ -10,6 +10,7 @@
없는 항목만 숫자가 빠지면 줄이 어긋난다. #}
{% extends "base.html" %}
{% from "partials/icons.html" import i_out, i_arrow %}
+{% from "partials/lang.html" import lang_switch %}
{% block content %}
<div class="{{ "wrap" if page.extra.tier == "landing" else "wrap wrap-read" }} project" data-tier="{{ page.extra.tier }}"
@@ -46,6 +47,8 @@
{%- if page.extra.repo %}<li class="stars">{{ page.extra.stars }} ★</li>{% endif %}
</ul>
+ {{ lang_switch(page) }}
+
{%- if page.extra.actions %}
<div class="btn-row">
{%- for a in page.extra.actions %}
diff --git a/templates/projects.html b/templates/projects.html
@@ -9,6 +9,7 @@
스키마는 총체적이다. 없는 값은 "" 나 [] 이고 키를 빼지 않는다. #}
{% extends "base.html" %}
+{% from "partials/lang.html" import lang_switch %}
{% from "partials/icons.html" import i_site, i_repo %}
{% block content %}
@@ -18,6 +19,7 @@
{%- if page.description %}
<p class="page-lede">{{ page.description }}</p>
{%- endif %}
+ <div class="page-meta">{{ lang_switch(page) }}</div>
</header>
<div class="fence" aria-hidden="true">+++</div>
|