aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.tsx11
-rw-r--r--src/app-routes.tsx56
-rw-r--r--src/astryx-themes.ts15
-rw-r--r--src/components/AppLayout.tsx79
-rw-r--r--src/components/ComponentsRoute.tsx16
-rw-r--r--src/components/IconsRoute.tsx16
-rw-r--r--src/components/Page.tsx42
-rw-r--r--src/components/PageLoading.tsx13
-rw-r--r--src/data/astryxComponents.ts1223
-rw-r--r--src/demos/ActionsDemo.tsx83
-rw-r--r--src/demos/ChatDemo.tsx50
-rw-r--r--src/demos/CommandPaletteDemo.tsx46
-rw-r--r--src/demos/DataDemo.tsx72
-rw-r--r--src/demos/DialogsDemo.tsx95
-rw-r--r--src/demos/FeedbackDemo.tsx35
-rw-r--r--src/demos/InputsDemo.tsx58
-rw-r--r--src/demos/LayoutDemo.tsx52
-rw-r--r--src/demos/MenusDemo.tsx97
-rw-r--r--src/demos/NavigationDemo.tsx35
-rw-r--r--src/demos/ProvidersDemo.tsx29
-rw-r--r--src/demos/SelectionDemo.tsx55
-rw-r--r--src/demos/SelectorsDemo.tsx33
-rw-r--r--src/demos/TypographyDemo.tsx36
-rw-r--r--src/index.css2
-rw-r--r--src/main.tsx19
-rw-r--r--src/pages/AboutPage.tsx202
-rw-r--r--src/pages/ComponentsPage.tsx422
-rw-r--r--src/pages/HomePage.tsx219
-rw-r--r--src/pages/IconsPage.tsx334
-rw-r--r--src/pages/NotFoundPage.tsx27
-rw-r--r--src/router.tsx28
-rw-r--r--src/theme/ThemeMode.tsx55
-rw-r--r--src/theme/themeModeContext.ts10
-rw-r--r--src/theme/useThemeMode.ts12
34 files changed, 3577 insertions, 0 deletions
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..91e52c9
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,11 @@
+import { LinkProvider } from '@astryxdesign/core/Link'
+import { Link as RouterLink, RouterProvider } from 'react-router-dom'
+import { router } from './router'
+
+export function App() {
+ return (
+ <LinkProvider component={RouterLink}>
+ <RouterProvider router={router} />
+ </LinkProvider>
+ )
+}
diff --git a/src/app-routes.tsx b/src/app-routes.tsx
new file mode 100644
index 0000000..58747bb
--- /dev/null
+++ b/src/app-routes.tsx
@@ -0,0 +1,56 @@
+import type { ReactNode } from 'react'
+import type { LucideIcon } from 'lucide-react'
+import { Blocks, House, Info, Shapes } from 'lucide-react'
+import { ComponentsRoute } from './components/ComponentsRoute'
+import { IconsRoute } from './components/IconsRoute'
+import { AboutPage } from './pages/AboutPage'
+import { HomePage } from './pages/HomePage'
+
+export interface AppRoute {
+ path: string
+ label: string
+ icon: LucideIcon
+ element: ReactNode
+ showInNav?: boolean
+}
+
+/**
+ * 页面、路由和侧边栏的唯一数据源。
+ * 新增页面时只在这里增加一项,路由与菜单会同步更新。
+ */
+export const appRoutes: AppRoute[] = [
+ {
+ path: '/',
+ label: '概览',
+ icon: House,
+ element: <HomePage />,
+ },
+ {
+ path: '/components',
+ label: '组件',
+ icon: Blocks,
+ element: <ComponentsRoute />,
+ },
+ {
+ path: '/icons',
+ label: '图标',
+ icon: Shapes,
+ element: <IconsRoute />,
+ },
+ {
+ path: '/about',
+ label: '关于',
+ icon: Info,
+ element: <AboutPage />,
+ },
+]
+
+export const navRoutes = appRoutes.filter(
+ (route) => route.showInNav !== false,
+)
+
+export function isRouteActive(routePath: string, pathname: string) {
+ return routePath === '/'
+ ? pathname === '/'
+ : pathname === routePath || pathname.startsWith(`${routePath}/`)
+}
diff --git a/src/astryx-themes.ts b/src/astryx-themes.ts
new file mode 100644
index 0000000..9f253e2
--- /dev/null
+++ b/src/astryx-themes.ts
@@ -0,0 +1,15 @@
+/**
+ * @generated by `astryx theme` — do not edit manually.
+ * @astryx-theme-record {"themes":{"neutral":"@astryxdesign/theme-neutral"},"defaultThemeSlug":"neutral"}
+ */
+
+import {neutralTheme as _astryxTheme0} from "@astryxdesign/theme-neutral/built";
+import "@astryxdesign/theme-neutral/theme.css";
+import "@astryxdesign/theme-neutral/fonts.css";
+
+export const themes = {
+ "neutral": _astryxTheme0,
+} as const;
+export type ThemeSlug = keyof typeof themes;
+
+export const defaultThemeSlug: keyof typeof themes = "neutral";
diff --git a/src/components/AppLayout.tsx b/src/components/AppLayout.tsx
new file mode 100644
index 0000000..9c93302
--- /dev/null
+++ b/src/components/AppLayout.tsx
@@ -0,0 +1,79 @@
+import { useLayoutEffect, useState } from 'react'
+import { Outlet, useLocation } from 'react-router-dom'
+import { AppShell } from '@astryxdesign/core/AppShell'
+import { Icon } from '@astryxdesign/core/Icon'
+import { IconButton } from '@astryxdesign/core/IconButton'
+import { NavIcon } from '@astryxdesign/core/NavIcon'
+import { SideNav, SideNavItem, SideNavSection } from '@astryxdesign/core/SideNav'
+import { TopNav, TopNavHeading } from '@astryxdesign/core/TopNav'
+import { Moon, Sparkles, Sun } from 'lucide-react'
+import { isRouteActive, navRoutes } from '../app-routes'
+import { useThemeMode } from '../theme/useThemeMode'
+
+export function AppLayout() {
+ const { pathname } = useLocation()
+ const { mode, toggleMode } = useThemeMode()
+ const [isMobileNavOpen, setIsMobileNavOpen] = useState(false)
+
+ useLayoutEffect(() => {
+ const main = document.getElementById('astryx-app-shell-main')
+ if (main) main.scrollTop = 0
+ window.scrollTo(0, 0)
+ }, [pathname])
+
+ const nextMode = mode === 'light' ? 'dark' : 'light'
+
+ return (
+ <AppShell
+ height="fill"
+ contentPadding={0}
+ mobileNav={{
+ isOpen: isMobileNavOpen,
+ onOpenChange: setIsMobileNavOpen,
+ }}
+ topNav={
+ <TopNav
+ label="主导航"
+ heading={
+ <TopNavHeading
+ heading="Astryx 起始模板"
+ headingHref="/"
+ logo={
+ <NavIcon
+ icon={<Icon icon={Sparkles} size="sm" color="inherit" />}
+ />
+ }
+ />
+ }
+ endContent={
+ <IconButton
+ label={`切换到${nextMode === 'dark' ? '深色' : '浅色'}主题`}
+ tooltip={`切换到${nextMode === 'dark' ? '深色' : '浅色'}主题`}
+ variant="ghost"
+ icon={<Icon icon={mode === 'light' ? Moon : Sun} />}
+ onClick={toggleMode}
+ />
+ }
+ />
+ }
+ sideNav={
+ <SideNav collapsible>
+ <SideNavSection title="探索">
+ {navRoutes.map(({ label, path, icon }) => (
+ <SideNavItem
+ key={path}
+ label={label}
+ href={path}
+ icon={icon}
+ selectedIcon={icon}
+ isSelected={isRouteActive(path, pathname)}
+ onClick={() => setIsMobileNavOpen(false)}
+ />
+ ))}
+ </SideNavSection>
+ </SideNav>
+ }>
+ <Outlet />
+ </AppShell>
+ )
+}
diff --git a/src/components/ComponentsRoute.tsx b/src/components/ComponentsRoute.tsx
new file mode 100644
index 0000000..53db94a
--- /dev/null
+++ b/src/components/ComponentsRoute.tsx
@@ -0,0 +1,16 @@
+import { lazy, Suspense } from 'react'
+import { PageLoading } from './PageLoading'
+
+const ComponentsPage = lazy(() =>
+ import('../pages/ComponentsPage').then((module) => ({
+ default: module.ComponentsPage,
+ })),
+)
+
+export function ComponentsRoute() {
+ return (
+ <Suspense fallback={<PageLoading />}>
+ <ComponentsPage />
+ </Suspense>
+ )
+}
diff --git a/src/components/IconsRoute.tsx b/src/components/IconsRoute.tsx
new file mode 100644
index 0000000..83c7690
--- /dev/null
+++ b/src/components/IconsRoute.tsx
@@ -0,0 +1,16 @@
+import { lazy, Suspense } from 'react'
+import { PageLoading } from './PageLoading'
+
+const IconsPage = lazy(() =>
+ import('../pages/IconsPage').then((module) => ({
+ default: module.IconsPage,
+ })),
+)
+
+export function IconsRoute() {
+ return (
+ <Suspense fallback={<PageLoading />}>
+ <IconsPage />
+ </Suspense>
+ )
+}
diff --git a/src/components/Page.tsx b/src/components/Page.tsx
new file mode 100644
index 0000000..4dd1248
--- /dev/null
+++ b/src/components/Page.tsx
@@ -0,0 +1,42 @@
+import type { ReactNode } from 'react'
+import { Layout, LayoutContent } from '@astryxdesign/core/Layout'
+import { Heading } from '@astryxdesign/core/Heading'
+import { Text } from '@astryxdesign/core/Text'
+import { VStack } from '@astryxdesign/core/VStack'
+
+export function Page({ children }: { children: ReactNode }) {
+ return (
+ <Layout
+ height="auto"
+ padding={6}
+ contentWidth={1120}
+ content={<LayoutContent>{children}</LayoutContent>}
+ />
+ )
+}
+
+interface PageHeaderProps {
+ eyebrow?: string
+ title: string
+ description?: string
+}
+
+export function PageHeader({ eyebrow, title, description }: PageHeaderProps) {
+ return (
+ <VStack gap={2} maxWidth={820}>
+ {eyebrow ? (
+ <Text type="label" color="accent">
+ {eyebrow}
+ </Text>
+ ) : null}
+ <Heading level={1} type="display-3" textWrap="balance">
+ {title}
+ </Heading>
+ {description ? (
+ <Text type="large" color="secondary" as="p" textWrap="pretty">
+ {description}
+ </Text>
+ ) : null}
+ </VStack>
+ )
+}
diff --git a/src/components/PageLoading.tsx b/src/components/PageLoading.tsx
new file mode 100644
index 0000000..3e98fe8
--- /dev/null
+++ b/src/components/PageLoading.tsx
@@ -0,0 +1,13 @@
+import { Center } from '@astryxdesign/core/Center'
+import { Spinner } from '@astryxdesign/core/Spinner'
+import { Page } from './Page'
+
+export function PageLoading() {
+ return (
+ <Page>
+ <Center minHeight={480}>
+ <Spinner size="lg" />
+ </Center>
+ </Page>
+ )
+}
diff --git a/src/data/astryxComponents.ts b/src/data/astryxComponents.ts
new file mode 100644
index 0000000..352b20d
--- /dev/null
+++ b/src/data/astryxComponents.ts
@@ -0,0 +1,1223 @@
+export type AstryxComponentGroup = 'base' | 'form' | 'overlay'
+
+export interface AstryxComponentEntry {
+ name: string
+ importPath: string
+ family: string
+ group: AstryxComponentGroup
+ docsUrl: string
+}
+
+export const ASTRYX_COMPONENTS: AstryxComponentEntry[] = [
+ {
+ name: 'AlertDialog',
+ importPath: '@astryxdesign/core/AlertDialog',
+ family: 'Dialog',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/AlertDialog',
+ },
+ {
+ name: 'AppShell',
+ importPath: '@astryxdesign/core/AppShell',
+ family: 'AppShell',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/AppShell',
+ },
+ {
+ name: 'AspectRatio',
+ importPath: '@astryxdesign/core/AspectRatio',
+ family: 'AspectRatio',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/AspectRatio',
+ },
+ {
+ name: 'Avatar',
+ importPath: '@astryxdesign/core/Avatar',
+ family: 'Avatar',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Avatar',
+ },
+ {
+ name: 'AvatarGroup',
+ importPath: '@astryxdesign/core/AvatarGroup',
+ family: 'Avatar',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/AvatarGroup',
+ },
+ {
+ name: 'AvatarGroupOverflow',
+ importPath: '@astryxdesign/core/AvatarGroup',
+ family: 'Avatar',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/AvatarGroupOverflow',
+ },
+ {
+ name: 'AvatarStatusDot',
+ importPath: '@astryxdesign/core/Avatar',
+ family: 'Avatar',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/AvatarStatusDot',
+ },
+ {
+ name: 'Badge',
+ importPath: '@astryxdesign/core/Badge',
+ family: 'Badge',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Badge',
+ },
+ {
+ name: 'Banner',
+ importPath: '@astryxdesign/core/Banner',
+ family: 'Banner',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Banner',
+ },
+ {
+ name: 'BaseTypeahead',
+ importPath: '@astryxdesign/core/Typeahead',
+ family: 'Typeahead',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/BaseTypeahead',
+ },
+ {
+ name: 'Blockquote',
+ importPath: '@astryxdesign/core/Blockquote',
+ family: 'Blockquote',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Blockquote',
+ },
+ {
+ name: 'BottomSheet',
+ importPath: '@astryxdesign/core/BottomSheet',
+ family: 'BottomSheet',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/BottomSheet',
+ },
+ {
+ name: 'BottomSheetSwitcher',
+ importPath: '@astryxdesign/core/BottomSheet',
+ family: 'BottomSheet',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/BottomSheetSwitcher',
+ },
+ {
+ name: 'BreadcrumbItem',
+ importPath: '@astryxdesign/core/Breadcrumbs',
+ family: 'Breadcrumbs',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/BreadcrumbItem',
+ },
+ {
+ name: 'Breadcrumbs',
+ importPath: '@astryxdesign/core/Breadcrumbs',
+ family: 'Breadcrumbs',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Breadcrumbs',
+ },
+ {
+ name: 'Button',
+ importPath: '@astryxdesign/core/Button',
+ family: 'Button',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Button',
+ },
+ {
+ name: 'ButtonGroup',
+ importPath: '@astryxdesign/core/ButtonGroup',
+ family: 'Button',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/ButtonGroup',
+ },
+ {
+ name: 'Calendar',
+ importPath: '@astryxdesign/core/Calendar',
+ family: 'Calendar',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/Calendar',
+ },
+ {
+ name: 'Card',
+ importPath: '@astryxdesign/core/Card',
+ family: 'Card',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Card',
+ },
+ {
+ name: 'Carousel',
+ importPath: '@astryxdesign/core/Carousel',
+ family: 'Carousel',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Carousel',
+ },
+ {
+ name: 'Center',
+ importPath: '@astryxdesign/core/Center',
+ family: 'Layout',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Center',
+ },
+ {
+ name: 'ChatComposer',
+ importPath: '@astryxdesign/core/Chat',
+ family: 'Chat',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/ChatComposer',
+ },
+ {
+ name: 'ChatComposerDrawer',
+ importPath: '@astryxdesign/core/Chat',
+ family: 'Chat',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/ChatComposerDrawer',
+ },
+ {
+ name: 'ChatComposerInput',
+ importPath: '@astryxdesign/core/Chat',
+ family: 'Chat',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/ChatComposerInput',
+ },
+ {
+ name: 'ChatDictationButton',
+ importPath: '@astryxdesign/core/Chat',
+ family: 'Chat',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/ChatDictationButton',
+ },
+ {
+ name: 'ChatLayout',
+ importPath: '@astryxdesign/core/Chat',
+ family: 'Chat',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/ChatLayout',
+ },
+ {
+ name: 'ChatLayoutScrollButton',
+ importPath: '@astryxdesign/core/Chat',
+ family: 'Chat',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/ChatLayoutScrollButton',
+ },
+ {
+ name: 'ChatMessage',
+ importPath: '@astryxdesign/core/Chat',
+ family: 'Chat',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/ChatMessage',
+ },
+ {
+ name: 'ChatMessageBubble',
+ importPath: '@astryxdesign/core/Chat',
+ family: 'Chat',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/ChatMessageBubble',
+ },
+ {
+ name: 'ChatMessageList',
+ importPath: '@astryxdesign/core/Chat',
+ family: 'Chat',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/ChatMessageList',
+ },
+ {
+ name: 'ChatMessageMetadata',
+ importPath: '@astryxdesign/core/Chat',
+ family: 'Chat',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/ChatMessageMetadata',
+ },
+ {
+ name: 'ChatSendButton',
+ importPath: '@astryxdesign/core/Chat',
+ family: 'Chat',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/ChatSendButton',
+ },
+ {
+ name: 'ChatSystemMessage',
+ importPath: '@astryxdesign/core/Chat',
+ family: 'Chat',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/ChatSystemMessage',
+ },
+ {
+ name: 'ChatTokenizedText',
+ importPath: '@astryxdesign/core/Chat',
+ family: 'Chat',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/ChatTokenizedText',
+ },
+ {
+ name: 'ChatToolCalls',
+ importPath: '@astryxdesign/core/Chat',
+ family: 'Chat',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/ChatToolCalls',
+ },
+ {
+ name: 'CheckboxInput',
+ importPath: '@astryxdesign/core/CheckboxInput',
+ family: 'Checkbox',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/CheckboxInput',
+ },
+ {
+ name: 'CheckboxList',
+ importPath: '@astryxdesign/core/CheckboxList',
+ family: 'Checkbox',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/CheckboxList',
+ },
+ {
+ name: 'CheckboxListItem',
+ importPath: '@astryxdesign/core/CheckboxList',
+ family: 'Checkbox',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/CheckboxListItem',
+ },
+ {
+ name: 'Citation',
+ importPath: '@astryxdesign/core/Citation',
+ family: 'Citation',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Citation',
+ },
+ {
+ name: 'ClickableCard',
+ importPath: '@astryxdesign/core/ClickableCard',
+ family: 'Card',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/ClickableCard',
+ },
+ {
+ name: 'Code',
+ importPath: '@astryxdesign/core/Code',
+ family: 'Code',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Code',
+ },
+ {
+ name: 'CodeBlock',
+ importPath: '@astryxdesign/core/CodeBlock',
+ family: 'CodeBlock',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/CodeBlock',
+ },
+ {
+ name: 'Collapsible',
+ importPath: '@astryxdesign/core/Collapsible',
+ family: 'Collapsible',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Collapsible',
+ },
+ {
+ name: 'CollapsibleGroup',
+ importPath: '@astryxdesign/core/Collapsible',
+ family: 'Collapsible',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/CollapsibleGroup',
+ },
+ {
+ name: 'CommandPalette',
+ importPath: '@astryxdesign/core/CommandPalette',
+ family: 'CommandPalette',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/CommandPalette',
+ },
+ {
+ name: 'CommandPaletteEmpty',
+ importPath: '@astryxdesign/core/CommandPalette',
+ family: 'CommandPalette',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/CommandPaletteEmpty',
+ },
+ {
+ name: 'CommandPaletteFooter',
+ importPath: '@astryxdesign/core/CommandPalette',
+ family: 'CommandPalette',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/CommandPaletteFooter',
+ },
+ {
+ name: 'CommandPaletteGroup',
+ importPath: '@astryxdesign/core/CommandPalette',
+ family: 'CommandPalette',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/CommandPaletteGroup',
+ },
+ {
+ name: 'CommandPaletteInput',
+ importPath: '@astryxdesign/core/CommandPalette',
+ family: 'CommandPalette',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/CommandPaletteInput',
+ },
+ {
+ name: 'CommandPaletteItem',
+ importPath: '@astryxdesign/core/CommandPalette',
+ family: 'CommandPalette',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/CommandPaletteItem',
+ },
+ {
+ name: 'CommandPaletteList',
+ importPath: '@astryxdesign/core/CommandPalette',
+ family: 'CommandPalette',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/CommandPaletteList',
+ },
+ {
+ name: 'ComplexSelector',
+ importPath: '@astryxdesign/core/ComplexSelector',
+ family: 'Selector',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/ComplexSelector',
+ },
+ {
+ name: 'ContextMenu',
+ importPath: '@astryxdesign/core/ContextMenu',
+ family: 'ContextMenu',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/ContextMenu',
+ },
+ {
+ name: 'DateInput',
+ importPath: '@astryxdesign/core/DateInput',
+ family: 'DateInput',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/DateInput',
+ },
+ {
+ name: 'DateRangeInput',
+ importPath: '@astryxdesign/core/DateRangeInput',
+ family: 'DateInput',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/DateRangeInput',
+ },
+ {
+ name: 'DateTimeInput',
+ importPath: '@astryxdesign/core/DateTimeInput',
+ family: 'DateInput',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/DateTimeInput',
+ },
+ {
+ name: 'Dialog',
+ importPath: '@astryxdesign/core/Dialog',
+ family: 'Dialog',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/Dialog',
+ },
+ {
+ name: 'DialogHeader',
+ importPath: '@astryxdesign/core/Dialog',
+ family: 'Dialog',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/DialogHeader',
+ },
+ {
+ name: 'Divider',
+ importPath: '@astryxdesign/core/Divider',
+ family: 'Divider',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Divider',
+ },
+ {
+ name: 'Drawer',
+ importPath: '@astryxdesign/core/Drawer',
+ family: 'Drawer',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/Drawer',
+ },
+ {
+ name: 'DrawerHeader',
+ importPath: '@astryxdesign/core/Drawer',
+ family: 'Drawer',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/DrawerHeader',
+ },
+ {
+ name: 'DropdownMenu',
+ importPath: '@astryxdesign/core/DropdownMenu',
+ family: 'DropdownMenu',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/DropdownMenu',
+ },
+ {
+ name: 'DropdownMenuCheckboxItem',
+ importPath: '@astryxdesign/core/DropdownMenu',
+ family: 'DropdownMenu',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/DropdownMenuCheckboxItem',
+ },
+ {
+ name: 'DropdownMenuDivider',
+ importPath: '@astryxdesign/core/DropdownMenu',
+ family: 'DropdownMenu',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/DropdownMenuDivider',
+ },
+ {
+ name: 'DropdownMenuGroup',
+ importPath: '@astryxdesign/core/DropdownMenu',
+ family: 'DropdownMenu',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/DropdownMenuGroup',
+ },
+ {
+ name: 'DropdownMenuItem',
+ importPath: '@astryxdesign/core/DropdownMenu',
+ family: 'DropdownMenu',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/DropdownMenuItem',
+ },
+ {
+ name: 'DropdownMenuRadioGroup',
+ importPath: '@astryxdesign/core/DropdownMenu',
+ family: 'DropdownMenu',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/DropdownMenuRadioGroup',
+ },
+ {
+ name: 'DropdownMenuRadioItem',
+ importPath: '@astryxdesign/core/DropdownMenu',
+ family: 'DropdownMenu',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/DropdownMenuRadioItem',
+ },
+ {
+ name: 'DropdownMenuSubMenu',
+ importPath: '@astryxdesign/core/DropdownMenu',
+ family: 'DropdownMenu',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/DropdownMenuSubMenu',
+ },
+ {
+ name: 'EmptyState',
+ importPath: '@astryxdesign/core/EmptyState',
+ family: 'EmptyState',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/EmptyState',
+ },
+ {
+ name: 'Field',
+ importPath: '@astryxdesign/core/Field',
+ family: 'Field',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/Field',
+ },
+ {
+ name: 'FieldLabel',
+ importPath: '@astryxdesign/core/Field',
+ family: 'Field',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/FieldLabel',
+ },
+ {
+ name: 'FieldStatus',
+ importPath: '@astryxdesign/core/FieldStatus',
+ family: 'Field',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/FieldStatus',
+ },
+ {
+ name: 'FileInput',
+ importPath: '@astryxdesign/core/FileInput',
+ family: 'FileInput',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/FileInput',
+ },
+ {
+ name: 'FormLayout',
+ importPath: '@astryxdesign/core/FormLayout',
+ family: 'Layout',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/FormLayout',
+ },
+ {
+ name: 'Grid',
+ importPath: '@astryxdesign/core/Grid',
+ family: 'Layout',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Grid',
+ },
+ {
+ name: 'GridSpan',
+ importPath: '@astryxdesign/core/Grid',
+ family: 'Layout',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/GridSpan',
+ },
+ {
+ name: 'Heading',
+ importPath: '@astryxdesign/core/Heading',
+ family: 'Heading',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Heading',
+ },
+ {
+ name: 'HoverCard',
+ importPath: '@astryxdesign/core/HoverCard',
+ family: 'HoverCard',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/HoverCard',
+ },
+ {
+ name: 'HStack',
+ importPath: '@astryxdesign/core/HStack',
+ family: 'Layout',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/HStack',
+ },
+ {
+ name: 'Icon',
+ importPath: '@astryxdesign/core/Icon',
+ family: 'Icon',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Icon',
+ },
+ {
+ name: 'IconButton',
+ importPath: '@astryxdesign/core/IconButton',
+ family: 'Button',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/IconButton',
+ },
+ {
+ name: 'InputGroup',
+ importPath: '@astryxdesign/core/InputGroup',
+ family: 'Field',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/InputGroup',
+ },
+ {
+ name: 'InputGroupText',
+ importPath: '@astryxdesign/core/InputGroup',
+ family: 'Field',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/InputGroupText',
+ },
+ {
+ name: 'InternationalizationProvider',
+ importPath: '@astryxdesign/core/i18n',
+ family: 'Utilities',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/InternationalizationProvider',
+ },
+ {
+ name: 'Item',
+ importPath: '@astryxdesign/core/Item',
+ family: 'Item',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Item',
+ },
+ {
+ name: 'Kbd',
+ importPath: '@astryxdesign/core/Kbd',
+ family: 'Kbd',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Kbd',
+ },
+ {
+ name: 'Layout',
+ importPath: '@astryxdesign/core/Layout',
+ family: 'Layout',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Layout',
+ },
+ {
+ name: 'LayoutContent',
+ importPath: '@astryxdesign/core/Layout',
+ family: 'Layout',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/LayoutContent',
+ },
+ {
+ name: 'LayoutFooter',
+ importPath: '@astryxdesign/core/Layout',
+ family: 'Layout',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/LayoutFooter',
+ },
+ {
+ name: 'LayoutHeader',
+ importPath: '@astryxdesign/core/Layout',
+ family: 'Layout',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/LayoutHeader',
+ },
+ {
+ name: 'LayoutPanel',
+ importPath: '@astryxdesign/core/Layout',
+ family: 'Layout',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/LayoutPanel',
+ },
+ {
+ name: 'Lightbox',
+ importPath: '@astryxdesign/core/Lightbox',
+ family: 'Lightbox',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/Lightbox',
+ },
+ {
+ name: 'Link',
+ importPath: '@astryxdesign/core/Link',
+ family: 'Link',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Link',
+ },
+ {
+ name: 'LinkProvider',
+ importPath: '@astryxdesign/core/Link',
+ family: 'Utilities',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/LinkProvider',
+ },
+ {
+ name: 'List',
+ importPath: '@astryxdesign/core/List',
+ family: 'List',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/List',
+ },
+ {
+ name: 'ListItem',
+ importPath: '@astryxdesign/core/List',
+ family: 'List',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/ListItem',
+ },
+ {
+ name: 'Markdown',
+ importPath: '@astryxdesign/core/Markdown',
+ family: 'Markdown',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Markdown',
+ },
+ {
+ name: 'MediaTheme',
+ importPath: '@astryxdesign/core/theme',
+ family: 'Utilities',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/MediaTheme',
+ },
+ {
+ name: 'MetadataList',
+ importPath: '@astryxdesign/core/MetadataList',
+ family: 'MetadataList',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/MetadataList',
+ },
+ {
+ name: 'MetadataListItem',
+ importPath: '@astryxdesign/core/MetadataList',
+ family: 'MetadataList',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/MetadataListItem',
+ },
+ {
+ name: 'MobileNav',
+ importPath: '@astryxdesign/core/MobileNav',
+ family: 'Navigation',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/MobileNav',
+ },
+ {
+ name: 'MoreMenu',
+ importPath: '@astryxdesign/core/MoreMenu',
+ family: 'MoreMenu',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/MoreMenu',
+ },
+ {
+ name: 'MultiSelector',
+ importPath: '@astryxdesign/core/MultiSelector',
+ family: 'Selector',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/MultiSelector',
+ },
+ {
+ name: 'NavIcon',
+ importPath: '@astryxdesign/core/NavIcon',
+ family: 'Navigation',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/NavIcon',
+ },
+ {
+ name: 'NumberInput',
+ importPath: '@astryxdesign/core/NumberInput',
+ family: 'NumberInput',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/NumberInput',
+ },
+ {
+ name: 'Outline',
+ importPath: '@astryxdesign/core/Outline',
+ family: 'Outline',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Outline',
+ },
+ {
+ name: 'OverflowList',
+ importPath: '@astryxdesign/core/OverflowList',
+ family: 'OverflowList',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/OverflowList',
+ },
+ {
+ name: 'Overlay',
+ importPath: '@astryxdesign/core/Overlay',
+ family: 'Overlay',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/Overlay',
+ },
+ {
+ name: 'Pagination',
+ importPath: '@astryxdesign/core/Pagination',
+ family: 'Pagination',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Pagination',
+ },
+ {
+ name: 'Popover',
+ importPath: '@astryxdesign/core/Popover',
+ family: 'Popover',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/Popover',
+ },
+ {
+ name: 'PowerSearch',
+ importPath: '@astryxdesign/core/PowerSearch',
+ family: 'PowerSearch',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/PowerSearch',
+ },
+ {
+ name: 'ProgressBar',
+ importPath: '@astryxdesign/core/ProgressBar',
+ family: 'ProgressBar',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/ProgressBar',
+ },
+ {
+ name: 'RadioList',
+ importPath: '@astryxdesign/core/RadioList',
+ family: 'Radio',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/RadioList',
+ },
+ {
+ name: 'RadioListItem',
+ importPath: '@astryxdesign/core/RadioList',
+ family: 'Radio',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/RadioListItem',
+ },
+ {
+ name: 'ScrollableArea',
+ importPath: '@astryxdesign/core/ScrollableArea',
+ family: 'ScrollableArea',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/ScrollableArea',
+ },
+ {
+ name: 'Section',
+ importPath: '@astryxdesign/core/Section',
+ family: 'Layout',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Section',
+ },
+ {
+ name: 'SegmentedControl',
+ importPath: '@astryxdesign/core/SegmentedControl',
+ family: 'SegmentedControl',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/SegmentedControl',
+ },
+ {
+ name: 'SegmentedControlItem',
+ importPath: '@astryxdesign/core/SegmentedControl',
+ family: 'SegmentedControl',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/SegmentedControlItem',
+ },
+ {
+ name: 'SelectableCard',
+ importPath: '@astryxdesign/core/SelectableCard',
+ family: 'Card',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/SelectableCard',
+ },
+ {
+ name: 'Selector',
+ importPath: '@astryxdesign/core/Selector',
+ family: 'Selector',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/Selector',
+ },
+ {
+ name: 'SelectorOption',
+ importPath: '@astryxdesign/core/Selector',
+ family: 'Selector',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/SelectorOption',
+ },
+ {
+ name: 'SideNav',
+ importPath: '@astryxdesign/core/SideNav',
+ family: 'Navigation',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/SideNav',
+ },
+ {
+ name: 'SideNavCollapseButton',
+ importPath: '@astryxdesign/core/SideNav',
+ family: 'Navigation',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/SideNavCollapseButton',
+ },
+ {
+ name: 'SideNavHeading',
+ importPath: '@astryxdesign/core/SideNav',
+ family: 'Navigation',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/SideNavHeading',
+ },
+ {
+ name: 'SideNavItem',
+ importPath: '@astryxdesign/core/SideNav',
+ family: 'Navigation',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/SideNavItem',
+ },
+ {
+ name: 'SideNavSection',
+ importPath: '@astryxdesign/core/SideNav',
+ family: 'Navigation',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/SideNavSection',
+ },
+ {
+ name: 'Skeleton',
+ importPath: '@astryxdesign/core/Skeleton',
+ family: 'Skeleton',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Skeleton',
+ },
+ {
+ name: 'Slider',
+ importPath: '@astryxdesign/core/Slider',
+ family: 'Slider',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/Slider',
+ },
+ {
+ name: 'Spinner',
+ importPath: '@astryxdesign/core/Spinner',
+ family: 'Spinner',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Spinner',
+ },
+ {
+ name: 'Stack',
+ importPath: '@astryxdesign/core/Stack',
+ family: 'Layout',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Stack',
+ },
+ {
+ name: 'StatusDot',
+ importPath: '@astryxdesign/core/StatusDot',
+ family: 'StatusDot',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/StatusDot',
+ },
+ {
+ name: 'Step',
+ importPath: '@astryxdesign/core/Stepper',
+ family: 'Stepper',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Step',
+ },
+ {
+ name: 'Stepper',
+ importPath: '@astryxdesign/core/Stepper',
+ family: 'Stepper',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Stepper',
+ },
+ {
+ name: 'Switch',
+ importPath: '@astryxdesign/core/Switch',
+ family: 'Switch',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/Switch',
+ },
+ {
+ name: 'Tab',
+ importPath: '@astryxdesign/core/TabList',
+ family: 'Tabs',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Tab',
+ },
+ {
+ name: 'Table',
+ importPath: '@astryxdesign/core/Table',
+ family: 'Table',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Table',
+ },
+ {
+ name: 'TableBody',
+ importPath: '@astryxdesign/core/Table',
+ family: 'Table',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/TableBody',
+ },
+ {
+ name: 'TableCell',
+ importPath: '@astryxdesign/core/Table',
+ family: 'Table',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/TableCell',
+ },
+ {
+ name: 'TableFooter',
+ importPath: '@astryxdesign/core/Table',
+ family: 'Table',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/TableFooter',
+ },
+ {
+ name: 'TableHeader',
+ importPath: '@astryxdesign/core/Table',
+ family: 'Table',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/TableHeader',
+ },
+ {
+ name: 'TableHeaderCell',
+ importPath: '@astryxdesign/core/Table',
+ family: 'Table',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/TableHeaderCell',
+ },
+ {
+ name: 'TableRow',
+ importPath: '@astryxdesign/core/Table',
+ family: 'Table',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/TableRow',
+ },
+ {
+ name: 'TableSelectionToolbar',
+ importPath: '@astryxdesign/core/Table',
+ family: 'Table',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/TableSelectionToolbar',
+ },
+ {
+ name: 'TabList',
+ importPath: '@astryxdesign/core/TabList',
+ family: 'Tabs',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/TabList',
+ },
+ {
+ name: 'TabMenu',
+ importPath: '@astryxdesign/core/TabList',
+ family: 'Tabs',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/TabMenu',
+ },
+ {
+ name: 'Text',
+ importPath: '@astryxdesign/core/Text',
+ family: 'Text',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Text',
+ },
+ {
+ name: 'TextArea',
+ importPath: '@astryxdesign/core/TextArea',
+ family: 'TextArea',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/TextArea',
+ },
+ {
+ name: 'TextInput',
+ importPath: '@astryxdesign/core/TextInput',
+ family: 'TextInput',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/TextInput',
+ },
+ {
+ name: 'Theme',
+ importPath: '@astryxdesign/core/theme',
+ family: 'Utilities',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Theme',
+ },
+ {
+ name: 'Thumbnail',
+ importPath: '@astryxdesign/core/Thumbnail',
+ family: 'Thumbnail',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Thumbnail',
+ },
+ {
+ name: 'TimeInput',
+ importPath: '@astryxdesign/core/TimeInput',
+ family: 'TimeInput',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/TimeInput',
+ },
+ {
+ name: 'Timer',
+ importPath: '@astryxdesign/core/Timer',
+ family: 'Timer',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Timer',
+ },
+ {
+ name: 'Timestamp',
+ importPath: '@astryxdesign/core/Timestamp',
+ family: 'Timestamp',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Timestamp',
+ },
+ {
+ name: 'Toast',
+ importPath: '@astryxdesign/core/Toast',
+ family: 'Toast',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/Toast',
+ },
+ {
+ name: 'ToggleButton',
+ importPath: '@astryxdesign/core/ToggleButton',
+ family: 'Button',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/ToggleButton',
+ },
+ {
+ name: 'ToggleButtonGroup',
+ importPath: '@astryxdesign/core/ToggleButton',
+ family: 'Button',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/ToggleButtonGroup',
+ },
+ {
+ name: 'Token',
+ importPath: '@astryxdesign/core/Token',
+ family: 'Token',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Token',
+ },
+ {
+ name: 'Tokenizer',
+ importPath: '@astryxdesign/core/Tokenizer',
+ family: 'Tokenizer',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/Tokenizer',
+ },
+ {
+ name: 'Toolbar',
+ importPath: '@astryxdesign/core/Toolbar',
+ family: 'Toolbar',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/Toolbar',
+ },
+ {
+ name: 'Tooltip',
+ importPath: '@astryxdesign/core/Tooltip',
+ family: 'Tooltip',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/Tooltip',
+ },
+ {
+ name: 'TopNav',
+ importPath: '@astryxdesign/core/TopNav',
+ family: 'Navigation',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/TopNav',
+ },
+ {
+ name: 'TopNavHeading',
+ importPath: '@astryxdesign/core/TopNav',
+ family: 'Navigation',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/TopNavHeading',
+ },
+ {
+ name: 'TopNavItem',
+ importPath: '@astryxdesign/core/TopNav',
+ family: 'Navigation',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/TopNavItem',
+ },
+ {
+ name: 'TopNavMegaMenu',
+ importPath: '@astryxdesign/core/TopNav',
+ family: 'Navigation',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/TopNavMegaMenu',
+ },
+ {
+ name: 'TopNavMegaMenuFeaturedCard',
+ importPath: '@astryxdesign/core/TopNav',
+ family: 'Navigation',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/TopNavMegaMenuFeaturedCard',
+ },
+ {
+ name: 'TopNavMegaMenuItem',
+ importPath: '@astryxdesign/core/TopNav',
+ family: 'Navigation',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/TopNavMegaMenuItem',
+ },
+ {
+ name: 'TopNavMenu',
+ importPath: '@astryxdesign/core/TopNav',
+ family: 'Navigation',
+ group: 'overlay',
+ docsUrl: 'https://astryx.atmeta.com/components/TopNavMenu',
+ },
+ {
+ name: 'TreeList',
+ importPath: '@astryxdesign/core/TreeList',
+ family: 'TreeList',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/TreeList',
+ },
+ {
+ name: 'Typeahead',
+ importPath: '@astryxdesign/core/Typeahead',
+ family: 'Typeahead',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/Typeahead',
+ },
+ {
+ name: 'TypeaheadItem',
+ importPath: '@astryxdesign/core/Typeahead',
+ family: 'Typeahead',
+ group: 'form',
+ docsUrl: 'https://astryx.atmeta.com/components/TypeaheadItem',
+ },
+ {
+ name: 'VisuallyHidden',
+ importPath: '@astryxdesign/core/VisuallyHidden',
+ family: 'VisuallyHidden',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/VisuallyHidden',
+ },
+ {
+ name: 'VStack',
+ importPath: '@astryxdesign/core/VStack',
+ family: 'Layout',
+ group: 'base',
+ docsUrl: 'https://astryx.atmeta.com/components/VStack',
+ },
+]
diff --git a/src/demos/ActionsDemo.tsx b/src/demos/ActionsDemo.tsx
new file mode 100644
index 0000000..1f5f1ad
--- /dev/null
+++ b/src/demos/ActionsDemo.tsx
@@ -0,0 +1,83 @@
+import { useState } from 'react'
+import { Button } from '@astryxdesign/core/Button'
+import { HStack } from '@astryxdesign/core/HStack'
+import { Icon } from '@astryxdesign/core/Icon'
+import { IconButton } from '@astryxdesign/core/IconButton'
+import { Kbd } from '@astryxdesign/core/Kbd'
+import { Link } from '@astryxdesign/core/Link'
+import {
+ SegmentedControl,
+ SegmentedControlItem,
+} from '@astryxdesign/core/SegmentedControl'
+import { Text } from '@astryxdesign/core/Text'
+import { ToggleButton, ToggleButtonGroup } from '@astryxdesign/core/ToggleButton'
+import { Toolbar } from '@astryxdesign/core/Toolbar'
+import { VStack } from '@astryxdesign/core/VStack'
+import { Heart, List, LayoutGrid } from 'lucide-react'
+
+export function ActionsDemo() {
+ const [layout, setLayout] = useState('网格')
+ const [view, setView] = useState('概览')
+
+ return (
+ <VStack gap={4}>
+ <Toolbar
+ label="常用操作"
+ startContent={
+ <HStack gap={2} wrap="wrap">
+ <Button label="主要操作" variant="primary" size="sm" />
+ <Button label="次要操作" variant="secondary" size="sm" />
+ <Button label="幽灵按钮" variant="ghost" size="sm" />
+ </HStack>
+ }
+ endContent={
+ <IconButton
+ label="收藏"
+ tooltip="收藏"
+ variant="ghost"
+ icon={<Icon icon={Heart} />}
+ />
+ }
+ />
+
+ <HStack gap={4} wrap="wrap" vAlign="center">
+ <ToggleButtonGroup
+ label="调整视图布局"
+ value={layout}
+ onChange={(value) => setLayout((value as string | null) ?? '网格')}
+ size="sm">
+ <ToggleButton
+ label="网格"
+ value="网格"
+ icon={<Icon icon={LayoutGrid} size="sm" />}
+ />
+ <ToggleButton
+ label="列表"
+ value="列表"
+ icon={<Icon icon={List} size="sm" />}
+ />
+ </ToggleButtonGroup>
+
+ <SegmentedControl
+ label="切换视图"
+ value={view}
+ onChange={setView}
+ size="sm">
+ <SegmentedControlItem label="概览" value="概览" />
+ <SegmentedControlItem label="活动" value="活动" />
+ <SegmentedControlItem label="成员" value="成员" />
+ </SegmentedControl>
+ </HStack>
+
+ <HStack gap={3} vAlign="center" wrap="wrap">
+ <Link href="/about" isStandalone>
+ 查看关于页
+ </Link>
+ <Text type="supporting" size="sm">
+ 快捷键
+ </Text>
+ <Kbd keys="mod+k" />
+ </HStack>
+ </VStack>
+ )
+}
diff --git a/src/demos/ChatDemo.tsx b/src/demos/ChatDemo.tsx
new file mode 100644
index 0000000..cf371f2
--- /dev/null
+++ b/src/demos/ChatDemo.tsx
@@ -0,0 +1,50 @@
+import {
+ ChatMessage,
+ ChatMessageBubble,
+ ChatMessageList,
+ ChatMessageMetadata,
+} from '@astryxdesign/core/Chat'
+import { Text } from '@astryxdesign/core/Text'
+import { Timestamp } from '@astryxdesign/core/Timestamp'
+
+export function ChatDemo() {
+ return (
+ <ChatMessageList>
+ <ChatMessage sender="user">
+ <ChatMessageBubble group="first">
+ 我把权限模块重构好了。
+ </ChatMessageBubble>
+ <ChatMessageBubble
+ group="last"
+ metadata={
+ <ChatMessageMetadata
+ timestamp={
+ <Timestamp value="2026-04-28T14:30:00" format="time" />
+ }
+ status="read"
+ />
+ }>
+ 可以帮我检查一下令牌校验的改动吗?
+ </ChatMessageBubble>
+ </ChatMessage>
+ <ChatMessage sender="assistant">
+ <ChatMessageBubble
+ variant="ghost"
+ metadata={
+ <ChatMessageMetadata
+ timestamp={
+ <Timestamp value="2026-04-28T14:31:00" format="time" />
+ }
+ footer={
+ <Text type="supporting" color="secondary">
+ Astryx 助手
+ </Text>
+ }
+ />
+ }>
+ 看过了,刷新令牌轮换和错误处理都覆盖到了,可以发布。
+ </ChatMessageBubble>
+ </ChatMessage>
+ </ChatMessageList>
+ )
+}
diff --git a/src/demos/CommandPaletteDemo.tsx b/src/demos/CommandPaletteDemo.tsx
new file mode 100644
index 0000000..e3c4119
--- /dev/null
+++ b/src/demos/CommandPaletteDemo.tsx
@@ -0,0 +1,46 @@
+import { useMemo, useState } from 'react'
+import { Button } from '@astryxdesign/core/Button'
+import { CommandPalette } from '@astryxdesign/core/CommandPalette'
+import { HStack } from '@astryxdesign/core/HStack'
+import { useToast } from '@astryxdesign/core/Toast'
+import { createStaticSource } from '@astryxdesign/core/Typeahead'
+
+export function CommandPaletteDemo() {
+ const [open, setOpen] = useState(false)
+ const showToast = useToast()
+ const source = useMemo(
+ () =>
+ createStaticSource([
+ { id: 'home', label: '回到概览' },
+ { id: 'components', label: '浏览组件' },
+ { id: 'about', label: '查看关于' },
+ { id: 'theme', label: '切换主题' },
+ ]),
+ [],
+ )
+
+ return (
+ <>
+ <HStack gap={2} wrap="wrap">
+ <Button
+ label="打开命令面板"
+ variant="primary"
+ onClick={() => setOpen(true)}
+ />
+ <Button
+ label="显示通知"
+ variant="secondary"
+ onClick={() =>
+ showToast({ body: '设置已保存', type: 'info' })
+ }
+ />
+ </HStack>
+ <CommandPalette
+ isOpen={open}
+ onOpenChange={setOpen}
+ searchSource={source}
+ label="命令面板"
+ />
+ </>
+ )
+}
diff --git a/src/demos/DataDemo.tsx b/src/demos/DataDemo.tsx
new file mode 100644
index 0000000..f2a5be7
--- /dev/null
+++ b/src/demos/DataDemo.tsx
@@ -0,0 +1,72 @@
+import { Avatar } from '@astryxdesign/core/Avatar'
+import { Badge } from '@astryxdesign/core/Badge'
+import { Divider } from '@astryxdesign/core/Divider'
+import { HStack } from '@astryxdesign/core/HStack'
+import { List, ListItem } from '@astryxdesign/core/List'
+import { Text } from '@astryxdesign/core/Text'
+import { Table, pixel, proportional } from '@astryxdesign/core/Table'
+import type { TableColumn } from '@astryxdesign/core/Table'
+import { VStack } from '@astryxdesign/core/VStack'
+
+interface Member extends Record<string, unknown> {
+ id: string
+ name: string
+ role: string
+ projects: number
+}
+
+const MEMBERS: Member[] = [
+ { id: '1', name: '林晓', role: '产品设计', projects: 4 },
+ { id: '2', name: '陈默', role: '前端开发', projects: 7 },
+ { id: '3', name: '周雨', role: '内容运营', projects: 3 },
+]
+
+const COLUMNS: TableColumn<Member>[] = [
+ { key: 'name', header: '成员', width: proportional(1) },
+ { key: 'role', header: '角色', width: proportional(1) },
+ { key: 'projects', header: '进行中', width: pixel(90) },
+]
+
+export function DataDemo() {
+ return (
+ <VStack gap={4}>
+ <List hasDividers>
+ <ListItem
+ label="林晓"
+ description="产品设计 · 4 个项目"
+ startContent={<Avatar name="林晓" size="sm" />}
+ endContent={<Badge variant="blue" label="3" />}
+ />
+ <ListItem
+ label="陈默"
+ description="前端开发 · 7 个项目"
+ startContent={<Avatar name="陈默" size="sm" />}
+ endContent={<Badge variant="teal" label="8" />}
+ />
+ <ListItem
+ label="周雨"
+ description="内容运营 · 3 个项目"
+ startContent={<Avatar name="周雨" size="sm" />}
+ endContent={<Badge variant="purple" label="2" />}
+ />
+ </List>
+
+ <Divider />
+
+ <VStack gap={2}>
+ <HStack gap={2} vAlign="center">
+ <Text type="label">团队进度</Text>
+ <Text type="supporting" size="sm">
+ 数据表格
+ </Text>
+ </HStack>
+ <Table
+ data={MEMBERS}
+ columns={COLUMNS}
+ idKey="id"
+ density="compact"
+ />
+ </VStack>
+ </VStack>
+ )
+}
diff --git a/src/demos/DialogsDemo.tsx b/src/demos/DialogsDemo.tsx
new file mode 100644
index 0000000..1b887df
--- /dev/null
+++ b/src/demos/DialogsDemo.tsx
@@ -0,0 +1,95 @@
+import { useState } from 'react'
+import { BottomSheet } from '@astryxdesign/core/BottomSheet'
+import { Button } from '@astryxdesign/core/Button'
+import { Dialog, DialogHeader } from '@astryxdesign/core/Dialog'
+import { Drawer, DrawerHeader } from '@astryxdesign/core/Drawer'
+import { HStack } from '@astryxdesign/core/HStack'
+import { Layout, LayoutContent } from '@astryxdesign/core/Layout'
+import { Section } from '@astryxdesign/core/Section'
+import { Heading } from '@astryxdesign/core/Heading'
+import { Text } from '@astryxdesign/core/Text'
+import { VStack } from '@astryxdesign/core/VStack'
+
+type ActiveLayer = 'dialog' | 'drawer' | 'sheet' | null
+
+export function DialogsDemo() {
+ const [active, setActive] = useState<ActiveLayer>(null)
+
+ return (
+ <>
+ <HStack gap={2} wrap="wrap">
+ <Button label="打开对话框" onClick={() => setActive('dialog')} />
+ <Button
+ label="打开抽屉"
+ variant="secondary"
+ onClick={() => setActive('drawer')}
+ />
+ <Button
+ label="打开底部面板"
+ variant="secondary"
+ onClick={() => setActive('sheet')}
+ />
+ </HStack>
+
+ <Dialog
+ isOpen={active === 'dialog'}
+ onOpenChange={(open) => setActive(open ? 'dialog' : null)}>
+ <Layout
+ header={
+ <DialogHeader
+ title="确认发布"
+ onOpenChange={(open) => setActive(open ? 'dialog' : null)}
+ />
+ }
+ content={
+ <LayoutContent>
+ <Text type="body" as="p">
+ 发布后,当前版本会立即对全部用户可见。你仍然可以稍后回滚。
+ </Text>
+ </LayoutContent>
+ }
+ />
+ </Dialog>
+
+ <Drawer
+ isOpen={active === 'drawer'}
+ onOpenChange={(open) => setActive(open ? 'drawer' : null)}
+ label="项目详情"
+ width={360}>
+ <Layout
+ header={
+ <DrawerHeader
+ title="项目详情"
+ onOpenChange={(open) => setActive(open ? 'drawer' : null)}
+ />
+ }
+ content={
+ <LayoutContent>
+ <VStack gap={2}>
+ <Heading level={3}>Astryx 起始模板</Heading>
+ <Text type="body" color="secondary" as="p">
+ 抽屉适合承载不打断主流程的详细信息和次要操作。
+ </Text>
+ </VStack>
+ </LayoutContent>
+ }
+ />
+ </Drawer>
+
+ <BottomSheet
+ isOpen={active === 'sheet'}
+ onOpenChange={(open) => setActive(open ? 'sheet' : null)}
+ label="筛选">
+ <Section padding={4}>
+ <VStack gap={3}>
+ <Heading level={3}>筛选条件</Heading>
+ <Text type="body" color="secondary" as="p">
+ 底部面板适合移动端上的快速筛选和短操作。
+ </Text>
+ <Button label="应用筛选" onClick={() => setActive(null)} />
+ </VStack>
+ </Section>
+ </BottomSheet>
+ </>
+ )
+}
diff --git a/src/demos/FeedbackDemo.tsx b/src/demos/FeedbackDemo.tsx
new file mode 100644
index 0000000..2909878
--- /dev/null
+++ b/src/demos/FeedbackDemo.tsx
@@ -0,0 +1,35 @@
+import { Banner } from '@astryxdesign/core/Banner'
+import { HStack } from '@astryxdesign/core/HStack'
+import { ProgressBar } from '@astryxdesign/core/ProgressBar'
+import { Skeleton } from '@astryxdesign/core/Skeleton'
+import { Spinner } from '@astryxdesign/core/Spinner'
+import { Text } from '@astryxdesign/core/Text'
+import { VStack } from '@astryxdesign/core/VStack'
+
+export function FeedbackDemo() {
+ return (
+ <VStack gap={4}>
+ <Banner
+ status="info"
+ title="新版本已经准备好"
+ description="刷新页面后即可使用最新的组件与主题。"
+ />
+ <VStack gap={2}>
+ <Text type="label">工作区初始化</Text>
+ <ProgressBar label="工作区初始化" value={68} hasValueLabel isLabelHidden />
+ </VStack>
+ <HStack gap={4} vAlign="center" wrap="wrap">
+ <HStack gap={2} vAlign="center">
+ <Spinner size="sm" />
+ <Text type="supporting" size="sm">
+ 正在同步数据…
+ </Text>
+ </HStack>
+ <VStack gap={1} width={160}>
+ <Skeleton width="100%" height={14} index={0} />
+ <Skeleton width="70%" height={14} index={1} />
+ </VStack>
+ </HStack>
+ </VStack>
+ )
+}
diff --git a/src/demos/InputsDemo.tsx b/src/demos/InputsDemo.tsx
new file mode 100644
index 0000000..47c9724
--- /dev/null
+++ b/src/demos/InputsDemo.tsx
@@ -0,0 +1,58 @@
+import { useState } from 'react'
+import { Banner } from '@astryxdesign/core/Banner'
+import { Button } from '@astryxdesign/core/Button'
+import { HStack } from '@astryxdesign/core/HStack'
+import { Switch } from '@astryxdesign/core/Switch'
+import { TextArea } from '@astryxdesign/core/TextArea'
+import { TextInput } from '@astryxdesign/core/TextInput'
+import { VStack } from '@astryxdesign/core/VStack'
+
+export function InputsDemo() {
+ const [name, setName] = useState('模板项目')
+ const [description, setDescription] = useState(
+ '一个使用 Astryx 搭建的 React 应用。',
+ )
+ const [updates, setUpdates] = useState(true)
+ const [saved, setSaved] = useState(false)
+
+ return (
+ <VStack gap={4}>
+ <TextInput
+ label="项目名称"
+ value={name}
+ onChange={setName}
+ isRequired
+ width="100%"
+ />
+ <TextArea
+ label="项目描述"
+ value={description}
+ onChange={setDescription}
+ rows={3}
+ width="100%"
+ />
+ <Switch
+ label="版本更新提醒"
+ description="有新版本时发送一封简短的邮件。"
+ value={updates}
+ onChange={setUpdates}
+ />
+ <HStack gap={2} hAlign="end">
+ <Button
+ label="保存设置"
+ variant="primary"
+ onClick={() => setSaved(true)}
+ />
+ </HStack>
+ {saved ? (
+ <Banner
+ status="success"
+ title="设置已保存"
+ description="表单值只保存在当前演示的内存中。"
+ isDismissable
+ onDismiss={() => setSaved(false)}
+ />
+ ) : null}
+ </VStack>
+ )
+}
diff --git a/src/demos/LayoutDemo.tsx b/src/demos/LayoutDemo.tsx
new file mode 100644
index 0000000..53da1bd
--- /dev/null
+++ b/src/demos/LayoutDemo.tsx
@@ -0,0 +1,52 @@
+import { useState } from 'react'
+import { Collapsible, CollapsibleGroup } from '@astryxdesign/core/Collapsible'
+import { Grid } from '@astryxdesign/core/Grid'
+import { Heading } from '@astryxdesign/core/Heading'
+import { SelectableCard } from '@astryxdesign/core/SelectableCard'
+import { Text } from '@astryxdesign/core/Text'
+import { VStack } from '@astryxdesign/core/VStack'
+
+const PLANS = [
+ { id: 'basic', name: '基础版', price: '¥9/月', desc: '适合个人项目' },
+ { id: 'pro', name: '专业版', price: '¥29/月', desc: '适合小型团队' },
+ { id: 'team', name: '团队版', price: '¥99/月', desc: '适合协作组织' },
+]
+
+export function LayoutDemo() {
+ const [selected, setSelected] = useState('pro')
+
+ return (
+ <VStack gap={4}>
+ <Grid columns={{ minWidth: 140, max: 3 }} gap={3}>
+ {PLANS.map((plan) => (
+ <SelectableCard
+ key={plan.id}
+ label={plan.name}
+ isSelected={selected === plan.id}
+ onChange={() => setSelected(plan.id)}>
+ <VStack gap={1}>
+ <Heading level={4}>{plan.name}</Heading>
+ <Text type="large" weight="bold">
+ {plan.price}
+ </Text>
+ <Text type="supporting" size="sm">
+ {plan.desc}
+ </Text>
+ </VStack>
+ </SelectableCard>
+ ))}
+ </Grid>
+
+ <CollapsibleGroup type="single" defaultValue="layout">
+ <Collapsible
+ value="layout"
+ trigger={<Text type="label">这个布局是如何组织的?</Text>}>
+ <Text type="body" color="secondary" as="p">
+ 外层使用 Grid 负责响应式列数,SelectableCard 负责可选中的卡片语义,Collapsible
+ 负责按需展开说明。所有间距都来自同一套 token。
+ </Text>
+ </Collapsible>
+ </CollapsibleGroup>
+ </VStack>
+ )
+}
diff --git a/src/demos/MenusDemo.tsx b/src/demos/MenusDemo.tsx
new file mode 100644
index 0000000..c928e6f
--- /dev/null
+++ b/src/demos/MenusDemo.tsx
@@ -0,0 +1,97 @@
+import { useState } from 'react'
+import { Avatar } from '@astryxdesign/core/Avatar'
+import { Button } from '@astryxdesign/core/Button'
+import { Card } from '@astryxdesign/core/Card'
+import { ContextMenu } from '@astryxdesign/core/ContextMenu'
+import { DropdownMenu } from '@astryxdesign/core/DropdownMenu'
+import { HStack } from '@astryxdesign/core/HStack'
+import { Heading } from '@astryxdesign/core/Heading'
+import { HoverCard } from '@astryxdesign/core/HoverCard'
+import { MoreMenu } from '@astryxdesign/core/MoreMenu'
+import { Popover } from '@astryxdesign/core/Popover'
+import { Text } from '@astryxdesign/core/Text'
+import { Tooltip } from '@astryxdesign/core/Tooltip'
+import { VStack } from '@astryxdesign/core/VStack'
+
+export function MenusDemo() {
+ const [popoverOpen, setPopoverOpen] = useState(false)
+
+ return (
+ <VStack gap={4}>
+ <HStack gap={2} wrap="wrap" vAlign="center">
+ <DropdownMenu
+ button={{ label: '操作菜单' }}
+ items={[
+ { label: '编辑', onClick: () => {} },
+ { label: '复制', onClick: () => {} },
+ { label: '删除', onClick: () => {} },
+ ]}
+ />
+ <MoreMenu
+ items={[
+ { label: '导出', onClick: () => {} },
+ { label: '分享', onClick: () => {} },
+ { label: '归档', onClick: () => {} },
+ ]}
+ />
+ <Tooltip content="这是一段提示文字" placement="above">
+ <Button label="悬停查看提示" variant="secondary" />
+ </Tooltip>
+ <Popover
+ isOpen={popoverOpen}
+ onOpenChange={setPopoverOpen}
+ hasAutoFocus={false}
+ placement="below"
+ label="设置"
+ width={260}
+ content={
+ <VStack gap={2}>
+ <Heading level={4}>快捷设置</Heading>
+ <Text type="body" color="secondary" as="p">
+ 弹层可以承载简短设置、筛选或上下文信息。
+ </Text>
+ </VStack>
+ }>
+ <Button label="打开浮层" variant="secondary" />
+ </Popover>
+ <HoverCard
+ placement="above"
+ content={
+ <VStack gap={2} width={220}>
+ <HStack gap={2} vAlign="center">
+ <Avatar name="林晓" size="md" />
+ <VStack gap={0}>
+ <Heading level={5}>林晓</Heading>
+ <Text type="supporting" size="sm">
+ 产品设计
+ </Text>
+ </VStack>
+ </HStack>
+ <Text type="body" color="secondary" as="p">
+ 专注于设计系统与复杂交互体验。
+ </Text>
+ </VStack>
+ }>
+ <Button label="悬停查看资料" variant="ghost" />
+ </HoverCard>
+ </HStack>
+
+ <ContextMenu
+ presentation="adaptive"
+ items={[
+ { label: '剪切', onClick: () => {} },
+ { label: '复制', onClick: () => {} },
+ { label: '粘贴', onClick: () => {} },
+ ]}>
+ <Card padding={4}>
+ <VStack gap={1} hAlign="center">
+ <Text type="label">右键或长按此区域</Text>
+ <Text type="supporting" size="sm">
+ 打开上下文菜单
+ </Text>
+ </VStack>
+ </Card>
+ </ContextMenu>
+ </VStack>
+ )
+}
diff --git a/src/demos/NavigationDemo.tsx b/src/demos/NavigationDemo.tsx
new file mode 100644
index 0000000..ebc75f2
--- /dev/null
+++ b/src/demos/NavigationDemo.tsx
@@ -0,0 +1,35 @@
+import { useState } from 'react'
+import { BreadcrumbItem, Breadcrumbs } from '@astryxdesign/core/Breadcrumbs'
+import { VStack } from '@astryxdesign/core/VStack'
+import { Step, Stepper } from '@astryxdesign/core/Stepper'
+import { Tab, TabList } from '@astryxdesign/core/TabList'
+
+export function NavigationDemo() {
+ const [tab, setTab] = useState('overview')
+ const [step, setStep] = useState(2)
+
+ return (
+ <VStack gap={5}>
+ <Breadcrumbs>
+ <BreadcrumbItem href="/">首页</BreadcrumbItem>
+ <BreadcrumbItem href="/components">组件</BreadcrumbItem>
+ <BreadcrumbItem isCurrent>导航示例</BreadcrumbItem>
+ </Breadcrumbs>
+
+ <TabList value={tab} onChange={setTab} hasDivider>
+ <Tab value="overview" label="概览" />
+ <Tab value="activity" label="活动" />
+ <Tab value="settings" label="设置" />
+ </TabList>
+
+ <VStack width="100%">
+ <Stepper activeStep={step} orientation="horizontal" onStepClick={setStep}>
+ <Step step={0} label="购物车" />
+ <Step step={1} label="配送" />
+ <Step step={2} label="支付" />
+ <Step step={3} label="完成" />
+ </Stepper>
+ </VStack>
+ </VStack>
+ )
+}
diff --git a/src/demos/ProvidersDemo.tsx b/src/demos/ProvidersDemo.tsx
new file mode 100644
index 0000000..48f5a9f
--- /dev/null
+++ b/src/demos/ProvidersDemo.tsx
@@ -0,0 +1,29 @@
+import { Button } from '@astryxdesign/core/Button'
+import { HStack } from '@astryxdesign/core/HStack'
+import { Link } from '@astryxdesign/core/Link'
+import { Text } from '@astryxdesign/core/Text'
+import { VStack } from '@astryxdesign/core/VStack'
+import { useThemeMode } from '../theme/useThemeMode'
+
+export function ProvidersDemo() {
+ const { mode, toggleMode } = useThemeMode()
+ const nextModeLabel = mode === 'light' ? '切换到深色主题' : '切换到浅色主题'
+
+ return (
+ <VStack gap={3}>
+ <Text type="body" color="secondary" as="p">
+ 当前页面已经由 Theme、LinkProvider 和 InternationalizationProvider
+ 包裹。切换主题或点击站内链接即可看到实际效果。
+ </Text>
+ <HStack gap={3} wrap="wrap" vAlign="center">
+ <Button label={nextModeLabel} variant="primary" onClick={toggleMode} />
+ <Link href="/about" isStandalone>
+ 通过 LinkProvider 打开的站内链接
+ </Link>
+ </HStack>
+ <Text type="supporting" size="sm">
+ Astryx 内置文案同样是中文,例如「必填」「关闭」「跳至内容」。
+ </Text>
+ </VStack>
+ )
+}
diff --git a/src/demos/SelectionDemo.tsx b/src/demos/SelectionDemo.tsx
new file mode 100644
index 0000000..41ca918
--- /dev/null
+++ b/src/demos/SelectionDemo.tsx
@@ -0,0 +1,55 @@
+import { useState } from 'react'
+import {
+ CheckboxList,
+ CheckboxListItem,
+} from '@astryxdesign/core/CheckboxList'
+import { Grid } from '@astryxdesign/core/Grid'
+import { RadioList, RadioListItem } from '@astryxdesign/core/RadioList'
+import { Slider } from '@astryxdesign/core/Slider'
+
+export function SelectionDemo() {
+ const [notifications, setNotifications] = useState<string[]>(['email'])
+ const [plan, setPlan] = useState('team')
+ const [volume, setVolume] = useState(60)
+
+ return (
+ <Grid columns={{ minWidth: 220, max: 2 }} gap={5}>
+ <CheckboxList
+ label="通知方式"
+ description="可以选择多个渠道。"
+ value={notifications}
+ onChange={setNotifications}
+ hasDividers>
+ <CheckboxListItem
+ label="邮件"
+ value="email"
+ description="每周一发送摘要"
+ />
+ <CheckboxListItem
+ label="推送"
+ value="push"
+ description="在设备上即时提醒"
+ />
+ <CheckboxListItem
+ label="短信"
+ value="sms"
+ description="仅用于重要通知"
+ />
+ </CheckboxList>
+
+ <RadioList label="当前套餐" value={plan} onChange={setPlan}>
+ <RadioListItem label="个人版" value="personal" />
+ <RadioListItem label="团队版" value="team" />
+ <RadioListItem label="企业版" value="enterprise" />
+ </RadioList>
+
+ <Slider
+ label="提醒音量"
+ value={volume}
+ onChange={setVolume}
+ width="100%"
+ valueDisplay="text"
+ />
+ </Grid>
+ )
+}
diff --git a/src/demos/SelectorsDemo.tsx b/src/demos/SelectorsDemo.tsx
new file mode 100644
index 0000000..5fd299f
--- /dev/null
+++ b/src/demos/SelectorsDemo.tsx
@@ -0,0 +1,33 @@
+import { useState } from 'react'
+import { DateInput } from '@astryxdesign/core/DateInput'
+import { Grid } from '@astryxdesign/core/Grid'
+import { Selector } from '@astryxdesign/core/Selector'
+
+type DateString =
+ `${number}${number}${number}${number}-${number}${number}-${number}${number}`
+
+export function SelectorsDemo() {
+ const [fruit, setFruit] = useState<string | undefined>()
+ const [date, setDate] = useState<DateString | undefined>()
+
+ return (
+ <Grid columns={{ minWidth: 220, max: 2 }} gap={5}>
+ <Selector
+ label="最喜欢的水果"
+ options={['苹果', '香蕉', '橙子', '芒果', '菠萝']}
+ placeholder="请选择一种水果"
+ value={fruit}
+ onChange={setFruit}
+ width="100%"
+ />
+ <DateInput
+ label="开始日期"
+ placeholder="选择日期"
+ value={date}
+ onChange={setDate}
+ hasClear
+ width="100%"
+ />
+ </Grid>
+ )
+}
diff --git a/src/demos/TypographyDemo.tsx b/src/demos/TypographyDemo.tsx
new file mode 100644
index 0000000..75842d9
--- /dev/null
+++ b/src/demos/TypographyDemo.tsx
@@ -0,0 +1,36 @@
+import { Blockquote } from '@astryxdesign/core/Blockquote'
+import { CodeBlock } from '@astryxdesign/core/CodeBlock'
+import { Divider } from '@astryxdesign/core/Divider'
+import { Heading } from '@astryxdesign/core/Heading'
+import { Text } from '@astryxdesign/core/Text'
+import { VStack } from '@astryxdesign/core/VStack'
+
+const SNIPPET = `const theme = defineTheme({
+ name: 'ocean',
+ tokens: {'--color-accent': '#0077B6'},
+})`
+
+export function TypographyDemo() {
+ return (
+ <VStack gap={3}>
+ <VStack gap={1}>
+ <Heading level={3}>清晰的内容层级</Heading>
+ <Text type="body" color="secondary" as="p">
+ 标题、正文、辅助文字和代码共享同一套字体与节奏,让界面在中文和英文下都保持稳定。
+ </Text>
+ </VStack>
+ <Blockquote cite="Astryx 设计原则">
+ 把复杂性留在系统里,把清晰留给界面。
+ </Blockquote>
+ <Divider />
+ <CodeBlock
+ code={SNIPPET}
+ language="tsx"
+ title="theme.ts"
+ hasLineNumbers={false}
+ width="100%"
+ size="sm"
+ />
+ </VStack>
+ )
+}
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000..d589761
--- /dev/null
+++ b/src/index.css
@@ -0,0 +1,2 @@
+@import '@astryxdesign/core/reset.css';
+@import '@astryxdesign/core/astryx.css';
diff --git a/src/main.tsx b/src/main.tsx
new file mode 100644
index 0000000..95ac71e
--- /dev/null
+++ b/src/main.tsx
@@ -0,0 +1,19 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import './index.css'
+import { InternationalizationProvider } from '@astryxdesign/core/i18n'
+import zhCN from '@astryxdesign/core/locales/zh-CN.generated.js'
+import { App } from './App'
+import { ThemeModeProvider } from './theme/ThemeMode'
+
+createRoot(document.getElementById('root')!).render(
+ <StrictMode>
+ <InternationalizationProvider
+ locale="zh-CN"
+ messages={{ 'zh-CN': zhCN }}>
+ <ThemeModeProvider>
+ <App />
+ </ThemeModeProvider>
+ </InternationalizationProvider>
+ </StrictMode>,
+)
diff --git a/src/pages/AboutPage.tsx b/src/pages/AboutPage.tsx
new file mode 100644
index 0000000..5f2aec2
--- /dev/null
+++ b/src/pages/AboutPage.tsx
@@ -0,0 +1,202 @@
+import { CodeBlock } from '@astryxdesign/core/CodeBlock'
+import { Divider } from '@astryxdesign/core/Divider'
+import { Grid } from '@astryxdesign/core/Grid'
+import { Heading } from '@astryxdesign/core/Heading'
+import { Icon } from '@astryxdesign/core/Icon'
+import { List, ListItem } from '@astryxdesign/core/List'
+import { Text } from '@astryxdesign/core/Text'
+import { VStack } from '@astryxdesign/core/VStack'
+import {
+ ArrowRight,
+ Atom,
+ Blocks,
+ BookOpen,
+ ExternalLink,
+ Palette,
+ Route,
+ Shapes,
+ Zap,
+} from 'lucide-react'
+import { Page, PageHeader } from '../components/Page'
+
+const COMMANDS = `npm run dev # 启动开发服务器
+npm run build # 类型检查并构建生产版本
+npm run lint # 运行 oxlint
+npm run preview # 预览生产构建
+
+npm run astryx -- component Button # 查看 Astryx 组件文档`
+
+const PROJECT_TREE = `src/
+ components/
+ AppLayout.tsx 应用外壳、顶部导航、侧边导航
+ Page.tsx 共用页面框架与页头
+ pages/
+ HomePage.tsx
+ ComponentsPage.tsx
+ AboutPage.tsx
+ NotFoundPage.tsx
+ theme/
+ ThemeMode.tsx 明暗主题提供者
+ astryx-themes.ts 生成的主题注册表
+ router.tsx React Router 路由表
+ App.tsx 提供者与路由
+ main.tsx 入口文件`
+
+const STACK = [
+ {
+ name: 'React',
+ version: '19.3',
+ icon: Atom,
+ description: '界面运行时与 Hooks',
+ },
+ {
+ name: 'Vite',
+ version: '8.3',
+ icon: Zap,
+ description: '开发服务器与生产构建工具',
+ },
+ {
+ name: 'React Router',
+ version: '7.18',
+ icon: Route,
+ description: '基于数据路由的客户端路由,可在 hash 与 history 模式间切换',
+ },
+ {
+ name: 'Astryx',
+ version: '0.7.2',
+ icon: Palette,
+ description: '设计系统、组件与 neutral 主题',
+ },
+]
+
+export function AboutPage() {
+ return (
+ <Page>
+ <VStack gap={8}>
+ <PageHeader
+ eyebrow="关于"
+ title="是起点,不是框架。"
+ description="项目把每一部分都保持得小而可见:一张路由表、一套布局、一个主题提供者,以及普通的 React 组件。你可以直接替换需要改的部分,而不用和隐藏的约定较劲。"
+ />
+
+ <Divider />
+
+ <Grid columns={{ minWidth: 320, max: 2 }} gap={8}>
+ <VStack gap={4}>
+ <Heading level={2}>技术栈</Heading>
+ <List hasDividers>
+ {STACK.map(({ name, version, icon, description }) => (
+ <ListItem
+ key={name}
+ label={name}
+ description={description}
+ startContent={
+ <Icon icon={icon} size="sm" color="secondary" />
+ }
+ endContent={
+ <Text type="supporting" size="sm">
+ {version}
+ </Text>
+ }
+ />
+ ))}
+ </List>
+ </VStack>
+
+ <VStack gap={4}>
+ <Heading level={2}>常用命令</Heading>
+ <CodeBlock
+ code={COMMANDS}
+ language="bash"
+ title="终端"
+ size="sm"
+ hasLanguageLabel={false}
+ width="100%"
+ />
+ <Text type="supporting" size="sm">
+ Vite 8 需要 Node 20.19+。
+ </Text>
+ </VStack>
+ </Grid>
+
+ <Divider />
+
+ <VStack gap={4} maxWidth={920}>
+ <Heading level={2}>项目结构</Heading>
+ <Text type="body" color="secondary" as="p">
+ 在 <Text type="code">src/pages</Text> 下新建页面,然后在{' '}
+ <Text type="code">src/router.tsx</Text> 中注册路由,并在{' '}
+ <Text type="code">src/components/AppLayout.tsx</Text>{' '}
+ 中添加对应的导航项。
+ </Text>
+ <CodeBlock
+ code={PROJECT_TREE}
+ language="plaintext"
+ title="项目结构"
+ width="100%"
+ />
+ </VStack>
+
+ <Divider />
+
+ <VStack gap={4}>
+ <Heading level={2}>继续探索</Heading>
+ <List hasDividers>
+ <ListItem
+ label="组件"
+ description="浏览全部 173 个组件的实时交互示例。"
+ href="/components"
+ startContent={<Icon icon={Blocks} size="sm" color="secondary" />}
+ endContent={<Icon icon={ArrowRight} size="sm" color="secondary" />}
+ />
+ <ListItem
+ label="图标"
+ description="浏览 Astryx 语义图标和 Lucide 图标库。"
+ href="/icons"
+ startContent={<Icon icon={Shapes} size="sm" color="secondary" />}
+ endContent={<Icon icon={ArrowRight} size="sm" color="secondary" />}
+ />
+ <ListItem
+ label="Astryx 文档"
+ description="组件接口、主题、布局指南和页面模板。"
+ href="https://astryx.atmeta.com"
+ target="_blank"
+ startContent={<Icon icon={BookOpen} size="sm" color="secondary" />}
+ endContent={
+ <Icon icon={ExternalLink} size="sm" color="secondary" />
+ }
+ />
+ <ListItem
+ label="React Router 文档"
+ description="路由、数据加载、动作和导航。"
+ href="https://reactrouter.com"
+ target="_blank"
+ startContent={<Icon icon={Route} size="sm" color="secondary" />}
+ endContent={
+ <Icon icon={ExternalLink} size="sm" color="secondary" />
+ }
+ />
+ </List>
+ </VStack>
+
+ <Divider />
+
+ <VStack gap={2}>
+ <Heading level={3}>主题</Heading>
+ <Text type="body" color="secondary" as="p">
+ 自动生成的 <Text type="code">src/astryx-themes.ts</Text>{' '}
+ 文件注册了 neutral 主题。可以运行{' '}
+ <Text type="code">npm run astryx -- theme add &lt;slug&gt;</Text>{' '}
+ 添加其他主题包,然后在{' '}
+ <Text type="code">src/theme/ThemeMode.tsx</Text>{' '}
+ 中把它传给主题提供者。
+ </Text>
+ <Text type="supporting" size="sm">
+ Astryx 自带预构建 CSS,因此这个应用不需要 StyleX、PostCSS 或 Babel
+ 插件。
+ </Text>
+ </VStack>
+ </VStack>
+ </Page>
+ )
+}
diff --git a/src/pages/ComponentsPage.tsx b/src/pages/ComponentsPage.tsx
new file mode 100644
index 0000000..8191acb
--- /dev/null
+++ b/src/pages/ComponentsPage.tsx
@@ -0,0 +1,422 @@
+import { Fragment, useMemo, useState, type ReactNode } from 'react'
+import { Card } from '@astryxdesign/core/Card'
+import { Center } from '@astryxdesign/core/Center'
+import { Collapsible, CollapsibleGroup } from '@astryxdesign/core/Collapsible'
+import { Divider } from '@astryxdesign/core/Divider'
+import { Grid } from '@astryxdesign/core/Grid'
+import { Heading } from '@astryxdesign/core/Heading'
+import { HStack } from '@astryxdesign/core/HStack'
+import { Icon } from '@astryxdesign/core/Icon'
+import { Section } from '@astryxdesign/core/Section'
+import { Text } from '@astryxdesign/core/Text'
+import { TextInput } from '@astryxdesign/core/TextInput'
+import { ToggleButton, ToggleButtonGroup } from '@astryxdesign/core/ToggleButton'
+import { VStack } from '@astryxdesign/core/VStack'
+import {
+ Blocks,
+ Layers,
+ Search,
+ SlidersHorizontal,
+} from 'lucide-react'
+import { Page, PageHeader } from '../components/Page'
+import { ASTRYX_COMPONENTS, type AstryxComponentGroup } from '../data/astryxComponents'
+import { ActionsDemo } from '../demos/ActionsDemo'
+import { ChatDemo } from '../demos/ChatDemo'
+import { CommandPaletteDemo } from '../demos/CommandPaletteDemo'
+import { DataDemo } from '../demos/DataDemo'
+import { DialogsDemo } from '../demos/DialogsDemo'
+import { FeedbackDemo } from '../demos/FeedbackDemo'
+import { InputsDemo } from '../demos/InputsDemo'
+import { LayoutDemo } from '../demos/LayoutDemo'
+import { MenusDemo } from '../demos/MenusDemo'
+import { NavigationDemo } from '../demos/NavigationDemo'
+import { ProvidersDemo } from '../demos/ProvidersDemo'
+import { SelectionDemo } from '../demos/SelectionDemo'
+import { SelectorsDemo } from '../demos/SelectorsDemo'
+import { TypographyDemo } from '../demos/TypographyDemo'
+
+interface DemoDefinition {
+ id: string
+ group: AstryxComponentGroup
+ title: string
+ description: string
+ preview: ReactNode
+ components: string[]
+}
+
+const DEMOS: DemoDefinition[] = [
+ {
+ id: 'actions',
+ group: 'base',
+ title: '操作与链接',
+ description: '按钮、图标按钮、工具栏、切换按钮和链接共享同一套交互状态。',
+ preview: <ActionsDemo />,
+ components: [
+ 'Button', 'ButtonGroup', 'IconButton', 'ToggleButton', 'ToggleButtonGroup',
+ 'SegmentedControl', 'SegmentedControlItem', 'Link', 'Kbd', 'Toolbar', 'Icon',
+ ],
+ },
+ {
+ id: 'typography',
+ group: 'base',
+ title: '文字与内容',
+ description: '标题、正文、引用、代码块和分隔线共同建立清晰的内容层级。',
+ preview: <TypographyDemo />,
+ components: [
+ 'Text', 'Heading', 'Blockquote', 'Code', 'CodeBlock', 'Markdown', 'Citation',
+ 'Token', 'Divider', 'VisuallyHidden', 'Timer', 'Timestamp',
+ ],
+ },
+ {
+ id: 'layout',
+ group: 'base',
+ title: '布局与卡片',
+ description: '响应式网格、可选卡片和折叠面板组成的真实布局结构。',
+ preview: <LayoutDemo />,
+ components: [
+ 'Stack', 'HStack', 'VStack', 'Grid', 'GridSpan', 'Center', 'Section', 'Card',
+ 'ClickableCard', 'SelectableCard', 'Collapsible', 'CollapsibleGroup',
+ 'ScrollableArea', 'Layout', 'LayoutHeader', 'LayoutContent', 'LayoutFooter',
+ 'LayoutPanel', 'Outline', 'OverflowList', 'AspectRatio', 'Thumbnail',
+ 'Carousel', 'MediaTheme',
+ ],
+ },
+ {
+ id: 'data',
+ group: 'base',
+ title: '列表、表格与头像',
+ description: '列表、表格、头像和计数徽标用于呈现结构化数据。',
+ preview: <DataDemo />,
+ components: [
+ 'List', 'ListItem', 'Item', 'MetadataList', 'MetadataListItem', 'Table',
+ 'TableBody', 'TableCell', 'TableFooter', 'TableHeader', 'TableHeaderCell',
+ 'TableRow', 'TableSelectionToolbar', 'TreeList', 'EmptyState', 'Skeleton',
+ 'Spinner', 'Avatar', 'AvatarGroup', 'AvatarGroupOverflow', 'AvatarStatusDot',
+ 'Badge', 'StatusDot', 'NavIcon',
+ ],
+ },
+ {
+ id: 'providers',
+ group: 'base',
+ title: '主题与全局能力',
+ description: '主题、链接和国际化 Provider 已经在页面中生效。',
+ preview: <ProvidersDemo />,
+ components: ['Theme', 'MediaTheme', 'LinkProvider', 'InternationalizationProvider'],
+ },
+ {
+ id: 'feedback',
+ group: 'base',
+ title: '反馈与状态',
+ description: '用横幅、进度、加载指示和骨架屏表达系统状态。',
+ preview: <FeedbackDemo />,
+ components: ['Banner', 'ProgressBar', 'Spinner', 'Skeleton', 'EmptyState'],
+ },
+ {
+ id: 'navigation',
+ group: 'base',
+ title: '导航与步骤',
+ description: '面包屑、标签页和步骤条帮助用户在层级和流程中定位。',
+ preview: <NavigationDemo />,
+ components: [
+ 'AppShell', 'TopNav', 'TopNavHeading', 'TopNavItem', 'TopNavMenu',
+ 'TopNavMegaMenu', 'TopNavMegaMenuFeaturedCard', 'TopNavMegaMenuItem',
+ 'SideNav', 'SideNavCollapseButton', 'SideNavHeading', 'SideNavItem',
+ 'SideNavSection', 'MobileNav', 'Breadcrumbs', 'BreadcrumbItem', 'Pagination',
+ 'TabList', 'Tab', 'TabMenu', 'Stepper', 'Step',
+ ],
+ },
+ {
+ id: 'chat',
+ group: 'base',
+ title: '聊天消息',
+ description: '消息列表、气泡、元数据和时间戳组成的对话界面。',
+ preview: <ChatDemo />,
+ components: [
+ 'ChatComposer', 'ChatComposerDrawer', 'ChatComposerInput', 'ChatDictationButton',
+ 'ChatLayout', 'ChatLayoutScrollButton', 'ChatMessage', 'ChatMessageBubble',
+ 'ChatMessageList', 'ChatMessageMetadata', 'ChatSendButton', 'ChatSystemMessage',
+ 'ChatTokenizedText', 'ChatToolCalls',
+ ],
+ },
+ {
+ id: 'inputs',
+ group: 'form',
+ title: '文本输入与校验',
+ description: '文本、长文本、开关和保存反馈组成的表单输入场景。',
+ preview: <InputsDemo />,
+ components: [
+ 'TextInput', 'TextArea', 'NumberInput', 'InputGroup', 'InputGroupText',
+ 'Field', 'FieldLabel', 'FieldStatus', 'FormLayout', 'FileInput', 'PowerSearch',
+ ],
+ },
+ {
+ id: 'selection',
+ group: 'form',
+ title: '选择控件',
+ description: '复选框、单选框和滑杆展示不同粒度的选择操作。',
+ preview: <SelectionDemo />,
+ components: [
+ 'CheckboxInput', 'CheckboxList', 'CheckboxListItem', 'RadioList',
+ 'RadioListItem', 'Switch', 'Slider',
+ ],
+ },
+ {
+ id: 'selectors',
+ group: 'form',
+ title: '选择器与日期',
+ description: '下拉选择、日期输入和更多选择模式集中在同一场景。',
+ preview: <SelectorsDemo />,
+ components: [
+ 'Selector', 'SelectorOption', 'MultiSelector', 'ComplexSelector', 'Typeahead',
+ 'BaseTypeahead', 'TypeaheadItem', 'Tokenizer', 'Calendar', 'DateInput',
+ 'DateRangeInput', 'DateTimeInput', 'TimeInput',
+ ],
+ },
+ {
+ id: 'dialogs',
+ group: 'overlay',
+ title: '对话框、抽屉与底部面板',
+ description: '点击按钮打开真实弹层,体验遮罩、焦点管理和关闭流程。',
+ preview: <DialogsDemo />,
+ components: [
+ 'Dialog', 'AlertDialog', 'DialogHeader', 'Drawer', 'DrawerHeader',
+ 'BottomSheet', 'BottomSheetSwitcher', 'Overlay',
+ ],
+ },
+ {
+ id: 'menus',
+ group: 'overlay',
+ title: '菜单与浮层',
+ description: '下拉菜单、上下文菜单、弹出框、提示和悬停卡片。',
+ preview: <MenusDemo />,
+ components: [
+ 'DropdownMenu', 'DropdownMenuCheckboxItem', 'DropdownMenuDivider',
+ 'DropdownMenuGroup', 'DropdownMenuItem', 'DropdownMenuRadioGroup',
+ 'DropdownMenuRadioItem', 'DropdownMenuSubMenu', 'ContextMenu', 'MoreMenu',
+ 'Popover', 'Tooltip', 'HoverCard',
+ ],
+ },
+ {
+ id: 'command',
+ group: 'overlay',
+ title: '命令面板与通知',
+ description: '命令面板把搜索、分组结果和键盘操作集中在一个浮层里。',
+ preview: <CommandPaletteDemo />,
+ components: [
+ 'CommandPalette', 'CommandPaletteEmpty', 'CommandPaletteFooter',
+ 'CommandPaletteGroup', 'CommandPaletteInput', 'CommandPaletteItem',
+ 'CommandPaletteList', 'Toast', 'Lightbox',
+ ],
+ },
+]
+
+const GROUPS = [
+ {
+ id: 'base',
+ label: '基础组件',
+ icon: Blocks,
+ description: '布局、排版、内容、操作、反馈、导航和数据展示等通用构建块。',
+ },
+ {
+ id: 'form',
+ label: '表单组件',
+ icon: SlidersHorizontal,
+ description: '输入、选择、日期、校验和表单布局控件。',
+ },
+ {
+ id: 'overlay',
+ label: '弹层组件',
+ icon: Layers,
+ description: '对话框、抽屉、菜单、浮层、提示和命令面板。',
+ },
+] as const
+
+type Filter = 'all' | AstryxComponentGroup
+
+const GROUP_COUNTS = GROUPS.reduce(
+ (counts, group) => {
+ counts[group.id] = ASTRYX_COMPONENTS.filter(
+ (component) => component.group === group.id,
+ ).length
+ return counts
+ },
+ { base: 0, form: 0, overlay: 0 } as Record<AstryxComponentGroup, number>,
+)
+
+function DemoCard({ demo }: { demo: DemoDefinition }) {
+ return (
+ <Card padding={0}>
+ <Section variant="muted" padding={5}>
+ {demo.preview}
+ </Section>
+ <Section variant="transparent" padding={5}>
+ <VStack gap={2}>
+ <Heading level={3}>{demo.title}</Heading>
+ <Text type="body" color="secondary" as="p">
+ {demo.description}
+ </Text>
+ <Text type="supporting" size="sm" maxLines={2}>
+ 覆盖组件:{demo.components.join('、')}
+ </Text>
+ </VStack>
+ </Section>
+ </Card>
+ )
+}
+
+export function ComponentsPage() {
+ const [filter, setFilter] = useState<Filter>('all')
+ const [query, setQuery] = useState('')
+
+ const results = useMemo(() => {
+ const normalized = query.trim().toLowerCase()
+
+ return DEMOS.filter((demo) => {
+ const matchesGroup = filter === 'all' || demo.group === filter
+ const matchesQuery =
+ normalized.length === 0 ||
+ demo.title.toLowerCase().includes(normalized) ||
+ demo.description.toLowerCase().includes(normalized) ||
+ demo.components.some((name) => name.toLowerCase().includes(normalized))
+
+ return matchesGroup && matchesQuery
+ })
+ }, [filter, query])
+
+ const sections = GROUPS.map((group) => ({
+ ...group,
+ demos: results.filter((demo) => demo.group === group.id),
+ })).filter((group) => group.demos.length > 0)
+
+ const coveredComponents = new Set(results.flatMap((demo) => demo.components)).size
+ const isFiltering = filter !== 'all' || query.trim().length > 0
+
+ return (
+ <Page>
+ <VStack gap={8}>
+ <PageHeader
+ eyebrow="实时预览"
+ title="组件"
+ description={`通过可交互的组合场景展示 Astryx 组件的使用效果。全部 ${ASTRYX_COMPONENTS.length} 个组件按基础、表单和弹层分类整理。`}
+ />
+
+ <VStack gap={2}>
+ <Text type="supporting" size="sm">
+ 组件名称与 API 保留 Astryx 原始命名;示例中的操作和内容均可直接体验。
+ </Text>
+ <HStack gap={3} wrap="wrap">
+ <Text type="supporting" size="sm">
+ 基础组件 {GROUP_COUNTS.base}
+ </Text>
+ <Text type="supporting" size="sm">
+ 表单组件 {GROUP_COUNTS.form}
+ </Text>
+ <Text type="supporting" size="sm">
+ 弹层组件 {GROUP_COUNTS.overlay}
+ </Text>
+ </HStack>
+ </VStack>
+
+ <Divider />
+
+ <VStack gap={4}>
+ <HStack gap={4} wrap="wrap" vAlign="end">
+ <TextInput
+ label="搜索组件"
+ isLabelHidden
+ placeholder="搜索组件名或场景"
+ value={query}
+ onChange={setQuery}
+ startIcon={Search}
+ size="lg"
+ width={360}
+ />
+ <ToggleButtonGroup
+ label="按组件分类筛选"
+ value={filter}
+ onChange={(value) => setFilter((value as Filter | null) ?? 'all')}
+ size="sm">
+ <ToggleButton label="全部" value="all" />
+ {GROUPS.map((group) => (
+ <ToggleButton
+ key={group.id}
+ label={group.label}
+ value={group.id}
+ />
+ ))}
+ </ToggleButtonGroup>
+ </HStack>
+ <Text type="supporting" size="sm">
+ {isFiltering
+ ? `${results.length} 个场景,覆盖 ${coveredComponents} 个组件`
+ : `${DEMOS.length} 个场景,覆盖 ${coveredComponents} 个组件`}
+ </Text>
+ </VStack>
+
+ {results.length === 0 ? (
+ <Center minHeight={220}>
+ <VStack gap={2} hAlign="center">
+ <Text type="label">没有找到匹配的场景。</Text>
+ <Text type="supporting" color="secondary">
+ 试试更宽泛的关键词,或切换回「全部」。
+ </Text>
+ </VStack>
+ </Center>
+ ) : (
+ sections.map((group, index) => (
+ <Fragment key={group.id}>
+ {index > 0 ? <Divider /> : null}
+ <VStack gap={4}>
+ <VStack gap={1}>
+ <HStack gap={2} vAlign="center" wrap="wrap">
+ <Icon icon={group.icon} size="md" color="accent" />
+ <Heading level={2}>{group.label}</Heading>
+ <Text type="supporting" size="sm">
+ {group.demos.length} 个场景
+ </Text>
+ </HStack>
+ <Text type="body" color="secondary" as="p">
+ {group.description}
+ </Text>
+ </VStack>
+ <Grid columns={{ minWidth: 460, max: 2 }} gap={4} align="start">
+ {group.demos.map((demo) => (
+ <DemoCard key={demo.id} demo={demo} />
+ ))}
+ </Grid>
+ </VStack>
+ </Fragment>
+ ))
+ )}
+
+ <Divider />
+
+ <CollapsibleGroup type="single">
+ <Collapsible
+ value="all-components"
+ trigger={<Text type="label">查看完整组件清单</Text>}>
+ <VStack gap={3}>
+ {GROUPS.map((group) => (
+ <VStack key={group.id} gap={1}>
+ <Text type="label">{group.label}</Text>
+ <Text type="supporting" size="sm">
+ {ASTRYX_COMPONENTS.filter(
+ (component) => component.group === group.id,
+ )
+ .map((component) => component.name)
+ .join('、')}
+ </Text>
+ </VStack>
+ ))}
+ </VStack>
+ </Collapsible>
+ </CollapsibleGroup>
+
+ <Text type="supporting" size="sm">
+ 共收录 {ASTRYX_COMPONENTS.length} 个组件,数据来自
+ @astryxdesign/core 0.7.2。
+ </Text>
+ </VStack>
+ </Page>
+ )
+}
diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx
new file mode 100644
index 0000000..a1e7668
--- /dev/null
+++ b/src/pages/HomePage.tsx
@@ -0,0 +1,219 @@
+import { Button } from '@astryxdesign/core/Button'
+import { Card } from '@astryxdesign/core/Card'
+import { CodeBlock } from '@astryxdesign/core/CodeBlock'
+import { Divider } from '@astryxdesign/core/Divider'
+import { Grid } from '@astryxdesign/core/Grid'
+import { Heading } from '@astryxdesign/core/Heading'
+import { HStack } from '@astryxdesign/core/HStack'
+import { Icon } from '@astryxdesign/core/Icon'
+import { List, ListItem } from '@astryxdesign/core/List'
+import { Text } from '@astryxdesign/core/Text'
+import { VStack } from '@astryxdesign/core/VStack'
+import {
+ ArrowRight,
+ Atom,
+ Check,
+ ExternalLink,
+ Layers,
+ Moon,
+ Palette,
+ Route,
+ Sparkles,
+ Terminal,
+ Zap,
+} from 'lucide-react'
+import { Page } from '../components/Page'
+
+const STACK = [
+ {
+ name: 'Vite',
+ version: '8.x',
+ icon: Zap,
+ description: '极速开发服务器、快速热更新,以及经过优化的生产构建。',
+ },
+ {
+ name: 'React',
+ version: '19.x',
+ icon: Atom,
+ description: '现代 Hooks 与全应用共享的组件模型。',
+ },
+ {
+ name: 'React Router',
+ version: '7.x',
+ icon: Route,
+ description: '嵌套路由、持续存在的应用外壳,以及类型安全的导航。',
+ },
+ {
+ name: 'Astryx',
+ version: '0.7',
+ icon: Palette,
+ description: '无障碍组件、设计令牌、主题与深色模式。',
+ },
+]
+
+const QUICK_START = `npm install
+npm run dev`
+
+const BUILT_IN = [
+ {
+ label: '应用外壳',
+ description:
+ '顶部导航、可折叠侧边栏和移动端抽屉都已替你处理好。',
+ icon: Layers,
+ },
+ {
+ label: '客户端路由',
+ description:
+ '所有页面共用一套布局,可在根目录配置中切换 hash 与 history 路由。',
+ icon: Route,
+ },
+ {
+ label: '明暗主题',
+ description:
+ 'Astryx neutral 主题可切换模式,并记住你的选择。',
+ icon: Moon,
+ },
+ {
+ label: '完整组件目录',
+ description:
+ '全部 173 个 Astryx 组件,按基础、表单和弹层分类。',
+ icon: Sparkles,
+ },
+]
+
+export function HomePage() {
+ return (
+ <Page>
+ <VStack gap={10}>
+ <VStack gap={5} maxWidth={860}>
+ <Text type="label" color="accent">
+ Vite + React + React Router + Astryx
+ </Text>
+ <VStack gap={3}>
+ <Heading level={1} type="display-2" textWrap="balance">
+ 为你的下一个 React 应用,准备更好的起点。
+ </Heading>
+ <Text type="large" color="secondary" as="p" textWrap="pretty">
+ Vite 让反馈循环保持飞快,React Router
+ 给应用真正的结构,Astryx 提供无障碍组件与主题。所有部分都已经连接好。
+ </Text>
+ </VStack>
+ <HStack gap={3} wrap="wrap">
+ <Button
+ label="浏览组件"
+ variant="primary"
+ href="/components"
+ endContent={<Icon icon={ArrowRight} size="sm" color="inherit" />}
+ />
+ <Button
+ label="Astryx 文档"
+ variant="secondary"
+ href="https://astryx.atmeta.com"
+ target="_blank"
+ rel="noreferrer"
+ endContent={
+ <Icon icon={ExternalLink} size="sm" color="inherit" />
+ }
+ />
+ </HStack>
+ </VStack>
+
+ <Divider />
+
+ <VStack gap={5}>
+ <VStack gap={2} maxWidth={680}>
+ <Text type="label" color="accent">
+ 技术栈
+ </Text>
+ <Heading level={2}>四件工具,已经连接好。</Heading>
+ <Text type="body" color="secondary" as="p">
+ 无需构建插件,也无需额外配置 CSS。Astryx
+ 直接提供预构建样式,项目里能看到的导入就是全部配置。
+ </Text>
+ </VStack>
+ <Grid columns={{ minWidth: 230, max: 4 }} gap={6}>
+ {STACK.map(({ name, version, icon, description }) => (
+ <HStack key={name} gap={3} vAlign="start">
+ <Icon icon={icon} size="md" color="accent" />
+ <VStack gap={1}>
+ <HStack gap={2} vAlign="center" wrap="wrap">
+ <Text type="label">{name}</Text>
+ <Text type="supporting" size="sm">
+ {version}
+ </Text>
+ </HStack>
+ <Text type="supporting" size="sm" as="p">
+ {description}
+ </Text>
+ </VStack>
+ </HStack>
+ ))}
+ </Grid>
+ </VStack>
+
+ <Divider />
+
+ <Grid columns={{ minWidth: 320, max: 2 }} gap={8}>
+ <VStack gap={4}>
+ <VStack gap={1}>
+ <Heading level={2}>已经接好的能力</Heading>
+ <Text type="body" color="secondary" as="p">
+ 一些方便替换、能随应用一起成长的小默认值。
+ </Text>
+ </VStack>
+ <List hasDividers>
+ {BUILT_IN.map(({ label, description, icon }) => (
+ <ListItem
+ key={label}
+ label={label}
+ description={description}
+ startContent={
+ <Icon icon={icon} size="sm" color="secondary" />
+ }
+ endContent={
+ <Icon icon={Check} size="sm" color="success" />
+ }
+ />
+ ))}
+ </List>
+ </VStack>
+
+ <Card padding={6} elevation="low">
+ <VStack gap={4}>
+ <VStack gap={1}>
+ <Heading level={2}>快速开始</Heading>
+ <Text type="body" color="secondary" as="p">
+ 安装依赖、启动 Vite,然后编辑{' '}
+ <Text type="code">src/</Text> 下的任意文件。
+ </Text>
+ </VStack>
+ <CodeBlock
+ code={QUICK_START}
+ language="bash"
+ title="终端"
+ hasLanguageLabel={false}
+ width="100%"
+ />
+ <HStack gap={2} vAlign="center">
+ <Icon icon={Terminal} size="sm" color="secondary" />
+ <Text type="supporting" size="sm">
+ 需要 Node 20.19+ 和 npm 10+。
+ </Text>
+ </HStack>
+ </VStack>
+ </Card>
+ </Grid>
+
+ <Divider />
+
+ <HStack gap={2} vAlign="center">
+ <Icon icon={Sparkles} size="sm" color="accent" />
+ <Text type="supporting" size="sm">
+ 基于 Astryx v0.7.2 和 neutral 主题构建。可在
+ src/astryx-themes.ts 中更改主题。
+ </Text>
+ </HStack>
+ </VStack>
+ </Page>
+ )
+}
diff --git a/src/pages/IconsPage.tsx b/src/pages/IconsPage.tsx
new file mode 100644
index 0000000..6b836ca
--- /dev/null
+++ b/src/pages/IconsPage.tsx
@@ -0,0 +1,334 @@
+import { useMemo, useState } from 'react'
+import { Button } from '@astryxdesign/core/Button'
+import { Center } from '@astryxdesign/core/Center'
+import { ClickableCard } from '@astryxdesign/core/ClickableCard'
+import { Divider } from '@astryxdesign/core/Divider'
+import { Grid } from '@astryxdesign/core/Grid'
+import { Heading } from '@astryxdesign/core/Heading'
+import { HStack } from '@astryxdesign/core/HStack'
+import { Icon } from '@astryxdesign/core/Icon'
+import type { IconName } from '@astryxdesign/core/Icon'
+import { Text } from '@astryxdesign/core/Text'
+import { TextInput } from '@astryxdesign/core/TextInput'
+import { useToast } from '@astryxdesign/core/Toast'
+import { ToggleButton, ToggleButtonGroup } from '@astryxdesign/core/ToggleButton'
+import { VStack } from '@astryxdesign/core/VStack'
+import {
+ ChevronLeft,
+ ChevronRight,
+ icons,
+ Search,
+ type LucideIcon,
+} from 'lucide-react'
+import { Page, PageHeader } from '../components/Page'
+
+type IconSet = 'semantic' | 'lucide'
+
+interface SemanticIcon {
+ name: IconName
+ label: string
+}
+
+const SEMANTIC_ICONS: SemanticIcon[] = [
+ { name: 'close', label: '关闭' },
+ { name: 'chevronDown', label: '向下展开' },
+ { name: 'chevronLeft', label: '向左' },
+ { name: 'chevronRight', label: '向右' },
+ { name: 'chevronsLeft', label: '跳到开头' },
+ { name: 'chevronsRight', label: '跳到末尾' },
+ { name: 'check', label: '确认' },
+ { name: 'success', label: '成功' },
+ { name: 'error', label: '错误' },
+ { name: 'warning', label: '警告' },
+ { name: 'info', label: '信息' },
+ { name: 'calendar', label: '日历' },
+ { name: 'clock', label: '时间' },
+ { name: 'externalLink', label: '外部链接' },
+ { name: 'menu', label: '菜单' },
+ { name: 'moreHorizontal', label: '更多' },
+ { name: 'search', label: '搜索' },
+ { name: 'upload', label: '上传' },
+ { name: 'arrowUp', label: '向上' },
+ { name: 'arrowDown', label: '向下' },
+ { name: 'arrowsUpDown', label: '排序' },
+ { name: 'funnel', label: '筛选' },
+ { name: 'eyeSlash', label: '隐藏' },
+ { name: 'viewColumns', label: '列设置' },
+ { name: 'copy', label: '复制' },
+ { name: 'checkDouble', label: '已复制' },
+ { name: 'wrench', label: '设置' },
+ { name: 'stop', label: '停止' },
+ { name: 'microphone', label: '麦克风' },
+]
+
+interface LucideEntry {
+ name: string
+ Icon: LucideIcon
+}
+
+const LUCIDE_ICONS: LucideEntry[] = Object.entries(icons)
+ .map(([name, Icon]) => ({ name, Icon }))
+ .sort((a, b) => a.name.localeCompare(b.name))
+
+const LUCIDE_PAGE_SIZE = 120
+
+export function IconsPage() {
+ const [activeSet, setActiveSet] = useState<IconSet>('semantic')
+ const [query, setQuery] = useState('')
+ const [page, setPage] = useState(1)
+ const showToast = useToast()
+
+ const changeSet = (value: string | null) => {
+ setActiveSet(value === 'lucide' ? 'lucide' : 'semantic')
+ setPage(1)
+ }
+
+ const changeQuery = (value: string) => {
+ setQuery(value)
+ setPage(1)
+ }
+
+ const copyUsage = async (code: string, name: string) => {
+ try {
+ if (!navigator.clipboard) throw new Error('Clipboard unavailable')
+ await navigator.clipboard.writeText(code)
+ showToast({ body: `已复制 ${name} 的用法`, type: 'info' })
+ } catch {
+ showToast({ body: '复制失败,请检查浏览器权限', type: 'error' })
+ }
+ }
+
+ const normalizedQuery = query.trim().toLowerCase()
+
+ const filteredSemantic = useMemo(
+ () =>
+ SEMANTIC_ICONS.filter(
+ (icon) =>
+ normalizedQuery.length === 0 ||
+ icon.name.toLowerCase().includes(normalizedQuery) ||
+ icon.label.includes(query.trim()),
+ ),
+ [normalizedQuery, query],
+ )
+
+ const filteredLucide = useMemo(
+ () =>
+ normalizedQuery.length === 0
+ ? LUCIDE_ICONS
+ : LUCIDE_ICONS.filter((icon) =>
+ icon.name.toLowerCase().includes(normalizedQuery),
+ ),
+ [normalizedQuery],
+ )
+
+ const totalPages = Math.max(1, Math.ceil(filteredLucide.length / LUCIDE_PAGE_SIZE))
+ const currentPage = Math.min(page, totalPages)
+ const pageStart = (currentPage - 1) * LUCIDE_PAGE_SIZE
+ const pageItems = filteredLucide.slice(
+ pageStart,
+ pageStart + LUCIDE_PAGE_SIZE,
+ )
+
+ const isSemantic = activeSet === 'semantic'
+ const visibleCount = isSemantic ? filteredSemantic.length : filteredLucide.length
+
+ return (
+ <Page>
+ <VStack gap={8}>
+ <PageHeader
+ eyebrow="图标资源"
+ title="图标"
+ description={`浏览 ${SEMANTIC_ICONS.length} 个 Astryx 语义图标和 ${LUCIDE_ICONS.length} 个 Lucide 图标。点击任意图标即可复制它的用法。`}
+ />
+
+ <Divider />
+
+ <VStack gap={4}>
+ <HStack gap={4} wrap="wrap" vAlign="end">
+ <TextInput
+ label="搜索图标"
+ isLabelHidden
+ placeholder="搜索图标名称"
+ value={query}
+ onChange={changeQuery}
+ startIcon={Search}
+ size="lg"
+ width={360}
+ />
+ <ToggleButtonGroup
+ label="选择图标集"
+ value={activeSet}
+ onChange={changeSet}
+ size="sm">
+ <ToggleButton
+ label={`语义图标 ${SEMANTIC_ICONS.length}`}
+ value="semantic"
+ />
+ <ToggleButton
+ label={`Lucide ${LUCIDE_ICONS.length}`}
+ value="lucide"
+ />
+ </ToggleButtonGroup>
+ </HStack>
+ <Text type="supporting" size="sm">
+ {isSemantic
+ ? '语义图标通过名称使用,会自动适配当前主题。'
+ : 'Lucide 图标以组件形式传入 Icon,可与语义图标混用。'}
+ </Text>
+ </VStack>
+
+ {visibleCount === 0 ? (
+ <Center minHeight={220}>
+ <VStack gap={2} hAlign="center">
+ <Icon icon={Search} size="lg" color="secondary" />
+ <Text type="label">没有找到匹配的图标。</Text>
+ <Text type="supporting" color="secondary">
+ 试试更短的关键词。
+ </Text>
+ </VStack>
+ </Center>
+ ) : isSemantic ? (
+ <>
+ <Text type="supporting" size="sm">
+ 共 {filteredSemantic.length} 个语义图标
+ </Text>
+ <Grid columns={{ minWidth: 160, max: 4 }} gap={3}>
+ {filteredSemantic.map((semanticIcon) => (
+ <ClickableCard
+ key={semanticIcon.name}
+ padding={4}
+ onClick={() =>
+ void copyUsage(
+ `<Icon icon="${semanticIcon.name}" size="lg" />`,
+ semanticIcon.name,
+ )
+ }
+ label={`复制 ${semanticIcon.name} 图标用法`}>
+ <VStack gap={2} hAlign="center">
+ <Icon
+ icon={semanticIcon.name}
+ size="lg"
+ color="accent"
+ />
+ <Text
+ type="code"
+ size="sm"
+ display="block"
+ justify="center"
+ maxLines={1}>
+ {semanticIcon.name}
+ </Text>
+ <Text
+ type="supporting"
+ size="sm"
+ display="block"
+ justify="center">
+ {semanticIcon.label}
+ </Text>
+ </VStack>
+ </ClickableCard>
+ ))}
+ </Grid>
+ </>
+ ) : (
+ <>
+ <HStack gap={2} vAlign="center" hAlign="between" wrap="wrap">
+ <Text type="supporting" size="sm">
+ 显示 {pageStart + 1}–{Math.min(pageStart + LUCIDE_PAGE_SIZE, filteredLucide.length)} 个,共 {filteredLucide.length} 个图标
+ </Text>
+ <HStack gap={2} vAlign="center">
+ <Button
+ label="上一页"
+ variant="secondary"
+ size="sm"
+ isDisabled={currentPage === 1}
+ onClick={() => setPage((value) => Math.max(1, value - 1))}
+ icon={<Icon icon={ChevronLeft} size="sm" />}
+ />
+ <Text type="supporting" size="sm">
+ 第 {currentPage} / {totalPages} 页
+ </Text>
+ <Button
+ label="下一页"
+ variant="secondary"
+ size="sm"
+ isDisabled={currentPage === totalPages}
+ onClick={() =>
+ setPage((value) => Math.min(totalPages, value + 1))
+ }
+ endContent={<Icon icon={ChevronRight} size="sm" />}
+ />
+ </HStack>
+ </HStack>
+
+ <Grid columns={{ minWidth: 160, max: 5 }} gap={3}>
+ {pageItems.map(({ name, Icon: LucideComponent }) => (
+ <ClickableCard
+ key={name}
+ padding={3}
+ onClick={() =>
+ void copyUsage(
+ `import { ${name} } from 'lucide-react'\n\n<Icon icon={${name}} size="lg" />`,
+ name,
+ )
+ }
+ label={`复制 ${name} 图标用法`}>
+ <VStack gap={2} hAlign="center">
+ <Icon icon={LucideComponent} size="lg" color="primary" />
+ <Text
+ type="supporting"
+ size="xsm"
+ display="block"
+ justify="center"
+ maxLines={2}
+ wordBreak="break-all">
+ {name}
+ </Text>
+ </VStack>
+ </ClickableCard>
+ ))}
+ </Grid>
+
+ <HStack gap={2} vAlign="center" hAlign="between" wrap="wrap">
+ <Text type="supporting" size="sm">
+ 点击图标复制 import 与 Icon 用法
+ </Text>
+ <HStack gap={2} vAlign="center">
+ <Button
+ label="上一页"
+ variant="secondary"
+ size="sm"
+ isDisabled={currentPage === 1}
+ onClick={() => setPage((value) => Math.max(1, value - 1))}
+ icon={<Icon icon={ChevronLeft} size="sm" />}
+ />
+ <Text type="supporting" size="sm">
+ 第 {currentPage} / {totalPages} 页
+ </Text>
+ <Button
+ label="下一页"
+ variant="secondary"
+ size="sm"
+ isDisabled={currentPage === totalPages}
+ onClick={() =>
+ setPage((value) => Math.min(totalPages, value + 1))
+ }
+ endContent={<Icon icon={ChevronRight} size="sm" />}
+ />
+ </HStack>
+ </HStack>
+ </>
+ )}
+
+ <Divider />
+
+ <VStack gap={1}>
+ <Heading level={2}>使用方式</Heading>
+ <Text type="body" color="secondary" as="p">
+ 语义图标可以直接传入名称;Lucide 图标需要先导入组件,再交给 Icon 渲染。
+ </Text>
+ </VStack>
+ </VStack>
+ </Page>
+ )
+}
diff --git a/src/pages/NotFoundPage.tsx b/src/pages/NotFoundPage.tsx
new file mode 100644
index 0000000..04ae800
--- /dev/null
+++ b/src/pages/NotFoundPage.tsx
@@ -0,0 +1,27 @@
+import { Button } from '@astryxdesign/core/Button'
+import { Center } from '@astryxdesign/core/Center'
+import { Heading } from '@astryxdesign/core/Heading'
+import { Text } from '@astryxdesign/core/Text'
+import { VStack } from '@astryxdesign/core/VStack'
+import { Page } from '../components/Page'
+
+export function NotFoundPage() {
+ return (
+ <Page>
+ <Center minHeight={520}>
+ <VStack gap={4} hAlign="center" maxWidth={440}>
+ <Text type="label" color="accent">
+ 404
+ </Text>
+ <Heading level={1} type="display-3" justify="center">
+ 页面未找到
+ </Heading>
+ <Text type="body" color="secondary" as="p" justify="center">
+ 这个模板里没有你访问的路由。回到概览页,再选择一个目的地吧。
+ </Text>
+ <Button label="返回概览" variant="primary" href="/" />
+ </VStack>
+ </Center>
+ </Page>
+ )
+}
diff --git a/src/router.tsx b/src/router.tsx
new file mode 100644
index 0000000..a7a0419
--- /dev/null
+++ b/src/router.tsx
@@ -0,0 +1,28 @@
+import {
+ createBrowserRouter,
+ createHashRouter,
+ type RouteObject,
+} from 'react-router-dom'
+import { appConfig } from '../app.config'
+import { appRoutes } from './app-routes'
+import { AppLayout } from './components/AppLayout'
+import { NotFoundPage } from './pages/NotFoundPage'
+
+const pageRoutes: RouteObject[] = appRoutes.map((route) =>
+ route.path === '/'
+ ? { index: true, element: route.element }
+ : { path: route.path.slice(1), element: route.element },
+)
+
+const routes: RouteObject[] = [
+ {
+ path: '/',
+ element: <AppLayout />,
+ children: [...pageRoutes, { path: '*', element: <NotFoundPage /> }],
+ },
+]
+
+export const router =
+ appConfig.routerMode === 'hash'
+ ? createHashRouter(routes)
+ : createBrowserRouter(routes)
diff --git a/src/theme/ThemeMode.tsx b/src/theme/ThemeMode.tsx
new file mode 100644
index 0000000..cd91d5d
--- /dev/null
+++ b/src/theme/ThemeMode.tsx
@@ -0,0 +1,55 @@
+import { useEffect, useMemo, useState, type ReactNode } from 'react'
+import { Theme } from '@astryxdesign/core/theme'
+import { defaultThemeSlug, themes } from '../astryx-themes'
+import {
+ ThemeModeContext,
+ type ColorMode,
+ type ThemeModeValue,
+} from './themeModeContext'
+
+const STORAGE_KEY = 'astryx-starter-mode'
+
+function getInitialMode(): ColorMode {
+ if (typeof window === 'undefined') return 'light'
+
+ try {
+ const stored = window.localStorage.getItem(STORAGE_KEY)
+ if (stored === 'light' || stored === 'dark') return stored
+ } catch {
+ // Storage can be unavailable in private browsing or embedded contexts.
+ }
+
+ return window.matchMedia('(prefers-color-scheme: dark)').matches
+ ? 'dark'
+ : 'light'
+}
+
+export function ThemeModeProvider({ children }: { children: ReactNode }) {
+ const [mode, setMode] = useState<ColorMode>(getInitialMode)
+
+ useEffect(() => {
+ try {
+ window.localStorage.setItem(STORAGE_KEY, mode)
+ } catch {
+ // The theme still switches for this session if persistence is blocked.
+ }
+ }, [mode])
+
+ const value = useMemo<ThemeModeValue>(
+ () => ({
+ mode,
+ toggleMode: () => {
+ setMode((current) => (current === 'light' ? 'dark' : 'light'))
+ },
+ }),
+ [mode],
+ )
+
+ return (
+ <Theme theme={themes[defaultThemeSlug]} mode={mode}>
+ <ThemeModeContext.Provider value={value}>
+ {children}
+ </ThemeModeContext.Provider>
+ </Theme>
+ )
+}
diff --git a/src/theme/themeModeContext.ts b/src/theme/themeModeContext.ts
new file mode 100644
index 0000000..5bbf8ba
--- /dev/null
+++ b/src/theme/themeModeContext.ts
@@ -0,0 +1,10 @@
+import { createContext } from 'react'
+
+export type ColorMode = 'light' | 'dark'
+
+export interface ThemeModeValue {
+ mode: ColorMode
+ toggleMode: () => void
+}
+
+export const ThemeModeContext = createContext<ThemeModeValue | null>(null)
diff --git a/src/theme/useThemeMode.ts b/src/theme/useThemeMode.ts
new file mode 100644
index 0000000..9769684
--- /dev/null
+++ b/src/theme/useThemeMode.ts
@@ -0,0 +1,12 @@
+import { useContext } from 'react'
+import { ThemeModeContext, type ThemeModeValue } from './themeModeContext'
+
+export function useThemeMode(): ThemeModeValue {
+ const context = useContext(ThemeModeContext)
+
+ if (context === null) {
+ throw new Error('useThemeMode must be used inside ThemeModeProvider')
+ }
+
+ return context
+}