for-humanity 0.1.2 → 0.2.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
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.1 — 2026-10-04
4
+
5
+ - Fingerprint stylesheet and browser-script URLs so new pages cannot reuse assets cached from an older deployment.
6
+ - Cache fingerprinted CSS, scripts and fonts with immutable headers on Cloudflare Pages.
7
+
8
+ ## 0.2.0 — 2026-10-04
9
+
10
+ - Support nested navigation with `group` name arrays while keeping single-name groups compatible.
11
+ - Order nested sibling groups with paths in `navigation`, independently of document folders.
12
+ - Extend active, hover and keyboard focus tree paths through ancestor groups.
13
+
3
14
  ## 0.1.2 — 2026-10-03
4
15
 
5
16
  Documentation and release automation update. The CLI commands and Node.js requirement are unchanged.
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.1.2
36
+ pnpm add -D --save-exact for-humanity@0.2.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.1.2` 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.2.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
 
58
58
  ### Develop the kit
@@ -74,7 +74,7 @@ pnpm preview # Preview the static site
74
74
  pnpm build:pages # Add Cloudflare Pages headers and a 404 page
75
75
  ```
76
76
 
77
- Changes pushed to `main` update the documentation site. A stable version tag such as `v0.1.2` runs checks, publishes the package to npm, then creates a GitHub Release with the same package file.
77
+ Changes pushed to `main` update the documentation site. A stable version tag such as `v0.2.1` runs checks, publishes the package to npm, then creates a GitHub Release with the same package file.
78
78
  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.
79
79
 
80
80
  ## Make it yours
@@ -94,6 +94,7 @@ export default {
94
94
 
95
95
  See [Commands](https://github.com/minu-ha/for-humanity/blob/main/templates/document/commands.md) for using another document folder, and [Settings](https://github.com/minu-ha/for-humanity/blob/main/templates/document/settings.md) for configuration.
96
96
  Each document other than the root README chooses a `group` and optional `order`. Pages stay independent; groups do not need parent documents or nested folders. Document names may share the same first letter.
97
+ Use `group: Guide` for one group or `group: [Projects, Example project, Research]` for nested navigation. Group paths do not change document URLs.
97
98
 
98
99
  ## Write with context
99
100
 
package/dist/cli.css CHANGED
@@ -295,22 +295,32 @@
295
295
  }
296
296
  }
297
297
 
