sdocs 0.0.52 → 0.0.53

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,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.0.53] - 2026-07-05
11
+
12
+ ### Added
13
+
14
+ - **Content layout is configurable — globally and in place.** The config's
15
+ `content` option sets per-kind defaults, and the same names work as entity
16
+ and block attributes (block beats entity beats config):
17
+ - `maxWidth` — the content column for `[DOCS]`/`[PAGE]` (default `1200px`),
18
+ the stage for `[LAYOUT]` (default `100%`) and for `[preview]`/`[example]`
19
+ (default `100%`; narrower stages center).
20
+ - `padding` — page content `32px`, preview/example stages `16px`
21
+ (settable on `[DOCS]` as the entity default), layout stages `0px`.
22
+ - `direction` / `gap` — preview/example stages are flex containers:
23
+ `flex-direction` (default `row`) and `gap` (default `16px`).
24
+ - `toc` — `[PAGE]` table-of-contents visibility (default `true`;
25
+ `toc="false"` hides it).
26
+
10
27
  ## [0.0.52] - 2026-07-05
11
28
 
12
29
  ### Added
@@ -288,7 +288,7 @@
288
288
  );
289
289
  </script>
290
290
 
291
- <div class="sdocs-component-view">
291
+ <div class="sdocs-component-view" style:max-width={doc.maxWidth}>
292
292
  {#if focusedSnippet}
293
293
  <!-- Example full-page view -->
294
294
  <div class="sdocs-view-header">
@@ -482,7 +482,7 @@
482
482
  <style>
483
483
  .sdocs-component-view {
484
484
  padding: 24px 32px;
485
- max-width: 1120px;
485
+ /* max-width comes from the doc entry (config/entity cascade) */
486
486
  font-family: var(--sans);
487
487
  }
488
488
  .sdocs-doc-section {
@@ -579,8 +579,8 @@
579
579
  overflow: hidden;
580
580
  }
581
581
  .sdocs-preview-wrapper {
582
+ /* stage padding lives inside the iframe (config/entity/block cascade) */
582
583
  background: var(--color-base-0);
583
- padding: 16px;
584
584
  }
585
585
  .sdocs-divider {
586
586
  border: none;
@@ -23,7 +23,7 @@
23
23
  </script>
24
24
 
25
25
  <div class="sdocs-page-view">
26
- <div class="sdocs-page-main">
26
+ <div class="sdocs-page-main" style:max-width={doc.maxWidth}>
27
27
  <!-- Header -->
28
28
  <div class="sdocs-view-header">
29
29
  <h1 class="sdocs-view-title">{displayTitle(meta.title)}</h1>
@@ -41,7 +41,7 @@
41
41
  </div>
42
42
 
43
43
  <!-- Table of Contents -->
44
- {#if toc.length > 0}
44
+ {#if toc.length > 0 && doc.showToc !== false}
45
45
  <aside class="sdocs-toc">
46
46
  <h3 class="sdocs-toc-title">On this page</h3>
47
47
  <nav>
@@ -1,4 +1,4 @@
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
2
  export { projectSdoc, type SdocProjection, type ProjectedLineKind, } from './projection.js';
3
3
  export { segmentPageBody, type PageSegment, } from './page-islands.js';
4
- export { parseSdoc, parseArgsLiteral, slugifyTitle, normalizeBody, type ArgValue, type PreviewBlock, type ExampleBlock, type DocsEntity, type PageEntity, type LayoutEntity, type SdocEntity, type SdocDocument, } from './parser.js';
4
+ export { parseSdoc, parseArgsLiteral, slugifyTitle, normalizeBody, type ArgValue, type Sizing, type PreviewBlock, type ExampleBlock, type DocsEntity, type PageEntity, type LayoutEntity, type SdocEntity, type SdocDocument, } from './parser.js';
@@ -5,6 +5,17 @@
5
5
  */
6
6
  import { type ScanError, type Span, type TagBlock } from './scanner.js';
7
7
  export type ArgValue = string | number | boolean;
8
+ /** Explicit presentation overrides from stage attributes */
9
+ export interface Sizing {
10
+ maxWidth: string | null;
11
+ padding: string | null;
12
+ /** flex-direction of preview/example stages */
13
+ direction: string | null;
14
+ /** gap of preview/example stages */
15
+ gap: string | null;
16
+ /** table-of-contents visibility (PAGE) */
17
+ toc: boolean | null;
18
+ }
8
19
  export interface PreviewBlock {
9
20
  /** The identifier from component={X}; null when invalid/missing (already reported) */
10
21
  componentName: string | null;
@@ -16,12 +27,14 @@ export interface PreviewBlock {
16
27
  title: string | null;
17
28
  /** Tab label: the title override or the component name */
18
29
  label: string;
30
+ sizing: Sizing;
19
31
  body: string;
20
32
  bodySpan: Span;
21
33
  span: Span;
22
34
  }
23
35
  export interface ExampleBlock {
24
36
  title: string;
37
+ sizing: Sizing;
25
38
  body: string;
26
39
  bodySpan: Span;
27
40
  span: Span;
@@ -31,6 +44,7 @@ export interface DocsEntity {
31
44
  title: string;
32
45
  slug: string;
33
46
  description: string | null;
47
+ sizing: Sizing;
34
48
  previews: PreviewBlock[];
35
49
  examples: ExampleBlock[];
36
50
  openerSpan: Span;
@@ -40,6 +54,7 @@ export interface PageEntity {
40
54
  kind: 'PAGE';
41
55
  title: string;
42
56
  slug: string;
57
+ sizing: Sizing;
43
58
  body: string;
44
59
  bodySpan: Span;
45
60
  openerSpan: Span;
@@ -49,7 +64,7 @@ export interface LayoutEntity {
49
64
  kind: 'LAYOUT';
50
65
  title: string;
51
66
  slug: string;
52
- padding: string | null;
67
+ sizing: Sizing;
53
68
  body: string;
54
69
  bodySpan: Span;
55
70
  openerSpan: Span;
@@ -43,17 +43,39 @@ export function slugifyTitle(title) {
43
43
  .replace(/^-+|-+$/g, '') || 'untitled');
44
44
  }
45
45
  const IDENTIFIER_RE = /^[A-Z][A-Za-z0-9_]*$/;
46
+ const SIZING_ATTR_RULES = {
47
+ maxWidth: { required: false, kind: 'string', hint: 'maxWidth="1200px"' },
48
+ padding: { required: false, kind: 'string', hint: 'padding="32px"' },
49
+ };
50
+ const STAGE_LAYOUT_ATTR_RULES = {
51
+ direction: { required: false, kind: 'string', hint: 'direction="column"' },
52
+ gap: { required: false, kind: 'string', hint: 'gap="16px"' },
53
+ };
54
+ function sizingOf(attrs) {
55
+ const toc = stringAttr(attrs, 'toc');
56
+ return {
57
+ maxWidth: stringAttr(attrs, 'maxWidth'),
58
+ padding: stringAttr(attrs, 'padding'),
59
+ direction: stringAttr(attrs, 'direction'),
60
+ gap: stringAttr(attrs, 'gap'),
61
+ toc: toc === null ? null : toc === 'true',
62
+ };
63
+ }
46
64
  const ENTITY_ATTR_RULES = {
47
65
  DOCS: {
48
66
  title: { required: true, kind: 'string', hint: 'title="Group / Name"' },
49
67
  description: { required: false, kind: 'string', hint: 'description="…"' },
68
+ ...SIZING_ATTR_RULES,
69
+ ...STAGE_LAYOUT_ATTR_RULES,
50
70
  },
51
71
  PAGE: {
52
72
  title: { required: true, kind: 'string', hint: 'title="Group / Name"' },
73
+ ...SIZING_ATTR_RULES,
74
+ toc: { required: false, kind: 'string', hint: 'toc="false"' },
53
75
  },
54
76
  LAYOUT: {
55
77
  title: { required: true, kind: 'string', hint: 'title="Group / Name"' },
56
- padding: { required: false, kind: 'string', hint: 'padding="48px"' },
78
+ ...SIZING_ATTR_RULES,
57
79
  },
58
80
  };
59
81
  const SUB_BLOCK_ATTR_RULES = {
@@ -61,9 +83,13 @@ const SUB_BLOCK_ATTR_RULES = {
61
83
  component: { required: true, kind: 'expression', hint: 'component={Button}' },
62
84
  args: { required: false, kind: 'expression', hint: 'args={{ label: "Hi" }}' },
63
85
  title: { required: false, kind: 'string', hint: 'title="…"' },
86
+ ...SIZING_ATTR_RULES,
87
+ ...STAGE_LAYOUT_ATTR_RULES,
64
88
  },
65
89
  example: {
66
90
  title: { required: true, kind: 'string', hint: 'title="…"' },
91
+ ...SIZING_ATTR_RULES,
92
+ ...STAGE_LAYOUT_ATTR_RULES,
67
93
  },
68
94
  };
69
95
  function checkAttrs(owner, attrs, rules, ownerSpan, diagnostics) {
@@ -192,6 +218,7 @@ function parsePreview(block, diagnostics) {
192
218
  argsRaw,
193
219
  title,
194
220
  label: title ?? componentName ?? 'Preview',
221
+ sizing: sizingOf(block.attrs),
195
222
  body: normalizeBody(block.body),
196
223
  bodySpan: block.bodySpan,
197
224
  span: block.span,
@@ -229,6 +256,7 @@ function parseDocs(entity, diagnostics) {
229
256
  exampleTitles.add(title);
230
257
  examples.push({
231
258
  title,
259
+ sizing: sizingOf(block.attrs),
232
260
  body: normalizeBody(block.body),
233
261
  bodySpan: block.bodySpan,
234
262
  span: block.span,
@@ -241,6 +269,7 @@ function parseDocs(entity, diagnostics) {
241
269
  title,
242
270
  slug: slugifyTitle(title),
243
271
  description: stringAttr(entity.attrs, 'description'),
272
+ sizing: sizingOf(entity.attrs),
244
273
  previews,
245
274
  examples,
246
275
  openerSpan: entity.openerSpan,
@@ -263,15 +292,13 @@ export function parseSdoc(source) {
263
292
  const base = {
264
293
  title,
265
294
  slug: slugifyTitle(title),
295
+ sizing: sizingOf(entity.attrs),
266
296
  body: normalizeBody(entity.body),
267
297
  bodySpan: entity.bodySpan,
268
298
  openerSpan: entity.openerSpan,
269
299
  span: entity.span,
270
300
  };
271
- typed =
272
- entity.kind === 'PAGE'
273
- ? { kind: 'PAGE', ...base }
274
- : { kind: 'LAYOUT', padding: stringAttr(entity.attrs, 'padding'), ...base };
301
+ typed = entity.kind === 'PAGE' ? { kind: 'PAGE', ...base } : { kind: 'LAYOUT', ...base };
275
302
  }
276
303
  if (slugs.has(typed.slug)) {
277
304
  diagnostics.push({
@@ -13,6 +13,11 @@ const DEFAULTS = {
13
13
  order: {},
14
14
  open: [],
15
15
  },
16
+ content: {
17
+ page: { maxWidth: '1200px', padding: '32px', toc: true },
18
+ docs: { maxWidth: '1200px', padding: '16px', direction: 'row', gap: '16px' },
19
+ layout: { maxWidth: '100%', padding: '0px' },
20
+ },
16
21
  };
17
22
  /** Find the config file path in the given directory */
18
23
  export function findConfigFile(root) {
@@ -83,5 +88,10 @@ export function resolveConfig(userConfig) {
83
88
  order: userConfig.sidebar?.order ?? DEFAULTS.sidebar.order,
84
89
  open: userConfig.sidebar?.open ?? DEFAULTS.sidebar.open,
85
90
  },
91
+ content: {
92
+ page: { ...DEFAULTS.content.page, ...userConfig.content?.page },
93
+ docs: { ...DEFAULTS.content.docs, ...userConfig.content?.docs },
94
+ layout: { ...DEFAULTS.content.layout, ...userConfig.content?.layout },
95
+ },
86
96
  };
87
97
  }
@@ -23,7 +23,12 @@ export declare function resolveScriptImports(script: string, docFilePath: string
23
23
  /** Generate a virtual Svelte iframe wrapper component for a snippet.
24
24
  * Includes $state for reactive prop updates via postMessage, invokes
25
25
  * component methods on request, and broadcasts exported state values. */
26
- export declare function generateIframeComponent(scriptPrelude: string, snippetBody: string, stateNames?: string[], componentName?: string): string;
26
+ export declare function generateIframeComponent(scriptPrelude: string, snippetBody: string, stateNames?: string[], componentName?: string, stage?: {
27
+ maxWidth: string;
28
+ padding: string;
29
+ direction?: string;
30
+ gap?: string;
31
+ }): string;
27
32
  /** The JS that boots a preview page: mount the wrapper + parent-frame messaging. */
28
33
  export declare function generateMountScript(iframeComponentId: string): string;
29
34
  /** Generate the HTML page served inside the iframe (dev / CLI-build inputs) */
@@ -81,7 +81,27 @@ function injectRootRef(snippetBody, componentName) {
81
81
  /** Generate a virtual Svelte iframe wrapper component for a snippet.
82
82
  * Includes $state for reactive prop updates via postMessage, invokes
83
83
  * component methods on request, and broadcasts exported state values. */
84
- export function generateIframeComponent(scriptPrelude, snippetBody, stateNames = [], componentName) {
84
+ export function generateIframeComponent(scriptPrelude, snippetBody, stateNames = [], componentName, stage) {
85
+ // The stage layout (config -> entity -> block cascade) applies here, inside
86
+ // the iframe, so every consumer of the preview page gets it. Preview and
87
+ // example stages are flex containers (direction + gap); page and layout
88
+ // stages are flow-root blocks. Both contain child margins, so the height
89
+ // reported for iframe auto-sizing is exact.
90
+ const stageStyle = [
91
+ ...(stage?.direction
92
+ ? [
93
+ 'display: flex',
94
+ `flex-direction: ${stage.direction}`,
95
+ 'flex-wrap: wrap',
96
+ 'align-items: flex-start',
97
+ `gap: ${stage.gap}`,
98
+ ]
99
+ : ['display: flow-root']),
100
+ ...(stage ? [`padding: ${stage.padding}`] : []),
101
+ ...(stage && stage.maxWidth !== '100%'
102
+ ? [`max-width: ${stage.maxWidth}`, 'margin-inline: auto']
103
+ : []),
104
+ ].join('; ');
85
105
  // The file <script> (imports + shared values), lifted verbatim so previews
86
106
  // and examples see everything the entity's siblings do.
87
107
  const importBlock = scriptPrelude.trim() ? scriptPrelude.trim() + '\n' : '';
@@ -151,7 +171,7 @@ ${stateBroadcast}
151
171
  });
152
172
  </script>
153
173
 
154
- <div id="sdocs-preview" style="display: flow-root">
174
+ <div id="sdocs-preview" style="${stageStyle}">
155
175
  {#snippet SdocsPreview(args)}
156
176
  ${injectRootRef(snippetBody, componentName)}
157
177
  {/snippet}
package/dist/types.d.ts CHANGED
@@ -19,6 +19,38 @@ export interface SdocsConfig {
19
19
  /** Folders expanded by default on load. */
20
20
  open?: string[];
21
21
  };
22
+ /** Content presentation per entity kind; entity/block attributes override these. */
23
+ content?: {
24
+ /** [PAGE] content. Defaults: maxWidth '1200px', padding '32px', toc true. */
25
+ page?: ContentSizing & {
26
+ /** Show the table of contents. Default: true */
27
+ toc?: boolean;
28
+ };
29
+ /** [DOCS] pages: maxWidth is the content column (default '1200px');
30
+ * padding/direction/gap are the default preview/example stage layout
31
+ * (defaults '16px', 'row', '16px'). */
32
+ docs?: ContentSizing & {
33
+ /** Stage flex-direction. Default: 'row' */
34
+ direction?: string;
35
+ /** Stage gap. Default: '16px' */
36
+ gap?: string;
37
+ };
38
+ /** [LAYOUT] stages. Defaults: maxWidth '100%', padding '0px'. */
39
+ layout?: ContentSizing;
40
+ };
41
+ }
42
+ /** Content sizing knobs (any CSS length; padding takes CSS shorthand) */
43
+ export interface ContentSizing {
44
+ maxWidth?: string;
45
+ padding?: string;
46
+ }
47
+ /** Resolved stage layout applied inside a preview iframe */
48
+ export interface StageLayout {
49
+ maxWidth: string;
50
+ padding: string;
51
+ /** flex-direction + gap; set for preview/example stages only */
52
+ direction?: string;
53
+ gap?: string;
22
54
  }
23
55
  /** Resolved config with all defaults applied */
24
56
  export interface ResolvedSdocsConfig {
@@ -32,6 +64,16 @@ export interface ResolvedSdocsConfig {
32
64
  order: Record<string, string[]>;
33
65
  open: string[];
34
66
  };
67
+ content: {
68
+ page: Required<ContentSizing> & {
69
+ toc: boolean;
70
+ };
71
+ docs: Required<ContentSizing> & {
72
+ direction: string;
73
+ gap: string;
74
+ };
75
+ layout: Required<ContentSizing>;
76
+ };
35
77
  }
36
78
  /** Entity metadata from a [DOCS]/[PAGE]/[LAYOUT] opener */
37
79
  export interface SdocMeta {
@@ -87,6 +129,8 @@ export interface ExtractedSnippet {
87
129
  highlightedHtml?: string;
88
130
  /** Preview URL for iframe (added by virtual module) */
89
131
  previewUrl?: string;
132
+ /** Resolved stage layout applied inside the iframe (config → entity → block) */
133
+ stage?: StageLayout;
90
134
  }
91
135
  /** One [preview] of a DOCS entity: a live showcase of one component */
92
136
  export interface PreviewEntry {
@@ -128,6 +172,8 @@ export interface DocEntry {
128
172
  content: ExtractedSnippet | null;
129
173
  /** Table of contents headings (pages only) */
130
174
  toc?: TocHeading[];
131
- /** Stage padding (layouts only) */
132
- padding?: string | null;
175
+ /** Resolved content-column max width (component and page kinds) */
176
+ maxWidth?: string;
177
+ /** Resolved table-of-contents visibility (pages only) */
178
+ showToc?: boolean;
133
179
  }
package/dist/vite.js CHANGED
@@ -228,7 +228,7 @@ export function sdocsPlugin(userConfig) {
228
228
  // example iframes fall back to the first preview's component.
229
229
  const preview = entry.previews.find((p) => p.snippet.slug === snippet.slug) ?? entry.previews[0];
230
230
  const stateNames = (preview?.componentData?.state ?? []).map((s) => s.name);
231
- return generateIframeComponent(scriptPrelude, snippet.body, stateNames, preview?.componentName ?? undefined);
231
+ return generateIframeComponent(scriptPrelude, snippet.body, stateNames, preview?.componentName ?? undefined, snippet.stage);
232
232
  }
233
233
  // Virtual mount script for an emitted preview page
234
234
  if (id.startsWith('\0' + MOUNT_PREFIX)) {
@@ -299,6 +299,27 @@ export function sdocsPlugin(userConfig) {
299
299
  examples: [],
300
300
  content: null,
301
301
  };
302
+ // Sizing cascade: block attribute -> entity attribute -> config default.
303
+ const kindKey = entity.kind === 'DOCS' ? 'docs' : entity.kind === 'PAGE' ? 'page' : 'layout';
304
+ const kindDefaults = config.content[kindKey];
305
+ const stageOf = (block) => ({
306
+ // Entity-level maxWidth on DOCS/PAGE is the content column, not the
307
+ // stage; stages inside them span their panel unless the block says so.
308
+ maxWidth: block?.maxWidth ??
309
+ (entity.kind === 'LAYOUT' ? (entity.sizing.maxWidth ?? kindDefaults.maxWidth) : '100%'),
310
+ padding: block?.padding ?? entity.sizing.padding ?? kindDefaults.padding,
311
+ // direction/gap flex the preview/example stages only
312
+ ...(entity.kind === 'DOCS' && block
313
+ ? {
314
+ direction: block.direction ?? entity.sizing.direction ?? config.content.docs.direction,
315
+ gap: block.gap ?? entity.sizing.gap ?? config.content.docs.gap,
316
+ }
317
+ : {}),
318
+ });
319
+ entry.maxWidth = entity.sizing.maxWidth ?? kindDefaults.maxWidth;
320
+ if (entity.kind === 'PAGE') {
321
+ entry.showToc = entity.sizing.toc ?? config.content.page.toc;
322
+ }
302
323
  if (entity.kind === 'DOCS') {
303
324
  if (entity.description)
304
325
  entry.meta.description = entity.description;
@@ -318,6 +339,7 @@ export function sdocsPlugin(userConfig) {
318
339
  console.warn(`[sdocs] ${filePath}: component {${preview.componentName}} is not imported in the file's <script>`);
319
340
  }
320
341
  }
342
+ snippet.stage = stageOf(preview.sizing);
321
343
  snippet.highlightedHtml = await highlight(snippet.body);
322
344
  entry.previews.push({
323
345
  label: preview.label,
@@ -330,6 +352,10 @@ export function sdocsPlugin(userConfig) {
330
352
  });
331
353
  }
332
354
  entry.examples = snippets.filter((s) => s.role === 'example');
355
+ entity.examples.forEach((example, i) => {
356
+ if (entry.examples[i])
357
+ entry.examples[i].stage = stageOf(example.sizing);
358
+ });
333
359
  for (const example of entry.examples) {
334
360
  example.highlightedHtml = await highlight(example.body);
335
361
  }
@@ -337,12 +363,13 @@ export function sdocsPlugin(userConfig) {
337
363
  else if (entity.kind === 'PAGE') {
338
364
  const rendered = await renderPageMarkdown(entity.body);
339
365
  snippets[0].body = rendered.html;
366
+ snippets[0].stage = stageOf();
340
367
  entry.content = snippets[0];
341
368
  entry.toc = rendered.toc;
342
369
  }
343
370
  else {
371
+ snippets[0].stage = stageOf();
344
372
  entry.content = snippets[0];
345
- entry.padding = entity.padding;
346
373
  }
347
374
  docEntries.set(entityKey(filePath, entity.slug), entry);
348
375
  }
@@ -369,7 +396,8 @@ export function sdocsPlugin(userConfig) {
369
396
  examples: e.examples.map((s) => withUrl(e, s)),
370
397
  content: e.content ? withUrl(e, e.content) : null,
371
398
  toc: e.toc,
372
- padding: e.padding,
399
+ maxWidth: e.maxWidth,
400
+ showToc: e.showToc,
373
401
  }));
374
402
  // Extract named CSS stylesheet names (empty array if single string or null)
375
403
  const cssNames = config.css && typeof config.css === 'object'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sdocs",
3
- "version": "0.0.52",
3
+ "version": "0.0.53",
4
4
  "description": "A lightweight documentation tool for Svelte 5 components",
5
5
  "type": "module",
6
6
  "license": "MIT",