Files

579 lines
20 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;