298
- /* src/component/widget/shell/_wg-shell-nav.css */
299
- .wg_shellNav__root {
300
- font-size: var(--app-font-size-dense);
301
- line-height: var(--app-line-height-tight);
302
- }
303
- .wg_shellNav__docs {
304
- display: flex;
305
- flex-direction: column;
306
- gap: var(--app-space-stack);
307
- }
308
- .wg_shellNav__list {
298
+ /* src/component/widget/shell/_wg-shell-nav-list.css */
299
+ .wg_shellNavList__root {
309
300
  margin: 0 0 0 var(--app-space-tree-inset);
310
301
  padding: 0;
311
302
  list-style: none;
312
303
  }
313
- .wg_shellNav__item {
304
+ .wg_shellNavList__branch {
305
+ position: relative;
306
+ display: block;
307
+ padding: var(--app-space-tree-row) 0 var(--app-space-tree-row) calc(var(--app-space-tree-indent) + var(--app-space-inline));
308
+ color: var(--app-color-text-muted);
309
+ font-size: var(--app-font-size-label);
310
+ line-height: var(--app-line-height-tight);
311
+ overflow-wrap: anywhere;
312
+ &::before {
313
+ position: absolute;
314
+ top: calc(0.5lh + var(--app-space-tree-row));
315
+ left: 0;
316
+ width: var(--app-space-tree-indent);
317
+ border-top: 1px solid var(--app-color-border-soft);
318
+ content: "";
319
+ pointer-events: none;
320
+ }
321
+ }
322
+ .wg_shellNavList__item {
323
+ display: flow-root;
314
324
  position: relative;
315
325
  &::before {
316
326
  position: absolute;
@@ -340,24 +350,36 @@
340
350
  &:where(:has(:focus-visible))::after {
341
351
  border-left-color: var(--app-color-text-strong);
342
352
  }
343
- &:where(:has(~ .wg_shellNav__item:hover))::before {
353
+ &:where(:has(~ .wg_shellNavList__item:hover))::before {
344
354
  border-left-color: var(--app-color-text-strong);
345
355
  }
346
- &:where(:has(~ .wg_shellNav__item :focus-visible))::before {
356
+ &:where(:has(~ .wg_shellNavList__item :focus-visible))::before {
347
357
  border-left-color: var(--app-color-text-strong);
348
358
  }
359
+ & > .wg_shellNavList__root {
360
+ margin-left: var(--app-space-tree-indent);
361
+ }
362
+ &:where(:hover) > .wg_shellNavList__branch::before {
363
+ border-top-color: var(--app-color-text-strong);
364
+ }
365
+ &:where(:has(:focus-visible)) > .wg_shellNavList__branch::before {
366
+ border-top-color: var(--app-color-text-strong);
367
+ }
349
368
  }
350
- .wg_shellNav__item--trail {
369
+ .wg_shellNavList__item--trail {
351
370
  &::before {
352
371
  border-left-color: var(--app-color-text-strong);
353
372
  }
354
373
  }
355
- .wg_shellNav__item--current {
374
+ .wg_shellNavList__item--current {
356
375
  &::after {
357
376
  border-left-color: var(--app-color-text-strong);
358
377
  }
378
+ & > .wg_shellNavList__branch::before {
379
+ border-top-color: var(--app-color-text-strong);
380
+ }
359
381
  }
360
- .wg_shellNav__link {
382
+ .wg_shellNavList__link {
361
383
  position: relative;
362
384
  display: block;
363
385
  padding: var(--app-space-tree-row) 0 var(--app-space-tree-row) calc(var(--app-space-tree-indent) + var(--app-space-inline));
@@ -365,6 +387,7 @@
365
387
  font-size: var(--app-font-size-dense);
366
388
  line-height: var(--app-line-height-tight);
367
389
  text-decoration: none;
390
+ overflow-wrap: anywhere;
368
391
  &::before {
369
392
  position: absolute;
370
393
  top: calc(0.5lh + var(--app-space-tree-row));
@@ -388,13 +411,24 @@
388
411
  border-top-color: currentcolor;
389
412
  }
390
413
  }
391
- .wg_shellNav__link--active {
414
+ .wg_shellNavList__link--active {
392
415
  color: var(--app-color-text-strong);
393
416
  font-weight: var(--app-font-weight-active);
394
417
  &::before {
395
418
  border-top-color: currentcolor;
396
419
  }
397
420
  }
421
+
422
+ /* src/component/widget/shell/_wg-shell-nav.css */
423
+ .wg_shellNav__root {
424
+ font-size: var(--app-font-size-dense);
425
+ line-height: var(--app-line-height-tight);
426
+ }
427
+ .wg_shellNav__docs {
428
+ display: flex;
429
+ flex-direction: column;
430
+ gap: var(--app-space-stack);
431
+ }
398
432
  .wg_shellNav__group {
399
433
  margin: 0 0 4px;
400
434
  color: var(--app-color-text-muted);
package/dist/cli.js CHANGED
@@ -30,7 +30,7 @@ var WgProse = (props) => {
30
30
  };
31
31
 
32
32
  // src/component/widget/shell/wg-shell.tsx
33
- import clsx4 from "clsx";
33
+ import clsx5 from "clsx";
34
34
 
35
35
  // src/component/widget/shell/_function/to-toc-groups.ts
36
36
  import { takeWhile } from "es-toolkit/array";
@@ -66,7 +66,50 @@ var toTocGroups = (outline) => {
66
66
  };
67
67
 
68
68
  // src/component/widget/shell/_wg-shell-nav.tsx
69
+ import clsx3 from "clsx";
70
+
71
+ // src/component/widget/shell/_wg-shell-nav-list.tsx
69
72
  import clsx2 from "clsx";
73
+ import { Fragment } from "react";
74
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
75
+ var WgShellNavList = (props) => {
76
+ const entries = [
77
+ ...props.group.docs.map((doc) => ({ kind: "document", key: `doc:${doc.id}`, doc, current: doc.id === props.currentDoc?.id })),
78
+ ...props.group.groups.map((group) => ({
79
+ kind: "group",
80
+ key: `group:${JSON.stringify(group.path)}`,
81
+ group,
82
+ current: group.path.every((name, index) => name === props.currentDoc?.data.group[index])
83
+ }))
84
+ ];
85
+ const currentIndex = entries.findIndex((entry) => entry.current);
86
+ return /* @__PURE__ */ jsx2("ul", { className: clsx2("wg_shellNavList__root"), "aria-label": props.group.name, children: entries.map((entry, index) => /* @__PURE__ */ jsxs2(
87
+ "li",
88
+ {
89
+ className: clsx2("wg_shellNavList__item", {
90
+ "wg_shellNavList__item--trail": index < currentIndex,
91
+ "wg_shellNavList__item--current": entry.current
92
+ }),
93
+ children: [
94
+ entry.kind === "document" && /* @__PURE__ */ jsx2(
95
+ "a",
96
+ {
97
+ className: clsx2("wg_shellNavList__link", { "wg_shellNavList__link--active": entry.current }),
98
+ href: `/${entry.doc.id}/`,
99
+ title: entry.doc.data.label,
100
+ "aria-current": entry.current ? "page" : void 0,
101
+ children: entry.doc.data.name
102
+ }
103
+ ),
104
+ entry.kind === "group" && /* @__PURE__ */ jsxs2(Fragment, { children: [
105
+ /* @__PURE__ */ jsx2("span", { className: clsx2("wg_shellNavList__branch"), children: entry.group.name }),
106
+ /* @__PURE__ */ jsx2(WgShellNavList, { group: entry.group, currentDoc: props.currentDoc })
107
+ ] })
108
+ ]
109
+ },
110
+ entry.key
111
+ )) });
112
+ };
70
113
 
71
114
  // src/constant/copy.ts
72
115
  var copy_site_title_default = "for humanity";
@@ -95,61 +138,60 @@ var copy_warn_unknown_status = "\uC124\uC815\uC5D0 \uC5C6\uB294 \uC0C1\uD0DC \uB
95
138
  var locale_doc_name = "en";
96
139
  var locale_doc_group = "ko";
97
140
 
98
- // src/content/to-doc-groups.ts
141
+ // src/content/to-doc-groups/_to-doc-group-branches.ts
142
+ var toDocGroupBranches = (options) => {
143
+ return [
144
+ ...Map.groupBy(
145
+ options.docs.filter((doc) => doc.data.group.length > options.path.length),
146
+ (doc) => doc.data.group[options.path.length]
147
+ )
148
+ ].map(([name, docs]) => {
149
+ const path = [...options.path, name];
150
+ return {
151
+ name,
152
+ path,
153
+ docs,
154
+ position: options.positions.get(JSON.stringify(path)) ?? options.unlistedPosition
155
+ };
156
+ }).toSorted((a, b) => a.position - b.position || a.name.localeCompare(b.name, locale_doc_group)).map((group) => {
157
+ return {
158
+ name: group.name,
159
+ path: group.path,
160
+ docs: group.docs.filter((doc) => doc.data.group.length === group.path.length).toSorted((a, b) => a.data.order - b.data.order || a.data.name.localeCompare(b.data.name, locale_doc_name) || a.id.localeCompare(b.id, locale_doc_name)),
161
+ groups: toDocGroupBranches({ docs: group.docs, path: group.path, positions: options.positions, unlistedPosition: options.unlistedPosition })
162
+ };
163
+ });
164
+ };
165
+
166
+ // src/content/to-doc-groups/to-doc-groups.ts
99
167
  var toDocGroups = (options) => {
100
- const positions = new Map(options.navigation.map((name, index) => [name, index]));
101
- return [...Map.groupBy(options.docs, (doc) => doc.data.group)].toSorted((a, b) => (positions.get(a[0]) ?? options.navigation.length) - (positions.get(b[0]) ?? options.navigation.length) || a[0].localeCompare(b[0], locale_doc_group)).map(([name, docs]) => ({
102
- name,
103
- docs: docs.toSorted((a, b) => a.data.order - b.data.order || a.data.name.localeCompare(b.data.name, locale_doc_name) || a.id.localeCompare(b.id, locale_doc_name))
104
- }));
168
+ const positions = new Map(options.navigation.flatMap((path, index) => path.map((_name, depth) => [JSON.stringify(path.slice(0, depth + 1)), index])).toReversed());
169
+ return toDocGroupBranches({ docs: options.docs, path: [], positions, unlistedPosition: options.navigation.length });
105
170
  };
106
171
 
107
172
  // src/component/widget/shell/_wg-shell-nav.tsx
108
- import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
173
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
109
174
  var WgShellNav = (props) => {
110
175
  const docGroups = toDocGroups({ docs: props.docs, navigation: props.site.navigation });
111
- return /* @__PURE__ */ jsx2("nav", { className: clsx2("wg_shellNav__root"), "aria-label": copy_nav_aria_label, children: /* @__PURE__ */ jsx2("div", { className: clsx2("wg_shellNav__docs"), children: docGroups.map((group) => {
112
- const currentIndex = group.docs.findIndex((doc) => doc.id === props.current);
113
- return /* @__PURE__ */ jsxs2("section", { className: clsx2("wg_shellNav__docGroup"), "aria-label": group.name, children: [
114
- /* @__PURE__ */ jsx2("div", { className: clsx2("wg_shellNav__group"), children: group.name }),
115
- /* @__PURE__ */ jsx2("ul", { className: clsx2("wg_shellNav__list"), children: group.docs.map((doc, index) => /* @__PURE__ */ jsx2(
116
- "li",
117
- {
118
- className: clsx2("wg_shellNav__item", {
119
- "wg_shellNav__item--trail": index < currentIndex,
120
- "wg_shellNav__item--current": index === currentIndex
121
- }),
122
- children: /* @__PURE__ */ jsx2(
123
- "a",
124
- {
125
- className: clsx2("wg_shellNav__link", {
126
- "wg_shellNav__link--active": index === currentIndex
127
- }),
128
- href: `/${doc.id}/`,
129
- title: doc.data.label,
130
- "aria-current": index === currentIndex ? "page" : void 0,
131
- children: doc.data.name
132
- }
133
- )
134
- },
135
- doc.id
136
- )) })
137
- ] }, group.name);
138
- }) }) });
176
+ const currentDoc = props.docs.find((doc) => doc.id === props.current);
177
+ return /* @__PURE__ */ jsx3("nav", { className: clsx3("wg_shellNav__root"), "aria-label": copy_nav_aria_label, children: /* @__PURE__ */ jsx3("div", { className: clsx3("wg_shellNav__docs"), children: docGroups.map((group) => /* @__PURE__ */ jsxs3("section", { className: clsx3("wg_shellNav__docGroup"), "aria-label": group.name, children: [
178
+ /* @__PURE__ */ jsx3("div", { className: clsx3("wg_shellNav__group"), children: group.name }),
179
+ /* @__PURE__ */ jsx3(WgShellNavList, { group, currentDoc })
180
+ ] }, group.name)) }) });
139
181
  };
140
182
 
141
183
  // src/component/widget/shell/_wg-shell-toc.tsx
142
- import clsx3 from "clsx";
143
- import { Fragment } from "react";
144
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
184
+ import clsx4 from "clsx";
185
+ import { Fragment as Fragment2 } from "react";
186
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
145
187
  var WgShellToc = (props) => {
146
- return /* @__PURE__ */ jsxs3("nav", { className: clsx3("wg_shellToc__root"), "aria-label": copy_nav_toc_label, children: [
147
- /* @__PURE__ */ jsx3("div", { className: clsx3("wg_shellToc__label"), children: copy_nav_toc_label }),
148
- props.groups.map((group) => /* @__PURE__ */ jsxs3(Fragment, { children: [
149
- group.part !== void 0 && /* @__PURE__ */ jsx3("div", { className: clsx3("wg_shellToc__group"), children: group.part }),
150
- /* @__PURE__ */ jsx3("ul", { className: clsx3("wg_shellToc__list"), children: group.sections.map((section) => /* @__PURE__ */ jsxs3("li", { className: clsx3("wg_shellToc__item"), children: [
151
- /* @__PURE__ */ jsx3("a", { className: clsx3("wg_shellToc__link"), href: `#${section.heading.slug}`, "data-toc-link": "", children: section.heading.text }),
152
- section.subs.length > 0 && /* @__PURE__ */ jsx3("ul", { className: clsx3("wg_shellToc__sub"), children: section.subs.map((sub) => /* @__PURE__ */ jsx3("li", { className: clsx3("wg_shellToc__subItem"), children: /* @__PURE__ */ jsx3("a", { className: clsx3("wg_shellToc__subLink"), href: `#${sub.heading.slug}`, "data-toc-sub-link": "", children: sub.heading.text }) }, sub.heading.slug)) })
188
+ return /* @__PURE__ */ jsxs4("nav", { className: clsx4("wg_shellToc__root"), "aria-label": copy_nav_toc_label, children: [
189
+ /* @__PURE__ */ jsx4("div", { className: clsx4("wg_shellToc__label"), children: copy_nav_toc_label }),
190
+ props.groups.map((group) => /* @__PURE__ */ jsxs4(Fragment2, { children: [
191
+ group.part !== void 0 && /* @__PURE__ */ jsx4("div", { className: clsx4("wg_shellToc__group"), children: group.part }),
192
+ /* @__PURE__ */ jsx4("ul", { className: clsx4("wg_shellToc__list"), children: group.sections.map((section) => /* @__PURE__ */ jsxs4("li", { className: clsx4("wg_shellToc__item"), children: [
193
+ /* @__PURE__ */ jsx4("a", { className: clsx4("wg_shellToc__link"), href: `#${section.heading.slug}`, "data-toc-link": "", children: section.heading.text }),
194
+ section.subs.length > 0 && /* @__PURE__ */ jsx4("ul", { className: clsx4("wg_shellToc__sub"), children: section.subs.map((sub) => /* @__PURE__ */ jsx4("li", { className: clsx4("wg_shellToc__subItem"), children: /* @__PURE__ */ jsx4("a", { className: clsx4("wg_shellToc__subLink"), href: `#${sub.heading.slug}`, "data-toc-sub-link": "", children: sub.heading.text }) }, sub.heading.slug)) })
153
195
  ] }, section.heading.slug)) })
154
196
  ] }, group.sections[0].heading.slug))
155
197
  ] });
@@ -171,85 +213,85 @@ var asset_content_types = {
171
213
  var asset_content_type_default = "application/octet-stream";
172
214
 
173
215
  // src/component/widget/shell/wg-shell.tsx
174
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
216
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
175
217
  var reloadScript = `new EventSource(${JSON.stringify(asset_reload_path)}).onmessage=function(){location.reload()}`;
176
218
  var WgShell = (props) => {
177
219
  const tocGroups = props.outline === void 0 ? [] : toTocGroups(props.outline);
178
- return /* @__PURE__ */ jsxs4("html", { lang: "ko", children: [
179
- /* @__PURE__ */ jsxs4("head", { children: [
180
- /* @__PURE__ */ jsx4("meta", { charSet: "utf-8" }),
181
- /* @__PURE__ */ jsx4("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
182
- props.site.description !== void 0 && /* @__PURE__ */ jsx4("meta", { name: "description", content: props.site.description }),
183
- /* @__PURE__ */ jsx4("title", { children: props.title === void 0 ? props.site.title : `${props.title} \xB7 ${props.site.title}` }),
184
- /* @__PURE__ */ jsx4("link", { rel: "icon", href: asset_favicon_path, type: "image/svg+xml" }),
185
- /* @__PURE__ */ jsx4("style", { dangerouslySetInnerHTML: { __html: props.assets.fontCss } }),
186
- props.assets.preload.map((href) => /* @__PURE__ */ jsx4("link", { rel: "preload", href, as: "font", type: "font/woff2", crossOrigin: "" }, href)),
187
- /* @__PURE__ */ jsx4("script", { dangerouslySetInnerHTML: { __html: 'document.documentElement.style.scrollBehavior="auto"' } }),
188
- /* @__PURE__ */ jsx4("link", { rel: "stylesheet", href: asset_style_path }),
189
- props.assets.reload && /* @__PURE__ */ jsx4("script", { dangerouslySetInnerHTML: { __html: reloadScript } })
220
+ return /* @__PURE__ */ jsxs5("html", { lang: "ko", children: [
221
+ /* @__PURE__ */ jsxs5("head", { children: [
222
+ /* @__PURE__ */ jsx5("meta", { charSet: "utf-8" }),
223
+ /* @__PURE__ */ jsx5("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }),
224
+ props.site.description !== void 0 && /* @__PURE__ */ jsx5("meta", { name: "description", content: props.site.description }),
225
+ /* @__PURE__ */ jsx5("title", { children: props.title === void 0 ? props.site.title : `${props.title} \xB7 ${props.site.title}` }),
226
+ /* @__PURE__ */ jsx5("link", { rel: "icon", href: asset_favicon_path, type: "image/svg+xml" }),
227
+ /* @__PURE__ */ jsx5("style", { dangerouslySetInnerHTML: { __html: props.assets.fontCss } }),
228
+ props.assets.preload.map((href) => /* @__PURE__ */ jsx5("link", { rel: "preload", href, as: "font", type: "font/woff2", crossOrigin: "" }, href)),
229
+ /* @__PURE__ */ jsx5("script", { dangerouslySetInnerHTML: { __html: 'document.documentElement.style.scrollBehavior="auto"' } }),
230
+ /* @__PURE__ */ jsx5("link", { rel: "stylesheet", href: props.assets.stylePath }),
231
+ props.assets.reload && /* @__PURE__ */ jsx5("script", { dangerouslySetInnerHTML: { __html: reloadScript } })
190
232
  ] }),
191
- /* @__PURE__ */ jsxs4("body", { className: clsx4("wg_shell__root"), children: [
192
- /* @__PURE__ */ jsxs4("div", { className: clsx4("wg_shell__sidebar"), children: [
193
- /* @__PURE__ */ jsxs4("div", { className: clsx4("wg_shell__brandRow"), children: [
194
- /* @__PURE__ */ jsx4("a", { className: clsx4("wg_shell__brand"), href: "/", "aria-current": props.current === void 0 ? "page" : void 0, children: props.site.title }),
195
- /* @__PURE__ */ jsx4(
233
+ /* @__PURE__ */ jsxs5("body", { className: clsx5("wg_shell__root"), children: [
234
+ /* @__PURE__ */ jsxs5("div", { className: clsx5("wg_shell__sidebar"), children: [
235
+ /* @__PURE__ */ jsxs5("div", { className: clsx5("wg_shell__brandRow"), children: [
236
+ /* @__PURE__ */ jsx5("a", { className: clsx5("wg_shell__brand"), href: "/", "aria-current": props.current === void 0 ? "page" : void 0, children: props.site.title }),
237
+ /* @__PURE__ */ jsx5(
196
238
  "button",
197
239
  {
198
- className: clsx4("wg_shell__menuToggle"),
240
+ className: clsx5("wg_shell__menuToggle"),
199
241
  type: "button",
200
242
  "aria-label": copy_nav_open,
201
243
  "aria-controls": "fh-navigation-drawer",
202
244
  "aria-expanded": "false",
203
245
  "data-navigation-open": "",
204
- children: /* @__PURE__ */ jsx4("svg", { className: clsx4("wg_shell__menuIcon"), viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx4("path", { d: "M4 6h16M4 12h16M4 18h16" }) })
246
+ children: /* @__PURE__ */ jsx5("svg", { className: clsx5("wg_shell__menuIcon"), viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx5("path", { d: "M4 6h16M4 12h16M4 18h16" }) })
205
247
  }
206
248
  )
207
249
  ] }),
208
- /* @__PURE__ */ jsxs4("div", { className: clsx4("wg_shell__navigation"), "data-navigation": "", children: [
209
- /* @__PURE__ */ jsx4(WgShellNav, { site: props.site, docs: props.docs, current: props.current }),
210
- tocGroups.length > 0 && /* @__PURE__ */ jsx4(WgShellToc, { groups: tocGroups })
250
+ /* @__PURE__ */ jsxs5("div", { className: clsx5("wg_shell__navigation"), "data-navigation": "", children: [
251
+ /* @__PURE__ */ jsx5(WgShellNav, { site: props.site, docs: props.docs, current: props.current }),
252
+ tocGroups.length > 0 && /* @__PURE__ */ jsx5(WgShellToc, { groups: tocGroups })
211
253
  ] })
212
254
  ] }),
213
- /* @__PURE__ */ jsx4("main", { className: clsx4("wg_shell__main"), children: props.children }),
214
- /* @__PURE__ */ jsxs4("dialog", { className: clsx4("wg_shell__drawer"), id: "fh-navigation-drawer", "aria-labelledby": "fh-navigation-title", "data-navigation-drawer": "", children: [
215
- /* @__PURE__ */ jsxs4("div", { className: clsx4("wg_shell__drawerHeader"), children: [
216
- /* @__PURE__ */ jsx4("h2", { className: clsx4("wg_shell__drawerTitle"), id: "fh-navigation-title", children: copy_nav_title }),
217
- /* @__PURE__ */ jsx4("button", { className: clsx4("wg_shell__drawerClose"), type: "button", "aria-label": copy_nav_close, "data-navigation-close": "", children: /* @__PURE__ */ jsx4("svg", { className: clsx4("wg_shell__menuIcon"), viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx4("path", { d: "m6 6 12 12M18 6 6 18" }) }) })
255
+ /* @__PURE__ */ jsx5("main", { className: clsx5("wg_shell__main"), children: props.children }),
256
+ /* @__PURE__ */ jsxs5("dialog", { className: clsx5("wg_shell__drawer"), id: "fh-navigation-drawer", "aria-labelledby": "fh-navigation-title", "data-navigation-drawer": "", children: [
257
+ /* @__PURE__ */ jsxs5("div", { className: clsx5("wg_shell__drawerHeader"), children: [
258
+ /* @__PURE__ */ jsx5("h2", { className: clsx5("wg_shell__drawerTitle"), id: "fh-navigation-title", children: copy_nav_title }),
259
+ /* @__PURE__ */ jsx5("button", { className: clsx5("wg_shell__drawerClose"), type: "button", "aria-label": copy_nav_close, "data-navigation-close": "", children: /* @__PURE__ */ jsx5("svg", { className: clsx5("wg_shell__menuIcon"), viewBox: "0 0 24 24", "aria-hidden": "true", focusable: "false", children: /* @__PURE__ */ jsx5("path", { d: "m6 6 12 12M18 6 6 18" }) }) })
218
260
  ] }),
219
- /* @__PURE__ */ jsx4("div", { className: clsx4("wg_shell__drawerContent"), "data-navigation-content": "" })
261
+ /* @__PURE__ */ jsx5("div", { className: clsx5("wg_shell__drawerContent"), "data-navigation-content": "" })
220
262
  ] }),
221
- /* @__PURE__ */ jsx4("img", { className: clsx4("wg_shell__cursorFace"), src: asset_favicon_path, width: "24", height: "24", alt: "", "aria-hidden": "true", draggable: false, "data-cursor-face": "" }),
222
- /* @__PURE__ */ jsx4("script", { type: "module", src: asset_client_path })
263
+ /* @__PURE__ */ jsx5("img", { className: clsx5("wg_shell__cursorFace"), src: asset_favicon_path, width: "24", height: "24", alt: "", "aria-hidden": "true", draggable: false, "data-cursor-face": "" }),
264
+ /* @__PURE__ */ jsx5("script", { type: "module", src: props.assets.clientPath })
223
265
  ] })
224
266
  ] });
225
267
  };
226
268
 
227
269
  // src/page/doc/pg-doc.tsx
228
- import { jsx as jsx5 } from "react/jsx-runtime";
270
+ import { jsx as jsx6 } from "react/jsx-runtime";
229
271
  var PgDoc = (props) => {
230
- return /* @__PURE__ */ jsx5(WgShell, { site: props.site, docs: props.docs, assets: props.assets, title: props.doc.data.name, current: props.doc.id, outline: props.doc.outline, children: /* @__PURE__ */ jsx5(WgProse, { html: props.doc.html }) });
272
+ return /* @__PURE__ */ jsx6(WgShell, { site: props.site, docs: props.docs, assets: props.assets, title: props.doc.data.name, current: props.doc.id, outline: props.doc.outline, children: /* @__PURE__ */ jsx6(WgProse, { html: props.doc.html }) });
231
273
  };
232
274
 
233
275
  // src/page/home/pg-home.tsx
234
- import { jsx as jsx6 } from "react/jsx-runtime";
276
+ import { jsx as jsx7 } from "react/jsx-runtime";
235
277
  var PgHome = (props) => {
236
- return /* @__PURE__ */ jsx6(WgShell, { site: props.site, docs: props.docs, assets: props.assets, outline: props.home?.outline, children: props.home === void 0 ? /* @__PURE__ */ jsx6(WgProse, { head: { title: props.site.title, lead: copy_home_missing } }) : /* @__PURE__ */ jsx6(WgProse, { html: props.home.html }) });
278
+ return /* @__PURE__ */ jsx7(WgShell, { site: props.site, docs: props.docs, assets: props.assets, outline: props.home?.outline, children: props.home === void 0 ? /* @__PURE__ */ jsx7(WgProse, { head: { title: props.site.title, lead: copy_home_missing } }) : /* @__PURE__ */ jsx7(WgProse, { html: props.home.html }) });
237
279
  };
238
280
 
239
281
  // src/app.tsx
240
- import { jsx as jsx7 } from "react/jsx-runtime";
282
+ import { jsx as jsx8 } from "react/jsx-runtime";
241
283
  var toHtml = (page) => {
242
284
  return `<!DOCTYPE html>${renderToStaticMarkup(page)}`;
243
285
  };
244
286
  var createApp = (options) => {
245
287
  const app = new Hono();
246
- app.get("/", (c) => c.html(toHtml(/* @__PURE__ */ jsx7(PgHome, { site: options.site, docs: options.store.docs, assets: options.assets, home: options.store.home }))));
288
+ app.get("/", (c) => c.html(toHtml(/* @__PURE__ */ jsx8(PgHome, { site: options.site, docs: options.store.docs, assets: options.assets, home: options.store.home }))));
247
289
  app.get(
248
290
  "/:slug{.+}/",
249
291
  ssgParams(() => options.store.docs.map((doc) => ({ slug: doc.id }))),
250
292
  (c) => {
251
293
  const doc = options.store.docs.find((entry) => entry.id === c.req.param("slug"));
252
- return doc === void 0 ? c.notFound() : c.html(toHtml(/* @__PURE__ */ jsx7(PgDoc, { site: options.site, docs: options.store.docs, assets: options.assets, doc })));
294
+ return doc === void 0 ? c.notFound() : c.html(toHtml(/* @__PURE__ */ jsx8(PgDoc, { site: options.site, docs: options.store.docs, assets: options.assets, doc })));
253
295
  }
254
296
  );
255
297
  return app;
@@ -935,7 +977,7 @@ import { VFile } from "vfile";
935
977
  import { parse as parseYaml } from "yaml";
936
978
 
937
979
  // src/type/doc-data.ts
938
- import { z } from "zod";
980
+ import { z as z2 } from "zod";
939
981
 
940
982
  // src/constant/doc.ts
941
983
  var doc_type = {
@@ -944,28 +986,32 @@ var doc_type = {
944
986
  };
945
987
  var doc_order_default = Number.MAX_SAFE_INTEGER;
946
988
 
989
+ // src/type/group-path.ts
990
+ import { z } from "zod";
991
+ var groupPathSchema = z.union([z.string().trim().min(1), z.array(z.string().trim().min(1)).min(1)]).transform((path) => typeof path === "string" ? [path] : path);
992
+
947
993
  // src/type/doc-data.ts
948
- var docDataSchema = z.object({
994
+ var docDataSchema = z2.object({
949
995
  /**
950
996
  * 문서 이름 · 제목·목록 · 같은 첫 글자 허용
951
997
  */
952
- name: z.string().trim().min(1),
998
+ name: z2.string().trim().min(1),
953
999
  /**
954
1000
  * 짧은 문서 설명 · 사이드바 툴팁
955
1001
  */
956
- label: z.string(),
1002
+ label: z2.string(),
957
1003
  /**
958
1004
  * 문서 종류 · blueprint 전용 표현 미구현
959
1005
  */
960
- type: z.enum(doc_type).default(doc_type.document),
1006
+ type: z2.enum(doc_type).default(doc_type.document),
961
1007
  /**
962
- * 사이드바의 문서 묶음
1008
+ * 사이드바의 문서 묶음 경로 · 문자열도 배열로 정규화
963
1009
  */
964
- group: z.string().trim().min(1),
1010
+ group: groupPathSchema,
965
1011
  /**
966
1012
  * 묶음 안의 읽는 순서 · 미지정 문서는 뒤에서 이름순
967
1013
  */
968
- order: z.number().int().nonnegative().default(doc_order_default)
1014
+ order: z2.number().int().nonnegative().default(doc_order_default)
969
1015
  });
970
1016
 
971
1017
  // src/content/read-docs.ts
@@ -1068,7 +1114,7 @@ var createDevApp = (options) => {
1068
1114
  };
1069
1115
 
1070
1116
  // src/type/site-config.ts
1071
- import { z as z2 } from "zod";
1117
+ import { z as z3 } from "zod";
1072
1118
 
1073
1119
  // src/constant/status.ts
1074
1120
  var status_kind = {
@@ -1081,36 +1127,36 @@ var status_default_phrases = [
1081
1127
  ];
1082
1128
 
1083
1129
  // src/type/site-config.ts
1084
- var siteConfigSchema = z2.object({
1130
+ var siteConfigSchema = z3.object({
1085
1131
  /**
1086
1132
  * 사이드바 이름과 탭 제목
1087
1133
  */
1088
- title: z2.string().default(copy_site_title_default),
1134
+ title: z3.string().default(copy_site_title_default),
1089
1135
  /**
1090
1136
  * HTML 설명 메타데이터 · 선택
1091
1137
  */
1092
- description: z2.string().optional(),
1138
+ description: z3.string().optional(),
1093
1139
  /**
1094
- * 탐색 묶음의 읽는 순서 · 미지정 묶음은 뒤에서 이름순
1140
+ * 탐색 묶음 경로의 읽는 순서 · 미지정 형제 묶음은 뒤에서 이름순
1095
1141
  */
1096
- navigation: z2.array(z2.string().trim().min(1)).refine((groups) => new Set(groups).size === groups.length, copy_error_navigation_clash).default([...site_navigation_default]),
1142
+ navigation: z3.array(groupPathSchema).refine((groups) => new Set(groups.map((path) => JSON.stringify(path))).size === groups.length, copy_error_navigation_clash).default(site_navigation_default.map((name) => [name])),
1097
1143
  /**
1098
1144
  * 본문에서 상태 표지로 표시할 문구
1099
1145
  */
1100
- status: z2.array(
1101
- z2.object({
1146
+ status: z3.array(
1147
+ z3.object({
1102
1148
  /**
1103
1149
  * 찾을 문구 원문
1104
1150
  */
1105
- phrase: z2.string().min(1).refine((phrase) => phrase.trim().length > 0),
1151
+ phrase: z3.string().min(1).refine((phrase) => phrase.trim().length > 0),
1106
1152
  /**
1107
1153
  * 상태 표지의 색 종류
1108
1154
  */
1109
- kind: z2.enum(status_kind),
1155
+ kind: z3.enum(status_kind),
1110
1156
  /**
1111
1157
  * 문구 뒤의 YYYY-MM-DD 날짜 포함 여부
1112
1158
  */
1113
- date: z2.boolean().optional()
1159
+ date: z3.boolean().optional()
1114
1160
  })
1115
1161
  ).default(status_default_phrases)
1116
1162
  });
@@ -1123,17 +1169,26 @@ var toErrorMessage = (error) => {
1123
1169
  return error instanceof Error ? error.message : String(error);
1124
1170
  };
1125
1171
 
1126
- // src/util/font/to-font-css.ts
1172
+ // src/util/file/to-asset-path.ts
1127
1173
  import { createHash } from "node:crypto";
1128
1174
  import { readFileSync } from "node:fs";
1175
+ import { posix as posix2 } from "node:path";
1176
+ var toAssetPath = (options) => {
1177
+ const extension = posix2.extname(options.path);
1178
+ return `${posix2.dirname(options.path)}/${posix2.basename(options.path, extension)}.${createHash("sha256").update(readFileSync(options.file)).digest("hex")}${extension}`;
1179
+ };
1180
+
1181
+ // src/util/font/to-font-css.ts
1182
+ import { createHash as createHash2 } from "node:crypto";
1183
+ import { readFileSync as readFileSync2 } from "node:fs";
1129
1184
  import { basename, dirname as dirname2, extname as extname2, join as join3 } from "node:path";
1130
1185
  var toFontCss = (options) => {
1131
1186
  const files = /* @__PURE__ */ new Map();
1132
- const css = [...readFileSync(options.css, "utf8").matchAll(/@font-face\s*\{[^}]*\}/g)].map(
1187
+ const css = [...readFileSync2(options.css, "utf8").matchAll(/@font-face\s*\{[^}]*\}/g)].map(
1133
1188
  (match) => match[0].replace(/font-display:\s*[^;]+;/g, `font-display: ${font_display_strategy};`).replace(/url\(([^)]+)\)/g, (_match, relativeUrl) => {
1134
1189
  const file = join3(dirname2(options.css), relativeUrl);
1135
1190
  const extension = extname2(file);
1136
- const hash = createHash("sha256").update(readFileSync(file)).digest("hex").slice(0, font_hash_length);
1191
+ const hash = createHash2("sha256").update(readFileSync2(file)).digest("hex").slice(0, font_hash_length);
1137
1192
  const url = `${options.fontDir}/${basename(file, extension)}.${hash}${extension}`;
1138
1193
  files.set(url, file);
1139
1194
  return `url(${url})`;
@@ -1182,9 +1237,11 @@ var main = async () => {
1182
1237
  throw new Error(`${copy_error_font_preload}: ${font_sans_preload_file}`);
1183
1238
  }
1184
1239
  const fontCss = [sans.css, mono.css, brand.css].join("\n");
1240
+ const stylePath = toAssetPath({ path: asset_style_path, file: join4(kitRoot, "dist/cli.css") });
1241
+ const clientPath = toAssetPath({ path: asset_client_path, file: join4(kitRoot, "dist/client.js") });
1185
1242
  const files = new Map([
1186
- [asset_style_path, join4(kitRoot, "dist/cli.css")],
1187
- [asset_client_path, join4(kitRoot, "dist/client.js")],
1243
+ [stylePath, join4(kitRoot, "dist/cli.css")],
1244
+ [clientPath, join4(kitRoot, "dist/client.js")],
1188
1245
  [asset_favicon_path, join4(kitRoot, "src/asset/favicon.svg")],
1189
1246
  ...sans.files,
1190
1247
  ...mono.files,
@@ -1196,7 +1253,7 @@ var main = async () => {
1196
1253
  const app = createApp({
1197
1254
  site: siteConfig,
1198
1255
  store,
1199
- assets: { fontCss, preload: [...brand.files.keys(), sansPreload[0], ...mono.files.keys()], reload: command === "dev" }
1256
+ assets: { stylePath, clientPath, fontCss, preload: [...brand.files.keys(), sansPreload[0], ...mono.files.keys()], reload: command === "dev" }
1200
1257
  });
1201
1258
  if (command === "build") {
1202
1259
  await rm(outDir, { recursive: true, force: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "for-humanity",
3
- "version": "0.1.2",
3
+ "version": "0.2.1",
4
4
  "description": "Documents written for humans to read: a docs kit with a sidebar, parts, sections and flowcharts, built on Hono and React",
5
5
  "keywords": [
6
6
  "docs",