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
|
@@ -1,9 +1,109 @@
|
|
|
1
|
-
/**
|
|
2
|
-
export function
|
|
1
|
+
/** Slug for one route segment — same rules as page heading anchors. */
|
|
2
|
+
export function slugifySegment(text) {
|
|
3
|
+
return (text
|
|
4
|
+
.toLowerCase()
|
|
5
|
+
.replace(/[^\w\s-]/g, '')
|
|
6
|
+
.trim()
|
|
7
|
+
.replace(/[\s_]+/g, '-') || 'item');
|
|
8
|
+
}
|
|
9
|
+
/** Split an optional `@Section` first segment off a title. */
|
|
10
|
+
export function splitSection(title) {
|
|
11
|
+
const trimmed = (title ?? '').trim();
|
|
12
|
+
if (!trimmed.startsWith('@'))
|
|
13
|
+
return { section: null, rest: trimmed };
|
|
14
|
+
const slash = trimmed.indexOf('/');
|
|
15
|
+
if (slash === -1)
|
|
16
|
+
return { section: trimmed.slice(1).trim() || null, rest: '' };
|
|
17
|
+
return {
|
|
18
|
+
section: trimmed.slice(1, slash).trim() || null,
|
|
19
|
+
rest: trimmed.slice(slash + 1).trim(),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Group docs into sections (from `@Section/` title prefixes, the rest into the
|
|
24
|
+
* default section), build each section's tree, and register every navigable
|
|
25
|
+
* route. Section order: the config list first, unlisted after (default
|
|
26
|
+
* section first, then alphabetical).
|
|
27
|
+
*/
|
|
28
|
+
export function buildSections(docs, sidebar, opts) {
|
|
29
|
+
const defaultName = opts?.defaultSection ?? 'Docs';
|
|
30
|
+
const byName = new Map();
|
|
31
|
+
let active = false;
|
|
32
|
+
for (const doc of docs) {
|
|
33
|
+
const { section } = splitSection(doc.meta.title);
|
|
34
|
+
if (section)
|
|
35
|
+
active = true;
|
|
36
|
+
const name = section ?? defaultName;
|
|
37
|
+
const list = byName.get(name);
|
|
38
|
+
if (list)
|
|
39
|
+
list.push(doc);
|
|
40
|
+
else
|
|
41
|
+
byName.set(name, [doc]);
|
|
42
|
+
}
|
|
43
|
+
const configOrder = opts?.order ?? [];
|
|
44
|
+
const names = [...byName.keys()].sort((a, b) => {
|
|
45
|
+
const ai = configOrder.indexOf(a);
|
|
46
|
+
const bi = configOrder.indexOf(b);
|
|
47
|
+
if (ai !== -1 || bi !== -1)
|
|
48
|
+
return (ai === -1 ? Infinity : ai) - (bi === -1 ? Infinity : bi);
|
|
49
|
+
if (a === defaultName)
|
|
50
|
+
return -1;
|
|
51
|
+
if (b === defaultName)
|
|
52
|
+
return 1;
|
|
53
|
+
return a.localeCompare(b);
|
|
54
|
+
});
|
|
55
|
+
const routes = new Map();
|
|
56
|
+
const usedSectionSlugs = new Set();
|
|
57
|
+
const sections = names.map((name) => {
|
|
58
|
+
const slug = uniqueSlug(slugifySegment(name), usedSectionSlugs);
|
|
59
|
+
const prefix = active ? [slug] : [];
|
|
60
|
+
const tree = buildTree(byName.get(name), sidebar, prefix);
|
|
61
|
+
registerRoutes(tree, routes);
|
|
62
|
+
return {
|
|
63
|
+
name,
|
|
64
|
+
slug,
|
|
65
|
+
isDefault: name === defaultName,
|
|
66
|
+
tree,
|
|
67
|
+
firstRoute: firstDocRoute(tree),
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
return { sections, routes, active };
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Resolve URL segments to a doc. Links that omit the section — bookmarks from
|
|
74
|
+
* before sections existed — fall back into the default section.
|
|
75
|
+
*/
|
|
76
|
+
export function resolveRoute(map, segments) {
|
|
77
|
+
if (segments.length === 0)
|
|
78
|
+
return null;
|
|
79
|
+
const hit = map.routes.get(segments.join('/'));
|
|
80
|
+
if (hit)
|
|
81
|
+
return hit;
|
|
82
|
+
if (map.active) {
|
|
83
|
+
const def = map.sections.find((s) => s.isDefault);
|
|
84
|
+
if (def && segments[0] !== def.slug) {
|
|
85
|
+
return map.routes.get([def.slug, ...segments].join('/')) ?? null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
/** Build a tree from flat doc entries; routes get `routePrefix` prepended. */
|
|
91
|
+
export function buildTree(docs, sidebar, routePrefix = []) {
|
|
3
92
|
const root = [];
|
|
4
93
|
const folderMap = new Map();
|
|
94
|
+
// Slugs claimed per parent (keyed by display path) — collisions get -2, -3…
|
|
95
|
+
const claimed = new Map();
|
|
96
|
+
const childRoute = (parentPath, parentRoute, name) => {
|
|
97
|
+
const key = parentPath.join('/');
|
|
98
|
+
let used = claimed.get(key);
|
|
99
|
+
if (!used) {
|
|
100
|
+
used = new Set();
|
|
101
|
+
claimed.set(key, used);
|
|
102
|
+
}
|
|
103
|
+
return [...parentRoute, uniqueSlug(slugifySegment(name), used)];
|
|
104
|
+
};
|
|
5
105
|
for (const doc of docs) {
|
|
6
|
-
const title = doc.meta.title
|
|
106
|
+
const title = splitSection(doc.meta.title).rest || 'Untitled';
|
|
7
107
|
const segments = title.split('/').map((s) => s.trim()).filter(Boolean);
|
|
8
108
|
if (segments.length === 0)
|
|
9
109
|
continue;
|
|
@@ -12,12 +112,14 @@ export function buildTree(docs, sidebar) {
|
|
|
12
112
|
const folderSegments = segments.slice(0, -1);
|
|
13
113
|
// Ensure all parent folders exist
|
|
14
114
|
let parent = root;
|
|
115
|
+
let parentRoute = routePrefix;
|
|
15
116
|
const currentPath = [];
|
|
16
117
|
for (let i = 0; i < folderSegments.length; i++) {
|
|
17
118
|
let segName = folderSegments[i];
|
|
18
119
|
const isGroup = i === 0 && segName.startsWith(':');
|
|
19
120
|
if (isGroup)
|
|
20
|
-
segName = segName.slice(1);
|
|
121
|
+
segName = segName.slice(1).trim();
|
|
122
|
+
const parentPath = [...currentPath];
|
|
21
123
|
currentPath.push(segName);
|
|
22
124
|
const key = currentPath.join('/');
|
|
23
125
|
let folder = folderMap.get(key);
|
|
@@ -32,6 +134,7 @@ export function buildTree(docs, sidebar) {
|
|
|
32
134
|
name: segName,
|
|
33
135
|
type: isGroup ? 'group' : 'folder',
|
|
34
136
|
path: [...currentPath],
|
|
137
|
+
route: childRoute(parentPath, parentRoute, segName),
|
|
35
138
|
children: [],
|
|
36
139
|
defaultExpanded: isGroup || sidebar?.open?.includes(segName),
|
|
37
140
|
};
|
|
@@ -40,6 +143,7 @@ export function buildTree(docs, sidebar) {
|
|
|
40
143
|
folderMap.set(key, folder);
|
|
41
144
|
}
|
|
42
145
|
parent = folder.children;
|
|
146
|
+
parentRoute = folder.route;
|
|
43
147
|
}
|
|
44
148
|
// Create the item node
|
|
45
149
|
const itemPath = [...currentPath, itemName];
|
|
@@ -51,28 +155,33 @@ export function buildTree(docs, sidebar) {
|
|
|
51
155
|
name: itemName,
|
|
52
156
|
type: 'component',
|
|
53
157
|
path: itemPath,
|
|
158
|
+
route: childRoute(currentPath, parentRoute, itemName),
|
|
54
159
|
children: [],
|
|
55
160
|
};
|
|
56
161
|
// Upgrade folder to component and attach doc data
|
|
57
162
|
componentNode.type = 'component';
|
|
58
163
|
componentNode.doc = doc;
|
|
59
164
|
componentNode.examples = examples;
|
|
60
|
-
// Add "Docs" child
|
|
165
|
+
// Add "Docs" child — same doc, same route as the component itself
|
|
61
166
|
componentNode.children.unshift({
|
|
62
167
|
name: 'Docs',
|
|
63
168
|
type: 'component',
|
|
64
169
|
path: itemPath,
|
|
170
|
+
route: componentNode.route,
|
|
65
171
|
children: [],
|
|
66
172
|
doc,
|
|
67
173
|
});
|
|
68
174
|
// Add example children
|
|
175
|
+
const usedExampleSlugs = new Set();
|
|
69
176
|
for (const ex of examples) {
|
|
70
177
|
componentNode.children.push({
|
|
71
178
|
name: ex,
|
|
72
179
|
type: 'component',
|
|
73
180
|
path: [...itemPath, ex],
|
|
181
|
+
route: [...componentNode.route, uniqueSlug(slugifySegment(ex), usedExampleSlugs)],
|
|
74
182
|
children: [],
|
|
75
183
|
doc,
|
|
184
|
+
snippetName: ex,
|
|
76
185
|
});
|
|
77
186
|
}
|
|
78
187
|
if (!existing) {
|
|
@@ -84,6 +193,7 @@ export function buildTree(docs, sidebar) {
|
|
|
84
193
|
name: itemName,
|
|
85
194
|
type: kind,
|
|
86
195
|
path: itemPath,
|
|
196
|
+
route: childRoute(currentPath, parentRoute, itemName),
|
|
87
197
|
children: [],
|
|
88
198
|
doc,
|
|
89
199
|
});
|
|
@@ -97,6 +207,39 @@ export function buildTree(docs, sidebar) {
|
|
|
97
207
|
}
|
|
98
208
|
return root;
|
|
99
209
|
}
|
|
210
|
+
/** Append -2, -3… until the slug is free within `used`; claims it. */
|
|
211
|
+
function uniqueSlug(slug, used) {
|
|
212
|
+
let candidate = slug;
|
|
213
|
+
for (let n = 2; used.has(candidate); n++) {
|
|
214
|
+
candidate = `${slug}-${n}`;
|
|
215
|
+
}
|
|
216
|
+
used.add(candidate);
|
|
217
|
+
return candidate;
|
|
218
|
+
}
|
|
219
|
+
/** Register every doc-bearing node's route; example children carry the name. */
|
|
220
|
+
function registerRoutes(nodes, routes) {
|
|
221
|
+
for (const node of nodes) {
|
|
222
|
+
if (node.doc) {
|
|
223
|
+
const key = node.route.join('/');
|
|
224
|
+
if (!routes.has(key)) {
|
|
225
|
+
routes.set(key, node.snippetName ? { doc: node.doc, snippetName: node.snippetName } : { doc: node.doc });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (node.children.length > 0)
|
|
229
|
+
registerRoutes(node.children, routes);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/** The first document route in tree order (for section tab targets). */
|
|
233
|
+
function firstDocRoute(nodes) {
|
|
234
|
+
for (const node of nodes) {
|
|
235
|
+
if (node.doc)
|
|
236
|
+
return node.route;
|
|
237
|
+
const inChildren = firstDocRoute(node.children);
|
|
238
|
+
if (inChildren)
|
|
239
|
+
return inChildren;
|
|
240
|
+
}
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
100
243
|
/** Reorder component children: Docs first, then examples, then sub-components sorted by name */
|
|
101
244
|
function reorderComponentChildren(nodes) {
|
|
102
245
|
for (const node of nodes) {
|
|
@@ -173,39 +316,10 @@ function sortByOrder(nodes, order) {
|
|
|
173
316
|
}
|
|
174
317
|
}
|
|
175
318
|
/**
|
|
176
|
-
* The title shown to users.
|
|
177
|
-
*
|
|
178
|
-
* is
|
|
319
|
+
* The title shown to users. The `@Section/` prefix routes to a top-bar
|
|
320
|
+
* section and a leading ':' on the first segment is a sidebar grouping
|
|
321
|
+
* directive (see buildTree) — neither is part of the displayed title.
|
|
179
322
|
*/
|
|
180
323
|
export function displayTitle(title) {
|
|
181
|
-
return (title
|
|
182
|
-
}
|
|
183
|
-
/** Check if a path matches a tree node (for active state) */
|
|
184
|
-
export function pathMatchesNode(currentPath, node) {
|
|
185
|
-
if (node.path.length !== currentPath.length)
|
|
186
|
-
return false;
|
|
187
|
-
return node.path.every((seg, i) => seg === currentPath[i]);
|
|
188
|
-
}
|
|
189
|
-
/** Find the doc entry for a given path */
|
|
190
|
-
export function findDocByPath(docs, path) {
|
|
191
|
-
if (path.length === 0)
|
|
192
|
-
return null;
|
|
193
|
-
for (const doc of docs) {
|
|
194
|
-
const title = doc.meta.title ?? '';
|
|
195
|
-
const segments = title.split('/').map((s) => s.trim()).filter(Boolean)
|
|
196
|
-
.map((s) => s.startsWith(':') ? s.slice(1) : s);
|
|
197
|
-
// Exact match → component docs view
|
|
198
|
-
if (segments.length === path.length && segments.every((s, i) => s === path[i])) {
|
|
199
|
-
return { doc };
|
|
200
|
-
}
|
|
201
|
-
// One extra segment → example
|
|
202
|
-
if (segments.length === path.length - 1 && segments.every((s, i) => s === path[i])) {
|
|
203
|
-
const snippetName = path[path.length - 1];
|
|
204
|
-
const hasSnippet = doc.examples?.some((s) => s.name === snippetName);
|
|
205
|
-
if (hasSnippet) {
|
|
206
|
-
return { doc, snippetName };
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return null;
|
|
324
|
+
return splitSection(title).rest.replace(/^:\s*/, '');
|
|
211
325
|
}
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
interface Props {
|
|
6
6
|
docs: DocEntry[];
|
|
7
|
-
|
|
7
|
+
title: string;
|
|
8
|
+
logo?: string | false;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
let { docs, logo }: Props = $props();
|
|
11
|
+
let { docs, title, logo = 'sdocs' }: Props = $props();
|
|
11
12
|
|
|
12
13
|
const componentCount = $derived(docs.filter((d) => d.kind === 'component').length);
|
|
13
14
|
const pageCount = $derived(docs.filter((d) => d.kind === 'page').length);
|
|
@@ -16,8 +17,12 @@
|
|
|
16
17
|
|
|
17
18
|
<div class="sdocs-home">
|
|
18
19
|
<div class="sdocs-home-brand">
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
{#if logo === 'sdocs'}
|
|
21
|
+
<Icon name="sdocs" --w="160px" --h="160px" --fill="#FC1D29" />
|
|
22
|
+
{:else if logo}
|
|
23
|
+
<img class="sdocs-home-logo-img" src={logo} alt="" />
|
|
24
|
+
{/if}
|
|
25
|
+
<h1 class="sdocs-home-logo">{title}</h1>
|
|
21
26
|
<p class="sdocs-home-tagline">A lightweight documentation tool for Svelte 5 components.</p>
|
|
22
27
|
</div>
|
|
23
28
|
|
|
@@ -65,6 +70,11 @@
|
|
|
65
70
|
color: var(--color-base-900);
|
|
66
71
|
margin: 0;
|
|
67
72
|
}
|
|
73
|
+
.sdocs-home-logo-img {
|
|
74
|
+
width: 160px;
|
|
75
|
+
height: 160px;
|
|
76
|
+
object-fit: contain;
|
|
77
|
+
}
|
|
68
78
|
.sdocs-home-tagline {
|
|
69
79
|
font-size: 16px;
|
|
70
80
|
color: var(--color-base-500);
|
|
@@ -37,7 +37,69 @@
|
|
|
37
37
|
};
|
|
38
38
|
});
|
|
39
39
|
|
|
40
|
+
// Scrollspy: the TOC highlights the section currently in view. The active
|
|
41
|
+
// heading is the last one above the threshold line near the viewport top.
|
|
42
|
+
// The scroll listener is capturing because the scrolling element is an
|
|
43
|
+
// ancestor container, not the window.
|
|
44
|
+
const SPY_OFFSET = 120;
|
|
45
|
+
let activeId = $state('');
|
|
46
|
+
let spyLockUntil = 0;
|
|
47
|
+
|
|
48
|
+
$effect(() => {
|
|
49
|
+
if (!container || toc.length === 0) return;
|
|
50
|
+
void PageComponent; // re-run once the page body has mounted
|
|
51
|
+
const ids = toc.map((h) => h.id);
|
|
52
|
+
let frame = 0;
|
|
53
|
+
const update = () => {
|
|
54
|
+
frame = 0;
|
|
55
|
+
if (performance.now() < spyLockUntil) return;
|
|
56
|
+
// At the bottom the last sections can never reach the threshold
|
|
57
|
+
// line — snap to the final entry so it is reachable at all.
|
|
58
|
+
let scroller: HTMLElement | null = container ?? null;
|
|
59
|
+
while (scroller) {
|
|
60
|
+
const o = getComputedStyle(scroller).overflowY;
|
|
61
|
+
if (
|
|
62
|
+
scroller.scrollHeight > scroller.clientHeight + 1 &&
|
|
63
|
+
(o === 'auto' || o === 'scroll')
|
|
64
|
+
) {
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
scroller = scroller.parentElement;
|
|
68
|
+
}
|
|
69
|
+
const atBottom = scroller
|
|
70
|
+
? scroller.scrollTop + scroller.clientHeight >= scroller.scrollHeight - 2
|
|
71
|
+
: window.scrollY + window.innerHeight >= document.documentElement.scrollHeight - 2;
|
|
72
|
+
if (atBottom) {
|
|
73
|
+
activeId = ids[ids.length - 1];
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
let current = ids[0] ?? '';
|
|
77
|
+
for (const id of ids) {
|
|
78
|
+
const el = container?.querySelector(`#${CSS.escape(id)}`);
|
|
79
|
+
if (!el) continue;
|
|
80
|
+
if (el.getBoundingClientRect().top > SPY_OFFSET) break;
|
|
81
|
+
current = id;
|
|
82
|
+
}
|
|
83
|
+
activeId = current;
|
|
84
|
+
};
|
|
85
|
+
const onScroll = () => {
|
|
86
|
+
if (!frame) frame = requestAnimationFrame(update);
|
|
87
|
+
};
|
|
88
|
+
update();
|
|
89
|
+
document.addEventListener('scroll', onScroll, { capture: true, passive: true });
|
|
90
|
+
window.addEventListener('resize', onScroll);
|
|
91
|
+
return () => {
|
|
92
|
+
document.removeEventListener('scroll', onScroll, { capture: true });
|
|
93
|
+
window.removeEventListener('resize', onScroll);
|
|
94
|
+
if (frame) cancelAnimationFrame(frame);
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
|
|
40
98
|
function scrollToHeading(id: string) {
|
|
99
|
+
// Highlight the target right away and hold it while the smooth scroll
|
|
100
|
+
// passes intermediate sections.
|
|
101
|
+
activeId = id;
|
|
102
|
+
spyLockUntil = performance.now() + 800;
|
|
41
103
|
container?.querySelector(`#${CSS.escape(id)}`)?.scrollIntoView({ behavior: 'smooth' });
|
|
42
104
|
}
|
|
43
105
|
|
|
@@ -102,6 +164,8 @@
|
|
|
102
164
|
<li class="sdocs-toc-item" style:padding-left="{(heading.level - 2) * 12}px">
|
|
103
165
|
<button
|
|
104
166
|
class="sdocs-toc-link"
|
|
167
|
+
class:is-active={heading.id === activeId}
|
|
168
|
+
aria-current={heading.id === activeId ? 'true' : undefined}
|
|
105
169
|
onclick={() => scrollToHeading(heading.id)}
|
|
106
170
|
>
|
|
107
171
|
{heading.text}
|
|
@@ -372,8 +436,10 @@
|
|
|
372
436
|
cursor: pointer;
|
|
373
437
|
text-align: left;
|
|
374
438
|
width: 100%;
|
|
439
|
+
transition: color 0.15s;
|
|
375
440
|
}
|
|
376
|
-
.sdocs-toc-link:hover
|
|
441
|
+
.sdocs-toc-link:hover,
|
|
442
|
+
.sdocs-toc-link.is-active {
|
|
377
443
|
color: var(--color-action-500);
|
|
378
444
|
}
|
|
379
445
|
</style>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { SvelteSet } from 'svelte/reactivity';
|
|
3
3
|
import type { TreeNode } from '../tree-builder.js';
|
|
4
|
-
import {
|
|
4
|
+
import { routeHref, navigate } from '../router.svelte.js';
|
|
5
5
|
import { Icon } from '../../ui/Icon/index.js';
|
|
6
6
|
import { NavTree } from '../../ui/index.js';
|
|
7
7
|
|
|
@@ -9,9 +9,11 @@
|
|
|
9
9
|
|
|
10
10
|
interface Props {
|
|
11
11
|
tree: TreeNode[];
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
currentRoute: string[];
|
|
13
|
+
title: string;
|
|
14
|
+
logo?: string | false;
|
|
15
|
+
/** Brand + actions render here unless a top bar already shows them */
|
|
16
|
+
showHeader?: boolean;
|
|
15
17
|
cssNames?: string[];
|
|
16
18
|
activeStylesheet?: string;
|
|
17
19
|
theme?: ThemeMode;
|
|
@@ -20,7 +22,7 @@
|
|
|
20
22
|
onThemeChange?: (theme: ThemeMode) => void;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
let { tree,
|
|
25
|
+
let { tree, currentRoute, title, logo = 'sdocs', showHeader = true, cssNames = [], activeStylesheet, theme = 'light', onToggleFullscreen, onStylesheetChange, onThemeChange }: Props = $props();
|
|
24
26
|
|
|
25
27
|
const themeIcons: Record<ThemeMode, string> = { light: '\u2600', dark: '\u263D' };
|
|
26
28
|
const themeLabels: Record<ThemeMode, string> = { light: 'Light', dark: 'Dark' };
|
|
@@ -85,13 +87,16 @@
|
|
|
85
87
|
return result;
|
|
86
88
|
}
|
|
87
89
|
|
|
88
|
-
function isActive(
|
|
89
|
-
if (
|
|
90
|
-
return
|
|
90
|
+
function isActive(nodeRoute: string[]): boolean {
|
|
91
|
+
if (nodeRoute.length > currentRoute.length) return false;
|
|
92
|
+
return nodeRoute.every((seg, i) => seg === currentRoute[i]);
|
|
91
93
|
}
|
|
92
94
|
|
|
93
|
-
function isExactActive(
|
|
94
|
-
return
|
|
95
|
+
function isExactActive(nodeRoute: string[]): boolean {
|
|
96
|
+
return (
|
|
97
|
+
nodeRoute.length === currentRoute.length &&
|
|
98
|
+
nodeRoute.every((seg, i) => seg === currentRoute[i])
|
|
99
|
+
);
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
function iconName(node: TreeNode, expanded: boolean): string {
|
|
@@ -184,35 +189,37 @@
|
|
|
184
189
|
</script>
|
|
185
190
|
|
|
186
191
|
<aside class="sdocs-sidebar">
|
|
187
|
-
|
|
188
|
-
<
|
|
189
|
-
{
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
{
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
192
|
+
{#if showHeader}
|
|
193
|
+
<div class="sdocs-sidebar-header">
|
|
194
|
+
<a href={routeHref([])} class="sdocs-logo">
|
|
195
|
+
{#if logo === 'sdocs'}
|
|
196
|
+
<Icon name="sdocs" --w="22px" --h="22px" --fill="#FC1D29" />
|
|
197
|
+
{:else if logo}
|
|
198
|
+
<img class="sdocs-logo-img" src={logo} alt="" />
|
|
199
|
+
{/if}
|
|
200
|
+
{title}
|
|
201
|
+
</a>
|
|
202
|
+
<div class="sdocs-header-actions">
|
|
203
|
+
{#if cssNames.length > 1}
|
|
204
|
+
<select
|
|
205
|
+
class="sdocs-css-picker"
|
|
206
|
+
value={activeStylesheet}
|
|
207
|
+
onchange={(e) => onStylesheetChange?.(e.currentTarget.value)}
|
|
208
|
+
>
|
|
209
|
+
{#each cssNames as name (name)}
|
|
210
|
+
<option value={name}>{name}</option>
|
|
211
|
+
{/each}
|
|
212
|
+
</select>
|
|
213
|
+
{/if}
|
|
214
|
+
<button class="sdocs-theme-btn" onclick={toggleTheme} title="{themeLabels[theme]} theme">
|
|
215
|
+
{themeIcons[theme]}
|
|
216
|
+
</button>
|
|
217
|
+
<button class="sdocs-fullscreen-btn" onclick={() => onToggleFullscreen?.()} title="Fullscreen">
|
|
218
|
+
⛶
|
|
219
|
+
</button>
|
|
220
|
+
</div>
|
|
214
221
|
</div>
|
|
215
|
-
|
|
222
|
+
{/if}
|
|
216
223
|
|
|
217
224
|
<div class="sdocs-sidebar-search">
|
|
218
225
|
<input
|
|
@@ -244,12 +251,12 @@
|
|
|
244
251
|
<NavTree.Item
|
|
245
252
|
label={node.name}
|
|
246
253
|
{expanded}
|
|
247
|
-
active={isActive(node.
|
|
254
|
+
active={isActive(node.route)}
|
|
248
255
|
onclick={() => {
|
|
249
256
|
const wasCollapsed = !expandedSet.has(pathKey);
|
|
250
257
|
toggleExpanded(pathKey);
|
|
251
258
|
if (wasCollapsed && node.type !== 'folder') {
|
|
252
|
-
|
|
259
|
+
navigate(node.route);
|
|
253
260
|
}
|
|
254
261
|
}}
|
|
255
262
|
--font-weight="500"
|
|
@@ -268,8 +275,8 @@
|
|
|
268
275
|
{:else}
|
|
269
276
|
<NavTree.Item
|
|
270
277
|
label={node.name}
|
|
271
|
-
href={
|
|
272
|
-
active={isExactActive(node.
|
|
278
|
+
href={routeHref(node.route)}
|
|
279
|
+
active={isExactActive(node.route)}
|
|
273
280
|
--font-weight={leafWeight(node)}
|
|
274
281
|
--bg-hover={hoverBg(node)}
|
|
275
282
|
--bg-active={activeBg(node)}
|
|
@@ -284,7 +291,7 @@
|
|
|
284
291
|
<style>
|
|
285
292
|
.sdocs-sidebar {
|
|
286
293
|
width: 260px;
|
|
287
|
-
height:
|
|
294
|
+
height: 100%;
|
|
288
295
|
overflow-y: auto;
|
|
289
296
|
border-right: 1px solid var(--color-base-200);
|
|
290
297
|
background: var(--color-base-0);
|