sdocs 0.0.60 → 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 +27 -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/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);
|
|
@@ -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);
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { SectionTree } from '../tree-builder.js';
|
|
3
|
+
import { routeHref } from '../router.svelte.js';
|
|
4
|
+
import { Icon } from '../../ui/Icon/index.js';
|
|
5
|
+
|
|
6
|
+
type ThemeMode = 'light' | 'dark';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
title: string;
|
|
10
|
+
logo: string | false;
|
|
11
|
+
sections: SectionTree[];
|
|
12
|
+
activeSlug?: string;
|
|
13
|
+
cssNames?: string[];
|
|
14
|
+
activeStylesheet?: string;
|
|
15
|
+
theme?: ThemeMode;
|
|
16
|
+
onToggleFullscreen?: () => void;
|
|
17
|
+
onStylesheetChange?: (name: string) => void;
|
|
18
|
+
onThemeChange?: (theme: ThemeMode) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let {
|
|
22
|
+
title,
|
|
23
|
+
logo,
|
|
24
|
+
sections,
|
|
25
|
+
activeSlug,
|
|
26
|
+
cssNames = [],
|
|
27
|
+
activeStylesheet,
|
|
28
|
+
theme = 'light',
|
|
29
|
+
onToggleFullscreen,
|
|
30
|
+
onStylesheetChange,
|
|
31
|
+
onThemeChange,
|
|
32
|
+
}: Props = $props();
|
|
33
|
+
|
|
34
|
+
const themeIcons: Record<ThemeMode, string> = { light: '☀', dark: '☽' };
|
|
35
|
+
const themeLabels: Record<ThemeMode, string> = { light: 'Light', dark: 'Dark' };
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<header class="sdocs-topbar">
|
|
39
|
+
<a href={routeHref([])} class="sdocs-topbar-brand">
|
|
40
|
+
{#if logo === 'sdocs'}
|
|
41
|
+
<Icon name="sdocs" --w="22px" --h="22px" --fill="#FC1D29" />
|
|
42
|
+
{:else if logo}
|
|
43
|
+
<img class="sdocs-topbar-logo" src={logo} alt="" />
|
|
44
|
+
{/if}
|
|
45
|
+
{title}
|
|
46
|
+
</a>
|
|
47
|
+
<nav class="sdocs-topbar-sections">
|
|
48
|
+
{#each sections as section (section.slug)}
|
|
49
|
+
<a
|
|
50
|
+
class="sdocs-topbar-tab"
|
|
51
|
+
class:is-active={section.slug === activeSlug}
|
|
52
|
+
aria-current={section.slug === activeSlug ? 'page' : undefined}
|
|
53
|
+
href={routeHref(section.firstRoute ?? [section.slug])}
|
|
54
|
+
>
|
|
55
|
+
{section.name}
|
|
56
|
+
</a>
|
|
57
|
+
{/each}
|
|
58
|
+
</nav>
|
|
59
|
+
<div class="sdocs-topbar-actions">
|
|
60
|
+
{#if cssNames.length > 1}
|
|
61
|
+
<select
|
|
62
|
+
class="sdocs-css-picker"
|
|
63
|
+
value={activeStylesheet}
|
|
64
|
+
onchange={(e) => onStylesheetChange?.(e.currentTarget.value)}
|
|
65
|
+
>
|
|
66
|
+
{#each cssNames as name (name)}
|
|
67
|
+
<option value={name}>{name}</option>
|
|
68
|
+
{/each}
|
|
69
|
+
</select>
|
|
70
|
+
{/if}
|
|
71
|
+
<button class="sdocs-topbar-btn" onclick={() => onThemeChange?.(theme === 'light' ? 'dark' : 'light')} title="{themeLabels[theme]} theme">
|
|
72
|
+
{themeIcons[theme]}
|
|
73
|
+
</button>
|
|
74
|
+
<button class="sdocs-topbar-btn" onclick={() => onToggleFullscreen?.()} title="Fullscreen">
|
|
75
|
+
⛶
|
|
76
|
+
</button>
|
|
77
|
+
</div>
|
|
78
|
+
</header>
|
|
79
|
+
|
|
80
|
+
<style>
|
|
81
|
+
.sdocs-topbar {
|
|
82
|
+
display: flex;
|
|
83
|
+
align-items: center;
|
|
84
|
+
gap: 24px;
|
|
85
|
+
padding: 0 16px;
|
|
86
|
+
height: 48px;
|
|
87
|
+
flex-shrink: 0;
|
|
88
|
+
border-bottom: 1px solid var(--color-base-200);
|
|
89
|
+
background: var(--color-base-0);
|
|
90
|
+
font-family: var(--sans);
|
|
91
|
+
}
|
|
92
|
+
.sdocs-topbar-brand {
|
|
93
|
+
display: flex;
|
|
94
|
+
align-items: center;
|
|
95
|
+
gap: 6px;
|
|
96
|
+
font-weight: 700;
|
|
97
|
+
font-size: 18px;
|
|
98
|
+
color: var(--color-base-900);
|
|
99
|
+
text-decoration: none;
|
|
100
|
+
}
|
|
101
|
+
.sdocs-topbar-logo {
|
|
102
|
+
width: 22px;
|
|
103
|
+
height: 22px;
|
|
104
|
+
object-fit: contain;
|
|
105
|
+
}
|
|
106
|
+
.sdocs-topbar-sections {
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: stretch;
|
|
109
|
+
gap: 4px;
|
|
110
|
+
height: 100%;
|
|
111
|
+
flex: 1;
|
|
112
|
+
min-width: 0;
|
|
113
|
+
overflow-x: auto;
|
|
114
|
+
}
|
|
115
|
+
.sdocs-topbar-tab {
|
|
116
|
+
display: flex;
|
|
117
|
+
align-items: center;
|
|
118
|
+
padding: 0 12px;
|
|
119
|
+
font-size: 13px;
|
|
120
|
+
font-weight: 500;
|
|
121
|
+
color: var(--color-base-500);
|
|
122
|
+
text-decoration: none;
|
|
123
|
+
border-bottom: 2px solid transparent;
|
|
124
|
+
white-space: nowrap;
|
|
125
|
+
}
|
|
126
|
+
.sdocs-topbar-tab:hover {
|
|
127
|
+
color: var(--color-base-900);
|
|
128
|
+
}
|
|
129
|
+
.sdocs-topbar-tab.is-active {
|
|
130
|
+
color: var(--color-action-500);
|
|
131
|
+
border-bottom-color: var(--color-action-500);
|
|
132
|
+
}
|
|
133
|
+
.sdocs-topbar-actions {
|
|
134
|
+
display: flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
gap: 6px;
|
|
137
|
+
}
|
|
138
|
+
.sdocs-css-picker {
|
|
139
|
+
font-size: 12px;
|
|
140
|
+
padding: 2px 4px;
|
|
141
|
+
border: 1px solid var(--color-base-200);
|
|
142
|
+
border-radius: 4px;
|
|
143
|
+
background: var(--color-base-0);
|
|
144
|
+
color: var(--color-base-600);
|
|
145
|
+
}
|
|
146
|
+
.sdocs-topbar-btn {
|
|
147
|
+
padding: 2px 6px;
|
|
148
|
+
border: 1px solid var(--color-base-200);
|
|
149
|
+
border-radius: 4px;
|
|
150
|
+
background: var(--color-base-0);
|
|
151
|
+
color: var(--color-base-600);
|
|
152
|
+
cursor: pointer;
|
|
153
|
+
font-size: 14px;
|
|
154
|
+
line-height: 1;
|
|
155
|
+
}
|
|
156
|
+
.sdocs-topbar-btn:hover {
|
|
157
|
+
background: var(--color-base-100);
|
|
158
|
+
}
|
|
159
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { SvelteComponentTyped } from "svelte";
|
|
2
|
+
declare const __propDef: {
|
|
3
|
+
props: Record<string, never>;
|
|
4
|
+
events: {
|
|
5
|
+
[evt: string]: CustomEvent<any>;
|
|
6
|
+
};
|
|
7
|
+
slots: {};
|
|
8
|
+
};
|
|
9
|
+
export type TopBarProps = typeof __propDef.props;
|
|
10
|
+
export type TopBarEvents = typeof __propDef.events;
|
|
11
|
+
export type TopBarSlots = typeof __propDef.slots;
|
|
12
|
+
export default class TopBar extends SvelteComponentTyped<TopBarProps, TopBarEvents, TopBarSlots> {
|
|
13
|
+
}
|
|
14
|
+
export {};
|
|
@@ -70,18 +70,35 @@ export const configSchema = {
|
|
|
70
70
|
doc: 'Folder of static assets served at the site root — images for pages, files for previews. Standalone CLI flows; embedded apps use the host\'s public directory.',
|
|
71
71
|
insert: ": './${0:static}'",
|
|
72
72
|
},
|
|
73
|
-
|
|
73
|
+
title: {
|
|
74
74
|
detail: 'string',
|
|
75
|
-
doc: '
|
|
75
|
+
doc: 'Header title text. Default: `sdocs`.',
|
|
76
76
|
insert: ": '${0:sdocs}'",
|
|
77
77
|
},
|
|
78
|
-
|
|
78
|
+
logo: {
|
|
79
79
|
detail: 'string | false',
|
|
80
|
-
doc: "
|
|
80
|
+
doc: "Header logo: `'sdocs'` for the built-in mascot, an image URL, or `false` to hide it. Default: `'sdocs'`.",
|
|
81
81
|
insert: ": '${0:sdocs}'",
|
|
82
82
|
values: ['sdocs'],
|
|
83
83
|
quoted: true,
|
|
84
84
|
},
|
|
85
|
+
sections: {
|
|
86
|
+
detail: 'string[]',
|
|
87
|
+
doc: 'Top-bar section order. Sections come from `@Section/…` title prefixes; unlisted sections follow alphabetically.',
|
|
88
|
+
insert: ': [$0]',
|
|
89
|
+
},
|
|
90
|
+
defaultSection: {
|
|
91
|
+
detail: 'string',
|
|
92
|
+
doc: 'Section for docs without an `@Section/` title prefix. Default: `Docs`.',
|
|
93
|
+
insert: ": '${0:Docs}'",
|
|
94
|
+
},
|
|
95
|
+
routing: {
|
|
96
|
+
detail: "'history' | 'hash'",
|
|
97
|
+
doc: "URL style: `'history'` for real paths (standalone CLI default), `'hash'` for `#/` URLs (embedded default).",
|
|
98
|
+
insert: ": '${0:history}'",
|
|
99
|
+
values: ['history', 'hash'],
|
|
100
|
+
quoted: true,
|
|
101
|
+
},
|
|
85
102
|
sidebar: {
|
|
86
103
|
detail: 'object',
|
|
87
104
|
doc: 'Sidebar ordering and default-expanded folders.',
|