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 +6 -8
- package/package.json +1 -1
- package/src/astro/index.js +38 -2
- package/src/components/index.js +0 -2
- package/src/css/custom.css +10 -181
- package/src/runtime/components/CommandDialog.astro +1 -1
- package/src/runtime/components/mdx/Callout.astro +13 -26
- package/src/runtime/components/mdx/Card.astro +8 -7
- package/src/runtime/components/mdx/CardGrid.astro +1 -1
- package/src/runtime/components/mdx/Code.astro +33 -13
- package/src/runtime/components/mdx/CodeGroup.astro +38 -14
- package/src/runtime/components/mdx/Preview.astro +15 -6
- package/src/runtime/components/mdx/Steps.astro +5 -1
- package/src/runtime/components/mdx/Tabs.astro +1 -1
- package/src/runtime/lib/docs.js +15 -22
- package/src/runtime/lib/highlight.js +27 -0
- package/src/runtime/pages/doc.astro +2 -1
- package/src/runtime/scripts/copy-code.js +28 -4
- package/src/runtime/components/mdx/Badge.astro +0 -15
- package/src/runtime/components/mdx/LinkButton.astro +0 -14
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
|
-
|
|
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
|
-
|
|
62
|
+
reallysimpledocs injects docs pages, Lunr search, per-page Markdown exports, `llms.txt`, and `llms-full.txt`.
|
|
63
63
|
|
|
64
|
-
Use `.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
package/src/astro/index.js
CHANGED
|
@@ -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: [
|
|
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");
|
package/src/components/index.js
CHANGED
|
@@ -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";
|
package/src/css/custom.css
CHANGED
|
@@ -119,20 +119,25 @@
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
.shiki
|
|
123
|
-
|
|
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
|
|
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,
|
|
10
|
-
const normalizedType = String(
|
|
11
|
-
const calloutType = normalizedType === "
|
|
12
|
-
const
|
|
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
|
|
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={[
|
|
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={
|
|
7
|
-
{
|
|
8
|
-
|
|
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>
|
|
@@ -1,19 +1,39 @@
|
|
|
1
1
|
---
|
|
2
|
-
import {
|
|
2
|
+
import { highlightCode } from "../../lib/highlight.js";
|
|
3
3
|
|
|
4
|
-
const {
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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(/</g, "<")
|
|
18
|
+
.replace(/>/g, ">")
|
|
19
|
+
.replace(/&/g, "&")
|
|
20
|
+
.replace(/"/g, '"')
|
|
21
|
+
.replace(/'/g, "'")
|
|
22
|
+
.trim();
|
|
23
|
+
}
|
|
14
24
|
---
|
|
15
25
|
|
|
16
|
-
<div
|
|
17
|
-
|
|
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
|
-
|
|
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
|
|
7
|
-
|
|
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(/</g, "<")
|
|
32
|
+
.replace(/>/g, ">")
|
|
33
|
+
.replace(/&/g, "&")
|
|
34
|
+
.replace(/"/g, '"')
|
|
35
|
+
.replace(/'/g, "'");
|
|
36
|
+
}
|
|
13
37
|
---
|
|
14
38
|
|
|
15
|
-
<div class:list={
|
|
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
|
-
<
|
|
75
|
+
<Fragment set:html={tab.html} />
|
|
52
76
|
</div>
|
|
53
77
|
);
|
|
54
78
|
})
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
---
|
|
2
|
-
import
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
<
|
|
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={
|
|
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) => {
|
package/src/runtime/lib/docs.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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(/^(\
|
|
287
|
+
.replace(/^(?:[ \t]*(?:import|export)\s+[^\n]*\n)+/, "")
|
|
286
288
|
.trimStart();
|
|
287
289
|
}
|
|
288
290
|
|
|
289
|
-
async function
|
|
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
|
|
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
|
|
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
|
|
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("
|
|
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("
|
|
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?.
|
|
50
|
+
frame = codeBlock.parentElement?.hasAttribute("data-code-block") ? codeBlock.parentElement : null;
|
|
43
51
|
if (!frame) {
|
|
44
52
|
frame = document.createElement("div");
|
|
45
|
-
frame.
|
|
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>
|