void-plugin-hono-jsx 0.1.0-rc.1 → 0.2.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +29 -23
  3. package/THIRD_PARTY_NOTICES.md +1 -1
  4. package/dist/plugins/hono-islands.d.ts.map +1 -1
  5. package/dist/plugins/hono-islands.js +11 -3
  6. package/dist/plugins/hono-islands.js.map +1 -1
  7. package/dist/plugins/hono-markdown.d.ts +3 -1
  8. package/dist/plugins/hono-markdown.d.ts.map +1 -1
  9. package/dist/plugins/hono-markdown.js +13 -6
  10. package/dist/plugins/hono-markdown.js.map +1 -1
  11. package/dist/plugins/markdown-compiler.d.ts +65 -2
  12. package/dist/plugins/markdown-compiler.d.ts.map +1 -1
  13. package/dist/plugins/markdown-compiler.js +54 -40
  14. package/dist/plugins/markdown-compiler.js.map +1 -1
  15. package/dist/plugins/markdown-copy.d.ts +3 -0
  16. package/dist/plugins/markdown-copy.d.ts.map +1 -0
  17. package/dist/plugins/markdown-copy.js +31 -0
  18. package/dist/plugins/markdown-copy.js.map +1 -0
  19. package/dist/plugins/markdown-features.d.ts +12 -0
  20. package/dist/plugins/markdown-features.d.ts.map +1 -0
  21. package/dist/plugins/markdown-features.js +56 -0
  22. package/dist/plugins/markdown-features.js.map +1 -0
  23. package/dist/theme/code.css +202 -0
  24. package/dist/theme/containers.css +125 -0
  25. package/dist/theme/content.css +4 -0
  26. package/dist/theme/index.css +7 -0
  27. package/dist/theme/prose.css +294 -0
  28. package/dist/theme/reset.css +100 -0
  29. package/package.json +20 -2
  30. package/src/plugins/hono-islands.ts +11 -3
  31. package/src/plugins/hono-markdown.ts +15 -8
  32. package/src/plugins/markdown-compiler.ts +46 -30
  33. package/src/plugins/markdown-copy.ts +30 -0
  34. package/src/plugins/markdown-features.ts +57 -0
  35. package/src/theme/code.css +202 -0
  36. package/src/theme/containers.css +125 -0
  37. package/src/theme/content.css +4 -0
  38. package/src/theme/index.css +7 -0
  39. package/src/theme/prose.css +294 -0
  40. package/src/theme/reset.css +100 -0
@@ -1,4 +1,5 @@
1
1
  import MarkdownExit from 'markdown-exit';
2
+ import { configureMarkdownFeatures, type HonoMarkdownOptions } from './markdown-features.ts';
2
3
  import { parseSync } from 'vite';
3
4
  import { parseDocument } from 'yaml';
4
5
  import { findIslandImports, editIslandSource } from 'void/pages-islands-plugin';
@@ -18,38 +19,53 @@ export function extractMarkdown(source: string) {
18
19
  return { body, script, imports, graphSource, clientCode };
19
20
  }
20
21
 
