openmanual 0.8.2 → 0.10.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.
@@ -1,7 +1,9 @@
1
1
  "use client";
2
- import { jsx } from "react/jsx-runtime";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { LocateFixed, RotateCcw, X, ZoomIn, ZoomOut } from "lucide-react";
4
+ import Panzoom from "@panzoom/panzoom";
3
5
  import { useTheme } from "next-themes";
4
- import { use, useEffect, useId, useState } from "react";
6
+ import { use, useCallback, useEffect, useId, useRef, useState } from "react";
5
7
  //#region src/components/mermaid.tsx
6
8
  function Mermaid({ chart }) {
7
9
  const [mounted, setMounted] = useState(false);
@@ -22,6 +24,20 @@ function cachePromise(key, setPromise) {
22
24
  function MermaidContent({ chart }) {
23
25
  const id = useId();
24
26
  const { resolvedTheme } = useTheme();
27
+ const [dialogState, setDialogState] = useState("closed");
28
+ const contentRef = useRef(null);
29
+ const overlayRef = useRef(null);
30
+ const prefersReducedMotion = useRef(false);
31
+ const pzRef = useRef(null);
32
+ useEffect(() => {
33
+ const mql = window.matchMedia("(prefers-reduced-motion: reduce)");
34
+ prefersReducedMotion.current = mql.matches;
35
+ const handler = (e) => {
36
+ prefersReducedMotion.current = e.matches;
37
+ };
38
+ mql.addEventListener("change", handler);
39
+ return () => mql.removeEventListener("change", handler);
40
+ }, []);
25
41
  const mermaid = use(cachePromise("mermaid", () => import("mermaid").then((m) => m.default)));
26
42
  mermaid.initialize({
27
43
  startOnLoad: false,
@@ -33,12 +49,183 @@ function MermaidContent({ chart }) {
33
49
  const { svg, bindFunctions } = use(cachePromise(`${chart}-${resolvedTheme}`, () => {
34
50
  return mermaid.render(id, chart.replaceAll("\\n", "\n"));
35
51
  }));
36
- return /* @__PURE__ */ jsx("div", {
37
- ref: (container) => {
38
- if (container) bindFunctions?.(container);
52
+ const handleOpen = useCallback(() => {
53
+ if (dialogState === "opening" || dialogState === "open") return;
54
+ setDialogState("opening");
55
+ }, [dialogState]);
56
+ const handleClose = useCallback(() => {
57
+ if (dialogState === "closed" || dialogState === "closing") return;
58
+ setDialogState("closing");
59
+ }, [dialogState]);
60
+ useEffect(() => {
61
+ if (dialogState === "opening") requestAnimationFrame(() => {
62
+ requestAnimationFrame(() => {
63
+ setDialogState("open");
64
+ });
65
+ });
66
+ }, [dialogState]);
67
+ useEffect(() => {
68
+ if (dialogState !== "closing") return;
69
+ const fallbackTimer = setTimeout(() => {
70
+ setDialogState("closed");
71
+ }, 250);
72
+ return () => clearTimeout(fallbackTimer);
73
+ }, [dialogState]);
74
+ const handleOverlayTransitionEnd = useCallback((e) => {
75
+ if (e.target !== overlayRef.current) return;
76
+ if (dialogState === "closing") setDialogState("closed");
77
+ }, [dialogState]);
78
+ const handleReset = useCallback(() => {
79
+ pzRef.current?.reset();
80
+ }, []);
81
+ const handleZoomIn = useCallback(() => {
82
+ pzRef.current?.zoomIn();
83
+ }, []);
84
+ const handleZoomOut = useCallback(() => {
85
+ pzRef.current?.zoomOut();
86
+ }, []);
87
+ const handleCenter = useCallback(() => {
88
+ pzRef.current?.reset();
89
+ pzRef.current?.pan(0, 0);
90
+ }, []);
91
+ useEffect(() => {
92
+ if (dialogState === "closed" || !contentRef.current) return;
93
+ const pz = Panzoom(contentRef.current, {
94
+ maxScale: 5,
95
+ minScale: .1,
96
+ contain: "outside"
97
+ });
98
+ pzRef.current = pz;
99
+ contentRef.current.parentElement?.addEventListener("wheel", pz.zoomWithWheel);
100
+ return () => {
101
+ pz.destroy();
102
+ contentRef.current?.parentElement?.removeEventListener("wheel", pz.zoomWithWheel);
103
+ };
104
+ }, [dialogState]);
105
+ useEffect(() => {
106
+ if (dialogState === "closed") return;
107
+ const handleEscape = (e) => {
108
+ if (e.key === "Escape") handleClose();
109
+ };
110
+ document.addEventListener("keydown", handleEscape);
111
+ document.body.style.overflow = "hidden";
112
+ return () => {
113
+ document.removeEventListener("keydown", handleEscape);
114
+ document.body.style.overflow = "";
115
+ };
116
+ }, [dialogState, handleClose]);
117
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("button", {
118
+ type: "button",
119
+ className: "block w-full cursor-zoom-in",
120
+ onClick: handleOpen,
121
+ children: /* @__PURE__ */ jsx("div", {
122
+ className: "w-full cursor-zoom-in pointer-events-none",
123
+ ref: (container) => {
124
+ if (container) bindFunctions?.(container);
125
+ },
126
+ dangerouslySetInnerHTML: { __html: svg }
127
+ })
128
+ }), /* @__PURE__ */ jsx("div", {
129
+ ref: overlayRef,
130
+ role: "dialog",
131
+ "aria-modal": dialogState !== "closed" ? "true" : void 0,
132
+ className: `fixed inset-0 z-50 flex items-center justify-center transition-opacity duration-200 ease-out ${dialogState === "closed" ? "pointer-events-none" : ""}`,
133
+ style: {
134
+ backgroundColor: "var(--color-fd-background)",
135
+ opacity: dialogState === "closed" || dialogState === "closing" ? 0 : 1,
136
+ transitionDuration: prefersReducedMotion.current ? "0.01ms" : void 0
39
137
  },
40
- dangerouslySetInnerHTML: { __html: svg }
41
- });
138
+ onClick: handleClose,
139
+ onTransitionEnd: handleOverlayTransitionEnd,
140
+ onKeyDown: (e) => {
141
+ if (e.key === "Escape") handleClose();
142
+ },
143
+ children: /* @__PURE__ */ jsxs("div", {
144
+ role: "document",
145
+ className: "relative overflow-hidden w-full h-full",
146
+ style: {
147
+ width: "100%",
148
+ height: "100%",
149
+ transform: dialogState === "closed" || dialogState === "closing" ? "scale(0.9)" : dialogState === "opening" ? "scale(0.9)" : "scale(1)",
150
+ opacity: dialogState === "closed" || dialogState === "closing" ? 0 : 1,
151
+ transition: dialogState === "closed" ? "none" : `transform ${prefersReducedMotion.current ? "0.01ms" : "200ms"} ease-out, opacity ${prefersReducedMotion.current ? "0.01ms" : "200ms"} ease-out`
152
+ },
153
+ onClick: (e) => e.stopPropagation(),
154
+ onKeyDown: (e) => e.stopPropagation(),
155
+ children: [/* @__PURE__ */ jsxs("div", {
156
+ className: "absolute top-4 left-1/2 z-10 flex -translate-x-1/2 items-center gap-1 rounded-lg px-2 py-1.5 backdrop-blur-md",
157
+ style: { backgroundColor: "var(--color-fd-foreground)" },
158
+ children: [
159
+ /* @__PURE__ */ jsx("button", {
160
+ type: "button",
161
+ onClick: handleZoomIn,
162
+ className: "mermaid-toolbar-btn inline-flex items-center justify-center rounded-md p-1.5 transition-colors cursor-pointer",
163
+ style: {
164
+ color: "var(--color-fd-background)",
165
+ "--hover-bg": "var(--color-fd-background)",
166
+ "--hover-color": "var(--color-fd-foreground)"
167
+ },
168
+ title: "放大",
169
+ children: /* @__PURE__ */ jsx(ZoomIn, { size: 16 })
170
+ }),
171
+ /* @__PURE__ */ jsx("button", {
172
+ type: "button",
173
+ onClick: handleZoomOut,
174
+ className: "mermaid-toolbar-btn inline-flex items-center justify-center rounded-md p-1.5 transition-colors cursor-pointer",
175
+ style: {
176
+ color: "var(--color-fd-background)",
177
+ "--hover-bg": "var(--color-fd-background)",
178
+ "--hover-color": "var(--color-fd-foreground)"
179
+ },
180
+ title: "缩小",
181
+ children: /* @__PURE__ */ jsx(ZoomOut, { size: 16 })
182
+ }),
183
+ /* @__PURE__ */ jsx("button", {
184
+ type: "button",
185
+ onClick: handleReset,
186
+ className: "mermaid-toolbar-btn inline-flex items-center justify-center rounded-md p-1.5 transition-colors cursor-pointer",
187
+ style: {
188
+ color: "var(--color-fd-background)",
189
+ "--hover-bg": "var(--color-fd-background)",
190
+ "--hover-color": "var(--color-fd-foreground)"
191
+ },
192
+ title: "重置缩放",
193
+ children: /* @__PURE__ */ jsx(RotateCcw, { size: 16 })
194
+ }),
195
+ /* @__PURE__ */ jsx("button", {
196
+ type: "button",
197
+ onClick: handleCenter,
198
+ className: "mermaid-toolbar-btn inline-flex items-center justify-center rounded-md p-1.5 transition-colors cursor-pointer",
199
+ style: {
200
+ color: "var(--color-fd-background)",
201
+ "--hover-bg": "var(--color-fd-background)",
202
+ "--hover-color": "var(--color-fd-foreground)"
203
+ },
204
+ title: "定位到中心",
205
+ children: /* @__PURE__ */ jsx(LocateFixed, { size: 16 })
206
+ }),
207
+ /* @__PURE__ */ jsx("div", { className: "mx-1 h-4 w-px bg-white/20" }),
208
+ /* @__PURE__ */ jsx("button", {
209
+ type: "button",
210
+ onClick: handleClose,
211
+ className: "mermaid-toolbar-btn inline-flex items-center justify-center rounded-md p-1.5 transition-colors cursor-pointer",
212
+ style: {
213
+ color: "var(--color-fd-background)",
214
+ "--hover-bg": "var(--color-fd-background)",
215
+ "--hover-color": "var(--color-fd-foreground)"
216
+ },
217
+ title: "关闭",
218
+ children: /* @__PURE__ */ jsx(X, { size: 16 })
219
+ })
220
+ ]
221
+ }), /* @__PURE__ */ jsx("div", {
222
+ ref: contentRef,
223
+ role: "img",
224
+ className: "flex w-full h-full items-center justify-center origin-top-left",
225
+ dangerouslySetInnerHTML: { __html: svg }
226
+ })]
227
+ })
228
+ })] });
42
229
  }
43
230
  //#endregion
44
231
  export { Mermaid };
@@ -4,7 +4,13 @@ import { RootProvider } from "fumadocs-ui/provider/next";
4
4
  //#region src/components/provider.tsx
5
5
  function Provider({ searchEnabled = true, children }) {
6
6
  return /* @__PURE__ */ jsx(RootProvider, {
7
- search: { enabled: searchEnabled },
7
+ search: {
8
+ enabled: searchEnabled,
9
+ options: {
10
+ type: "static",
11
+ api: "/api/search"
12
+ }
13
+ },
8
14
  children
9
15
  });
10
16
  }
@@ -0,0 +1,30 @@
1
+ import * as _$react_jsx_runtime0 from "react/jsx-runtime";
2
+
3
+ //#region src/components/safe-search-dialog.d.ts
4
+ interface SafeSearchDialogProps {
5
+ defaultTag?: string;
6
+ tags?: {
7
+ value: string;
8
+ name: string;
9
+ }[];
10
+ api?: string;
11
+ delayMs?: number;
12
+ type?: 'fetch' | 'static';
13
+ allowClear?: boolean;
14
+ links?: [string, string][];
15
+ footer?: React.ReactNode;
16
+ open?: boolean;
17
+ onOpenChange?: (open: boolean) => void;
18
+ }
19
+ declare function SafeSearchDialog({
20
+ defaultTag,
21
+ tags,
22
+ api,
23
+ delayMs,
24
+ type,
25
+ allowClear,
26
+ links,
27
+ footer
28
+ }: SafeSearchDialogProps): _$react_jsx_runtime0.JSX.Element;
29
+ //#endregion
30
+ export { SafeSearchDialog as default };
@@ -0,0 +1,77 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { useMemo, useState } from "react";
4
+ import { useDocsSearch } from "fumadocs-core/search/client";
5
+ import { useOnChange } from "fumadocs-core/utils/use-on-change";
6
+ import { SearchDialog, SearchDialogClose, SearchDialogContent, SearchDialogFooter, SearchDialogHeader, SearchDialogIcon, SearchDialogInput, SearchDialogList, SearchDialogOverlay, TagsList, TagsListItem, useSearch } from "fumadocs-ui/components/dialog/search";
7
+ import { useI18n } from "fumadocs-ui/contexts/i18n";
8
+ //#region src/components/safe-search-dialog.tsx
9
+ /**
10
+ * SafeSearchDialog - 安全搜索对话框
11
+ *
12
+ * 基于 fumadocs-ui DefaultSearchDialog 复刻,核心修复:
13
+ * 在传递 items 给 SearchDialogList 前增加 Array.isArray 校验,
14
+ * 防止 query.data 为非数组值时触发 `items?.map is not a function` 报错。
15
+ *
16
+ * 对应源码: fumadocs-ui/dist/components/dialog/search-default.js
17
+ * 修复位置: 原 search-default.js 第 48 行
18
+ * 原: items={query.data !== "empty" ? query.data : defaultItems}
19
+ * 新: items={Array.isArray(query.data) ? query.data : defaultItems}
20
+ */
21
+ function SafeSearchDialog({ defaultTag, tags = [], api, delayMs, type = "fetch", allowClear = false, links = [], footer }) {
22
+ const { locale } = useI18n();
23
+ const [tag, setTag] = useState(defaultTag);
24
+ const { open, onOpenChange } = useSearch();
25
+ const { search, setSearch, query } = useDocsSearch(type === "fetch" ? {
26
+ type: "fetch",
27
+ ...api != null && { api },
28
+ ...locale != null && { locale },
29
+ ...tag != null && { tag },
30
+ ...delayMs != null && { delayMs }
31
+ } : {
32
+ type: "static",
33
+ ...api != null && { from: api },
34
+ ...locale != null && { locale },
35
+ ...tag != null && { tag },
36
+ ...delayMs != null && { delayMs }
37
+ });
38
+ const defaultItems = useMemo(() => {
39
+ if (links.length === 0) return null;
40
+ return links.map(([name, link]) => ({
41
+ type: "page",
42
+ id: name,
43
+ content: name,
44
+ url: link
45
+ }));
46
+ }, [links]);
47
+ useOnChange(defaultTag, (v) => {
48
+ setTag(v);
49
+ });
50
+ const safeItems = Array.isArray(query.data) ? query.data : defaultItems;
51
+ return /* @__PURE__ */ jsxs(SearchDialog, {
52
+ open,
53
+ onOpenChange,
54
+ search,
55
+ onSearchChange: setSearch,
56
+ isLoading: query.isLoading,
57
+ children: [
58
+ /* @__PURE__ */ jsx(SearchDialogOverlay, {}),
59
+ /* @__PURE__ */ jsxs(SearchDialogContent, { children: [/* @__PURE__ */ jsxs(SearchDialogHeader, { children: [
60
+ /* @__PURE__ */ jsx(SearchDialogIcon, {}),
61
+ /* @__PURE__ */ jsx(SearchDialogInput, {}),
62
+ /* @__PURE__ */ jsx(SearchDialogClose, {})
63
+ ] }), /* @__PURE__ */ jsx(SearchDialogList, { items: safeItems })] }),
64
+ /* @__PURE__ */ jsxs(SearchDialogFooter, { children: [tags.length > 0 && /* @__PURE__ */ jsx(TagsList, {
65
+ ...tag != null && { tag },
66
+ onTagChange: setTag,
67
+ allowClear,
68
+ children: tags.map((tagItem) => /* @__PURE__ */ jsx(TagsListItem, {
69
+ value: tagItem.value,
70
+ children: tagItem.name
71
+ }, tagItem.value))
72
+ }), footer] })
73
+ ]
74
+ });
75
+ }
76
+ //#endregion
77
+ export { SafeSearchDialog as default };
package/dist/index.d.ts CHANGED
@@ -12,6 +12,7 @@ declare const OpenManualConfigSchema: z.ZodObject<{
12
12
  strict: "strict";
13
13
  all: "all";
14
14
  }>>;
15
+ favicon: z.ZodOptional<z.ZodString>;
15
16
  navbar: z.ZodOptional<z.ZodObject<{
16
17
  logo: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
17
18
  light: z.ZodString;
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ const LogoSchema = z.union([z.string(), z.object({
7
7
  light: z.string(),
8
8
  dark: z.string()
9
9
  })]);
10
+ const FaviconSchema = z.string();
10
11
  const NavbarSchema = z.object({
11
12
  logo: LogoSchema.optional(),
12
13
  github: z.url().optional(),
@@ -42,6 +43,7 @@ const OpenManualConfigSchema = z.object({
42
43
  siteUrl: z.url().optional(),
43
44
  locale: z.string().optional(),
44
45
  contentPolicy: z.enum(["strict", "all"]).optional(),
46
+ favicon: FaviconSchema.optional(),
45
47
  navbar: NavbarSchema.optional(),
46
48
  footer: FooterSchema.optional(),
47
49
  sidebar: z.array(SidebarGroupSchema).optional(),
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/core/config/schema.ts","../src/core/config/loader.ts"],"sourcesContent":["import { z } from 'zod';\n\nexport const LogoSchema = z.union([z.string(), z.object({ light: z.string(), dark: z.string() })]);\n\nexport const NavbarSchema = z.object({\n logo: LogoSchema.optional(),\n github: z.url().optional(),\n links: z\n .array(\n z.object({\n label: z.string(),\n href: z.string(),\n })\n )\n .optional(),\n});\n\nexport const FooterSchema = z.object({\n text: z.string().optional(),\n});\n\nexport const SidebarPageSchema = z.object({\n slug: z.string(),\n title: z.string(),\n icon: z.string().optional(),\n});\n\nexport const SidebarGroupSchema = z.object({\n group: z.string(),\n icon: z.string().optional(),\n collapsed: z.boolean().optional(),\n pages: z.array(SidebarPageSchema),\n});\n\nexport const ThemeSchema = z.object({\n primaryHue: z.number().min(0).max(360).optional(),\n darkMode: z.boolean().optional(),\n});\n\nexport const SearchSchema = z.object({\n enabled: z.boolean().optional(),\n});\n\nexport const MdxSchema = z.object({\n latex: z.boolean().optional(),\n});\n\nexport const PageActionsSchema = z.object({\n enabled: z.boolean().optional(),\n});\n\nexport const OpenManualConfigSchema = z.object({\n name: z.string().min(1),\n description: z.string().optional(),\n contentDir: z.string().optional(),\n outputDir: z.string().optional(),\n siteUrl: z.url().optional(),\n locale: z.string().optional(),\n contentPolicy: z.enum(['strict', 'all']).optional(),\n navbar: NavbarSchema.optional(),\n footer: FooterSchema.optional(),\n sidebar: z.array(SidebarGroupSchema).optional(),\n theme: ThemeSchema.optional(),\n search: SearchSchema.optional(),\n mdx: MdxSchema.optional(),\n pageActions: PageActionsSchema.optional(),\n});\n\nexport type OpenManualConfig = z.infer<typeof OpenManualConfigSchema>;\nexport type NavbarConfig = z.infer<typeof NavbarSchema>;\nexport type FooterConfig = z.infer<typeof FooterSchema>;\nexport type SidebarGroup = z.infer<typeof SidebarGroupSchema>;\nexport type SidebarPage = z.infer<typeof SidebarPageSchema>;\nexport type ThemeConfig = z.infer<typeof ThemeSchema>;\nexport type LogoConfig = z.infer<typeof LogoSchema>;\n\nexport function collectConfiguredSlugs(config: OpenManualConfig): Set<string> {\n const slugs = new Set<string>();\n if (config.sidebar) {\n for (const group of config.sidebar) {\n for (const page of group.pages) {\n slugs.add(page.slug);\n }\n }\n }\n return slugs;\n}\n\nexport function buildTitleMap(config: OpenManualConfig): Record<string, string> {\n const map: Record<string, string> = {};\n if (config.sidebar) {\n for (const group of config.sidebar) {\n for (const page of group.pages) {\n map[page.slug] = page.title;\n }\n }\n }\n return map;\n}\n","import { readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { type OpenManualConfig, OpenManualConfigSchema } from './schema.js';\n\nconst DEFAULT_CONFIG: Partial<OpenManualConfig> = {\n contentDir: 'content',\n outputDir: 'dist',\n locale: 'zh',\n navbar: {},\n footer: {},\n theme: {\n primaryHue: 213,\n darkMode: true,\n },\n search: {\n enabled: true,\n },\n mdx: {},\n pageActions: { enabled: true },\n};\n\nexport async function loadConfig(cwd: string = process.cwd()): Promise<OpenManualConfig> {\n const configPath = join(cwd, 'openmanual.json');\n\n let rawJson: string;\n try {\n rawJson = await readFile(configPath, 'utf-8');\n } catch {\n throw new Error(`openmanual.json not found in ${cwd}. Please create one.`);\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(rawJson);\n } catch {\n throw new Error('openmanual.json is not valid JSON.');\n }\n\n const result = OpenManualConfigSchema.safeParse(parsed);\n if (!result.success) {\n const errors = result.error.issues\n .map((i) => ` - ${i.path.join('.')}: ${i.message}`)\n .join('\\n');\n throw new Error(`openmanual.json validation failed:\\n${errors}`);\n }\n\n return mergeDefaults(result.data);\n}\n\nfunction mergeDefaults(config: OpenManualConfig): OpenManualConfig {\n return {\n ...config,\n contentPolicy: config.contentPolicy ?? 'strict',\n contentDir: config.contentDir ?? DEFAULT_CONFIG.contentDir ?? 'content',\n outputDir: config.outputDir ?? DEFAULT_CONFIG.outputDir ?? 'dist',\n locale: config.locale ?? DEFAULT_CONFIG.locale ?? 'zh',\n navbar: {\n ...DEFAULT_CONFIG.navbar,\n ...config.navbar,\n logo: config.navbar?.logo ?? config.name,\n },\n footer: {\n ...DEFAULT_CONFIG.footer,\n ...config.footer,\n text: config.footer?.text ?? `MIT ${new Date().getFullYear()} © ${config.name}.`,\n },\n theme: {\n ...DEFAULT_CONFIG.theme,\n ...config.theme,\n },\n search: {\n ...DEFAULT_CONFIG.search,\n ...config.search,\n },\n mdx: {\n ...DEFAULT_CONFIG.mdx,\n ...config.mdx,\n },\n pageActions: {\n ...DEFAULT_CONFIG.pageActions,\n ...config.pageActions,\n },\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/core/config/schema.ts","../src/core/config/loader.ts"],"sourcesContent":["import { z } from 'zod';\n\nexport const LogoSchema = z.union([z.string(), z.object({ light: z.string(), dark: z.string() })]);\n\nexport const FaviconSchema = z.string();\n\nexport const NavbarSchema = z.object({\n logo: LogoSchema.optional(),\n github: z.url().optional(),\n links: z\n .array(\n z.object({\n label: z.string(),\n href: z.string(),\n })\n )\n .optional(),\n});\n\nexport const FooterSchema = z.object({\n text: z.string().optional(),\n});\n\nexport const SidebarPageSchema = z.object({\n slug: z.string(),\n title: z.string(),\n icon: z.string().optional(),\n});\n\nexport const SidebarGroupSchema = z.object({\n group: z.string(),\n icon: z.string().optional(),\n collapsed: z.boolean().optional(),\n pages: z.array(SidebarPageSchema),\n});\n\nexport const ThemeSchema = z.object({\n primaryHue: z.number().min(0).max(360).optional(),\n darkMode: z.boolean().optional(),\n});\n\nexport const SearchSchema = z.object({\n enabled: z.boolean().optional(),\n});\n\nexport const MdxSchema = z.object({\n latex: z.boolean().optional(),\n});\n\nexport const PageActionsSchema = z.object({\n enabled: z.boolean().optional(),\n});\n\nexport const OpenManualConfigSchema = z.object({\n name: z.string().min(1),\n description: z.string().optional(),\n contentDir: z.string().optional(),\n outputDir: z.string().optional(),\n siteUrl: z.url().optional(),\n locale: z.string().optional(),\n contentPolicy: z.enum(['strict', 'all']).optional(),\n favicon: FaviconSchema.optional(),\n navbar: NavbarSchema.optional(),\n footer: FooterSchema.optional(),\n sidebar: z.array(SidebarGroupSchema).optional(),\n theme: ThemeSchema.optional(),\n search: SearchSchema.optional(),\n mdx: MdxSchema.optional(),\n pageActions: PageActionsSchema.optional(),\n});\n\nexport type OpenManualConfig = z.infer<typeof OpenManualConfigSchema>;\nexport type NavbarConfig = z.infer<typeof NavbarSchema>;\nexport type FooterConfig = z.infer<typeof FooterSchema>;\nexport type SidebarGroup = z.infer<typeof SidebarGroupSchema>;\nexport type SidebarPage = z.infer<typeof SidebarPageSchema>;\nexport type ThemeConfig = z.infer<typeof ThemeSchema>;\nexport type LogoConfig = z.infer<typeof LogoSchema>;\nexport type FaviconConfig = z.infer<typeof FaviconSchema>;\n\nexport function collectConfiguredSlugs(config: OpenManualConfig): Set<string> {\n const slugs = new Set<string>();\n if (config.sidebar) {\n for (const group of config.sidebar) {\n for (const page of group.pages) {\n slugs.add(page.slug);\n }\n }\n }\n return slugs;\n}\n\nexport function buildTitleMap(config: OpenManualConfig): Record<string, string> {\n const map: Record<string, string> = {};\n if (config.sidebar) {\n for (const group of config.sidebar) {\n for (const page of group.pages) {\n map[page.slug] = page.title;\n }\n }\n }\n return map;\n}\n","import { readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { type OpenManualConfig, OpenManualConfigSchema } from './schema.js';\n\nconst DEFAULT_CONFIG: Partial<OpenManualConfig> = {\n contentDir: 'content',\n outputDir: 'dist',\n locale: 'zh',\n navbar: {},\n footer: {},\n theme: {\n primaryHue: 213,\n darkMode: true,\n },\n search: {\n enabled: true,\n },\n mdx: {},\n pageActions: { enabled: true },\n};\n\nexport async function loadConfig(cwd: string = process.cwd()): Promise<OpenManualConfig> {\n const configPath = join(cwd, 'openmanual.json');\n\n let rawJson: string;\n try {\n rawJson = await readFile(configPath, 'utf-8');\n } catch {\n throw new Error(`openmanual.json not found in ${cwd}. Please create one.`);\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(rawJson);\n } catch {\n throw new Error('openmanual.json is not valid JSON.');\n }\n\n const result = OpenManualConfigSchema.safeParse(parsed);\n if (!result.success) {\n const errors = result.error.issues\n .map((i) => ` - ${i.path.join('.')}: ${i.message}`)\n .join('\\n');\n throw new Error(`openmanual.json validation failed:\\n${errors}`);\n }\n\n return mergeDefaults(result.data);\n}\n\nfunction mergeDefaults(config: OpenManualConfig): OpenManualConfig {\n return {\n ...config,\n contentPolicy: config.contentPolicy ?? 'strict',\n contentDir: config.contentDir ?? DEFAULT_CONFIG.contentDir ?? 'content',\n outputDir: config.outputDir ?? DEFAULT_CONFIG.outputDir ?? 'dist',\n locale: config.locale ?? DEFAULT_CONFIG.locale ?? 'zh',\n navbar: {\n ...DEFAULT_CONFIG.navbar,\n ...config.navbar,\n logo: config.navbar?.logo ?? config.name,\n },\n footer: {\n ...DEFAULT_CONFIG.footer,\n ...config.footer,\n text: config.footer?.text ?? `MIT ${new Date().getFullYear()} © ${config.name}.`,\n },\n theme: {\n ...DEFAULT_CONFIG.theme,\n ...config.theme,\n },\n search: {\n ...DEFAULT_CONFIG.search,\n ...config.search,\n },\n mdx: {\n ...DEFAULT_CONFIG.mdx,\n ...config.mdx,\n },\n pageActions: {\n ...DEFAULT_CONFIG.pageActions,\n ...config.pageActions,\n },\n };\n}\n"],"mappings":"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"}
@@ -3,12 +3,14 @@ import * as PageTree from "fumadocs-core/page-tree";
3
3
  //#region src/utils/restructure-tree.d.ts
4
4
  interface SidebarConfigEntry {
5
5
  group: string;
6
+ icon?: string;
6
7
  collapsed?: boolean;
7
8
  pages: readonly {
8
9
  slug: string;
10
+ icon?: string;
9
11
  }[];
10
12
  }
11
13
  declare function slugToUrl(slug: string): string;
12
- declare function restructureTree(tree: PageTree.Root, sidebarConfig: readonly SidebarConfigEntry[]): PageTree.Root;
14
+ declare function restructureTree(tree: PageTree.Root, sidebarConfig: readonly SidebarConfigEntry[], iconMap?: Record<string, React.ReactNode>): PageTree.Root;
13
15
  //#endregion
14
16
  export { SidebarConfigEntry, restructureTree, slugToUrl };
@@ -2,7 +2,7 @@
2
2
  function slugToUrl(slug) {
3
3
  return slug === "index" ? "/" : `/${slug}`;
4
4
  }
5
- function restructureTree(tree, sidebarConfig) {
5
+ function restructureTree(tree, sidebarConfig, iconMap) {
6
6
  const consumed = /* @__PURE__ */ new Set();
7
7
  const newChildren = [];
8
8
  const children = tree.children ?? [];
@@ -13,13 +13,17 @@ function restructureTree(tree, sidebarConfig) {
13
13
  const idx = children.findIndex((c, i) => !consumed.has(i) && c.type === "page" && c.url === url);
14
14
  if (idx >= 0) {
15
15
  const node = children[idx];
16
- if (node) folderChildren.push(node);
16
+ if (node) folderChildren.push(page.icon && iconMap?.[page.icon] ? {
17
+ ...node,
18
+ icon: iconMap[page.icon]
19
+ } : node);
17
20
  consumed.add(idx);
18
21
  }
19
22
  }
20
23
  if (folderChildren.length > 0) newChildren.push({
21
24
  type: "folder",
22
25
  name: group.group,
26
+ icon: group.icon && iconMap ? iconMap[group.icon] : void 0,
23
27
  defaultOpen: !group.collapsed,
24
28
  children: folderChildren
25
29
  });
@@ -29,10 +33,26 @@ function restructureTree(tree, sidebarConfig) {
29
33
  const idx = children.findIndex((child, i) => !consumed.has(i) && child.type === "folder" && child.children?.some((c) => c.type === "page" && c.url?.startsWith(`/${dirPrefix}/`)));
30
34
  if (idx >= 0) {
31
35
  consumed.add(idx);
36
+ const originalFolder = children[idx];
37
+ const childrenWithIcons = (originalFolder.children ?? []).map((child) => {
38
+ if (child.type === "page") {
39
+ const matchedPage = group.pages.find((p) => {
40
+ const url = slugToUrl(p.slug);
41
+ return child.url === url;
42
+ });
43
+ if (matchedPage?.icon && iconMap?.[matchedPage.icon]) return {
44
+ ...child,
45
+ icon: iconMap[matchedPage.icon]
46
+ };
47
+ }
48
+ return child;
49
+ });
32
50
  newChildren.push({
33
- ...children[idx],
51
+ ...originalFolder,
34
52
  name: group.group,
35
- defaultOpen: !group.collapsed
53
+ icon: group.icon && iconMap ? iconMap[group.icon] : void 0,
54
+ defaultOpen: !group.collapsed,
55
+ children: childrenWithIcons
36
56
  });
37
57
  }
38
58
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openmanual",
3
- "version": "0.8.2",
3
+ "version": "0.10.0",
4
4
  "author": "shenjingnan <sjn.code@gmail.com>",
5
5
  "description": "AI 友好的开源文档系统框架",
6
6
  "type": "module",
@@ -51,6 +51,7 @@
51
51
  "url": "https://github.com/shenjingnan/openmanual.git"
52
52
  },
53
53
  "dependencies": {
54
+ "@panzoom/panzoom": "^4.6.2",
54
55
  "chokidar": "^5.0.0",
55
56
  "commander": "^14.0.3",
56
57
  "fast-glob": "^3.3.3",
@@ -64,8 +65,10 @@
64
65
  "zod": "^4.0.0"
65
66
  },
66
67
  "peerDependencies": {
68
+ "lucide-react": ">=0.400.0",
67
69
  "mermaid": ">=11.0.0",
68
- "next-themes": ">=0.4.0"
70
+ "next-themes": ">=0.4.0",
71
+ "tailwind-merge": ">=2.0.0"
69
72
  },
70
73
  "devDependencies": {
71
74
  "@biomejs/biome": "^2.4.9",
@@ -86,7 +89,7 @@
86
89
  },
87
90
  "scripts": {
88
91
  "build": "tsdown",
89
- "dev": "concurrently -n tsdown,dev -c blue,green \"tsdown --watch\" \"sleep 2 && OPENMANUAL_ROOT=$(pwd) node dist/bin.js dev --watch --cwd docs\"",
92
+ "dev": "tsdown --watch",
90
93
  "dev:cli": "tsx watch src/cli/bin.ts",
91
94
  "test": "vitest run",
92
95
  "test:watch": "vitest",