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 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>Default</div>{/if}
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
- const tagStart = snippetBody.indexOf(`<${name}`);
111
- if (tagStart === -1) return snippetBody;
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
- value: s.name in liveStateValues ? JSON.stringify(liveStateValues[s.name]) : undefined,
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
- <DataTable
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
- <DataTable
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 = String(value).trim();
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 = String(value).trim();
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 snippet's root component so the wrapper can reach
39
- * its exported methods and state. Skipped when the author already binds. */
40
- function injectRootRef(snippetBody) {
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
- const match = snippetBody.match(/<([A-Z][A-Za-z0-9_]*)/);
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
- return generateIframeComponent(absoluteImports, snippet.body, stateNames);
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,6 +1,6 @@
1
1
  {
2
2
  "name": "sdocs",
3
- "version": "0.0.38",
3
+ "version": "0.0.40",
4
4
  "description": "A lightweight documentation tool for Svelte 5 components",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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 {};