21
- export function compileMarkdown(source: string) {
22
- const extracted = extractMarkdown(source);
23
- const match = extracted.body.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
24
- let frontmatter: Record<string, unknown> = {};
25
- if (match) {
26
- const yaml = parseDocument(match[1], { uniqueKeys: true });
27
- if (yaml.errors.length) throw new Error('Hono Markdown: invalid frontmatter: ' + yaml.errors.map(error => error.message).join('; '));
28
- const value: unknown = yaml.toJS({ maxAliasCount: 50 });
29
- if (value !== null && (typeof value !== 'object' || Array.isArray(value))) throw new Error('Hono Markdown: frontmatter must be a mapping.');
30
- frontmatter = (value ?? {}) as Record<string, unknown>;
31
- try { JSON.stringify(frontmatter); } catch { throw new Error('Hono Markdown: frontmatter must be JSON serializable.'); }
32
- }
33
- const content = match ? extracted.body.slice(match[0].length) : extracted.body;
22
+ export function createMarkdownCompiler(options: HonoMarkdownOptions = {}) {
34
23
  const compiler = new MarkdownExit({ html: true, linkify: true });
35
- const tokens = compiler.parse(content, {});
36
- const headings: Heading[] = [];
37
- const slugs = new Map<string, number>();
38
- const authoredHtml = tokens.flatMap(token => token.type === 'html_block' || token.type === 'html_inline' ? [token.content] : (token.children ?? []).filter(child => child.type === 'html_inline').map(child => child.content)).join('\n');
39
- const usedIds = new Set([...authoredHtml.matchAll(/\bid\s*=\s*(?:"([^"]*)"|'([^']*)')/g)].map(match => match[1] ?? match[2]));
40
- for (let index = 0; index < tokens.length; index++) {
41
- if (tokens[index].type !== 'heading_open') continue;
42
- const inline = tokens[index + 1];
43
- const text = inline.children?.map(token => token.type === 'text' || token.type === 'code_inline' ? token.content : token.type === 'image' ? token.content : '').join('') ?? inline.content;
44
- const base = text.toLowerCase().replace(/[^\p{L}\p{N} _-]/gu, '').trim().replace(/\s+/g, '-') || 'heading';
45
- let count = slugs.get(base) ?? 0;
46
- let slug = count ? `${base}-${count}` : base;
47
- while (usedIds.has(slug)) { count++; slug = `${base}-${count}`; }
48
- slugs.set(base, count + 1); usedIds.add(slug);
49
- tokens[index].attrSet('id', slug);
50
- headings.push({ depth: Number(tokens[index].tag.slice(1)), slug, text });
24
+ configureMarkdownFeatures(compiler, options);
25
+ function parse(source: string) {
26
+ const extracted = extractMarkdown(source);
27
+ const match = extracted.body.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
28
+ let frontmatter: Record<string, unknown> = {};
29
+ if (match) {
30
+ const yaml = parseDocument(match[1], { uniqueKeys: true });
31
+ if (yaml.errors.length) throw new Error('Hono Markdown: invalid frontmatter: ' + yaml.errors.map(error => error.message).join('; '));
32
+ const value: unknown = yaml.toJS({ maxAliasCount: 50 });
33
+ if (value !== null && (typeof value !== 'object' || Array.isArray(value))) throw new Error('Hono Markdown: frontmatter must be a mapping.');
34
+ frontmatter = (value ?? {}) as Record<string, unknown>;
35
+ try { JSON.stringify(frontmatter); } catch { throw new Error('Hono Markdown: frontmatter must be JSON serializable.'); }
36
+ }
37
+ const content = match ? extracted.body.slice(match[0].length) : extracted.body;
38
+ const env: Record<string, unknown> = {};
39
+ const tokens = compiler.parse(content, env);
40
+ const headings: Heading[] = [];
41
+ const slugs = new Map<string, number>();
42
+ const authoredHtml = tokens.flatMap(token => token.type === 'html_block' || token.type === 'html_inline' ? [token.content] : (token.children ?? []).filter(child => child.type === 'html_inline').map(child => child.content)).join('\n');
43
+ const usedIds = new Set([...authoredHtml.matchAll(/\bid\s*=\s*(?:"([^"]*)"|'([^']*)')/g)].map(match => match[1] ?? match[2]));
44
+ for (let index = 0; index < tokens.length; index++) {
45
+ if (tokens[index].type !== 'heading_open') continue;
46
+ const inline = tokens[index + 1];
47
+ const text = inline.children?.map(token => token.type === 'text' || token.type === 'code_inline' ? token.content : token.type === 'image' ? token.content : '').join('') ?? inline.content;
48
+ const base = text.toLowerCase().replace(/[^\p{L}\p{N} _-]/gu, '').trim().replace(/\s+/g, '-') || 'heading';
49
+ let count = slugs.get(base) ?? 0;
50
+ let slug = count ? `${base}-${count}` : base;
51
+ while (usedIds.has(slug)) { count++; slug = `${base}-${count}`; }
52
+ slugs.set(base, count + 1); usedIds.add(slug);
53
+ tokens[index].attrSet('id', slug);
54
+ headings.push({ depth: Number(tokens[index].tag.slice(1)), slug, text });
55
+ }
56
+ return { ...extracted, source, frontmatter, headings, tokens, env, hasCode: tokens.some(token => token.type === 'fence') };
51
57
  }
52
- return { ...extracted, frontmatter, headings, html: compiler.renderer.render(tokens, compiler.options, {}) };
58
+
59
+ async function render(parsed: ReturnType<typeof parse>) {
60
+ const { tokens, env, ...result } = parsed;
61
+ return { ...result, html: await compiler.renderer.renderAsync(tokens, compiler.options, env) };
62
+ }
63
+ return { parse, render };
64
+ }
65
+
66
+ export async function compileMarkdown(source: string, options: HonoMarkdownOptions = {}) {
67
+ const compiler = createMarkdownCompiler(options);
68
+ return compiler.render(compiler.parse(source));
53
69
  }
54
70
 
55
71
  // Component tag splitting follows @void/md 0.27.0's emit-shared convention.
@@ -0,0 +1,30 @@
1
+ // Imported as a module only by Markdown pages with fenced code. No Hono runtime
2
+ // or island scheduler is required for code-copy-only documents.
3
+ export const markdownCopyId = 'virtual:void-hono-markdown-copy';
4
+ export const markdownCopyCode = `
5
+ const timers = new WeakMap();
6
+ document.addEventListener('click', async event => {
7
+ const button = event.target instanceof Element && event.target.closest("div[class*='language-'] > button.copy");
8
+ if (!button || button.disabled) return;
9
+ const pre = button.parentElement?.querySelector('pre');
10
+ if (!pre) return;
11
+ clearTimeout(timers.get(button));
12
+ button.classList.remove('copied', 'copy-error');
13
+ const reset = () => { button.classList.remove('copied', 'copy-error'); button.title = 'Copy Code'; button.setAttribute('aria-label', 'Copy Code'); };
14
+ try {
15
+ if (!navigator.clipboard?.writeText) throw new Error('Clipboard unavailable');
16
+ const lines = Array.from(pre.querySelectorAll('.line:not(.diff.remove)'), line => line.textContent);
17
+ if (lines.at(-1) === '') lines.pop();
18
+ button.disabled = true;
19
+ await navigator.clipboard.writeText(lines.join('\\n'));
20
+ button.classList.add('copied');
21
+ button.title = 'Copied'; button.setAttribute('aria-label', 'Copied');
22
+ } catch {
23
+ button.classList.add('copy-error');
24
+ button.title = 'Copy failed'; button.setAttribute('aria-label', 'Copy failed');
25
+ } finally {
26
+ button.disabled = false;
27
+ timers.set(button, setTimeout(reset, 2000));
28
+ }
29
+ });
30
+ `;
@@ -0,0 +1,57 @@
1
+ import { createRequire } from 'node:module';
2
+ import type MarkdownExit from 'markdown-exit';
3
+ import type { PluginWithParams } from 'markdown-exit';
4
+
5
+ /** Matches @void/md's public Markdown options; languages load on demand. */
6
+ export type HonoMarkdownOptions = {
7
+ shiki?: { themes?: { light: string; dark: string } };
8
+ };
9
+
10
+ export function configureMarkdownFeatures(md: MarkdownExit, options: HonoMarkdownOptions) {
11
+ const container = createRequire(import.meta.url)('markdown-it-container') as PluginWithParams;
12
+ for (const [kind, title] of [['tip', 'TIP'], ['info', 'INFO'], ['warning', 'WARNING'], ['danger', 'DANGER'], ['details', 'Details']]) {
13
+ md.use(container, kind, { render(tokens: Parameters<NonNullable<typeof md.renderer.rules.fence>>[0], index: number, _options: unknown, env: Parameters<NonNullable<typeof md.renderer.rules.fence>>[3]) {
14
+ if (tokens[index].nesting !== 1) return kind === 'details' ? '</details>\n' : '</div>\n';
15
+ const authored = tokens[index].info.trim().slice(kind.length).trim();
16
+ const rendered = md.renderInline(authored || title, { references: env.references });
17
+ if (kind === 'details') return `<details class="${kind} custom-block"><summary>${rendered}</summary>\n`;
18
+ return `<div class="${kind} custom-block"><p class="custom-block-title${authored ? '' : ' custom-block-title-default'}">${rendered}</p>\n`;
19
+ } });
20
+ }
21
+
22
+ // One highlighter per plugin, not a global first-options-wins singleton.
23
+ const themes = { light: options.shiki?.themes?.light ?? 'github-light', dark: options.shiki?.themes?.dark ?? 'github-dark' };
24
+ const initialize = async () => {
25
+ const [shiki, { createJavaScriptRegexEngine }, transformers] = await Promise.all([
26
+ import('shiki'), import('shiki/engine/javascript'), import('@shikijs/transformers'),
27
+ ]);
28
+ for (const theme of Object.values(themes)) if (!Object.hasOwn(shiki.bundledThemes, theme)) throw new Error(`Hono Markdown: unknown Shiki theme ${JSON.stringify(theme)}.`);
29
+ const selected = themes as { light: keyof typeof shiki.bundledThemes; dark: keyof typeof shiki.bundledThemes };
30
+ const highlighter = await shiki.createHighlighter({ engine: createJavaScriptRegexEngine(), themes: [...new Set(Object.values(selected))], langs: [] });
31
+ return { highlighter, selected, languages: shiki.bundledLanguages, transformers: [
32
+ transformers.transformerMetaHighlight(), transformers.transformerNotationDiff(),
33
+ transformers.transformerNotationHighlight(), transformers.transformerNotationFocus(), transformers.transformerNotationErrorLevel(),
34
+ ] };
35
+ };
36
+ let initialized: ReturnType<typeof initialize> | undefined;
37
+ const loading = new Map<string, Promise<void>>();
38
+ md.renderer.rules.fence = async (tokens, index) => {
39
+ const token = tokens[index];
40
+ const { highlighter, selected, languages, transformers } = await (initialized ??= initialize());
41
+ const label = /^[\w+-]+/.exec(token.info.trim())?.[0] ?? 'text';
42
+ let lang = label;
43
+ if (!['text', 'txt', 'plain', 'plaintext', 'ansi'].includes(lang) && !highlighter.getLoadedLanguages().includes(lang)) {
44
+ if (Object.hasOwn(languages, lang)) {
45
+ if (!loading.has(lang)) loading.set(lang, highlighter.loadLanguage(lang as keyof typeof languages));
46
+ await loading.get(lang);
47
+ } else lang = 'text';
48
+ }
49
+ const highlighted = highlighter.codeToHtml(token.content, { lang, themes: selected, defaultColor: 'light', transformers, meta: { __raw: token.info } });
50
+ const numbers = /:line-numbers\b/.test(token.info);
51
+ const start = Number(/\{=(\d+)\}/.exec(token.info)?.[1] ?? 1);
52
+ const count = [...highlighted.matchAll(/class="line(?:[ "])/g)].length;
53
+ const gutter = numbers ? `<div class="line-numbers-wrapper" aria-hidden="true">${Array.from({ length: count }, (_, offset) => `<span class="line-number">${start + offset}</span><br>`).join('')}</div>` : '';
54
+ const active = /(?:^|\s)active(?:\s|$)/.test(token.info) ? ' active' : '';
55
+ return `<div class="language-${md.utils.escapeHtml(label)}${active}${numbers ? ' line-numbers-mode' : ''}"><button type="button" title="Copy Code" aria-label="Copy Code" class="copy"></button><span class="lang">${md.utils.escapeHtml(label)}</span>${highlighted}${gutter}</div>\n`;
56
+ };
57
+ }
@@ -0,0 +1,202 @@
1
+ /* Void Markdown — Code Blocks */
2
+
3
+ .void-md div[class*='language-'] {
4
+ position: relative;
5
+ margin: 1em 0;
6
+ border-radius: 8px;
7
+ overflow: hidden;
8
+ background: var(--vmd-bg-soft, #f8fafc);
9
+ }
10
+
11
+ @media (prefers-color-scheme: dark) {
12
+ :root:not([data-theme='light']) .void-md div[class*='language-'] {
13
+ background: #1e293b;
14
+ }
15
+ }
16
+
17
+ :root[data-theme='dark'] .void-md div[class*='language-'] {
18
+ background: #1e293b;
19
+ }
20
+
21
+ .void-md div[class*='language-'] pre {
22
+ margin: 0;
23
+ padding: 1.25em 1.5em;
24
+ overflow-x: auto;
25
+ background: transparent;
26
+ }
27
+
28
+ .void-md div[class*='language-'] code {
29
+ font-family: var(--vmd-font-mono, ui-monospace, 'Cascadia Code', Menlo, monospace);
30
+ font-size: 0.875em;
31
+ line-height: 1.7;
32
+ display: block;
33
+ width: fit-content;
34
+ min-width: 100%;
35
+ }
36
+
37
+ /* Language label */
38
+ .void-md div[class*='language-'] .lang {
39
+ position: absolute;
40
+ top: 0.5em;
41
+ right: 3em;
42
+ font-size: 0.75em;
43
+ font-weight: 500;
44
+ color: var(--vmd-text-muted, #94a3b8);
45
+ pointer-events: none;
46
+ text-transform: uppercase;
47
+ }
48
+
49
+ /* Copy button */
50
+ .void-md div[class*='language-'] .copy {
51
+ position: absolute;
52
+ top: 0.5em;
53
+ right: 0.5em;
54
+ width: 2em;
55
+ height: 2em;
56
+ border: 1px solid var(--vmd-border, #e2e8f0);
57
+ border-radius: 4px;
58
+ background: var(--vmd-bg-soft, #f8fafc);
59
+ cursor: pointer;
60
+ opacity: 0;
61
+ transition: opacity 0.15s;
62
+ }
63
+
64
+ .void-md div[class*='language-']:hover .copy {
65
+ opacity: 1;
66
+ }
67
+
68
+ .void-md div[class*='language-'] .copy::before {
69
+ content: '';
70
+ display: block;
71
+ width: 100%;
72
+ height: 100%;
73
+ background: currentColor;
74
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Crect x='9' y='9' width='13' height='13' rx='2'/%3E%3Cpath d='M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1'/%3E%3C/svg%3E")
75
+ center / 60% no-repeat;
76
+ }
77
+
78
+ .void-md div[class*='language-'] .copy.copied {
79
+ opacity: 1;
80
+ }
81
+
82
+ .void-md div[class*='language-'] .copy.copied::before {
83
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
84
+ }
85
+
86
+ /* Shiki dual theme — keep inline light colors, only swap in dark mode */
87
+ .void-md pre.shiki.shiki-themes {
88
+ background-color: var(--shiki-light-bg, #fff) !important;
89
+ }
90
+
91
+ @media (prefers-color-scheme: dark) {
92
+ :root:not([data-theme='light']) .void-md pre.shiki.shiki-themes {
93
+ background-color: var(--shiki-dark-bg, #1e293b) !important;
94
+ color: var(--shiki-dark, #e1e4e8) !important;
95
+ }
96
+
97
+ :root:not([data-theme='light']) .void-md pre.shiki.shiki-themes span[style*='--shiki-dark'] {
98
+ color: var(--shiki-dark) !important;
99
+ }
100
+ }
101
+
102
+ :root[data-theme='dark'] .void-md pre.shiki.shiki-themes {
103
+ background-color: var(--shiki-dark-bg, #1e293b) !important;
104
+ color: var(--shiki-dark, #e1e4e8) !important;
105
+ }
106
+
107
+ :root[data-theme='dark'] .void-md pre.shiki.shiki-themes span[style*='--shiki-dark'] {
108
+ color: var(--shiki-dark) !important;
109
+ }
110
+
111
+ /* Shiki notation transformers */
112
+ .void-md pre .diff.add {
113
+ background: rgba(16, 185, 129, 0.14);
114
+ }
115
+
116
+ .void-md pre .diff.add::before {
117
+ content: '+';
118
+ position: absolute;
119
+ left: 0.5em;
120
+ color: #10b981;
121
+ }
122
+
123
+ .void-md pre .diff.remove {
124
+ background: rgba(239, 68, 68, 0.14);
125
+ opacity: 0.7;
126
+ }
127
+
128
+ .void-md pre .diff.remove::before {
129
+ content: '-';
130
+ position: absolute;
131
+ left: 0.5em;
132
+ color: #ef4444;
133
+ }
134
+
135
+ .void-md pre .diff {
136
+ position: relative;
137
+ padding-left: 2em;
138
+ margin: 0 -1.5em;
139
+ padding-right: 1.5em;
140
+ }
141
+
142
+ .void-md pre .highlighted {
143
+ background: rgba(99, 102, 241, 0.1);
144
+ margin: 0 -1.5em;
145
+ padding: 0 1.5em;
146
+ display: inline-block;
147
+ width: calc(100% + 3em);
148
+ }
149
+
150
+ .void-md pre .highlighted.warning {
151
+ background: rgba(234, 179, 8, 0.14);
152
+ }
153
+
154
+ .void-md pre .highlighted.error {
155
+ background: rgba(239, 68, 68, 0.14);
156
+ }
157
+
158
+ .void-md pre .has-focus .line:not(.focused) {
159
+ opacity: 0.4;
160
+ transition: opacity 0.3s;
161
+ }
162
+
163
+ .void-md pre:hover .has-focus .line:not(.focused) {
164
+ opacity: 1;
165
+ }
166
+
167
+ /* Line numbers */
168
+ .void-md div[class*='language-'].line-numbers-mode {
169
+ padding-left: 2.5em;
170
+ }
171
+
172
+ .void-md .line-numbers-wrapper {
173
+ position: absolute;
174
+ top: 0;
175
+ left: 0;
176
+ bottom: 0;
177
+ width: 2.5em;
178
+ padding: 1.25em 0;
179
+ text-align: center;
180
+ font-family: var(--vmd-font-mono, monospace);
181
+ font-size: 0.875em;
182
+ line-height: 1.7;
183
+ color: var(--vmd-text-muted, #94a3b8);
184
+ border-right: 1px solid var(--vmd-border, #e2e8f0);
185
+ background: var(--vmd-bg-soft, #f8fafc);
186
+ }
187
+
188
+ @media (prefers-color-scheme: dark) {
189
+ :root:not([data-theme='light']) .void-md .line-numbers-wrapper {
190
+ background: #1e293b;
191
+ }
192
+ }
193
+
194
+ :root[data-theme='dark'] .void-md .line-numbers-wrapper {
195
+ background: #1e293b;
196
+ }
197
+
198
+ /* Keep the official theme usable with a keyboard and touch input. */
199
+ .void-md div[class*='language-'] .copy:focus-visible,
200
+ .void-md div[class*='language-'] .copy.copy-error { opacity: 1; }
201
+ .void-md div[class*='language-'] .copy:focus-visible { outline: 2px solid var(--vmd-link, #2563eb); outline-offset: 2px; }
202
+ @media (hover: none) { .void-md div[class*='language-'] .copy { opacity: 1; } }
@@ -0,0 +1,125 @@
1
+ /* Void Markdown — Containers & GitHub Alerts */
2
+
3
+ .void-md .custom-block {
4
+ margin: 1em 0;
5
+ padding: 1em 1.25em;
6
+ border-radius: 8px;
7
+ border-left: 4px solid;
8
+ }
9
+
10
+ .void-md .custom-block-title {
11
+ font-weight: 600;
12
+ margin-bottom: 0.5em;
13
+ }
14
+
15
+ .void-md .custom-block > :last-child {
16
+ margin-bottom: 0;
17
+ }
18
+
19
+ /* Tip */
20
+ .void-md .tip {
21
+ border-color: #10b981;
22
+ background: rgba(16, 185, 129, 0.08);
23
+ }
24
+
25
+ .void-md .tip .custom-block-title {
26
+ color: #10b981;
27
+ }
28
+
29
+ /* Info / Note */
30
+ .void-md .info,
31
+ .void-md .note {
32
+ border-color: #3b82f6;
33
+ background: rgba(59, 130, 246, 0.08);
34
+ }
35
+
36
+ .void-md .info .custom-block-title,
37
+ .void-md .note .custom-block-title {
38
+ color: #3b82f6;
39
+ }
40
+
41
+ /* Warning / Important */
42
+ .void-md .warning,
43
+ .void-md .important {
44
+ border-color: #eab308;
45
+ background: rgba(234, 179, 8, 0.08);
46
+ }
47
+
48
+ .void-md .warning .custom-block-title,
49
+ .void-md .important .custom-block-title {
50
+ color: #ca8a04;
51
+ }
52
+
53
+ /* Danger / Caution */
54
+ .void-md .danger,
55
+ .void-md .caution {
56
+ border-color: #ef4444;
57
+ background: rgba(239, 68, 68, 0.08);
58
+ }
59
+
60
+ .void-md .danger .custom-block-title,
61
+ .void-md .caution .custom-block-title {
62
+ color: #ef4444;
63
+ }
64
+
65
+ /* Details (collapsible) */
66
+ .void-md details.custom-block {
67
+ cursor: pointer;
68
+ }
69
+
70
+ .void-md details.custom-block summary {
71
+ font-weight: 600;
72
+ cursor: pointer;
73
+ }
74
+
75
+ .void-md details.custom-block[open] summary {
76
+ margin-bottom: 0.5em;
77
+ }
78
+
79
+ /* Dark mode adjustments */
80
+ @media (prefers-color-scheme: dark) {
81
+ :root:not([data-theme='light']) .void-md .tip {
82
+ background: rgba(16, 185, 129, 0.12);
83
+ }
84
+ :root:not([data-theme='light']) .void-md .info,
85
+ :root:not([data-theme='light']) .void-md .note {
86
+ background: rgba(59, 130, 246, 0.12);
87
+ }
88
+ :root:not([data-theme='light']) .void-md .warning,
89
+ :root:not([data-theme='light']) .void-md .important {
90
+ background: rgba(234, 179, 8, 0.12);
91
+ }
92
+ :root:not([data-theme='light']) .void-md .warning .custom-block-title,
93
+ :root:not([data-theme='light']) .void-md .important .custom-block-title {
94
+ color: #eab308;
95
+ }
96
+ :root:not([data-theme='light']) .void-md .danger,
97
+ :root:not([data-theme='light']) .void-md .caution {
98
+ background: rgba(239, 68, 68, 0.12);
99
+ }
100
+ }
101
+
102
+ :root[data-theme='dark'] .void-md .tip {
103
+ background: rgba(16, 185, 129, 0.12);
104
+ }
105
+ :root[data-theme='dark'] .void-md .info,
106
+ :root[data-theme='dark'] .void-md .note {
107
+ background: rgba(59, 130, 246, 0.12);
108
+ }
109
+ :root[data-theme='dark'] .void-md .warning,
110
+ :root[data-theme='dark'] .void-md .important {
111
+ background: rgba(234, 179, 8, 0.12);
112
+ }
113
+ :root[data-theme='dark'] .void-md .warning .custom-block-title,
114
+ :root[data-theme='dark'] .void-md .important .custom-block-title {
115
+ color: #eab308;
116
+ }
117
+ :root[data-theme='dark'] .void-md .danger,
118
+ :root[data-theme='dark'] .void-md .caution {
119
+ background: rgba(239, 68, 68, 0.12);
120
+ }
121
+
122
+ /* GitHub Alerts */
123
+ .void-md .github-alert {
124
+ border-left-width: 4px;
125
+ }
@@ -0,0 +1,4 @@
1
+ /* Void Markdown — Content Styles (scoped to .void-md) */
2
+ @import './prose.css';
3
+ @import './code.css';
4
+ @import './containers.css';
@@ -0,0 +1,7 @@
1
+ /* Void Markdown — Full Theme (reset + baseline + content)
2
+ *
3
+ * Use this entry for standalone markdown sites that need a complete stylesheet.
4
+ * For embedding markdown in an existing app, use content.css instead.
5
+ */
6
+ @import './reset.css';
7
+ @import './content.css';