sqzass.git

sqzass

~ A static site generator written in Rust
git clone https://github.com/debuman/sqzass.git
commit b40eb914c5a541524a8126b740dfdb565fa22b2f
parent c24222ce6413ae1d9cbcbad2c80b867d700ac7ec
Author: sqzer <110329244+sqzer-x@users.noreply.github.com>
Date: Sun, 26 Jul 2026 00:45:32 +0900

Icons for GitHub and theme, and fix the table and code-block treatments

GitHub and the theme toggle are inline SVG now. There is a recorded decision in
this repository against glyphs — ◐ and ☾ are missing from enough fallback
stacks to render as a box — but the reason behind it is font coverage, and an
inline SVG does not consult a font. Icon-only controls have no accessible name,
so both carry aria-label and title, and both are square: a control whose width
follows its label breaks the rhythm of the row it sits in.

The theme button shows where you are going, not where you are — a sun on a dark
page, a moon on a light one. A button names its action.

Tables were `display: block` so they could scroll sideways, which costs them
their table layout: every one shrank to its content and used about 40% of the
column, reading as an afterthought rather than a reference. Ours are two or
three columns of short text, so they are ordinary full-width tables again, with
the header rule darkened so the labels separate from the data. A genuinely wide
table can have a wrapper the day one exists.

Code blocks were cut off at the right edge with nothing to say more was there.
I first drew the shadow with the background-attachment trick, and it left a grey
band on blocks that did not overflow at all — the mask was narrower than the
shadow it was meant to cover. The browser already ships something that appears
exactly when content overflows and never otherwise, so the scrollbar is styled
thin and made visible instead. Fewer moving parts, no artefact to tune.

Copy buttons, from the recipe features/highlighting.md tells readers to use.
Documenting a thing our own site does not do is a claim without evidence. They
read the `<code>` element rather than the `<pre>`, because the latter now
contains the button and would copy its own label.

The sidebar keeps its single dividing rule. I tried a surface under it as well
and it read as a card floating in the page rather than a zone beside it — the
layering that sidebar needs comes from the hover and current states on its
items, and a tint across the whole panel buries them.

Diffstat:
Mdocs/i18n/en.toml | 2++
Mdocs/i18n/ko.toml | 2++
Mdocs/static/css/main.css | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Mdocs/static/js/ui.js | 33+++++++++++++++++++++++++++++++++
Mdocs/templates/base.html | 27+++++++++++++++++++++++----
5 files changed, 137 insertion(+), 10 deletion(-)

diff --git a/docs/i18n/en.toml b/docs/i18n/en.toml
@@ -18,3 +18,5 @@ next            = "Next"
 not_found_title = "Page not found"
 not_found_body  = "That page does not exist, or it moved and left no forwarding address. Everything in this documentation is below, and Ctrl+/ searches it."
 overview        = "Overview"
+copy            = "Copy"
+copied          = "Copied"
diff --git a/docs/i18n/ko.toml b/docs/i18n/ko.toml
@@ -18,3 +18,5 @@ next            = "다음"
 not_found_title = "페이지를 찾을 수 없습니다"
 not_found_body  = "그런 페이지가 없거나, 옮겨 가면서 안내를 남기지 않았습니다. 이 문서의 전부가 아래에 있고, Ctrl+/ 로 검색할 수 있습니다."
 overview        = "개요"
+copy            = "복사"
+copied          = "복사됨"
diff --git a/docs/static/css/main.css b/docs/static/css/main.css
@@ -203,6 +203,20 @@ a:hover { text-decoration-thickness: 2px; }
   border-color: var(--ink-3);
 }
 
+/* 아이콘만 있는 컨트롤은 정사각이어야 한다. 글자 폭에 따라 너비가 달라지면
+   같은 줄의 컨트롤들이 서로 다른 리듬을 갖는다. */
+.chip-icon {
+  width: var(--control-h);
+  padding: 0;
+  justify-content: center;
+}
+.chip-icon svg { display: block; }
+
+/* 버튼은 상태가 아니라 동작을 말한다. 다크에서는 해(→ 라이트로), 라이트에서는 달. */
+.theme-toggle .i-moon { display: none; }
+:root[data-theme="light"] .theme-toggle .i-sun { display: none; }
+:root[data-theme="light"] .theme-toggle .i-moon { display: block; }
+
 .search-trigger { min-width: 11rem; justify-content: space-between; }
 .search-trigger kbd {
   font: inherit;
@@ -225,8 +239,11 @@ a:hover { text-decoration-thickness: 2px; }
   text-transform: uppercase;
   margin-bottom: 0.9rem;
 }
