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.
@@ -1,9 +1,109 @@
1
- /** Build a tree from flat doc entries */
2
- export function buildTree(docs, sidebar) {
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 ?? 'Untitled';
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. A leading ':' on the first segment is a sidebar
177
- * grouping directive (see buildTree), not part of the displayed title, so it
178
- * is stripped for headings.
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 ?? '').replace(/^:\s*/, '');
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
- logo: string;
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
- <Icon name="sdocs" --w="160px" --h="160px" --fill="#FC1D29" />
20
- <h1 class="sdocs-home-logo">{logo}</h1>
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 { pathToHash } from '../router.svelte.js';
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
- currentPath: string[];
13
- logo: string;
14
- icon?: string | false;
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, currentPath, logo, icon = 'sdocs', cssNames = [], activeStylesheet, theme = 'light', onToggleFullscreen, onStylesheetChange, onThemeChange }: Props = $props();
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(nodePath: string[]): boolean {
89
- if (nodePath.length > currentPath.length) return false;
90
- return nodePath.every((seg, i) => seg === currentPath[i]);
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(nodePath: string[]): boolean {
94
- return nodePath.length === currentPath.length && nodePath.every((seg, i) => seg === currentPath[i]);
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
- <div class="sdocs-sidebar-header">
188
- <a href="#/" class="sdocs-logo">
189
- {#if icon === 'sdocs'}
190
- <Icon name="sdocs" --w="22px" --h="22px" --fill="#FC1D29" />
191
- {:else if icon}
192
- <img class="sdocs-logo-img" src={icon} alt="" />
193
- {/if}
194
- {logo}
195
- </a>
196
- <div class="sdocs-header-actions">
197
- {#if cssNames.length > 1}
198
- <select
199
- class="sdocs-css-picker"
200
- value={activeStylesheet}
201
- onchange={(e) => onStylesheetChange?.(e.currentTarget.value)}
202
- >
203
- {#each cssNames as name (name)}
204
- <option value={name}>{name}</option>
205
- {/each}
206
- </select>
207
- {/if}
208
- <button class="sdocs-theme-btn" onclick={toggleTheme} title="{themeLabels[theme]} theme">
209
- {themeIcons[theme]}
210
- </button>
211
- <button class="sdocs-fullscreen-btn" onclick={() => onToggleFullscreen?.()} title="Fullscreen">
212
- &#x26F6;
213
- </button>
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
+ &#x26F6;
219
+ </button>
220
+ </div>
214
221
  </div>
215
- </div>
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.path)}
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
- window.location.hash = pathToHash(node.path);
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={pathToHash(node.path)}
272
- active={isExactActive(node.path)}
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: 100vh;
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
+ &#x26F6;
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
- logo: {
73
+ title: {
74
74
  detail: 'string',
75
- doc: 'Sidebar logo text. Default: `sdocs`.',
75
+ doc: 'Header title text. Default: `sdocs`.',
76
76
  insert: ": '${0:sdocs}'",
77
77
  },
78
- icon: {
78
+ logo: {
79
79
  detail: 'string | false',
80
- doc: "Sidebar logo icon: `'sdocs'` for the built-in mascot, an image URL, or `false` to hide it. Default: `'sdocs'`.",
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.',