for-humanity 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.9.0 — 2026-10-13
6
+
7
+ - Add code labels: a fence meta of `bad` or `good` with an optional title renders the code block with a colored left stripe and a caption, keeping Shiki highlighting.
8
+ - Give table header rows a soft background and a solid rule, widen the space above sections to 44px and subsections to 28px, and set section headings to 15px so long reference pages read in blocks.
9
+
5
10
  ## 0.8.1 — 2026-10-13
6
11
 
7
12
  - Render tab-indented code blocks at four columns per tab instead of the browser default of eight.
package/README.md CHANGED
@@ -33,7 +33,7 @@ Write Markdown. Keep the context. Share a static site.
33
33
  Node.js 22 or later and pnpm. Install the CLI in your document project:
34
34
 
35
35
  ```sh
36
- pnpm add -D --save-exact for-humanity@0.8.1
36
+ pnpm add -D --save-exact for-humanity@0.9.0
37
37
  mkdir docs
38
38
  ```
39
39
 
@@ -52,7 +52,7 @@ pnpm exec for-humanity preview docs
52
52
 
53
53
  If your Markdown files are in the project root, use `.` instead of `docs`. The root `README.md` becomes the home page; root `AGENTS.md` and `CLAUDE.md` stay out of the site.
54
54
 
