sdocs 0.0.42 → 0.0.44

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,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.0.44] - 2026-07-04
11
+
12
+ ### Changed
13
+
14
+ - The README documents the block format — the npm listing previously showed
15
+ the retired `export const meta` convention.
16
+
17
+ ## [0.0.43] - 2026-07-04
18
+
19
+ ### Added
20
+
21
+ - **`projectSdoc` in `sdocs/language`** — a line-preserving projection of a
22
+ `.sdoc` file onto a virtual Svelte document: every authored line keeps its
23
+ exact position, sub-block openers become the same `{#snippet}` wrappers
24
+ the build generates, PAGE prose stays live with code regions masked, and
25
+ a trailer past the authored end marks everything as used. This is the
26
+ foundation of the VS Code extension's sdoc language server.
27
+
28
+ ### Changed
29
+
30
+ - The Explorer's own UI library docs (`src/lib/ui/*.sdoc`) are converted to
31
+ the block format.
32
+
10
33
  ## [0.0.42] - 2026-07-04
11
34
 
12
35
  ### Changed
package/README.md CHANGED
@@ -96,93 +96,80 @@ That's it — your docs page lives at `/docs` inside your existing app.
96
96
 
97
97
  ## Writing Docs
98
98
 
99
- sdocs supports three types of doc files:
99
+ A `.sdoc` file is `<script>` at the top, entity blocks in the middle, and an
100
+ optional `<style>` at the bottom. Every entity is its own sidebar entry, so
101
+ one file can hold several.
100
102
 
101
- ### Component Docs (`.sdoc`)
103
+ ### Component docs `[DOCS]`
102
104
 
103
- Document a Svelte component with interactive controls and examples.
104
-
105
- ```svelte
106
- <!-- Button.sdoc -->
105
+ ```sdoc
107
106
  <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
- };
107
+ import Button from './Button.svelte';
120
108
  </script>
121
109
 
