Browse Source

feat(@vben/docs): preview components are supported within documents (#4250)

Vben 8 months ago
parent
commit
cbf601581d

+ 43 - 0
docs/.vitepress/components/demo-preview.vue

@@ -0,0 +1,43 @@
+<script setup lang="ts">
+import { computed } from 'vue';
+
+import PreviewGroup from './preview-group.vue';
+
+interface Props {
+  files?: string;
+}
+
+const props = withDefaults(defineProps<Props>(), { files: '() => []' });
+
+const parsedFiles = computed(() => {
+  try {
+    return JSON.parse(decodeURIComponent(props.files ?? ''));
+  } catch {
+    return [];
+  }
+});
+</script>
+
+<template>
+  <div class="border-border shadow-float relative rounded-xl border">
+    <div
+      class="not-prose relative w-full overflow-x-auto rounded-t-lg px-4 py-6"
+    >
+      <div class="flex w-full max-w-[700px] px-2">
+        <slot v-if="parsedFiles.length > 0"></slot>
+        <div v-else class="text-destructive text-sm">
+          <span class="bg-destructive text-foreground rounded-sm px-1 py-1">
+            ERROR:
+          </span>
+          The preview directory does not exist. Please check the 'dir'
+          parameter.
+        </div>
+      </div>
+    </div>
+    <PreviewGroup v-if="parsedFiles.length > 0" :files="parsedFiles">
+      <template v-for="file in parsedFiles" #[file]>
+        <slot :name="file"></slot>
+      </template>
+    </PreviewGroup>
+  </div>
+</template>

+ 1 - 0
docs/.vitepress/components/index.ts

@@ -0,0 +1 @@
+export { default as DemoPreview } from './demo-preview.vue';

+ 108 - 0
docs/.vitepress/components/preview-group.vue

@@ -0,0 +1,108 @@
+<script setup lang="ts">
+import { computed, ref, useSlots } from 'vue';
+
+import { VbenTooltip } from '@vben-core/shadcn-ui';
+
+import { Code } from 'lucide-vue-next';
+import {
+  TabsContent,
+  TabsIndicator,
+  TabsList,
+  TabsRoot,
+  TabsTrigger,
+} from 'radix-vue';
+
+defineOptions({
+  inheritAttrs: false,
+});
+
+const props = withDefaults(
+  defineProps<{
+    files?: string[];
+  }>(),
+  { files: () => [] },
+);
+
+const open = ref(false);
+
+const slots = useSlots();
+
+const tabs = computed(() => {
+  return props.files.map((file) => {
+    return {
+      component: slots[file],
+      label: file,
+    };
+  });
+});
+
+const currentTab = ref('index.vue');
+
+const toggleOpen = () => {
+  open.value = !open.value;
+};
+</script>
+
+<template>
+  <TabsRoot
+    v-model="currentTab"
+    class="bg-background-deep border-border overflow-hidden rounded-b-xl border-t"
+    @update:model-value="open = true"
+  >
+    <div class="border-border bg-background flex border-b-2 pr-2">
+      <div class="flex w-full items-center justify-between text-[13px]">
+        <TabsList class="relative flex">
+          <template v-if="open">
+            <TabsIndicator
+              class="absolute bottom-0 left-0 h-[2px] w-[--radix-tabs-indicator-size] translate-x-[--radix-tabs-indicator-position] rounded-full transition-[width,transform] duration-300"
+            >
+              <div class="size-full bg-[var(--vp-c-indigo-1)]"></div>
+            </TabsIndicator>
+            <TabsTrigger
+              v-for="(tab, index) in tabs"
+              :key="index"
+              :value="tab.label"
+              class="border-box text-foreground px-4 py-3 data-[state=active]:text-[var(--vp-c-indigo-1)]"
+              tabindex="-1"
+            >
+              {{ tab.label }}
+            </TabsTrigger>
+          </template>
+        </TabsList>
+
+        <div
+          :class="{
+            'py-2': !open,
+          }"
+          class="flex items-center"
+        >
+          <VbenTooltip side="top">
+            <template #trigger>
+              <Code
+                class="hover:bg-accent size-6.5 cursor-pointer rounded-full p-1.5"
+                @click="toggleOpen"
+              />
+            </template>
+            {{ open ? 'Collapse code' : 'Expand code' }}
+          </VbenTooltip>
+        </div>
+      </div>
+    </div>
+    <div
+      :class="`${open ? 'h-[unset] max-h-[80vh]' : 'h-0'}`"
+      class="block overflow-y-scroll bg-[var(--vp-code-block-bg)] transition-all duration-300"
+    >
+      <TabsContent
+        v-for="tab in tabs"
+        :key="tab.label"
+        :value="tab.label"
+        as-child
+        class="rounded-xl"
+      >
+        <div class="text-foreground relative rounded-xl">
+          <component :is="tab.component" class="border-0" />
+        </div>
+      </TabsContent>
+    </div>
+  </TabsRoot>
+</template>