55
- Version `0.8.1` supports `dev`, `build` and `preview`. `init` is planned; see the [Roadmap](https://github.com/minu-ha/for-humanity/blob/main/templates/document/roadmap.md) and [Changelog](https://github.com/minu-ha/for-humanity/blob/main/CHANGELOG.md).
55
+ Version `0.9.0` supports `dev`, `build` and `preview`. `init` is planned; see the [Roadmap](https://github.com/minu-ha/for-humanity/blob/main/templates/document/roadmap.md) and [Changelog](https://github.com/minu-ha/for-humanity/blob/main/CHANGELOG.md).
56
56
  Cloudflare Pages can host the generated site. See [Deployment](https://github.com/minu-ha/for-humanity/blob/main/templates/document/deployment.md) for the build settings.
57
57
  Set an optional public root `url` in the site configuration to generate `robots.txt` and `sitemap.xml`; projects without a URL emit neither file. See [Crawling](https://github.com/minu-ha/for-humanity/blob/main/templates/document/crawling.md).
58
58
 
@@ -75,7 +75,7 @@ pnpm preview # Preview the static site
75
75
  pnpm build:pages # Add Cloudflare Pages headers and a 404 page
76
76
  ```
77
77
 
78
- Changes pushed to `main` update the documentation site. A stable version tag such as `v0.8.1` runs checks, publishes the package to npm, then creates a GitHub Release with the same package file.
78
+ Changes pushed to `main` update the documentation site. A stable version tag such as `v0.9.0` runs checks, publishes the package to npm, then creates a GitHub Release with the same package file.
79
79
  The npm README updates with each package release. See [Maintenance](https://github.com/minu-ha/for-humanity/blob/main/templates/document/maintenance.md#packaging-and-release) for the release steps.
80
80
 
81
81
  ## Make it yours
package/dist/cli.css CHANGED
@@ -44,7 +44,7 @@
44
44
  border-top: 0;
45
45
  }
46
46
  & h3 {
47
- margin: var(--app-space-group) 0 var(--app-space-inline);
47
+ margin: var(--app-space-subsection) 0 var(--app-space-inline);
48
48
  color: var(--app-color-text-strong);
49
49
  font-size: var(--app-font-size-lead);
50
50
  font-weight: var(--app-font-weight-strong);
@@ -144,7 +144,8 @@
144
144
  }
145
145
  & th {
146
146
  padding: var(--app-space-inline) var(--app-space-stack);
147
- border-bottom: 1px dotted var(--app-color-border);
147
+ border-bottom: 1px solid var(--app-color-border);
148
+ background: var(--app-color-code);
148
149
  color: var(--app-color-text-strong);
149
150
  font-weight: var(--app-font-weight-strong);
150
151
  text-align: left;
@@ -168,6 +169,29 @@
168
169
  border: 0;
169
170
  border-top: 1px dotted var(--app-color-border);
170
171
  }
172
+ & .wg_prose__codeLabel {
173
+ margin: var(--app-space-block) 0;
174
+ border-left: 3px solid var(--app-color-bad);
175
+ }
176
+ & .wg_prose__codeLabel--good {
177
+ border-left-color: var(--app-color-good);
178
+ }
179
+ & .wg_prose__codeLabelTitle {
180
+ padding: var(--app-space-inline) var(--app-space-block);
181
+ background: var(--app-color-bad-soft);
182
+ color: var(--app-color-bad);
183
+ font-size: var(--app-font-size-dense);
184
+ font-weight: var(--app-font-weight-strong);
185
+ }
186
+ & .wg_prose__codeLabel--good .wg_prose__codeLabelTitle {
187
+ background: var(--app-color-good-soft);
188
+ color: var(--app-color-good);
189
+ }
190
+ & .wg_prose__codeLabel pre {
191
+ margin: 0;
192
+ border-left: 0;
193
+ border-radius: 0 var(--app-radius-box) var(--app-radius-box) 0;
194
+ }
171
195
  }
172
196
  .wg_prose__head {
173
197
  margin-bottom: var(--app-space-inline);
@@ -737,6 +761,10 @@
737
761
  --app-color-border-layout: light-dark(#eee, #2b2b2b);
738
762
  --app-color-accent: light-dark(#555, #b5b5b5);
739
763
  --app-color-accent-soft: light-dark(#f5f5f2, #222);
764
+ --app-color-bad: light-dark(#b2412e, #ff9b8a);
765
+ --app-color-bad-soft: light-dark(#fbeeec, #3a2522);
766
+ --app-color-good: light-dark(#2f7a47, #8bd3a3);
767
+ --app-color-good-soft: light-dark(#eaf5ee, #1f3327);
740
768
  --app-color-link: light-dark(#11a, #9db8ff);
741
769
  --app-color-link-visited: light-dark(#51a, #c5a5ed);
742
770
  --app-color-link-hover: light-dark(#44d, #c1d1ff);
@@ -756,7 +784,7 @@
756
784
  --app-font-size-body: 12px;
757
785
  --app-font-size-lead: 12px;
758
786
  --app-font-size-part: 13px;
759
- --app-font-size-section: 14px;
787
+ --app-font-size-section: 15px;
760
788
  --app-font-size-title: 16px;
761
789
  --app-font-weight-mark: 400;
762
790
  --app-font-weight-active: 600;
@@ -775,7 +803,8 @@
775
803
  --app-space-stack: 10px;
776
804
  --app-space-block: 14px;
777
805
  --app-space-group: 20px;
778
- --app-space-section: 32px;
806
+ --app-space-subsection: 28px;
807
+ --app-space-section: 44px;
779
808
  --app-space-tree-row: 0px;
780
809
  --app-space-tree-group: 16px;
781
810
  --app-space-tree-label: 2px;
package/dist/cli.js CHANGED
@@ -1231,8 +1231,33 @@ var remarkBlocks = () => (tree, file) => {
1231
1231
  });
1232
1232
  };
1233
1233
 
1234
- // src/component/widget/prose/_function/remark-flow.ts
1234
+ // src/component/widget/prose/_function/remark-code-labels.ts
1235
1235
  import { visit as visit3 } from "unist-util-visit";
1236
+ var code_label_pattern = /^(bad|good)(?:\s+(.+?))?\s*$/;
1237
+ var inserted_node_count = 3;
1238
+ var escapeHtml = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1239
+ var remarkCodeLabels = () => (tree) => {
1240
+ visit3(tree, "code", (node, index, parent) => {
1241
+ if (typeof node.meta !== "string" || parent === void 0 || index === void 0) {
1242
+ return;
1243
+ }
1244
+ const match = code_label_pattern.exec(node.meta);
1245
+ if (match === null) {
1246
+ return;
1247
+ }
1248
+ const [, tone, title] = match;
1249
+ const caption = title === void 0 ? "" : `<figcaption class="wg_prose__codeLabelTitle">${escapeHtml(title)}</figcaption>`;
1250
+ node.meta = null;
1251
+ parent.children.splice(index, 1, { type: "html", value: `<figure class="wg_prose__codeLabel wg_prose__codeLabel--${tone}">${caption}` }, node, {
1252
+ type: "html",
1253
+ value: "</figure>"
1254
+ });
1255
+ return index + inserted_node_count;
1256
+ });
1257
+ };
1258
+
1259
+ // src/component/widget/prose/_function/remark-flow.ts
1260
+ import { visit as visit4 } from "unist-util-visit";
1236
1261
 
1237
1262
  // src/util/mermaid/render-flow.js
1238
1263
  import { renderMermaidASCII } from "beautiful-mermaid";
@@ -1546,7 +1571,7 @@ var renderFlow = (source) => gridToSvg(renderMermaidASCII(widenCjk(source), ASCI
1546
1571
 
1547
1572
  // src/component/widget/prose/_function/remark-flow.ts
1548
1573
  var remarkFlow = () => (tree, file) => {
1549
- visit3(tree, "code", (node, index, parent) => {
1574
+ visit4(tree, "code", (node, index, parent) => {
1550
1575
  if (node.lang !== "mermaid" || parent === void 0 || index === void 0) {
1551
1576
  return;
1552
1577
  }
@@ -1564,10 +1589,10 @@ var remarkFlow = () => (tree, file) => {
1564
1589
  // src/component/widget/prose/_function/remark-links.ts
1565
1590
  import { existsSync } from "node:fs";
1566
1591
  import { dirname, posix, relative, resolve } from "node:path";
1567
- import { visit as visit4 } from "unist-util-visit";
1592
+ import { visit as visit5 } from "unist-util-visit";
1568
1593
  var remarkLinks = (options) => (tree, file) => {
1569
1594
  const here = dirname(relative(options.root, file.path));
1570
- visit4(tree, (node) => {
1595
+ visit5(tree, (node) => {
1571
1596
  if (node.type !== "link" && node.type !== "definition") {
1572
1597
  return;
1573
1598
  }
@@ -1601,7 +1626,7 @@ var remarkParts = () => (tree) => {
1601
1626
  };
1602
1627
 
1603
1628
  // src/component/widget/prose/_function/remark-status.ts
1604
- import { escape as escapeHtml, escapeRegExp } from "es-toolkit/string";
1629
+ import { escape as escapeHtml2, escapeRegExp } from "es-toolkit/string";
1605
1630
  import { findAndReplace } from "mdast-util-find-and-replace";
1606
1631
 
1607
1632
  // src/component/widget/prose/_constant/status.ts
@@ -1627,7 +1652,7 @@ var remarkStatus = (options) => {
1627
1652
  }
1628
1653
  return {
1629
1654
  type: "html",
1630
- value: `<span class="wg_prose__pill wg_prose__pill--${item.kind}">${escapeHtml(text)}</span>`
1655
+ value: `<span class="wg_prose__pill wg_prose__pill--${item.kind}">${escapeHtml2(text)}</span>`
1631
1656
  };
1632
1657
  }
1633
1658
  ],
@@ -1651,9 +1676,9 @@ var remarkStatus = (options) => {
1651
1676
  };
1652
1677
 
1653
1678
  // src/component/widget/prose/_function/remark-swatch.ts
1654
- import { visit as visit5 } from "unist-util-visit";
1679
+ import { visit as visit6 } from "unist-util-visit";
1655
1680
  var remarkSwatch = () => (tree) => {
1656
- visit5(tree, "inlineCode", (node) => {
1681
+ visit6(tree, "inlineCode", (node) => {
1657
1682
  if (/^#[0-9a-f]{3,8}$/i.test(node.value.trim())) {
1658
1683
  node.data = {
1659
1684
  hProperties: { className: ["wg_prose__color"], style: `--wg-prose-chip-color: ${node.value.trim()}` }
@@ -1663,9 +1688,9 @@ var remarkSwatch = () => (tree) => {
1663
1688
  };
1664
1689
 
1665
1690
  // src/component/widget/prose/_function/remark-unknown-directives.ts
1666
- import { SKIP as SKIP2, visit as visit6 } from "unist-util-visit";
1691
+ import { SKIP as SKIP2, visit as visit7 } from "unist-util-visit";
1667
1692
  var remarkUnknownDirectives = () => (tree, file) => {
1668
- visit6(tree, (node, index, parent) => {
1693
+ visit7(tree, (node, index, parent) => {
1669
1694
  if (node.type === "textDirective" && parent !== void 0 && index !== void 0) {
1670
1695
  parent.children[index] = {
1671
1696
  type: "text",
@@ -1682,7 +1707,7 @@ var remarkUnknownDirectives = () => (tree, file) => {
1682
1707
  // src/entry/cli/_function/create-processor/_rehype-assets.ts
1683
1708
  import { existsSync as existsSync2, realpathSync, statSync } from "node:fs";
1684
1709
  import { dirname as dirname2, extname as extname2, isAbsolute, relative as relative2, resolve as resolve2, sep } from "node:path";
1685
- import { visit as visit7 } from "unist-util-visit";
1710
+ import { visit as visit8 } from "unist-util-visit";
1686
1711
 
1687
1712
  // src/util/file/to-asset-path.ts
1688
1713
  import { createHash } from "node:crypto";
@@ -1695,7 +1720,7 @@ var toAssetPath = (options) => {
1695
1720
 
1696
1721
  // src/entry/cli/_function/create-processor/_rehype-assets.ts
1697
1722
  var rehypeAssets = (options) => (tree, file) => {
1698
- visit7(tree, "element", (node) => {
1723
+ visit8(tree, "element", (node) => {
1699
1724
  const property = node.tagName === "img" ? "src" : "href";
1700
1725
  const url = node.properties[property];
1701
1726
  if (node.tagName !== "img" && node.tagName !== "a" || typeof url !== "string" || /^(?:[a-z][a-z\d+.-]*:|\/|#)/i.test(url)) {
@@ -1735,11 +1760,11 @@ var rehypeAssets = (options) => (tree, file) => {
1735
1760
  // src/entry/cli/_function/create-processor/_rehype-heading-ids.ts
1736
1761
  import Slugger from "github-slugger";
1737
1762
  import { toString as toPlainText3 } from "hast-util-to-string";
1738
- import { visit as visit8 } from "unist-util-visit";
1763
+ import { visit as visit9 } from "unist-util-visit";
1739
1764
  var rehypeHeadingIds = () => (tree, file) => {
1740
1765
  const slugger = new Slugger();
1741
1766
  const headings = [];
1742
- visit8(tree, "element", (node) => {
1767
+ visit9(tree, "element", (node) => {
1743
1768
  const depth = /^h([1-6])$/.exec(node.tagName)?.[1];
1744
1769
  if (depth === void 0) {
1745
1770
  return;
@@ -1765,7 +1790,7 @@ var remarkReport = (options) => (_tree, file) => {
1765
1790
 
1766
1791
  // src/entry/cli/_function/create-processor/create-processor.ts
1767
1792
  var createProcessor = (options) => {
1768
- return unified().use(remarkParse).use(remarkFrontmatter, ["yaml"]).use(remarkGfm).use(remarkSmartypants).use(remarkDirective).use(remarkBlocks).use(remarkParts).use(remarkFlow).use(remarkStatus, { status: options.site.status }).use(remarkSwatch).use(remarkLinks, { root: options.root }).use(remarkUnknownDirectives).use(remarkReport, { root: options.root }).use(remarkRehype, { allowDangerousHtml: true }).use(rehypeShiki, { theme: createCssVariablesTheme({ name: "for-humanity", variablePrefix: "--app-code-" }) }).use(rehypeHead).use(rehypeHeadingIds).use(rehypeSections).use(rehypeTables).use(rehypeRaw).use(rehypeAssets, { root: options.root, files: options.files ?? /* @__PURE__ */ new Map() }).use(rehypeStringify, { allowDangerousHtml: true });
1793
+ return unified().use(remarkParse).use(remarkFrontmatter, ["yaml"]).use(remarkGfm).use(remarkSmartypants).use(remarkDirective).use(remarkBlocks).use(remarkParts).use(remarkFlow).use(remarkCodeLabels).use(remarkStatus, { status: options.site.status }).use(remarkSwatch).use(remarkLinks, { root: options.root }).use(remarkUnknownDirectives).use(remarkReport, { root: options.root }).use(remarkRehype, { allowDangerousHtml: true }).use(rehypeShiki, { theme: createCssVariablesTheme({ name: "for-humanity", variablePrefix: "--app-code-" }) }).use(rehypeHead).use(rehypeHeadingIds).use(rehypeSections).use(rehypeTables).use(rehypeRaw).use(rehypeAssets, { root: options.root, files: options.files ?? /* @__PURE__ */ new Map() }).use(rehypeStringify, { allowDangerousHtml: true });
1769
1794
  };
1770
1795
 
1771
1796
  // src/entry/cli/_function/read-docs.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "for-humanity",
3
- "version": "0.8.1",
3
+ "version": "0.9.0",
4
4
  "description": "Documents written for humans to read: a docs kit with a sidebar, parts, sections and flowcharts, built on Hono JSX",
5
5
  "keywords": [
6
6
  "docs",
@@ -0,0 +1,47 @@
1
+ import type {Root} from "mdast";
2
+ import {visit} from "unist-util-visit";
3
+
4
+ /**
5
+ * 펜스 메타의 라벨 · `bad 제목`, `good 제목` · 제목은 선택
6
+ */
7
+ const code_label_pattern = /^(bad|good)(?:\s+(.+?))?\s*$/;
8
+
9
+ /**
10
+ * figure 여는 조각, 코드, 닫는 조각
11
+ */
12
+ const inserted_node_count = 3;
13
+
14
+ /**
15
+ * 제목을 HTML 문자열에 넣기 위한 escape
16
+ */
17
+ const escapeHtml = (text: string) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
18
+
19
+ /**
20
+ * 코드 펜스 메타 `bad`·`good` → 색 띠와 제목이 붙은 figure · 다른 메타는 그대로 둠
21
+ * 코드 노드는 유지해 Shiki 강조를 받고, figure 조각은 마지막 rehype-raw에서 합쳐짐
22
+ */
23
+ export const remarkCodeLabels = () => (tree: Root) => {
24
+ visit(tree, "code", (node, index, parent) => {
25
+ if (typeof node.meta !== "string" || parent === undefined || index === undefined) {
26
+ return;
27
+ }
28
+
29
+ const match = code_label_pattern.exec(node.meta);
30
+
31
+ if (match === null) {
32
+ return;
33
+ }
34
+
35
+ const [, tone, title] = match;
36
+ const caption = title === undefined ? "" : `<figcaption class="wg_prose__codeLabelTitle">${escapeHtml(title)}</figcaption>`;
37
+
38
+ node.meta = null;
39
+ parent.children.splice(index, 1, {type: "html", value: `<figure class="wg_prose__codeLabel wg_prose__codeLabel--${tone}">${caption}`}, node, {
40
+ type: "html",
41
+ value: "</figure>",
42
+ });
43
+
44
+ // 끼워 넣은 세 노드 다음부터 계속
45
+ return index + inserted_node_count;
46
+ });
47
+ };
@@ -60,7 +60,7 @@
60
60
  }
61
61
 
62
62
  & h3 {
63
- margin: var(--app-space-group) 0 var(--app-space-inline);
63
+ margin: var(--app-space-subsection) 0 var(--app-space-inline);
64
64
  color: var(--app-color-text-strong);
65
65
  font-size: var(--app-font-size-lead);
66
66
  font-weight: var(--app-font-weight-strong);
@@ -185,7 +185,8 @@
185
185
 
186
186
  & th {
187
187
  padding: var(--app-space-inline) var(--app-space-stack);
188
- border-bottom: 1px dotted var(--app-color-border);
188
+ border-bottom: 1px solid var(--app-color-border);
189
+ background: var(--app-color-code);
189
190
  color: var(--app-color-text-strong);
190
191
  font-weight: var(--app-font-weight-strong);
191
192
  text-align: left;
@@ -214,6 +215,35 @@
214
215
  border: 0;
215
216
  border-top: 1px dotted var(--app-color-border);
216
217
  }
218
+
219
+ /* 라벨 코드 · 왼쪽 띠와 제목 · 코드 상자의 여백은 figure 가 맡음 · 토큰은 token.css 의 bad·good */
220
+ & .wg_prose__codeLabel {
221
+ margin: var(--app-space-block) 0;
222
+ border-left: 3px solid var(--app-color-bad);
223
+ }
224
+
225
+ & .wg_prose__codeLabel--good {
226
+ border-left-color: var(--app-color-good);
227
+ }
228
+
229
+ & .wg_prose__codeLabelTitle {
230
+ padding: var(--app-space-inline) var(--app-space-block);
231
+ background: var(--app-color-bad-soft);
232
+ color: var(--app-color-bad);
233
+ font-size: var(--app-font-size-dense);
234
+ font-weight: var(--app-font-weight-strong);
235
+ }
236
+
237
+ & .wg_prose__codeLabel--good .wg_prose__codeLabelTitle {
238
+ background: var(--app-color-good-soft);
239
+ color: var(--app-color-good);
240
+ }
241
+
242
+ & .wg_prose__codeLabel pre {
243
+ margin: 0;
244
+ border-left: 0;
245
+ border-radius: 0 var(--app-radius-box) var(--app-radius-box) 0;
246
+ }
217
247
  /* stylelint-enable selector-disallowed-list */
218
248
  }
219
249
 
@@ -13,6 +13,7 @@ import {rehypeHead} from "@/component/widget/prose/_function/rehype-head";
13
13
  import {rehypeSections} from "@/component/widget/prose/_function/rehype-sections/rehype-sections";
14
14
  import {rehypeTables} from "@/component/widget/prose/_function/rehype-tables";
15
15
  import {remarkBlocks} from "@/component/widget/prose/_function/remark-blocks";
16
+ import {remarkCodeLabels} from "@/component/widget/prose/_function/remark-code-labels";
16
17
  import {remarkFlow} from "@/component/widget/prose/_function/remark-flow";
17
18
  import {remarkLinks} from "@/component/widget/prose/_function/remark-links";
18
19
  import {remarkParts} from "@/component/widget/prose/_function/remark-parts";
@@ -25,7 +26,7 @@ import {remarkReport} from "@/entry/cli/_function/create-processor/_remark-repor
25
26
  import type {SiteConfig} from "@/type/site-config";
26
27
 
27
28
  /**
28
- * Markdown 처리 순서: remark → 코드 강조 → 머리 → 제목 id → 가름 수집 → 표
29
+ * Markdown 처리 순서: remark(부품·흐름도·코드 라벨) → 코드 강조 → 머리 → 제목 id → 가름 수집 → 표
29
30
  * 플러그인 HTML 조각은 마지막 rehype-raw에서 해석
30
31
  * Shiki inline style: token.css의 --app-code-* 참조
31
32
  */
@@ -39,6 +40,7 @@ export const createProcessor = (options: {site: SiteConfig; root: string; files?
39
40
  .use(remarkBlocks)
40
41
  .use(remarkParts)
41
42
  .use(remarkFlow)
43
+ .use(remarkCodeLabels)
42
44
  .use(remarkStatus, {status: options.site.status})
43
45
  .use(remarkSwatch)
44
46
  .use(remarkLinks, {root: options.root})
@@ -73,6 +73,30 @@ test("site port defaults to 4321 and accepts only integers in the TCP range", ()
73
73
  }
74
74
  });
75
75
 
76
+ test("code fences labelled bad or good render a captioned figure and keep highlighting", async () => {
77
+ const root = await mkdtemp(join(tmpdir(), "for-humanity-code-labels-"));
78
+
79
+ try {
80
+ await writeFile(
81
+ join(root, "guide.md"),
82
+ "---\nname: Guide\nlabel: Guide\ngroup: Guide\n---\n\n## Read\n\n```ts bad 리터럴 <폴백>\nconst a = 1;\n```\n\n```ts good\nconst b = 2;\n```\n\n```ts other meta\nconst c = 3;\n```\n",
83
+ );
84
+ const site = siteConfigSchema.parse({});
85
+ const [doc] = await readDocs({root, processor: createProcessor({site, root})});
86
+
87
+ assert.ok(doc);
88
+ assert.match(
89
+ doc.html,
90
+ /<figure class="wg_prose__codeLabel wg_prose__codeLabel--bad"><figcaption class="wg_prose__codeLabelTitle">리터럴 &#x3C;폴백><\/figcaption>\s*<pre class="shiki/,
91
+ );
92
+ assert.match(doc.html, /<figure class="wg_prose__codeLabel wg_prose__codeLabel--good">\s*<pre class="shiki/);
93
+ assert.equal(doc.html.split("<figure").length - 1, 2, "other meta stays a plain code block");
94
+ assert.equal(doc.html.split("</figure>").length - 1, 2);
95
+ } finally {
96
+ await rm(root, {recursive: true, force: true});
97
+ }
98
+ });
99
+
76
100
  test("cursor face is off unless the site turns it on", () => {
77
101
  assert.equal(siteConfigSchema.parse({}).cursorFace, false);
78
102
  assert.equal(siteConfigSchema.parse({cursorFace: true}).cursorFace, true);
@@ -35,6 +35,12 @@
35
35
  --app-color-accent: light-dark(#555, #b5b5b5);
36
36
  --app-color-accent-soft: light-dark(#f5f5f2, #222);
37
37
 
38
+ /* 라벨 코드 · bad·good 띠와 제목 · soft 제목 바탕 */
39
+ --app-color-bad: light-dark(#b2412e, #ff9b8a);
40
+ --app-color-bad-soft: light-dark(#fbeeec, #3a2522);
41
+ --app-color-good: light-dark(#2f7a47, #8bd3a3);
42
+ --app-color-good-soft: light-dark(#eaf5ee, #1f3327);
43
+
38
44
  /* 링크 · 기본·방문·hover */
39
45
  --app-color-link: light-dark(#11a, #9db8ff);
40
46
  --app-color-link-visited: light-dark(#51a, #c5a5ed);
@@ -63,7 +69,7 @@
63
69
  --app-font-size-body: 12px;
64
70
  --app-font-size-lead: 12px;
65
71
  --app-font-size-part: 13px;
66
- --app-font-size-section: 14px;
72
+ --app-font-size-section: 15px;
67
73
  --app-font-size-title: 16px;
68
74
 
69
75
  /* 굵기 · 기본 400 · 현재 위치·제목·강조 600 */
@@ -90,7 +96,8 @@
90
96
  --app-space-stack: 10px;
91
97
  --app-space-block: 14px;
92
98
  --app-space-group: 20px;
93
- --app-space-section: 32px;
99
+ --app-space-subsection: 28px;
100
+ --app-space-section: 44px;
94
101
 
95
102
  /* 탐색 트리 · 항목 위아래 여백·묶음 사이·묶음 라벨 아래·중첩 들여쓰기 */
96
103
  --app-space-tree-row: 0px;