for-humanity 0.1.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.
Files changed (177) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/LICENSE +21 -0
  3. package/README.md +118 -0
  4. package/dist/cli.css +962 -0
  5. package/dist/cli.js +1241 -0
  6. package/dist/client.js +5 -0
  7. package/package.json +92 -0
  8. package/src/app.tsx +57 -0
  9. package/src/asset/favicon.svg +5 -0
  10. package/src/asset/font/architects-daughter/LICENSE.txt +93 -0
  11. package/src/asset/font/architects-daughter/architects-daughter-latin-400-normal.woff2 +0 -0
  12. package/src/asset/font/architects-daughter/architects-daughter.css +10 -0
  13. package/src/asset/font/jetbrains-mono/LICENSE +93 -0
  14. package/src/asset/font/jetbrains-mono/jetbrains-mono-latin-wght-normal.woff2 +0 -0
  15. package/src/asset/font/jetbrains-mono/jetbrains-mono.css +11 -0
  16. package/src/asset/font/pretendard/LICENSE.txt +94 -0
  17. package/src/asset/font/pretendard/pretendardvariable-dynamic-subset.css +1008 -0
  18. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.0.woff2 +0 -0
  19. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.1.woff2 +0 -0
  20. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.10.woff2 +0 -0
  21. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.11.woff2 +0 -0
  22. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.12.woff2 +0 -0
  23. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.13.woff2 +0 -0
  24. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.14.woff2 +0 -0
  25. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.15.woff2 +0 -0
  26. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.16.woff2 +0 -0
  27. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.17.woff2 +0 -0
  28. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.18.woff2 +0 -0
  29. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.19.woff2 +0 -0
  30. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.2.woff2 +0 -0
  31. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.20.woff2 +0 -0
  32. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.21.woff2 +0 -0
  33. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.22.woff2 +0 -0
  34. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.23.woff2 +0 -0
  35. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.24.woff2 +0 -0
  36. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.25.woff2 +0 -0
  37. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.26.woff2 +0 -0
  38. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.27.woff2 +0 -0
  39. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.28.woff2 +0 -0
  40. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.29.woff2 +0 -0
  41. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.3.woff2 +0 -0
  42. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.30.woff2 +0 -0
  43. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.31.woff2 +0 -0
  44. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.32.woff2 +0 -0
  45. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.33.woff2 +0 -0
  46. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.34.woff2 +0 -0
  47. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.35.woff2 +0 -0
  48. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.36.woff2 +0 -0
  49. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.37.woff2 +0 -0
  50. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.38.woff2 +0 -0
  51. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.39.woff2 +0 -0
  52. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.4.woff2 +0 -0
  53. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.40.woff2 +0 -0
  54. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.41.woff2 +0 -0
  55. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.42.woff2 +0 -0
  56. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.43.woff2 +0 -0
  57. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.44.woff2 +0 -0
  58. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.45.woff2 +0 -0
  59. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.46.woff2 +0 -0
  60. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.47.woff2 +0 -0
  61. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.48.woff2 +0 -0
  62. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.49.woff2 +0 -0
  63. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.5.woff2 +0 -0
  64. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.50.woff2 +0 -0
  65. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.51.woff2 +0 -0
  66. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.52.woff2 +0 -0
  67. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.53.woff2 +0 -0
  68. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.54.woff2 +0 -0
  69. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.55.woff2 +0 -0
  70. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.56.woff2 +0 -0
  71. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.57.woff2 +0 -0
  72. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.58.woff2 +0 -0
  73. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.59.woff2 +0 -0
  74. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.6.woff2 +0 -0
  75. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.60.woff2 +0 -0
  76. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.61.woff2 +0 -0
  77. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.62.woff2 +0 -0
  78. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.63.woff2 +0 -0
  79. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.64.woff2 +0 -0
  80. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.65.woff2 +0 -0
  81. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.66.woff2 +0 -0
  82. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.67.woff2 +0 -0
  83. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.68.woff2 +0 -0
  84. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.69.woff2 +0 -0
  85. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.7.woff2 +0 -0
  86. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.70.woff2 +0 -0
  87. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.71.woff2 +0 -0
  88. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.72.woff2 +0 -0
  89. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.73.woff2 +0 -0
  90. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.74.woff2 +0 -0
  91. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.75.woff2 +0 -0
  92. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.76.woff2 +0 -0
  93. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.77.woff2 +0 -0
  94. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.78.woff2 +0 -0
  95. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.79.woff2 +0 -0
  96. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.8.woff2 +0 -0
  97. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.80.woff2 +0 -0
  98. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.81.woff2 +0 -0
  99. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.82.woff2 +0 -0
  100. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.83.woff2 +0 -0
  101. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.84.woff2 +0 -0
  102. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.85.woff2 +0 -0
  103. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.86.woff2 +0 -0
  104. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.87.woff2 +0 -0
  105. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.88.woff2 +0 -0
  106. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.89.woff2 +0 -0
  107. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.9.woff2 +0 -0
  108. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.90.woff2 +0 -0
  109. package/src/asset/font/pretendard/woff2-dynamic-subset/PretendardVariable.subset.91.woff2 +0 -0
  110. package/src/cli.ts +162 -0
  111. package/src/client.ts +322 -0
  112. package/src/component/widget/prose/_constant/heading-highlight.ts +4 -0
  113. package/src/component/widget/prose/_constant/section.ts +7 -0
  114. package/src/component/widget/prose/_constant/status.ts +9 -0
  115. package/src/component/widget/prose/_function/has-block-content.ts +30 -0
  116. package/src/component/widget/prose/_function/rehype-head.ts +31 -0
  117. package/src/component/widget/prose/_function/rehype-sections/_to-section-headings.ts +20 -0
  118. package/src/component/widget/prose/_function/rehype-sections/rehype-sections.ts +21 -0
  119. package/src/component/widget/prose/_function/rehype-tables.ts +22 -0
  120. package/src/component/widget/prose/_function/remark-blocks.ts +112 -0
  121. package/src/component/widget/prose/_function/remark-flow.ts +26 -0
  122. package/src/component/widget/prose/_function/remark-links.ts +33 -0
  123. package/src/component/widget/prose/_function/remark-parts.ts +26 -0
  124. package/src/component/widget/prose/_function/remark-status.ts +59 -0
  125. package/src/component/widget/prose/_function/remark-swatch.ts +16 -0
  126. package/src/component/widget/prose/_function/remark-unknown-directives.ts +25 -0
  127. package/src/component/widget/prose/_type/doc-section.ts +9 -0
  128. package/src/component/widget/prose/wg-prose.css +381 -0
  129. package/src/component/widget/prose/wg-prose.tsx +43 -0
  130. package/src/component/widget/shell/_constant/cursor-face.ts +9 -0
  131. package/src/component/widget/shell/_constant/navigation.ts +4 -0
  132. package/src/component/widget/shell/_constant/reading-line.ts +4 -0
  133. package/src/component/widget/shell/_function/bind-mobile-navigation.ts +140 -0
  134. package/src/component/widget/shell/_function/to-toc-groups.ts +38 -0
  135. package/src/component/widget/shell/_type/doc-outline.ts +16 -0
  136. package/src/component/widget/shell/_type/toc-group.ts +39 -0
  137. package/src/component/widget/shell/_wg-shell-nav.css +134 -0
  138. package/src/component/widget/shell/_wg-shell-nav.tsx +75 -0
  139. package/src/component/widget/shell/_wg-shell-toc.css +268 -0
  140. package/src/component/widget/shell/_wg-shell-toc.tsx +57 -0
  141. package/src/component/widget/shell/wg-shell.css +261 -0
  142. package/src/component/widget/shell/wg-shell.tsx +147 -0
  143. package/src/constant/asset.ts +48 -0
  144. package/src/constant/cli.ts +19 -0
  145. package/src/constant/copy.ts +108 -0
  146. package/src/constant/doc.ts +13 -0
  147. package/src/constant/font.ts +39 -0
  148. package/src/constant/locale.ts +9 -0
  149. package/src/constant/site.ts +9 -0
  150. package/src/constant/status.ts +15 -0
  151. package/src/content/create-processor.ts +59 -0
  152. package/src/content/read-docs.test.ts +242 -0
  153. package/src/content/read-docs.ts +75 -0
  154. package/src/content/read-home.ts +32 -0
  155. package/src/content/rehype-heading-ids.ts +34 -0
  156. package/src/content/remark-report.ts +15 -0
  157. package/src/content/to-doc-groups.ts +18 -0
  158. package/src/dev.ts +79 -0
  159. package/src/env.d.ts +6 -0
  160. package/src/page/doc/pg-doc.tsx +35 -0
  161. package/src/page/home/pg-home.tsx +40 -0
  162. package/src/style/base.css +28 -0
  163. package/src/style/token.css +162 -0
  164. package/src/type/doc-content.ts +15 -0
  165. package/src/type/doc-data.ts +33 -0
  166. package/src/type/doc-file-data.ts +21 -0
  167. package/src/type/doc-heading.ts +17 -0
  168. package/src/type/doc.ts +16 -0
  169. package/src/type/site-assets.ts +17 -0
  170. package/src/type/site-config.ts +55 -0
  171. package/src/type/vfile-data.d.ts +10 -0
  172. package/src/util/dom/find-hash-target.ts +18 -0
  173. package/src/util/dom/reveal-hash-target.ts +15 -0
  174. package/src/util/error/to-error-message.ts +10 -0
  175. package/src/util/font/to-font-css.test.ts +41 -0
  176. package/src/util/font/to-font-css.ts +28 -0
  177. package/src/util/mermaid/render-flow.js +321 -0
