sdocs 0.0.18 → 0.0.20

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/README.md ADDED
@@ -0,0 +1,281 @@
1
+ # sdocs
2
+
3
+ A lightweight documentation tool for Svelte 5 components. Discover `.sdoc` files in your project and get an interactive component explorer with live previews, prop controls, and code highlighting.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # Initialize config
9
+ npx sdocs init
10
+
11
+ # Start dev server
12
+ npx sdocs dev
13
+ ```
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install sdocs
19
+ ```
20
+
21
+ **Requirements:** Svelte 5, Vite 6+, `@sveltejs/vite-plugin-svelte` 5+
22
+
23
+ ## Usage
24
+
25
+ sdocs can be used in two ways: as a **standalone CLI tool** or **embedded in your existing project**.
26
+
27
+ ### Standalone (CLI)
28
+
29
+ Run sdocs as its own dev server:
30
+
31
+ ```bash
32
+ npx sdocs dev # Start dev server with HMR
33
+ npx sdocs build # Build static documentation site
34
+ npx sdocs preview # Preview the built site locally
35
+ npx sdocs init # Scaffold a sdocs.config.js file
36
+ ```
37
+
38
+ ### Embedded in a SvelteKit / Vite Project
39
+
40
+ Use sdocs as a Vite plugin inside your existing project. This way sdocs runs alongside your app without needing a separate server.
41
+
42
+ **1. Add the Vite plugin**
43
+
44
+ ```js
45
+ // vite.config.js
46
+ import { sveltekit } from '@sveltejs/kit/vite';
47
+ import { sdocsPlugin } from 'sdocs/vite';
48
+
49
+ export default {
50
+ plugins: [
51
+ sveltekit(),
52
+ sdocsPlugin({
53
+ include: ['./src/lib/**/*.sdoc'],
54
+ css: './src/styles/global.css',
55
+ logo: 'My Design System',
56
+ })
57
+ ],
58
+ };
59
+ ```
60
+
61
+ The plugin discovers `.sdoc` files and exposes them via a `virtual:sdocs` module.
62
+
63
+ **2. Create a page that mounts the sdocs app**
64
+
65
+ ```svelte
66
+ <!-- src/routes/docs/+page.svelte -->
67
+ <script>
68
+ import App from 'sdocs/client';
69
+ import { docs, cssNames } from 'virtual:sdocs';
70
+ </script>
71
+
72
+ <App
73
+ {docs}
74
+ {cssNames}
75
+ logo="My Design System"
76
+ sidebarConfig={{
77
+ order: { root: ['Components', '*'] },
78
+ open: ['Components'],
79
+ }}
80
+ />
81
+ ```
82
+
83
+ **3. Add the virtual module type declaration** (optional, for TypeScript)
84
+
85
+ ```ts
86
+ // src/app.d.ts or any .d.ts file
87
+ declare module 'virtual:sdocs' {
88
+ import type { DocEntry } from 'sdocs';
89
+ export const docs: DocEntry[];
90
+ export const cssNames: string[];
91
+ export default docs;
92
+ }
93
+ ```
94
+
95
+ That's it — your docs page lives at `/docs` inside your existing app.
96
+
97
+ ## Writing Docs
98
+
99
+ sdocs supports three types of doc files:
100
+
101
+ ### Component Docs (`.sdoc`)
102
+
103
+ Document a Svelte component with interactive controls and examples.
104
+
105
+ ```svelte
106
+ <!-- Button.sdoc -->
107
+ <script lang="ts">
108
+ import Button from './Button.svelte';
109
+
110
+ export const meta = {
111
+ component: Button,
112
+ title: 'Components / Button',
113
+ description: 'A flexible button component.',
114
+ args: {
115
+ label: 'Click me',
116
+ size: 'md',
117
+ disabled: false,
118
+ },
119
+ };
120
+ </script>
121
+
122
+ {#snippet Default()}
123
+ <Button {...args} />
124
+ {/snippet}
125
+
126
+ {#snippet WithIcon()}
127
+ <Button>
128
+ <Icon name="settings" /> Settings
129
+ </Button>
130
+ {/snippet}
131
+ ```
132
+
133
+ - **`component`** — the Svelte component to document (auto-extracts props, events, snippets, methods, state, CSS custom properties)
134
+ - **`title`** — slash-separated path for sidebar navigation (e.g. `'Components / Button'`)
135
+ - **`args`** — default prop values, used as initial values for interactive controls
136
+ - **`Default` snippet** — gets live interactive controls. Auto-generated as `<Component {...args} />` if omitted.
137
+ - **Named snippets** — static examples listed in the sidebar
138
+
139
+ ### Page Docs (`.page.sdoc`)
140
+
141
+ Freeform content pages with auto-generated table of contents.
142
+
143
+ ```svelte
144
+ <!-- GettingStarted.page.sdoc -->
145
+ <script lang="ts">
146
+ export const meta = {
147
+ title: 'Docs / Getting Started',
148
+ description: 'How to set up sdocs.',
149
+ };
150
+ </script>
151
+
152
+ <h1>Getting Started</h1>
153
+ <p>Install sdocs and create your first doc file.</p>
154
+
155
+ <h2>Installation</h2>
156
+ <p>Run <code>npm install sdocs</code>...</p>
157
+ ```
158
+
159
+ Table of contents is auto-generated from `<h2>`, `<h3>`, and `<h4>` headings.
160
+
161
+ ### Layout Docs (`.layout.sdoc`)
162
+
163
+ Component compositions rendered in an isolated iframe.
164
+
165
+ ```svelte
166
+ <!-- LoginForm.layout.sdoc -->
167
+ <script lang="ts">
168
+ import Card from './Card.svelte';
169
+ import Input from './Input.svelte';
170
+ import Button from './Button.svelte';
171
+
172
+ export const meta = {
173
+ title: 'Patterns / Login Form',
174
+ description: 'A login form combining multiple components.',
175
+ settings: { padding: '24px' },
176
+ };
177
+ </script>
178
+
179
+ <Card padding="24px">
180
+ <Input label="Email" type="email" />
181
+ <Input label="Password" type="password" />
182
+ <Button>Sign in</Button>
183
+ </Card>
184
+ ```
185
+
186
+ ## Prop Extraction
187
+
188
+ sdocs automatically extracts from your Svelte components:
189
+
190
+ | What | Source |
191
+ |------|--------|
192
+ | **Props** | `$props()` destructuring + `interface Props {}` |
193
+ | **Events** | Callback props (`onclick`, `onchange`, etc.) |
194
+ | **Snippets** | Props typed as `Snippet` or `Snippet<[...]>` |
195
+ | **Methods** | Exported functions |
196
+ | **State** | Exported `$state` / `$derived` values |
197
+ | **CSS Custom Properties** | `var(--name)` usages in `<style>` |
198
+
199
+ JSDoc comments on props are picked up as descriptions.
200
+
201
+ ## Interactive Controls
202
+
203
+ The Default snippet gets live controls based on prop types:
204
+
205
+ | Prop Type | Control |
206
+ |-----------|---------|
207
+ | `string` | Text input |
208
+ | `number` | Number input |
209
+ | `boolean` | Checkbox |
210
+ | Color (`#hex`) | Color picker |
211
+ | Dimension (`16px`) | Number + unit |
212
+
213
+ ## Configuration
214
+
215
+ Create `sdocs.config.js` in your project root (or run `npx sdocs init`):
216
+
217
+ ```js
218
+ /** @type {import('sdocs').SdocsConfig} */
219
+ export default {
220
+ // Glob pattern(s) to find .sdoc files
221
+ include: ['./src/**/*.sdoc'],
222
+
223
+ // Dev server port
224
+ port: 3000,
225
+
226
+ // Open browser on start
227
+ open: true,
228
+
229
+ // Sidebar logo text
230
+ logo: 'My Design System',
231
+
232
+ // CSS loaded in preview iframes
233
+ css: './src/styles/global.css',
234
+
235
+ // Sidebar ordering
236
+ sidebar: {
237
+ order: {
238
+ root: ['Components', '*', 'Documentation'],
239
+ },
240
+ open: ['Components'],
241
+ },
242
+ };
243
+ ```
244
+
245
+ ### CSS Stylesheet Switching
246
+
247
+ Provide named stylesheets to let users switch between them (e.g. light/dark themes):
248
+
249
+ ```js
250
+ css: {
251
+ light: './src/styles/light.css',
252
+ dark: './src/styles/dark.css',
253
+ }
254
+ ```
255
+
256
+ ### Sidebar Ordering
257
+
258
+ Control the order of items in the sidebar with `sidebar.order`. Use `'*'` as a wildcard for remaining items in alphabetical order:
259
+
260
+ ```js
261
+ sidebar: {
262
+ order: {
263
+ root: ['Getting Started', 'Components', '*'],
264
+ Components: ['Button', 'Input', '*'],
265
+ },
266
+ open: ['Components'],
267
+ }
268
+ ```
269
+
270
+ ## Package Exports
271
+
272
+ | Export | Description |
273
+ |--------|-------------|
274
+ | `sdocs` | Main entry — `sdocsPlugin` + types |
275
+ | `sdocs/vite` | Vite plugin function |
276
+ | `sdocs/client` | App.svelte UI component |
277
+ | `sdocs/ui` | Reusable UI components (Button, Frame, Icon, Control, NavTree, Stack) |
278
+
279
+ ## License
280
+
281
+ MIT
@@ -8,7 +8,7 @@
8
8
  import LayoutView from './views/LayoutView.svelte';
