feat(docs): group package catalog

This commit is contained in:
Maofeng
2026-09-29 20:56:44 +08:00
parent 24b32ea5cc
commit 68f142dfa3
17 changed files with 203 additions and 35 deletions
+17
View File
@@ -0,0 +1,17 @@
- key: foundations
order: 10
label:
zh-Hans: 基础能力
en-US: Foundations
- key: application
order: 20
label:
zh-Hans: 应用构建
en-US: Application
- key: content
order: 30
label:
zh-Hans: 内容与数据
en-US: Content and data
+113 -4
View File
@@ -7,6 +7,7 @@ import {
type DocLocale,
type DocLocation,
type DocPackage,
type DocPackageGroup,
type DocPage,
type DocSection,
type DocTocItem,
@@ -19,14 +20,37 @@ type DocContentModule = {
}
type PackageDefinition = {
group: string
key: string
label: string
description: Record<DocLocale, string>
order: number
}
type PackageGroupDefinition = {
key: string
label: Record<DocLocale, string>
order: number
}
type ParsedDocPage = DocPage & { packageKey: string }
const packageGroupModules = import.meta.glob<unknown>("./package-groups.yaml", {
eager: true,
import: "default",
})
const packageGroupDefinitions = Object.values(packageGroupModules)
.flatMap(parsePackageGroupDefinitions)
.toSorted(
(left, right) =>
left.order - right.order || left.key.localeCompare(right.key)
)
const packageGroupsByKey = new Map(
packageGroupDefinitions.map((group) => [group.key, group])
)
const packageMetaModules = import.meta.glob<unknown>(
"../../../packages/*/docs/meta.yaml",
{ eager: true, import: "default" }
@@ -34,10 +58,16 @@ const packageMetaModules = import.meta.glob<unknown>(
const packageDefinitions = Object.entries(packageMetaModules)
.map(([path, metadata]) => parsePackageDefinition(path, metadata))
.toSorted(
(left, right) =>
left.order - right.order || left.key.localeCompare(right.key)
)
.toSorted((left, right) => {
const leftGroup = getPackageGroupDefinition(left.group)
const rightGroup = getPackageGroupDefinition(right.group)
return (
leftGroup.order - rightGroup.order ||
left.order - right.order ||
left.key.localeCompare(right.key)
)
})
const docModuleLoaders = import.meta.glob<DocContentModule>(
"../../../packages/*/docs/*/**/*.mdx",
@@ -67,12 +97,21 @@ const packagesByLocale: Record<DocLocale, readonly DocPackage[]> = {
),
}
const packageGroupsByLocale: Record<DocLocale, readonly DocPackageGroup[]> = {
"zh-Hans": createDocPackageGroups("zh-Hans"),
"en-US": createDocPackageGroups("en-US"),
}
validateLocalizedPagePairs(packagesByLocale)
export function getDocPackages(locale: DocLocale = defaultDocLocale) {
return packagesByLocale[locale]
}
export function getDocPackageGroups(locale: DocLocale = defaultDocLocale) {
return packageGroupsByLocale[locale]
}
export function getDocHref(docPackage: DocPackage, page?: DocPage) {
const prefix = getLocalePrefix(docPackage.locale)
const target = page ?? getOverviewPage(docPackage)
@@ -203,6 +242,7 @@ function createDocPackage(
}
return {
group: definition.group,
key: definition.key,
label: definition.label,
description: definition.description[locale],
@@ -211,6 +251,74 @@ function createDocPackage(
}
}
function createDocPackageGroups(locale: DocLocale): readonly DocPackageGroup[] {
const packages = packagesByLocale[locale]
return packageGroupDefinitions.flatMap((definition) => {
const groupPackages = packages.filter(
(docPackage) => docPackage.group === definition.key
)
if (groupPackages.length === 0) return []
return {
key: definition.key,
label: definition.label[locale],
packages: groupPackages,
}
})
}
function parsePackageGroupDefinitions(
value: unknown
): PackageGroupDefinition[] {
if (!Array.isArray(value)) {
throw new Error("Package documentation groups must be an array.")
}
const groups = value.map((entry, index) => {
const path = `package-groups.yaml[${index}]`
if (!isRecord(entry)) {
throw new Error(
`Package documentation group must be an object: "${path}".`
)
}
const label = entry.label
if (!isRecord(label)) {
throw new Error(
`Package documentation group requires a localized "label" object: "${path}".`
)
}
return {
key: readMetaString(entry, "key", path),
label: {
"zh-Hans": readMetaString(label, "zh-Hans", path),
"en-US": readMetaString(label, "en-US", path),
},
order: readMetaNumber(entry, "order", path),
}
})
const duplicate = groups.find(
(group, index) =>
groups.findIndex((item) => item.key === group.key) !== index
)
if (duplicate) {
throw new Error(`Duplicate package documentation group "${duplicate.key}".`)
}
return groups
}
function getPackageGroupDefinition(key: string) {
const group = packageGroupsByKey.get(key)
if (!group) {
throw new Error(`Unknown package documentation group "${key}".`)
}
return group
}
function parsePackageDefinition(
path: string,
value: unknown
@@ -236,6 +344,7 @@ function parsePackageDefinition(
}
return {
group: readMetaString(value, "group", path),
key: match[1]!,
label: readMetaString(value, "label", path),
description: {
+7
View File
@@ -43,6 +43,7 @@ export type DocSearchEntry = {
}
export type DocPackage = {
group: string
key: string
label: string
description: string
@@ -50,6 +51,12 @@ export type DocPackage = {
pages: readonly DocPage[]
}
export type DocPackageGroup = {
key: string
label: string
packages: readonly DocPackage[]
}
export type DocLocation = {
locale: DocLocale
package: DocPackage
+42 -19
View File
@@ -4,13 +4,18 @@ import {
SiteFooter,
SiteHeader,
} from "../components/layout"
import { getDocHref, getDocPackages } from "../content/registry"
import { getDocHref, getDocPackageGroups } from "../content/registry"
import { useLocale, useTranslate } from "../lib/locale"
export default function IndexPage() {
const locale = useLocale()
const t = useTranslate()
const docPackages = getDocPackages(locale)
const packageGroups = getDocPackageGroups(locale)
const packageNumbers = new Map(
packageGroups
.flatMap((group) => group.packages)
.map((docPackage, index) => [docPackage.key, index + 1])
)
return (
<>
@@ -19,27 +24,45 @@ export default function IndexPage() {
actions={<Actions />}
/>
<Container
className="grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-6 lg:grid-cols-3 lg:gap-8"
className="space-y-12 lg:space-y-16"
aria-label={t({ zh: "文档导航", us: "Documentation" })}
>
{docPackages.map((docPackage, index) => (
<a
key={docPackage.key}
href={getDocHref(docPackage)}
className="block overflow-hidden rounded-xl border border-slate-200 bg-indigo-50 outline-0 outline-offset-2 transition-colors hover:border-indigo-500 hover:bg-indigo-50 focus-visible:outline-2 focus-visible:outline-indigo-500 dark:border-slate-800 dark:bg-slate-900 dark:hover:border-indigo-400 dark:hover:bg-slate-800/80 dark:focus-visible:outline-indigo-400"
{packageGroups.map((group) => (
<section
key={group.key}
aria-labelledby={`package-group-${group.key}`}
>
<div className="min-h-fit p-4 sm:min-h-40 lg:min-h-46">
<div className="text-slate-400 dark:text-slate-500">
{index > 9 ? `${index + 1}` : `0${index + 1}`}
</div>
<h2 className="mt-2 mb-4 text-xl font-semibold text-slate-950 dark:text-slate-100">
{docPackage.label}
</h2>
<p className="text-slate-700 dark:text-slate-300">
{docPackage.description}
</p>
<h2
id={`package-group-${group.key}`}
className="mb-5 text-xl font-semibold text-slate-950 sm:mb-6 sm:text-2xl dark:text-slate-100"
>
{group.label}
</h2>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 sm:gap-6 lg:grid-cols-3 lg:gap-8">
{group.packages.map((docPackage) => (
<a
key={docPackage.key}
href={getDocHref(docPackage)}
className="block overflow-hidden rounded-xl border border-slate-200 bg-indigo-50 outline-0 outline-offset-2 transition-colors hover:border-indigo-500 hover:bg-indigo-50 focus-visible:outline-2 focus-visible:outline-indigo-500 dark:border-slate-800 dark:bg-slate-900 dark:hover:border-indigo-400 dark:hover:bg-slate-800/80 dark:focus-visible:outline-indigo-400"
>
<div className="min-h-fit p-4 sm:min-h-40 lg:min-h-46">
<div className="text-slate-400 dark:text-slate-500">
{String(packageNumbers.get(docPackage.key)).padStart(
2,
"0"
)}
</div>
<h3 className="mt-2 mb-4 text-xl font-semibold text-slate-950 dark:text-slate-100">
{docPackage.label}
</h3>
<p className="text-slate-700 dark:text-slate-300">
{docPackage.description}
</p>
</div>
</a>
))}
</div>
</a>
</section>
))}
</Container>
<SiteFooter className="sm:py-12" />
+1 -1
View File
@@ -29,7 +29,7 @@ function loadYamlModules(): Plugin {
name: "load-yaml-modules",
enforce: "pre",
transform(source, id) {
if (!/\/meta\.ya?ml(?:\?|$)/.test(id)) return
if (!/\.ya?ml(?:\?|$)/.test(id)) return
try {
return {
+3 -2
View File
@@ -1,5 +1,6 @@
group: foundations
order: 20
label: Appearance
description:
zh-Hans: 使用原子化控件、主题词典和可组合偏好副作用构建应用外观设置。
en-US: Build application appearance settings with atomic controls, theme tokens, and composable preference effects.
zh-Hans: 配置主题、颜色、文字缩放、动态效果与界面亮度。
en-US: Configure themes, colors, text scaling, motion, and interface brightness.
+1
View File
@@ -1,3 +1,4 @@
group: application
order: 10
label: Blocks
description:
+2 -1
View File
@@ -1,4 +1,5 @@
order: 30
group: content
order: 40
label: Cropper
description:
zh-Hans: 通过可组合交互、几何计算和 Canvas 导出构建图片裁剪流程。
+1
View File
@@ -1,3 +1,4 @@
group: foundations
order: 40
label: I18n
description:
+2 -1
View File
@@ -1,4 +1,5 @@
order: 50
group: content
order: 20
label: Lexical
description:
zh-Hans: 从根节点和内容区域开始,逐步构建支持自定义操作与媒体的富文本编辑器。
+2 -1
View File
@@ -1,4 +1,5 @@
order: 60
group: content
order: 30
label: Media
description:
zh-Hans: 通过存储 Adapter 构建媒体浏览、上传、目录管理和资源选择流程。
+2 -1
View File
@@ -1,4 +1,5 @@
order: 70
group: application
order: 30
label: Overlays
description:
zh-Hans: 使用统一的异步 API 编排 alert、confirm、prompt、modal、sheet、drawer 和 toast。
+2 -1
View File
@@ -1,4 +1,5 @@
order: 80
group: application
order: 20
label: Page
description:
zh-Hans: 组合页面标题、描述、操作、面包屑、路由标签和内容区,并按需将标题挂载到外部布局。
+2 -1
View File
@@ -1,4 +1,5 @@
order: 90
group: foundations
order: 30
label: Preferences
description:
zh-Hans: 使用 SSR 快照、持久化回调和可组合副作用管理类型安全且可扩展的应用偏好。
+2 -1
View File
@@ -1,4 +1,5 @@
order: 100
group: content
order: 10
label: Record
description:
zh-Hans: 通过可组合上下文、类型安全字段路径和记录迭代器构建数据驱动界面。
+2 -1
View File
@@ -1,4 +1,5 @@
order: 110
group: application
order: 40
label: Search
description:
zh-Hans: 通过可替换的 Adapter、分组结果、分页和搜索历史构建应用级搜索体验。
+2 -1
View File
@@ -1,4 +1,5 @@
order: 120
group: foundations
order: 10
label: UI
description:
zh-Hans: 配置共享 UI 层,并学习组合组件、Hooks、图标和本地化词典。