-.crumbs a { color: var(--ink-3); text-decoration: none; }
-.crumbs a:hover { color: var(--accent); }
+.crumbs a { color: var(--ink-2); text-decoration: none; }
+.crumbs a:hover { color: var(--accent); text-decoration: underline; }
+/* 마지막 조각은 현재 위치라 링크가 아니다. 다른 조각과 같은 밝기면 어디까지가
+   갈 수 있는 곳인지 읽히지 않는다. */
+.crumbs a:last-child { color: var(--ink); }
 .crumbs a + a::before { content: "/"; margin-right: 0.5rem; color: var(--rule); }
 
 /* ---------- layout ---------- */
@@ -262,6 +279,9 @@ main { min-width: 0; max-width: var(--measure); }
 /* <details>다. 넓은 화면에서는 열린 채 고정이라 summary를 감추고, 좁은 화면에서만
    서랍이 된다. 구역을 나누는 건 오른쪽 경계선 하나 — 배경을 따로 깔지 않아도
    사이드바가 본문과 다른 층이라는 게 읽힌다. */
+/* 구역을 나누는 건 경계선 하나다. 여기에 면과 라운드를 더 얹어 봤더니 사이드바가
+   구역이 아니라 떠 있는 카드로 읽혔다. 층은 항목의 hover와 현재 표시가 만든다 —
+   판 전체에 색을 깔면 그 층이 묻힌다. */
 .sidebar {
   border-right: 1px solid var(--rule);
   padding-right: 1rem;
@@ -451,15 +471,59 @@ h4:hover > .anchor, h5:hover > .anchor, h6:hover > .anchor,
 }
 
 .prose pre.highlight {
+  position: relative;
   font-family: var(--mono);
   font-size: 0.875rem;
   line-height: 1.6;
   background: var(--raised);
   border: 1px solid var(--rule);
-  border-radius: 4px;
+  border-radius: var(--radius);
   padding: 0.9rem 1.1rem;
   margin: 0 0 1.3rem;
   overflow-x: auto;
+
+  /* 넘치는 코드에는 스크롤이 있다는 표시가 필요하다. 잘린 줄만 보이고 아무
+     힌트가 없으면 사람들은 그게 전부인 줄 안다.
+     그림자를 그리는 CSS 기법을 먼저 써 봤는데, 넘치지도 않는 블록에 회색 띠가
+     남았다. 브라우저가 이미 정확히 이 일을 하는 것을 갖고 있다 — 넘칠 때만
+     나타나는 스크롤바. 보이게만 만들면 된다. */
+  scrollbar-width: thin;
+  scrollbar-color: var(--ink-3) transparent;
+}
+.prose pre.highlight::-webkit-scrollbar { height: 6px; }
+.prose pre.highlight::-webkit-scrollbar-thumb {
+  background: var(--ink-3);
+  border-radius: 3px;
+}
+.prose pre.highlight::-webkit-scrollbar-track { background: transparent; }
+
+/* 복사 버튼. 도구 설정 키가 아니라 이 사이트의 CSS와 JS다 — 문서에도 그렇게
+   적어 두었고, 여기가 그 말을 지키는 자리다.
+   포커스로도 닿아야 하므로 hover에만 기대지 않는다. */
+.copy {
+  position: absolute;
+  top: 0.45rem;
+  right: 0.45rem;
+  height: 1.5rem;
+  padding: 0 0.45rem;
+  font: inherit;
+  font-size: 0.7rem;
+  color: var(--ink-3);
+  background: var(--paper);
+  border: 1px solid var(--rule);
+  border-radius: var(--radius);
+  cursor: pointer;
+  opacity: 0;
+  transition: opacity 0.12s ease;
+}
+.prose pre.highlight:hover .copy,
+.copy:focus-visible { opacity: 1; }
+.copy:hover { color: var(--ink); border-color: var(--ink-3); }
+.copy[data-done] { color: var(--accent); border-color: var(--accent); opacity: 1; }
+
+/* 터치 화면에는 hover가 없다. 항상 보이게 둔다. */
+@media (hover: none) {
+  .prose pre.highlight .copy { opacity: 1; }
 }
 .prose pre.highlight code {
   background: none;
@@ -470,20 +534,27 @@ h4:hover > .anchor, h5:hover > .anchor, h6:hover > .anchor,
 
 /* ---------- tables ---------- */
 
+/* `display: block`이었다. 넓은 표를 가로로 굴리려던 것인데, 그러면 표가 내부
+   레이아웃을 잃고 내용 폭으로 쭈그러들어 본문의 40%만 쓰는 모양이 된다.
+   우리 표는 짧은 텍스트 두세 열이라 그냥 감싸면 되고, 진짜 넓은 표가 생기면
+   그때 감싸는 상자를 두는 게 맞다. */
 .prose table {
   border-collapse: collapse;
   width: 100%;
+  max-width: 100%;
   font-size: var(--s-1);
   margin: 0 0 1.3rem;
-  display: block;
-  overflow-x: auto;
 }
 .prose th, .prose td {
   text-align: left;
-  padding: 0.5rem 0.9rem 0.5rem 0;
+  padding: 0.55rem 0.9rem 0.55rem 0;
   border-bottom: 1px solid var(--rule);
   vertical-align: top;
 }
+/* 마지막 열의 오른쪽 여백은 필요 없다 — 표의 오른쪽 끝이 이미 여백이다. */
+.prose th:last-child, .prose td:last-child { padding-right: 0; }
+/* 머리글과 본문 사이는 더 굵게. 어디까지가 라벨인지가 한눈에 보여야 한다. */
+.prose thead th { border-bottom-color: var(--ink-3); }
 .prose th {
   font-family: var(--mono);
   font-size: 0.75rem;
diff --git a/docs/static/js/ui.js b/docs/static/js/ui.js
@@ -55,3 +55,36 @@
   );
   headings.forEach(function (h) { io.observe(h); });
 })();
