storybook-addon-md 0.1.0

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.
@@ -0,0 +1,188 @@
1
+ import { readFile, realpath, stat } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import glob from 'fast-glob';
4
+ import { isMap, parseDocument } from 'yaml';
5
+ import { unified } from 'unified';
6
+ import remarkParse from 'remark-parse';
7
+ import remarkGfm from 'remark-gfm';
8
+ import remarkStringify from 'remark-stringify';
9
+ import { visit } from 'unist-util-visit';
10
+ const markdown = unified().use(remarkParse).use(remarkGfm).use(remarkStringify);
11
+ const storyExtensions = ['tsx', 'ts', 'jsx', 'js'];
12
+ export const slash = (value) => value.split(path.sep).join('/');
13
+ export const fail = (source, message) => new Error(`[storybook-addon-md] ${source}: ${message}`);
14
+ export async function localFile(file, root, source, kind) {
15
+ try {
16
+ const actual = await realpath(file);
17
+ const relative = path.relative(await realpath(root), actual);
18
+ if (relative.startsWith(`..${path.sep}`) || relative === '..' || path.isAbsolute(relative)) {
19
+ throw fail(source, `${kind} must be inside root: ${file}`);
20
+ }
21
+ if (!(await stat(actual)).isFile())
22
+ throw new Error('not a file');
23
+ return file;
24
+ }
25
+ catch (error) {
26
+ if (error instanceof Error && error.message.startsWith('[storybook-addon-md]'))
27
+ throw error;
28
+ throw Object.assign(fail(source, `missing ${kind}: ${file}`), { dependency: file });
29
+ }
30
+ }
31
+ export function parseMarkdown(text, source) {
32
+ const normalized = text.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n');
33
+ if (!normalized.startsWith('---\n'))
34
+ return { body: normalized, metadata: {} };
35
+ const end = /^---\s*$/gm;
36
+ end.lastIndex = 4;
37
+ const closing = end.exec(normalized);
38
+ if (!closing)
39
+ throw fail(source, 'frontmatter must end with ---');
40
+ const yaml = parseDocument(normalized.slice(4, closing.index), { uniqueKeys: true });
41
+ if (yaml.errors.length)
42
+ throw fail(source, `invalid frontmatter: ${yaml.errors[0].message}`);
43
+ if (yaml.warnings.length)
44
+ throw fail(source, `invalid frontmatter: ${yaml.warnings[0].message}`);
45
+ if (yaml.contents && !isMap(yaml.contents))
46
+ throw fail(source, 'frontmatter must be a mapping');
47
+ let metadata;
48
+ try {
49
+ metadata = yaml.toJS({ maxAliasCount: 50 }) ?? {};
50
+ JSON.stringify(metadata, (_, value) => {
51
+ if (typeof value === 'number' && !Number.isFinite(value))
52
+ throw new Error('metadata numbers must be finite');
53
+ return value;
54
+ });
55
+ }
56
+ catch (error) {
57
+ throw fail(source, `invalid frontmatter: ${error instanceof Error ? error.message : String(error)}`);
58
+ }
59
+ if (typeof metadata !== 'object' || Array.isArray(metadata)) {
60
+ throw fail(source, 'frontmatter must be a mapping');
61
+ }
62
+ for (const key of Object.keys(metadata)) {
63
+ if (!/^[a-z][a-z0-9_-]*$/.test(key))
64
+ throw fail(source, `frontmatter keys must be lowercase: ${key}`);
65
+ }
66
+ if ('title' in metadata && (typeof metadata.title !== 'string' || !metadata.title.trim())) {
67
+ throw fail(source, 'title must be a non-empty string');
68
+ }
69
+ if ('status' in metadata && (typeof metadata.status !== 'string' || !metadata.status.trim())) {
70
+ throw fail(source, 'status must be a non-empty string');
71
+ }
72
+ if ('tags' in metadata &&
73
+ (!Array.isArray(metadata.tags) ||
74
+ metadata.tags.some((tag) => typeof tag !== 'string' || !tag.trim()))) {
75
+ throw fail(source, 'tags must be an array of non-empty strings');
76
+ }
77
+ if ('stories' in metadata) {
78
+ const references = Array.isArray(metadata.stories) ? metadata.stories : [metadata.stories];
79
+ if (!references.length || references.some((ref) => typeof ref !== 'string' || !ref.trim())) {
80
+ throw fail(source, 'stories must be a relative path or a non-empty array of relative paths');
81
+ }
82
+ }
83
+ return { body: normalized.slice(closing.index + closing[0].length).replace(/^\n/, ''), metadata };
84
+ }
85
+ async function associations(file, metadata, root) {
86
+ if ('stories' in metadata) {
87
+ const references = Array.isArray(metadata.stories) ? metadata.stories : [metadata.stories];
88
+ return Promise.all([...new Set(references)].map(async (ref) => {
89
+ if (path.isAbsolute(ref) || !/\.stories\.[jt]sx?$/.test(ref)) {
90
+ throw fail(file, `stories must reference a relative .stories.js, .jsx, .ts, or .tsx file: ${ref}`);
91
+ }
92
+ return localFile(path.resolve(path.dirname(file), ref), root, file, 'story reference');
93
+ }));
94
+ }
95
+ if (!file.endsWith('.metadata.md'))
96
+ return [];
97
+ const base = file.slice(0, -'.metadata.md'.length);
98
+ const matches = [];
99
+ for (const extension of storyExtensions) {
100
+ const candidate = `${base}.stories.${extension}`;
101
+ if (await stat(candidate).then((value) => value.isFile(), () => false))
102
+ matches.push(candidate);
103
+ }
104
+ if (matches.length !== 1) {
105
+ throw Object.assign(fail(file, matches.length
106
+ ? 'ambiguous sibling stories; set stories explicitly'
107
+ : 'missing sibling story for .metadata.md; set stories explicitly or rename the Markdown file'), { dependencies: storyExtensions.map((extension) => `${base}.stories.${extension}`) });
108
+ }
109
+ return [await localFile(matches[0], root, file, 'story reference')];
110
+ }
111
+ export async function resolveAssets(body, file, root) {
112
+ const tree = markdown.parse(body);
113
+ const nodes = [];
114
+ visit(tree, (node) => {
115
+ if (node.type === 'image' || node.type === 'link' || node.type === 'definition')
116
+ nodes.push(node);
117
+ });
118
+ const assets = [];
119
+ for (const node of nodes) {
120
+ const url = node.url;
121
+ if (!url || /^(?:[a-z][a-z\d+.-]*:|\/|#|\?)/i.test(url))
122
+ continue;
123
+ const [, pathname, suffix = ''] = /^([^?#]*)(.*)$/.exec(url);
124
+ let decoded;
125
+ try {
126
+ decoded = decodeURIComponent(pathname);
127
+ }
128
+ catch {
129
+ throw fail(file, `invalid local URL: ${url}`);
130
+ }
131
+ const asset = await localFile(path.resolve(path.dirname(file), decoded), root, file, 'local asset');
132
+ const token = `SBMDASSET${assets.length}END`;
133
+ if (body.includes(token))
134
+ throw fail(file, `reserved asset token in content: ${token}`);
135
+ assets.push({ file: asset, suffix, token });
136
+ node.url = token;
137
+ }
138
+ return { markdown: markdown.stringify(tree), assets };
139
+ }
140
+ export async function discover({ root, patterns, exclude = [], output }) {
141
+ if (!Array.isArray(patterns) ||
142
+ !patterns.length ||
143
+ patterns.some((item) => typeof item !== 'string' ||
144
+ !item ||
145
+ path.isAbsolute(item) ||
146
+ item.split('/').includes('..'))) {
147
+ throw fail(root, 'patterns must be a non-empty array of globs relative to root');
148
+ }
149
+ if (!Array.isArray(exclude) ||
150
+ exclude.some((item) => typeof item !== 'string' ||
151
+ !item ||
152
+ path.isAbsolute(item) ||
153
+ item.split('/').includes('..'))) {
154
+ throw fail(root, 'exclude must be an array of globs relative to root');
155
+ }
156
+ const files = await glob(patterns, {
157
+ cwd: root,
158
+ absolute: true,
159
+ onlyFiles: true,
160
+ unique: true,
161
+ followSymbolicLinks: false,
162
+ ignore: [
163
+ ...exclude,
164
+ '**/node_modules/**',
165
+ '**/.git/**',
166
+ '**/storybook-static/**',
167
+ `${slash(path.relative(root, output))}/**`,
168
+ ],
169
+ });
170
+ return Promise.all(files
171
+ .sort()
172
+ .filter((file) => file.endsWith('.md'))
173
+ .map(async (file) => {
174
+ await localFile(file, root, file, 'Markdown file');
175
+ const { body, metadata } = parseMarkdown(await readFile(file, 'utf8'), file);
176
+ const stories = await associations(file, metadata, root);
177
+ const content = await resolveAssets(body, file, root);
178
+ return {
179
+ file,
180
+ source: slash(path.relative(root, file)),
181
+ title: metadata.title ??
182
+ `Documentation/${slash(path.relative(root, file)).replace(/\.md$/, '')}`,
183
+ metadata,
184
+ stories,
185
+ ...content,
186
+ };
187
+ }));
188
+ }
@@ -0,0 +1,16 @@
1
+ import type { ContentOptions, DiscoveredDocument } from './content.js';
2
+ export declare function writeChanged(file: string, content: string): Promise<void>;
3
+ export declare function documentModule(document: DiscoveredDocument, output: string): string;
4
+ export declare function generate(options: ContentOptions): Promise<{
5
+ markdown: string;
6
+ assets: {
7
+ file: string;
8
+ suffix: string;
9
+ token: string;
10
+ }[];
11
+ file: string;
12
+ source: string;
13
+ title: string;
14
+ metadata: import("./content.js").Frontmatter;
15
+ stories: string[];
16
+ }[]>;
@@ -0,0 +1,98 @@
1
+ import { mkdir, readFile, readdir, unlink, writeFile } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ import { discover, fail, localFile, slash } from './content.js';
5
+ const attribute = (value) => value.replace(/[&"<>\r\n]/g, (character) => `&#${character.charCodeAt(0)};`);
6
+ const id = (value) => createHash('sha256').update(value).digest('hex').slice(0, 16);
7
+ const specifier = (from, to) => {
8
+ const relative = slash(path.relative(from, to));
9
+ return relative.startsWith('.') ? relative : `./${relative}`;
10
+ };
11
+ const assetPath = (file, output) => {
12
+ if (!/[?#%]/.test(file))
13
+ return file;
14
+ const extension = path.extname(file);
15
+ return path.join(output, `asset-${id(file)}${/^\.[a-z0-9]+$/i.test(extension) ? extension : '.bin'}`);
16
+ };
17
+ export async function writeChanged(file, content) {
18
+ if ((await readFile(file, 'utf8').catch(() => null)) !== content)
19
+ await writeFile(file, content);
20
+ }
21
+ export function documentModule(document, output) {
22
+ const imports = document.assets.map((asset, index) => `import asset${index} from ${JSON.stringify(`${specifier(output, assetPath(asset.file, output))}?url&no-inline`)};`);
23
+ const expression = document.assets.reduce((value, asset, index) => `${value}.split(${JSON.stringify(asset.token)}).join(asset${index}.replace('?no-inline', '') + ${JSON.stringify(asset.suffix)})`, JSON.stringify(document.markdown));
24
+ return `${imports.join('\n')}\nexport default { source: ${JSON.stringify(document.source)}, metadata: JSON.parse(${JSON.stringify(JSON.stringify(document.metadata))}), markdown: ${expression} };\n`;
25
+ }
26
+ export async function generate(options) {
27
+ const { output, root, presentation, stylesheet } = options;
28
+ const documents = await discover(options);
29
+ if (presentation)
30
+ await localFile(presentation, root, presentation, 'presentation module');
31
+ if (stylesheet)
32
+ await localFile(stylesheet, root, stylesheet, 'stylesheet');
33
+ const files = new Map();
34
+ const assets = new Map();
35
+ const groups = new Map();
36
+ const titles = new Set();
37
+ for (const document of documents) {
38
+ for (const asset of document.assets) {
39
+ const target = assetPath(asset.file, output);
40
+ if (target !== asset.file)
41
+ assets.set(path.basename(target), await readFile(asset.file));
42
+ }
43
+ const moduleName = `content-${id(document.source)}.js`;
44
+ files.set(moduleName, documentModule(document, output));
45
+ if (!document.stories.length) {
46
+ const sidebarId = document.title
47
+ .toLowerCase()
48
+ .replace(/[^a-z0-9]+/g, '-')
49
+ .replace(/^-|-$/g, '');
50
+ if (!sidebarId || titles.has(sidebarId))
51
+ throw fail(document.file, `duplicate or empty sidebar title: ${document.title}`);
52
+ titles.add(sidebarId);
53
+ groups.set(`doc:${document.source}`, { title: document.title, documents: [moduleName] });
54
+ }
55
+ for (const story of document.stories) {
56
+ const key = `story:${slash(path.relative(root, story))}`;
57
+ if (!groups.has(key))
58
+ groups.set(key, { story, documents: [] });
59
+ groups.get(key).documents.push(moduleName);
60
+ }
61
+ }
62
+ for (const [key, group] of groups) {
63
+ const imports = [
64
+ "import { Meta } from '@storybook/addon-docs/blocks';",
65
+ "import { Documentation } from 'storybook-addon-md/runtime';",
66
+ "import './status.js';",
67
+ "import 'storybook-addon-md/styles.css';",
68
+ ...group.documents.map((name, index) => `import document${index} from './${name}';`),
69
+ ];
70
+ if (group.story)
71
+ imports.push(`import * as ComponentStories from ${JSON.stringify(specifier(output, group.story))};`);
72
+ if (presentation)
73
+ imports.push(`import * as presentation from ${JSON.stringify(specifier(output, presentation))};`);
74
+ if (stylesheet)
75
+ imports.push(`import ${JSON.stringify(specifier(output, stylesheet))};`);
76
+ const meta = group.story
77
+ ? '<Meta of={ComponentStories} name="Markdown" />'
78
+ : `<Meta title="${attribute(group.title)}" />`;
79
+ files.set(`page-${id(key)}.mdx`, `${imports.join('\n')}\n\n${meta}\n\n<Documentation documents={[${group.documents.map((_, index) => `document${index}`).join(', ')}]} attached={${Boolean(group.story)}} title={${JSON.stringify(group.title ?? '')}}${presentation ? ' presentation={presentation}' : ''} />\n`);
80
+ }
81
+ files.set('status.js', 'export {};\n');
82
+ await mkdir(output, { recursive: true });
83
+ for (const [name, content] of assets) {
84
+ const file = path.join(output, name);
85
+ const previous = await readFile(file).catch(() => null);
86
+ if (!previous?.equals(content))
87
+ await writeFile(file, content);
88
+ }
89
+ for (const [name, content] of files)
90
+ await writeChanged(path.join(output, name), content);
91
+ for (const name of await readdir(output)) {
92
+ if (/^(?:page-[a-f0-9]+\.mdx|content-[a-f0-9]+\.js|asset-[a-f0-9]+\.[a-z0-9]+)$/.test(name) &&
93
+ !files.has(name) &&
94
+ !assets.has(name))
95
+ await unlink(path.join(output, name));
96
+ }
97
+ return documents;
98
+ }
@@ -0,0 +1,8 @@
1
+ export interface MarkdownOptions {
2
+ generatedDir?: string;
3
+ patterns: string[];
4
+ exclude?: string[];
5
+ stylesheet?: string;
6
+ root?: string;
7
+ presentation?: string;
8
+ }
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ import type { UserConfig } from 'vite';
2
+ import type { MarkdownOptions } from './index.js';
3
+ import type { ContentOptions } from './content.js';
4
+ type PresetOptions = MarkdownOptions & {
5
+ configDir: string;
6
+ };
7
+ type StoryEntry = string | {
8
+ directory: string;
9
+ files?: string;
10
+ titlePrefix?: string;
11
+ };
12
+ export declare function stories(existing: StoryEntry[] | undefined, options: PresetOptions): Promise<StoryEntry[]>;
13
+ export declare function watchDocumentation(config: ContentOptions, { onError, onUpdate, }?: {
14
+ onError?: (error: Error) => void;
15
+ onUpdate?: () => void;
16
+ }): {
17
+ ready: Promise<void>;
18
+ close(): Promise<void>;
19
+ };
20
+ export declare function viteFinal(config: UserConfig, options: PresetOptions): Promise<UserConfig>;
21
+ export declare function webpackFinal(): void;
22
+ export {};
package/dist/preset.js ADDED
@@ -0,0 +1,137 @@
1
+ import path from 'node:path';
2
+ import { createHash } from 'node:crypto';
3
+ import { watch } from 'chokidar';
4
+ import glob from 'fast-glob';
5
+ import picomatch from 'picomatch';
6
+ import { generate, writeChanged } from './generator.js';
7
+ import { fail, slash } from './content.js';
8
+ const sessions = new Map();
9
+ function settings(options) {
10
+ const configDir = path.resolve(options.configDir);
11
+ const root = path.resolve(configDir, options.root ?? '..');
12
+ const generatedDir = options.generatedDir ?? 'storybook-markdown-generated';
13
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(generatedDir) ||
14
+ ['node_modules', 'storybook-static'].includes(generatedDir)) {
15
+ throw fail(configDir, 'generatedDir must be a visible folder name containing only letters, digits, hyphens or underscores');
16
+ }
17
+ return {
18
+ root,
19
+ output: path.join(process.cwd(), generatedDir, createHash('sha256').update(configDir).digest('hex').slice(0, 12)),
20
+ patterns: options.patterns,
21
+ exclude: options.exclude,
22
+ stylesheet: options.stylesheet ? path.resolve(root, options.stylesheet) : undefined,
23
+ presentation: options.presentation ? path.resolve(root, options.presentation) : undefined,
24
+ };
25
+ }
26
+ export async function stories(existing = [], options) {
27
+ const key = path.resolve(options.configDir);
28
+ if (!sessions.has(key)) {
29
+ const config = settings(options);
30
+ sessions.set(key, { config, ready: generate(config) });
31
+ }
32
+ const session = sessions.get(key);
33
+ await session.ready;
34
+ return [...existing, { directory: session.config.output, files: '*.mdx' }];
35
+ }
36
+ export function watchDocumentation(config, { onError = () => { }, onUpdate = () => { }, } = {}) {
37
+ const matchers = glob
38
+ .generateTasks(config.patterns, { ignore: config.exclude ?? [] })
39
+ .map((task) => ({
40
+ include: picomatch(task.positive),
41
+ exclude: picomatch(task.negative),
42
+ }));
43
+ const dependencies = new Set();
44
+ const configuredDependencies = [config.presentation, config.stylesheet].filter((file) => Boolean(file));
45
+ let timer;
46
+ let closed = false;
47
+ let queue = Promise.resolve();
48
+ const refresh = () => {
49
+ queue = queue.then(async () => {
50
+ if (closed)
51
+ return;
52
+ try {
53
+ const documents = await generate(config);
54
+ dependencies.clear();
55
+ for (const file of configuredDependencies)
56
+ dependencies.add(file);
57
+ for (const document of documents) {
58
+ for (const file of document.stories)
59
+ dependencies.add(file);
60
+ for (const asset of document.assets)
61
+ dependencies.add(asset.file);
62
+ if (document.file.endsWith('.metadata.md') && !('stories' in document.metadata)) {
63
+ for (const extension of ['js', 'jsx', 'ts', 'tsx']) {
64
+ dependencies.add(document.file.replace(/\.metadata\.md$/, `.stories.${extension}`));
65
+ }
66
+ }
67
+ }
68
+ onUpdate();
69
+ }
70
+ catch (caught) {
71
+ const error = caught instanceof Error ? caught : new Error(String(caught));
72
+ if ('dependencies' in error && Array.isArray(error.dependencies)) {
73
+ for (const file of error.dependencies)
74
+ if (typeof file === 'string')
75
+ dependencies.add(file);
76
+ }
77
+ if ('dependency' in error && typeof error.dependency === 'string')
78
+ dependencies.add(error.dependency);
79
+ await writeChanged(path.join(config.output, 'status.js'), `throw new Error(${JSON.stringify(error.message)});\n`);
80
+ onError(error);
81
+ }
82
+ });
83
+ return queue;
84
+ };
85
+ const watcher = watch(config.root, {
86
+ ignoreInitial: true,
87
+ ignored: (file) => file === config.output ||
88
+ /(?:^|[/\\])(?:node_modules|\.git|storybook-static)(?:[/\\]|$)/.test(file),
89
+ awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 20 },
90
+ });
91
+ watcher.on('all', (event, file) => {
92
+ if (event === 'addDir' || event === 'unlinkDir')
93
+ return;
94
+ const relative = slash(path.relative(config.root, file));
95
+ const markdown = file.endsWith('.md') &&
96
+ matchers.some(({ include, exclude }) => include(relative) && !exclude(relative));
97
+ if (!markdown && !dependencies.has(file) && !configuredDependencies.includes(file))
98
+ return;
99
+ clearTimeout(timer);
100
+ timer = setTimeout(refresh, 80);
101
+ });
102
+ watcher.on('error', (error) => onError(error instanceof Error ? error : new Error(String(error))));
103
+ const ready = new Promise((resolve) => watcher.once('ready', () => refresh().then(resolve)));
104
+ return {
105
+ ready,
106
+ async close() {
107
+ closed = true;
108
+ clearTimeout(timer);
109
+ await watcher.close();
110
+ await queue;
111
+ },
112
+ };
113
+ }
114
+ export async function viteFinal(config, options) {
115
+ const session = sessions.get(path.resolve(options.configDir))?.config ?? settings(options);
116
+ return {
117
+ ...config,
118
+ plugins: [
119
+ ...(config.plugins ?? []),
120
+ {
121
+ name: 'storybook-addon-md',
122
+ configureServer(server) {
123
+ const watcher = watchDocumentation(session, {
124
+ onError(error) {
125
+ server.config.logger.error(error.message);
126
+ server.ws.send({ type: 'error', err: { message: error.message, stack: '' } });
127
+ },
128
+ });
129
+ server.httpServer?.once('close', () => void watcher.close());
130
+ },
131
+ },
132
+ ],
133
+ };
134
+ }
135
+ export function webpackFinal() {
136
+ throw fail('builder', 'only @storybook/react-vite 10.6.0 with Vite 7 is supported');
137
+ }
@@ -0,0 +1,10 @@
1
+ import type { MarkdownDocument, LayoutProps, Presentation } from './types.js';
2
+ export type { MarkdownDocument, LayoutProps, Presentation } from './types.js';
3
+ export declare function DefaultMarkdownRenderer({ markdown }: MarkdownDocument): import("react").JSX.Element;
4
+ export declare function DefaultLayout({ documents, title, attached, children, examples }: LayoutProps): import("react").JSX.Element;
5
+ export declare function Documentation({ documents, title, attached, presentation, }: {
6
+ documents: MarkdownDocument[];
7
+ title: string;
8
+ attached?: boolean;
9
+ presentation?: Presentation;
10
+ }): import("react").JSX.Element;
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useTheme } from 'storybook/theming';
3
+ import { Markdown, Title, Primary, Controls, Stories, CodeOrSourceMdx, HeadersMdx, } from '@storybook/addon-docs/blocks';
4
+ export function DefaultMarkdownRenderer({ markdown }) {
5
+ return (_jsx(Markdown, { options: {
6
+ disableParsingRawHTML: true,
7
+ overrides: { code: CodeOrSourceMdx, ...HeadersMdx, a: 'a' },
8
+ }, children: markdown }));
9
+ }
10
+ export function DefaultLayout({ documents, title, attached, children, examples }) {
11
+ const tags = [
12
+ ...new Set(documents.flatMap(({ metadata }) => Array.isArray(metadata.tags) ? metadata.tags : [])),
13
+ ];
14
+ const statuses = [
15
+ ...new Set(documents.flatMap(({ metadata }) => typeof metadata.status === 'string' ? [metadata.status] : [])),
16
+ ];
17
+ return (_jsxs(_Fragment, { children: [_jsx("div", { className: "storybook-addon-md-title", children: attached ? _jsx(Title, {}) : _jsx("h1", { children: title.split('/').at(-1) }) }), (tags.length > 0 || statuses.length > 0) && (_jsxs("ul", { className: "storybook-addon-md-tags", "aria-label": "Documentation tags", children: [tags.map((tag) => (_jsx("li", { className: "storybook-addon-md-tag", children: tag }, `tag:${tag}`))), statuses.map((status) => (_jsx("li", { className: "storybook-addon-md-tag", "data-status": status, children: status }, `status:${status}`)))] })), children, examples] }));
18
+ }
19
+ export function Documentation({ documents, title, attached = false, presentation = {}, }) {
20
+ const theme = useTheme();
21
+ const defaults = {
22
+ '--sbmd-native-color': theme.color.defaultText,
23
+ '--sbmd-native-link-color': theme.color.secondary,
24
+ '--sbmd-native-font-family': theme.typography.fonts.base,
25
+ '--sbmd-native-border-color': theme.appBorderColor,
26
+ '--sbmd-native-code-background': theme.background.content,
27
+ };
28
+ const Layout = presentation.Layout ?? DefaultLayout;
29
+ const Renderer = presentation.MarkdownRenderer ?? DefaultMarkdownRenderer;
30
+ const examples = attached ? (_jsxs(_Fragment, { children: [_jsx(Primary, {}), _jsx(Controls, {}), _jsx(Stories, { includePrimary: false })] })) : null;
31
+ return (_jsx("div", { className: "storybook-addon-md-page", style: defaults, children: _jsx(Layout, { documents: documents, title: title, attached: attached, examples: examples, children: documents.map((document) => (_jsx("div", { className: "storybook-addon-md", children: _jsx(Renderer, { ...document }) }, document.source))) }) }));
32
+ }