diff options
Diffstat (limited to 'src')
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 <slug></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 +} |