+
+/* Copy buttons on code blocks.
+ *
+ * The documentation says line numbers and a copy button are the site's own CSS
+ * and JavaScript rather than configuration keys. This is that claim, kept.
+ */
+(function () {
+  var s = document.currentScript || document.querySelector("script[data-copy]");
+  var COPY = (s && s.dataset.copy) || "Copy";
+  var DONE = (s && s.dataset.copied) || "Copied";
+  if (!navigator.clipboard) return;
+
+  document.querySelectorAll(".prose pre.highlight").forEach(function (pre) {
+    var code = pre.querySelector("code");
+    if (!code) return;
+    var b = document.createElement("button");
+    b.type = "button";
+    b.className = "copy";
+    b.textContent = COPY;
+    b.addEventListener("click", function () {
+      // pre.textContent를 쓰면 버튼 자신의 라벨까지 복사된다.
+      navigator.clipboard.writeText(code.textContent).then(function () {
+        b.textContent = DONE;
+        b.dataset.done = "";
+        setTimeout(function () {
+          b.textContent = COPY;
+          delete b.dataset.done;
+        }, 1200);
+      });
+    });
+    pre.appendChild(b);
+  });
+})();
diff --git a/docs/templates/base.html b/docs/templates/base.html
@@ -82,9 +82,27 @@
     {%- for lang in page.translations %}
     <a class="chip" href="{{ lang.url }}" hreflang="{{ lang.code }}">{{ lang.name }}</a>
     {%- endfor %}
-    <a class="chip chip-wide" href="https://github.com/sqzer-x/sqzass">GitHub</a>
-    {#- 글자다. ◐/☾ 같은 기호는 폴백 스택에 없는 경우가 많아 네모로 나온다. #}
-    <button class="chip theme-toggle" type="button">{{ t("theme") }}</button>
+    {#- 아이콘은 인라인 SVG다. 이 레포에 "글리프 말고 텍스트"라는 결정이 있지만,
+        그 근거는 ◐/☾ 같은 문자가 폰트 폴백 스택에 없어 네모로 나온다는 것이었다.
+        SVG는 폰트를 타지 않으므로 그 이유가 적용되지 않는다. 대신 아이콘만 있는
+        컨트롤은 이름이 없으므로 aria-label을 반드시 준다. #}
+    <a class="chip chip-icon chip-wide" href="https://github.com/sqzer-x/sqzass"
+       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>
+    {#- 지금 테마가 아니라 **누르면 갈 곳**을 보여 준다. 다크에서는 해가 뜨고,
+        라이트에서는 달이 뜬다 — 버튼은 상태가 아니라 동작을 말해야 한다. #}
+    <button class="chip chip-icon theme-toggle" type="button"
+            aria-label="{{ t("theme") }}" title="{{ t("theme") }}">
+      <svg class="i-sun" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" focusable="false"
+           fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
+        <circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/>
+      </svg>
+      <svg class="i-moon" viewBox="0 0 24 24" width="16" height="16" aria-hidden="true" focusable="false"
+           fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
+        <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z"/>
+      </svg>
+    </button>
   </div>
 </header>
 
@@ -153,6 +171,7 @@ document.querySelector(".theme-toggle").addEventListener("click", function () {
 });
 </script>
 <script src="{{ asset("js/search.js") }}" defer></script>
-<script src="{{ asset("js/ui.js") }}" defer></script>
+<script src="{{ asset("js/ui.js") }}" defer
+        data-copy="{{ t("copy") }}" data-copied="{{ t("copied") }}"></script>
 </body>
 </html>