sdocs 0.0.52 → 0.0.54

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,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.0.54] - 2026-07-05
11
+
12
+ ### Added
13
+
14
+ - **Stage alignment on previews and examples.** Two direction-aware
15
+ attributes (also settable per-kind in `content.docs` and on `[DOCS]`):
16
+ `align` — horizontal (`left`/`center`/`right`/`justify`) — and `alignY` —
17
+ vertical (`top`/`middle`/`bottom`/`justify`). sdocs maps each to the right
18
+ flex property for the current `direction`, so `align="center"` centers
19
+ horizontally in a row or a column; `justify` spreads items along the flow.
20
+ - **`attributeRules(kind)` in `sdocs/language`** — the allowed attributes and
21
+ value shapes for a block kind, the single source of truth behind both
22
+ diagnostics and the VS Code extension's attribute completions.
23
+
24
+ ## [0.0.53] - 2026-07-05
25
+
26
+ ### Added
27
+
28
+ - **Content layout is configurable — globally and in place.** The config's
29
+ `content` option sets per-kind defaults, and the same names work as entity
30
+ and block attributes (block beats entity beats config):
31
+ - `maxWidth` — the content column for `[DOCS]`/`[PAGE]` (default `1200px`),
32
+ the stage for `[LAYOUT]` (default `100%`) and for `[preview]`/`[example]`
33
+ (default `100%`; narrower stages center).
34
+ - `padding` — page content `32px`, preview/example stages `16px`
35
+ (settable on `[DOCS]` as the entity default), layout stages `0px`.
36
+ - `direction` / `gap` — preview/example stages are flex containers:
37
+ `flex-direction` (default `row`) and `gap` (default `16px`).
38
+ - `toc` — `[PAGE]` table-of-contents visibility (default `true`;
39
+ `toc="false"` hides it).
40
+
10
41
  ## [0.0.52] - 2026-07-05
11
42
 
12
43
  ### 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, attributeRules, type AttrRule, type ArgValue, type Sizing, type PreviewBlock, type ExampleBlock, type DocsEntity, type PageEntity, type LayoutEntity, type SdocEntity, type SdocDocument, } from './parser.js';
@@ -1,4 +1,4 @@
1
1
  export { scanSdoc, offsetToPosition, ENTITY_KINDS, SUB_BLOCK_KINDS, } from './scanner.js';
2
2
  export { projectSdoc, } from './projection.js';
3
3
  export { segmentPageBody, } from './page-islands.js';
