feat: import animal-island-ui snapshot

This commit is contained in:
2026-05-19 19:31:52 +08:00
commit e1065947d6
139 changed files with 14957 additions and 0 deletions
+578
View File
@@ -0,0 +1,578 @@
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(
<span key={`${li}-${seg.start}`} style={seg.style}>
{line.slice(seg.start, seg.end)}
</span>
);
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 }) => (
<pre style={S.codeBox}>{highlightCode(code)}</pre>
);
const FeatureCard: React.FC<{ feature: typeof features[0] }> = ({ feature }) => {
const [hovered, setHovered] = useState(false);
return (
<Card
style={{
...S.featureCard,
transform: hovered ? 'translateY(-4px)' : 'none',
boxShadow: hovered
? '0 8px 24px rgba(114, 93, 66, 0.15)'
: 'none',
transition: 'all 0.3s ease',
}}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<img
src={
new URL(
`./img/nook-phone/${feature.icon}`,
import.meta.url
).href
}
style={{
width: 42,
height: 42,
transform: hovered
? 'scale(1.1) rotate(-4deg)'
: 'scale(1) rotate(0deg)',
transition: 'transform 0.3s ease',
animation: hovered ? 'iconBounce 0.4s ease forwards' : 'none',
}}
alt={feature.title}
/>
<style>
{`
@keyframes iconBounce {
0% { transform: scale(1) rotate(0deg); }
50% { transform: scale(1.2) rotate(-5deg); }
100% { transform: scale(1.1) rotate(-4deg); }
}
`}
</style>
<div style={S.featureTitle}>{feature.title}</div>
<div style={S.featureDesc}>{feature.desc}</div>
</Card>
);
};
// ============================================
// 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<HomePageProps> = ({ onNavigate }) => {
const isMobile = useIsMobile();
const [showScrollHint, setShowScrollHint] = useState(true);
const pageRef = React.useRef<HTMLDivElement>(null);
const handleScroll = () => {
if (pageRef.current) {
if (pageRef.current.scrollTop > 70) {
setShowScrollHint(false);
} else {
setShowScrollHint(true);
}
}
};
return (
<div ref={pageRef} style={{ ...S.page, overflow: 'auto' }} onScroll={handleScroll}>
{/* Hero */}
<div style={{ ...S.hero }}>
<div style={isMobile ? S.heroContentMobile : S.heroContent}>
{isMobile && (
<div style={{ textAlign: 'center' }}>
<img
src={
new URL('./img/animal_icon.png', import.meta.url).href
}
style={{ width: 180, height: 112 }}
alt="logo"
decoding="async"
/>
</div>
)}
<div style={isMobile ? { textAlign: 'center' as const } : S.heroText}>
<h1 style={{ ...S.heroTitle, fontSize: isMobile ? 37 : 60 }}>
{isMobile ? 'Animal Island UI' : <>Animal <br /> Island UI</>}
<span style={S.heroVersion}>v0.8.1</span>
</h1>
<Typewriter speed={60}>
<p style={{ ...S.heroSubtitle, fontSize: isMobile ? 14 : 17 }}>
Animal风格的 React TypeScript + Vite Web
</p>
</Typewriter>
<div style={{ ...S.heroActions, justifyContent: isMobile ? 'center' : 'flex-start' }}>
<Button
type="primary"
size="large"
onClick={() => onNavigate?.('/button')}
>
使
</Button>
</div>
</div>
{!isMobile && (
<div style={{ textAlign: 'center' }}>
<img
src={
new URL('./img/animal_icon.png', import.meta.url).href
}
style={{ width: 320, height: 200 }}
alt="logo"
decoding="async"
/>
</div>
)}
</div>
</div>
<div style={{
position: 'absolute',
bottom: 40,
left: '50%',
transform: 'translateX(-50%)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: 4,
cursor: 'pointer',
animation: showScrollHint ? 'bounce 2s ease-in-out infinite' : 'none',
opacity: showScrollHint ? 1 : 0,
transition: 'opacity 0.3s ease',
pointerEvents: showScrollHint ? 'auto' : 'none'
}}>
<span style={{ color: '#FFF9E6', fontSize: 12, textShadow: '0 1px 2px rgba(0,0,0,0.3)' }}></span>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M12 5v14M5 12l7 7 7-7" stroke="#FFF9E6" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
<style>{`
@keyframes bounce {
0%, 100% { transform: translateX(-50%) translateY(0); opacity: 1; }
50% { transform: translateX(-50%) translateY(-8px); opacity: 0.7; }
}
`}</style>
{/* Features */}
<div style={{ ...S.section, padding: isMobile ? '32px 16px' : '48px 40px' }}>
<div style={S.sectionTitle}></div>
<div style={S.sectionDesc}> animal-island-ui</div>
<div style={S.features}>
{features.map((f) => (
<FeatureCard key={f.title} feature={f} />
))}
</div>
</div>
<Divider style={{ width: isMobile ? '90%' : 800, margin: '0 auto' }} />
{/* Components */}
<div style={{ ...S.section, padding: isMobile ? '32px 16px' : '48px 40px' }}>
<div style={S.sectionTitle}></div>
<div style={S.sectionDesc}>线</div>
<div style={S.compGrid}>
{components.map((c) => (
<Card
key={c.key}
style={S.compCard}
onClick={() => onNavigate?.(`/${c.key}`)}
>
<div style={S.compName}>{c.name}</div>
<div style={S.compDesc}>{c.desc}</div>
</Card>
))}
</div>
</div>
<Divider style={{ width: isMobile ? '90%' : 800, margin: '0 auto' }} />
{/* Install */}
<div style={{ ...S.section, padding: isMobile ? '32px 16px' : '48px 40px' }}>
<div style={S.sectionTitle}></div>
<div style={S.sectionDesc}></div>
<CodeBlock
code={`// 使用 npm 安装\nnpm install animal-island-ui`}
/>
</div>
<Divider style={{ width: isMobile ? '90%' : 800, margin: '0 auto' }} />
{/* Quick Start */}
<div style={{ ...S.section, padding: isMobile ? '32px 16px' : '48px 40px' }}>
<div style={S.sectionTitle}></div>
<div style={S.sectionDesc}>使</div>
<CodeBlock
code={`// 1. 引入组件\nimport { Button, Modal, Switch } from 'animal-island-ui';\nimport 'animal-island-ui/style';\n\nfunction App() {\n return <Button>开始</Button>;\n}`}
/>
</div>
<Divider style={{ width: isMobile ? '90%' : 800, margin: '0 auto' }} />
{/* Theme */}
<div style={{ ...S.section, padding: isMobile ? '32px 16px' : '48px 40px' }}>
<div style={S.sectionTitle}></div>
<div style={S.sectionDesc}>
CSS
</div>
<CodeBlock
code={`/* 覆盖主题变量 */\n:root {\n --animal-primary-color: #19c8b9;\n --animal-text-color: #827157;\n --animal-font-family: 'Noto Sans SC', sans-serif;\n --animal-border-radius-base: 18px;\n /* ... 40+ 设计令牌 */\n}`}
/>
</div>
{/* Footer */}
<div style={{ ...S.footer, padding: isMobile ? '24px 16px' : '32px 40px' }}>
<div style={S.footerLinks}>
<span
style={S.footerLink}
onClick={() => onNavigate?.('/button')}
>
</span>
<span
style={S.footerLink}
onClick={() =>
window.open(
'https://github.com/guokaigdg/animal-island-ui',
'_blank'
)
}
>
GitHub
</span>
</div>
<div>MIT License · React + TypeScript + Vite</div>
</div>
</div>
);
}
export default HomePage;