sdocs 0.0.57 → 0.0.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,54 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.0.59] - 2026-07-05
11
+
12
+ ### Changed
13
+
14
+ - **Code blocks render on a dark stage.** Highlighted code — page fences and
15
+ the code panels — always uses the dark syntax theme, so blocks read as
16
+ code at a glance in both app themes (they previously rendered white-on-white
17
+ in light mode).
18
+ - **A page's body `#` title matches the entity title size** (24px/700) — it
19
+ is the page title, after all.
20
+
21
+ ### Fixed
22
+
23
+ - **Every fence gets a themed block.** `bash`, `json`, `markdown`, `yaml`,
24
+ and `diff` fences highlight properly, and unknown languages fall back to a
25
+ plaintext shiki block instead of an unstyled bare `<pre>`.
26
+
27
+ ## [0.0.58] - 2026-07-05
28
+
29
+ ### Added
30
+
31
+ - **Richer page markdown.** GitHub-style alerts (`> [!NOTE]`, `[!TIP]`,
32
+ `[!IMPORTANT]`, `[!WARNING]`, `[!CAUTION]`) render as tinted callouts;
33
+ task lists get proper checkboxes; table columns honor `:---:` alignment;
34
+ external links open in a new tab (internal ones stay in the app); images
35
+ render lazily and Svelte-safe.
36
+ - **`static` config option.** A folder of static assets served at the site
37
+ root in dev and copied into `dist/` by `sdocs build` — `![hero](/hero.png)`
38
+ in a page just works. Powers the standalone CLI flows; embedded apps keep
39
+ using the host's public directory.
40
+ - **Page column alignment.** `content.page.contentX` (config) and
41
+ `contentX` on `[PAGE]` align the content column `left`/`center`/`right`.
42
+ - **A body `#` heading takes over as the page title** — the header no longer
43
+ duplicates it; the entity `title` keeps naming the sidebar entry.
44
+
45
+ ### Changed
46
+
47
+ - **A page's `maxWidth` now bounds the content column together with its
48
+ table of contents**; when the toc is hidden the prose takes its space.
49
+
50
+ ### Fixed
51
+
52
+ - **Code fences can show component tags.** A `<Component />` line inside a
53
+ markdown fence, preceded by a blank line, was treated as a live Svelte
54
+ island and rendered (or crashed the page) instead of staying highlighted
55
+ code. Fences now shield island detection, matching the scanner and the
56
+ editor projection.
57
+
10
58
  ## [0.0.57] - 2026-07-05
11
59
 
12
60
  ### Added