4
- export { parseSdoc, parseArgsLiteral, slugifyTitle, normalizeBody, } from './parser.js';
4
+ export { parseSdoc, parseArgsLiteral, slugifyTitle, normalizeBody, attributeRules, } from './parser.js';
@@ -5,6 +5,21 @@
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
+ /** horizontal alignment of preview/example stage contents */
17
+ align: string | null;
18
+ /** vertical alignment of preview/example stage contents */
19
+ alignY: string | null;
20
+ /** table-of-contents visibility (PAGE) */
21
+ toc: boolean | null;
22
+ }
8
23
  export interface PreviewBlock {
9
24
  /** The identifier from component={X}; null when invalid/missing (already reported) */
10
25
  componentName: string | null;
@@ -16,12 +31,14 @@ export interface PreviewBlock {
16
31
  title: string | null;
17
32
  /** Tab label: the title override or the component name */
18
33
  label: string;
34
+ sizing: Sizing;
19
35
  body: string;
20
36
  bodySpan: Span;
21
37
  span: Span;
22
38
  }
23
39
  export interface ExampleBlock {
24
40
  title: string;
41
+ sizing: Sizing;
25
42
  body: string;
26
43
  bodySpan: Span;
27
44
  span: Span;
@@ -31,6 +48,7 @@ export interface DocsEntity {
31
48
  title: string;
32
49
  slug: string;
33
50
  description: string | null;
51
+ sizing: Sizing;
34
52
  previews: PreviewBlock[];
35
53
  examples: ExampleBlock[];
36
54
  openerSpan: Span;
@@ -40,6 +58,7 @@ export interface PageEntity {
40
58
  kind: 'PAGE';
41
59
  title: string;
42
60
  slug: string;
61
+ sizing: Sizing;
43
62
  body: string;
44
63
  bodySpan: Span;
45
64
  openerSpan: Span;
@@ -49,7 +68,7 @@ export interface LayoutEntity {
49
68
  kind: 'LAYOUT';
50
69
  title: string;
51
70
  slug: string;
52
- padding: string | null;
71
+ sizing: Sizing;
53
72
  body: string;
54
73
  bodySpan: Span;
55
74
  openerSpan: Span;
@@ -73,6 +92,17 @@ export interface SdocDocument {
73
92
  export declare function normalizeBody(raw: string): string;
74
93
  /** Slug used for entity addressing: relPath + '#' + slug. */
75
94
  export declare function slugifyTitle(title: string): string;
95
+ export interface AttrRule {
96
+ /** required | optional */
97
+ required: boolean;
98
+ /** expected value kind */
99
+ kind: 'string' | 'expression';
100
+ hint: string;
101
+ }
102
+ /** Allowed attributes and their value shapes for a block, keyed by kind
103
+ * ('DOCS'|'PAGE'|'LAYOUT'|'preview'|'example'). Single source of truth for
104
+ * both diagnostics and editor attribute completions. */
105
+ export declare function attributeRules(kind: string): Record<string, AttrRule>;
76
106
  /**
77
107
  * Parse a preview args expression: a flat object literal whose values are
78
108
  * plain literals (strings, numbers, booleans). Anything richer belongs in
@@ -43,17 +43,43 @@ 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
+ align: { required: false, kind: 'string', hint: 'align="center"' },
54
+ alignY: { required: false, kind: 'string', hint: 'alignY="middle"' },
55
+ };
56
+ function sizingOf(attrs) {
57
+ const toc = stringAttr(attrs, 'toc');
58
+ return {
59
+ maxWidth: stringAttr(attrs, 'maxWidth'),
60
+ padding: stringAttr(attrs, 'padding'),
61
+ direction: stringAttr(attrs, 'direction'),
62
+ gap: stringAttr(attrs, 'gap'),
63
+ align: stringAttr(attrs, 'align'),
64
+ alignY: stringAttr(attrs, 'alignY'),
65
+ toc: toc === null ? null : toc === 'true',
66
+ };
67
+ }
46
68
  const ENTITY_ATTR_RULES = {
47
69
  DOCS: {
48
70
  title: { required: true, kind: 'string', hint: 'title="Group / Name"' },
49
71
  description: { required: false, kind: 'string', hint: 'description="…"' },
72
+ ...SIZING_ATTR_RULES,
73
+ ...STAGE_LAYOUT_ATTR_RULES,
50
74
  },
51
75
  PAGE: {
52
76
  title: { required: true, kind: 'string', hint: 'title="Group / Name"' },
77
+ ...SIZING_ATTR_RULES,
78
+ toc: { required: false, kind: 'string', hint: 'toc="false"' },
53
79
  },
54
80
  LAYOUT: {
55
81
  title: { required: true, kind: 'string', hint: 'title="Group / Name"' },
56
- padding: { required: false, kind: 'string', hint: 'padding="48px"' },
82
+ ...SIZING_ATTR_RULES,
57
83
  },
58
84
  };
59
85
  const SUB_BLOCK_ATTR_RULES = {
@@ -61,11 +87,21 @@ const SUB_BLOCK_ATTR_RULES = {
61
87
  component: { required: true, kind: 'expression', hint: 'component={Button}' },
62
88
  args: { required: false, kind: 'expression', hint: 'args={{ label: "Hi" }}' },
63
89
  title: { required: false, kind: 'string', hint: 'title="…"' },
90
+ ...SIZING_ATTR_RULES,
91
+ ...STAGE_LAYOUT_ATTR_RULES,
64
92
  },
65
93
  example: {
66
94
  title: { required: true, kind: 'string', hint: 'title="…"' },
95
+ ...SIZING_ATTR_RULES,
96
+ ...STAGE_LAYOUT_ATTR_RULES,
67
97
  },
68
98
  };
99
+ /** Allowed attributes and their value shapes for a block, keyed by kind
100
+ * ('DOCS'|'PAGE'|'LAYOUT'|'preview'|'example'). Single source of truth for
101
+ * both diagnostics and editor attribute completions. */
102
+ export function attributeRules(kind) {
103
+ return ENTITY_ATTR_RULES[kind] ?? SUB_BLOCK_ATTR_RULES[kind] ?? {};
104
+ }
69
105
  function checkAttrs(owner, attrs, rules, ownerSpan, diagnostics) {
70
106
  for (const [name, value] of Object.entries(attrs)) {
71
107
  const rule = rules[name];
@@ -192,6 +228,7 @@ function parsePreview(block, diagnostics) {
192
228
  argsRaw,
193
229
  title,
194
230
  label: title ?? componentName ?? 'Preview',
231
+ sizing: sizingOf(block.attrs),
195
232
  body: normalizeBody(block.body),
196
233
  bodySpan: block.bodySpan,
197
234
  span: block.span,
@@ -229,6 +266,7 @@ function parseDocs(entity, diagnostics) {
229
266
  exampleTitles.add(title);
230
267
  examples.push({
231
268
  title,
269
+ sizing: sizingOf(block.attrs),
232
270
  body: normalizeBody(block.body),
233
271
  bodySpan: block.bodySpan,
234
272
  span: block.span,
@@ -241,6 +279,7 @@ function parseDocs(entity, diagnostics) {
241
279
  title,
242
280
  slug: slugifyTitle(title),
243
281
  description: stringAttr(entity.attrs, 'description'),
282
+ sizing: sizingOf(entity.attrs),
244
283
  previews,
245
284
  examples,
246
285
  openerSpan: entity.openerSpan,
@@ -263,15 +302,13 @@ export function parseSdoc(source) {
263
302
  const base = {
264
303
  title,
265
304
  slug: slugifyTitle(title),
305
+ sizing: sizingOf(entity.attrs),
266
306
  body: normalizeBody(entity.body),
267
307
  bodySpan: entity.bodySpan,
268
308
  openerSpan: entity.openerSpan,
269
309
  span: entity.span,
270
310
  };
271
- typed =
272
- entity.kind === 'PAGE'
273
- ? { kind: 'PAGE', ...base }
274
- : { kind: 'LAYOUT', padding: stringAttr(entity.attrs, 'padding'), ...base };
311
+ typed = entity.kind === 'PAGE' ? { kind: 'PAGE', ...base } : { kind: 'LAYOUT', ...base };
275
312
  }
276
313
  if (slugs.has(typed.slug)) {
277
314
  diagnostics.push({
@@ -13,6 +13,18 @@ const DEFAULTS = {
13
13
  order: {},
14
14
  open: [],
15
15
  },
16
+ content: {
17
+ page: { maxWidth: '1200px', padding: '32px', toc: true },
18
+ docs: {
19
+ maxWidth: '1200px',
20
+ padding: '16px',
21
+ direction: 'row',
22
+ gap: '16px',
23
+ align: 'left',
24
+ alignY: 'top',
25
+ },
26
+ layout: { maxWidth: '100%', padding: '0px' },
27
+ },
16
28
  };
17
29
  /** Find the config file path in the given directory */
18
30
  export function findConfigFile(root) {
@@ -83,5 +95,10 @@ export function resolveConfig(userConfig) {
83
95
  order: userConfig.sidebar?.order ?? DEFAULTS.sidebar.order,
84
96
  open: userConfig.sidebar?.open ?? DEFAULTS.sidebar.open,
85
97
  },
98
+ content: {
99
+ page: { ...DEFAULTS.content.page, ...userConfig.content?.page },
100
+ docs: { ...DEFAULTS.content.docs, ...userConfig.content?.docs },
101
+ layout: { ...DEFAULTS.content.layout, ...userConfig.content?.layout },
102
+ },
86
103
  };
87
104
  }
@@ -23,7 +23,14 @@ 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
+ align?: string;
32
+ alignY?: string;
33
+ }): string;
27
34
  /** The JS that boots a preview page: mount the wrapper + parent-frame messaging. */
28
35
  export declare function generateMountScript(iframeComponentId: string): string;
29
36
  /** Generate the HTML page served inside the iframe (dev / CLI-build inputs) */
@@ -81,7 +81,54 @@ 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 + alignment); page and
88
+ // layout stages are flow-root blocks. Both contain child margins, so the
89
+ // height reported for iframe auto-sizing is exact.
90
+ //
91
+ // align/alignY are *physical* (horizontal/vertical); which flex property
92
+ // each drives depends on direction. The flow axis (main) takes
93
+ // justify-content, the other (cross) takes align-items — and align-items
94
+ // has no distribution value, so a 'justify' that lands on the cross axis
95
+ // falls back to stretch.
96
+ const H = {
97
+ left: 'flex-start',
98
+ center: 'center',
99
+ right: 'flex-end',
100
+ justify: 'space-between',
101
+ };
102
+ const V = {
103
+ top: 'flex-start',
104
+ middle: 'center',
105
+ bottom: 'flex-end',
106
+ justify: 'space-between',
107
+ };
108
+ const flexStage = () => {
109
+ const h = H[stage.align ?? 'left'] ?? 'flex-start';
110
+ const v = V[stage.alignY ?? 'top'] ?? 'flex-start';
111
+ const isColumn = String(stage.direction).startsWith('column');
112
+ const justify = isColumn ? v : h;
113
+ let alignItems = isColumn ? h : v;
114
+ if (alignItems === 'space-between')
115
+ alignItems = 'stretch';
116
+ return [
117
+ 'display: flex',
118
+ `flex-direction: ${stage.direction}`,
119
+ 'flex-wrap: wrap',
120
+ `justify-content: ${justify}`,
121
+ `align-items: ${alignItems}`,
122
+ `gap: ${stage.gap}`,
123
+ ];
124
+ };
125
+ const stageStyle = [
126
+ ...(stage?.direction ? flexStage() : ['display: flow-root']),
127
+ ...(stage ? [`padding: ${stage.padding}`] : []),
128
+ ...(stage && stage.maxWidth !== '100%'
129
+ ? [`max-width: ${stage.maxWidth}`, 'margin-inline: auto']
130
+ : []),
131
+ ].join('; ');
85
132
  // The file <script> (imports + shared values), lifted verbatim so previews
86
133
  // and examples see everything the entity's siblings do.
87
134
  const importBlock = scriptPrelude.trim() ? scriptPrelude.trim() + '\n' : '';
@@ -151,7 +198,7 @@ ${stateBroadcast}
151
198
  });
152
199
  </script>
153
200
 
154
- <div id="sdocs-preview" style="display: flow-root">
201
+ <div id="sdocs-preview" style="${stageStyle}">
155
202
  {#snippet SdocsPreview(args)}
156
203
  ${injectRootRef(snippetBody, componentName)}
157
204
  {/snippet}
package/dist/types.d.ts CHANGED
@@ -19,6 +19,46 @@ 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
+ /** Horizontal alignment of stage contents: 'left'|'center'|'right'|'justify'. Default: 'left' */
38
+ align?: string;
39
+ /** Vertical alignment of stage contents: 'top'|'middle'|'bottom'. Default: 'top' */
40
+ alignY?: string;
41
+ };
42
+ /** [LAYOUT] stages. Defaults: maxWidth '100%', padding '0px'. */
43
+ layout?: ContentSizing;
44
+ };
45
+ }
46
+ /** Content sizing knobs (any CSS length; padding takes CSS shorthand) */
47
+ export interface ContentSizing {
48
+ maxWidth?: string;
49
+ padding?: string;
50
+ }
51
+ /** Resolved stage layout applied inside a preview iframe */
52
+ export interface StageLayout {
53
+ maxWidth: string;
54
+ padding: string;
55
+ /** flex-direction + gap + align; set for preview/example stages only */
56
+ direction?: string;
57
+ gap?: string;
58
+ /** horizontal ('left'|'center'|'right'|'justify') — mapped by direction */
59
+ align?: string;
60
+ /** vertical ('top'|'middle'|'bottom') — mapped by direction */
61
+ alignY?: string;
22
62
  }
23
63
  /** Resolved config with all defaults applied */
24
64
  export interface ResolvedSdocsConfig {
@@ -32,6 +72,18 @@ export interface ResolvedSdocsConfig {
32
72
  order: Record<string, string[]>;
33
73
  open: string[];
34
74
  };
75
+ content: {
76
+ page: Required<ContentSizing> & {
77
+ toc: boolean;
78
+ };
79
+ docs: Required<ContentSizing> & {
80
+ direction: string;
81
+ gap: string;
82
+ align: string;
83
+ alignY: string;
84
+ };
85
+ layout: Required<ContentSizing>;
86
+ };
35
87
  }
36
88
  /** Entity metadata from a [DOCS]/[PAGE]/[LAYOUT] opener */
37
89
  export interface SdocMeta {
@@ -87,6 +139,8 @@ export interface ExtractedSnippet {
87
139
  highlightedHtml?: string;
88
140
  /** Preview URL for iframe (added by virtual module) */
89
141
  previewUrl?: string;
142
+ /** Resolved stage layout applied inside the iframe (config → entity → block) */
143
+ stage?: StageLayout;
90
144
  }
91
145
  /** One [preview] of a DOCS entity: a live showcase of one component */
92
146
  export interface PreviewEntry {
@@ -128,6 +182,8 @@ export interface DocEntry {
128
182
  content: ExtractedSnippet | null;
129
183
  /** Table of contents headings (pages only) */
130
184
  toc?: TocHeading[];
131
- /** Stage padding (layouts only) */
132
- padding?: string | null;
185
+ /** Resolved content-column max width (component and page kinds) */
186
+ maxWidth?: string;
187
+ /** Resolved table-of-contents visibility (pages only) */
188
+ showToc?: boolean;
133
189
  }
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,29 @@ 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/align 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
+ align: block.align ?? entity.sizing.align ?? config.content.docs.align,
317
+ alignY: block.alignY ?? entity.sizing.alignY ?? config.content.docs.alignY,
318
+ }
319
+ : {}),
320
+ });
321
+ entry.maxWidth = entity.sizing.maxWidth ?? kindDefaults.maxWidth;
322
+ if (entity.kind === 'PAGE') {
323
+ entry.showToc = entity.sizing.toc ?? config.content.page.toc;
324
+ }
302
325
  if (entity.kind === 'DOCS') {
303
326
  if (entity.description)
304
327
  entry.meta.description = entity.description;
@@ -318,6 +341,7 @@ export function sdocsPlugin(userConfig) {
318
341
  console.warn(`[sdocs] ${filePath}: component {${preview.componentName}} is not imported in the file's <script>`);
319
342
  }
320
343
  }
344
+ snippet.stage = stageOf(preview.sizing);
321
345
  snippet.highlightedHtml = await highlight(snippet.body);
322
346
  entry.previews.push({
323
347
  label: preview.label,
@@ -330,6 +354,10 @@ export function sdocsPlugin(userConfig) {
330
354
  });
331
355
  }
332
356
  entry.examples = snippets.filter((s) => s.role === 'example');
357
+ entity.examples.forEach((example, i) => {
358
+ if (entry.examples[i])
359
+ entry.examples[i].stage = stageOf(example.sizing);
360
+ });
333
361
  for (const example of entry.examples) {
334
362
  example.highlightedHtml = await highlight(example.body);
335
363
  }
@@ -337,12 +365,13 @@ export function sdocsPlugin(userConfig) {
337
365
  else if (entity.kind === 'PAGE') {
338
366
  const rendered = await renderPageMarkdown(entity.body);
339
367
  snippets[0].body = rendered.html;
368
+ snippets[0].stage = stageOf();
340
369
  entry.content = snippets[0];
341
370
  entry.toc = rendered.toc;
342
371
  }
343
372
  else {
373
+ snippets[0].stage = stageOf();
344
374
  entry.content = snippets[0];
345
- entry.padding = entity.padding;
346
375
  }
347
376
  docEntries.set(entityKey(filePath, entity.slug), entry);
348
377
  }
@@ -369,7 +398,8 @@ export function sdocsPlugin(userConfig) {
369
398
  examples: e.examples.map((s) => withUrl(e, s)),
370
399
  content: e.content ? withUrl(e, e.content) : null,
371
400
  toc: e.toc,
372
- padding: e.padding,
401
+ maxWidth: e.maxWidth,
402
+ showToc: e.showToc,
373
403
  }));
374
404
  // Extract named CSS stylesheet names (empty array if single string or null)
375
405
  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.54",
4
4
  "description": "A lightweight documentation tool for Svelte 5 components",
5
5
  "type": "module",
6
6
  "license": "MIT",