for-humanity 0.8.1 → 0.9.1

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,16 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.9.1 — 2026-10-13
6
+
7
+ - Draw tables with an outer border and cell borders, box flowcharts with a border, and separate sections with a solid rule and 64px of space.
8
+ - Tint the whole code block of a `bad` or `good` label instead of relying on the caption, so a label can carry its reason as a code comment.
9
+
10
+ ## 0.9.0 — 2026-10-13
11
+
12
+ - 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.
13
+ - 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.
14
+
5
15
  ## 0.8.1 — 2026-10-13
6
16
 
7
17
  - 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.1
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.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).
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.1` 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
@@ -23,8 +23,8 @@
23
23
  }
24
24
  & h2 {
25
25
  margin: var(--app-space-section) 0 var(--app-space-stack);
26
- padding-top: var(--app-space-block);
27
- border-top: 1px dotted var(--app-color-border);
26
+ padding-top: var(--app-space-group);
27
+ border-top: 1px solid var(--app-color-border);
28
28
  color: var(--app-color-text-strong);
29
29
  font-size: var(--app-font-size-section);
30
30
  font-weight: var(--app-font-weight-strong);
@@ -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);
@@ -138,13 +138,16 @@
138
138
  }
139
139
  & table {
140
140
  width: 100%;
141
+ border: 1px solid var(--app-color-border-soft);
141
142
  border-collapse: collapse;
142
143
  font-size: var(--app-font-size-dense);
143
144
  font-variant-numeric: tabular-nums;
144
145
  }
145
146
  & th {
146
147
  padding: var(--app-space-inline) var(--app-space-stack);
147
- border-bottom: 1px dotted var(--app-color-border);
148
+ border: 1px solid var(--app-color-border-soft);
149
+ border-bottom-color: var(--app-color-border);
150
+ background: var(--app-color-code);
148
151
  color: var(--app-color-text-strong);
149
152
  font-weight: var(--app-font-weight-strong);
150
153
  text-align: left;
@@ -153,13 +156,10 @@
153
156
  }
154
157
  & td {
155
158
  padding: var(--app-space-inline) var(--app-space-stack);
156
- border-bottom: 1px solid var(--app-color-border-soft);
159
+ border: 1px solid var(--app-color-border-soft);
157
160
  text-align: left;
158
161
  vertical-align: top;
159
162
  }
160
- & tbody tr:last-child td {
161
- border-bottom: 0;
162
- }
163
163
  & tbody tr:hover td {
164
164
  background: var(--app-color-accent-soft);
165
165
  }
@@ -168,6 +168,29 @@
168
168
  border: 0;
169
169
  border-top: 1px dotted var(--app-color-border);
170
170
  }
171
+ & .wg_prose__codeLabel {
172
+ margin: var(--app-space-block) 0;
173
+ border-left: 3px solid var(--app-color-bad);
174
+ }
175
+ & .wg_prose__codeLabel--good {
176
+ border-left-color: var(--app-color-good);
177
+ }
178
+ & .wg_prose__codeLabelTitle {
179
+ padding: var(--app-space-inline) var(--app-space-block);
180
+ background: var(--app-color-bad-soft);
181
+ color: var(--app-color-bad);
182
+ font-size: var(--app-font-size-dense);
183
+ font-weight: var(--app-font-weight-strong);
184
+ }
185
+ & .wg_prose__codeLabel--good .wg_prose__codeLabelTitle {
186
+ background: var(--app-color-good-soft);
187
+ color: var(--app-color-good);
188
+ }
189
+ & .wg_prose__codeLabel pre {
190
+ margin: 0;
191
+ border-left: 0;
192
+ border-radius: 0 var(--app-radius-box) var(--app-radius-box) 0;
193
+ }
171
194
  }
172
195
  .wg_prose__head {
173
196
  margin-bottom: var(--app-space-inline);
@@ -248,7 +271,9 @@
248
271
  .wg_prose__flow {
249
272
  overflow-x: auto;
250
273
  margin: var(--app-space-block) 0;
251
- padding: var(--app-space-block) 0;
274
+ padding: var(--app-space-block);
275
+ border: 1px solid var(--app-color-border-soft);
276
+ border-radius: var(--app-radius-box);
252
277
  letter-spacing: 0;
253
278
  & svg {
254
279
  display: block;
@@ -737,6 +762,10 @@
737
762
  --app-color-border-layout: light-dark(#eee, #2b2b2b);
738
763
  --app-color-accent: light-dark(#555, #b5b5b5);
739
764
  --app-color-accent-soft: light-dark(#f5f5f2, #222);
765
+ --app-color-bad: light-dark(#b2412e, #ff9b8a);
766
+ --app-color-bad-soft: light-dark(#fbeeec, #3a2522);
767
+ --app-color-good: light-dark(#2f7a47, #8bd3a3);
768
+ --app-color-good-soft: light-dark(#eaf5ee, #1f3327);
740
769
  --app-color-link: light-dark(#11a, #9db8ff);
741
770
  --app-color-link-visited: light-dark(#51a, #c5a5ed);
742
771
  --app-color-link-hover: light-dark(#44d, #c1d1ff);
@@ -756,7 +785,7 @@
756
785
  --app-font-size-body: 12px;
757
786
  --app-font-size-lead: 12px;
758
787
  --app-font-size-part: 13px;
759
- --app-font-size-section: 14px;
788
+ --app-font-size-section: 15px;
760
789
  --app-font-size-title: 16px;
761
790
  --app-font-weight-mark: 400;
762
791
  --app-font-weight-active: 600;
@@ -775,7 +804,8 @@
775
804
  --app-space-stack: 10px;
776
805
  --app-space-block: 14px;
777
806
  --app-space-group: 20px;
778
- --app-space-section: 32px;
807
+ --app-space-subsection: 28px;
808
+ --app-space-section: 64px;
779
809
  --app-space-tree-row: 0px;
780
810
  --app-space-tree-group: 16px;
781
811
  --app-space-tree-label: 2px;
@@ -827,6 +857,12 @@
827
857
  --app-space-gutter: 16px;
828
858
  }
829
859
  }
860
+ .wg_prose__codeLabel--bad {
861
+ --app-code-background: var(--app-color-bad-soft);
862
+ }
863
+ .wg_prose__codeLabel--good {
864
+ --app-code-background: var(--app-color-good-soft);
865
+ }
830
866
 
831
867
  /* src/component/widget/shell/wg-shell.css */
832
868
  .wg_shell__root {
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.1",
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
+ };
@@ -34,8 +34,8 @@
34
34
 
35
35
  & h2 {
36
36
  margin: var(--app-space-section) 0 var(--app-space-stack);
37
- padding-top: var(--app-space-block);
38
- border-top: 1px dotted var(--app-color-border);
37
+ padding-top: var(--app-space-group);
38
+ border-top: 1px solid var(--app-color-border);
39
39
  color: var(--app-color-text-strong);
40
40
  font-size: var(--app-font-size-section);
41
41
  font-weight: var(--app-font-weight-strong);
@@ -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);
@@ -176,8 +176,10 @@
176
176
  margin-bottom: 0;
177
177
  }
178
178
 
179
+ /* 표 · 코드 상자처럼 바깥 선과 칸 선 · 머리 행은 바탕과 진한 밑선 */
179
180
  & table {
180
181
  width: 100%;
182
+ border: 1px solid var(--app-color-border-soft);
181
183
  border-collapse: collapse;
182
184
  font-size: var(--app-font-size-dense);
183
185
  font-variant-numeric: tabular-nums;
@@ -185,7 +187,9 @@
185
187
 
186
188
  & th {
187
189
  padding: var(--app-space-inline) var(--app-space-stack);
188
- border-bottom: 1px dotted var(--app-color-border);
190
+ border: 1px solid var(--app-color-border-soft);
191
+ border-bottom-color: var(--app-color-border);
192
+ background: var(--app-color-code);
189
193
  color: var(--app-color-text-strong);
190
194
  font-weight: var(--app-font-weight-strong);
191
195
  text-align: left;
@@ -195,15 +199,11 @@
195
199
 
196
200
  & td {
197
201
  padding: var(--app-space-inline) var(--app-space-stack);
198
- border-bottom: 1px solid var(--app-color-border-soft);
202
+ border: 1px solid var(--app-color-border-soft);
199
203
  text-align: left;
200
204
  vertical-align: top;
201
205
  }
202
206
 
203
- & tbody tr:last-child td {
204
- border-bottom: 0;
205
- }
206
-
207
207
  /* 표의 읽는 행 표시 */
208
208
  & tbody tr:hover td {
209
209
  background: var(--app-color-accent-soft);
@@ -214,6 +214,35 @@
214
214
  border: 0;
215
215
  border-top: 1px dotted var(--app-color-border);
216
216
  }
217
+
218
+ /* 라벨 코드 · 왼쪽 띠와 제목 · 코드 상자의 여백은 figure 가 맡음 · 토큰은 token.css 의 bad·good */
219
+ & .wg_prose__codeLabel {
220
+ margin: var(--app-space-block) 0;
221
+ border-left: 3px solid var(--app-color-bad);
222
+ }
223
+
224
+ & .wg_prose__codeLabel--good {
225
+ border-left-color: var(--app-color-good);
226
+ }
227
+
228
+ & .wg_prose__codeLabelTitle {
229
+ padding: var(--app-space-inline) var(--app-space-block);
230
+ background: var(--app-color-bad-soft);
231
+ color: var(--app-color-bad);
232
+ font-size: var(--app-font-size-dense);
233
+ font-weight: var(--app-font-weight-strong);
234
+ }
235
+
236
+ & .wg_prose__codeLabel--good .wg_prose__codeLabelTitle {
237
+ background: var(--app-color-good-soft);
238
+ color: var(--app-color-good);
239
+ }
240
+
241
+ & .wg_prose__codeLabel pre {
242
+ margin: 0;
243
+ border-left: 0;
244
+ border-radius: 0 var(--app-radius-box) var(--app-radius-box) 0;
245
+ }
217
246
  /* stylelint-enable selector-disallowed-list */
218
247
  }
219
248
 
@@ -324,7 +353,9 @@
324
353
  .wg_prose__flow {
325
354
  overflow-x: auto;
326
355
  margin: var(--app-space-block) 0;
327
- padding: var(--app-space-block) 0;
356
+ padding: var(--app-space-block);
357
+ border: 1px solid var(--app-color-border-soft);
358
+ border-radius: var(--app-radius-box);
328
359
 
329
360
  /* 라벨 위치는 자간 0의 격자로 계산 */
330
361
  letter-spacing: 0;
@@ -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: 64px;
94
101
 
95
102
  /* 탐색 트리 · 항목 위아래 여백·묶음 사이·묶음 라벨 아래·중첩 들여쓰기 */
96
103
  --app-space-tree-row: 0px;
@@ -164,3 +171,12 @@
164
171
  --app-space-gutter: 16px;
165
172
  }
166
173
  }
174
+
175
+ /* 라벨 코드 · Shiki 가 pre 에 인라인으로 쓰는 코드 바탕을 figure 색으로 바꿈 */
176
+ .wg_prose__codeLabel--bad {
177
+ --app-code-background: var(--app-color-bad-soft);
178
+ }
179
+
180
+ .wg_prose__codeLabel--good {
181
+ --app-code-background: var(--app-color-good-soft);
182
+ }