reallysimpledocs 1.0.0-beta.2 → 1.0.0-beta.3

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A simple documentation system for Astro, built with **Basecoat** and **Tailwind CSS**.
4
4
 
5
- RSD keeps docs portable: content lives in `docs/`, navigation lives in `docs/docs.json`, and the Astro integration owns the docs UI.
5
+ reallysimpledocs keeps docs portable: content lives in `docs/`, navigation lives in `docs/docs.json`, and the Astro integration owns the docs UI.
6
6
 
7
7
  ## Add to an Astro site
8
8
 
@@ -59,21 +59,19 @@ docs/
59
59
  Welcome to the docs.
60
60
  ```
61
61
 
62
- RSD injects docs pages, Lunr search, per-page Markdown exports, `llms.txt`, and `llms-full.txt`.
62
+ reallysimpledocs injects docs pages, Lunr search, per-page Markdown exports, `llms.txt`, and `llms-full.txt`.
63
63
 
64
- Use `.md` for normal pages and `.mdx` when a page needs Astro components:
64
+ Use `.mdx` when a page needs reallysimpledocs components. Common reallysimpledocs components are available by default, so you do not need to import them in each page:
65
65
 
66
66
  ```mdx
67
- import { Callout, Preview } from "reallysimpledocs/components";
68
-
69
67
  # Button
70
68
 
71
- <Callout type="tip">MDX is optional. Plain Markdown stays the default.</Callout>
69
+ <Callout title="MDX works">MDX is optional. Plain Markdown stays the default.</Callout>
72
70
  ```
73
71
 
74
72
  ## Use Basecoat on the rest of the site
75
73
 
76
- RSD styles and scripts its generated docs pages. For custom Astro pages outside the docs route, use Basecoat directly in your own layout.
74
+ reallysimpledocs styles and scripts its generated docs pages. For custom Astro pages outside the docs route, use Basecoat directly in your own layout.
77
75
 
78
76
  Install stays the same:
79
77
 
@@ -103,7 +101,7 @@ import "basecoat-css/all.min";
103
101
  <slot />
104
102
  ```
105
103
 
106
- Use the same Basecoat `style` in your RSD config and your site stylesheet if you want the docs and custom pages to match.
104
+ Use the same Basecoat `style` in your reallysimpledocs config and your site stylesheet if you want the docs and custom pages to match.
107
105
 
108
106
  ## Local development (this repo)