122
- {#snippet Default()}
123
- <Button {...args} />
124
- {/snippet}
110
+ [DOCS title="Components / Button" description="A flexible button component."]
111
+
112
+ [preview component={Button} args={{ label: 'Click me', disabled: false }}]
113
+ <Button {...args} />
114
+ [/preview]
125
115
 
126
- {#snippet WithIcon()}
127
- <Button>
128
- <Icon name="settings" /> Settings
129
- </Button>
130
- {/snippet}
116
+ [example title="With icon"]
117
+ <Button><Icon name="settings" /> Settings</Button>
118
+ [/example]
119
+
120
+ [/DOCS]
131
121
  ```
132
122
 
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
123
+ - **`[preview]`** — a live showcase with interactive controls.
124
+ `component={X}` names the previewed component (its props, events,
125
+ snippets, methods, state, and CSS custom properties are extracted
126
+ automatically) and `args` sets the control defaults. A `[DOCS]` block can
127
+ hold several previewsthey render as tabs, each fully live.
128
+ - **`[example]`** — frozen showcases rendered exactly as written, shown
129
+ below the preview area. Each needs a unique `title`.
130
+ - **`title`** — slash-separated path for sidebar navigation.
138
131
 
139
- ### Page Docs (`.page.sdoc`)
132
+ ### Pages `[PAGE]`
140
133
 
141
- Freeform content pages with auto-generated table of contents.
134
+ Freeform markdown content with `{expression}` interpolation and Svelte
135
+ component islands; code fences are inert. The table of contents is generated
136
+ from the headings.
142
137
 
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>
138
+ ```sdoc
139
+ [PAGE title="Docs / Getting Started"]
151
140
 
152
- <h1>Getting Started</h1>
153
- <p>Install sdocs and create your first doc file.</p>
141
+ ## Installation
154
142
 
155
- <h2>Installation</h2>
156
- <p>Run <code>npm install sdocs</code>...</p>
157
- ```
143
+ Run `npm install sdocs` and create your first doc file.
158
144
 
159
- Table of contents is auto-generated from `<h2>`, `<h3>`, and `<h4>` headings.
145
+ [/PAGE]
146
+ ```
160
147
 
161
- ### Layout Docs (`.layout.sdoc`)
148
+ ### Layouts `[LAYOUT]`
162
149
 
163
- Component compositions rendered in an isolated iframe.
150
+ Full-page component compositions rendered on an isolated stage.
164
151
 
165
- ```svelte
166
- <!-- LoginForm.layout.sdoc -->
152
+ ```sdoc
167
153
  <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
- };
154
+ import Card from './Card.svelte';
155
+ import Input from './Input.svelte';
156
+ import Button from './Button.svelte';
177
157
  </script>
178
158
 
179
- <Card padding="24px">
180
- <Input label="Email" type="email" />
181
- <Input label="Password" type="password" />
182
- <Button>Sign in</Button>
183
- </Card>
159
+ [LAYOUT title="Patterns / Login Form" padding="24px"]
160
+
161
+ <Card padding="24px">
162
+ <Input label="Email" type="email" />
163
+ <Input label="Password" type="password" />
164
+ <Button>Sign in</Button>
165
+ </Card>
166
+
167
+ [/LAYOUT]
184
168
  ```
185
169
 
170
+ The full language reference lives at
171
+ [gabilungu.github.io/sdocs/language](https://gabilungu.github.io/sdocs/language).
172
+
186
173
  ## Prop Extraction
187
174
 
188
175
  sdocs automatically extracts from your Svelte components:
@@ -200,7 +187,7 @@ JSDoc comments on props are picked up as descriptions.
200
187
 
201
188
  ## Interactive Controls
202
189
 
203
- The Default snippet gets live controls based on prop types:
190
+ Each preview gets live controls based on prop types:
204
191
 
205
192
  | Prop Type | Control |
206
193
  |-----------|---------|
@@ -275,6 +262,8 @@ sidebar: {
275
262
  | `sdocs/vite` | Vite plugin function |
276
263
  | `sdocs/explorer` | Explorer.svelte UI component |
277
264
  | `sdocs/ui` | Reusable UI components (Button, Frame, Icon, Control, NavTree, Stack) |
265
+ | `sdocs/language` | The sdoc scanner, parser, and Svelte projection |
266
+ | `sdocs/grammar/sdoc.tmLanguage.json` | TextMate grammar for editors and highlighters |
278
267
 
279
268
  ## License
280
269
 
@@ -1,2 +1,3 @@
1
1
  export { scanSdoc, offsetToPosition, ENTITY_KINDS, SUB_BLOCK_KINDS, type Span, type EntityKind, type SubBlockKind, type AttrValue, type Attrs, type SubBlock, type Entity, type TagBlock, type ScanError, type SdocFile, } from './scanner.js';
2
+ export { projectSdoc, type SdocProjection, type ProjectedLineKind, } from './projection.js';
2
3
  export { parseSdoc, parseArgsLiteral, slugifyTitle, normalizeBody, type ArgValue, type PreviewBlock, type ExampleBlock, type DocsEntity, type PageEntity, type LayoutEntity, type SdocEntity, type SdocDocument, } from './parser.js';
@@ -1,2 +1,3 @@
1
1
  export { scanSdoc, offsetToPosition, ENTITY_KINDS, SUB_BLOCK_KINDS, } from './scanner.js';
2
+ export { projectSdoc, } from './projection.js';
2
3
  export { parseSdoc, parseArgsLiteral, slugifyTitle, normalizeBody, } from './parser.js';
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Line-preserving projection of a .sdoc file into a virtual Svelte document.
3
+ *
4
+ * Every authored line keeps its exact (line, column) in the virtual text, so
5
+ * editor position mapping is the identity function. Block syntax the Svelte
6
+ * compiler must not see becomes blank lines; sub-block openers are rewritten
7
+ * IN PLACE to `{#snippet …(args)}` wrappers — byte-compatible with the shape
8
+ * the build pipeline generates at runtime — and a trailer appended past the
9
+ * authored end renders every snippet and references previewed components, so
10
+ * nothing draws unused-import noise. Diagnostics on trailer lines
11
+ * (line >= sourceLineCount) are generated, never authored.
12
+ */
13
+ import type { SdocFile } from './scanner.js';
14
+ export type ProjectedLineKind = 'verbatim' | 'blank' | 'wrapper' | 'masked';
15
+ export interface SdocProjection {
16
+ /** The virtual Svelte document text */
17
+ text: string;
18
+ /** Authored line count; virtual lines at or past this are generated */
19
+ sourceLineCount: number;
20
+ /** Per authored line: how it was projected */
21
+ lineKinds: ProjectedLineKind[];
22
+ }
23
+ export declare function projectSdoc(file: SdocFile): SdocProjection;
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Line-preserving projection of a .sdoc file into a virtual Svelte document.
3
+ *
4
+ * Every authored line keeps its exact (line, column) in the virtual text, so
5
+ * editor position mapping is the identity function. Block syntax the Svelte
6
+ * compiler must not see becomes blank lines; sub-block openers are rewritten
7
+ * IN PLACE to `{#snippet …(args)}` wrappers — byte-compatible with the shape
8
+ * the build pipeline generates at runtime — and a trailer appended past the
9
+ * authored end renders every snippet and references previewed components, so
10
+ * nothing draws unused-import noise. Diagnostics on trailer lines
11
+ * (line >= sourceLineCount) are generated, never authored.
12
+ */
13
+ function lineStartsOf(source) {
14
+ const starts = [0];
15
+ for (let i = 0; i < source.length; i++) {
16
+ if (source[i] === '\n')
17
+ starts.push(i + 1);
18
+ }
19
+ return starts;
20
+ }
21
+ function lineOfOffset(starts, offset) {
22
+ let lo = 0;
23
+ let hi = starts.length - 1;
24
+ while (lo < hi) {
25
+ const mid = (lo + hi + 1) >> 1;
26
+ if (starts[mid] <= offset)
27
+ lo = mid;
28
+ else
29
+ hi = mid - 1;
30
+ }
31
+ return lo;
32
+ }
33
+ /** Mask markdown code so a PAGE body reads as Svelte-safe prose: fence lines
34
+ * and fence interiors go blank; inline `code` spans are space-overwritten
35
+ * (column-preserving). Everything else — prose, {expressions}, component
36
+ * islands — stays verbatim. */
37
+ function maskMarkdownLine(line, state) {
38
+ const fenceMarker = /^\s*(`{3,}|~{3,})/.test(line);
39
+ if (fenceMarker) {
40
+ state.inFence = !state.inFence;
41
+ return { text: '', masked: true };
42
+ }
43
+ if (state.inFence)
44
+ return { text: '', masked: true };
45
+ if (!line.includes('`'))
46
+ return { text: line, masked: false };
47
+ const text = line.replace(/`[^`]*`/g, (m) => ' '.repeat(m.length));
48
+ return { text, masked: text !== line };
49
+ }
50
+ export function projectSdoc(file) {
51
+ const source = file.source;
52
+ const starts = lineStartsOf(source);
53
+ const total = starts.length;
54
+ const out = new Array(total).fill('');
55
+ const kinds = new Array(total).fill('blank');
56
+ const copyVerbatim = (span) => {
57
+ const first = lineOfOffset(starts, span.start);
58
+ const last = lineOfOffset(starts, Math.max(span.start, span.end - 1));
59
+ for (let l = first; l <= last; l++) {
60
+ const end = l + 1 < total ? starts[l + 1] - 1 : source.length;
61
+ out[l] = source.slice(starts[l], end).replace(/\r$/, '');
62
+ kinds[l] = 'verbatim';
63
+ }
64
+ };
65
+ if (file.script)
66
+ copyVerbatim(file.script.span);
67
+ if (file.style)
68
+ copyVerbatim(file.style.span);
69
+ const isTs = /lang\s*=\s*["']ts["']/.test(file.script?.attrsText ?? '');
70
+ const argsParam = isTs ? '(args: any)' : '(args)';
71
+ const snippets = [];
72
+ const componentRefs = new Set();
73
+ const wrapBlock = (name, openerSpan, bodySpan, closerLineOf, // block span; its end sits on the closer line
74
+ withArgs, maskBody) => {
75
+ const openFirst = lineOfOffset(starts, openerSpan.start);
76
+ const openLast = lineOfOffset(starts, Math.max(openerSpan.start, openerSpan.end - 1));
77
+ out[openFirst] = `{#snippet ${name}${withArgs ? argsParam : '()'}}`;
78
+ kinds[openFirst] = 'wrapper';
79
+ for (let l = openFirst + 1; l <= openLast; l++) {
80
+ out[l] = '';
81
+ kinds[l] = 'blank';
82
+ }
83
+ if (bodySpan.end > bodySpan.start) {
84
+ if (maskBody) {
85
+ const first = lineOfOffset(starts, bodySpan.start);
86
+ const last = lineOfOffset(starts, Math.max(bodySpan.start, bodySpan.end - 1));
87
+ const state = { inFence: false };
88
+ for (let l = first; l <= last; l++) {
89
+ const end = l + 1 < total ? starts[l + 1] - 1 : source.length;
90
+ const line = source.slice(starts[l], end).replace(/\r$/, '');
91
+ const masked = maskMarkdownLine(line, state);
92
+ out[l] = masked.text;
93
+ kinds[l] = masked.masked ? 'masked' : 'verbatim';
94
+ }
95
+ }
96
+ else {
97
+ copyVerbatim(bodySpan);
98
+ }
99
+ }
100
+ const closeLine = lineOfOffset(starts, Math.max(closerLineOf.start, closerLineOf.end - 1));
101
+ out[closeLine] = '{/snippet}';
102
+ kinds[closeLine] = 'wrapper';
103
+ snippets.push({ name, withArgs });
104
+ };
105
+ file.entities.forEach((entity, e) => {
106
+ if (entity.kind === 'DOCS') {
107
+ entity.blocks.forEach((block, b) => {
108
+ wrapBlock(`__sdocs$${e}_${b}`, block.openerSpan, block.bodySpan, block.span, true, false);
109
+ const component = block.attrs.component;
110
+ if (component?.kind === 'expression') {
111
+ const name = component.raw.trim();
112
+ if (/^[A-Z][A-Za-z0-9_]*$/.test(name))
113
+ componentRefs.add(name);
114
+ }
115
+ });
116
+ }
117
+ else {
118
+ wrapBlock(`__sdocs$${e}`, entity.openerSpan, entity.bodySpan, entity.span, false, entity.kind === 'PAGE');
119
+ }
120
+ });
121
+ // Trailer: render every snippet and reference previewed components so the
122
+ // Svelte/TS layer sees everything as used. Lives past the authored end.
123
+ const trailer = ['{#if false}'];
124
+ for (const snippet of snippets) {
125
+ trailer.push(`{@render ${snippet.name}(${snippet.withArgs ? '{}' : ''})}`);
126
+ }
127
+ for (const name of componentRefs) {
128
+ trailer.push(`<${name} />`);
129
+ }
130
+ trailer.push('{/if}');
131
+ return {
132
+ text: out.join('\n') + '\n' + trailer.join('\n') + '\n',
133
+ sourceLineCount: total,
134
+ lineKinds: kinds,
135
+ };
136
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,117 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { readFileSync, readdirSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { compile } from 'svelte/compiler';
5
+ import { scanSdoc } from './scanner.js';
6
+ import { projectSdoc } from './projection.js';
7
+ const SOURCE = `<script lang="ts">
8
+ import Tabs from './Tabs.svelte';
9
+ import Tab from './Tab.svelte';
10
+ const shared = 1;
11
+ </script>
12
+
13
+ [DOCS title="Nav / Tabs" description="A tab bar."]
14
+
15
+ [preview component={Tabs} args={{ active: 0 }}]
16
+ <Tabs {...args} active={shared}>x</Tabs>
17
+ [/preview]
18
+
19
+ [example title="Wrapped child"]
20
+ <Tabs><Tab {...args} /></Tabs>
21
+ [/example]
22
+
23
+ [/DOCS]
24
+
25
+ [PAGE title="Guide"]
26
+
27
+ ## Heading
28
+
29
+ Uses {shared} and <Tabs />.
30
+
31
+ \`\`\`svelte
32
+ <NotReal {broken} />
33
+ \`\`\`
34
+
35
+ Inline \`{code}\` is masked.
36
+
37
+ [/PAGE]
38
+
39
+ <style>
40
+ .x { color: red; }
41
+ </style>
42
+ `;
43
+ describe('projectSdoc', () => {
44
+ const projection = projectSdoc(scanSdoc(SOURCE));
45
+ const lines = projection.text.split('\n');
46
+ const sourceLines = SOURCE.split('\n');
47
+ it('preserves every authored line position', () => {
48
+ expect(projection.sourceLineCount).toBe(sourceLines.length);
49
+ for (let i = 0; i < sourceLines.length; i++) {
50
+ const kind = projection.lineKinds[i];
51
+ if (kind === 'verbatim')
52
+ expect(lines[i]).toBe(sourceLines[i]);
53
+ if (kind === 'blank')
54
+ expect(lines[i]).toBe('');
55
+ }
56
+ });
57
+ const at = (needle) => sourceLines.findIndex((l) => l.includes(needle));
58
+ it('keeps script, style, and bodies verbatim at identical lines', () => {
59
+ expect(lines[1]).toBe("\timport Tabs from './Tabs.svelte';");
60
+ expect(lines[at('{...args} active')]).toBe(sourceLines[at('{...args} active')]);
61
+ expect(lines[at('color: red')]).toContain('color: red');
62
+ });
63
+ it('rewrites sub-block openers to snippet wrappers in place', () => {
64
+ expect(lines[at('[preview')]).toBe('{#snippet __sdocs$0_0(args: any)}');
65
+ expect(lines[at('[/preview]')]).toBe('{/snippet}');
66
+ expect(lines[at('[example')]).toBe('{#snippet __sdocs$0_1(args: any)}');
67
+ });
68
+ it('blanks entity tags and masks PAGE code while keeping prose live', () => {
69
+ expect(lines[at('[DOCS')]).toBe('');
70
+ const proseLine = sourceLines.findIndex((l) => l.includes('Uses {shared}'));
71
+ expect(lines[proseLine]).toBe(sourceLines[proseLine]);
72
+ const fenceBody = sourceLines.findIndex((l) => l.includes('NotReal'));
73
+ expect(lines[fenceBody]).toBe('');
74
+ const inline = sourceLines.findIndex((l) => l.includes('Inline'));
75
+ expect(lines[inline]).not.toContain('{code}');
76
+ expect(lines[inline].length).toBe(sourceLines[inline].length);
77
+ });
78
+ it('appends a trailer that renders snippets and references components', () => {
79
+ const trailer = lines.slice(projection.sourceLineCount).join('\n');
80
+ expect(trailer).toContain('{@render __sdocs$0_0({})}');
81
+ expect(trailer).toContain('{@render __sdocs$1()}');
82
+ expect(trailer).toContain('<Tabs />');
83
+ });
84
+ it('produces text the Svelte compiler accepts', () => {
85
+ expect(() => compile(projection.text, { generate: false })).not.toThrow();
86
+ });
87
+ });
88
+ describe('projectSdoc over the real corpus', () => {
89
+ const dirs = [
90
+ resolve(__dirname, '../../../../../apps/site/src/lib/ui'),
91
+ resolve(__dirname, '../../../../../apps/testapp-embedded/src/lib/UI'),
92
+ resolve(__dirname, '../../../../../apps/testapp-standalone/src'),
93
+ ];
94
+ const files = [];
95
+ for (const dir of dirs) {
96
+ try {
97
+ for (const f of readdirSync(dir, { recursive: true })) {
98
+ if (f.endsWith('.sdoc'))
99
+ files.push(resolve(dir, f));
100
+ }
101
+ }
102
+ catch {
103
+ // corpus dir not present in this checkout
104
+ }
105
+ }
106
+ it('found the corpus', () => {
107
+ expect(files.length).toBeGreaterThanOrEqual(10);
108
+ });
109
+ for (const file of files) {
110
+ it(`compiles ${file.split('/').slice(-1)[0]}`, () => {
111
+ const source = readFileSync(file, 'utf-8');
112
+ const projection = projectSdoc(scanSdoc(source));
113
+ expect(projection.sourceLineCount).toBe(source.split('\n').length);
114
+ expect(() => compile(projection.text, { generate: false })).not.toThrow();
115
+ });
116
+ }
117
+ });
@@ -0,0 +1,28 @@
1
+ [PAGE title=":Pages / About"]
2
+
3
+ # About sdocs
4
+
5
+ sdocs is a lightweight documentation tool for Svelte 5 components.
6
+ It discovers `.sdoc` files in your project and renders an interactive
7
+ component explorer with live previews.
8
+
9
+ ## Features
10
+
11
+ - Zero-config component discovery
12
+ - Live preview with prop controls
13
+ - Multiple titled examples per component
14
+ - Page and layout support
15
+ - Light and dark themes
16
+
17
+ ## Getting Started
18
+
19
+ Create a `.sdoc` file next to your component and add a `[DOCS]` block
20
+ with a `[preview]` naming the component.
21
+
22
+ ## Entities
23
+
24
+ - **`[DOCS]`** — component documentation with previews and examples
25
+ - **`[PAGE]`** — standalone content pages
26
+ - **`[LAYOUT]`** — full-page layout previews
27
+
28
+ [/PAGE]
@@ -1,52 +1,49 @@
1
1
  <script lang="ts">
2
- import type { ComponentProps } from 'svelte';
3
2
  import Button from './Button.svelte';
4
3
  import { Icon } from '../Icon/index.js';
5
-
6
- export const meta = {
7
- component: Button,
8
- title: 'Button',
9
- description: 'A flexible button with optional left and right snippet slots for icons or badges.',
10
- };
11
4
  </script>
12
5
 
13
- {#snippet Default(args: ComponentProps<typeof Button>)}
14
- <Button {...args}>Click me</Button>
15
- {/snippet}
16
-
17
- {#snippet WithLeftIcon()}
18
- <Button onclick={() => alert('Settings')}>
19
- {#snippet left()}<Icon name="folder" --w="14px" --h="14px" />{/snippet}
20
- Settings
21
- </Button>
22
- {/snippet}
23
-
24
- {#snippet WithRightIcon()}
25
- <Button>
26
- Next
27
- {#snippet right()}<Icon name="chevron-right" --w="14px" --h="14px" />{/snippet}
28
- </Button>
29
- {/snippet}
30
-
31
- {#snippet BothSlots()}
32
- <Button>
33
- {#snippet left()}<Icon name="file-text" --w="14px" --h="14px" />{/snippet}
34
- Export
35
- {#snippet right()}<span style="font-size: 11px; opacity: 0.6;">PDF</span>{/snippet}
36
- </Button>
37
- {/snippet}
38
-
39
- {#snippet IconOnly()}
40
- <div style="display: flex; gap: 8px;">
41
- <Button title="Light theme" --p="4px 6px">
42
- {#snippet left()}<Icon name="sun" --w="16px" --h="16px" />{/snippet}
6
+ [DOCS title="Button" description="A flexible button with optional left and right snippet slots for icons or badges."]
7
+
8
+ [preview component={Button}]
9
+ <Button {...args}>Click me</Button>
10
+ [/preview]
11
+
12
+ [example title="WithLeftIcon"]
13
+ <Button onclick={() => alert('Settings')}>
14
+ {#snippet left()}<Icon name="folder" --w="14px" --h="14px" />{/snippet}
15
+ Settings
16
+ </Button>
17
+ [/example]
18
+
19
+ [example title="WithRightIcon"]
20
+ <Button>
21
+ Next
22
+ {#snippet right()}<Icon name="chevron-right" --w="14px" --h="14px" />{/snippet}
43
23
  </Button>
44
- <Button title="Fullscreen" --p="4px 6px">
45
- {#snippet left()}<Icon name="maximize" --w="16px" --h="16px" />{/snippet}
24
+ [/example]
25
+
26
+ [example title="BothSlots"]
27
+ <Button>
28
+ {#snippet left()}<Icon name="file-text" --w="14px" --h="14px" />{/snippet}
29
+ Export
30
+ {#snippet right()}<span style="font-size: 11px; opacity: 0.6;">PDF</span>{/snippet}
46
31
  </Button>
47
- </div>
48
- {/snippet}
32
+ [/example]
33
+
34
+ [example title="IconOnly"]
35
+ <div style="display: flex; gap: 8px;">
36
+ <Button title="Light theme" --p="4px 6px">
37
+ {#snippet left()}<Icon name="sun" --w="16px" --h="16px" />{/snippet}
38
+ </Button>
39
+ <Button title="Fullscreen" --p="4px 6px">
40
+ {#snippet left()}<Icon name="maximize" --w="16px" --h="16px" />{/snippet}
41
+ </Button>
42
+ </div>
43
+ [/example]
44
+
45
+ [example title="Disabled"]
46
+ <Button disabled>Can't click</Button>
47
+ [/example]
49
48
 
50
- {#snippet Disabled()}
51
- <Button disabled>Can't click</Button>
52
- {/snippet}
49
+ [/DOCS]
@@ -1,17 +1,11 @@
1
1
  <script lang="ts">
2
2
  import Checkbox from './Checkbox.svelte';
3
-
4
- export const meta = {
5
- component: Checkbox,
6
- title: 'Control / Checkbox',
7
- description: 'A checkbox control for boolean values.',
8
- args: {
9
- label: 'Enabled',
10
- value: true,
11
- },
12
- };
13
3
  </script>
14
4
 
15
- {#snippet Default(args)}
16
- <Checkbox {...args} onchange={(v) => console.log('changed:', v)} />
17
- {/snippet}
5
+ [DOCS title="Control / Checkbox" description="A checkbox control for boolean values."]
6
+
7
+ [preview component={Checkbox} args={{ label: 'Enabled', value: true }}]
8
+ <Checkbox {...args} onchange={(v) => console.log('changed:', v)} />
9
+ [/preview]
10
+
11
+ [/DOCS]
@@ -1,17 +1,11 @@
1
1
  <script lang="ts">
2
2
  import Color from './Color.svelte';
3
-
4
- export const meta = {
5
- component: Color,
6
- title: 'UI / Control / Color',
7
- description: 'A color picker control.',
8
- args: {
9
- label: 'Background',
10
- value: '#3b82f6',
11
- },
12
- };
13
3
  </script>
14
4
 
15
- {#snippet Default(args)}
16
- <Color {...args} onchange={(v) => console.log('changed:', v)} />
17
- {/snippet}
5
+ [DOCS title="UI / Control / Color" description="A color picker control."]
6
+
7
+ [preview component={Color} args={{ label: 'Background', value: '#3b82f6' }}]
8
+ <Color {...args} onchange={(v) => console.log('changed:', v)} />
9
+ [/preview]
10
+
11
+ [/DOCS]