feat: implement hierarchical documentation structure and nested navigation
This commit is contained in:
parent
ba16745daa
commit
31cae2b83d
@ -2,25 +2,68 @@
|
||||
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { useState } from 'react'
|
||||
import { ChevronRight, ChevronDown, FileText } from 'lucide-react'
|
||||
import type { DocCollection } from './types'
|
||||
import { useState, useEffect } from 'react'
|
||||
import {
|
||||
ChevronRight,
|
||||
ChevronDown,
|
||||
Book,
|
||||
Settings,
|
||||
Zap,
|
||||
Shield,
|
||||
HelpCircle,
|
||||
Code,
|
||||
Terminal,
|
||||
Activity,
|
||||
GraduationCap,
|
||||
Layout,
|
||||
Layers,
|
||||
Puzzle
|
||||
} from 'lucide-react'
|
||||
import type { DocCollection, DocVersionOption } from './types'
|
||||
|
||||
interface DocsSidebarProps {
|
||||
collections: DocCollection[]
|
||||
}
|
||||
|
||||
// Helper to humanize category names
|
||||
const humanize = (s: string) => {
|
||||
if (!s) return ''
|
||||
return s.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase())
|
||||
}
|
||||
|
||||
// Icon mapping for categories
|
||||
const ICON_MAP: Record<string, any> = {
|
||||
'getting-started': Book,
|
||||
'architecture': Zap,
|
||||
'usage': Settings,
|
||||
'advanced': GraduationCap,
|
||||
'api': Code,
|
||||
'development': Terminal,
|
||||
'operations': Activity,
|
||||
'governance': Shield,
|
||||
'appendix': HelpCircle,
|
||||
'integrations': Puzzle,
|
||||
'overview': Layout,
|
||||
'core-concepts': Layers,
|
||||
}
|
||||
|
||||
const ADVANCED_GROUP = ['api', 'development', 'operations', 'governance', 'advanced']
|
||||
|
||||
export default function DocsSidebar({ collections }: DocsSidebarProps) {
|
||||
const pathname = usePathname()
|
||||
|
||||
// Sort collections by title or defined order if any
|
||||
const sortedCollections = [...collections].sort((a, b) => a.title.localeCompare(b.title))
|
||||
// Sort collections: Console first, then others alphabetically
|
||||
const sortedCollections = [...collections].sort((a, b) => {
|
||||
if (a.slug.includes('console')) return -1
|
||||
if (b.slug.includes('console')) return 1
|
||||
return a.title.localeCompare(b.title)
|
||||
})
|
||||
|
||||
return (
|
||||
<aside className="sticky top-[64px] hidden h-[calc(100vh-64px)] w-64 shrink-0 overflow-y-auto border-r border-surface-border bg-background py-6 pl-8 pr-4 lg:block">
|
||||
<nav className="space-y-6">
|
||||
<aside className="sticky top-[64px] hidden h-[calc(100vh-64px)] w-72 shrink-0 overflow-y-auto border-r border-surface-border bg-background/50 backdrop-blur-sm py-8 pl-8 pr-4 lg:block">
|
||||
<nav className="space-y-10">
|
||||
{sortedCollections.map((collection) => (
|
||||
<SidebarGroup
|
||||
<CollectionGroup
|
||||
key={collection.slug}
|
||||
collection={collection}
|
||||
activePath={pathname}
|
||||
@ -31,44 +74,162 @@ export default function DocsSidebar({ collections }: DocsSidebarProps) {
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarGroup({ collection, activePath }: { collection: DocCollection; activePath: string }) {
|
||||
function CollectionGroup({ collection, activePath }: { collection: DocCollection; activePath: string }) {
|
||||
const [isOpen, setIsOpen] = useState(true)
|
||||
|
||||
// Check if any child is active to auto-expand (optional, defaulted to true for now)
|
||||
const isActive = collection.versions.some(v => activePath === `/docs/${collection.slug}/${v.slug}`)
|
||||
// Group versions by category
|
||||
const grouped: Record<string, DocVersionOption[]> = {}
|
||||
const topLevel: DocVersionOption[] = []
|
||||
|
||||
collection.versions.forEach(v => {
|
||||
const category = v.category
|
||||
if (!category || category === 'overview' || category === 'index') {
|
||||
topLevel.push(v)
|
||||
} else {
|
||||
if (!grouped[category]) grouped[category] = []
|
||||
grouped[category].push(v)
|
||||
}
|
||||
})
|
||||
|
||||
const hasAdvanced = ADVANCED_GROUP.some(k => grouped[k])
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-4">
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="flex w-full items-center justify-between text-sm font-semibold text-heading transistion hover:text-primary"
|
||||
className="group flex w-full items-center justify-between text-xs font-bold uppercase tracking-widest text-text-subtle transition-colors hover:text-primary"
|
||||
>
|
||||
<span>{collection.title}</span>
|
||||
{isOpen ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="h-1 w-1 rounded-full bg-primary/40 group-hover:bg-primary transition-colors"></span>
|
||||
{collection.title}
|
||||
</span>
|
||||
{isOpen ? <ChevronDown className="h-3.5 w-3.5 opacity-50" /> : <ChevronRight className="h-3.5 w-3.5 opacity-50" />}
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<ul className="space-y-1 border-l border-surface-border pl-4">
|
||||
{collection.versions.map((version) => {
|
||||
const href = `/docs/${collection.slug}/${version.slug}`
|
||||
const isPageActive = activePath === href
|
||||
<div className="space-y-6">
|
||||
{/* Uncategorized / Overview / README */}
|
||||
{topLevel.length > 0 && (
|
||||
<ul className="space-y-1">
|
||||
{topLevel.map(v => (
|
||||
<SidebarLink key={v.slug} version={v} collectionSlug={collection.slug} activePath={activePath} />
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
return (
|
||||
<li key={version.slug}>
|
||||
<Link
|
||||
href={href}
|
||||
className={`block rounded-md px-2 py-1.5 text-sm transition-colors ${isPageActive
|
||||
? 'bg-primary/10 text-primary font-medium'
|
||||
: 'text-text-muted hover:text-heading hover:bg-surface-muted'
|
||||
}`}
|
||||
>
|
||||
{version.title}
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
{/* Main Categories (Getting Started, Architecture, etc.) */}
|
||||
<div className="space-y-4">
|
||||
{Object.entries(grouped)
|
||||
.filter(([k]) => !ADVANCED_GROUP.includes(k))
|
||||
.sort((a, b) => a[0].localeCompare(b[0]))
|
||||
.map(([category, versions]) => (
|
||||
<CategorySection
|
||||
key={category}
|
||||
title={category}
|
||||
versions={versions}
|
||||
collectionSlug={collection.slug}
|
||||
activePath={activePath}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Advanced Section Dropdown */}
|
||||
{hasAdvanced && (
|
||||
<AdvancedSection
|
||||
grouped={grouped}
|
||||
collectionSlug={collection.slug}
|
||||
activePath={activePath}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CategorySection({ title, versions, collectionSlug, activePath }: { title: string; versions: DocVersionOption[]; collectionSlug: string; activePath: string }) {
|
||||
const Icon = ICON_MAP[title] || Book
|
||||
|
||||
// Auto-expand if active
|
||||
const isActive = versions.some(v => activePath === `/docs/${collectionSlug}/${v.slug}`)
|
||||
const [isExpanded, setIsExpanded] = useState(true)
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div
|
||||
className={`flex w-full items-center gap-2.5 px-2 py-1 text-[11px] font-bold uppercase tracking-tight ${isActive ? 'text-primary' : 'text-text-muted/80'}`}
|
||||
>
|
||||
<Icon className={`h-3.5 w-3.5 ${isActive ? 'text-primary' : 'text-text-subtle'}`} />
|
||||
<span>{humanize(title)}</span>
|
||||
</div>
|
||||
<ul className="ml-3.5 space-y-1 border-l border-surface-border pl-4">
|
||||
{versions.map(v => (
|
||||
<SidebarLink key={v.slug} version={v} collectionSlug={collectionSlug} activePath={activePath} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AdvancedSection({ grouped, collectionSlug, activePath }: { grouped: Record<string, DocVersionOption[]>; collectionSlug: string; activePath: string }) {
|
||||
// Check if anything inside is active to auto-expand
|
||||
const isInsideActive = ADVANCED_GROUP.some(k => grouped[k]?.some(v => activePath === `/docs/${collectionSlug}/${v.slug}`))
|
||||
|
||||
const [isExpanded, setIsExpanded] = useState(isInsideActive)
|
||||
|
||||
useEffect(() => {
|
||||
if (isInsideActive) setIsExpanded(true)
|
||||
}, [isInsideActive])
|
||||
|
||||
return (
|
||||
<div className="space-y-2 rounded-lg bg-surface-muted/30 p-2">
|
||||
<button
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className={`flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-xs font-bold transition-all ${isExpanded ? 'text-primary bg-primary/5' : 'text-text-muted hover:text-primary hover:bg-primary/5'}`}
|
||||
>
|
||||
<GraduationCap className={`h-4 w-4 ${isExpanded ? 'text-primary' : 'text-text-subtle'}`} />
|
||||
<span className="uppercase tracking-wide">Advanced</span>
|
||||
<ChevronDown className={`ml-auto h-3.5 w-3.5 transition-transform duration-300 ${isExpanded ? '' : '-rotate-90'}`} />
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="space-y-5 animate-in fade-in slide-in-from-top-1 duration-300">
|
||||
{ADVANCED_GROUP.map(k => grouped[k] && (
|
||||
<div key={k} className="space-y-2 py-1">
|
||||
<div className="flex items-center gap-2 px-3 text-[10px] font-bold uppercase tracking-widest text-text-subtle/50">
|
||||
<span className="h-[1px] w-2 bg-surface-border"></span>
|
||||
{humanize(k)}
|
||||
</div>
|
||||
<ul className="ml-4 space-y-1 border-l border-surface-border/50 pl-4">
|
||||
{grouped[k].map(v => (
|
||||
<SidebarLink key={v.slug} version={v} collectionSlug={collectionSlug} activePath={activePath} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SidebarLink({ version, collectionSlug, activePath }: { version: DocVersionOption; collectionSlug: string; activePath: string }) {
|
||||
const href = `/docs/${collectionSlug}/${version.slug}`
|
||||
const isPageActive = activePath === href
|
||||
|
||||
return (
|
||||
<li>
|
||||
<Link
|
||||
href={href}
|
||||
className={`group flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-all duration-200 ${isPageActive
|
||||
? 'bg-primary/10 text-primary font-medium shadow-sm'
|
||||
: 'text-text-muted hover:text-heading hover:bg-surface-muted'
|
||||
}`}
|
||||
>
|
||||
{isPageActive && <span className="h-1.5 w-1.5 rounded-full bg-primary" />}
|
||||
<span className="truncate">{version.title}</span>
|
||||
{!isPageActive && <ChevronRight className="ml-auto h-3 w-3 opacity-0 transition-all -translate-x-2 group-hover:opacity-30 group-hover:translate-x-0" />}
|
||||
</Link>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
@ -41,8 +41,9 @@ export const generateStaticParams = async () => {
|
||||
|
||||
export const dynamicParams = false
|
||||
|
||||
export async function generateMetadata({ params }: { params: { collection: string; version: string } }): Promise<Metadata> {
|
||||
const doc = await getDocVersion(params.collection, params.version)
|
||||
export async function generateMetadata({ params }: { params: Promise<{ collection: string; slug: string[] }> }): Promise<Metadata> {
|
||||
const resolvedParams = await params
|
||||
const doc = await getDocVersion(resolvedParams.collection, resolvedParams.slug)
|
||||
if (!doc) return {}
|
||||
return {
|
||||
title: `${doc.version.title} - ${doc.collection.title} | Documentation`,
|
||||
@ -53,13 +54,14 @@ export async function generateMetadata({ params }: { params: { collection: strin
|
||||
export default async function DocVersionPage({
|
||||
params,
|
||||
}: {
|
||||
params: { collection: string; version: string }
|
||||
params: Promise<{ collection: string; slug: string[] }>
|
||||
}) {
|
||||
if (!isFeatureEnabled('appModules', '/docs')) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const doc = await getDocVersion(params.collection, params.version)
|
||||
const resolvedParams = await params
|
||||
const doc = await getDocVersion(resolvedParams.collection, resolvedParams.slug)
|
||||
if (!doc) {
|
||||
notFound()
|
||||
}
|
||||
@ -101,14 +103,6 @@ export default async function DocVersionPage({
|
||||
tags={version.tags}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* We could add TOC here later */}
|
||||
{/*
|
||||
<div>
|
||||
<h3 className="mb-3 text-sm font-semibold uppercase tracking-wider text-text-subtle">On This Page</h3>
|
||||
<TOC content={version.content} />
|
||||
</div>
|
||||
*/}
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
@ -20,13 +20,14 @@ export const generateStaticParams = async () => {
|
||||
export default async function CollectionPage({
|
||||
params,
|
||||
}: {
|
||||
params: { collection: string }
|
||||
params: Promise<{ collection: string }>
|
||||
}) {
|
||||
if (!isFeatureEnabled('appModules', '/docs')) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const doc = await getDocResource(params.collection)
|
||||
const resolvedParams = await params
|
||||
const doc = await getDocResource(resolvedParams.collection)
|
||||
if (!doc) {
|
||||
notFound()
|
||||
}
|
||||
|
||||
@ -37,13 +37,16 @@ export async function getDocVersionParams() {
|
||||
return getDocParams()
|
||||
}
|
||||
|
||||
export async function getDocVersion(slug: string, version: string) {
|
||||
export async function getDocVersion(collectionSlug: string, slugSegments: string | string[]) {
|
||||
if (!isDocsModuleEnabled()) {
|
||||
return undefined
|
||||
}
|
||||
const collection = await getDocCollection(slug)
|
||||
const collection = await getDocCollection(collectionSlug)
|
||||
if (!collection) return undefined
|
||||
const versionMatch = collection.versions.find((item) => item.slug === version)
|
||||
|
||||
const targetSlug = Array.isArray(slugSegments) ? slugSegments.join('/') : slugSegments
|
||||
const versionMatch = collection.versions.find((item) => item.slug === targetSlug)
|
||||
|
||||
if (!versionMatch) return undefined
|
||||
return { collection, version: versionMatch }
|
||||
}
|
||||
|
||||
@ -7,6 +7,8 @@ export interface DocVersionOption {
|
||||
tags?: string[]
|
||||
content: string
|
||||
isMdx: boolean
|
||||
category?: string
|
||||
subcategory?: boolean
|
||||
}
|
||||
|
||||
export interface DocCollection {
|
||||
@ -17,4 +19,5 @@ export interface DocCollection {
|
||||
tags: string[]
|
||||
versions: DocVersionOption[]
|
||||
defaultVersionSlug: string
|
||||
category?: string
|
||||
}
|
||||
|
||||
@ -12,6 +12,8 @@ export interface DocVersion {
|
||||
tags: string[]
|
||||
content: string
|
||||
isMdx: boolean
|
||||
category?: string
|
||||
subcategory?: boolean
|
||||
}
|
||||
|
||||
export interface DocCollection {
|
||||
@ -68,17 +70,43 @@ function buildExcerpt(markdown: string): string {
|
||||
function normalizeDoc(file: Awaited<ReturnType<typeof readDocFiles>>[number]): DocVersion & {
|
||||
collection: string
|
||||
collectionLabel: string
|
||||
category?: string
|
||||
subcategory?: boolean
|
||||
} {
|
||||
const segments = file.slug.split('/')
|
||||
|
||||
// collection is usually the first segment (e.g., '01-console')
|
||||
const collection = typeof file.metadata.collection === 'string' ? file.metadata.collection : segments[0] || 'docs'
|
||||
const collectionLabel =
|
||||
typeof file.metadata.collectionLabel === 'string'
|
||||
? file.metadata.collectionLabel
|
||||
: humanize(collection)
|
||||
|
||||
const versionSlug = typeof file.metadata.versionSlug === 'string' ? file.metadata.versionSlug : segments.at(-1) ?? 'latest'
|
||||
const label = typeof file.metadata.version === 'string' ? file.metadata.version : versionSlug
|
||||
const title = typeof file.metadata.title === 'string' ? file.metadata.title : label
|
||||
// category is usually the second segment (e.g., 'getting-started')
|
||||
const category = typeof file.metadata.category === 'string'
|
||||
? file.metadata.category
|
||||
: (segments.length > 2 ? segments[1] : undefined)
|
||||
|
||||
const subcategory = !!file.metadata.subcategory || (segments.length > 2 && segments.at(-1) !== 'index')
|
||||
|
||||
// Determine the slug within the collection
|
||||
// segments[0] is the collection (e.g., '01-console')
|
||||
// We want the rest of the path as the nested slug
|
||||
const relativeSegments = segments.slice(1)
|
||||
let innerSlug = relativeSegments.join('/')
|
||||
|
||||
// Handle 'index' or 'README' at any level
|
||||
if (innerSlug.endsWith('/index') || innerSlug.endsWith('/README')) {
|
||||
innerSlug = innerSlug.replace(/\/(index|README)$/, '')
|
||||
} else if (innerSlug === 'index' || innerSlug === 'README') {
|
||||
innerSlug = ''
|
||||
}
|
||||
|
||||
// If empty (it was just the collection-level README or index), use 'overview'
|
||||
const finalSlug = innerSlug || 'overview'
|
||||
|
||||
const label = typeof file.metadata.version === 'string' ? file.metadata.version : (segments.at(-1) ?? 'latest')
|
||||
const title = typeof file.metadata.title === 'string' ? file.metadata.title : (finalSlug === 'overview' ? 'Overview' : humanize(segments.at(-1) || label))
|
||||
const description =
|
||||
typeof file.metadata.description === 'string' ? file.metadata.description : buildExcerpt(file.content)
|
||||
const updatedAt = typeof file.metadata.updatedAt === 'string' ? file.metadata.updatedAt : undefined
|
||||
@ -88,7 +116,7 @@ function normalizeDoc(file: Awaited<ReturnType<typeof readDocFiles>>[number]): D
|
||||
const isMdx = String(file.metadata.format || '').toLowerCase() === 'mdx'
|
||||
|
||||
return {
|
||||
slug: versionSlug,
|
||||
slug: finalSlug,
|
||||
label,
|
||||
title,
|
||||
description,
|
||||
@ -98,6 +126,8 @@ function normalizeDoc(file: Awaited<ReturnType<typeof readDocFiles>>[number]): D
|
||||
isMdx,
|
||||
collection,
|
||||
collectionLabel,
|
||||
category,
|
||||
subcategory,
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,6 +155,8 @@ export const getDocCollections = cache(async (): Promise<DocCollection[]> => {
|
||||
tags: doc.tags,
|
||||
content: doc.content,
|
||||
isMdx: doc.isMdx,
|
||||
category: doc.category,
|
||||
subcategory: doc.subcategory,
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
@ -197,6 +229,9 @@ export async function getDocVersion(
|
||||
export async function getDocParams() {
|
||||
const collections = await getDocCollections()
|
||||
return collections.flatMap((collection) =>
|
||||
collection.versions.map((version) => ({ collection: collection.slug, version: version.slug })),
|
||||
collection.versions.map((version) => ({
|
||||
collection: collection.slug,
|
||||
slug: version.slug.split('/')
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
@ -19,38 +19,38 @@ const tailwindConfig = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
border: 'var(--color-surface-border)',
|
||||
input: 'var(--color-surface-border)',
|
||||
ring: 'var(--color-ring)',
|
||||
background: 'var(--color-background)',
|
||||
foreground: 'var(--color-text)',
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))',
|
||||
DEFAULT: 'var(--color-primary)',
|
||||
foreground: 'var(--color-primary-foreground)',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))',
|
||||
DEFAULT: 'var(--color-surface-muted)',
|
||||
foreground: 'var(--color-text-muted)',
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))',
|
||||
DEFAULT: 'var(--color-danger)',
|
||||
foreground: 'var(--color-danger-foreground)',
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))',
|
||||
DEFAULT: 'var(--color-surface-muted)',
|
||||
foreground: 'var(--color-text-muted)',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))',
|
||||
DEFAULT: 'var(--color-accent)',
|
||||
foreground: 'var(--color-accent-foreground)',
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))',
|
||||
DEFAULT: 'var(--color-surface-elevated)',
|
||||
foreground: 'var(--color-text)',
|
||||
},
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))',
|
||||
DEFAULT: 'var(--color-surface)',
|
||||
foreground: 'var(--color-text)',
|
||||
},
|
||||
brand: {
|
||||
DEFAULT: '#3366FF', // 主色
|
||||
@ -61,6 +61,18 @@ const tailwindConfig = {
|
||||
navy: '#1E2E55', // 海军蓝
|
||||
heading: '#2E3A59', // 标题色
|
||||
},
|
||||
surface: {
|
||||
DEFAULT: 'var(--color-surface)',
|
||||
muted: 'var(--color-surface-muted)',
|
||||
border: 'var(--color-surface-border)',
|
||||
hover: 'var(--color-surface-hover)',
|
||||
},
|
||||
text: {
|
||||
DEFAULT: 'var(--color-text)',
|
||||
muted: 'var(--color-text-muted)',
|
||||
subtle: 'var(--color-text-subtle)',
|
||||
},
|
||||
heading: 'var(--color-heading)',
|
||||
},
|
||||
|
||||
// 字体配置
|
||||
|
||||
Loading…
Reference in New Issue
Block a user