import React, { useState, useEffect } from 'react';
import { Card, Divider, Button, Switch, Collapse, Typewriter } from '../src';
import { useIsMobile } from './tools';
// ============================================
// Syntax highlighting
// ============================================
const HL_TOKENS: { pattern: RegExp; style: React.CSSProperties }[] = [
{
pattern: /(\/\/.*$|\/\*[\s\S]*?\*\/)/gm,
style: { color: '#6b5e50', fontStyle: 'italic', fontWeight: 400 },
},
{
pattern: /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g,
style: { color: '#a8d4a0' },
},
{ pattern: /(<\/?[\w.]+|\/?>)/g, style: { color: '#f0a870' } },
{
pattern:
/\b(import|from|const|let|var|function|return|export|default|true|false|null|undefined)\b/g,
style: { color: '#d4a0e0' },
},
{ pattern: /\b(npm|yarn|pnpm)\b/g, style: { color: '#f0a870' } },
{
pattern: /(install|uninstall|run|add|remove)\b/g,
style: { color: '#a8d4a0' },
},
{ pattern: /(\{|\})/g, style: { color: '#d4b896' } },
{ pattern: /(=>)/g, style: { color: '#d4a0e0' } },
{ pattern: /(--[\w-]+)(?=\s*:)/g, style: { color: '#e8c87a' } },
{ pattern: /(:root)/g, style: { color: '#f0a870' } },
{ pattern: /(#[0-9a-fA-F]{3,8})\b/g, style: { color: '#8ab8e0' } },
];
const highlightCode = (code: string): React.ReactNode[] => {
const parts: React.ReactNode[] = [];
const lines = code.split('\n');
lines.forEach((line, li) => {
type Seg = { start: number; end: number; style: React.CSSProperties };
const segs: Seg[] = [];
for (const t of HL_TOKENS) {
const re = new RegExp(t.pattern.source, t.pattern.flags);
let m: RegExpExecArray | null;
while ((m = re.exec(line)) !== null) {
const s =
m.index + (m[0] !== m[1] && m[1] ? m[0].indexOf(m[1]) : 0);
const text = m[1] || m[0];
segs.push({ start: s, end: s + text.length, style: t.style });
}
}
segs.sort((a, b) => a.start - b.start);
const merged: Seg[] = [];
for (const seg of segs) {
if (
merged.length === 0 ||
seg.start >= merged[merged.length - 1].end
)
merged.push(seg);
}
let idx = 0;
for (const seg of merged) {
if (seg.start > idx) parts.push(line.slice(idx, seg.start));
parts.push(
{line.slice(seg.start, seg.end)}
);
idx = seg.end;
}
if (idx < line.length) parts.push(line.slice(idx));
if (li < lines.length - 1) parts.push('\n');
});
return parts;
};
const CodeBlock: React.FC<{ code: string }> = ({ code }) => (
{highlightCode(code)}
);
const FeatureCard: React.FC<{ feature: typeof features[0] }> = ({ feature }) => {
const [hovered, setHovered] = useState(false);
return (
setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
{feature.title}
{feature.desc}
);
};
// ============================================
// Styles
// ============================================
const S = {
page: {
width: '100%',
minHeight: '100vh',
overflowY: 'auto',
overflowX: 'hidden',
} as React.CSSProperties,
// Hero
hero: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '100vh',
padding: '60px 40px 40px',
position: 'relative',
} as React.CSSProperties,
heroContent: {
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: 150,
alignItems: 'center',
maxWidth: 880,
width: '100%',
} as React.CSSProperties,
heroContentMobile: {
display: 'grid',
gridTemplateColumns: '1fr',
gap: 32,
alignItems: 'center',
maxWidth: 880,
width: '100%',
} as React.CSSProperties,
heroText: {
textAlign: 'left' as const,
} as React.CSSProperties,
heroLogo: {
fontSize: 72,
lineHeight: 1,
marginBottom: 16,
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
} as React.CSSProperties,
heroTitle: {
fontFamily: "Nunito, 'Zen Maru Gothic', -apple-system, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif",
fontSize: 55,
fontWeight: 800,
lineHeight: 1.1,
color: '#FFF9E6',
textShadow: '0px 4px 1px rgba(0, 0, 0, 0.4)',
margin: '0 0 12px',
} as React.CSSProperties,
heroVersion: {
display: 'inline-block',
fontSize: 12,
fontWeight: 600,
padding: '2px 10px',
borderRadius: 10,
background: '#e6f9f6',
color: '#19c8b9',
marginLeft: 8,
verticalAlign: 'middle',
textShadow: 'none',
} as React.CSSProperties,
heroSubtitle: {
fontSize: 17,
color: '#7c5734',
lineHeight: 1.7,
margin: '0 0 28px',
maxWidth: 520,
} as React.CSSProperties,
heroActions: {
display: 'flex',
gap: 16,
alignItems: 'center',
} as React.CSSProperties,
// Sections
section: {
padding: '48px 40px',
maxWidth: 960,
margin: '0 auto',
} as React.CSSProperties,
sectionTitle: {
fontFamily: "Nunito, 'Zen Maru Gothic', -apple-system, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif",
fontSize: 24,
fontWeight: 700,
color: '#725d42',
margin: '0 0 8px',
textAlign: 'center' as const,
} as React.CSSProperties,
sectionDesc: {
fontSize: 14,
color: '#7c5734',
textAlign: 'center' as const,
marginBottom: 32,
} as React.CSSProperties,
// Features
features: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
gap: 16,
} as React.CSSProperties,
featureCard: {
padding: '24px 20px',
textAlign: 'center' as const,
} as React.CSSProperties,
featureIcon: { fontSize: 32, marginBottom: 12 } as React.CSSProperties,
featureTitle: {
fontSize: 15,
fontWeight: 700,
color: '#725d42',
marginBottom: 6,
} as React.CSSProperties,
featureDesc: {
fontSize: 13,
color: '#7c5734',
lineHeight: 1.6,
display: '-webkit-box' as const,
WebkitLineClamp: 3,
WebkitBoxOrient: 'vertical' as const,
overflow: 'hidden',
textOverflow: 'ellipsis' as const,
} as React.CSSProperties,
// Component grid
compGrid: {
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
gap: 12,
} as React.CSSProperties,
compCard: {
padding: '16px 20px',
cursor: 'pointer',
} as React.CSSProperties,
compName: {
fontSize: 15,
fontWeight: 700,
color: '#725d42',
marginBottom: 4,
} as React.CSSProperties,
compDesc: {
fontSize: 12,
color: '#7c5734',
lineHeight: 1.5,
} as React.CSSProperties,
// Code block
codeBox: {
maxWidth: 600,
margin: '0 auto',
padding: '20px 28px',
background: '#2b2118',
border: '1px solid #3d3028',
borderRadius: 20,
fontFamily: "'SF Mono', 'Fira Code', Consolas, monospace",
fontSize: 13,
fontWeight: 600,
color: '#e8d5bc',
textAlign: 'left' as const,
lineHeight: 1.8,
whiteSpace: 'pre' as const,
overflow: 'auto' as const,
tabSize: 4,
} as React.CSSProperties,
// Footer
footer: {
padding: '32px 40px',
textAlign: 'center' as const,
fontSize: 12,
color: '#7c5734',
marginTop: 32,
} as React.CSSProperties,
footerLinks: {
display: 'flex',
justifyContent: 'center',
gap: 20,
marginBottom: 12,
} as React.CSSProperties,
footerLink: {
fontSize: 13,
color: '#7c5734',
cursor: 'pointer',
} as React.CSSProperties,
};
// ============================================
// Data
// ============================================
const features = [
{
icon: 'nook1.svg',
title: 'Animal风格',
desc: 'SVG 有机形状裁切,3D 按压按钮,温暖质朴的自然 UI 质感',
},
{
icon: 'Property-Shopping.svg',
title: '18 个组件',
desc: 'Button / Input / Switch / Modal / Typewriter / Card / Collapse / Cursor / Divider / Time / Phone / Footer / Icon / Checkbox / Select / Tabs / CodeBlock / Loading 等',
},
{
icon: 'Property-Camera.svg',
title: '主题定制',
desc: '40+ CSS 自定义属性,运行时换肤无需重新构建',
},
{
icon: 'Property-Recipes.svg',
title: '开箱即用',
desc: 'ESM + CJS 双格式输出,TypeScript 类型声明完整',
},
];
const components = [
{
key: 'button',
name: 'Button',
desc: '5 种类型、3 种尺寸、加载/危险/幽灵模式',
},
{ key: 'input', name: 'Input', desc: '前后缀、一键清空、校验状态' },
{
key: 'switch',
name: 'Switch',
desc: '受控/非受控、自定义文案、加载状态',
},
{ key: 'checkbox', name: 'Checkbox', desc: '多选框组件,支持水平/垂直排列' },
{
key: 'select',
name: 'Select',
desc: '下拉选择器,支持搜索和禁用',
},
{ key: 'tabs', name: 'Tabs', desc: '标签页组件,支持受控/非受控模式' },
{ key: 'modal', name: 'Modal', desc: 'SVG 有机形状弹窗、ESC 关闭' },
{
key: 'typewriter',
name: 'Typewriter',
desc: '逐字打字机效果,支持多行与富内容',
},
{ key: 'card', name: 'Card', desc: '默认/标题两种卡片风格' },
{ key: 'collapse', name: 'Collapse', desc: 'FAQ 折叠面板、平滑展开动画' },
{ key: 'cursor', name: 'Cursor', desc: '自定义手指光标,支持多种尺寸' },
{ key: 'divider-comp', name: 'Divider', desc: '装饰性水平分割线' },
{ key: 'icon', name: 'Icon', desc: 'SVG 图标库' },
{ key: 'footer', name: 'Footer', desc: '页脚组件' },
{ key: 'time', name: 'Time', desc: '可爱风格时间显示' },
{ key: 'phone', name: 'Phone', desc: 'Phone 模拟器' },
{ key: 'codeblock', name: 'CodeBlock', desc: '代码语法高亮组件' },
{ key: 'loading', name: 'Loading', desc: '动森风格小岛加载动画' },
];
// ============================================
// HomePage
// ============================================
interface HomePageProps {
onNavigate?: (path: string) => void;
}
const HomePage: React.FC = ({ onNavigate }) => {
const isMobile = useIsMobile();
const [showScrollHint, setShowScrollHint] = useState(true);
const pageRef = React.useRef(null);
const handleScroll = () => {
if (pageRef.current) {
if (pageRef.current.scrollTop > 70) {
setShowScrollHint(false);
} else {
setShowScrollHint(true);
}
}
};
return (
{/* Hero */}
{isMobile && (
)}
{isMobile ? 'Animal Island UI' : <>Animal
Island UI>}
v0.8.1
Animal风格的 React 组件库,基于 TypeScript + Vite 构建,让 Web 应用充满温暖质感
{!isMobile && (
)}
{/* Features */}
特性
为什么选择 animal-island-ui
{features.map((f) => (
))}
{/* Components */}
组件一览
点击卡片查看详细文档和在线演示
{components.map((c) => (
onNavigate?.(`/${c.key}`)}
>
{c.name}
{c.desc}
))}
{/* Install */}
{/* Quick Start */}
快速上手
引入组件即可使用,样式自动加载
开始;\n}`}
/>
{/* Theme */}
主题定制
通过覆盖 CSS 自定义属性实现运行时换肤,无需重新构建
{/* Footer */}
onNavigate?.('/button')}
>
组件文档
window.open(
'https://github.com/guokaigdg/animal-island-ui',
'_blank'
)
}
>
GitHub
MIT License · React + TypeScript + Vite
);
}
export default HomePage;