@@ -0,0 +1,59 @@
1
+ import {escape as escapeHtml, escapeRegExp} from "es-toolkit/string";
2
+ import type {Root} from "mdast";
3
+ import {findAndReplace, type RegExpMatchObject} from "mdast-util-find-and-replace";
4
+ import type {VFile} from "vfile";
5
+ import {status_candidate_pattern, status_date_pattern} from "@/component/widget/prose/_constant/status";
6
+ import {copy_warn_unknown_status} from "@/constant/copy";
7
+ import type {SiteConfig} from "@/type/site-config";
8
+
9
+ /**
10
+ * 설정 문구의 상태 표지 변환 · 괄호 제거 · 제목·링크 제외
11
+ * 미등록 날짜 문구는 파일 경고 · remark-report에서 출력
12
+ */
13
+ export const remarkStatus = (options: {status: SiteConfig["status"]}) => {
14
+ const statuses = options.status.toSorted((a, b) => b.phrase.length - a.phrase.length);
15
+ const phrases = statuses.map((item) => [escapeRegExp(item.phrase), ...(item.date ? [`(?: ${status_date_pattern})?`] : [])].join("")).join("|");
16
+ const pattern = new RegExp(String.raw`\((${phrases})\)|(${phrases})`, "g");
17
+ const ignore = ["link", "linkReference", "heading"];
18
+
19
+ return (tree: Root, file: VFile) => {
20
+ findAndReplace(
21
+ tree,
22
+ [
23
+ pattern,
24
+ (_match: string, inParens: string | undefined, bare: string | undefined) => {
25
+ const text = inParens ?? bare;
26
+ const item = statuses.find((status) => text?.startsWith(status.phrase));
27
+
28
+ // findAndReplace의 false: 원문 유지
29
+ if (text === undefined || item === undefined) {
30
+ return false;
31
+ }
32
+
33
+ return {
34
+ type: "html",
35
+ value: `<span class="wg_prose__pill wg_prose__pill--${item.kind}">${escapeHtml(text)}</span>`,
36
+ };
37
+ },
38
+ ],
39
+ {ignore},
40
+ );
41
+
42
+ findAndReplace(
43
+ tree,
44
+ [
45
+ status_candidate_pattern,
46
+ (match: string, info: RegExpMatchObject) => {
47
+ // 치환된 텍스트에 position 부재 · 위치가 남은 가까운 조상 기준
48
+ file.message(
49
+ `${copy_warn_unknown_status}: ${match}`,
50
+ info.stack.findLast((node) => node.position !== undefined),
51
+ );
52
+
53
+ return false;
54
+ },
55
+ ],
56
+ {ignore},
57
+ );
58
+ };
59
+ };
@@ -0,0 +1,16 @@
1
+ import type {Root} from "mdast";
2
+ import {visit} from "unist-util-visit";
3
+
4
+ /**
5
+ * 인라인 hex 색 값의 칩 표시
6
+ * 문서별 색은 --wg-prose-chip-color로 전달 · 고정 스타일시트 값 불가
7
+ */
8
+ export const remarkSwatch = () => (tree: Root) => {
9
+ visit(tree, "inlineCode", (node) => {
10
+ if (/^#[0-9a-f]{3,8}$/i.test(node.value.trim())) {
11
+ node.data = {
12
+ hProperties: {className: ["wg_prose__color"], style: `--wg-prose-chip-color: ${node.value.trim()}`},
13
+ };
14
+ }
15
+ });
16
+ };
@@ -0,0 +1,25 @@
1
+ import type {Root} from "mdast";
2
+ import {SKIP, visit} from "unist-util-visit";
3
+ import type {VFile} from "vfile";
4
+ import {copy_error_unknown_directive} from "@/constant/copy";
5
+
6
+ /**
7
+ * hName 없는 블록 지시문은 작성 오류
8
+ * textDirective는 원문 복원 · 예:이것 같은 일반 문장의 오인 방지
9
+ */
10
+ export const remarkUnknownDirectives = () => (tree: Root, file: VFile) => {
11
+ visit(tree, (node, index, parent) => {
12
+ if (node.type === "textDirective" && parent !== undefined && index !== undefined) {
13
+ parent.children[index] = {
14
+ type: "text",
15
+ value: String(file.value).slice(node.position?.start.offset, node.position?.end.offset),
16
+ };
17
+
18
+ return SKIP;
19
+ }
20
+
21
+ if ((node.type === "leafDirective" || node.type === "containerDirective") && node.data?.hName === undefined) {
22
+ file.fail(`${copy_error_unknown_directive}: ${node.name}`, node);
23
+ }
24
+ });
25
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * h2·h3의 가름 · 문서 순서
3
+ */
4
+ export interface DocSection {
5
+ /**
6
+ * 이 절에서 시작하는 가름 이름
7
+ */
8
+ part?: string;
9
+ }
@@ -0,0 +1,381 @@
1
+ /*
2
+ * Markdown 본문과 생성 부품의 표현
3
+ * 요소 선택자는 소유 root 안에 한정
4
+ */
5
+ .wg_prose__root {
6
+ /* stylelint-disable selector-disallowed-list -- Markdown 생성 태그 영역 */
7
+ & h1 {
8
+ margin: 0 0 var(--app-space-stack);
9
+ color: var(--app-color-text-strong);
10
+ font-size: var(--app-font-size-title);
11
+ font-weight: var(--app-font-weight-title);
12
+ line-height: var(--app-line-height-title);
13
+ letter-spacing: var(--app-letter-spacing-title);
14
+ text-wrap: balance;
15
+
16
+ /* 문서 탐색 라벨과 같은 글자 위쪽 기준 · 글꼴 크기별 내부 여백 제거 */
17
+ text-box: trim-start cap alphabetic;
18
+ }
19
+
20
+ & p {
21
+ margin: 0 0 var(--app-space-stack);
22
+ }
23
+
24
+ & img {
25
+ max-width: 100%;
26
+ height: auto;
27
+ vertical-align: middle;
28
+ }
29
+
30
+ /* 제목 아래 첫 문단 · 소개 크기 */
31
+ & h1 + p {
32
+ font-size: var(--app-font-size-lead);
33
+ }
34
+
35
+ & h2 {
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);
39
+ color: var(--app-color-text-strong);
40
+ font-size: var(--app-font-size-section);
41
+ font-weight: var(--app-font-weight-strong);
42
+ line-height: var(--app-line-height-heading);
43
+ letter-spacing: var(--app-letter-spacing-heading);
44
+ text-wrap: balance;
45
+ scroll-margin-top: var(--app-space-anchor);
46
+ }
47
+
48
+ /* 머리 아래 첫 절의 중복 구분선 제거 */
49
+ & .wg_prose__head + h2 {
50
+ margin-top: var(--app-space-group);
51
+ padding-top: 0;
52
+ border-top: 0;
53
+ }
54
+
55
+ /* 가름 아래 절의 중복 구분선 제거 */
56
+ & .wg_prose__part + h2 {
57
+ margin-top: var(--app-space-block);
58
+ padding-top: 0;
59
+ border-top: 0;
60
+ }
61
+
62
+ & h3 {
63
+ margin: var(--app-space-group) 0 var(--app-space-inline);
64
+ color: var(--app-color-text-strong);
65
+ font-size: var(--app-font-size-lead);
66
+ font-weight: var(--app-font-weight-strong);
67
+ line-height: var(--app-line-height-tight);
68
+ scroll-margin-top: var(--app-space-anchor);
69
+ }
70
+
71
+ & h4 {
72
+ margin: var(--app-space-group) 0 6px;
73
+ color: var(--app-color-text-strong);
74
+ font-size: var(--app-font-size-body);
75
+ font-weight: var(--app-font-weight-strong);
76
+ }
77
+
78
+ & h5 {
79
+ margin: var(--app-space-block) 0 6px;
80
+ color: var(--app-color-text-muted);
81
+ font-size: var(--app-font-size-dense);
82
+ font-weight: var(--app-font-weight-strong);
83
+ }
84
+
85
+ & a {
86
+ color: var(--app-color-link);
87
+ text-decoration: none;
88
+ text-underline-offset: 2px;
89
+ }
90
+
91
+ & a:visited {
92
+ color: var(--app-color-link-visited);
93
+ }
94
+
95
+ & a:hover {
96
+ color: var(--app-color-link-hover);
97
+ text-decoration: underline;
98
+ }
99
+
100
+ & a:focus-visible {
101
+ border-radius: var(--app-radius-mark);
102
+ outline: var(--app-outline-focus);
103
+ outline-offset: var(--app-outline-focus-offset);
104
+ }
105
+
106
+ & strong {
107
+ color: var(--app-color-text-strong);
108
+ font-weight: var(--app-font-weight-strong);
109
+ }
110
+
111
+ & ul {
112
+ margin: 0 0 var(--app-space-stack);
113
+ padding-left: 20px;
114
+ }
115
+
116
+ & ol {
117
+ margin: 0 0 var(--app-space-stack);
118
+ padding-left: 22px;
119
+ }
120
+
121
+ & li {
122
+ margin-bottom: 4px;
123
+ }
124
+
125
+ & li > ul {
126
+ margin: 4px 0 0;
127
+ }
128
+
129
+ & li > ol {
130
+ margin: 4px 0 0;
131
+ }
132
+
133
+ /* JetBrains Mono 합자 비활성 · >=, != 원문 구분 */
134
+ & code {
135
+ padding: 1px 3px;
136
+ border-radius: var(--app-radius-inline);
137
+ background: var(--app-color-code);
138
+ color: var(--app-color-text-strong);
139
+ font-family: var(--app-font-mono);
140
+ font-size: 1em;
141
+ font-variant-ligatures: none;
142
+ }
143
+
144
+ /* 코드 블록 · Shiki의 --app-code-* 참조 */
145
+ & pre {
146
+ overflow-x: auto;
147
+ margin: var(--app-space-block) 0;
148
+ padding: var(--app-space-stack) var(--app-space-block);
149
+ border: 1px solid var(--app-color-border);
150
+ border-radius: var(--app-radius-box);
151
+ background: var(--app-color-code);
152
+ font-family: var(--app-font-mono);
153
+ font-size: var(--app-font-size-detail);
154
+ font-variant-ligatures: none;
155
+ line-height: var(--app-line-height-code);
156
+ }
157
+
158
+ & pre code {
159
+ padding: 0;
160
+ background: none;
161
+ color: inherit;
162
+ font-size: inherit;
163
+ }
164
+
165
+ /* 인용과 첫머리 안내 */
166
+ & blockquote {
167
+ margin: var(--app-space-block) 0;
168
+ padding: 0 0 0 var(--app-space-block);
169
+ border-left: 1px solid var(--app-color-border);
170
+ }
171
+
172
+ & blockquote > :last-child {
173
+ margin-bottom: 0;
174
+ }
175
+
176
+ & table {
177
+ width: 100%;
178
+ border-collapse: collapse;
179
+ font-size: var(--app-font-size-dense);
180
+ font-variant-numeric: tabular-nums;
181
+ }
182
+
183
+ & th {
184
+ padding: var(--app-space-inline) var(--app-space-stack);
185
+ border-bottom: 1px dotted var(--app-color-border);
186
+ color: var(--app-color-text-strong);
187
+ font-weight: var(--app-font-weight-strong);
188
+ text-align: left;
189
+ vertical-align: bottom;
190
+ white-space: nowrap;
191
+ }
192
+
193
+ & td {
194
+ padding: var(--app-space-inline) var(--app-space-stack);
195
+ border-bottom: 1px solid var(--app-color-border-soft);
196
+ text-align: left;
197
+ vertical-align: top;
198
+ }
199
+
200
+ & tbody tr:last-child td {
201
+ border-bottom: 0;
202
+ }
203
+
204
+ /* 표의 읽는 행 표시 */
205
+ & tbody tr:hover td {
206
+ background: var(--app-color-accent-soft);
207
+ }
208
+
209
+ & hr {
210
+ margin: var(--app-space-section) 0;
211
+ border: 0;
212
+ border-top: 1px dotted var(--app-color-border);
213
+ }
214
+ /* stylelint-enable selector-disallowed-list */
215
+ }
216
+
217
+ /* 공통 문서 머리 · 제목·소개 */
218
+ .wg_prose__head {
219
+ margin-bottom: var(--app-space-inline);
220
+ padding-bottom: var(--app-space-block);
221
+ border-bottom: 1px dotted var(--app-color-border);
222
+
223
+ /* stylelint-disable-next-line selector-disallowed-list -- Markdown 생성 머리 */
224
+ & > :last-child {
225
+ margin-bottom: 0;
226
+ }
227
+
228
+ /* stylelint-disable selector-disallowed-list -- README의 가운데 정렬 HTML 소개 */
229
+ & [align="center"] {
230
+ padding: var(--app-space-group) 0;
231
+ text-align: center;
232
+ }
233
+
234
+ & [align="center"] > h1 {
235
+ margin-block: var(--app-space-group);
236
+ font-size: 24px;
237
+ }
238
+
239
+ & [align="center"] > p {
240
+ margin-bottom: var(--app-space-block);
241
+ }
242
+
243
+ & [align="center"] > img {
244
+ width: 72px;
245
+ image-rendering: pixelated;
246
+ }
247
+ /* stylelint-enable selector-disallowed-list */
248
+ }
249
+
250
+ /* 제목 글자만 강조 · 여러 줄에서도 각 줄을 칠하고 문서 배치는 유지 */
251
+ .wg_prose__headingText {
252
+ box-decoration-break: clone;
253
+
254
+ /* 줄바꿈된 글자 뒤의 바탕은 opacity 레이어로 분리할 수 없어 배경색만 전환 */
255
+ transition: background-color var(--app-motion-duration-highlight-fade) var(--app-motion-easing-out);
256
+ }
257
+
258
+ .wg_prose__headingText--highlighted {
259
+ background-color: var(--app-color-anchor-highlight);
260
+ transition: none;
261
+ }
262
+
263
+ /* ::part 가름의 공통 표현 */
264
+ .wg_prose__part {
265
+ margin-top: var(--app-space-section);
266
+ padding-top: var(--app-space-stack);
267
+ border-top: 1px dotted var(--app-color-border);
268
+ color: var(--app-color-text-strong);
269
+ font-size: var(--app-font-size-part);
270
+ font-weight: var(--app-font-weight-strong);
271
+ line-height: var(--app-line-height-heading);
272
+ letter-spacing: var(--app-letter-spacing-heading);
273
+ }
274
+
275
+ /* 넓은 표의 내부 가로 스크롤 */
276
+ .wg_prose__table {
277
+ overflow-x: auto;
278
+ margin: var(--app-space-block) 0;
279
+ }
280
+
281
+ /* 상태 표지 · 확인 초록, 미확인 amber */
282
+ .wg_prose__pill {
283
+ display: inline-block;
284
+ padding: 0 5px;
285
+ border-radius: var(--app-radius-inline);
286
+ font-size: var(--app-font-size-detail);
287
+ font-weight: var(--app-font-weight-strong);
288
+ line-height: 1.6;
289
+ white-space: nowrap;
290
+ }
291
+
292
+ .wg_prose__pill--verified {
293
+ background: var(--app-color-verified-soft);
294
+ color: var(--app-color-verified);
295
+ }
296
+
297
+ .wg_prose__pill--unverified {
298
+ background: var(--app-color-unverified-soft);
299
+ color: var(--app-color-unverified);
300
+ }
301
+
302
+ /* 인라인 hex 색 칩 · remark-swatch의 CSS 변수 */
303
+ .wg_prose__color {
304
+ /* 색 칩과 값 사이 줄바꿈 방지 */
305
+ white-space: nowrap;
306
+
307
+ &::before {
308
+ display: inline-block;
309
+ width: 0.8em;
310
+ height: 0.8em;
311
+ margin-right: 5px;
312
+ border: 1px solid var(--app-color-border);
313
+ border-radius: var(--app-radius-mark);
314
+ background: var(--wg-prose-chip-color, transparent);
315
+ content: "";
316
+ vertical-align: -0.05em;
317
+ }
318
+ }
319
+
320
+ /* 빌드 시 생성한 격자 SVG 흐름도 */
321
+ .wg_prose__flow {
322
+ overflow-x: auto;
323
+ margin: var(--app-space-block) 0;
324
+ padding: var(--app-space-block) 0;
325
+
326
+ /* stylelint-disable-next-line selector-disallowed-list -- 외부 렌더러 생성 SVG */
327
+ & svg {
328
+ display: block;
329
+ max-width: 100%;
330
+ height: auto;
331
+ margin: 0 auto;
332
+ }
333
+ }
334
+
335
+ /* 보충 설명 · 본문과 분리되는 제목·왼쪽 선 */
336
+ .wg_prose__note {
337
+ margin: var(--app-space-block) 0;
338
+ padding-left: var(--app-space-block);
339
+ border-left: 2px solid var(--app-color-border);
340
+
341
+ & > :last-child {
342
+ margin-bottom: 0;
343
+ }
344
+ }
345
+
346
+ .wg_prose__noteTitle {
347
+ display: block;
348
+ margin-bottom: var(--app-space-inline);
349
+ color: var(--app-color-text-strong);
350
+ font-weight: var(--app-font-weight-strong);
351
+ }
352
+
353
+ /* 구현 상세 · 펼침 상태와 marker는 native Details 소유 */
354
+ .wg_prose__details {
355
+ margin: var(--app-space-block) 0;
356
+ padding-bottom: var(--app-space-stack);
357
+ border-top: 1px dotted var(--app-color-border);
358
+ border-bottom: 1px dotted var(--app-color-border);
359
+
360
+ & > :last-child {
361
+ margin-bottom: 0;
362
+ }
363
+ }
364
+
365
+ .wg_prose__detailsSummary {
366
+ padding: var(--app-space-inline) 0;
367
+ color: var(--app-color-link);
368
+ font-weight: var(--app-font-weight-strong);
369
+ cursor: pointer;
370
+
371
+ &:hover {
372
+ color: var(--app-color-link-hover);
373
+ text-decoration: underline;
374
+ text-underline-offset: 2px;
375
+ }
376
+
377
+ &:focus-visible {
378
+ outline: var(--app-outline-focus);
379
+ outline-offset: var(--app-outline-focus-offset);
380
+ }
381
+ }
@@ -0,0 +1,43 @@
1
+ import clsx from "clsx";
2
+ import "./wg-prose.css";
3
+
4
+ /**
5
+ * Markdown HTML과 본문 부품의 공통 표현 입력
6
+ * remark·rehype 플러그인은 _function 소유
7
+ */
8
+ export interface WgProseProps {
9
+ /**
10
+ * 머리를 포함한 렌더링 완료 HTML
11
+ */
12
+ html?: string;
13
+ /**
14
+ * README가 없을 때의 안내 머리
15
+ */
16
+ head?: {
17
+ /**
18
+ * 문서 h1 제목
19
+ */
20
+ title: string;
21
+ /**
22
+ * 제목 아래 소개 · 선택
23
+ */
24
+ lead?: string;
25
+ };
26
+ }
27
+
28
+ export const WgProse = (props: WgProseProps) => {
29
+ if (props.html !== undefined) {
30
+ return <article className={clsx("wg_prose__root")} dangerouslySetInnerHTML={{__html: props.html}} />;
31
+ }
32
+
33
+ return (
34
+ <article className={clsx("wg_prose__root")}>
35
+ {props.head !== undefined && (
36
+ <header className={clsx("wg_prose__head")}>
37
+ <h1>{props.head.title}</h1>
38
+ {props.head.lead !== undefined && <p>{props.head.lead}</p>}
39
+ </header>
40
+ )}
41
+ </article>
42
+ );
43
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * 얼굴이 클릭 지점과 겹치지 않는 거리 · 화면 가장자리에도 같은 여백 적용
3
+ */
4
+ export const cursor_face_offset_px = 16;
5
+
6
+ /**
7
+ * 같은 탭의 문서 이동에서 커서 위치를 한 번 이어받는 저장소 키
8
+ */
9
+ export const cursor_face_storage_key = "fh-cursor-face";
@@ -0,0 +1,4 @@
1
+ /**
2
+ * 모바일 탐색 전환 · wg-shell.css의 폭 조건과 일치
3
+ */
4
+ export const navigation_mobile_query = "(width < 1024px)";
@@ -0,0 +1,4 @@
1
+ /**
2
+ * 읽는 선의 반올림 여유 · 경계의 제목을 현재 절에 포함
3
+ */
4
+ export const reading_line_slack_px = 1;
@@ -0,0 +1,140 @@
1
+ import {navigation_mobile_query} from "@/component/widget/shell/_constant/navigation";
2
+ import {revealHashTarget} from "@/util/dom/reveal-hash-target";
3
+
4
+ /**
5
+ * 같은 문서 탐색을 데스크톱 사이드바와 모바일 모달 사이에서 이동
6
+ * 닫기·크기 변경에서 스크롤과 포커스 복원 · 초기화 실패 시 기본 목록 유지
7
+ */
8
+ export const bindMobileNavigation = () => {
9
+ const navigation = document.querySelector<HTMLElement>("[data-navigation]");
10
+ const dialog = document.querySelector<HTMLDialogElement>("[data-navigation-drawer]");
11
+ const content = document.querySelector<HTMLElement>("[data-navigation-content]");
12
+ const openButton = document.querySelector<HTMLButtonElement>("[data-navigation-open]");
13
+ const closeButton = document.querySelector<HTMLButtonElement>("[data-navigation-close]");
14
+ const sidebar = navigation?.parentElement;
15
+
16
+ if (!navigation || !dialog || !content || !openButton || !closeButton || !sidebar) {
17
+ return;
18
+ }
19
+
20
+ const mobile = matchMedia(navigation_mobile_query);
21
+
22
+ /**
23
+ * Escape 등 네이티브 닫기에서도 배경 스크롤과 버튼의 상태 복원
24
+ */
25
+ const handleClose = () => {
26
+ // 닫기 직후 다시 연 모달에 이전 close 이벤트가 개입하지 않도록 함
27
+ if (dialog.open) {
28
+ return;
29
+ }
30
+
31
+ document.body.classList.remove("wg_shell__root--drawerOpen");
32
+ openButton.setAttribute("aria-expanded", "false");
33
+ };
34
+
35
+ /**
36
+ * 기본 링크 이동보다 먼저 잠금 해제 · 네이티브 close가 열기 버튼으로 포커스 복원
37
+ */
38
+ const closeNavigation = () => {
39
+ dialog.close();
40
+ handleClose();
41
+ };
42
+
43
+ /**
44
+ * 네이티브 모달로 외부 포커스 차단 · 모바일에서만 열기
45
+ */
46
+ const handleOpen = () => {
47
+ if (!mobile.matches || dialog.open) {
48
+ return;
49
+ }
50
+
51
+ dialog.showModal();
52
+ document.body.classList.add("wg_shell__root--drawerOpen");
53
+ openButton.setAttribute("aria-expanded", "true");
54
+ };
55
+
56
+ /**
57
+ * 대화상자 바깥 배경 클릭만 닫기 · 내부의 빈 공간은 유지
58
+ */
59
+ const handleBackdropClick = (event: MouseEvent) => {
60
+ const bounds = dialog.getBoundingClientRect();
61
+
62
+ if (event.target === dialog && (event.clientX < bounds.left || event.clientX > bounds.right || event.clientY < bounds.top || event.clientY > bounds.bottom)) {
63
+ closeNavigation();
64
+ }
65
+ };
66
+
67
+ /**
68
+ * 링크의 기본 이동은 유지 · 목차는 닫은 뒤 실제 제목에 키보드 위치 전달
69
+ */
70
+ const handleNavigationClick = (event: MouseEvent) => {
71
+ if (
72
+ !dialog.open ||
73
+ event.defaultPrevented ||
74
+ event.button !== 0 ||
75
+ event.metaKey ||
76
+ event.ctrlKey ||
77
+ event.shiftKey ||
78
+ event.altKey ||
79
+ !(event.target instanceof Element)
80
+ ) {
81
+ return;
82
+ }
83
+
84
+ const link = event.target.closest("a[href]");
85
+
86
+ if (!(link instanceof HTMLAnchorElement) || (link.target !== "" && link.target !== "_self") || link.hasAttribute("download")) {
87
+ return;
88
+ }
89
+
90
+ closeNavigation();
91
+
92
+ if (link.origin === location.origin && link.pathname === location.pathname && link.search === location.search && link.hash) {
93
+ const target = revealHashTarget(link.hash);
94
+
95
+ if (target) {
96
+ target.tabIndex = -1;
97
+ target.focus({preventScroll: true});
98
+ }
99
+ }
100
+ };
101
+
102
+ /**
103
+ * 반응형 전환에도 목록 중복 없음 · 열린 모달을 데스크톱으로 가져가기 전에 닫기
104
+ */
105
+ const handleLayoutChange = () => {
106
+ const focused = document.activeElement;
107
+
108
+ if (mobile.matches) {
109
+ openButton.classList.add("wg_shell__menuToggle--visible");
110
+ content.append(navigation);
111
+
112
+ if (focused instanceof HTMLElement && navigation.contains(focused)) {
113
+ openButton.focus({preventScroll: true});
114
+ }
115
+
116
+ return;
117
+ }
118
+
119
+ closeNavigation();
120
+ sidebar.append(navigation);
121
+
122
+ if (focused instanceof HTMLElement && navigation.contains(focused)) {
123
+ focused.focus({preventScroll: true});
124
+ } else if (focused === openButton || (focused instanceof HTMLElement && dialog.contains(focused))) {
125
+ sidebar.querySelector<HTMLAnchorElement>(".wg_shell__brand")?.focus({preventScroll: true});
126
+ }
127
+
128
+ // CSS의 폭 조건이 먼저 버튼을 숨겨 포커스가 사라지지 않도록 복원 후 숨김
129
+ openButton.classList.remove("wg_shell__menuToggle--visible");
130
+ };
131
+
132
+ openButton.addEventListener("click", handleOpen);
133
+ closeButton.addEventListener("click", closeNavigation);
134
+ dialog.addEventListener("close", handleClose);
135
+ dialog.addEventListener("click", handleBackdropClick);
136
+ navigation.addEventListener("click", handleNavigationClick);
137
+ mobile.addEventListener("change", handleLayoutChange);
138
+ handleLayoutChange();
139
+ sidebar.classList.add("wg_shell__sidebar--navigationReady");
140
+ };