9
9
  import HomePage from './views/HomePage.svelte';
10
10
  import { onMount } from 'svelte';
11
- import '../ui/styles/theme.css';
11
+ import '../ui/styles/sdocs.css';
12
12
 
13
13
  type ThemeMode = 'light' | 'dark';
14
14
 
@@ -1,5 +1,5 @@
1
1
  import type { DocEntry } from '../types.js';
2
- import '../ui/styles/theme.css';
2
+ import '../ui/styles/sdocs.css';
3
3
  interface Props {
4
4
  docs: DocEntry[];
5
5
  logo?: string;
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
+ import { Icon } from '../../ui/Icon/index.js';
3
4
 
4
5
  interface Props {
5
6
  title: string;
@@ -12,9 +13,11 @@
12
13
  </script>
13
14
 
14
15
  <div class="sdocs-panel">
15
- <button class="sdocs-panel-header" onclick={() => expanded = !expanded}>
16
- <span class="sdocs-panel-arrow" class:expanded>&#9654;</span>
16
+ <button class="sdocs-panel-header" onclick={() => (expanded = !expanded)}>
17
17
  <span class="sdocs-panel-title">{title}</span>
18
+ <span class="sdocs-panel-chevron">
19
+ <Icon name={expanded ? 'chevron-down' : 'chevron-right'} --w="14px" --h="14px" />
20
+ </span>
18
21
  </button>
19
22
  {#if expanded}
20
23
  <div class="sdocs-panel-body">
@@ -25,39 +28,36 @@
25
28
 
26
29
  <style>
27
30
  .sdocs-panel {
28
- border: 1px solid var(--color-base-200);
29
- border-radius: 8px;
31
+ /* border: 1px solid var(--color-base-200); */
32
+ /* border-radius: 8px; */
30
33
  overflow: hidden;
34
+ background: var(--color-base-0);
31
35
  }
32
36
  .sdocs-panel-header {
33
37
  display: flex;
34
38
  align-items: center;
35
39
  gap: 8px;
36
40
  width: 100%;
37
- padding: 10px 14px;
41
+ padding: 6px 8px 6px 12px;
38
42
  border: none;
39
43
  background: var(--color-base-50);
40
44
  font: inherit;
41
45
  font-size: 13px;
42
46
  font-weight: 600;
43
- color: var(--color-base-900);
47
+ color: var(--color-base-700);
44
48
  cursor: pointer;
45
49
  text-align: left;
46
50
  }
47
51
  .sdocs-panel-header:hover {
48
52
  background: var(--color-base-100);
49
53
  }
50
- .sdocs-panel-arrow {
51
- font-size: 8px;
52
- transition: transform 0.15s;
53
- display: inline-block;
54
- width: 10px;
55
- }
56
- .sdocs-panel-arrow.expanded {
57
- transform: rotate(90deg);
54
+ .sdocs-panel-chevron {
55
+ margin-left: auto;
56
+ display: flex;
57
+ align-items: center;
58
58
  }
59
59
  .sdocs-panel-body {
60
60
  padding: 14px;
61
- border-top: 1px solid var(--color-base-200);
61
+ border-top: 1px solid var(--color-base-100);
62
62
  }
63
63
  </style>
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { DocEntry } from '../../types.js';
3
+ import { Icon } from '../../ui/Icon/index.js';
3
4
  import CollapsiblePanel from './CollapsiblePanel.svelte';
4
5
  import PreviewFrame from './PreviewFrame.svelte';
5
6
  import ControlsPanel from './ControlsPanel.svelte';
@@ -50,17 +51,17 @@
50
51
  cssValues = { ...cssValues, [name]: value };
51
52
  }
52
53
 
53
- function generateUsageCode(name: string, props: Record<string, unknown>, css: Record<string, string>): string {
54
+ function formatAttr(name: string, value: unknown): string {
55
+ if (typeof value === 'string') return `${name}="${value}"`;
56
+ if (typeof value === 'boolean') return value ? name : `${name}={false}`;
57
+ return `${name}={${JSON.stringify(value)}}`;
58
+ }
59
+
60
+ function generateFallbackCode(name: string, props: Record<string, unknown>, css: Record<string, string>): string {
54
61
  const attrs: string[] = [];
55
62
  for (const [key, value] of Object.entries(props)) {
56
63
  if (value === undefined || value === null || value === '') continue;
57
- if (typeof value === 'string') {
58
- attrs.push(`${key}="${value}"`);
59
- } else if (typeof value === 'boolean') {
60
- attrs.push(value ? key : `${key}={false}`);
61
- } else {
62
- attrs.push(`${key}={${JSON.stringify(value)}}`);
63
- }
64
+ attrs.push(formatAttr(key, value));
64
65
  }
65
66
  for (const [key, value] of Object.entries(css)) {
66
67
  if (value === undefined || value === '') continue;
@@ -71,7 +72,126 @@
71
72
  return `<${name}\n ${attrs.join('\n ')}\n/>`;
72
73
  }
73
74
 
74
- const usageCode = $derived(generateUsageCode(componentName, propValues, cssValues));
75
+ /**
76
+ * Patch the Default snippet body with prop/CSS changes from Controls.
77
+ * Finds the root component opening tag and updates/adds changed attributes.
78
+ */
79
+ function patchSnippetCode(
80
+ snippetBody: string,
81
+ name: string,
82
+ currentProps: Record<string, unknown>,
83
+ currentCss: Record<string, string>,
84
+ initialProps: Record<string, unknown>,
85
+ initialCss: Record<string, string>,
86
+ ): string {
87
+ // Collect only changed props/css
88
+ const changes: [string, unknown][] = [];
89
+ for (const [key, value] of Object.entries(currentProps)) {
90
+ if (JSON.stringify(value) !== JSON.stringify(initialProps[key])) {
91
+ changes.push([key, value]);
92
+ }
93
+ }
94
+ for (const [key, value] of Object.entries(currentCss)) {
95
+ if (value !== (initialCss[key] ?? '')) {
96
+ changes.push([key, value]);
97
+ }
98
+ }
99
+
100
+ if (changes.length === 0) return snippetBody;
101
+
102
+ // Find the opening tag: <ComponentName ...> or <ComponentName ... />
103
+ const tagStart = snippetBody.indexOf(`<${name}`);
104
+ if (tagStart === -1) return snippetBody;
105
+
106
+ // Find the end of the opening tag, respecting {} expressions
107
+ let braceDepth = 0;
108
+ let tagEnd = -1;
109
+ for (let i = tagStart + name.length + 1; i < snippetBody.length; i++) {
110
+ if (snippetBody[i] === '{') braceDepth++;
111
+ else if (snippetBody[i] === '}') braceDepth--;
112
+ else if (braceDepth === 0 && snippetBody[i] === '>') {
113
+ tagEnd = i;
114
+ break;
115
+ }
116
+ }
117
+ if (tagEnd === -1) return snippetBody;
118
+
119
+ const isSelfClosing = snippetBody[tagEnd - 1] === '/';
120
+ const attrsStart = tagStart + `<${name}`.length;
121
+ const attrsEnd = isSelfClosing ? tagEnd - 1 : tagEnd;
122
+ let attrs = snippetBody.slice(attrsStart, attrsEnd);
123
+
124
+ for (const [attrName, value] of changes) {
125
+ const formatted = formatAttr(attrName, value);
126
+ const escaped = attrName.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
127
+
128
+ // Try replacing existing: name="...", name={...}, or bare name
129
+ const patterns = [
130
+ new RegExp(`(\\s)${escaped}="[^"]*"`),
131
+ new RegExp(`(\\s)${escaped}=\\{[^}]*\\}`),
132
+ new RegExp(`(\\s)${escaped}(?=\\s|$)`),
133
+ ];
134
+
135
+ let replaced = false;
136
+ for (const pattern of patterns) {
137
+ if (pattern.test(attrs)) {
138
+ attrs = attrs.replace(pattern, `$1${formatted}`);
139
+ replaced = true;
140
+ break;
141
+ }
142
+ }
143
+
144
+ if (!replaced) {
145
+ attrs += ` ${formatted}`;
146
+ }
147
+ }
148
+
149
+ const closing = isSelfClosing ? '/>' : '>';
150
+ return snippetBody.slice(0, attrsStart) + attrs + closing + snippetBody.slice(tagEnd + 1);
151
+ }
152
+
153
+ // Initial values for diffing (computed once per doc change)
154
+ const initialProps = $derived(meta.args ?? {});
155
+ const initialCss = $derived.by(() => {
156
+ const css: Record<string, string> = {};
157
+ if (cd?.cssProps) {
158
+ for (const cp of cd.cssProps) {
159
+ if (cp.default) css[cp.name] = cp.default;
160
+ }
161
+ }
162
+ return css;
163
+ });
164
+
165
+ const usageCode = $derived.by(() => {
166
+ if (defaultSnippet?.body) {
167
+ return patchSnippetCode(defaultSnippet.body, componentName, propValues, cssValues, initialProps, initialCss);
168
+ }
169
+ return generateFallbackCode(componentName, propValues, cssValues);
170
+ });
171
+
172
+ // Highlighted usage code via server-side Shiki
173
+ let highlightedUsageHtml = $state('');
174
+ let highlightTimer: ReturnType<typeof setTimeout> | undefined;
175
+
176
+ $effect(() => {
177
+ const code = usageCode;
178
+ clearTimeout(highlightTimer);
179
+ highlightTimer = setTimeout(async () => {
180
+ try {
181
+ const res = await fetch('/__sdocs/highlight', {
182
+ method: 'POST',
183
+ headers: { 'Content-Type': 'application/json' },
184
+ body: JSON.stringify({ code, lang: 'svelte' }),
185
+ });
186
+ if (res.ok) {
187
+ const { html } = await res.json();
188
+ highlightedUsageHtml = html;
189
+ }
190
+ } catch {
191
+ // Fallback: leave previous value
192
+ }
193
+ }, 150);
194
+ });
75
195
 
76
196
  function handleReset() {
77
197
  propValues = { ...(meta.args ?? {}) };
@@ -146,7 +266,7 @@
146
266
  </div>
147
267
  <div class="sdocs-panels">
148
268
  <CollapsiblePanel title="Preview">
149
- <PreviewFrame src={focusedSnippet.previewUrl} {activeStylesheet} />
269
+ <PreviewFrame src={focusedSnippet.previewUrl ?? ''} {activeStylesheet} />
150
270
  </CollapsiblePanel>
151
271
  <CollapsiblePanel title="Code" defaultExpanded={false}>
152
272
  <div class="sdocs-code-block">{@html focusedSnippet.highlightedHtml ?? ''}</div>
@@ -164,17 +284,23 @@
164
284
  <div class="sdocs-panels">
165
285
  <!-- Showcase -->
166
286
  {#if defaultSnippet}
167
- <CollapsiblePanel title="Preview">
287
+ <div class="sdocs-preview-wrapper">
168
288
  <PreviewFrame
169
- src={defaultSnippet.previewUrl}
289
+ src={defaultSnippet.previewUrl ?? ''}
170
290
  props={propValues}
171
291
  cssVars={cssValues}
172
292
  {activeStylesheet}
173
293
  />
174
- </CollapsiblePanel>
294
+ </div>
175
295
 
176
- <CollapsiblePanel title="Preview Code">
177
- <div class="sdocs-code-block"><pre><code>{usageCode}</code></pre></div>
296
+ <CollapsiblePanel title="Preview Code" defaultExpanded={false}>
297
+ <div class="sdocs-code-block">
298
+ {#if highlightedUsageHtml}
299
+ {@html highlightedUsageHtml}
300
+ {:else}
301
+ <pre><code>{usageCode}</code></pre>
302
+ {/if}
303
+ </div>
178
304
  </CollapsiblePanel>
179
305
  {/if}
180
306
 
@@ -262,29 +388,37 @@
262
388
  />
263
389
  </CollapsiblePanel>
264
390
 
265
- <!-- Examples -->
266
- {#if exampleSnippets.length > 0}
267
- <h2 class="sdocs-section-title">Examples</h2>
268
- {#each exampleSnippets as example (example.name)}
269
- <div class="sdocs-example">
270
- <h3 class="sdocs-example-title">{example.name}</h3>
271
- <CollapsiblePanel title="Preview">
272
- <PreviewFrame src={example.previewUrl} {activeStylesheet} />
273
- </CollapsiblePanel>
391
+ </div>
392
+
393
+ {#if exampleSnippets.length > 0}
394
+ <hr class="sdocs-divider" />
395
+ <h2 class="sdocs-section-title">Examples</h2>
396
+ {#each exampleSnippets as example (example.name)}
397
+ <div class="sdocs-example">
398
+ <h3 class="sdocs-example-title">
399
+ <Icon name="bookmark" --w="14px" --h="14px" --fill="var(--color-example-500)" />
400
+ {example.name}
401
+ </h3>
402
+ <div class="sdocs-panels">
403
+ <div class="sdocs-preview-wrapper">
404
+ <PreviewFrame src={example.previewUrl ?? ''} {activeStylesheet} />
405
+ </div>
274
406
  <CollapsiblePanel title="Code" defaultExpanded={false}>
275
407
  <div class="sdocs-code-block">{@html example.highlightedHtml ?? ''}</div>
276
408
  </CollapsiblePanel>
277
409
  </div>
278
- {/each}
279
- {/if}
410
+ </div>
411
+ {/each}
412
+ {/if}
280
413
 
281
- <!-- Source -->
282
- {#if doc.highlightedSource}
414
+ {#if doc.highlightedSource}
415
+ <hr class="sdocs-divider" />
416
+ <div class="sdocs-panels">
283
417
  <CollapsiblePanel title="Component Source" defaultExpanded={false}>
284
418
  <div class="sdocs-code-block">{@html doc.highlightedSource}</div>
285
419
  </CollapsiblePanel>
286
- {/if}
287
- </div>
420
+ </div>
421
+ {/if}
288
422
  {/if}
289
423
  </div>
290
424
 
@@ -311,13 +445,29 @@
311
445
  .sdocs-panels {
312
446
  display: flex;
313
447
  flex-direction: column;
314
- gap: 12px;
448
+ gap: 1px;
449
+ border: 1px solid var(--color-base-200);
450
+ background: var(--color-base-200);
451
+ border-radius: 8px;
452
+ overflow: hidden;
453
+ }
454
+ .sdocs-preview-wrapper {
455
+ background: var(--color-base-0);
456
+ padding: 16px;
457
+ }
458
+ .sdocs-divider {
459
+ border: none;
460
+ border-top: 1px solid var(--color-base-200);
461
+ margin: 24px 0;
315
462
  }
316
463
  .sdocs-section-title {
317
464
  font-size: 18px;
318
465
  font-weight: 600;
319
466
  color: var(--color-base-900);
320
- margin: 24px 0 8px;
467
+ margin: 24px 0 16px;
468
+ }
469
+ .sdocs-example + .sdocs-example {
470
+ margin-top: 16px;
321
471
  }
322
472
  .sdocs-example {
323
473
  display: flex;
@@ -325,6 +475,9 @@
325
475
  gap: 8px;
326
476
  }
327
477
  .sdocs-example-title {
478
+ display: flex;
479
+ align-items: center;
480
+ gap: 6px;
328
481
  font-size: 15px;
329
482
  font-weight: 600;
330
483
  color: var(--color-base-800);
@@ -334,6 +487,7 @@
334
487
  overflow-x: auto;
335
488
  font-size: 13px;
336
489
  line-height: 1.5;
490
+ tab-size: 4;
337
491
  }
338
492
  .sdocs-code-block :global(pre) {
339
493
  margin: 0;
@@ -74,5 +74,6 @@
74
74
  color: var(--color-base-400);
75
75
  font-size: 13px;
76
76
  font-style: italic;
77
+ margin: 0;
77
78
  }
78
79
  </style>
@@ -83,15 +83,11 @@
83
83
 
84
84
  <style>
85
85
  .sdocs-preview-frame {
86
- border: 1px solid var(--color-base-200);
87
- border-radius: 6px;
88
86
  overflow: hidden;
89
87
  background: var(--color-base-0);
90
88
  }
91
89
  .sdocs-preview-frame.full-height {
92
90
  flex: 1;
93
- border: none;
94
- border-radius: 0;
95
91
  }
96
92
  .sdocs-iframe {
97
93
  width: 100%;
@@ -33,6 +33,7 @@ function generateIndexHtml(title) {
33
33
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
34
34
  <link rel="icon" type="image/png" href="./client/favicon.png">
35
35
  <title>${title}</title>
36
+ <style>body { margin: 0; }</style>
36
37
  </head>
37
38
  <body>
38
39
  <div id="app"></div>