sdocs 0.0.38 → 0.0.40
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 +30 -0
- package/dist/explorer/views/ApiTable.svelte +7 -3
- package/dist/explorer/views/ComponentView.svelte +9 -24
- package/dist/explorer/views/format.js +8 -2
- package/dist/server/snippet-compiler.d.ts +1 -1
- package/dist/server/snippet-compiler.js +15 -6
- package/dist/server/snippet-compiler.test.d.ts +1 -0
- package/dist/server/snippet-compiler.test.js +23 -0
- package/dist/vite.js +2 -1
- package/package.json +1 -1
- package/dist/explorer/views/DataTable.svelte +0 -121
- package/dist/explorer/views/DataTable.svelte.d.ts +0 -14
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.0.40] - 2026-07-04
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Wrapped previews bind to the documented component.** When a preview
|
|
15
|
+
wraps the documented component in another one (a `<Tab>` shown inside
|
|
16
|
+
`<Tabs>`), method calls and live state used to attach to the outer
|
|
17
|
+
wrapper — the first capitalized tag in the snippet. The preview ref now
|
|
18
|
+
binds to the doc's own component wherever it appears in the snippet,
|
|
19
|
+
falling back to the first capitalized tag when it's absent.
|
|
20
|
+
- **Usage-code patching matches the component name exactly.** Attribute
|
|
21
|
+
updates from the controls located the root tag with a prefix search, so
|
|
22
|
+
a component like `Tab` could patch a sibling `<Tabs>` tag instead of its
|
|
23
|
+
own. The tag search now requires a whole-name match.
|
|
24
|
+
|
|
25
|
+
## [0.0.39] - 2026-07-04
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **Methods and States join the unified section layout.** Methods render a
|
|
30
|
+
single signature chip (`(params) => returns`, colored like events) with
|
|
31
|
+
the Run button in the control rail; States show their live value in a
|
|
32
|
+
"Current value" rail. The value rail widens to align section right edges
|
|
33
|
+
when it's the last column. All six sections now share one table
|
|
34
|
+
implementation.
|
|
35
|
+
- **JSDoc types display like TypeScript types.** `import('module').`
|
|
36
|
+
qualifiers are stripped for display and classification, so
|
|
37
|
+
`import('svelte').Snippet<[…]>` renders as the same pink `Snippet<[…]>`
|
|
38
|
+
chip as in TS components.
|
|
39
|
+
|
|
10
40
|
## [0.0.38] - 2026-07-04
|
|
11
41
|
|
|
12
42
|
### Changed
|
|
@@ -16,12 +16,16 @@
|
|
|
16
16
|
control?: Snippet<[Row]>;
|
|
17
17
|
/** Show the Default column (rows without defaults, e.g. events, turn it off) */
|
|
18
18
|
showDefault?: boolean;
|
|
19
|
+
/** Header label of the Default column (e.g. "Current value" for states) */
|
|
20
|
+
defaultLabel?: string;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
let { rows, control, showDefault = true }: Props = $props();
|
|
23
|
+
let { rows, control, showDefault = true, defaultLabel = 'Default' }: Props = $props();
|
|
22
24
|
|
|
25
|
+
// The default/value column widens to the control-rail width when it is the
|
|
26
|
+
// last column, so section right edges align.
|
|
23
27
|
const gridColumns = $derived(
|
|
24
|
-
['200px', 'minmax(0, 1fr)', ...(showDefault ? ['120px'] : []), ...(control ? ['200px'] : [])].join(' '),
|
|
28
|
+
['200px', 'minmax(0, 1fr)', ...(showDefault ? [control ? '120px' : '200px'] : []), ...(control ? ['200px'] : [])].join(' '),
|
|
25
29
|
);
|
|
26
30
|
</script>
|
|
27
31
|
|
|
@@ -30,7 +34,7 @@
|
|
|
30
34
|
<div class="sdocs-api-header" style:grid-template-columns={gridColumns}>
|
|
31
35
|
<div>Name</div>
|
|
32
36
|
<div>Details</div>
|
|
33
|
-
{#if showDefault}<div>
|
|
37
|
+
{#if showDefault}<div>{defaultLabel}</div>{/if}
|
|
34
38
|
{#if control}<div>Control</div>{/if}
|
|
35
39
|
</div>
|
|
36
40
|
{#each rows as row (row.name)}
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
import { highlightSvelte } from '../highlighter.js';
|
|
5
5
|
import CollapsiblePanel from './CollapsiblePanel.svelte';
|
|
6
6
|
import PreviewFrame from './PreviewFrame.svelte';
|
|
7
|
-
import DataTable from './DataTable.svelte';
|
|
8
7
|
import ApiTable from './ApiTable.svelte';
|
|
9
8
|
import PropControl from './PropControl.svelte';
|
|
10
9
|
import CssPropControl from './CssPropControl.svelte';
|
|
@@ -107,8 +106,11 @@
|
|
|
107
106
|
if (changes.length === 0) return snippetBody;
|
|
108
107
|
|
|
109
108
|
// Find the opening tag: <ComponentName ...> or <ComponentName ... />
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
// (whole-name match, so <Tab doesn't hit <Tabs)
|
|
110
|
+
const escapedName = name.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
111
|
+
const tagMatch = snippetBody.match(new RegExp(`<${escapedName}(?=[\\s/>])`));
|
|
112
|
+
if (!tagMatch || tagMatch.index === undefined) return snippetBody;
|
|
113
|
+
const tagStart = tagMatch.index;
|
|
112
114
|
|
|
113
115
|
// Find the end of the opening tag, respecting {} expressions
|
|
114
116
|
let braceDepth = 0;
|
|
@@ -249,15 +251,15 @@
|
|
|
249
251
|
const methodsRows = $derived(
|
|
250
252
|
(cd?.methods ?? []).map((m) => ({
|
|
251
253
|
name: m.name,
|
|
254
|
+
type: `(${m.params ?? ''}) => ${m.returnType || 'void'}`,
|
|
252
255
|
params: m.params,
|
|
253
|
-
returns: m.returnType,
|
|
254
256
|
description: m.description,
|
|
255
257
|
})),
|
|
256
258
|
);
|
|
257
259
|
|
|
258
260
|
const stateRows = $derived(
|
|
259
261
|
(cd?.state ?? []).map((s) => ({
|
|
260
|
-
|
|
262
|
+
default: s.name in liveStateValues ? JSON.stringify(liveStateValues[s.name]) : undefined,
|
|
261
263
|
name: s.name,
|
|
262
264
|
type: s.type,
|
|
263
265
|
description: s.description,
|
|
@@ -380,16 +382,7 @@
|
|
|
380
382
|
<Icon name="square-function" --w="15px" --h="15px" --fill="var(--color-base-500)" />
|
|
381
383
|
Methods
|
|
382
384
|
</h2>
|
|
383
|
-
<
|
|
384
|
-
columns={[
|
|
385
|
-
{ key: 'name', label: 'Name', kind: 'name' },
|
|
386
|
-
{ key: 'params', label: 'Parameters' },
|
|
387
|
-
{ key: 'returns', label: 'Returns', kind: 'type' },
|
|
388
|
-
{ key: 'description', label: 'Description', kind: 'text' },
|
|
389
|
-
]}
|
|
390
|
-
rows={methodsRows}
|
|
391
|
-
control={methodsRows.length > 0 ? methodControl : undefined}
|
|
392
|
-
/>
|
|
385
|
+
<ApiTable rows={methodsRows} showDefault={false} control={methodControl} />
|
|
393
386
|
</section>
|
|
394
387
|
|
|
395
388
|
<section class="sdocs-doc-section">
|
|
@@ -397,15 +390,7 @@
|
|
|
397
390
|
<Icon name="database" --w="15px" --h="15px" --fill="var(--color-base-500)" />
|
|
398
391
|
States
|
|
399
392
|
</h2>
|
|
400
|
-
<
|
|
401
|
-
columns={[
|
|
402
|
-
{ key: 'name', label: 'Name', kind: 'name' },
|
|
403
|
-
{ key: 'type', label: 'Type', kind: 'type' },
|
|
404
|
-
{ key: 'value', label: 'Current value', kind: 'value' },
|
|
405
|
-
{ key: 'description', label: 'Description', kind: 'text' },
|
|
406
|
-
]}
|
|
407
|
-
rows={stateRows}
|
|
408
|
-
/>
|
|
393
|
+
<ApiTable rows={stateRows} defaultLabel="Current value" />
|
|
409
394
|
</section>
|
|
410
395
|
|
|
411
396
|
{#if exampleSnippets.length > 0}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
/** Drop `import('module').` qualifiers — JSDoc types carry them, TS ones don't */
|
|
2
|
+
function normalizeType(value) {
|
|
3
|
+
return String(value)
|
|
4
|
+
.trim()
|
|
5
|
+
.replace(/import\((['"])[^'"]*\1\)\./g, '');
|
|
6
|
+
}
|
|
1
7
|
/** Classify a type string for color coding (conventions, not a formal standard) */
|
|
2
8
|
export function typeClass(value) {
|
|
3
|
-
const v =
|
|
9
|
+
const v = normalizeType(value);
|
|
4
10
|
if (v === 'string' || /^'[^']*'(\s*\|\s*'[^']*')*$/.test(v))
|
|
5
11
|
return 'string';
|
|
6
12
|
if (v === 'number' || /^\d+(\.\d+)?(\s*\|\s*\d+(\.\d+)?)*$/.test(v))
|
|
@@ -36,7 +42,7 @@ export function valueClass(value) {
|
|
|
36
42
|
}
|
|
37
43
|
/** Split a clean union type into its members; everything else stays whole */
|
|
38
44
|
export function typeParts(value) {
|
|
39
|
-
const v =
|
|
45
|
+
const v = normalizeType(value);
|
|
40
46
|
if (/^'[^']*'(\s*\|\s*'[^']*')+$/.test(v) || /^\d+(\.\d+)?(\s*\|\s*\d+(\.\d+)?)+$/.test(v)) {
|
|
41
47
|
return v.split('|').map((part) => part.trim());
|
|
42
48
|
}
|
|
@@ -11,7 +11,7 @@ export declare function resolveImportsToAbsolute(imports: string[], docFilePath:
|
|
|
11
11
|
/** Generate a virtual Svelte iframe wrapper component for a snippet.
|
|
12
12
|
* Includes $state for reactive prop updates via postMessage, invokes
|
|
13
13
|
* component methods on request, and broadcasts exported state values. */
|
|
14
|
-
export declare function generateIframeComponent(absoluteImports: string[], snippetBody: string, stateNames?: string[]): string;
|
|
14
|
+
export declare function generateIframeComponent(absoluteImports: string[], snippetBody: string, stateNames?: string[], componentName?: string): string;
|
|
15
15
|
/** The JS that boots a preview page: mount the wrapper + parent-frame messaging. */
|
|
16
16
|
export declare function generateMountScript(iframeComponentId: string): string;
|
|
17
17
|
/** Generate the HTML page served inside the iframe (dev / CLI-build inputs) */
|
|
@@ -35,12 +35,21 @@ export function resolveImportsToAbsolute(imports, docFilePath) {
|
|
|
35
35
|
return imp;
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
|
-
/** Add bind:this to the
|
|
39
|
-
* its exported methods and state.
|
|
40
|
-
|
|
38
|
+
/** Add bind:this to the documented component in the snippet so the wrapper
|
|
39
|
+
* can reach its exported methods and state. Binds the first occurrence of
|
|
40
|
+
* the doc's component when named (so wrapped children like
|
|
41
|
+
* <Tabs><Tab/></Tabs> documenting Tab bind the right instance), otherwise
|
|
42
|
+
* the first capitalized tag. Skipped when the author already binds. */
|
|
43
|
+
function injectRootRef(snippetBody, componentName) {
|
|
41
44
|
if (snippetBody.includes('bind:this'))
|
|
42
45
|
return snippetBody;
|
|
43
|
-
|
|
46
|
+
let match = null;
|
|
47
|
+
if (componentName) {
|
|
48
|
+
const escaped = componentName.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
49
|
+
match = snippetBody.match(new RegExp(`<${escaped}(?=[\\s/>])`));
|
|
50
|
+
}
|
|
51
|
+
if (!match)
|
|
52
|
+
match = snippetBody.match(/<[A-Z][A-Za-z0-9_]*(?=[\s/>])/);
|
|
44
53
|
if (!match || match.index === undefined)
|
|
45
54
|
return snippetBody;
|
|
46
55
|
const insertAt = match.index + match[0].length;
|
|
@@ -49,7 +58,7 @@ function injectRootRef(snippetBody) {
|
|
|
49
58
|
/** Generate a virtual Svelte iframe wrapper component for a snippet.
|
|
50
59
|
* Includes $state for reactive prop updates via postMessage, invokes
|
|
51
60
|
* component methods on request, and broadcasts exported state values. */
|
|
52
|
-
export function generateIframeComponent(absoluteImports, snippetBody, stateNames = []) {
|
|
61
|
+
export function generateIframeComponent(absoluteImports, snippetBody, stateNames = [], componentName) {
|
|
53
62
|
const importBlock = absoluteImports.length > 0
|
|
54
63
|
? absoluteImports.join('\n') + '\n'
|
|
55
64
|
: '';
|
|
@@ -118,7 +127,7 @@ ${stateBroadcast}
|
|
|
118
127
|
|
|
119
128
|
<div id="sdocs-preview">
|
|
120
129
|
{#snippet SdocsPreview(args)}
|
|
121
|
-
${injectRootRef(snippetBody)}
|
|
130
|
+
${injectRootRef(snippetBody, componentName)}
|
|
122
131
|
{/snippet}
|
|
123
132
|
{@render SdocsPreview(args)}
|
|
124
133
|
</div>`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { generateIframeComponent } from './snippet-compiler.js';
|
|
3
|
+
describe('root ref injection', () => {
|
|
4
|
+
it('binds the documented component, not the wrapper', () => {
|
|
5
|
+
const out = generateIframeComponent([], '<Tabs>\n\t<Tab {...args} />\n</Tabs>', [], 'Tab');
|
|
6
|
+
expect(out).toContain('<Tab bind:this={__sdocsRef} {...args} />');
|
|
7
|
+
expect(out).not.toContain('<Tabs bind:this');
|
|
8
|
+
});
|
|
9
|
+
it('binds the first capitalized tag when no component is named', () => {
|
|
10
|
+
const out = generateIframeComponent([], '<Card><Button /></Card>', []);
|
|
11
|
+
expect(out).toContain('<Card bind:this={__sdocsRef}>');
|
|
12
|
+
});
|
|
13
|
+
it('falls back to the first capitalized tag when the named component is absent', () => {
|
|
14
|
+
const out = generateIframeComponent([], '<Tabs active={0}>text</Tabs>', [], 'Tab');
|
|
15
|
+
expect(out).toContain('<Tabs bind:this={__sdocsRef} active={0}>');
|
|
16
|
+
});
|
|
17
|
+
it('leaves snippets with an author bind:this untouched', () => {
|
|
18
|
+
const body = '<Tab bind:this={mine} />';
|
|
19
|
+
const out = generateIframeComponent([], body, [], 'Tab');
|
|
20
|
+
expect(out).toContain(body);
|
|
21
|
+
expect(out).not.toContain('bind:this={__sdocsRef}');
|
|
22
|
+
});
|
|
23
|
+
});
|
package/dist/vite.js
CHANGED
|
@@ -217,7 +217,8 @@ export function sdocsPlugin(userConfig) {
|
|
|
217
217
|
return null;
|
|
218
218
|
const absoluteImports = docImportsCache.get(parsed.docFilePath) ?? [];
|
|
219
219
|
const stateNames = (entry.componentData?.state ?? []).map((s) => s.name);
|
|
220
|
-
|
|
220
|
+
const componentName = entry.componentPath?.split('/').pop()?.replace('.svelte', '');
|
|
221
|
+
return generateIframeComponent(absoluteImports, snippet.body, stateNames, componentName);
|
|
221
222
|
}
|
|
222
223
|
// Virtual mount script for an emitted preview page
|
|
223
224
|
if (id.startsWith('\0' + MOUNT_PREFIX)) {
|
package/package.json
CHANGED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import type { Snippet } from 'svelte';
|
|
3
|
-
import { typeClass, typeParts, valueClass } from './format.js';
|
|
4
|
-
|
|
5
|
-
interface Column {
|
|
6
|
-
key: string;
|
|
7
|
-
label: string;
|
|
8
|
-
/** 'name' renders as bold plain text, 'text' as plain text,
|
|
9
|
-
* 'type' as a code chip color-coded by the type,
|
|
10
|
-
* 'value' as mono text color-coded by the literal; default is a code chip */
|
|
11
|
-
kind?: 'name' | 'text' | 'type' | 'value';
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface Props {
|
|
15
|
-
columns: Column[];
|
|
16
|
-
rows: Record<string, unknown>[];
|
|
17
|
-
/** When set, adds a Control column rendering this snippet per row */
|
|
18
|
-
control?: Snippet<[Record<string, unknown>]>;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
let { columns, rows, control }: Props = $props();
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
</script>
|
|
25
|
-
|
|
26
|
-
{#if rows.length > 0}
|
|
27
|
-
<table class="sdocs-table">
|
|
28
|
-
<thead>
|
|
29
|
-
<tr>
|
|
30
|
-
{#each columns as col (col.key)}
|
|
31
|
-
<th>{col.label}</th>
|
|
32
|
-
{/each}
|
|
33
|
-
{#if control}
|
|
34
|
-
<th class="sdocs-table-control-col">Control</th>
|
|
35
|
-
{/if}
|
|
36
|
-
</tr>
|
|
37
|
-
</thead>
|
|
38
|
-
<tbody>
|
|
39
|
-
{#each rows as row, i (i)}
|
|
40
|
-
<tr>
|
|
41
|
-
{#each columns as col (col.key)}
|
|
42
|
-
<td>
|
|
43
|
-
{#if row[col.key] == null}
|
|
44
|
-
<span class="sdocs-table-empty">—</span>
|
|
45
|
-
{:else if col.kind === 'name'}
|
|
46
|
-
<span class="sdocs-table-name">
|
|
47
|
-
{row[col.key]}{#if row.required === true}<span class="sdocs-table-required" title="Required">*</span>{/if}
|
|
48
|
-
</span>
|
|
49
|
-
{:else if col.kind === 'text'}
|
|
50
|
-
{row[col.key]}
|
|
51
|
-
{:else if col.kind === 'type'}
|
|
52
|
-
{#each typeParts(row[col.key]) as part, i (i)}
|
|
53
|
-
{#if i > 0}<span class="sdocs-typesep">|</span>{/if}
|
|
54
|
-
<code class="sdocs-type-{typeClass(row[col.key])}">{part}</code>
|
|
55
|
-
{/each}
|
|
56
|
-
{:else if col.kind === 'value'}
|
|
57
|
-
<span class="sdocs-value sdocs-value-{valueClass(row[col.key])}">{row[col.key]}</span>
|
|
58
|
-
{:else}
|
|
59
|
-
<code>{row[col.key]}</code>
|
|
60
|
-
{/if}
|
|
61
|
-
</td>
|
|
62
|
-
{/each}
|
|
63
|
-
{#if control}
|
|
64
|
-
<td class="sdocs-table-control-col">
|
|
65
|
-
{@render control(row)}
|
|
66
|
-
</td>
|
|
67
|
-
{/if}
|
|
68
|
-
</tr>
|
|
69
|
-
{/each}
|
|
70
|
-
</tbody>
|
|
71
|
-
</table>
|
|
72
|
-
{:else}
|
|
73
|
-
<p class="sdocs-table-none">None</p>
|
|
74
|
-
{/if}
|
|
75
|
-
|
|
76
|
-
<style>
|
|
77
|
-
.sdocs-table {
|
|
78
|
-
width: 100%;
|
|
79
|
-
border-collapse: collapse;
|
|
80
|
-
font-size: 13px;
|
|
81
|
-
}
|
|
82
|
-
.sdocs-table th {
|
|
83
|
-
text-align: left;
|
|
84
|
-
padding: 6px 12px;
|
|
85
|
-
border-bottom: 2px solid var(--color-base-200);
|
|
86
|
-
font-weight: 600;
|
|
87
|
-
color: var(--color-base-600);
|
|
88
|
-
font-size: 12px;
|
|
89
|
-
}
|
|
90
|
-
.sdocs-table td {
|
|
91
|
-
padding: 6px 12px;
|
|
92
|
-
border-bottom: 1px solid var(--color-base-100);
|
|
93
|
-
color: var(--color-base-800);
|
|
94
|
-
}
|
|
95
|
-
.sdocs-table code {
|
|
96
|
-
font-family: var(--mono);
|
|
97
|
-
font-size: 12px;
|
|
98
|
-
background: var(--color-base-100);
|
|
99
|
-
padding: 1px 4px;
|
|
100
|
-
border-radius: 3px;
|
|
101
|
-
}
|
|
102
|
-
.sdocs-table-name {
|
|
103
|
-
font-weight: 600;
|
|
104
|
-
}
|
|
105
|
-
.sdocs-table-required {
|
|
106
|
-
color: var(--color-red-500);
|
|
107
|
-
}
|
|
108
|
-
.sdocs-table-control-col {
|
|
109
|
-
width: 220px;
|
|
110
|
-
min-width: 180px;
|
|
111
|
-
}
|
|
112
|
-
.sdocs-table-empty {
|
|
113
|
-
color: var(--color-base-300);
|
|
114
|
-
}
|
|
115
|
-
.sdocs-table-none {
|
|
116
|
-
color: var(--color-base-400);
|
|
117
|
-
font-size: 13px;
|
|
118
|
-
font-style: italic;
|
|
119
|
-
margin: 0;
|
|
120
|
-
}
|
|
121
|
-
</style>
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { SvelteComponentTyped } from "svelte";
|
|
2
|
-
declare const __propDef: {
|
|
3
|
-
props: Record<string, never>;
|
|
4
|
-
events: {
|
|
5
|
-
[evt: string]: CustomEvent<any>;
|
|
6
|
-
};
|
|
7
|
-
slots: {};
|
|
8
|
-
};
|
|
9
|
-
export type DataTableProps = typeof __propDef.props;
|
|
10
|
-
export type DataTableEvents = typeof __propDef.events;
|
|
11
|
-
export type DataTableSlots = typeof __propDef.slots;
|
|
12
|
-
export default class DataTable extends SvelteComponentTyped<DataTableProps, DataTableEvents, DataTableSlots> {
|
|
13
|
-
}
|
|
14
|
-
export {};
|