for-humanity 0.4.4 → 0.6.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 (85) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +6 -5
  3. package/dist/browser.js +39 -0
  4. package/dist/cli.css +350 -262
  5. package/dist/cli.js +1174 -364
  6. package/package.json +8 -11
  7. package/src/component/widget/{shell → cursor-face}/_constant/cursor-face.ts +5 -0
  8. package/src/component/widget/cursor-face/wg-cursor-face.css +15 -0
  9. package/src/component/widget/cursor-face/wg-cursor-face.tsx +153 -0
  10. package/src/component/widget/navigation/_constant/navigation-data-schema.ts +35 -0
  11. package/src/component/widget/navigation/_constant/navigation.ts +9 -0
  12. package/src/component/widget/navigation/_function/restore-navigation-scroll.ts +59 -0
  13. package/src/{content → component/widget/navigation/_function}/to-doc-groups/_to-doc-branches.ts +2 -2
  14. package/src/{content → component/widget/navigation/_function}/to-doc-groups/_to-doc-group-branches.ts +3 -3
  15. package/src/{content → component/widget/navigation/_function}/to-doc-groups/to-doc-groups.ts +1 -1
  16. package/src/component/widget/navigation/_function/to-navigation-branch-id.ts +6 -0
  17. package/src/component/widget/navigation/_function/to-navigation-data/_to-navigation-document.ts +14 -0
  18. package/src/component/widget/navigation/_function/to-navigation-data/_to-navigation-group.ts +14 -0
  19. package/src/component/widget/navigation/_function/to-navigation-data/to-navigation-data.ts +25 -0
  20. package/src/component/widget/navigation/_function/to-navigation-json.ts +8 -0
  21. package/src/component/widget/{shell → navigation}/_function/to-toc-groups.ts +2 -2
  22. package/src/{type → component/widget/navigation/_type}/doc-group.ts +1 -1
  23. package/src/component/widget/navigation/_type/navigation-data.ts +83 -0
  24. package/src/component/widget/navigation/_type/navigation-focus-snapshot.ts +21 -0
  25. package/src/component/widget/navigation/_wg-navigation-branch-toggle.css +28 -0
  26. package/src/component/widget/navigation/_wg-navigation-branch-toggle.tsx +48 -0
  27. package/src/component/widget/{shell/_wg-shell-nav-list.css → navigation/_wg-navigation-nav-list.css} +24 -16
  28. package/src/component/widget/navigation/_wg-navigation-nav-list.tsx +106 -0
  29. package/src/component/widget/{shell/_wg-shell-nav.css → navigation/_wg-navigation-nav.css} +9 -3
  30. package/src/component/widget/navigation/_wg-navigation-nav.tsx +43 -0
  31. package/src/component/widget/navigation/_wg-navigation-scroll.tsx +117 -0
  32. package/src/component/widget/{shell/_wg-shell-toc.css → navigation/_wg-navigation-toc.css} +39 -24
  33. package/src/component/widget/navigation/_wg-navigation-toc.tsx +155 -0
  34. package/src/component/widget/navigation/wg-navigation.css +282 -0
  35. package/src/component/widget/navigation/wg-navigation.tsx +479 -0
  36. package/src/component/widget/prose/_function/rehype-sections/rehype-sections.ts +1 -1
  37. package/src/component/widget/shell/wg-shell.css +13 -269
  38. package/src/component/widget/shell/wg-shell.tsx +16 -97
  39. package/src/component/widget/shell-controls/wg-shell-controls.css +4 -0
  40. package/src/component/widget/shell-controls/wg-shell-controls.tsx +170 -0
  41. package/src/constant/asset.ts +0 -5
  42. package/src/constant/copy.ts +5 -0
  43. package/src/entry/browser/browser.tsx +22 -0
  44. package/src/entry/cli/_constant/crawling.ts +14 -0
  45. package/src/{app.tsx → entry/cli/_function/create-app.tsx} +6 -6
  46. package/src/{dev.ts → entry/cli/_function/create-dev-app.ts} +9 -1
  47. package/src/{content → entry/cli/_function/create-processor}/create-processor.ts +3 -3
  48. package/src/{content → entry/cli/_function}/read-docs.ts +21 -2
  49. package/src/{content → entry/cli/_function}/read-home.ts +1 -1
  50. package/src/entry/cli/_function/to-crawling-app.ts +42 -0
  51. package/src/{content/read-docs.test.ts → entry/cli/cli.test.ts} +211 -31
  52. package/src/{cli.ts → entry/cli/cli.ts} +31 -15
  53. package/src/store/navigation/_constant/tree.ts +6 -0
  54. package/src/store/navigation/create-navigation-stores.test.ts +92 -0
  55. package/src/store/navigation/create-navigation-stores.ts +73 -0
  56. package/src/store/navigation/navigation-state.ts +46 -0
  57. package/src/store/navigation/navigation-storage.ts +14 -0
  58. package/src/store/navigation/to-navigation-scroll-state.ts +42 -0
  59. package/src/store/navigation/to-navigation-tree-state.ts +19 -0
  60. package/src/store/navigation/use-navigation-tree-store.ts +12 -0
  61. package/src/style/token.css +3 -2
  62. package/src/type/doc-content.ts +1 -1
  63. package/src/type/doc-file-data.ts +1 -1
  64. package/src/{component/widget/shell/_type → type}/doc-outline.ts +1 -1
  65. package/src/type/site-assets.ts +2 -2
  66. package/src/type/site-config.ts +10 -1
  67. package/dist/client.js +0 -7
  68. package/src/client.ts +0 -335
  69. package/src/component/widget/shell/_constant/navigation.ts +0 -9
  70. package/src/component/widget/shell/_function/bind-mobile-navigation.ts +0 -147
  71. package/src/component/widget/shell/_function/bind-navigation-overflow.ts +0 -47
  72. package/src/component/widget/shell/_function/bind-navigation-scroll.ts +0 -84
  73. package/src/component/widget/shell/_function/restore-navigation-scroll.ts +0 -47
  74. package/src/component/widget/shell/_wg-shell-nav-list.tsx +0 -87
  75. package/src/component/widget/shell/_wg-shell-nav.tsx +0 -49
  76. package/src/component/widget/shell/_wg-shell-toc.tsx +0 -57
  77. /package/src/{constant → component/widget/navigation/_constant}/locale.ts +0 -0
  78. /package/src/component/widget/{shell → navigation}/_constant/reading-line.ts +0 -0
  79. /package/src/{type → component/widget/navigation/_type}/doc-branch.ts +0 -0
  80. /package/src/component/widget/{shell → navigation}/_type/toc-group.ts +0 -0
  81. /package/src/{constant → entry/cli/_constant}/cli.ts +0 -0
  82. /package/src/{content/rehype-assets.ts → entry/cli/_function/create-processor/_rehype-assets.ts} +0 -0
  83. /package/src/{content/rehype-heading-ids.ts → entry/cli/_function/create-processor/_rehype-heading-ids.ts} +0 -0
  84. /package/src/{content/remark-report.ts → entry/cli/_function/create-processor/_remark-report.ts} +0 -0
  85. /package/src/{component/widget/prose/_type → type}/doc-section.ts +0 -0
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import {EventEmitter} from "node:events";
8
- import {existsSync, watch} from "node:fs";
8
+ import {existsSync, readFileSync, watch} from "node:fs";
9
9
  import {copyFile, mkdir, rm, writeFile} from "node:fs/promises";