@@ -19,6 +19,8 @@ export async function buildCommand() {
19
19
  await build({
20
20
  configFile: false,
21
21
  root: sdocsDir,
22
+ // The project's static assets (config `static`), copied into dist/
23
+ publicDir: config.static ?? false,
22
24
  resolve: {
23
25
  dedupe: svelteDedupe(cwd),
24
26
  },
@@ -37,6 +37,8 @@ export async function devCommand() {
37
37
  const server = await createServer({
38
38
  configFile: false,
39
39
  root: sdocsDir,
40
+ // The project's static assets (config `static`), served at the site root
41
+ publicDir: config.static ?? false,
40
42
  resolve: {
41
43
  dedupe: svelteDedupe(cwd),
42
44
  },
@@ -40,6 +40,11 @@
40
40
  function scrollToHeading(id: string) {
41
41
  container?.querySelector(`#${CSS.escape(id)}`)?.scrollIntoView({ behavior: 'smooth' });
42
42
  }
43
+
44
+ // contentX aligns the whole column (content + toc) inside the view.
45
+ const columnMargin = $derived(
46
+ doc.contentX === 'center' ? '0 auto' : doc.contentX === 'right' ? '0 0 0 auto' : undefined,
47
+ );
43
48
  </script>
44
49
 
45
50
  {#snippet exampleFrame(index: number)}
@@ -65,52 +70,61 @@
65
70
  {/snippet}
66
71
 
67
72
  <div class="sdocs-page-view" style:padding={doc.padding}>
68
- <div class="sdocs-page-main" style:max-width={doc.maxWidth}>
69
- <!-- Header -->
70
- <div class="sdocs-view-header">
71
- <h1 class="sdocs-view-title">{displayTitle(meta.title)}</h1>
72
- {#if meta.description}
73
- <p class="sdocs-view-description">{meta.description}</p>
73
+ <!-- maxWidth constrains the whole column — content plus toc; contentX
74
+ places it. Without a toc the content takes the toc's space. -->
75
+ <div class="sdocs-page-inner" style:max-width={doc.maxWidth} style:margin={columnMargin}>
76
+ <div class="sdocs-page-main">
77
+ <!-- Header: a body `#` heading takes over as the page title -->
78
+ {#if !doc.bodyTitle}
79
+ <div class="sdocs-view-header">
80
+ <h1 class="sdocs-view-title">{displayTitle(meta.title)}</h1>
81
+ {#if meta.description}
82
+ <p class="sdocs-view-description">{meta.description}</p>
83
+ {/if}
84
+ </div>
74
85
  {/if}
75
- </div>
76
86
 
77
- <!-- Content -->
78
- <div class="sdocs-page-content" bind:this={container}>
79
- {#if PageComponent}
80
- <PageComponent __sdocsExample={exampleFrame} />
81
- {/if}
87
+ <!-- Content -->
88
+ <div class="sdocs-page-content" bind:this={container}>
89
+ {#if PageComponent}
90
+ <PageComponent __sdocsExample={exampleFrame} />
91
+ {/if}
92
+ </div>
82
93
  </div>
83
- </div>
84
94
 
85
- <!-- Table of Contents -->
86
- {#if toc.length > 0 && doc.showToc !== false}
87
- <aside class="sdocs-toc">
88
- <h3 class="sdocs-toc-title">On this page</h3>
89
- <nav>
90
- <ul class="sdocs-toc-list">
91
- {#each toc as heading (heading.id)}
92
- <li class="sdocs-toc-item" style:padding-left="{(heading.level - 2) * 12}px">
93
- <button
94
- class="sdocs-toc-link"
95
- onclick={() => scrollToHeading(heading.id)}
96
- >
97
- {heading.text}
98
- </button>
99
- </li>
100
- {/each}
101
- </ul>
102
- </nav>
103
- </aside>
104
- {/if}
95
+ <!-- Table of Contents -->
96
+ {#if toc.length > 0 && doc.showToc !== false}
97
+ <aside class="sdocs-toc">
98
+ <h3 class="sdocs-toc-title">On this page</h3>
99
+ <nav>
100
+ <ul class="sdocs-toc-list">
101
+ {#each toc as heading (heading.id)}
102
+ <li class="sdocs-toc-item" style:padding-left="{(heading.level - 2) * 12}px">
103
+ <button
104
+ class="sdocs-toc-link"
105
+ onclick={() => scrollToHeading(heading.id)}
106
+ >
107
+ {heading.text}
108
+ </button>
109
+ </li>
110
+ {/each}
111
+ </ul>
112
+ </nav>
113
+ </aside>
114
+ {/if}
115
+ </div>
105
116
  </div>
106
117
 
107
118
  <style>
108
119
  .sdocs-page-view {
109
- display: flex;
110
- gap: 24px;
111
120
  /* padding comes from the doc entry (config/entity cascade) */
112
121
  font-family: var(--sans);
113
122
  }
123
+ .sdocs-page-inner {
124
+ display: flex;
125
+ gap: 24px;
126
+ /* max-width and margin (contentX) come from the doc entry */
127
+ }
114
128
  .sdocs-page-main {
115
129
  flex: 1;
116
130
  min-width: 0;
@@ -148,7 +162,11 @@
148
162
  margin: 1.6em 0 0.5em;
149
163
  scroll-margin-top: 16px;
150
164
  }
151
- .sdocs-page-content :global(h1) { font-size: 22px; }
165
+ /* The body h1 serves as the page title — match .sdocs-view-title */
166
+ .sdocs-page-content :global(h1) {
167
+ font-size: 24px;
168
+ font-weight: 700;
169
+ }
152
170
  .sdocs-page-content :global(h2) { font-size: 18px; }
153
171
  .sdocs-page-content :global(h3) { font-size: 15px; }
154
172
  .sdocs-page-content :global(h4),
@@ -207,6 +225,9 @@
207
225
  .sdocs-page-content :global(table) {
208
226
  border-collapse: collapse;
209
227
  margin: 0.9em 0;
228
+ display: block;
229
+ max-width: 100%;
230
+ overflow-x: auto;
210
231
  }
211
232
  .sdocs-page-content :global(th),
212
233
  .sdocs-page-content :global(td) {
@@ -214,12 +235,61 @@
214
235
  padding: 6px 12px;
215
236
  text-align: left;
216
237
  }
238
+ .sdocs-page-content :global(td[align='center']),
239
+ .sdocs-page-content :global(th[align='center']) {
240
+ text-align: center;
241
+ }
242
+ .sdocs-page-content :global(td[align='right']),
243
+ .sdocs-page-content :global(th[align='right']) {
244
+ text-align: right;
245
+ }
217
246
  .sdocs-page-content :global(th) {
218
247
  background: var(--color-base-50);
219
248
  font-weight: 600;
220
249
  }
221
250
  .sdocs-page-content :global(img) {
222
251
  max-width: 100%;
252
+ border-radius: 6px;
253
+ }
254
+
255
+ /* Task lists: GitHub-style checkboxes, no bullet */
256
+ .sdocs-page-content :global(li:has(> input[type='checkbox']:first-child)) {
257
+ list-style: none;
258
+ margin-left: -1.3em;
259
+ }
260
+ .sdocs-page-content :global(li > input[type='checkbox']) {
261
+ margin-right: 6px;
262
+ vertical-align: -2px;
263
+ }
264
+
265
+ /* GitHub-style alerts: > [!NOTE] / [!TIP] / [!IMPORTANT] / [!WARNING] / [!CAUTION] */
266
+ .sdocs-page-content :global(.sdocs-alert) {
267
+ margin: 0.9em 0;
268
+ padding: 8px 16px;
269
+ border-left: 3px solid var(--sdocs-alert-color);
270
+ border-radius: 0 6px 6px 0;
271
+ background: color-mix(in srgb, var(--sdocs-alert-color) 6%, transparent);
272
+ }
273
+ .sdocs-page-content :global(.sdocs-alert-label) {
274
+ font-size: 13px;
275
+ font-weight: 650;
276
+ color: var(--sdocs-alert-color);
277
+ margin: 0.25em 0 0;
278
+ }
279
+ .sdocs-page-content :global(.sdocs-alert-note) {
280
+ --sdocs-alert-color: var(--color-blue-500);
281
+ }
282
+ .sdocs-page-content :global(.sdocs-alert-tip) {
283
+ --sdocs-alert-color: var(--color-green-500);
284
+ }
285
+ .sdocs-page-content :global(.sdocs-alert-important) {
286
+ --sdocs-alert-color: var(--color-purple-500);
287
+ }
288
+ .sdocs-page-content :global(.sdocs-alert-warning) {
289
+ --sdocs-alert-color: var(--color-amber-500);
290
+ }
291
+ .sdocs-page-content :global(.sdocs-alert-caution) {
292
+ --sdocs-alert-color: var(--color-red-500);
223
293
  }
224
294
 
225
295
  /* ── Example stages in the page flow ── */
@@ -65,6 +65,11 @@ export const configSchema = {
65
65
  doc: 'CSS loaded inside preview iframes — a single stylesheet path, or a map of named stylesheets to switch between.',
66
66
  insert: ": '$0'",
67
67
  },
68
+ static: {
69
+ detail: 'string',
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
+ insert: ": './${0:static}'",
72
+ },
68
73
  logo: {
69
74
  detail: 'string',
70
75
  doc: 'Sidebar logo text. Default: `sdocs`.',
@@ -101,7 +106,7 @@ export const configSchema = {
101
106
  object: {
102
107
  page: {
103
108
  detail: 'object',
104
- doc: '`[PAGE]` content. Defaults: `maxWidth` `1200px`, `padding` `32px`, `toc` `true`.',
109
+ doc: '`[PAGE]` content. Defaults: `maxWidth` `1200px`, `padding` `32px`, `toc` `true`, `contentX` `left`.',
105
110
  insert: ': {\n\t$0\n}',
106
111
  object: {
107
112
  maxWidth,
@@ -112,6 +117,13 @@ export const configSchema = {
112
117
  insert: ': ${0:true}',
113
118
  values: ['true', 'false'],
114
119
  },
120
+ contentX: {
121
+ detail: "'left' | 'center' | 'right'",
122
+ doc: 'Horizontal alignment of the page content column (with its toc). Default: `left`.',
123
+ insert: ": '${0:center}'",
124
+ values: ['left', 'center', 'right'],
125
+ quoted: true,
126
+ },
115
127
  },
116
128
  },
117
129
  docs: {
@@ -19,5 +19,7 @@ export interface PageSegment {
19
19
  /** Verbatim lines of the segment */
20
20
  lines: string[];
21
21
  }
22
- /** Split a page body into prose and island segments. */
22
+ /** Split a page body into prose and island segments. Lines inside markdown
23
+ * code fences are always prose — a component tag shown in a fence must stay
24
+ * displayed code, never a live island. */
23
25
  export declare function segmentPageBody(body: string): PageSegment[];
@@ -80,7 +80,9 @@ function lineDepths(line) {
80
80
  }
81
81
  return { svelte, html };
82
82
  }
83
- /** Split a page body into prose and island segments. */
83
+ /** Split a page body into prose and island segments. Lines inside markdown
84
+ * code fences are always prose — a component tag shown in a fence must stay
85
+ * displayed code, never a live island. */
84
86
  export function segmentPageBody(body) {
85
87
  const lines = body.split('\n');
86
88
  const segments = [];
@@ -92,9 +94,21 @@ export function segmentPageBody(body) {
92
94
  }
93
95
  current.lines.push(line);
94
96
  };
97
+ let inFence = false;
95
98
  let i = 0;
96
99
  while (i < lines.length) {
97
100
  const line = lines[i];
101
+ if (/^\s*(`{3,}|~{3,})/.test(line)) {
102
+ inFence = !inFence;
103
+ push('prose', line);
104
+ i++;
105
+ continue;
106
+ }
107
+ if (inFence) {
108
+ push('prose', line);
109
+ i++;
110
+ continue;
111
+ }
98
112
  const prevBlank = i === 0 || lines[i - 1].trim() === '';
99
113
  if (prevBlank && line.trim() !== '' && startsIsland(line)) {
100
114
  // Consume the island: until Svelte blocks and HTML tags balance out.
@@ -75,6 +75,8 @@ const ENTITY_ATTR_RULES = {
75
75
  PAGE: {
76
76
  title: { required: true, kind: 'string', hint: 'title="Group / Name"' },
77
77
  ...SIZING_ATTR_RULES,
78
+ // On PAGE, contentX aligns the content column (with its toc), not a stage.
79
+ contentX: { required: false, kind: 'string', hint: 'contentX="center"' },
78
80
  toc: { required: false, kind: 'string', hint: 'toc="false"' },
79
81
  },
80
82
  LAYOUT: {
@@ -7,6 +7,7 @@ const DEFAULTS = {
7
7
  port: 3000,
8
8
  open: false,
9
9
  css: null,
10
+ static: null,
10
11
  logo: 'sdocs',
11
12
  icon: 'sdocs',
12
13
  sidebar: {
@@ -14,7 +15,7 @@ const DEFAULTS = {
14
15
  open: [],
15
16
  },
16
17
  content: {
17
- page: { maxWidth: '1200px', padding: '32px', toc: true },
18
+ page: { maxWidth: '1200px', padding: '32px', toc: true, contentX: 'left' },
18
19
  docs: {
19
20
  maxWidth: '1200px',
20
21
  padding: '16px',
@@ -69,6 +70,8 @@ export function resolveAndFinalize(userConfig, root) {
69
70
  const resolved = resolveConfig(userConfig);
70
71
  resolved.include = resolveIncludePatterns(resolved.include, root);
71
72
  resolved.css = resolveCssPaths(resolved.css, root);
73
+ if (resolved.static)
74
+ resolved.static = resolve(root, resolved.static);
72
75
  return resolved;
73
76
  }
74
77
  /** Import a config file (.js/.mjs; .ts only on Node with native type stripping) */
@@ -89,6 +92,7 @@ export function resolveConfig(userConfig) {
89
92
  port: userConfig.port ?? DEFAULTS.port,
90
93
  open: userConfig.open ?? DEFAULTS.open,
91
94
  css: userConfig.css ?? DEFAULTS.css,
95
+ static: userConfig.static ?? DEFAULTS.static,
92
96
  logo: userConfig.logo ?? DEFAULTS.logo,
93
97
  icon: userConfig.icon ?? DEFAULTS.icon,
94
98
  sidebar: {
@@ -1,4 +1,5 @@
1
- /** Highlight source code and return HTML */
1
+ /** Highlight source code and return HTML. Unknown languages render as
2
+ * plaintext, so a fence is always a themed shiki block, never a bare pre. */
2
3
  export declare function highlight(code: string, lang?: string): Promise<string>;
3
4
  /** Dispose the highlighter (for cleanup) */
4
5
  export declare function disposeHighlighter(): Promise<void>;
@@ -5,16 +5,21 @@ async function getHighlighter() {
5
5
  if (!highlighterPromise) {
6
6
  highlighterPromise = createHighlighter({
7
7
  themes: ['github-light', 'github-dark'],
8
- langs: ['svelte', 'typescript', 'javascript', 'css', 'html'],
8
+ langs: [
9
+ 'svelte', 'typescript', 'javascript', 'css', 'html',
10
+ 'bash', 'json', 'markdown', 'yaml', 'diff',
11
+ ],
9
12
  });
10
13
  }
11
14
  return highlighterPromise;
12
15
  }
13
- /** Highlight source code and return HTML */
16
+ /** Highlight source code and return HTML. Unknown languages render as
17
+ * plaintext, so a fence is always a themed shiki block, never a bare pre. */
14
18
  export async function highlight(code, lang = 'svelte') {
15
19
  const highlighter = await getHighlighter();
20
+ const resolved = highlighter.getLoadedLanguages().includes(lang) ? lang : 'text';
16
21
  return highlighter.codeToHtml(code, {
17
- lang,
22
+ lang: resolved,
18
23
  themes: {
19
24
  light: 'github-light',
20
25
  dark: 'github-dark',
@@ -11,6 +11,9 @@ import type { TocHeading } from '../types.js';
11
11
  export interface RenderedPage {
12
12
  html: string;
13
13
  toc: TocHeading[];
14
+ /** Text of the body's first `#` heading, when present — it takes over as
15
+ * the page's displayed title. */
16
+ bodyTitle?: string;
14
17
  }
15
18
  /**
16
19
  * Render a [PAGE] body: Svelte islands (see segmentPageBody) pass through
@@ -97,26 +97,56 @@ function plainText(tokens) {
97
97
  export async function renderPageMarkdown(source) {
98
98
  const toc = [];
99
99
  const usedIds = new Set();
100
+ const state = { toc, usedIds, bodyTitle: undefined };
100
101
  const parts = [];
101
102
  for (const segment of segmentPageBody(source)) {
102
103
  if (segment.kind === 'island') {
103
104
  parts.push(segment.lines.join('\n'));
104
105
  }
105
106
  else {
106
- parts.push(await renderProse(segment.lines.join('\n'), toc, usedIds));
107
+ parts.push(await renderProse(segment.lines.join('\n'), state));
107
108
  }
108
109
  }
109
- return { html: parts.join('\n'), toc };
110
+ return { html: parts.join('\n'), toc, bodyTitle: state.bodyTitle };
110
111
  }
111
- async function renderProse(source, toc, usedIds) {
112
+ /** GitHub-style alert kinds recognized on a blockquote's first line. */
113
+ const ALERT_KINDS = ['NOTE', 'TIP', 'IMPORTANT', 'WARNING', 'CAUTION'];
114
+ const ALERT_RE = new RegExp(`^\\[!(${ALERT_KINDS.join('|')})\\][ \\t]*(?:\\n|$)`);
115
+ async function renderProse(source, state) {
116
+ const { toc, usedIds } = state;
112
117
  const headingIds = new WeakMap();
113
118
  const fenceHtml = new WeakMap();
119
+ const alertKinds = new WeakMap();
114
120
  const marked = new Marked({ gfm: true });
115
121
  const tokens = marked.lexer(source);
122
+ /** Detect a GitHub-style alert marker on a blockquote's first line and
123
+ * strip it from the tokens; the renderer wraps the rest as a callout. */
124
+ const detectAlert = (token) => {
125
+ if (!('tokens' in token) || !token.tokens)
126
+ return;
127
+ const para = token.tokens[0];
128
+ if (para?.type !== 'paragraph' || !para.tokens)
129
+ return;
130
+ const first = para.tokens[0];
131
+ if (first?.type !== 'text')
132
+ return;
133
+ const m = ALERT_RE.exec(first.text ?? '');
134
+ if (!m)
135
+ return;
136
+ alertKinds.set(token, m[1]);
137
+ first.text = first.text.slice(m[0].length);
138
+ if (first.text === '')
139
+ para.tokens.shift();
140
+ if (para.tokens.length === 0)
141
+ token.tokens.shift();
142
+ };
116
143
  const walk = (list) => {
117
144
  for (const token of list) {
118
145
  if (token.type === 'heading') {
119
146
  const text = plainText(token.tokens ?? []);
147
+ if (token.depth === 1 && state.bodyTitle === undefined) {
148
+ state.bodyTitle = text;
149
+ }
120
150
  const baseId = slugify(text);
121
151
  let id = baseId;
122
152
  for (let n = 2; usedIds.has(id); n++)
@@ -127,6 +157,8 @@ async function renderProse(source, toc, usedIds) {
127
157
  toc.push({ text, level: token.depth, id });
128
158
  }
129
159
  }
160
+ if (token.type === 'blockquote')
161
+ detectAlert(token);
130
162
  if ('tokens' in token && token.tokens)
131
163
  walk(token.tokens);
132
164
  if ('items' in token && token.items)
@@ -155,6 +187,31 @@ async function renderProse(source, toc, usedIds) {
155
187
  ? `<h${token.depth} id="${id}">${html}</h${token.depth}>\n`
156
188
  : `<h${token.depth}>${html}</h${token.depth}>\n`;
157
189
  },
190
+ // GitHub-style alerts: `> [!NOTE]` blockquotes become callouts.
191
+ blockquote(token) {
192
+ const body = this.parser.parse(token.tokens ?? []);
193
+ const kind = alertKinds.get(token);
194
+ if (!kind)
195
+ return `<blockquote>\n${body}</blockquote>\n`;
196
+ const label = kind.charAt(0) + kind.slice(1).toLowerCase();
197
+ return `<div class="sdocs-alert sdocs-alert-${kind.toLowerCase()}"><p class="sdocs-alert-label">${label}</p>\n${body}</div>\n`;
198
+ },
199
+ // External links open away from the docs app; hrefs stay inert.
200
+ link(token) {
201
+ const inner = this.parser.parseInline(token.tokens ?? []);
202
+ const href = escapeBraces(escapeHtml(token.href ?? ''));
203
+ const title = token.title ? ` title="${escapeBraces(escapeHtml(token.title))}"` : '';
204
+ const external = /^https?:\/\//i.test(token.href ?? '');
205
+ const attrs = external ? ' target="_blank" rel="noopener noreferrer"' : '';
206
+ return `<a href="${href}"${title}${attrs}>${inner}</a>`;
207
+ },
208
+ // Self-closing and brace-escaped so images are always Svelte-safe.
209
+ image(token) {
210
+ const src = escapeBraces(escapeHtml(token.href ?? ''));
211
+ const alt = escapeBraces(escapeHtml(token.text ?? ''));
212
+ const title = token.title ? ` title="${escapeBraces(escapeHtml(token.title))}"` : '';
213
+ return `<img src="${src}" alt="${alt}"${title} loading="lazy" />`;
214
+ },
158
215
  code(token) {
159
216
  return (fenceHtml.get(token) ??
160
217
  `<pre><code>${escapeBraces(escapeHtml(token.text))}</code></pre>\n`);
package/dist/types.d.ts CHANGED
@@ -8,6 +8,10 @@ export interface SdocsConfig {
8
8
  open?: boolean;
9
9
  /** CSS loaded in preview iframes. Single path or named stylesheets. */
10
10
  css?: string | Record<string, string>;
11
+ /** Folder of static assets served at the site root — images for pages,
12
+ * files for previews. Standalone CLI flows (`sdocs dev`/`build`); when
13
+ * embedding the Vite plugin, use the host app's own public directory. */
14
+ static?: string;
11
15
  /** Sidebar logo text. Default: 'sdocs' */
12
16
  logo?: string;
13
17
  /** Sidebar logo icon: 'sdocs' for the built-in mascot, an image URL, or false to hide. Default: 'sdocs' */
@@ -25,6 +29,9 @@ export interface SdocsConfig {
25
29
  page?: ContentSizing & {
26
30
  /** Show the table of contents. Default: true */
27
31
  toc?: boolean;
32
+ /** Horizontal alignment of the content column (with its toc) inside
33
+ * the view: 'left'|'center'|'right'. Default: 'left' */
34
+ contentX?: string;
28
35
  };
29
36
  /** [DOCS] pages: maxWidth is the content column (default '1200px');
30
37
  * padding/direction/gap are the default preview/example stage layout
@@ -66,6 +73,7 @@ export interface ResolvedSdocsConfig {
66
73
  port: number;
67
74
  open: boolean;
68
75
  css: string | Record<string, string> | null;
76
+ static: string | null;
69
77
  logo: string;
70
78
  icon: string | false;
71
79
  sidebar: {
@@ -75,6 +83,7 @@ export interface ResolvedSdocsConfig {
75
83
  content: {
76
84
  page: Required<ContentSizing> & {
77
85
  toc: boolean;
86
+ contentX: string;
78
87
  };
79
88
  docs: Required<ContentSizing> & {
80
89
  direction: string;
@@ -186,8 +195,13 @@ export interface DocEntry {
186
195
  maxWidth?: string;
187
196
  /** Resolved page padding (pages only) */
188
197
  padding?: string;
198
+ /** Resolved horizontal alignment of the content column (pages only) */
199
+ contentX?: string;
189
200
  /** Resolved table-of-contents visibility (pages only) */
190
201
  showToc?: boolean;
202
+ /** The body's `#` heading, shown as the page title instead of the entity
203
+ * title (pages only) */
204
+ bodyTitle?: string;
191
205
  /** Key into the virtual module's pageModules map (pages only) */
192
206
  contentKey?: string;
193
207
  }
@@ -28,6 +28,15 @@
28
28
  text-decoration: none;
29
29
  }
30
30
 
31
+ /* Highlighted code blocks always render shiki's dark theme — a dark stage
32
+ * for code in both app themes (the inline light-theme colors lose to the
33
+ * dual-theme --shiki-dark variables). */
34
+ .sdocs-app pre.shiki,
35
+ .sdocs-app pre.shiki span {
36
+ color: var(--shiki-dark) !important;
37
+ background-color: var(--shiki-dark-bg) !important;
38
+ }
39
+
31
40
  /* Type chips and value literals, color-coded by kind (shared by doc tables) */
32
41
  .sdocs-app code[class*='sdocs-type-'] {
33
42
  font-family: var(--mono);
package/dist/vite.js CHANGED
@@ -386,6 +386,8 @@ export function sdocsPlugin(userConfig) {
386
386
  entry.content = snippets[0];
387
387
  entry.toc = rendered.toc;
388
388
  entry.padding = entity.sizing.padding ?? config.content.page.padding;
389
+ entry.contentX = entity.sizing.contentX ?? config.content.page.contentX;
390
+ entry.bodyTitle = rendered.bodyTitle;
389
391
  entry.contentKey = encodeEntityId(filePath, entity.slug);
390
392
  entry.examples = snippets.filter((s) => s.role === 'example');
391
393
  entity.examples.forEach((example, i) => {
@@ -436,7 +438,9 @@ export function sdocsPlugin(userConfig) {
436
438
  toc: e.toc,
437
439
  maxWidth: e.maxWidth,
438
440
  padding: e.padding,
441
+ contentX: e.contentX,
439
442
  showToc: e.showToc,
443
+ bodyTitle: e.bodyTitle,
440
444
  contentKey: e.contentKey,
441
445
  }));
442
446
  // Extract named CSS stylesheet names (empty array if single string or null)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdocs",
3
- "version": "0.0.57",
3
+ "version": "0.0.59",
4
4
  "description": "A lightweight documentation tool for Svelte 5 components",
5
5
  "type": "module",
6
6
  "license": "MIT",