sdocs 0.0.59 → 0.0.61

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
@@ -7,6 +7,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.0.61] - 2026-07-06
11
+
12
+ ### Added
13
+
14
+ - **Sections: full documentation sites in the Explorer.** An `@Section/`
15
+ prefix on any entity title (`[PAGE title="@Guides/Installation"]`) groups
16
+ docs under a full-width top bar, each section with its own sidebar — mix
17
+ pages-only sections with component sections freely. Docs without a prefix
18
+ land in a configurable default section (`defaultSection`, default
19
+ `Docs`); the bar only appears once a second section exists, so existing
20
+ projects look unchanged. Tab order comes from the new `sections` config
21
+ (unlisted sections follow alphabetically).
22
+ - **Real URLs (history routing).** Doc routes are now slugified paths —
23
+ `/components/button/sizes` instead of `#/Components/Button/Sizes`. The
24
+ CLI dev server serves the shell for any path, `sdocs build` emits an
25
+ `index.html` per route (deep links work on GitHub Pages with no rewrite
26
+ rules), and old `#/` bookmarks translate on load. Embedding keeps hash
27
+ URLs by default; the new `routing` config/prop switches either way, and
28
+ section-less links resolve into the default section.
29
+
30
+ ### Changed
31
+
32
+ - **Config/prop rename: `title` + `logo`.** `title` is the header text
33
+ (was `logo`), `logo` the mascot image (was `icon`). Old configs with an
34
+ `icon` key are mapped automatically (with a console note); the `icon`
35
+ Explorer prop still works as an alias.
36
+
37
+ ## [0.0.60] - 2026-07-06
38
+
39
+ ### Added
40
+
41
+ - **The page table of contents highlights the current section.** As the
42
+ page scrolls, the "On this page" entry for the section in view lights up;
43
+ clicking an entry highlights it immediately and holds it through the
44
+ smooth scroll.
45
+
10
46
  ## [0.0.59] - 2026-07-05
11
47
 
12
48
  ### Changed
@@ -1,9 +1,14 @@
1
1
  import { resolve } from 'node:path';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
3
  import { build } from 'vite';
3
4
  import { svelte } from '@sveltejs/vite-plugin-svelte';
4
5
  import { loadConfig } from '../server/config.js';
5
6
  import { sdocsPlugin } from '../vite.js';
6
7
  import { generateBuildFiles, cleanBuildFiles } from '../server/app-gen.js';
8
+ import { discoverDocFiles } from '../server/discovery.js';
9
+ import { parseSdoc } from '../language/index.js';
10
+ import { planEntitySnippets } from '../server/doc-model.js';
11
+ import { buildSections } from '../explorer/tree-builder.js';
7
12
  import { svelteDedupe } from './dev.js';
8
13
  export async function buildCommand() {
9
14
  const cwd = process.cwd();
@@ -36,9 +41,52 @@ export async function buildCommand() {
36
41
  },
37
42
  },
38
43
  });
44
+ // History routing: every route gets a physical index.html (a copy of the
45
+ // shell — asset URLs are root-absolute), so deep links work on any
46
+ // static host with no rewrite rules.
47
+ if ((config.routing ?? 'history') === 'history') {
48
+ const count = await emitRoutePages(config, cwd);
49
+ console.log(`[sdocs] Emitted ${count} route page(s)`);
50
+ }
39
51
  console.log(`[sdocs] Build complete → dist/`);
40
52
  }
41
53
  finally {
42
54
  await cleanBuildFiles(sdocsDir);
43
55
  }
44
56
  }