109
107
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reallysimpledocs",
3
- "version": "1.0.0-beta.2",
3
+ "version": "1.0.0-beta.3",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.js",
@@ -26,6 +26,19 @@ const normalizeArray = (value) => (Array.isArray(value) ? value : value ? [value
26
26
 
27
27
  const normalizeImportPath = (filePath) => filePath.replaceAll(path.sep, "/");
28
28
 
29
+ const isInsideDir = (dir, file) => {
30
+ const relative = path.relative(dir, file);
31
+ return relative && !relative.startsWith("..") && !path.isAbsolute(relative);
32
+ };
33
+
34
+ const shikiConfig = {
35
+ themes: {
36
+ light: "github-light",
37
+ dark: "github-dark",
38
+ },
39
+ defaultColor: false,
40
+ };
41
+
29
42
  function collectMdxModules(docsDir) {
30
43
  if (!fs.existsSync(docsDir)) return {};
31
44
  const modules = {};
@@ -78,7 +91,6 @@ export default function reallySimpleDocs(options = {}) {
78
91
  siteFile: normalizedOptions.siteFile ? path.resolve(root, normalizedOptions.siteFile) : null,
79
92
  routeBase: normalizeRouteBase(normalizedOptions.routeBase),
80
93
  };
81
- const mdxModules = collectMdxModules(docsDir);
82
94
  const resolveUserId = (id) => (id?.startsWith(".") ? path.resolve(root, id) : id);
83
95
  const components = {
84
96
  SidebarHeader:
@@ -123,7 +135,15 @@ export default function reallySimpleDocs(options = {}) {
123
135
  });
124
136
 
125
137
  updateConfig({
126
- integrations: [mdx()],
138
+ integrations: [
139
+ mdx({
140
+ syntaxHighlight: "shiki",
141
+ shikiConfig,
142
+ }),
143
+ ],
144
+ markdown: {
145
+ shikiConfig,
146
+ },
127
147
  vite: {
128
148
  build: {
129
149
  cssMinify: false,
@@ -132,6 +152,21 @@ export default function reallySimpleDocs(options = {}) {
132
152
  tailwindcss(),
133
153
  {
134
154
  name: "reallysimpledocs-virtual-modules",
155
+ configureServer(server) {
156
+ const invalidateMdxPages = () => {
157
+ const module = server.moduleGraph.getModuleById("\0virtual:reallysimpledocs/mdx-pages");
158
+ if (module) server.moduleGraph.invalidateModule(module);
159
+ server.ws.send({ type: "full-reload" });
160
+ };
161
+
162
+ server.watcher.add(path.join(docsDir, "**/*.mdx"));
163
+ server.watcher.on("add", (file) => {
164
+ if (file.endsWith(".mdx") && isInsideDir(docsDir, file)) invalidateMdxPages();
165
+ });
166
+ server.watcher.on("unlink", (file) => {
167
+ if (file.endsWith(".mdx") && isInsideDir(docsDir, file)) invalidateMdxPages();
168
+ });
169
+ },
135
170
  resolveId(id) {
136
171
  if (id === "virtual:reallysimpledocs/config") return "\0virtual:reallysimpledocs/config";
137
172
  if (id === "virtual:reallysimpledocs/mdx-pages") return "\0virtual:reallysimpledocs/mdx-pages";
@@ -144,6 +179,7 @@ export default function reallySimpleDocs(options = {}) {
144
179
  load(id) {
145
180
  if (id === "\0virtual:reallysimpledocs/config") return `export default ${JSON.stringify(virtualConfig)};`;
146
181
  if (id === "\0virtual:reallysimpledocs/mdx-pages") {
182
+ const mdxModules = collectMdxModules(docsDir);
147
183
  const imports = Object.entries(mdxModules)
148
184
  .map(([slug, filePath], index) => `import * as mdx${index} from ${JSON.stringify(filePath)};`)
149
185
  .join("\n");
@@ -2,13 +2,11 @@ export { default as DocsLayout } from "../runtime/components/DocsLayout.astro";
2
2
  export { default as CommandDialog } from "../runtime/components/CommandDialog.astro";
3
3
  export { default as DropdownMenu } from "../runtime/components/DropdownMenu.astro";
4
4
  export { default as Sidebar } from "../runtime/components/Sidebar.astro";
5
- export { default as Badge } from "../runtime/components/mdx/Badge.astro";
6
5
  export { default as Callout } from "../runtime/components/mdx/Callout.astro";
7
6
  export { default as Card } from "../runtime/components/mdx/Card.astro";
8
7
  export { default as CardGrid } from "../runtime/components/mdx/CardGrid.astro";
9
8
  export { default as Code } from "../runtime/components/mdx/Code.astro";
10
9
  export { default as CodeGroup } from "../runtime/components/mdx/CodeGroup.astro";
11
- export { default as LinkButton } from "../runtime/components/mdx/LinkButton.astro";
12
10
  export { default as Preview } from "../runtime/components/mdx/Preview.astro";
13
11
  export { default as Step } from "../runtime/components/mdx/Step.astro";
14
12
  export { default as Steps } from "../runtime/components/mdx/Steps.astro";
@@ -119,20 +119,25 @@
119
119
  }
120
120
  }
121
121
 
122
- .shiki {
123
- @apply bg-muted dark:bg-muted/50;
122
+ .shiki,
123
+ .astro-code {
124
+ @apply bg-muted/50;
125
+ background-color: color-mix(in oklab, var(--color-muted) 50%, transparent) !important;
124
126
  color: var(--shiki-light);
125
127
  }
126
128
 
127
- .shiki span {
129
+ .shiki span,
130
+ .astro-code span {
128
131
  color: var(--shiki-light);
129
132
  }
130
133
 
131
- html.dark .shiki {
134
+ html.dark .shiki,
135
+ html.dark .astro-code {
132
136
  color: var(--shiki-dark);
133
137
  }
134
138
 
135
- html.dark .shiki span {
139
+ html.dark .shiki span,
140
+ html.dark .astro-code span {
136
141
  color: var(--shiki-dark);
137
142
  }
138
143
 
@@ -155,179 +160,3 @@ html.dark .shiki span {
155
160
  .command mark {
156
161
  @apply bg-search-highlight/50 text-foreground rounded-sm px-0.5 dark:bg-search-highlight/25;
157
162
  }
158
-
159
- .docs-card-link {
160
- @apply transition-colors hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50;
161
- }
162
-
163
- .alert[data-variant='warning'] > svg,
164
- .alert[data-variant='warning'] > h3,
165
- .alert[data-variant='warning'] > section {
166
- @apply text-amber-700 dark:text-amber-300;
167
- }
168
-
169
- .alert-destructive[data-variant='danger'] > svg,
170
- .alert-destructive[data-variant='danger'] > h3,
171
- .alert-destructive[data-variant='danger'] > section {
172
- @apply text-destructive;
173
- }
174
-
175
- .code-block {
176
- @apply relative my-6 max-w-full overflow-hidden rounded-lg;
177
- }
178
-
179
- .code-block > header,
180
- .code-group > header,
181
- .preview > header {
182
- @apply flex h-10 items-center gap-2 border-b border-border/50 bg-muted px-3 text-sm font-medium dark:bg-muted/50;
183
- }
184
-
185
- .code-block > header > :first-child,
186
- .code-group > header > :first-child,
187
- .preview > header > :first-child {
188
- @apply mr-auto;
189
- }
190
-
191
- .code-block > pre {
192
- @apply my-0 max-w-full overflow-x-auto rounded-none border-0;
193
- }
194
-
195
- .code-block > pre > code {
196
- @apply block min-w-full w-max px-4 py-3 text-[13px];
197
- }
198
-
199
- .code-block > .shiki {
200
- @apply my-0 rounded-none border-0;
201
- }
202
-
203
- .code-block > button {
204
- @apply absolute right-2.5 top-2.5;
205
- }
206
-
207
- .code-block button,
208
- .code-group button[aria-label='Copy code'],
209
- .preview button[aria-label='Copy code'] {
210
- @apply flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground outline-none hover:bg-accent hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 dark:hover:bg-accent/50 [&_svg]:size-4;
211
- }
212
-
213
- .code-block > header > button,
214
- .code-group > header > button,
215
- .preview > header > button {
216
- @apply ml-auto;
217
- }
218
-
219
- .code-group {
220
- @apply relative my-6 max-w-full overflow-hidden rounded-lg;
221
- }
222
-
223
- .code-group > header > .tabs {
224
- @apply my-0 min-w-0 max-w-full gap-0;
225
- }
226
-
227
- .code-group > header > .tabs > nav {
228
- @apply h-full justify-start rounded-none bg-transparent p-0;
229
- }
230
-
231
- .code-group > [role='tabpanel'] {
232
- @apply border-t-0;
233
- }
234
-
235
- .code-group .code-block {
236
- @apply my-0 min-w-0 rounded-none;
237
- }
238
-
239
- .code-group .code-block > header {
240
- @apply hidden;
241
- }
242
-
243
- .preview {
244
- @apply relative my-6 max-w-full overflow-hidden rounded-lg border border-border/50 bg-muted dark:bg-muted/50;
245
- }
246
-
247
- .preview > header > .tabs {
248
- @apply my-0 min-w-0 max-w-full gap-0;
249
- }
250
-
251
- .preview > header > .tabs > nav {
252
- @apply h-full justify-start rounded-none bg-transparent p-0;
253
- }
254
-
255
- .preview > [role='tabpanel'] {
256
- @apply border-t-0;
257
- }
258
-
259
- .preview > [data-code-panel] .code-block {
260
- @apply my-0 min-w-0 rounded-none;
261
- }
262
-
263
- .preview > header > button[aria-label='Copy code'] {
264
- display: none;
265
- }
266
-
267
- .preview:has(> [data-code-panel]:not([hidden])) > header > button[aria-label='Copy code'] {
268
- display: flex;
269
- }
270
-
271
- .preview > [data-preview-panel] > div {
272
- @apply flex min-h-64 w-full items-center justify-center bg-muted p-6 dark:bg-muted/50;
273
- }
274
-
275
- .steps {
276
- @apply mb-12 mt-6 md:ml-4 md:border-l md:pl-8;
277
- counter-reset: step;
278
- }
279
-
280
- .steps > section {
281
- counter-increment: step;
282
- }
283
-
284
- .steps > section > h3 {
285
- @apply relative mt-12 scroll-m-28 text-lg font-medium tracking-tight;
286
- }
287
-
288
- .steps > section:first-child > h3 {
289
- @apply mt-0;
290
- }
291
-
292
- .steps > section > h3::before {
293
- @apply mr-4 inline-flex size-7 items-center justify-center rounded-full bg-muted text-sm font-medium text-muted-foreground;
294
- content: counter(step);
295
- }
296
-
297
- @media (width >= 48rem) {
298
- .steps > section > h3::before {
299
- position: absolute;
300
- left: 0;
301
- top: 0;
302
- margin-right: 0;
303
- transform: translateX(-2.875rem);
304
- }
305
- }
306
-
307
- .steps > section > * + * {
308
- @apply mt-6;
309
- }
310
-
311
- .steps > section > h3 + * {
312
- @apply mt-4;
313
- }
314
-
315
- .steps > section > * {
316
- @apply leading-relaxed;
317
- }
318
-
319
- .steps > section > *:first-child {
320
- @apply mt-0;
321
- }
322
-
323
- .steps > section > *:last-child {
324
- @apply mb-0;
325
- }
326
-
327
- .prose > .alert,
328
- .prose > .alert-destructive,
329
- .prose > .card,
330
- .prose > .grid,
331
- .prose > .tabs {
332
- @apply my-6;
333
- }
@@ -199,7 +199,7 @@ const { indexUrl } = Astro.props;
199
199
 
200
200
  async function loadIndex() {
201
201
  if (state.index) return state;
202
- if (!indexUrl) throw new Error("Missing RSD search index URL.");
202
+ if (!indexUrl) throw new Error("Missing reallysimpledocs search index URL.");
203
203
  if (!state.loadPromise) {
204
204
  renderLoading();
205
205
  state.loadPromise = fetch(indexUrl)
@@ -1,29 +1,11 @@
1
1
  ---
2
- import CircleAlert from "lucide-static/icons/circle-alert.svg?raw";
3
- import Info from "lucide-static/icons/info.svg?raw";
4
- import Lightbulb from "lucide-static/icons/lightbulb.svg?raw";
5
- import TriangleAlert from "lucide-static/icons/triangle-alert.svg?raw";
6
2
  import * as lucideIcons from "lucide-static";
7
3
  import { addSvgClass } from "../../lib/html.js";
8
4
 
9
- const { type, variant, title, icon: showIcon = true, class: className = "" } = Astro.props;
10
- const normalizedType = String(variant || type || "note").toLowerCase();
11
- const calloutType = normalizedType === "note" ? "info" : normalizedType;
12
- const destructive = calloutType === "danger";
13
- const labels = {
14
- note: "Note",
15
- info: "Info",
16
- tip: "Tip",
17
- warning: "Warning",
18
- danger: "Danger",
19
- };
20
- const icons = {
21
- note: Info,
22
- info: Info,
23
- tip: Lightbulb,
24
- warning: TriangleAlert,
25
- danger: CircleAlert,
26
- };
5
+ const { type, title = "Note", icon: iconName, class: className = "" } = Astro.props;
6
+ const normalizedType = String(type || "").toLowerCase();
7
+ const calloutType = normalizedType === "warning" || normalizedType === "danger" ? normalizedType : "";
8
+ const danger = calloutType === "danger";
27
9
 
28
10
  function resolveNamedIcon(name) {
29
11
  if (!name || typeof name !== "string") return "";
@@ -39,13 +21,18 @@ function resolveNamedIcon(name) {
39
21
  return lucideIcons[pascalName] || "";
40
22
  }
41
23
 
42
- const iconSource =
43
- showIcon === false ? "" : typeof showIcon === "string" ? resolveNamedIcon(showIcon) : icons[normalizedType] || Info;
24
+ const iconSource = typeof iconName === "string" ? resolveNamedIcon(iconName) : "";
44
25
  const icon = addSvgClass(iconSource, "size-4");
45
- const heading = title || labels[normalizedType] || labels.note;
26
+ const heading = title;
27
+ const variantClass =
28
+ calloutType === "warning"
29
+ ? "[&>svg]:text-amber-700 [&>h3]:text-amber-700 [&>section]:text-amber-700 dark:[&>svg]:text-amber-300 dark:[&>h3]:text-amber-300 dark:[&>section]:text-amber-300"
30
+ : danger
31
+ ? "[&>svg]:text-destructive [&>h3]:text-destructive [&>section]:text-destructive"
32
+ : "";
46
33
  ---
47
34
 
48
- <div class:list={[destructive ? "alert-destructive" : "alert", className]} data-variant={calloutType}>
35
+ <div class:list={[danger ? "alert-destructive" : "alert", "my-6", variantClass, className]}>
49
36
  {icon && <Fragment set:html={icon} />}
50
37
  <h3>{heading}</h3>
51
38
  <section>
@@ -1,13 +1,14 @@
1
1
  ---
2
2
  const { title, description, href, class: className = "" } = Astro.props;
3
3
  const Tag = href ? "a" : "article";
4
+ const classes = [
5
+ "my-6 block w-full rounded-lg border bg-card p-3 text-card-foreground no-underline",
6
+ href && "transition-colors hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50",
7
+ className,
8
+ ];
4
9
  ---
5
10
 
6
- <Tag href={href} class:list={["card w-full no-underline", href && "docs-card-link", className]}>
7
- {(title || description) && (
8
- <header>
9
- {title && <h3>{title}</h3>}
10
- {description && <p>{description}</p>}
11
- </header>
12
- )}
11
+ <Tag href={href} class:list={classes}>
12
+ {title && <h3 class="font-medium">{title}</h3>}
13
+ {description && <p class="mt-1 text-sm text-muted-foreground">{description}</p>}
13
14
  </Tag>
@@ -2,6 +2,6 @@
2
2
  const { class: className = "" } = Astro.props;
3
3
  ---
4
4
 
5
- <div class:list={["grid w-full gap-4 sm:grid-cols-2", className]}>
5
+ <div class:list={["my-6 grid w-full gap-4 sm:grid-cols-2 [&>*]:my-0", className]}>
6
6
  <slot />
7
7
  </div>
@@ -1,19 +1,39 @@
1
1
  ---
2
- import { codeToHtml } from "shiki";
2
+ import { highlightCode } from "../../lib/highlight.js";
3
3
 
4
- const { code = "", lang = "text", title, class: className = "" } = Astro.props;
5
- const highlightLang = String(lang || "text").toLowerCase() === "mdx" ? "tsx" : String(lang || "text");
6
- const html = await codeToHtml(String(code || ""), {
7
- lang: highlightLang,
8
- themes: {
9
- light: "github-light",
10
- dark: "github-dark",
11
- },
12
- defaultColor: false,
13
- });
4
+ const { lang = "text", title, class: className = "" } = Astro.props;
5
+ const slotHtml = Astro.slots.has("default") ? await Astro.slots.render("default") : "";
6
+ const rawCode = decodeSlotText(slotHtml);
7
+ const html = await highlightCode(rawCode, lang);
8
+ const classes = [
9
+ "relative my-6 w-full max-w-full overflow-hidden rounded-lg",
10
+ className,
11
+ ];
12
+
13
+ function decodeSlotText(value) {
14
+ return String(value || "")
15
+ .replace(/^<p>/, "")
16
+ .replace(/<\/p>$/, "")
17
+ .replace(/&lt;/g, "<")
18
+ .replace(/&gt;/g, ">")
19
+ .replace(/&amp;/g, "&")
20
+ .replace(/&quot;/g, '"')
21
+ .replace(/&#39;/g, "'")
22
+ .trim();
23
+ }
14
24
  ---
15
25
 
16
- <div class:list={["code-block", className]}>
17
- {title && <header><span>{title}</span></header>}
26
+ <div
27
+ data-code-block
28
+ data-code-title={title || undefined}
29
+ class:list={classes}
30
+ >
31
+ {
32
+ title && (
33
+ <header class="flex h-10 items-center gap-2 border-b border-border/50 bg-muted/50 px-3 text-sm font-medium">
34
+ <span class="mr-auto">{title}</span>
35
+ </header>
36
+ )
37
+ }
18
38
  <Fragment set:html={html} />
19
39
  </div>
@@ -1,21 +1,45 @@
1
1
  ---
2
- import Code from "./Code.astro";
3
-
4
- const { tabs = [], defaultTab, class: className = "" } = Astro.props;
2
+ const { defaultTab, class: className = "" } = Astro.props;
5
3
  const id = `code-group-${Math.random().toString(36).slice(2)}`;
6
- const normalizedTabs = tabs.map((tab, index) => ({
7
- ...tab,
8
- value: tab.value || tab.label || String(index),
9
- lang: tab.lang || "text",
10
- code: tab.code || "",
11
- }));
4
+ const slotHtml = Astro.slots.has("default") ? await Astro.slots.render("default") : "";
5
+ const normalizedTabs = parseCodeTabs(slotHtml);
12
6
  const defaultValue = defaultTab || normalizedTabs[0]?.value;
7
+ const classes = [
8
+ "relative my-6 w-full max-w-full overflow-hidden rounded-lg [&>[role=tabpanel]]:border-t-0 [&_[data-code-block]]:my-0 [&_[data-code-block]]:min-w-0 [&_[data-code-block]]:rounded-none [&_[data-code-block]>header]:hidden",
9
+ className,
10
+ ];
11
+
12
+ function parseCodeTabs(html) {
13
+ const blocks = [];
14
+ const pattern =
15
+ /<div\b(?=[^>]*\bdata-code-block\b)(?=[^>]*\bdata-code-title="([^"]*)")[^>]*>([\s\S]*?)<\/div>/g;
16
+ let match;
17
+ while ((match = pattern.exec(html))) {
18
+ const label = decodeHtml(match[1]).trim();
19
+ if (!label) continue;
20
+ blocks.push({
21
+ label,
22
+ value: label,
23
+ html: match[0],
24
+ });
25
+ }
26
+ return blocks;
27
+ }
28
+
29
+ function decodeHtml(value) {
30
+ return String(value || "")
31
+ .replace(/&lt;/g, "<")
32
+ .replace(/&gt;/g, ">")
33
+ .replace(/&amp;/g, "&")
34
+ .replace(/&quot;/g, '"')
35
+ .replace(/&#39;/g, "'");
36
+ }
13
37
  ---
14
38
 
15
- <div class:list={["code-group", className]}>
16
- <header>
17
- <div class="tabs">
18
- <nav role="tablist" aria-orientation="horizontal" data-variant="line">
39
+ <div class:list={classes} data-code-group>
40
+ <header class="flex h-10 items-center gap-2 border-b border-border/50 bg-muted/50 px-3 text-sm font-medium">
41
+ <div class="tabs my-0 min-w-0 max-w-full gap-0">
42
+ <nav class="h-full justify-start rounded-none bg-transparent p-0" role="tablist" aria-orientation="horizontal" data-variant="line">
19
43
  {
20
44
  normalizedTabs.map((tab, index) => {
21
45
  const selected = tab.value === defaultValue;
@@ -48,7 +72,7 @@ const defaultValue = defaultTab || normalizedTabs[0]?.value;
48
72
  aria-selected={selected ? "true" : "false"}
49
73
  hidden={!selected}
50
74
  >
51
- <Code code={tab.code} lang={tab.lang} />
75
+ <Fragment set:html={tab.html} />
52
76
  </div>
53
77
  );
54
78
  })
@@ -1,14 +1,18 @@
1
1
  ---
2
- import Code from "./Code.astro";
2
+ import { highlightCode } from "../../lib/highlight.js";
3
3
 
4
4
  const { code = "", lang = "html", title = "Preview", class: className = "" } = Astro.props;
5
5
  const id = `preview-${Math.random().toString(36).slice(2)}`;
6
+ const html = await highlightCode(code, lang);
6
7
  ---
7
8
 
8
- <section class="preview">
9
- <header>
10
- <div class="tabs">
11
- <nav role="tablist" aria-orientation="horizontal" data-variant="line">
9
+ <section
10
+ class="relative my-6 max-w-full overflow-hidden rounded-lg border [&>[role=tabpanel]]:border-t-0 [&>[data-code-panel]_[data-code-block]]:my-0 [&>[data-code-panel]_[data-code-block]]:min-w-0 [&>[data-code-panel]_[data-code-block]]:rounded-none [&>[data-preview-panel]>div]:flex [&>[data-preview-panel]>div]:min-h-64 [&>[data-preview-panel]>div]:w-full [&>[data-preview-panel]>div]:items-center [&>[data-preview-panel]>div]:justify-center [&>[data-preview-panel]>div]:p-6"
11
+ data-preview
12
+ >
13
+ <header class="flex h-10 items-center gap-2 border-b border-border/50 bg-muted/50 px-3 text-sm font-medium">
14
+ <div class="tabs my-0 min-w-0 max-w-full gap-0">
15
+ <nav class="h-full justify-start rounded-none bg-transparent p-0" role="tablist" aria-orientation="horizontal" data-variant="line">
12
16
  <button
13
17
  type="button"
14
18
  role="tab"
@@ -53,6 +57,11 @@ const id = `preview-${Math.random().toString(36).slice(2)}`;
53
57
  aria-selected="false"
54
58
  hidden
55
59
  >
56
- <Code code={code} lang={lang} />
60
+ <div
61
+ class="relative my-6 w-full max-w-full overflow-hidden rounded-lg"
62
+ data-code-block
63
+ >
64
+ <Fragment set:html={html} />
65
+ </div>
57
66
  </section>
58
67
  </section>
@@ -1,7 +1,11 @@
1
1
  ---
2
2
  const { class: className = "" } = Astro.props;
3
+ const classes = [
4
+ "mb-12 mt-6 [counter-reset:step] md:ml-4 md:border-l md:pl-8 [&>section]:mb-10 [&>section]:[counter-increment:step] [&>section:last-child]:mb-0 [&>section>*+*]:mt-6 [&>section>*]:leading-relaxed [&>section>*:first-child]:mt-0 [&>section>*:last-child]:mb-0 [&>section>h3+*]:mt-4 [&>section>h3]:relative [&>section>h3]:scroll-m-28 [&>section>h3]:text-lg [&>section>h3]:font-medium [&>section>h3]:tracking-tight [&>section>h3]:before:mr-4 [&>section>h3]:before:inline-flex [&>section>h3]:before:size-7 [&>section>h3]:before:items-center [&>section>h3]:before:justify-center [&>section>h3]:before:rounded-full [&>section>h3]:before:bg-muted [&>section>h3]:before:text-sm [&>section>h3]:before:font-medium [&>section>h3]:before:text-muted-foreground [&>section>h3]:before:content-[counter(step)] md:[&>section>h3]:before:absolute md:[&>section>h3]:before:left-0 md:[&>section>h3]:before:top-0 md:[&>section>h3]:before:mr-0 md:[&>section>h3]:before:-translate-x-[2.875rem]",
5
+ className,
6
+ ];
3
7
  ---
4
8
 
5
- <div role="list" class:list={["steps", className]}>
9
+ <div role="list" class:list={classes}>
6
10
  <slot />
7
11
  </div>
@@ -9,7 +9,7 @@ const normalizedTabs = tabs.map((tab, index) => ({
9
9
  const defaultValue = defaultTab || normalizedTabs.find((tab) => !tab.disabled)?.value || normalizedTabs[0]?.value;
10
10
  ---
11
11
 
12
- <div class:list={["tabs", className]}>
12
+ <div class:list={["tabs my-6", className]}>
13
13
  <nav role="tablist" aria-orientation={orientation} data-variant={variant}>
14
14
  {
15
15
  normalizedTabs.map((tab, index) => {
@@ -2,8 +2,8 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { marked } from "marked";
4
4
  import GithubSlugger from "github-slugger";
5
- import { codeToHtml } from "shiki";
6
5
  import * as lucideIcons from "lucide-static";
6
+ import { highlightCode as renderHighlightedCode } from "./highlight.js";
7
7
  import { markdownPath, routePath } from "./paths.js";
8
8
 
9
9
  export function getManifest(config) {
@@ -117,7 +117,7 @@ export async function renderDoc(config, page) {
117
117
  };
118
118
 
119
119
  const tokens = marked.lexer(content);
120
- await highlightCode(tokens);
120
+ await highlightMarkdownCode(tokens);
121
121
  const html = marked.parser(tokens, { renderer });
122
122
  return { page, html, headings };
123
123
  }
@@ -164,7 +164,9 @@ export function resolveIcon(icon) {
164
164
  .join("")
165
165
  : trimmed.charAt(0).toUpperCase() + trimmed.slice(1);
166
166
  const iconClass = trimmed.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
167
- return lucideIcons[pascalName]?.replace('class="lucide', `class="lucide lucide-${iconClass}`) || "";
167
+ const svg = lucideIcons[pascalName] || "";
168
+ if (!svg || svg.includes(`lucide-${iconClass}`)) return svg;
169
+ return svg.replace('class="lucide', `class="lucide lucide-${iconClass}`);
168
170
  }
169
171
 
170
172
  export function buildMenus(config, currentSlug) {
@@ -282,11 +284,11 @@ function normalizeDocSource(source) {
282
284
  const firstHeading = withoutFrontmatter.search(/^#\s+/m);
283
285
  if (firstHeading > 0) return withoutFrontmatter.slice(firstHeading).trimStart();
284
286
  return withoutFrontmatter
285
- .replace(/^(\s*(?:import|export)\s+[^\n]*\n)+/m, "")
287
+ .replace(/^(?:[ \t]*(?:import|export)\s+[^\n]*\n)+/, "")
286
288
  .trimStart();
287
289
  }
288
290
 
289
- async function highlightCode(tokens) {
291
+ async function highlightMarkdownCode(tokens) {
290
292
  for (const token of tokens) {
291
293
  if (token.type === "code") {
292
294
  const html = await highlightToken(token);
@@ -300,33 +302,24 @@ async function highlightCode(tokens) {
300
302
  }
301
303
 
302
304
  for (const childTokens of nestedTokenLists(token)) {
303
- await highlightCode(childTokens);
305
+ await highlightMarkdownCode(childTokens);
304
306
  }
305
307
  }
306
308
  }
307
309
 
308
310
  async function highlightToken(token) {
311
+ const lang = normalizeLanguage(token.lang);
309
312
  try {
310
- return await codeToHtml(token.text, {
311
- lang: normalizeLanguage(token.lang),
312
- themes: {
313
- light: "github-light",
314
- dark: "github-dark",
315
- },
316
- defaultColor: false,
317
- });
313
+ return wrapCodeBlock(await renderHighlightedCode(token.text, lang));
318
314
  } catch {
319
- return codeToHtml(token.text, {
320
- lang: "text",
321
- themes: {
322
- light: "github-light",
323
- dark: "github-dark",
324
- },
325
- defaultColor: false,
326
- });
315
+ return wrapCodeBlock(await renderHighlightedCode(token.text, "text"));
327
316
  }
328
317
  }
329
318
 
319
+ function wrapCodeBlock(html) {
320
+ return `<div class="relative my-6 w-full max-w-full overflow-hidden rounded-lg" data-code-block>${html}</div>`;
321
+ }
322
+
330
323
  function normalizeLanguage(lang) {
331
324
  const value = lang?.trim().split(/\s+/)[0].toLowerCase() || "text";
332
325
  if (value === "njk") return "html";
@@ -0,0 +1,27 @@
1
+ import { codeToHtml } from "shiki";
2
+
3
+ export async function highlightCode(code, lang = "text") {
4
+ const value = String(code || "");
5
+ const language = String(lang || "text").toLowerCase() === "mdx" ? "tsx" : String(lang || "text");
6
+ const html = await codeToHtml(value, {
7
+ lang: language,
8
+ themes: {
9
+ light: "github-light",
10
+ dark: "github-dark",
11
+ },
12
+ defaultColor: false,
13
+ });
14
+ return addCodeLayoutClasses(html);
15
+ }
16
+
17
+ function addCodeLayoutClasses(html) {
18
+ return String(html)
19
+ .replace(
20
+ /<pre class="/,
21
+ '<pre class="!my-0 max-w-full overflow-x-auto !rounded-none !border-0 ',
22
+ )
23
+ .replace(
24
+ /<code>/,
25
+ '<code class="block min-w-full w-max px-4 py-3 text-[13px]">',
26
+ );
27
+ }
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  import config from "virtual:reallysimpledocs/config";
3
3
  import { getMdxPage } from "virtual:reallysimpledocs/mdx-pages";
4
+ import * as RsdComponents from "../../components/index.js";
4
5
  import DocsLayout from "../components/DocsLayout.astro";
5
6
  import { getDocHeadings, getPages, renderDoc } from "../lib/docs.js";
6
7
  import { routeSlug } from "../lib/paths.js";
@@ -20,5 +21,5 @@ const doc = isMdx ? { page, headings: getDocHeadings(config, page), html: "" } :
20
21
  ---
21
22
 
22
23
  <DocsLayout page={page} headings={doc.headings}>
23
- {MdxContent ? <MdxContent /> : <Fragment set:html={doc.html} />}
24
+ {MdxContent ? <MdxContent components={RsdComponents} /> : <Fragment set:html={doc.html} />}
24
25
  </DocsLayout>
@@ -20,17 +20,25 @@ const copyText = async (text) => {
20
20
  };
21
21
 
22
22
  const activePanelCode = (root) => root.querySelector(':scope > [role="tabpanel"]:not([hidden]) code');
23
+ const codeFrameClass = "relative my-6 w-full max-w-full overflow-hidden rounded-lg";
24
+ const codePreClasses = ["!my-0", "max-w-full", "overflow-x-auto", "!rounded-none", "!border-0"];
25
+ const codeContentClasses = ["block", "min-w-full", "w-max", "px-4", "py-3", "text-[13px]"];
26
+ const copyButtonClass =
27
+ "flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground outline-none hover:bg-accent hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 dark:hover:bg-accent/50 [&_svg]:size-4";
23
28
 
24
29
  const initCopyCode = (codeBlock) => {
25
30
  const codeEl = codeBlock.querySelector(":scope > code");
26
31
  if (!codeEl) return;
27
32
 
33
+ codeBlock.classList.add(...codePreClasses);
34
+ codeEl.classList.add(...codeContentClasses);
35
+
28
36
  let target = null;
29
- let frame = codeBlock.closest(".code-group");
37
+ let frame = codeBlock.closest("[data-code-group]");
30
38
  let getCode = frame ? () => activePanelCode(frame) || codeEl : () => codeEl;
31
39
  if (frame) target = frame.querySelector(":scope > header");
32
40
 
33
- const preview = codeBlock.closest(".preview");
41
+ const preview = codeBlock.closest("[data-preview]");
34
42
  const previewCodePanel = preview?.querySelector(":scope > [data-code-panel]");
35
43
  if (!frame && previewCodePanel?.contains(codeBlock)) {
36
44
  frame = preview;
@@ -39,10 +47,11 @@ const initCopyCode = (codeBlock) => {
39
47
  }
40
48
 
41
49
  if (!frame) {
42
- frame = codeBlock.parentElement?.classList.contains("code-block") ? codeBlock.parentElement : null;
50
+ frame = codeBlock.parentElement?.hasAttribute("data-code-block") ? codeBlock.parentElement : null;
43
51
  if (!frame) {
44
52
  frame = document.createElement("div");
45
- frame.className = "code-block";
53
+ frame.dataset.codeBlock = "";
54
+ frame.className = codeFrameClass;
46
55
  codeBlock.parentNode.insertBefore(frame, codeBlock);
47
56
  frame.appendChild(codeBlock);
48
57
  }
@@ -58,6 +67,11 @@ const initCopyCode = (codeBlock) => {
58
67
  const button = document.createElement("button");
59
68
  button.type = "button";
60
69
  button.setAttribute("aria-label", "Copy code");
70
+ button.className = `${copyButtonClass} ${
71
+ target === frame
72
+ ? "absolute right-2.5 top-2.5 bg-muted/50 shadow-sm backdrop-blur-sm"
73
+ : "ml-auto"
74
+ }`;
61
75
  button.innerHTML = Copy;
62
76
 
63
77
  button.addEventListener("click", async () => {
@@ -79,6 +93,16 @@ const initCopyCode = (codeBlock) => {
79
93
  });
80
94
 
81
95
  target.appendChild(button);
96
+ if (previewCodePanel?.contains(codeBlock)) {
97
+ const syncPreviewCopyButton = () => {
98
+ button.hidden = previewCodePanel.hidden;
99
+ };
100
+ syncPreviewCopyButton();
101
+ new MutationObserver(syncPreviewCopyButton).observe(previewCodePanel, {
102
+ attributes: true,
103
+ attributeFilter: ["hidden"],
104
+ });
105
+ }
82
106
  codeBlock.dataset.copyCodeInitialized = true;
83
107
  };
84
108
 
@@ -1,15 +0,0 @@
1
- ---
2
- const { variant = "secondary", class: className = "" } = Astro.props;
3
- const variants = {
4
- primary: "badge-primary",
5
- secondary: "badge-secondary",
6
- destructive: "badge-destructive",
7
- outline: "badge-outline",
8
- ghost: "badge-ghost",
9
- link: "badge-link",
10
- };
11
- ---
12
-
13
- <span class:list={[variants[variant] || "badge-secondary", className]}>
14
- <slot />
15
- </span>
@@ -1,14 +0,0 @@
1
- ---
2
- const { href, variant = "default", class: className = "", target, rel } = Astro.props;
3
- const variants = {
4
- default: "btn",
5
- secondary: "btn-secondary",
6
- outline: "btn-outline",
7
- ghost: "btn-ghost",
8
- link: "btn-link",
9
- };
10
- ---
11
-
12
- <a href={href} target={target} rel={rel} class:list={[variants[variant] || "btn", className]}>
13
- <slot />
14
- </a>