sdocs 0.0.42 → 0.0.43
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 +16 -0
- package/dist/language/index.d.ts +1 -0
- package/dist/language/index.js +1 -0
- package/dist/language/projection.d.ts +23 -0
- package/dist/language/projection.js +136 -0
- package/dist/language/projection.test.d.ts +1 -0
- package/dist/language/projection.test.js +117 -0
- package/dist/ui/About.sdoc +28 -0
- package/dist/ui/Button/Button.sdoc +41 -44
- package/dist/ui/Control/Checkbox.sdoc +7 -13
- package/dist/ui/Control/Color.sdoc +7 -13
- package/dist/ui/Control/Dimension.sdoc +7 -13
- package/dist/ui/Control/Number.sdoc +7 -13
- package/dist/ui/Control/Select.sdoc +7 -14
- package/dist/ui/Control/Text.sdoc +7 -13
- package/dist/ui/Dashboard.sdoc +57 -0
- package/dist/ui/Frame/Frame.sdoc +46 -48
- package/dist/ui/Icon/Icon.sdoc +33 -38
- package/dist/ui/NavTree/Group.sdoc +36 -38
- package/dist/ui/NavTree/Item.sdoc +52 -57
- package/dist/ui/NavTree/NavTree.sdoc +89 -91
- package/dist/ui/Stack/Stack.sdoc +41 -43
- package/package.json +1 -1
- package/dist/ui/About.page.sdoc +0 -37
- package/dist/ui/Dashboard.layout.sdoc +0 -60
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.0.43] - 2026-07-04
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`projectSdoc` in `sdocs/language`** — a line-preserving projection of a
|
|
15
|
+
`.sdoc` file onto a virtual Svelte document: every authored line keeps its
|
|
16
|
+
exact position, sub-block openers become the same `{#snippet}` wrappers
|
|
17
|
+
the build generates, PAGE prose stays live with code regions masked, and
|
|
18
|
+
a trailer past the authored end marks everything as used. This is the
|
|
19
|
+
foundation of the VS Code extension's sdoc language server.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- The Explorer's own UI library docs (`src/lib/ui/*.sdoc`) are converted to
|
|
24
|
+
the block format.
|
|
25
|
+
|
|
10
26
|
## [0.0.42] - 2026-07-04
|
|
11
27
|
|
|
12
28
|
### Changed
|
package/dist/language/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/language/index.js
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
{
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
Settings
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
{
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
{
|
|
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]
|
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import Dimension from './Dimension.svelte';
|
|
3
|
-
|
|
4
|
-
export const meta = {
|
|
5
|
-
component: Dimension,
|
|
6
|
-
title: 'UI / Control / Dimension',
|
|
7
|
-
description: 'A dimension input control for CSS values (px).',
|
|
8
|
-
args: {
|
|
9
|
-
label: 'Width',
|
|
10
|
-
value: '120px',
|
|
11
|
-
},
|
|
12
|
-
};
|
|
13
3
|
</script>
|
|
14
4
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
{
|
|
5
|
+
[DOCS title="UI / Control / Dimension" description="A dimension input control for CSS values (px)."]
|
|
6
|
+
|
|
7
|
+
[preview component={Dimension} args={{ label: 'Width', value: '120px' }}]
|
|
8
|
+
<Dimension {...args} onchange={(v) => console.log('changed:', v)} />
|
|
9
|
+
[/preview]
|
|
10
|
+
|
|
11
|
+
[/DOCS]
|
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import Number from './Number.svelte';
|
|
3
|
-
|
|
4
|
-
export const meta = {
|
|
5
|
-
component: Number,
|
|
6
|
-
title: 'UI / Control / Number',
|
|
7
|
-
description: 'A numeric input control.',
|
|
8
|
-
args: {
|
|
9
|
-
label: 'Count',
|
|
10
|
-
value: 42,
|
|
11
|
-
},
|
|
12
|
-
};
|
|
13
3
|
</script>
|
|
14
4
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
{
|
|
5
|
+
[DOCS title="UI / Control / Number" description="A numeric input control."]
|
|
6
|
+
|
|
7
|
+
[preview component={Number} args={{ label: 'Count', value: 42 }}]
|
|
8
|
+
<Number {...args} onchange={(v) => console.log('changed:', v)} />
|
|
9
|
+
[/preview]
|
|
10
|
+
|
|
11
|
+
[/DOCS]
|
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import Select from './Select.svelte';
|
|
3
|
-
|
|
4
|
-
export const meta = {
|
|
5
|
-
component: Select,
|
|
6
|
-
title: 'UI / Control / Select',
|
|
7
|
-
description: 'A dropdown select control.',
|
|
8
|
-
args: {
|
|
9
|
-
label: 'Size',
|
|
10
|
-
value: 'md',
|
|
11
|
-
options: ['sm', 'md', 'lg'],
|
|
12
|
-
},
|
|
13
|
-
};
|
|
14
3
|
</script>
|
|
15
4
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
{
|
|
5
|
+
[DOCS title="UI / Control / Select" description="A dropdown select control."]
|
|
6
|
+
|
|
7
|
+
[preview component={Select} args={{ label: 'Size', value: 'md' }}]
|
|
8
|
+
<Select {...args} options={['sm', 'md', 'lg']} onchange={(v) => console.log('changed:', v)} />
|
|
9
|
+
[/preview]
|
|
10
|
+
|
|
11
|
+
[/DOCS]
|
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import Text from './Text.svelte';
|
|
3
|
-
|
|
4
|
-
export const meta = {
|
|
5
|
-
component: Text,
|
|
6
|
-
title: 'UI / Control / Text',
|
|
7
|
-
description: 'A text input control for string values.',
|
|
8
|
-
args: {
|
|
9
|
-
label: 'Name',
|
|
10
|
-
value: 'Hello',
|
|
11
|
-
},
|
|
12
|
-
};
|
|
13
3
|
</script>
|
|
14
4
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
{
|
|
5
|
+
[DOCS title="UI / Control / Text" description="A text input control for string values."]
|
|
6
|
+
|
|
7
|
+
[preview component={Text} args={{ label: 'Name', value: 'Hello' }}]
|
|
8
|
+
<Text {...args} onchange={(v) => console.log('changed:', v)} />
|
|
9
|
+
[/preview]
|
|
10
|
+
|
|
11
|
+
[/DOCS]
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[LAYOUT title=":Layouts / Dashboard"]
|
|
2
|
+
|
|
3
|
+
<div style="display: flex; flex-direction: column; height: 100vh; ">
|
|
4
|
+
<header style="padding: 12px 24px; background: #1e293b; color: white; display: flex; align-items: center; justify-content: space-between;">
|
|
5
|
+
<span style="font-weight: 700; font-size: 16px;">Dashboard</span>
|
|
6
|
+
<nav style="display: flex; gap: 16px; font-size: 14px;">
|
|
7
|
+
<span style="color: #94a3b8; cursor: pointer;">Settings</span>
|
|
8
|
+
<span style="color: #94a3b8; cursor: pointer;">Profile</span>
|
|
9
|
+
</nav>
|
|
10
|
+
</header>
|
|
11
|
+
<div style="display: flex; flex: 1; overflow: hidden;">
|
|
12
|
+
<aside style="width: 200px; background: #f8fafc; border-right: 1px solid #e2e8f0; padding: 16px;">
|
|
13
|
+
<nav style="display: flex; flex-direction: column; gap: 4px;">
|
|
14
|
+
<div style="padding: 8px 12px; border-radius: 6px; color: #334155; background: #e2e8f0; font-size: 14px; font-weight: 500;">Overview</div>
|
|
15
|
+
<div style="padding: 8px 12px; border-radius: 6px; color: #64748b; font-size: 14px;">Analytics</div>
|
|
16
|
+
<div style="padding: 8px 12px; border-radius: 6px; color: #64748b; font-size: 14px;">Users</div>
|
|
17
|
+
<div style="padding: 8px 12px; border-radius: 6px; color: #64748b; font-size: 14px;">Reports</div>
|
|
18
|
+
</nav>
|
|
19
|
+
</aside>
|
|
20
|
+
<main style="flex: 1; padding: 24px; overflow-y: auto; background: #ffffff;">
|
|
21
|
+
<h1 style="font-size: 24px; font-weight: 700; margin: 0 0 16px; color: #0f172a;">Overview</h1>
|
|
22
|
+
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 24px;">
|
|
23
|
+
<div style="padding: 20px; border-radius: 8px; border: 1px solid #e2e8f0;">
|
|
24
|
+
<div style="font-size: 13px; color: #64748b; margin-bottom: 4px;">Total Users</div>
|
|
25
|
+
<div style="font-size: 28px; font-weight: 700; color: #0f172a;">1,234</div>
|
|
26
|
+
</div>
|
|
27
|
+
<div style="padding: 20px; border-radius: 8px; border: 1px solid #e2e8f0;">
|
|
28
|
+
<div style="font-size: 13px; color: #64748b; margin-bottom: 4px;">Revenue</div>
|
|
29
|
+
<div style="font-size: 28px; font-weight: 700; color: #0f172a;">$12.4k</div>
|
|
30
|
+
</div>
|
|
31
|
+
<div style="padding: 20px; border-radius: 8px; border: 1px solid #e2e8f0;">
|
|
32
|
+
<div style="font-size: 13px; color: #64748b; margin-bottom: 4px;">Active Now</div>
|
|
33
|
+
<div style="font-size: 28px; font-weight: 700; color: #0f172a;">89</div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
<div style="padding: 20px; border-radius: 8px; border: 1px solid #e2e8f0;">
|
|
37
|
+
<h2 style="font-size: 16px; font-weight: 600; margin: 0 0 12px; color: #0f172a;">Recent Activity</h2>
|
|
38
|
+
<div style="display: flex; flex-direction: column; gap: 12px;">
|
|
39
|
+
<div style="display: flex; justify-content: space-between; padding-bottom: 12px; border-bottom: 1px solid #f1f5f9;">
|
|
40
|
+
<span style="color: #334155; font-size: 14px;">New user registered</span>
|
|
41
|
+
<span style="color: #94a3b8; font-size: 13px;">2 min ago</span>
|
|
42
|
+
</div>
|
|
43
|
+
<div style="display: flex; justify-content: space-between; padding-bottom: 12px; border-bottom: 1px solid #f1f5f9;">
|
|
44
|
+
<span style="color: #334155; font-size: 14px;">Payment received</span>
|
|
45
|
+
<span style="color: #94a3b8; font-size: 13px;">15 min ago</span>
|
|
46
|
+
</div>
|
|
47
|
+
<div style="display: flex; justify-content: space-between;">
|
|
48
|
+
<span style="color: #334155; font-size: 14px;">Report generated</span>
|
|
49
|
+
<span style="color: #94a3b8; font-size: 13px;">1 hour ago</span>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</main>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
[/LAYOUT]
|