57
+ /** Re-derive the route map from the doc files and copy the shell into each route. */
58
+ async function emitRoutePages(config, cwd) {
59
+ const files = await discoverDocFiles(config.include, cwd);
60
+ const stubs = [];
61
+ for (const filePath of files) {
62
+ const doc = parseSdoc(await readFile(filePath, 'utf-8'));
63
+ for (const entity of doc.entities) {
64
+ stubs.push({
65
+ kind: entity.kind === 'DOCS' ? 'component' : entity.kind === 'PAGE' ? 'page' : 'layout',
66
+ filePath,
67
+ entitySlug: entity.slug,
68
+ meta: { title: entity.title },
69
+ previews: [],
70
+ examples: entity.kind === 'DOCS'
71
+ ? planEntitySnippets(entity)
72
+ .filter((s) => s.role === 'example')
73
+ .map((s) => ({ name: s.name, slug: s.slug, role: s.role, body: '' }))
74
+ : [],
75
+ content: null,
76
+ });
77
+ }
78
+ }
79
+ const map = buildSections(stubs, config.sidebar, {
80
+ defaultSection: config.defaultSection,
81
+ order: config.sections,
82
+ });
83
+ const shell = await readFile(resolve(cwd, 'dist/index.html'), 'utf-8');
84
+ let count = 0;
85
+ for (const key of map.routes.keys()) {
86
+ const dir = resolve(cwd, 'dist', key);
87
+ await mkdir(dir, { recursive: true });
88
+ await writeFile(resolve(dir, 'index.html'), shell);
89
+ count++;
90
+ }
91
+ return count;
92
+ }
@@ -18,11 +18,14 @@ export default {
18
18
  // Or named stylesheets:
19
19
  // css: { light: './src/styles/light.css', dark: './src/styles/dark.css' },
20
20
 
21
- // Sidebar logo text (default: 'sdocs')
21
+ // Header title text (default: 'sdocs')
22
+ // title: 'sdocs',
23
+
24
+ // Header logo: 'sdocs' for the mascot, an image URL, or false to hide (default: 'sdocs')
22
25
  // logo: 'sdocs',
23
26
 
24
- // Sidebar logo icon: 'sdocs' for the mascot, an image URL, or false to hide (default: 'sdocs')
25
- // icon: 'sdocs',
27
+ // Top-bar section order sections come from @Section/ title prefixes
28
+ // sections: ['Guides', 'Components'],
26
29
 
27
30
  // Sidebar configuration
28
31
  // sidebar: {
@@ -1,8 +1,9 @@
1
1
  <script lang="ts">
2
2
  import type { DocEntry } from '../types.js';
3
- import { initRouter, getPath } from './router.svelte.js';
4
- import { buildTree, findDocByPath } from './tree-builder.js';
3
+ import { initRouter, getRoute, navigate, type RoutingMode } from './router.svelte.js';
4
+ import { buildSections, resolveRoute } from './tree-builder.js';
5
5
  import Sidebar from './views/Sidebar.svelte';
6
+ import TopBar from './views/TopBar.svelte';
6
7
  import ComponentView from './views/ComponentView.svelte';
7
8
  import PageView from './views/PageView.svelte';
8
9
  import LayoutView from './views/LayoutView.svelte';
@@ -14,7 +15,11 @@
14
15
 
15
16
  interface Props {
16
17
  docs: DocEntry[];
17
- logo?: string;
18
+ /** Header title text */
19
+ title?: string;
20
+ /** Header logo: 'sdocs' for the built-in mascot, an image URL, or false to hide */
21
+ logo?: string | false;
22
+ /** @deprecated pre-0.0.61 name for `logo` */
18
23
  icon?: string | false;
19
24
  cssNames?: string[];
20
25
  /** URL prefix for preview pages when the host app is served under a sub-path (e.g. SvelteKit's base). */
@@ -25,20 +30,43 @@
25
30
  order?: Record<string, string[]>;
26
31
  open?: string[];
27
32
  };
33
+ /** Top-bar section order (unlisted sections follow alphabetically) */
34
+ sections?: string[];
35
+ /** Section for docs without an `@Section/` title prefix. Default: 'Docs' */
36
+ defaultSection?: string;
37
+ /** 'history' for real paths (server must fall back to the shell),
38
+ * 'hash' for #/ URLs. Embedded default: 'hash'. */
39
+ routing?: RoutingMode;
40
+ /** Path prefix for history-mode routes (host app sub-path) */
41
+ basePath?: string;
28
42
  }
29
43
 
30
44
  let {
31
45
  docs,
46
+ title,
32
47
  logo = 'sdocs',
33
- icon = 'sdocs',
48
+ icon,
34
49
  cssNames = [],
35
50
  previewBase = '',
36
51
  pageModules = {},
37
- sidebarConfig
52
+ sidebarConfig,
53
+ sections,
54
+ defaultSection = 'Docs',
55
+ routing = 'hash',
56
+ basePath = ''
38
57
  }: Props = $props();
39
58
 
40
59
  setContext('sdocs-preview-base', previewBase);
41
60
 
