diff options
Diffstat (limited to 'src/pages/IconsPage.tsx')
| -rw-r--r-- | src/pages/IconsPage.tsx | 334 |
1 files changed, 334 insertions, 0 deletions
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> + ) +} |