10
10
  import {basename, dirname, extname, join, relative, resolve, sep} from "node:path";
11
11
  import {fileURLToPath, pathToFileURL} from "node:url";
@@ -14,16 +14,18 @@ import {serveStatic} from "@hono/node-server/serve-static";
14
14
  import {debounce} from "es-toolkit";
15
15
  import {Hono} from "hono";
16
16
  import {toSSG} from "hono/ssg";
17
- import {createApp} from "@/app";
18
- import {asset_client_path, asset_favicon_path, asset_font_dir, asset_media_extensions, asset_style_path} from "@/constant/asset";
19
- import {cli_config_file_name, cli_default_command, cli_default_docs_dir, cli_dev_port, cli_reload_delay_ms} from "@/constant/cli";
17
+ import {asset_favicon_path, asset_font_dir, asset_media_extensions, asset_style_path} from "@/constant/asset";
20
18
  import {copy_error_config, copy_error_font_preload, copy_error_prefix, copy_error_unknown_command} from "@/constant/copy";
21
19
  import {font_brand_css, font_cache_control, font_mono_css, font_sans_css, font_sans_preload_file} from "@/constant/font";
22
20
  import {site_config_absent} from "@/constant/site";
23
- import {createProcessor} from "@/content/create-processor";
24
- import {readDocs} from "@/content/read-docs";
25
- import {readHome} from "@/content/read-home";
26
- import {createDevApp} from "@/dev";
21
+ import {cli_config_file_name, cli_default_command, cli_default_docs_dir, cli_dev_port, cli_reload_delay_ms} from "@/entry/cli/_constant/cli";
22
+ import {crawling_reserved_roots} from "@/entry/cli/_constant/crawling";
23
+ import {createApp} from "@/entry/cli/_function/create-app";
24
+ import {createDevApp} from "@/entry/cli/_function/create-dev-app";
25
+ import {createProcessor} from "@/entry/cli/_function/create-processor/create-processor";
26
+ import {readDocs} from "@/entry/cli/_function/read-docs";
27
+ import {readHome} from "@/entry/cli/_function/read-home";
28
+ import {toCrawlingApp} from "@/entry/cli/_function/to-crawling-app";
27
29
  import {siteConfigSchema} from "@/type/site-config";
28
30
  import {toChangeQueue} from "@/util/async/to-change-queue";
29
31
  import {toErrorMessage} from "@/util/error/to-error-message";