61
+ // Pre-0.0.61 props: `logo` was the header text and `icon` the image. An
62
+ // `icon` prop — or a logo value that can't be an asset path — is the old
63
+ // shape; map it onto the new semantics so embedded apps keep rendering.
64
+ const logoLooksLikeText = $derived(
65
+ typeof logo === 'string' && logo !== 'sdocs' && !/[./:]/.test(logo),
66
+ );
67
+ const headerTitle = $derived(title ?? (logoLooksLikeText ? (logo as string) : 'sdocs'));
68
+ const headerLogo = $derived(icon !== undefined ? icon : logoLooksLikeText ? 'sdocs' : logo);
69
+
42
70
  let sidebarHidden = $state(false);
43
71
  let activeStylesheet = $state<string | undefined>(undefined);
44
72
  let theme = $state<ThemeMode>('light');
@@ -51,7 +79,7 @@
51
79
  });
52
80
 
53
81
  onMount(() => {
54
- initRouter();
82
+ initRouter(routing, basePath);
55
83
  const saved = localStorage.getItem('sdocs-theme') as ThemeMode | null;
56
84
  if (saved && (saved === 'light' || saved === 'dark')) {
57
85
  theme = saved;
@@ -66,43 +94,89 @@
66
94
  localStorage.setItem('sdocs-theme', theme);
67
95
  });
68
96
 
69
- const currentPath = $derived(getPath());
70
- const tree = $derived(buildTree(docs, sidebarConfig));
71
- const resolved = $derived(findDocByPath(docs, currentPath));
97
+ const currentRoute = $derived(getRoute());
98
+ const sectionMap = $derived(
99
+ buildSections(docs, sidebarConfig, { defaultSection, order: sections }),
100
+ );
101
+ const showTopBar = $derived(sectionMap.sections.length > 1);
102
+ const activeSection = $derived(
103
+ (sectionMap.active
104
+ ? sectionMap.sections.find((s) => s.slug === currentRoute[0])
105
+ : undefined) ??
106
+ sectionMap.sections.find((s) => s.isDefault) ??
107
+ sectionMap.sections[0],
108
+ );
109
+ const resolved = $derived(resolveRoute(sectionMap, currentRoute));
110
+
111
+ /** History mode: internal <a> clicks route client-side instead of reloading. */
112
+ function onLinkClick(e: MouseEvent) {
113
+ if (routing !== 'history') return;
114
+ if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey)
115
+ return;
116
+ const anchor = (e.target as Element | null)?.closest?.('a');
117
+ if (!anchor || anchor.target || anchor.hasAttribute('download')) return;
118
+ const href = anchor.getAttribute('href');
119
+ if (!href || href.startsWith('#')) return;
120
+ const url = new URL(anchor.href, location.href);
121
+ if (url.origin !== location.origin) return;
122
+ const base = basePath.replace(/\/$/, '');
123
+ if (base && !url.pathname.startsWith(base + '/')) return;
124
+ e.preventDefault();
125
+ navigate(url.pathname.slice(base.length).split('/').filter(Boolean).map(decodeURIComponent));
126
+ }
72
127
  </script>
73
128
 