+ 1 - 1
docs/.vitepress/config/en.mts

@@ -203,7 +203,7 @@ function nav(): DefaultTheme.NavItem[] {
     },
     {
       link: '/commercial/technical-support',
-      text: '🦄 Technical Support',
+      text: '🦄 Tech Support',
     },
     {
       link: '/sponsor/personal',

+ 2 - 2
docs/.vitepress/config/index.mts

@@ -2,12 +2,12 @@ import { withPwa } from '@vite-pwa/vitepress';
 import { defineConfigWithTheme } from 'vitepress';
 
 import { en } from './en.mts';
-import { shard } from './shard.mts';
+import { shared } from './shared.mts';
 import { zh } from './zh.mts';
 
 export default withPwa(
   defineConfigWithTheme({
-    ...shard,
+    ...shared,
     locales: {
       en: {
         label: 'English',

+ 135 - 0
docs/.vitepress/config/plugins/demo-preview.ts

@@ -0,0 +1,135 @@
+import type { MarkdownEnv, MarkdownRenderer } from 'vitepress';
+
+import crypto from 'node:crypto';
+import { readdirSync } from 'node:fs';
+import { join } from 'node:path';
+
+export const rawPathRegexp =
+  // eslint-disable-next-line regexp/no-super-linear-backtracking, regexp/strict
+  /^(.+?(?:\.([\da-z]+))?)(#[\w-]+)?(?: ?{(\d+(?:[,-]\d+)*)? ?(\S+)?})? ?(?:\[(.+)])?$/;
+
+function rawPathToToken(rawPath: string) {
+  const [
+    filepath = '',
+    extension = '',
+    region = '',
+    lines = '',
+    lang = '',
+    rawTitle = '',
+  ] = (rawPathRegexp.exec(rawPath) || []).slice(1);
+
+  const title = rawTitle || filepath.split('/').pop() || '';
+
+  return { extension, filepath, lang, lines, region, title };
+}
+
+export const demoPreviewPlugin = (md: MarkdownRenderer) => {
+  md.core.ruler.after('inline', 'demo-preview', (state) => {
+    const insertComponentImport = (importString: string) => {
+      const index = state.tokens.findIndex(
+        (i) => i.type === 'html_block' && i.content.match(/<script setup>/g),
+      );
+      if (index === -1) {
+        const importComponent = new state.Token('html_block', '', 0);
+        importComponent.content = `<script setup>\n${importString}\n</script>\n`;
+        state.tokens.splice(0, 0, importComponent);
+      } else {
+        if (state.tokens[index]) {
+          const content = state.tokens[index].content;
+          state.tokens[index].content = content.replace(
+            '</script>',
+            `${importString}\n</script>`,
+          );
+        }
+      }
+    };
+    // Define the regular expression to match the desired pattern
+    const regex = /<DemoPreview[^>]*\sdir="([^"]*)"/g;
+    // Iterate through the Markdown content and replace the pattern
+    state.src = state.src.replaceAll(regex, (_match, dir) => {
+      const componentDir = join(process.cwd(), 'src', dir);
+
+      let childFiles: string[] = [];
+      let dirExists = true;
+
+      try {
+        childFiles =
+          readdirSync(componentDir, {
+            encoding: 'utf8',
+            recursive: false,
+            withFileTypes: false,
+          }) || [];
+      } catch {
+        dirExists = false;
+      }
+
+      if (!dirExists) {
+        return '';
+      }
+
+      const uniqueWord = generateContentHash(componentDir);
+
+      const ComponentName = `DemoComponent_${uniqueWord}`;
+      insertComponentImport(
+        `import ${ComponentName} from '${componentDir}/index.vue'`,
+      );
+      const { path: _path } = state.env as MarkdownEnv;
+
+      const index = state.tokens.findIndex((i) => i.content.match(regex));
+
+      if (!state.tokens[index]) {
+        return '';
+      }
+
+      state.tokens[index].content =
+        `<DemoPreview files="${encodeURIComponent(JSON.stringify(childFiles))}" ><${ComponentName}/>
+        `;
+
+      const _dummyToken = new state.Token('', '', 0);
+      const tokenArray: Array<typeof _dummyToken> = [];
+      childFiles.forEach((filename) => {
+        // const slotName = filename.replace(extname(filename), '');
+
+        const templateStart = new state.Token('html_inline', '', 0);
+        templateStart.content = `<template #${filename}>`;
+        tokenArray.push(templateStart);
+
+        const resolvedPath = join(componentDir, filename);
+
+        const { extension, filepath, lang, lines, title } =
+          rawPathToToken(resolvedPath);
+        // Add code tokens for each line
+        const token = new state.Token('fence', 'code', 0);
+        token.info = `${lang || extension}${lines ? `{${lines}}` : ''}${
+          title ? `[${title}]` : ''
+        }`;
+
+        token.content = `<<< ${filepath}`;
+        (token as any).src = [resolvedPath];
+        tokenArray.push(token);
+
+        const templateEnd = new state.Token('html_inline', '', 0);
+        templateEnd.content = '</template>';
+        tokenArray.push(templateEnd);
+      });
+      const endTag = new state.Token('html_inline', '', 0);
+      endTag.content = '</DemoPreview>';
+      tokenArray.push(endTag);
+
+      state.tokens.splice(index + 1, 0, ...tokenArray);
+
+      // console.log(
+      //   state.md.renderer.render(state.tokens, state?.options ?? [], state.env),
+      // );
+      return '';
+    });
+  });
+};
+
+function generateContentHash(input: string, length: number = 10): string {
+  // 使用 SHA-256 生成哈希值
+  const hash = crypto.createHash('sha256').update(input).digest('hex');
+
+  // 将哈希值转换为 Base36 编码,并取指定长度的字符作为结果
+  return Number.parseInt(hash, 16).toString(36).slice(0, length);
+}

+ 34 - 2
docs/.vitepress/config/shard.mts → docs/.vitepress/config/shared.mts

@@ -1,4 +1,5 @@
 import type { PwaOptions } from '@vite-pwa/vitepress';
+import type { HeadConfig } from 'vitepress';
 
 import { resolve } from 'node:path';
 
@@ -6,12 +7,20 @@ import {
   GitChangelog,
   GitChangelogMarkdownSection,
 } from '@nolebase/vitepress-plugin-git-changelog/vite';
-import { defineConfig, type HeadConfig } from 'vitepress';
+import tailwind from 'tailwindcss';
+import { defineConfig, postcssIsolateStyles } from 'vitepress';
 
+import { demoPreviewPlugin } from './plugins/demo-preview';
 import { search as zhSearch } from './zh.mts';
 
-export const shard = defineConfig({
+export const shared = defineConfig({
+  appearance: 'dark',
   head: head(),
+  markdown: {
+    preConfig(md) {
+      md.use(demoPreviewPlugin);
+    },
+  },
   pwa: pwa(),
   srcDir: 'src',
   themeConfig: {
@@ -36,11 +45,34 @@ export const shard = defineConfig({
       chunkSizeWarningLimit: Infinity,
       minify: 'terser',
     },
+    css: {
+      postcss: {
+        plugins: [
+          tailwind(),
+          postcssIsolateStyles({ includeFiles: [/vp-doc\.css/] }),
+        ],
+      },
+    },
     json: {
       stringify: true,
     },
     plugins: [
       GitChangelog({
+        mapAuthors: [
+          {
+            mapByNameAliases: ['Vben'],
+            name: 'vben',
+            username: 'anncwb',
+          },
+          {
+            name: 'vince',
+            username: 'vince292007',
+          },
+          {
+            name: 'Li Kui',
+            username: 'likui628',
+          },
+        ],
         repoURL: () => 'https://github.com/vbenjs/vue-vben-admin',
       }),
       GitChangelogMarkdownSection(),

+ 36 - 24
docs/.vitepress/config/zh.mts

@@ -38,6 +38,7 @@ export const zh = defineConfig({
 
     sidebar: {
       '/commercial/': { base: '/commercial/', items: sidebarCommercial() },
+      '/components/': { base: '/components/', items: sidebarComponents() },
       '/guide/': { base: '/guide/', items: sidebarGuide() },
     },
     sidebarMenuLabel: '菜单',
@@ -60,6 +61,11 @@ function sidebarGuide(): DefaultTheme.SidebarItem[] {
         },
         { link: 'introduction/quick-start', text: '快速开始' },
         { link: 'introduction/thin', text: '精简版本' },
+        {
+          base: '/',
+          link: 'components/introduction',
+          text: '组件文档',
+        },
       ],
     },
     {
@@ -117,7 +123,7 @@ function sidebarCommercial(): DefaultTheme.SidebarItem[] {
   return [
     {
       link: 'community',
-      text: '社区交流',
+      text: '社区',
     },
     {
       link: 'technical-support',
@@ -130,6 +136,30 @@ function sidebarCommercial(): DefaultTheme.SidebarItem[] {
   ];
 }
 
+function sidebarComponents(): DefaultTheme.SidebarItem[] {
+  return [
+    {
+      text: '组件',
+      items: [
+        {
+          link: 'introduction',
+          text: '介绍',
+        },
+      ],
+    },
+    {
+      collapsed: false,
+      text: '通用组件',
+      items: [
+        {
+          link: 'common-ui/vben-modal',
+          text: 'Modal 弹窗',
+        },
+      ],
+    },
+  ];
+}
+
 function nav(): DefaultTheme.NavItem[] {
   return [
     {
@@ -138,28 +168,10 @@ function nav(): DefaultTheme.NavItem[] {
         {
           link: '/guide/introduction/vben',
           text: '指南',
-          // items: [
-          //   {
-          //     link: '/guide/introduction/vben',
-          //     text: '简介',
-          //   },
-          //   {
-          //     link: '/guide/essentials/concept',
-          //     text: '基础',
-          //   },
-          //   {
-          //     link: '/guide/in-depth/layout',
-          //     text: '深入',
-          //   },
-          //   {
-          //     link: '/guide/project/standard',
-          //     text: '工程',
-          //   },
-          //   {
-          //     link: '/guide/other/project-update',
-          //     text: '其他',
-          //   },
-          // ],
+        },
+        {
+          link: '/components/introduction',
+          text: '组件',
         },
         {
           text: '历史版本',
@@ -234,7 +246,7 @@ function nav(): DefaultTheme.NavItem[] {
     },
     {
       link: '/commercial/community',
-      text: '👨‍👦‍👦 社区交流',
+      text: '👨‍👦‍👦 社区',
       // items: [
       //   {
       //     link: 'https://qun.qq.com/qqweb/qunpro/share?_wv=3&_wwv=128&appChannel=share&inviteCode=22ySzj7pKiw&businessType=9&from=246610&biz=ka&mainSourceId=share&subSourceId=others&jumpsource=shorturl#/pc',

+ 5 - 3
docs/.vitepress/theme/index.ts

@@ -1,9 +1,10 @@
 // https://vitepress.dev/guide/custom-theme
-import type { Theme } from 'vitepress';
+import type { EnhanceAppContext, Theme } from 'vitepress';
 
 import { NolebaseGitChangelogPlugin } from '@nolebase/vitepress-plugin-git-changelog/client';
 import DefaultTheme from 'vitepress/theme';
 
+import { DemoPreview } from '../components';
 import SiteLayout from './components/site-layout.vue';
 import VbenContributors from './components/vben-contributors.vue';
 import { initHmPlugin } from './plugins/hm';
@@ -13,9 +14,10 @@ import './styles';
 import '@nolebase/vitepress-plugin-git-changelog/client/style.css';
 
 export default {
-  enhanceApp({ app }) {
-    // ...
+  enhanceApp(ctx: EnhanceAppContext) {
+    const { app } = ctx;
     app.component('VbenContributors', VbenContributors);
+    app.component('DemoPreview', DemoPreview);
     app.use(NolebaseGitChangelogPlugin);
     // 百度统计
     initHmPlugin();

+ 1 - 0
docs/.vitepress/theme/styles/index.ts

@@ -1,2 +1,3 @@
 import './variables.css';
 import './base.css';
+import '@vben/styles';

+ 9 - 1
docs/package.json

@@ -8,10 +8,18 @@
     "docs:preview": "vitepress preview"
   },
   "dependencies": {
-    "medium-zoom": "^1.1.0"
+    "@vben-core/shadcn-ui": "workspace:*",
+    "@vben/common-ui": "workspace:*",
+    "@vben/styles": "workspace:*",
+    "@vueuse/core": "^11.0.3",
+    "lucide-vue-next": "^0.436.0",
+    "markdown-it": "^14.1.0",
+    "medium-zoom": "^1.1.0",
+    "radix-vue": "^1.9.4"
   },
   "devDependencies": {
     "@nolebase/vitepress-plugin-git-changelog": "^2.4.0",
+    "@types/markdown-it": "^14.1.2",
     "@vite-pwa/vitepress": "^0.5.0",
     "vitepress": "^1.3.4",
     "vue": "^3.4.38"

+ 45 - 0
docs/src/components/common-ui/vben-modal.md

@@ -0,0 +1,45 @@
+---
+outline: deep
+---
+
+# vben-modal
+
+::: tip
+
+文档还在完善中,敬请期待。
+
+:::
+
+框架提供的模态框组件,支持`拖拽`、`全屏`、`自定义`等功能。
+
+## 基础用法
+
+使用 `useVbenModal` 创建最基于的模态框。
+
+<DemoPreview dir="demos/vben-modal/basic" />
+
+## 组件抽离
+
+modal 内的内容一般业务中,会比较复杂,所以我们可以将 modal 内的内容抽离出来。
+
+<DemoPreview dir="demos/vben-modal/extra" />
+
+## API
+
+### 属性
+
+| 属性名 | 描述  | 类型     | 默认值 |
+| ------ | ----- | -------- | ------ |
+| title  | 标题. | `string` | —      |
+
+### 事件
+
+| 事件名 | 描述 | 类型 |
+| ------ | ---- | ---- |
+| TODO   | TODO | TODO |
+
+### 插槽
+
+| 插槽名  | 描述 |
+| ------- | ---- |
+| default | xx.  |

+ 11 - 0
docs/src/components/introduction.md

@@ -0,0 +1,11 @@
+# 介绍
+
+::: tip README
+
+该文档介绍的是框架组件的使用方法、属性、事件等。如果你觉得组件封装的不好,或者不符合你的需求,你可以直接使用原生的组件,或者自己封装一个组件,不需要拘泥于框架提供的组件。我们只是提供了一些常用的组件,方便你快速开发。是否使用,取决于你的需求。
+
+:::
+
+## 通用组件
+
+通用组件是一些常用的组件,比如弹窗、抽屉、表单等。大部分基于 `Tailwind CSS` 实现,可适用于不同 UI 组件库的应用。

+ 11 - 0
docs/src/demos/vben-modal/basic/index.vue

@@ -0,0 +1,11 @@
+<script lang="ts" setup>
+import { useVbenModal, VbenButton } from '@vben/common-ui';
+
+const [Modal, modalApi] = useVbenModal();
+</script>
+<template>
+  <div>
+    <VbenButton @click="() => modalApi.open()">打开弹窗</VbenButton>
+    <Modal title="基础示例"> modal content </Modal>
+  </div>
+</template>

+ 22 - 0
docs/src/demos/vben-modal/extra/index.vue

@@ -0,0 +1,22 @@
+<script lang="ts" setup>
+import { useVbenModal, VbenButton } from '@vben/common-ui';
+
+import ExtraModal from './modal.vue';
+
+const [Modal, modalApi] = useVbenModal({
+  // 链接抽离的组件
+  connectedComponent: ExtraModal,
+});
+
+function openModal() {
+  modalApi.open();
+}
+</script>
+
+<template>
+  <div>
+    <Modal />
+
+    <VbenButton @click="openModal">打开弹窗</VbenButton>
+  </div>
+</template>

+ 8 - 0
docs/src/demos/vben-modal/extra/modal.vue

@@ -0,0 +1,8 @@
+<script lang="ts" setup>
+import { useVbenModal } from '@vben/common-ui';
+
+const [Modal] = useVbenModal();
+</script>
+<template>
+  <Modal title="基础示例"> extra modal content </Modal>
+</template>

+ 2 - 0
docs/src/guide/in-depth/loading.md

@@ -42,3 +42,5 @@ VITE_INJECT_APP_LOADING=false
   <div class="title"><%= VITE_APP_TITLE %></div>
 </div>
 ```
+
+:::

+ 11 - 0
docs/tailwind.config.mjs

@@ -0,0 +1,11 @@
+import tailwindcssConfig from '@vben/tailwind-config';
+
+export default {
+  ...tailwindcssConfig,
+  content: [
+    ...tailwindcssConfig.content,
+    '.vitepress/**/*.{js,mts,ts,vue}',
+    'src/demos/**/*.{js,mts,ts,vue}',
+    'src/**/*.md',
+  ],
+};

+ 8 - 1
docs/tsconfig.json

@@ -1,6 +1,13 @@
 {
   "$schema": "https://json.schemastore.org/tsconfig",
   "extends": "@vben/tsconfig/web.json",
-  "include": [".vitepress/*.mts", ".vitepress/**/*.ts", ".vitepress/**/*.vue"],
+  "include": [
+    ".vitepress/*.mts",
+    ".vitepress/**/*.ts",
+    ".vitepress/**/*.vue",
+    "src/*.mts",
+    "src/**/*.ts",
+    "src/**/*.vue"
+  ],
   "exclude": ["node_modules"]
 }

+ 1 - 1
package.json

@@ -99,7 +99,7 @@
     "node": ">=20",
     "pnpm": ">=9"
   },
-  "packageManager": "pnpm@9.7.1",
+  "packageManager": "pnpm@9.9.0",
   "pnpm": {
     "peerDependencyRules": {
       "allowedVersions": {

+ 2 - 2
packages/@core/ui-kit/shadcn-ui/src/components/tooltip/tooltip.vue

@@ -14,7 +14,7 @@ interface Props {
   contentClass?: any;
   contentStyle?: StyleValue;
   delayDuration?: number;
-  side: TooltipContentProps['side'];
+  side?: TooltipContentProps['side'];
 }
 
 withDefaults(defineProps<Props>(), {
@@ -33,7 +33,7 @@ withDefaults(defineProps<Props>(), {
         :class="contentClass"
         :side="side"
         :style="contentStyle"
-        class="side-content text-popover-foreground bg-popover"
+        class="side-content text-popover-foreground bg-accent rounded-md"
       >
         <slot></slot>
       </TooltipContent>

+ 2 - 0
packages/effects/common-ui/src/components/index.ts

@@ -1,3 +1,5 @@
 export * from './ellipsis-text';
 export * from './page';
 export * from '@vben-core/popup-ui';
+
+export { VbenButton } from '@vben-core/shadcn-ui';

+ 60 - 1
pnpm-lock.yaml

@@ -322,13 +322,37 @@ importers:
 
   docs:
     dependencies:
+      '@vben-core/shadcn-ui':
+        specifier: workspace:*
+        version: link:../packages/@core/ui-kit/shadcn-ui
+      '@vben/common-ui':
+        specifier: workspace:*
+        version: link:../packages/effects/common-ui
+      '@vben/styles':
+        specifier: workspace:*
+        version: link:../packages/styles
+      '@vueuse/core':
+        specifier: ^11.0.3
+        version: 11.0.3(vue@3.4.38(typescript@5.5.4))
+      lucide-vue-next:
+        specifier: ^0.436.0
+        version: 0.436.0(vue@3.4.38(typescript@5.5.4))
+      markdown-it:
+        specifier: ^14.1.0
+        version: 14.1.0
       medium-zoom:
         specifier: ^1.1.0
         version: 1.1.0
+      radix-vue:
+        specifier: ^1.9.4
+        version: 1.9.4(vue@3.4.38(typescript@5.5.4))
     devDependencies:
       '@nolebase/vitepress-plugin-git-changelog':
         specifier: ^2.4.0
         version: 2.4.0(@algolia/client-search@4.24.0)(@types/node@22.5.0)(async-validator@4.2.5)(axios@1.7.5)(nprogress@0.2.0)(postcss@8.4.41)(qrcode@1.5.4)(sass@1.77.8)(search-insights@2.16.3)(sortablejs@1.15.2)(terser@5.31.6)(typescript@5.5.4)
+      '@types/markdown-it':
+        specifier: ^14.1.2
+        version: 14.1.2
       '@vite-pwa/vitepress':
         specifier: ^0.5.0
         version: 0.5.0(vite-plugin-pwa@0.20.1(vite@5.4.2(@types/node@22.5.0)(less@4.2.0)(sass@1.77.8)(terser@5.31.6))(workbox-build@7.1.1)(workbox-window@7.1.0))
@@ -3438,7 +3462,6 @@ packages:
 
   '@ls-lint/ls-lint@2.2.3':
     resolution: {integrity: sha512-ekM12jNm/7O2I/hsRv9HvYkRdfrHpiV1epVuI2NP+eTIcEgdIdKkKCs9KgQydu/8R5YXTov9aHdOgplmCHLupw==}
-    cpu: [x64, arm64, s390x]
     os: [darwin, linux, win32]
     hasBin: true
 
@@ -6958,6 +6981,9 @@ packages:
   lines-and-columns@1.2.4:
     resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
 
+  linkify-it@5.0.0:
+    resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
+
   lint-staged@15.2.9:
     resolution: {integrity: sha512-BZAt8Lk3sEnxw7tfxM7jeZlPRuT4M68O0/CwZhhaw6eeWu0Lz5eERE3m386InivXB64fp/mDID452h48tvKlRQ==}
     engines: {node: '>=18.12.0'}
@@ -7137,6 +7163,10 @@ packages:
   mark.js@8.11.1:
     resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==}
 
+  markdown-it@14.1.0:
+    resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==}
+    hasBin: true
+
   mathml-tag-names@2.1.3:
     resolution: {integrity: sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==}
 
@@ -7146,6 +7176,9 @@ packages:
   mdn-data@2.0.30:
     resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
 
+  mdurl@2.0.0:
+    resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==}
+
   medium-zoom@1.1.0:
     resolution: {integrity: sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ==}
 
@@ -8304,6 +8337,10 @@ packages:
     engines: {node: '>=16'}
     hasBin: true
 
+  punycode.js@2.3.1:
+    resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==}
+    engines: {node: '>=6'}
+
   punycode@2.3.1:
     resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
     engines: {node: '>=6'}
@@ -9310,6 +9347,9 @@ packages:
     engines: {node: '>=14.17'}
     hasBin: true
 
+  uc.micro@2.1.0:
+    resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==}
+
   ufo@1.5.4:
     resolution: {integrity: sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==}
 
@@ -16391,6 +16431,10 @@ snapshots:
 
   lines-and-columns@1.2.4: {}
 
+  linkify-it@5.0.0:
+    dependencies:
+      uc.micro: 2.1.0
+
   lint-staged@15.2.9:
     dependencies:
       chalk: 5.3.0
@@ -16607,12 +16651,23 @@ snapshots:
 
   mark.js@8.11.1: {}
 
+  markdown-it@14.1.0:
+    dependencies:
+      argparse: 2.0.1
+      entities: 4.5.0
+      linkify-it: 5.0.0
+      mdurl: 2.0.0
+      punycode.js: 2.3.1
+      uc.micro: 2.1.0
+
   mathml-tag-names@2.1.3: {}
 
   mdn-data@2.0.28: {}
 
   mdn-data@2.0.30: {}
 
+  mdurl@2.0.0: {}
+
   medium-zoom@1.1.0: {}
 
   memoize-one@6.0.0: {}
@@ -17781,6 +17836,8 @@ snapshots:
       picocolors: 1.0.1
       sade: 1.8.1
 
+  punycode.js@2.3.1: {}
+
   punycode@2.3.1: {}
 
   pupa@3.1.0:
@@ -18908,6 +18965,8 @@ snapshots:
 
   typescript@5.5.4: {}
 
+  uc.micro@2.1.0: {}
+
   ufo@1.5.4: {}
 
   unbox-primitive@1.0.2: