mirror of
https://github.com/HoshinoSuzumi/rayine-ui.git
synced 2025-05-23 02:02:27 +08:00
docs framework: tocs, contents, highlighter
This commit is contained in:
parent
ab9fe5f242
commit
a6ee301d5b
11
docs/app.vue
11
docs/app.vue
@ -1,4 +1,7 @@
|
|||||||
<script>
|
<script setup lang="ts">
|
||||||
|
const { data: navigation } = await useAsyncData('navigation', () => fetchContentNavigation())
|
||||||
|
|
||||||
|
provide('navigation', navigation)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -8,3 +11,9 @@
|
|||||||
</NuxtLayout>
|
</NuxtLayout>
|
||||||
</RayMessageProvider>
|
</RayMessageProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -3,12 +3,13 @@ import FileTypeVue from "./icon/VscodeIconsFileTypeVue.vue"
|
|||||||
import FileTypeTypescript from "./icon/VscodeIconsFileTypeTypescriptOfficial.vue"
|
import FileTypeTypescript from "./icon/VscodeIconsFileTypeTypescriptOfficial.vue"
|
||||||
import FileTypeJavascript from "./icon/VscodeIconsFileTypeJsOfficial.vue"
|
import FileTypeJavascript from "./icon/VscodeIconsFileTypeJsOfficial.vue"
|
||||||
import TablerTerminal from "./icon/TablerTerminal.vue";
|
import TablerTerminal from "./icon/TablerTerminal.vue";
|
||||||
|
import { camelCase, kebabCase, upperFirst } from "scule";
|
||||||
|
|
||||||
const hightlighter = useShikiHighlighter();
|
const route = useRoute();
|
||||||
|
const highlighter = useShikiHighlighter();
|
||||||
|
|
||||||
const slots = defineSlots<{
|
const slots = defineSlots<{
|
||||||
default?: () => VNode[];
|
default?: () => VNode[];
|
||||||
code?: () => VNode[];
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const IconComponents = {
|
const IconComponents = {
|
||||||
@ -19,33 +20,19 @@ const IconComponents = {
|
|||||||
'js': FileTypeJavascript,
|
'js': FileTypeJavascript,
|
||||||
}
|
}
|
||||||
|
|
||||||
const codeSlotContent = computed(() => {
|
|
||||||
if (slots.code) {
|
|
||||||
const slotContent = slots.code();
|
|
||||||
let contentLines = slotContent
|
|
||||||
.map(vnode => vnode.children || '')
|
|
||||||
.join('')
|
|
||||||
.replace('\n', '') // remove first line break
|
|
||||||
.split('\n');
|
|
||||||
|
|
||||||
// calculate the minimum indent
|
|
||||||
const minIndent = contentLines.reduce((min, line) => {
|
|
||||||
const match = line.match(/^(\s*)\S/);
|
|
||||||
if (match) {
|
|
||||||
return Math.min(min, match[1].length);
|
|
||||||
}
|
|
||||||
return min;
|
|
||||||
}, Infinity);
|
|
||||||
|
|
||||||
// remove the minimum indent from each line
|
|
||||||
const stringContent = contentLines.map(line => line.slice(minIndent)).join('\n');
|
|
||||||
|
|
||||||
return stringContent;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
})
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
slug: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
slots: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
filename: {
|
filename: {
|
||||||
type: String,
|
type: String,
|
||||||
default: '',
|
default: '',
|
||||||
@ -54,13 +41,27 @@ const props = defineProps({
|
|||||||
type: String as PropType<keyof typeof IconComponents>,
|
type: String as PropType<keyof typeof IconComponents>,
|
||||||
default: '',
|
default: '',
|
||||||
},
|
},
|
||||||
code: {
|
|
||||||
type: String,
|
|
||||||
default: '',
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: ast } = await useAsyncData(`${'name'}-ast-${JSON.stringify({ slots: props.slots, code: props.code })}`, async () => {
|
const componentName = props.slug || `Ray${upperFirst(camelCase(route.params.slug[route.params.slug.length - 1]))}`
|
||||||
|
const componentProps = reactive({ ...props.props })
|
||||||
|
|
||||||
|
const code = computed(() => {
|
||||||
|
let code = `\`\`\`html
|
||||||
|
<template>
|
||||||
|
<${componentName}`
|
||||||
|
|
||||||
|
for (const [k, v] of Object.entries(componentProps)) {
|
||||||
|
code += ` ${typeof v === 'boolean' || typeof v === 'number' || typeof v === 'object' ? ':' : ''}${kebabCase(k)}="${typeof v === 'object' ? renderObject(v) : v}"`
|
||||||
|
}
|
||||||
|
|
||||||
|
code += `/>\n</template>
|
||||||
|
\`\`\`
|
||||||
|
`
|
||||||
|
return code;
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data: codeRender } = await useAsyncData(`${componentName}-renderer-${JSON.stringify({ slots: slots, code: code.value })}`, async () => {
|
||||||
let formatted = ''
|
let formatted = ''
|
||||||
try {
|
try {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@ -70,24 +71,25 @@ const { data: ast } = await useAsyncData(`${'name'}-ast-${JSON.stringify({ slots
|
|||||||
singleQuote: true
|
singleQuote: true
|
||||||
})
|
})
|
||||||
} catch {
|
} catch {
|
||||||
formatted = props.code
|
formatted = code.value
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseMarkdown(formatted, {
|
return parseMarkdown(formatted, {
|
||||||
highlight: {
|
highlight: {
|
||||||
highlighter,
|
highlighter,
|
||||||
theme: {
|
theme: {
|
||||||
light: 'material-theme-lighter',
|
light: 'light-plus',
|
||||||
default: 'material-theme',
|
dark: 'dark-plus'
|
||||||
dark: 'material-theme-palenight'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}, {
|
||||||
|
watch: [code]
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="border border-neutral-200 dark:border-neutral-700 rounded-lg">
|
<div class="border border-neutral-200 dark:border-neutral-700 rounded-lg not-prose my-2 overflow-hidden">
|
||||||
<div v-if="filename" class="p-4 py-2 border-b border-neutral-200 dark:border-neutral-700">
|
<div v-if="filename" class="p-4 py-2 border-b border-neutral-200 dark:border-neutral-700">
|
||||||
<span class="flex items-center gap-1">
|
<span class="flex items-center gap-1">
|
||||||
<component v-if="lang" :is="IconComponents[lang]" class="inline" />
|
<component v-if="lang" :is="IconComponents[lang]" class="inline" />
|
||||||
@ -95,16 +97,15 @@ const { data: ast } = await useAsyncData(`${'name'}-ast-${JSON.stringify({ slots
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-if="slots.default">
|
<div :class="['p-4 overflow-auto', !!codeRender ? 'border-b border-neutral-200 dark:border-neutral-700' : '']">
|
||||||
<div :class="['p-4 overflow-auto', $slots.code ? 'border-b border-neutral-200 dark:border-neutral-700' : '']">
|
<component :is="componentName" v-bind="componentProps">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</component>
|
||||||
</template>
|
</div>
|
||||||
|
|
||||||
<template v-if="slots.code">
|
<template v-if="codeRender">
|
||||||
<div class="p-4 overflow-auto">
|
<div class="overflow-auto">
|
||||||
<!-- <LazyShiki class="text-sm" :lang="lang" :code="codeSlotContent" /> -->
|
<ContentRenderer :value="codeRender" v-if="codeRender" class="p-4 bg-neutral-50 dark:bg-neutral-800/50" />
|
||||||
<ContentRenderer :value="ast" v-if="ast"/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
@ -4,11 +4,12 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header class="w-full flex justify-between items-center py-2 border-b border-b-neutral-100 dark:border-b-neutral-800">
|
<header
|
||||||
<div class="text-neutral-900 dark:text-neutral-100">
|
class="w-full flex justify-between items-center py-2 border-b border-b-neutral-100 dark:border-b-neutral-800 h-16 z-50 sticky top-0 bg-white/90 dark:bg-neutral-900">
|
||||||
|
<NuxtLink to="/" class="text-neutral-900 dark:text-neutral-100">
|
||||||
<h1 class="font-medium text-xl">RayineSoft<sup class="text-sm"> ©</sup></h1>
|
<h1 class="font-medium text-xl">RayineSoft<sup class="text-sm"> ©</sup></h1>
|
||||||
<h2 class="font-normal text-xs">Common Components</h2>
|
<h2 class="font-normal text-xs">Common Components</h2>
|
||||||
</div>
|
</NuxtLink>
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<NuxtLink to="/" class="text-neutral-400 dark:text-neutral-500"
|
<NuxtLink to="/" class="text-neutral-400 dark:text-neutral-500"
|
||||||
active-class="!text-neutral-700 dark:!text-neutral-300">Docs</NuxtLink>
|
active-class="!text-neutral-700 dark:!text-neutral-300">Docs</NuxtLink>
|
||||||
|
91
docs/components/Toc.vue
Normal file
91
docs/components/Toc.vue
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from "vue";
|
||||||
|
|
||||||
|
interface TocItem {
|
||||||
|
id: string;
|
||||||
|
depth: number;
|
||||||
|
text: string;
|
||||||
|
children?: TocItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
toc: TocItem[];
|
||||||
|
maxDepth?: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const maxDepth = props.maxDepth ?? 3;
|
||||||
|
|
||||||
|
const filteredToc = computed(() => {
|
||||||
|
const filterByDepth = (items: TocItem[]): TocItem[] => {
|
||||||
|
return items
|
||||||
|
.filter(item => item.depth <= maxDepth)
|
||||||
|
.map(item => ({
|
||||||
|
...item,
|
||||||
|
children: item.children ? filterByDepth(item.children) : undefined,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return filterByDepth(props.toc);
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeLinks = ref<string[]>([]);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
activeLinks.value.push(entry.target.id);
|
||||||
|
} else {
|
||||||
|
activeLinks.value = activeLinks.value.filter(link => link !== entry.target.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// { rootMargin: '0px 0px -80% 0px' }
|
||||||
|
);
|
||||||
|
|
||||||
|
filteredToc.value.forEach(item => {
|
||||||
|
const element = document.getElementById(item.id);
|
||||||
|
if (element) {
|
||||||
|
observer.observe(element);
|
||||||
|
}
|
||||||
|
item.children?.forEach(child => {
|
||||||
|
const childElement = document.getElementById(child.id);
|
||||||
|
if (childElement) {
|
||||||
|
observer.observe(childElement);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ul>
|
||||||
|
<template v-for="item in filteredToc" :key="item.id">
|
||||||
|
<li>
|
||||||
|
<NuxtLink :href="'#' + item.id" class="link" :class="{ 'active': activeLinks.includes(item.id) }">
|
||||||
|
{{ item.text }}
|
||||||
|
</NuxtLink>
|
||||||
|
<ul v-if="item.children && item.children.length" class="ml-4">
|
||||||
|
<template v-for="child in item.children" :key="child.id">
|
||||||
|
<li>
|
||||||
|
<NuxtLink :href="'#' + child.id" class="link" :class="{ 'active': activeLinks.includes(child.id) }">
|
||||||
|
{{ child.text }}
|
||||||
|
</NuxtLink>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.link {
|
||||||
|
@apply text-xs text-neutral-400 dark:text-neutral-500 font-medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link.active {
|
||||||
|
@apply text-primary;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,7 +1,8 @@
|
|||||||
import { createShikiHighlighter } from "@nuxtjs/mdc/runtime/highlighter/shiki";
|
import { createShikiHighlighter } from "@nuxtjs/mdc/runtime/highlighter/shiki";
|
||||||
import MaterialTheme from "shiki/themes/material-theme.mjs";
|
import MaterialTheme from "shiki/themes/material-theme.mjs";
|
||||||
import MaterialThemeLighter from "shiki/themes/material-theme-lighter.mjs";
|
import MaterialThemeLighter from "shiki/themes/material-theme-lighter.mjs";
|
||||||
import MaterialThemePalenight from "shiki/themes/material-theme-palenight.mjs";
|
import LightPlus from "shiki/themes/light-plus.mjs";
|
||||||
|
import DarkPlus from "shiki/themes/dark-plus.mjs";
|
||||||
import HtmlLang from "shiki/langs/html.mjs";
|
import HtmlLang from "shiki/langs/html.mjs";
|
||||||
import MdcLang from "shiki/langs/mdc.mjs";
|
import MdcLang from "shiki/langs/mdc.mjs";
|
||||||
import VueLang from "shiki/langs/vue.mjs";
|
import VueLang from "shiki/langs/vue.mjs";
|
||||||
@ -14,9 +15,10 @@ export const useShikiHighlighter = () => {
|
|||||||
if (!highlighter) {
|
if (!highlighter) {
|
||||||
highlighter = createShikiHighlighter({
|
highlighter = createShikiHighlighter({
|
||||||
bundledThemes: {
|
bundledThemes: {
|
||||||
"material-theme": MaterialTheme,
|
|
||||||
"material-theme-lighter": MaterialThemeLighter,
|
"material-theme-lighter": MaterialThemeLighter,
|
||||||
"material-theme-palenight": MaterialThemePalenight,
|
"material-theme": MaterialTheme,
|
||||||
|
"light-plus": LightPlus,
|
||||||
|
"dark-plus": DarkPlus,
|
||||||
},
|
},
|
||||||
bundledLangs: {
|
bundledLangs: {
|
||||||
html: HtmlLang,
|
html: HtmlLang,
|
||||||
|
@ -1,25 +1,31 @@
|
|||||||
# Button
|
---
|
||||||
|
description: Create a button component with different variants and colors
|
||||||
Buttons are used to trigger an action or event, such as submitting a form, opening a dialog, canceling an action, or performing a delete operation.
|
---
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```html
|
Default button style
|
||||||
<RayButton>Click me</RayButton>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
::ComponentPreview
|
||||||
|
Button
|
||||||
::DocExampleBlock
|
|
||||||
test
|
|
||||||
|
|
||||||
#code
|
|
||||||
console.log('Hello Rayine')
|
|
||||||
::
|
::
|
||||||
|
|
||||||
::RayButton
|
### Variants
|
||||||
|
|
||||||
|
::ComponentPreview
|
||||||
---
|
---
|
||||||
color: red
|
props:
|
||||||
|
variant: soft
|
||||||
---
|
---
|
||||||
Hello Rayine
|
Button
|
||||||
|
::
|
||||||
|
|
||||||
|
### Colors
|
||||||
|
|
||||||
|
::ComponentPreview
|
||||||
|
---
|
||||||
|
props:
|
||||||
|
color: violet
|
||||||
|
---
|
||||||
|
Button
|
||||||
::
|
::
|
||||||
|
@ -1,18 +1 @@
|
|||||||
::DocContentBlock
|
# index
|
||||||
---
|
|
||||||
title: Button
|
|
||||||
accent-title: true
|
|
||||||
---
|
|
||||||
::
|
|
||||||
|
|
||||||
::DocContentBlock
|
|
||||||
---
|
|
||||||
title: Variants
|
|
||||||
---
|
|
||||||
::
|
|
||||||
|
|
||||||
::DocExampleBlock
|
|
||||||
---
|
|
||||||
lang: vue
|
|
||||||
---
|
|
||||||
::
|
|
||||||
|
@ -18,11 +18,13 @@ body {
|
|||||||
@apply bg-white dark:bg-neutral-900 text-neutral-900 dark:text-neutral-100;
|
@apply bg-white dark:bg-neutral-900 text-neutral-900 dark:text-neutral-100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shiki,
|
/* .shiki,
|
||||||
.shiki span {
|
.shiki span {
|
||||||
background-color: rgba(0, 0, 0, 0) !important;
|
background-color: rgba(0, 0, 0, 0) !important;
|
||||||
}
|
} */
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
|
|
||||||
.shiki,
|
.shiki,
|
||||||
@ -34,5 +36,5 @@ body {
|
|||||||
font-weight: var(--shiki-dark-font-weight) !important;
|
font-weight: var(--shiki-dark-font-weight) !important;
|
||||||
text-decoration: var(--shiki-dark-text-decoration) !important;
|
text-decoration: var(--shiki-dark-text-decoration) !important;
|
||||||
}
|
}
|
||||||
}
|
} */
|
||||||
</style>
|
</style>
|
||||||
|
@ -4,15 +4,15 @@ import defaultTheme from "tailwindcss/defaultTheme";
|
|||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: "2024-04-03",
|
compatibilityDate: "2024-04-03",
|
||||||
modules: ["@nuxt/content", "@nuxt/fonts", module],
|
modules: ["@nuxt/content", "@nuxt/fonts", "@nuxtjs/color-mode", module],
|
||||||
devtools: { enabled: true },
|
devtools: { enabled: true },
|
||||||
rayui: {
|
rayui: {
|
||||||
|
// @ts-ignore
|
||||||
globalComponents: true,
|
globalComponents: true,
|
||||||
safeColors: ["amber", "emerald", "red", "sky", "violet", "cyan"],
|
safeColors: ["amber", "emerald", "red", "sky", "violet", "cyan"],
|
||||||
},
|
},
|
||||||
tailwindcss: {
|
tailwindcss: {
|
||||||
config: {
|
config: {
|
||||||
darkMode: "media",
|
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
@ -22,6 +22,10 @@ export default defineNuxtConfig({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
colorMode: {
|
||||||
|
preference: "system",
|
||||||
|
classSuffix: "",
|
||||||
|
},
|
||||||
components: [
|
components: [
|
||||||
{
|
{
|
||||||
path: "~/components",
|
path: "~/components",
|
||||||
@ -33,6 +37,11 @@ export default defineNuxtConfig({
|
|||||||
langs: ["postcss", "mdc", "html", "vue", "ts", "js"],
|
langs: ["postcss", "mdc", "html", "vue", "ts", "js"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
mdc: {
|
||||||
|
highlight: {
|
||||||
|
themes: ["material-theme-lighter", "material-theme", "light-plus", "dark-plus"],
|
||||||
|
},
|
||||||
|
},
|
||||||
typescript: {
|
typescript: {
|
||||||
includeWorkspace: true,
|
includeWorkspace: true,
|
||||||
},
|
},
|
||||||
|
@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxt/content": "^2.13.4",
|
"@nuxt/content": "^2.13.4",
|
||||||
|
"@nuxtjs/color-mode": "^3.5.2",
|
||||||
"@nuxtjs/mdc": "^0.9.2",
|
"@nuxtjs/mdc": "^0.9.2",
|
||||||
"nuxt": "^3.14.159",
|
"nuxt": "^3.14.159",
|
||||||
"nuxt-shiki": "^0.3.0",
|
"nuxt-shiki": "^0.3.0",
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import type { NavItem } from '@nuxt/content';
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
|
|
||||||
const { data: page } = await useAsyncData(route.path, () => queryContent(route.path).findOne())
|
const { data: page } = await useAsyncData(route.path, () => queryContent(route.path).findOne())
|
||||||
@ -8,17 +10,41 @@ if (!page.value) {
|
|||||||
statusCode: 404, statusMessage: 'Page not found', fatal: true
|
statusCode: 404, statusMessage: 'Page not found', fatal: true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nav = inject<Ref<NavItem[]>>('navigation')
|
||||||
|
const navigation = computed(() => nav?.value)
|
||||||
|
|
||||||
|
console.log(navigation.value);
|
||||||
|
|
||||||
|
const hasToc = computed(() => page.value?.body?.toc && page.value?.body?.toc?.links.length !== 0)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="grid grid-cols-12 gap-4 pb-10">
|
||||||
<ContentRenderer v-if="page.body" :value="page" />
|
<div class="col-span-12" :class="{ 'md:col-span-9': hasToc }">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-3xl text-primary font-medium">{{ page?.title || 'untitled' }}</h1>
|
||||||
|
<p v-if="page?.description" class="text-lg text-neutral-500 mt-2">{{ page.description }}</p>
|
||||||
|
</div>
|
||||||
|
<hr class="my-4 dark:border-neutral-700" />
|
||||||
|
<div class="doc-body">
|
||||||
|
<ContentRenderer v-if="page?.body" :value="page" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="hasToc" class="hidden" :class="{ 'col-span-3 md:block': hasToc }">
|
||||||
|
<div class="bg-neutral-50 dark:bg-neutral-800/50 rounded-lg px-4 py-3 overflow-hidden overflow-y-auto sticky top-[calc(64px+16px)]">
|
||||||
|
<span class="text-xs text-neutral-600 dark:text-neutral-300 font-medium inline-block mb-2">
|
||||||
|
Table of contents
|
||||||
|
</span>
|
||||||
|
<Toc :toc="page!.body!.toc!.links" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.doc-body {
|
.doc-body {
|
||||||
@apply prose dark:prose-invert max-w-none;
|
@apply prose prose-neutral dark:prose-invert max-w-none prose-headings:no-underline;
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
@apply my-8 border-t border-neutral-200 dark:border-neutral-700;
|
@apply my-8 border-t border-neutral-200 dark:border-neutral-700;
|
||||||
@ -28,8 +54,16 @@ if (!page.value) {
|
|||||||
@apply text-3xl text-primary font-bold my-4 first:mt-0;
|
@apply text-3xl text-primary font-bold my-4 first:mt-0;
|
||||||
}
|
}
|
||||||
|
|
||||||
& > p {
|
h2 a,
|
||||||
@apply text-base text-neutral-900 dark:text-neutral-100;
|
h3 a,
|
||||||
|
h4 a,
|
||||||
|
h5 a,
|
||||||
|
h6 a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&>p {
|
||||||
|
@apply text-base text-justify text-neutral-900 dark:text-neutral-100;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
17
docs/utils/index.ts
Normal file
17
docs/utils/index.ts
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
export const renderObject = (obj: any): string => {
|
||||||
|
if (Array.isArray(obj)) {
|
||||||
|
return `[${obj.map(renderObject).join(", ")}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === "object") {
|
||||||
|
return `{ ${Object.entries(obj)
|
||||||
|
.map(([key, value]) => `${key}: ${renderObject(value)}`)
|
||||||
|
.join(", ")} }`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj === "string") {
|
||||||
|
return `'${obj}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
};
|
17
pnpm-lock.yaml
generated
17
pnpm-lock.yaml
generated
@ -78,6 +78,9 @@ importers:
|
|||||||
'@nuxt/content':
|
'@nuxt/content':
|
||||||
specifier: ^2.13.4
|
specifier: ^2.13.4
|
||||||
version: 2.13.4(ioredis@5.4.1)(magicast@0.3.5)(nuxt@3.14.159(@parcel/watcher@2.5.0)(@types/node@22.9.0)(eslint@9.15.0(jiti@2.4.0))(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.27.2)(terser@5.36.0)(typescript@5.6.3)(vite@5.4.11(@types/node@22.9.0)(terser@5.36.0))(vue-tsc@2.1.10(typescript@5.6.3)))(rollup@4.27.2)(vue@3.5.13(typescript@5.6.3))
|
version: 2.13.4(ioredis@5.4.1)(magicast@0.3.5)(nuxt@3.14.159(@parcel/watcher@2.5.0)(@types/node@22.9.0)(eslint@9.15.0(jiti@2.4.0))(ioredis@5.4.1)(magicast@0.3.5)(optionator@0.9.4)(rollup@4.27.2)(terser@5.36.0)(typescript@5.6.3)(vite@5.4.11(@types/node@22.9.0)(terser@5.36.0))(vue-tsc@2.1.10(typescript@5.6.3)))(rollup@4.27.2)(vue@3.5.13(typescript@5.6.3))
|
||||||
|
'@nuxtjs/color-mode':
|
||||||
|
specifier: ^3.5.2
|
||||||
|
version: 3.5.2(magicast@0.3.5)(rollup@4.27.2)
|
||||||
'@nuxtjs/mdc':
|
'@nuxtjs/mdc':
|
||||||
specifier: ^0.9.2
|
specifier: ^0.9.2
|
||||||
version: 0.9.2(magicast@0.3.5)(rollup@4.27.2)
|
version: 0.9.2(magicast@0.3.5)(rollup@4.27.2)
|
||||||
@ -950,6 +953,9 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: ^3.3.4
|
vue: ^3.3.4
|
||||||
|
|
||||||
|
'@nuxtjs/color-mode@3.5.2':
|
||||||
|
resolution: {integrity: sha512-cC6RfgZh3guHBMLLjrBB2Uti5eUoGM9KyauOaYS9ETmxNWBMTvpgjvSiSJp1OFljIXPIqVTJ3xtJpSNZiO3ZaA==}
|
||||||
|
|
||||||
'@nuxtjs/mdc@0.9.2':
|
'@nuxtjs/mdc@0.9.2':
|
||||||
resolution: {integrity: sha512-dozIPTPjEYu8jChHNCICZP3mN0sFC6l3aLxTkgv/DAr1EI8jqqqoSZKevzuiHUWGNTguS70+fLcztCwrzWdoYA==}
|
resolution: {integrity: sha512-dozIPTPjEYu8jChHNCICZP3mN0sFC6l3aLxTkgv/DAr1EI8jqqqoSZKevzuiHUWGNTguS70+fLcztCwrzWdoYA==}
|
||||||
|
|
||||||
@ -6276,6 +6282,17 @@ snapshots:
|
|||||||
- vti
|
- vti
|
||||||
- vue-tsc
|
- vue-tsc
|
||||||
|
|
||||||
|
'@nuxtjs/color-mode@3.5.2(magicast@0.3.5)(rollup@4.27.2)':
|
||||||
|
dependencies:
|
||||||
|
'@nuxt/kit': 3.14.159(magicast@0.3.5)(rollup@4.27.2)
|
||||||
|
pathe: 1.1.2
|
||||||
|
pkg-types: 1.2.1
|
||||||
|
semver: 7.6.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- magicast
|
||||||
|
- rollup
|
||||||
|
- supports-color
|
||||||
|
|
||||||
'@nuxtjs/mdc@0.9.2(magicast@0.3.5)(rollup@4.27.2)':
|
'@nuxtjs/mdc@0.9.2(magicast@0.3.5)(rollup@4.27.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nuxt/kit': 3.14.159(magicast@0.3.5)(rollup@4.27.2)
|
'@nuxt/kit': 3.14.159(magicast@0.3.5)(rollup@4.27.2)
|
||||||
|
Loading…
Reference in New Issue
Block a user