74
- <div class="sdocs-app">
75
- {#if !sidebarHidden}
76
- <Sidebar
77
- {tree}
78
- {currentPath}
79
- {logo}
80
- {icon}
129
+ <svelte:window onclick={onLinkClick} />
130
+
131
+ <div class="sdocs-app" class:sdocs-app-with-topbar={showTopBar}>
132
+ {#if showTopBar}
133
+ <TopBar
134
+ title={headerTitle}
135
+ logo={headerLogo}
136
+ sections={sectionMap.sections}
137
+ activeSlug={activeSection?.slug}
81
138
  {cssNames}
82
139
  {activeStylesheet}
83
140
  {theme}
84
- onToggleFullscreen={() => sidebarHidden = true}
85
- onStylesheetChange={(name) => activeStylesheet = name}
86
- onThemeChange={(t) => theme = t}
141
+ onToggleFullscreen={() => (sidebarHidden = true)}
142
+ onStylesheetChange={(name) => (activeStylesheet = name)}
143
+ onThemeChange={(t) => (theme = t)}
87
144
  />
88
- {:else}
89
- <button class="sdocs-exit-fullscreen" onclick={() => sidebarHidden = false}>
90
- &#9664; Exit fullscreen
91
- </button>
92
145
  {/if}
93
- <main class="sdocs-main" class:sdocs-main-fullscreen={sidebarHidden}>
94
- {#if resolved}
95
- {#if resolved.doc.kind === 'page'}
96
- <PageView doc={resolved.doc} {activeStylesheet} {pageModules} />
97
- {:else if resolved.doc.kind === 'layout'}
98
- <LayoutView doc={resolved.doc} {activeStylesheet} />
99
- {:else}
100
- <ComponentView doc={resolved.doc} snippetName={resolved.snippetName} {activeStylesheet} />
101
- {/if}
146
+ <div class="sdocs-body">
147
+ {#if !sidebarHidden}
148
+ <Sidebar
149
+ tree={activeSection?.tree ?? []}
150
+ {currentRoute}
151
+ title={headerTitle}
152
+ logo={headerLogo}
153
+ showHeader={!showTopBar}
154
+ cssNames={showTopBar ? [] : cssNames}
155
+ {activeStylesheet}
156
+ {theme}
157
+ onToggleFullscreen={() => (sidebarHidden = true)}
158
+ onStylesheetChange={(name) => (activeStylesheet = name)}
159
+ onThemeChange={(t) => (theme = t)}
160
+ />
102
161
  {:else}
103
- <HomePage {docs} {logo} />
162
+ <button class="sdocs-exit-fullscreen" onclick={() => (sidebarHidden = false)}>
163
+ &#9664; Exit fullscreen
164
+ </button>
104
165
  {/if}
105
- </main>
166
+ <main class="sdocs-main" class:sdocs-main-fullscreen={sidebarHidden}>
167
+ {#if resolved}
168
+ {#if resolved.doc.kind === 'page'}
169
+ <PageView doc={resolved.doc} {activeStylesheet} {pageModules} />
170
+ {:else if resolved.doc.kind === 'layout'}
171
+ <LayoutView doc={resolved.doc} {activeStylesheet} />
172
+ {:else}
173
+ <ComponentView doc={resolved.doc} snippetName={resolved.snippetName} {activeStylesheet} />
174
+ {/if}
175
+ {:else}
176
+ <HomePage {docs} title={headerTitle} logo={headerLogo} />
177
+ {/if}
178
+ </main>
179
+ </div>
106
180
  </div>
107
181
 
108
182
  <style>
@@ -110,10 +184,16 @@
110
184
  margin: 0;
111
185
  padding: 0;
112
186
  display: flex;
187
+ flex-direction: column;
113
188
  height: 100vh;
114
189
  overflow: hidden;
115
190
  position: relative;
116
191
  }
192
+ .sdocs-body {
193
+ display: flex;
194
+ flex: 1;
195
+ min-height: 0;
196
+ }
117
197
  .sdocs-main {
118
198
  flex: 1;
119
199
  overflow-y: auto;
@@ -1,11 +1,23 @@
1
- /** Hash-based router for sdocs. Uses #/Segment/Segment/... format. */
2
- /** Get the current path segments */
3
- export declare function getPath(): string[];
4
- /** Navigate to a path */
5
- export declare function navigate(segments: string[]): void;
6
- /** Initialize the router call once on app startup */
7
- export declare function initRouter(): void;
8
- /** Build a title path from meta.title (e.g. 'Demo / Button' ['Demo', 'Button']) */
9
- export declare function titleToPath(title: string): string[];
10
- /** Build a hash string from a title path (for href attributes) */
11
- export declare function pathToHash(segments: string[]): string;
1
+ /**
2
+ * Router for the Explorer. Two modes over the same slug-segment routes:
3
+ * - 'history': real paths (/guides/installation). The standalone CLI default —
4
+ * its dev server and static build guarantee every path serves the shell.
5
+ * - 'hash': #/guides/installation. The embedded default — works under any
6
+ * host routing without server cooperation.
7
+ */
8
+ export type RoutingMode = 'history' | 'hash';
9
+ /** Get the current route segments */
10
+ export declare function getRoute(): string[];
11
+ /** Navigate to a route (segments are slugs) */
12
+ export declare function navigate(segments: string[], opts?: {
13
+ replace?: boolean;
14
+ }): void;
15
+ /** Href string for a route, in the active mode (for <a href>) */
16
+ export declare function routeHref(segments: string[]): string;
17
+ /**
18
+ * Initialize the router — call once on app startup.
19
+ * In history mode a leftover `#/…` URL (a pre-history bookmark) is translated
20
+ * in place: its segments were display names with spaces as hyphens, which
21
+ * lowercased are today's slugs for almost every title.
22
+ */
23
+ export declare function initRouter(routingMode: RoutingMode, basePath?: string): void;
@@ -1,45 +1,75 @@
1
- /** Hash-based router for sdocs. Uses #/Segment/Segment/... format. */
2
- /** Current hash path segments (reactive) */
3
- let currentPath = $state([]);
4
- /** Get the current path segments */
5
- export function getPath() {
6
- return currentPath;
1
+ /**
2
+ * Router for the Explorer. Two modes over the same slug-segment routes:
3
+ * - 'history': real paths (/guides/installation). The standalone CLI default —
4
+ * its dev server and static build guarantee every path serves the shell.
5
+ * - 'hash': #/guides/installation. The embedded default — works under any
6
+ * host routing without server cooperation.
7
+ */
8
+ // Reactive: initRouter runs on mount, after hrefs were first rendered — they
9
+ // recompute when the mode lands.
10
+ let mode = $state('hash');
11
+ let base = $state('');
12
+ /** Current route segments (reactive) */
13
+ let currentRoute = $state([]);
14
+ /** Get the current route segments */
15
+ export function getRoute() {
16
+ return currentRoute;
7
17
  }
8
- /** Navigate to a path */
9
- export function navigate(segments) {
10
- const hash = segments.length > 0
11
- ? '#/' + segments.map(encodeSegment).join('/')
12
- : '#/';
13
- window.location.hash = hash;
18
+ /** Navigate to a route (segments are slugs) */
19
+ export function navigate(segments, opts) {
20
+ const url = routeHref(segments);
21
+ if (mode === 'history') {
22
+ if (opts?.replace)
23
+ history.replaceState(null, '', url);
24
+ else
25
+ history.pushState(null, '', url);
26
+ currentRoute = segments;
27
+ }
28
+ else {
29
+ if (opts?.replace)
30
+ location.replace(url);
31
+ else
32
+ location.hash = url;
33
+ // hashchange fires and re-parses; set eagerly for same-hash calls
34
+ currentRoute = segments;
35
+ }
14
36
  }
15
- /** Parse hash into path segments */
16
- function parseHash() {
17
- const hash = window.location.hash;
18
- if (!hash || hash === '#' || hash === '#/')
19
- return [];
20
- const path = hash.startsWith('#/') ? hash.slice(2) : hash.slice(1);
21
- return path.split('/').filter(Boolean).map(decodeSegment);
37
+ /** Href string for a route, in the active mode (for <a href>) */
38
+ export function routeHref(segments) {
39
+ const path = segments.map(encodeURIComponent).join('/');
40
+ return mode === 'history' ? `${base}/${path}` : `#/${path}`;
22
41
  }
23
- /** Encode a segment: spaces → hyphens */
24
- function encodeSegment(s) {
25
- return s.replace(/\s+/g, '-');
42
+ function parseLocation() {
43
+ const raw = mode === 'history'
44
+ ? location.pathname.startsWith(base)
45
+ ? location.pathname.slice(base.length)
46
+ : location.pathname
47
+ : location.hash.replace(/^#/, '');
48
+ return raw.split('/').filter(Boolean).map(decodeURIComponent);
26
49
  }
27
- /** Decode a segment: hyphens → spaces */
28
- function decodeSegment(s) {
29
- return s.replace(/-/g, ' ');
30
- }
31
- /** Initialize the router call once on app startup */
32
- export function initRouter() {
33
- currentPath = parseHash();
34
- window.addEventListener('hashchange', () => {
35
- currentPath = parseHash();
36
- });
37
- }
38
- /** Build a title path from meta.title (e.g. 'Demo / Button' → ['Demo', 'Button']) */
39
- export function titleToPath(title) {
40
- return title.split('/').map((s) => s.trim()).filter(Boolean);
41
- }
42
- /** Build a hash string from a title path (for href attributes) */
43
- export function pathToHash(segments) {
44
- return '#/' + segments.map(encodeSegment).join('/');
50
+ /**
51
+ * Initialize the router — call once on app startup.
52
+ * In history mode a leftover `#/…` URL (a pre-history bookmark) is translated
53
+ * in place: its segments were display names with spaces as hyphens, which
54
+ * lowercased are today's slugs for almost every title.
55
+ */
56
+ export function initRouter(routingMode, basePath = '') {
57
+ mode = routingMode;
58
+ base = basePath.replace(/\/$/, '');
59
+ if (mode === 'history' && location.hash.startsWith('#/')) {
60
+ const legacy = location.hash
61
+ .slice(2)
62
+ .split('/')
63
+ .filter(Boolean)
64
+ .map((s) => decodeURIComponent(s).toLowerCase());
65
+ history.replaceState(null, '', `${base}/${legacy.join('/')}`);
66
+ }
67
+ currentRoute = parseLocation();
68
+ const onChange = () => {
69
+ currentRoute = parseLocation();
70
+ };
71
+ if (mode === 'history')
72
+ window.addEventListener('popstate', onChange);
73
+ else
74
+ window.addEventListener('hashchange', onChange);
45
75
  }
@@ -3,12 +3,16 @@ export type TreeNodeType = 'folder' | 'group' | 'component' | 'page' | 'layout';
3
3
  export interface TreeNode {
4
4
  name: string;
5
5
  type: TreeNodeType;
6
- /** Full path segments from root (for routing) */
6
+ /** Display path segments from the section root */
7
7
  path: string[];
8
+ /** URL route segments from the site root (slugified, section included) */
9
+ route: string[];
8
10
  /** Children nodes */
9
11
  children: TreeNode[];
10
12
  /** The doc entry (only for component/page/layout nodes) */
11
13
  doc?: DocEntry;
14
+ /** Set on example child nodes: the example this node opens */
15
+ snippetName?: string;
12
16
  /** Example titles for component nodes (sidebar sub-pages) */
13
17
  examples?: string[];
14
18
  /** Whether this node should be expanded by default */
@@ -18,19 +22,55 @@ interface SidebarConfig {
18
22
  order?: Record<string, string[]>;
19
23
  open?: string[];
20
24
  }
21
- /** Build a tree from flat doc entries */
22
- export declare function buildTree(docs: DocEntry[], sidebar?: SidebarConfig): TreeNode[];
25
+ /** One top-bar section: a name and its own sidebar tree */
26
+ export interface SectionTree {
27
+ name: string;
28
+ slug: string;
29
+ isDefault: boolean;
30
+ tree: TreeNode[];
31
+ /** Route of the section's first document (top-bar tab target) */
32
+ firstRoute: string[] | null;
33
+ }
34
+ /** What a URL route resolves to */
35
+ export interface RouteTarget {
36
+ doc: DocEntry;
37
+ snippetName?: string;
38
+ }
39
+ /** Everything the Explorer needs to render sections and resolve URLs */
40
+ export interface SectionMap {
41
+ sections: SectionTree[];
42
+ routes: Map<string, RouteTarget>;
43
+ /** True once any doc declares an `@Section` — routes carry the section slug */
44
+ active: boolean;
45
+ }
46
+ /** Slug for one route segment — same rules as page heading anchors. */
47
+ export declare function slugifySegment(text: string): string;
48
+ /** Split an optional `@Section` first segment off a title. */
49
+ export declare function splitSection(title: string | null | undefined): {
50
+ section: string | null;
51
+ rest: string;
52
+ };
23
53
  /**
24
- * The title shown to users. A leading ':' on the first segment is a sidebar
25
- * grouping directive (see buildTree), not part of the displayed title, so it
26
- * is stripped for headings.
54
+ * Group docs into sections (from `@Section/` title prefixes, the rest into the
55
+ * default section), build each section's tree, and register every navigable
56
+ * route. Section order: the config list first, unlisted after (default
57
+ * section first, then alphabetical).
58
+ */
59
+ export declare function buildSections(docs: DocEntry[], sidebar?: SidebarConfig, opts?: {
60
+ defaultSection?: string;
61
+ order?: string[];
62
+ }): SectionMap;
63
+ /**
64
+ * Resolve URL segments to a doc. Links that omit the section — bookmarks from
65
+ * before sections existed — fall back into the default section.
66
+ */
67
+ export declare function resolveRoute(map: SectionMap, segments: string[]): RouteTarget | null;
68
+ /** Build a tree from flat doc entries; routes get `routePrefix` prepended. */
69
+ export declare function buildTree(docs: DocEntry[], sidebar?: SidebarConfig, routePrefix?: string[]): TreeNode[];
70
+ /**
71
+ * The title shown to users. The `@Section/` prefix routes to a top-bar
72
+ * section and a leading ':' on the first segment is a sidebar grouping
73
+ * directive (see buildTree) — neither is part of the displayed title.
27
74
  */
28
75
  export declare function displayTitle(title: string | null | undefined): string;
29
- /** Check if a path matches a tree node (for active state) */
30
- export declare function pathMatchesNode(currentPath: string[], node: TreeNode): boolean;
31
- /** Find the doc entry for a given path */
32
- export declare function findDocByPath(docs: DocEntry[], path: string[]): {
33
- doc: DocEntry;
34
- snippetName?: string;
35
- } | null;
36
76
  export {};