@@ -86,10 +88,9 @@ const main = async () => {
86
88
 
87
89
  const fontCss = [sans.css, mono.css, brand.css].join("\n");
88
90
  const stylePath = toAssetPath({path: asset_style_path, file: join(kitRoot, "dist/cli.css")});
89
- const clientPath = toAssetPath({path: asset_client_path, file: join(kitRoot, "dist/client.js")});
91
+ const browserScript = readFileSync(join(kitRoot, "dist/browser.js"), "utf8");
90
92
  const kitFiles = new Map([
91
93
  [stylePath, join(kitRoot, "dist/cli.css")],
92
- [clientPath, join(kitRoot, "dist/client.js")],
93
94
  [asset_favicon_path, join(kitRoot, "src/asset/favicon.svg")],
94
95
  ...sans.files,
95
96
  ...mono.files,
@@ -97,12 +98,16 @@ const main = async () => {
97
98
  ]);
98
99
  const files = new Map(kitFiles);
99
100
  const processor = createProcessor({site: siteConfig, root: docsRoot, files});
100
- const [docs, home] = await Promise.all([readDocs({root: docsRoot, processor}), readHome({root: docsRoot, processor})]);
101
+ const [docs, home] = await Promise.all([
102
+ readDocs({root: docsRoot, processor, reserved: siteConfig.url === undefined ? undefined : crawling_reserved_roots}),
103
+ readHome({root: docsRoot, processor}),
104
+ ]);
101
105
  const store = {docs, home};
106
+ const crawling = siteConfig.url === undefined ? undefined : toCrawlingApp({url: siteConfig.url, store});
102
107
  const app = createApp({
103
108
  site: siteConfig,
104
109
  store,
105
- assets: {stylePath, clientPath, fontCss, preload: [...brand.files.keys(), sansPreload[0], ...mono.files.keys()], reload: command === "dev"},
110
+ assets: {stylePath, browserScript, fontCss, preload: [...brand.files.keys(), sansPreload[0], ...mono.files.keys()], reload: command === "dev"},
106
111
  });
107
112
 
108
113
  if (command === "build") {
@@ -115,12 +120,20 @@ const main = async () => {
115
120
  throw result.error;
116
121
  }
117
122
 
123
+ const crawlerResult = crawling === undefined ? undefined : await toSSG(crawling, {writeFile, mkdir}, {dir: outDir});
124
+
125
+ if (crawlerResult?.success === false) {
126
+ throw crawlerResult.error;
127
+ }
128
+
118
129
  for (const [url, file] of files) {
119
130
  await mkdir(dirname(join(outDir, url)), {recursive: true});
120
131
  await copyFile(file, join(outDir, url));
121
132
  }
122
133
 
123
- console.log(`${result.files.length} pages, ${files.size} assets → ${relative(process.cwd(), outDir)}`);
134
+ console.log(
135
+ `${crawlerResult === undefined ? result.files.length : result.files.length + crawlerResult.files.length} files, ${files.size} assets → ${relative(process.cwd(), outDir)}`,
136
+ );
124
137
 
125
138
  return;
126
139
  }
@@ -128,7 +141,7 @@ const main = async () => {
128
141
  const reload = new EventEmitter();
129
142
  let generation = 0;
130
143
 
131
- serve({fetch: createDevApp({pages: app, files, reload}).fetch, port: cli_dev_port}, (info) => console.log(`http://localhost:${info.port}/`));
144
+ serve({fetch: createDevApp({pages: app, crawling, files, reload}).fetch, port: cli_dev_port}, (info) => console.log(`http://localhost:${info.port}/`));
132
145
 
133
146
  /**
134
147
  * 갱신은 하나씩 실행 · 연속 저장 합치기 · 최신 변경만 적용
@@ -140,7 +153,10 @@ const main = async () => {
140
153
  const currentGeneration = generation;
141
154
  const nextFiles = new Map(kitFiles);
142
155
  const nextProcessor = createProcessor({site: siteConfig, root: docsRoot, files: nextFiles});
143
- const [docs, home] = await Promise.all([readDocs({root: docsRoot, processor: nextProcessor}), readHome({root: docsRoot, processor: nextProcessor})]);
156
+ const [docs, home] = await Promise.all([
157
+ readDocs({root: docsRoot, processor: nextProcessor, reserved: siteConfig.url === undefined ? undefined : crawling_reserved_roots}),
158
+ readHome({root: docsRoot, processor: nextProcessor}),
159
+ ]);
144
160
 
145
161
  if (currentGeneration !== generation) {
146
162
  return;
@@ -0,0 +1,6 @@
1
+ import type {NavigationTreeState} from "@/store/navigation/navigation-state";
2
+
3
+ /**
4
+ * 서버에서 사용자 저장소 없이 모든 가지를 펼치는 고정 스냅샷
5
+ */
6
+ export const tree_server_state: NavigationTreeState = {collapsed: {}};
@@ -0,0 +1,92 @@
1
+ import assert from "node:assert/strict";
2
+ import {test} from "node:test";
3
+ import {createNavigationStores} from "@/store/navigation/create-navigation-stores";
4
+ import {navigation_scroll_storage_key, navigation_tree_storage_key} from "@/store/navigation/navigation-storage";
5
+
6
+ const fixture = () => {
7
+ const local = new Map<string, string>();
8
+ const session = new Map<string, string>();
9
+ const storage = (values: Map<string, string>) => ({
10
+ getItem: (key: string) => {
11
+ const value = values.get(key);
12
+ return value === undefined ? null : value;
13
+ },
14
+ setItem: (key: string, value: string) => {
15
+ values.set(key, value);
16
+ },
17
+ removeItem: (key: string) => {
18
+ values.delete(key);
19
+ },
20
+ });
21
+ return {local, session, options: {local: () => storage(local), session: () => storage(session)}};
22
+ };
23
+
24
+ test("starts with every branch expanded and persists explicit choices across pages", () => {
25
+ const setup = fixture();
26
+ const stores = createNavigationStores(setup.options);
27
+ assert.deepEqual(stores.tree.getState(), {collapsed: {}});
28
+ stores.tree.setState({collapsed: {'group:["Projects"]': true, "outline:one:heading:setup": false}});
29
+ assert.deepEqual(createNavigationStores(setup.options).tree.getState(), stores.tree.getState());
30
+ assert.equal(setup.session.has(navigation_tree_storage_key), false);
31
+ });
32
+
33
+ test("retains independent wide, desktop and drawer scroll positions within a tab", () => {
34
+ const setup = fixture();
35
+ const stores = createNavigationStores(setup.options);
36
+ stores.scroll.setState({
37
+ positions: {
38
+ wide: {navigation: "docs", pathname: "/one/", documents: 450, outline: 300},
39
+ desktop: {navigation: "docs", pathname: "/two/", documents: 750, outline: 100},
40
+ drawer: {navigation: "docs", pathname: "/one/", documents: 200, outline: 800},
41
+ },
42
+ });
43
+ assert.deepEqual(createNavigationStores(setup.options).scroll.getState(), stores.scroll.getState());
44
+ assert.equal(setup.local.has(navigation_scroll_storage_key), false);
45
+ });
46
+
47
+ test("rejects malformed and unknown-version persistence without losing defaults", () => {
48
+ for (const value of ["{", "null", '{"state":null,"version":1}', '{"state":{"collapsed":{"group:a":true}},"version":999}']) {
49
+ const setup = fixture();
50
+ setup.local.set(navigation_tree_storage_key, value);
51
+ setup.session.set(navigation_scroll_storage_key, value);
52
+ const stores = createNavigationStores(setup.options);
53
+ assert.deepEqual(stores.tree.getState(), {collapsed: {}});
54
+ assert.deepEqual(stores.scroll.getState(), {positions: {}});
55
+ }
56
+ });
57
+
58
+ test("narrows persisted branch flags and rejects invalid scroll coordinates", () => {
59
+ const setup = fixture();
60
+ setup.local.set(navigation_tree_storage_key, JSON.stringify({version: 1, state: {collapsed: {good: true, bad: "true"}, unexpected: true}}));
61
+ setup.session.set(
62
+ navigation_scroll_storage_key,
63
+ JSON.stringify({
64
+ version: 1,
65
+ state: {
66
+ positions: {
67
+ desktop: {navigation: "docs", pathname: "/one/", documents: -1, outline: 0},
68
+ wide: {navigation: "docs", pathname: "/one/", documents: 100, outline: 20},
69
+ drawer: {navigation: "docs", pathname: "/one/", documents: 100, outline: "20"},
70
+ },
71
+ },
72
+ }),
73
+ );
74
+ const stores = createNavigationStores(setup.options);
75
+ assert.deepEqual(stores.tree.getState(), {collapsed: {good: true}});
76
+ assert.deepEqual(stores.scroll.getState().positions.wide, {navigation: "docs", pathname: "/one/", documents: 100, outline: 20});
77
+ assert.equal(stores.scroll.getState().positions.desktop, undefined);
78
+ assert.equal(stores.scroll.getState().positions.drawer, undefined);
79
+ });
80
+
81
+ test("keeps in-memory interaction working when storage reads and writes are blocked", () => {
82
+ const blocked = () => {
83
+ throw new Error("Storage blocked");
84
+ };
85
+ const stores = createNavigationStores({local: blocked, session: blocked});
86
+ stores.tree.setState({collapsed: {"document:one": true}});
87
+ stores.tree.persist.rehydrate();
88
+ assert.equal(stores.tree.getState().collapsed["document:one"], true);
89
+ stores.scroll.setState({positions: {desktop: {navigation: "docs", pathname: "/", documents: 100, outline: 200}}});
90
+ stores.scroll.persist.rehydrate();
91
+ assert.equal(stores.scroll.getState().positions.desktop?.documents, 100);
92
+ });
@@ -0,0 +1,73 @@
1
+ import {createJSONStorage, persist, type StateStorage} from "zustand/middleware";
2
+ import {createStore} from "zustand/vanilla";
3
+ import type {NavigationScrollState, NavigationTreeState} from "@/store/navigation/navigation-state";
4
+ import {navigation_scroll_storage_key, navigation_storage_version, navigation_tree_storage_key} from "@/store/navigation/navigation-storage";
5
+ import {toNavigationScrollState} from "@/store/navigation/to-navigation-scroll-state";
6
+ import {toNavigationTreeState} from "@/store/navigation/to-navigation-tree-state";
7
+
8
+ /**
9
+ * 브라우저 저장소를 늦게 읽는 경계 · 차단 환경과 테스트의 메모리 저장소 지원
10
+ */
11
+ interface NavigationStorage {
12
+ /**
13
+ * 브라우저 재실행 후에도 유지할 접힘 선택
14
+ */
15
+ local: () => StateStorage;
16
+ /**
17
+ * 같은 탭 안에서만 복원할 스크롤 위치
18
+ */
19
+ session: () => StateStorage;
20
+ }
21
+
22
+ /**
23
+ * 브라우저 탐색의 vanilla 스토어 생성기 · Hono Hook이 아닌 생성기이므로 use- 접두사를 붙이지 않음
24
+ * 서버 요청은 이 생성기를 실행하지 않으며 저장 실패 시 메모리 상태 유지
25
+ */
26
+ export const createNavigationStores = (options: NavigationStorage) => {
27
+ /**
28
+ * 저장소 접근이 막혀도 persist 갱신이 버튼·스크롤 동작을 중단하지 않도록 격리
29
+ */
30
+ const toSafeStorage = (getStorage: NavigationStorage["local"]): StateStorage => {
31
+ return {
32
+ getItem: (key) => {
33
+ // persist의 읽기 오류 경로로 보내 기존 메모리 상태를 유지 · null은 실제 저장소 초기화에만 사용
34
+ return getStorage().getItem(key);
35
+ },
36
+ setItem: (key, value) => {
37
+ try {
38
+ return getStorage().setItem(key, value);
39
+ } catch {
40
+ /* 저장 실패 시에도 스토어의 메모리 상태는 유지 */
41
+ }
42
+ },
43
+ removeItem: (key) => {
44
+ try {
45
+ return getStorage().removeItem(key);
46
+ } catch {
47
+ /* 저장소 차단이 탐색을 막지 않음 */
48
+ }
49
+ },
50
+ };
51
+ };
52
+
53
+ return {
54
+ tree: createStore<NavigationTreeState>()(
55
+ persist(() => ({collapsed: {}}), {
56
+ name: navigation_tree_storage_key,
57
+ version: navigation_storage_version,
58
+ storage: createJSONStorage(() => toSafeStorage(options.local)),
59
+ merge: (persisted) => toNavigationTreeState(persisted),
60
+ migrate: () => ({collapsed: {}}),
61
+ }),
62
+ ),
63
+ scroll: createStore<NavigationScrollState>()(
64
+ persist(() => ({positions: {}}), {
65
+ name: navigation_scroll_storage_key,
66
+ version: navigation_storage_version,
67
+ storage: createJSONStorage(() => toSafeStorage(options.session)),
68
+ merge: (persisted) => toNavigationScrollState(persisted),
69
+ migrate: () => ({positions: {}}),
70
+ }),
71
+ ),
72
+ };
73
+ };
@@ -0,0 +1,46 @@
1
+ /**
2
+ * 스크롤 영역의 배치 · 넓은 3열, 좁은 2열, 모바일 모달
3
+ */
4
+ export type NavigationLayout = "wide" | "desktop" | "drawer";
5
+
6
+ /**
7
+ * 사용자가 명시적으로 바꾼 가지 선택 · 없는 키는 펼침
8
+ */
9
+ export interface NavigationTreeState {
10
+ /**
11
+ * 문서 ID·그룹 전체 경로·페이지별 heading으로 구분한 접힘 상태
12
+ */
13
+ collapsed: Record<string, boolean>;
14
+ }
15
+
16
+ /**
17
+ * 한 화면 배치의 마지막 탐색 위치 · 목록 변경과 다른 페이지의 목차는 재사용하지 않음
18
+ */
19
+ export interface NavigationPosition {
20
+ /**
21
+ * 현재 사이트의 공통 문서 URL·제목 목록
22
+ */
23
+ navigation: string;
24
+ /**
25
+ * 목차가 속한 페이지
26
+ */
27
+ pathname: string;
28
+ /**
29
+ * 공통 문서 목록의 세로 위치
30
+ */
31
+ documents: number;
32
+ /**
33
+ * 현재 페이지 목차의 세로 위치
34
+ */
35
+ outline: number;
36
+ }
37
+
38
+ /**
39
+ * 같은 탭 안에서 유지하는 배치별 탐색 위치
40
+ */
41
+ export interface NavigationScrollState {
42
+ /**
43
+ * 아직 열지 않은 배치의 위치는 저장하지 않음
44
+ */
45
+ positions: Partial<Record<NavigationLayout, NavigationPosition>>;
46
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * 문서·현재 페이지 목차의 접힘 선택 · 기본은 전체 펼침
3
+ */
4
+ export const navigation_tree_storage_key = "for-humanity:navigation-tree";
5
+
6
+ /**
7
+ * 저장 형태가 바뀌면 기존 자료를 기본 상태로 돌리는 persist 버전
8
+ */
9
+ export const navigation_storage_version = 1;
10
+
11
+ /**
12
+ * 같은 탭에서 문서 이동 후 탐색 위치 복원 · 데스크톱과 드로어는 별도 저장
13
+ */
14
+ export const navigation_scroll_storage_key = "for-humanity:navigation-scroll";
@@ -0,0 +1,42 @@
1
+ import type {NavigationPosition, NavigationScrollState} from "@/store/navigation/navigation-state";
2
+
3
+ /**
4
+ * 알 수 없는 저장 형태·음수·무한 좌표 제외 · 초기 HTML과 Zustand에 같은 검증 적용
5
+ */
6
+ export const toNavigationScrollState = (value: unknown): NavigationScrollState => {
7
+ /**
8
+ * 한 배치의 완전한 좌표만 복원 대상으로 수용
9
+ */
10
+ const toPosition = (position: unknown): NavigationPosition | undefined => {
11
+ if (
12
+ typeof position !== "object" ||
13
+ position === null ||
14
+ !("navigation" in position) ||
15
+ typeof position.navigation !== "string" ||
16
+ !("pathname" in position) ||
17
+ typeof position.pathname !== "string" ||
18
+ !("documents" in position) ||
19
+ typeof position.documents !== "number" ||
20
+ !Number.isFinite(position.documents) ||
21
+ position.documents < 0 ||
22
+ !("outline" in position) ||
23
+ typeof position.outline !== "number" ||
24
+ !Number.isFinite(position.outline) ||
25
+ position.outline < 0
26
+ ) {
27
+ return;
28
+ }
29
+ return {navigation: position.navigation, pathname: position.pathname, documents: position.documents, outline: position.outline};
30
+ };
31
+
32
+ if (typeof value !== "object" || value === null || !("positions" in value) || typeof value.positions !== "object" || value.positions === null) {
33
+ return {positions: {}};
34
+ }
35
+
36
+ const positions = {
37
+ wide: toPosition("wide" in value.positions ? value.positions.wide : undefined),
38
+ desktop: toPosition("desktop" in value.positions ? value.positions.desktop : undefined),
39
+ drawer: toPosition("drawer" in value.positions ? value.positions.drawer : undefined),
40
+ };
41
+ return {positions: Object.fromEntries(Object.entries(positions).filter((entry) => entry[1] !== undefined))};
42
+ };
@@ -0,0 +1,19 @@
1
+ import type {NavigationTreeState} from "@/store/navigation/navigation-state";
2
+
3
+ /**
4
+ * persist 저장 자료에서 boolean 접힘 선택만 수용
5
+ */
6
+ export const toNavigationTreeState = (value: unknown): NavigationTreeState => {
7
+ if (
8
+ typeof value !== "object" ||
9
+ value === null ||
10
+ !("collapsed" in value) ||
11
+ typeof value.collapsed !== "object" ||
12
+ value.collapsed === null ||
13
+ Array.isArray(value.collapsed)
14
+ ) {
15
+ return {collapsed: {}};
16
+ }
17
+
18
+ return {collapsed: Object.fromEntries(Object.entries(value.collapsed).filter((entry) => typeof entry[1] === "boolean"))};
19
+ };
@@ -0,0 +1,12 @@
1
+ import {useSyncExternalStore} from "hono/jsx";
2
+ import {tree_server_state} from "@/store/navigation/_constant/tree";
3
+ import type {createNavigationStores} from "@/store/navigation/create-navigation-stores";
4
+
5
+ /**
6
+ * Hono 컴포넌트의 persist 구독 · 브라우저 최초 렌더도 저장된 값을 즉시 소비
7
+ */
8
+ export const useNavigationTreeStore = (tree?: ReturnType<typeof createNavigationStores>["tree"]) => {
9
+ const subscribe = tree === undefined ? () => () => {} : tree.subscribe;
10
+ const getSnapshot = tree === undefined ? () => tree_server_state : tree.getState;
11
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
12
+ };
@@ -1,7 +1,7 @@
1
1
  /*
2
2
  * 공통 토큰의 단일 출처 · --app-종류-쓰임
3
3
  * 사용 기준: templates/document/design.md · 테마 분기는 color-scheme에 한정
4
- * 폭 기준 1024px·640px · 공유 사이드바와 본문 배치
4
+ * 폭 기준 1536px·1024px·640px · 양쪽 탐색과 본문 배치
5
5
  */
6
6
 
7
7
  :root {
@@ -102,7 +102,7 @@
102
102
  --app-space-gutter: 28px;
103
103
  --app-space-column: 32px;
104
104
  --app-space-sidebar-gap: 32px;
105
- --app-size-sidebar: 200px;
105
+ --app-size-sidebar: 240px;
106
106
  --app-size-main: 900px;
107
107
  --app-size-page: calc(var(--app-size-main) + (var(--app-size-sidebar) + var(--app-space-sidebar-gap) + var(--app-space-gutter)) * 2);
108
108
  --app-size-brand: calc(var(--app-font-size-title) * var(--app-line-height-title) + var(--app-space-page-top) + var(--app-space-group));
@@ -110,6 +110,7 @@
110
110
  --app-size-menu-button: 44px;
111
111
  --app-size-drawer: 320px;
112
112
  --app-size-navigation-fade: 28px;
113
+ --app-size-tree-toggle: 24px;
113
114
 
114
115
  /* 화면 위쪽 제목 여백 · hash 도착점과 목차 읽는 선의 공통 기준 */
115
116
  --app-space-anchor: 80px;
@@ -1,4 +1,4 @@
1
- import type {DocOutline} from "@/component/widget/shell/_type/doc-outline";
1
+ import type {DocOutline} from "@/type/doc-outline";
2
2
 
3
3
  /**
4
4
  * README 홈과 일반 문서가 공유하는 렌더링 결과
@@ -1,6 +1,6 @@
1
- import type {DocSection} from "@/component/widget/prose/_type/doc-section";
2
1
  import type {DocData} from "@/type/doc-data";
3
2
  import type {DocHeading} from "@/type/doc-heading";
3
+ import type {DocSection} from "@/type/doc-section";
4
4
 
5
5
  /**
6
6
  * 문서 처리 중간 계약 · read-docs의 머리말과 rehype의 제목·절 정보
@@ -1,5 +1,5 @@
1
- import type {DocSection} from "@/component/widget/prose/_type/doc-section";
2
1
  import type {DocHeading} from "@/type/doc-heading";
2
+ import type {DocSection} from "@/type/doc-section";
3
3
 
4
4
  /**
5
5
  * 문서 렌더링 결과의 목차 입력
@@ -7,9 +7,9 @@ export interface SiteAssets {
7
7
  */
8
8
  stylePath: string;
9
9
  /**
10
- * 내용 지문으로 구분한 브라우저 스크립트 URL
10
+ * 셸 DOM 뒤에서 동기 실행할 단일 Hono JSX 브라우저 번들
11
11
  */
12
- clientPath: string;
12
+ browserScript: string;
13
13
  /**
14
14
  * 내장 @font-face · head의 style 내용
15
15
  */
@@ -1,5 +1,5 @@
1
1
  import {z} from "zod";
2
- import {copy_error_navigation_clash, copy_site_title_default} from "@/constant/copy";
2
+ import {copy_error_navigation_clash, copy_error_site_url, copy_site_title_default} from "@/constant/copy";
3
3
  import {site_navigation_default} from "@/constant/site";
4
4
  import {status_default_phrases, status_kind} from "@/constant/status";
5
5
  import {groupPathSchema} from "@/type/group-path";
@@ -17,6 +17,15 @@ export const siteConfigSchema = z.object({
17
17
  * HTML 설명 메타데이터 · 선택
18
18
  */
19
19
  description: z.string().optional(),
20
+ /**
21
+ * 공개 사이트 루트 URL · 지정할 때만 robots.txt와 sitemap.xml 생성
22
+ */
23
+ url: z
24
+ .url({protocol: /^https?$/})
25
+ .transform((value) => new URL(value))
26
+ .refine((value) => value.username === "" && value.password === "" && value.pathname === "/" && value.search === "" && value.hash === "", copy_error_site_url)
27
+ .transform((value) => value.origin)
28
+ .optional(),
20
29
  /**
21
30
  * 탐색 묶음 경로의 읽는 순서 · 미지정 형제 묶음은 뒤에서 이름순
22
31
  */
package/dist/client.js DELETED
@@ -1,7 +0,0 @@
1
- var E="fh-cursor-face";var b="(width < 1024px)",S="for-humanity:navigation-scroll";var v=e=>{if(!e)return null;try{return document.getElementById(decodeURIComponent(e.slice(1)))}catch(t){if(t instanceof URIError)return null;throw t}};var p=e=>{let t=v(e);for(let o=t?.closest("details");o;o=o.parentElement?.closest("details"))o.open=!0;return t};var q=()=>{let e=document.querySelector("[data-navigation]"),t=document.querySelector("[data-navigation-drawer]"),o=document.querySelector("[data-navigation-content]"),r=document.querySelector("[data-navigation-open]"),c=document.querySelector("[data-navigation-close]"),n=e?.parentElement;if(!e||!t||!o||!r||!c||!n)return;let l=matchMedia(b),s=()=>{t.open||(document.body.classList.remove("wg_shell__root--drawerOpen"),r.setAttribute("aria-expanded","false"))},u=()=>{t.close(),s()},g=()=>{!l.matches||t.open||(t.showModal(),document.body.classList.add("wg_shell__root--drawerOpen"),r.setAttribute("aria-expanded","true"))},a=i=>{let d=t.getBoundingClientRect();i.target===t&&(i.clientX<d.left||i.clientX>d.right||i.clientY<d.top||i.clientY>d.bottom)&&u()},y=i=>{if(!t.open||i.defaultPrevented||i.button!==0||i.metaKey||i.ctrlKey||i.shiftKey||i.altKey||!(i.target instanceof Element))return;let d=i.target.closest("a[href]");if(!(!(d instanceof HTMLAnchorElement)||d.target!==""&&d.target!=="_self"||d.hasAttribute("download"))&&(u(),d.origin===location.origin&&d.pathname===location.pathname&&d.search===location.search&&d.hash)){let f=p(d.hash);f&&(f.tabIndex=-1,f.focus({preventScroll:!0}))}},m=()=>{let i=document.activeElement;if(l.matches){r.classList.add("wg_shell__menuToggle--visible"),e.parentElement!==o&&o.append(e),i instanceof HTMLElement&&e.contains(i)&&r.focus({preventScroll:!0});return}u(),e.parentElement!==n&&n.append(e),i instanceof HTMLElement&&e.contains(i)?i.focus({preventScroll:!0}):(i===r||i instanceof HTMLElement&&t.contains(i))&&n.querySelector(".wg_shell__brand")?.focus({preventScroll:!0}),r.classList.remove("wg_shell__menuToggle--visible")};r.addEventListener("click",g),c.addEventListener("click",u),t.addEventListener("close",s),t.addEventListener("click",a),e.addEventListener("click",y),l.addEventListener("change",m),m(),n.classList.add("wg_shell__sidebar--navigationReady")};var C=e=>{let t=document.querySelector("[data-navigation]"),o=document.querySelector('[data-navigation-scroll="documents"]'),r=document.querySelector('[data-navigation-scroll="outline"]');if(!(!t||!o||!r))try{let c=sessionStorage.getItem(e);if(c===null)return;let n=JSON.parse(c),l=[...t.querySelectorAll(".wg_shellNav__root a[href]")].map(s=>`${s.pathname}:${s.textContent}`).join(`
2
- `);if(typeof n!="object"||n===null||!("navigation"in n)||n.navigation!==l)return;"documents"in n&&typeof n.documents=="number"&&Number.isFinite(n.documents)&&n.documents>=0&&(o.scrollTop=n.documents),"pathname"in n&&n.pathname===location.pathname&&"outline"in n&&typeof n.outline=="number"&&Number.isFinite(n.outline)&&n.outline>=0&&(r.scrollTop=n.outline)}catch{}};var N=()=>{let e=document.querySelector("[data-navigation]"),t=document.querySelector('[data-navigation-scroll="documents"]'),o=document.querySelector('[data-navigation-scroll="outline"]'),r=document.querySelector("[data-navigation-drawer]"),c=document.querySelector("[data-navigation-open]");if(!e||!t||!o||!r||!c)return;let n=matchMedia(b),l={mobile:n.matches},s=[t,o],u=new Map,g=[...e.querySelectorAll(".wg_shellNav__root a[href]")].map(m=>`${m.pathname}:${m.textContent}`).join(`
3
- `),a=()=>{if(l.mobile!==n.matches||n.matches&&!r.open)return;let m=`${S}:${n.matches?"drawer":"desktop"}`;u.set(m,new Map(s.map(i=>[i,i.scrollTop])));try{sessionStorage.setItem(m,JSON.stringify({navigation:g,pathname:location.pathname,documents:t.scrollTop,outline:o.scrollTop}))}catch{}},y=()=>{if(l.mobile=n.matches,n.matches&&!r.open)return;let m=`${S}:${n.matches?"drawer":"desktop"}`,i=u.get(m);if(i===void 0)C(m);else for(let d of s){let f=i.get(d);f!==void 0&&(d.scrollTop=f)}a()};for(let m of s)m.addEventListener("scroll",a,{passive:!0});e.addEventListener("click",a,{capture:!0}),r.addEventListener("cancel",a),r.querySelector("[data-navigation-close]")?.addEventListener("click",a,{capture:!0}),r.addEventListener("click",a,{capture:!0}),c.addEventListener("click",y),n.addEventListener("change",y),addEventListener("pagehide",a),a()};q();N();var h=document.querySelector("[data-cursor-face]");if(h){let e=matchMedia("(hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference)"),t={x:0,y:0,frame:0},o=()=>{cancelAnimationFrame(t.frame),t.frame=0,h.classList.remove("wg_shell__cursorFace--visible")},r=()=>{e.matches||o()},c=()=>{t.frame===0&&(t.frame=requestAnimationFrame(()=>{t.frame=0,h.style.transform=`translate3d(
4
- ${Math.max(0,Math.min(t.x+16,document.documentElement.clientWidth-h.width-16))}px,
5
- ${Math.max(0,Math.min(t.y+16,document.documentElement.clientHeight-h.height-16))}px,
6
- 0
7
- )`,h.classList.add("wg_shell__cursorFace--visible")}))},n=g=>{if(!(e.matches&&g.pointerType==="mouse")){o();return}t.x=g.clientX,t.y=g.clientY,c()},l=()=>{h.classList.contains("wg_shell__cursorFace--visible")&&c()},s=()=>{if(h.classList.contains("wg_shell__cursorFace--visible"))try{sessionStorage.setItem(E,JSON.stringify({x:t.x,y:t.y}))}catch{}},u=()=>{try{let g=sessionStorage.getItem(E);if(sessionStorage.removeItem(E),g!==null&&e.matches){let a=JSON.parse(g);typeof a=="object"&&a!==null&&"x"in a&&"y"in a&&typeof a.x=="number"&&typeof a.y=="number"&&Number.isFinite(a.x)&&Number.isFinite(a.y)&&(t.x=a.x,t.y=a.y,c())}}catch{}};u(),document.addEventListener("pointermove",n,{passive:!0}),document.addEventListener("pointerdown",n,{passive:!0}),document.documentElement.addEventListener("pointerleave",o),addEventListener("blur",o),addEventListener("resize",l),addEventListener("pagehide",s),addEventListener("pageshow",u),e.addEventListener("change",r)}var H=p(location.hash),L=null,w=0,_=null,k=()=>{clearTimeout(w),_?.abort(),_=null,L?.classList.remove("wg_prose__headingText--highlighted"),L=null},A=e=>{k(),L=e===null?null:e.querySelector(".wg_prose__headingText"),L!==null&&(L.classList.add("wg_prose__headingText--highlighted"),w=window.setTimeout(k,3e3),_=new AbortController,document.addEventListener("scrollend",O,{signal:_.signal}))},O=e=>{e.target===document&&(clearTimeout(w),_?.abort(),_=null,w=window.setTimeout(k,3e3))},K=e=>{if(!(e.defaultPrevented||e.button!==0||e.metaKey||e.ctrlKey||e.shiftKey||e.altKey)&&e.currentTarget instanceof HTMLAnchorElement&&(e.currentTarget.target===""||e.currentTarget.target==="_self")&&!e.currentTarget.hasAttribute("download")&&e.currentTarget.origin===location.origin&&e.currentTarget.pathname===location.pathname&&e.currentTarget.search===location.search&&e.currentTarget.hash){let t=p(e.currentTarget.hash);e.currentTarget.hash===location.hash&&A(t)}},P=()=>{let e=p(location.hash);e?.scrollIntoView(),A(e)};for(let e of document.querySelectorAll("a[href]"))e.addEventListener("click",K);addEventListener("hashchange",P);var M=[...document.querySelectorAll("[data-toc-link]")].flatMap(e=>{let t=v(e.hash),o=e.parentElement;return!t||!o?[]:[{heading:t,link:e,item:o,list:o.parentElement,subs:[...o.querySelectorAll("[data-toc-sub-link]")].flatMap(r=>{let c=v(r.hash),n=r.parentElement;return c&&n?[{heading:c,link:r,item:n}]:[]})}]}),x=M.at(0);if(x){let e=()=>{let t=Number.parseFloat(getComputedStyle(x.heading).scrollMarginTop)+1,o=M.findLast(l=>l.heading.checkVisibility()&&l.heading.getBoundingClientRect().top<=t)??x,r=o.subs.findLast(l=>l.heading.checkVisibility()&&l.heading.getBoundingClientRect().top<=t),c=M.indexOf(o),n=r===void 0?-1:o.subs.indexOf(r);for(let[l,s]of M.entries()){let u=s===o&&r===void 0;s.link.classList.toggle("wg_shellToc__link--active",u),u?s.link.setAttribute("aria-current","location"):s.link.removeAttribute("aria-current"),s.item.classList.toggle("wg_shellToc__item--trail",s.list===o.list&&l<c),s.item.classList.toggle("wg_shellToc__item--current",s===o);for(let[g,a]of s.subs.entries())a.link.classList.toggle("wg_shellToc__subLink--active",a===r),a===r?a.link.setAttribute("aria-current","location"):a.link.removeAttribute("aria-current"),a.item.classList.toggle("wg_shellToc__subItem--trail",s===o&&g<n),a.item.classList.toggle("wg_shellToc__subItem--current",a===r)}};addEventListener("scroll",e,{passive:!0}),addEventListener("resize",e),addEventListener("toggle",e,!0),e()}var B=new AbortController;for(let e of["wheel","touchmove","keydown","mousedown"])addEventListener(e,()=>B.abort(),{once:!0,passive:!0});var I=async()=>{await document.fonts.ready,H&&!B.signal.aborted&&(H.scrollIntoView({behavior:"instant"}),A(H)),document.documentElement.style.removeProperty("scroll-behavior")};document.readyState==="complete"?I():addEventListener("load",I,{once:!0});