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 +36 -0
- package/dist/commands/build.js +48 -0
- package/dist/commands/init.js +6 -3
- package/dist/explorer/Explorer.svelte +114 -34
- package/dist/explorer/router.svelte.d.ts +23 -11
- package/dist/explorer/router.svelte.js +70 -40
- package/dist/explorer/tree-builder.d.ts +53 -13
- package/dist/explorer/tree-builder.js +152 -38
- package/dist/explorer/views/HomePage.svelte +14 -4
- package/dist/explorer/views/PageView.svelte +67 -1
- package/dist/explorer/views/Sidebar.svelte +50 -43
- package/dist/explorer/views/TopBar.svelte +159 -0
- package/dist/explorer/views/TopBar.svelte.d.ts +14 -0
- package/dist/language/config-schema.js +21 -4
- package/dist/server/app-gen.js +6 -3
- package/dist/server/config.js +18 -3
- package/dist/types.d.ts +21 -6
- package/package.json +1 -1
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
|
package/dist/commands/build.js
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
|
+
}
|
package/dist/commands/init.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
25
|
-
//
|
|
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,
|
|
4
|
-
import {
|
|
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
|
-
|
|
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
|
|
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
|
|
70
|
-
const
|
|
71
|
-
|
|
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
|
-
<
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
{
|
|
80
|
-
{
|
|
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
|
-
◀ Exit fullscreen
|
|
91
|
-
</button>
|
|
92
145
|
{/if}
|
|
93
|
-
<
|
|
94
|
-
{#if
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
<
|
|
162
|
+
<button class="sdocs-exit-fullscreen" onclick={() => (sidebarHidden = false)}>
|
|
163
|
+
◀ Exit fullscreen
|
|
164
|
+
</button>
|
|
104
165
|
{/if}
|
|
105
|
-
|
|
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
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
9
|
-
export function navigate(segments) {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
/**
|
|
16
|
-
function
|
|
17
|
-
const
|
|
18
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
/**
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
22
|
-
export
|
|
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
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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 {};
|