lishiqi.conard 2e214c5b76 Add initial project files including .gitignore, LICENSE, README, and skill definitions
- Created .gitignore to exclude unnecessary files.
- Added MIT License for project licensing.
- Introduced README.md and README.zh-CN.md for documentation in English and Chinese.
- Implemented web design engineer skill with detailed workflow and design principles.
- Included advanced patterns and code templates for reference in the skill.
- Added demo HTML files showcasing the skill's capabilities.
2026-04-21 19:53:49 +08:00

522 lines
14 KiB
Markdown
Raw 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.

# Advanced Reference: Component Patterns & Code Templates
This file contains advanced patterns and code templates to reference when implementing specific tasks.
## Table of Contents
1. [Responsive Slide Engine](#responsive-slide-engine)
2. [Device Simulation Frames](#device-simulation-frames)
3. [Tweaks Panel Implementation](#tweaks-panel-implementation)
4. [Animation Timeline Engine](#animation-timeline-engine)
5. [Design Canvas (Multi-option Comparison)](#design-canvas)
6. [Dark Mode Toggle](#dark-mode-toggle)
7. [Data Visualization Templates](#data-visualization-templates)
---
## Responsive Slide Engine
For building fixed-size presentations that auto-fit to any viewport.
**Key conventions**:
- Internal arrays use 0-indexed, **but numbers displayed to the user are always 1-indexed**
- Each `<section class="slide">` gets `data-screen-label="01 Title"`, `data-screen-label="02 Agenda"`, etc. for easy reference
- Control buttons go **outside** the `.stage` scaled container to ensure usability on small screens
```html
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #000;
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
overflow: hidden;
font-family: system-ui, sans-serif;
}
.stage {
width: 1920px;
height: 1080px;
position: relative;
transform-origin: center center;
}
.slide {
position: absolute;
inset: 0;
display: none;
padding: 80px;
}
.slide.active { display: flex; }
.controls {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 12px;
z-index: 1000;
}
.controls button {
padding: 8px 16px;
border: none;
border-radius: 6px;
background: rgba(255,255,255,0.15);
color: white;
cursor: pointer;
font-size: 14px;
}
.slide-counter {
position: fixed;
bottom: 20px;
right: 20px;
color: rgba(255,255,255,0.6);
font-size: 14px;
}
</style>
<script>
// Auto-fit scaling
function scaleStage() {
const stage = document.querySelector('.stage');
const scaleX = window.innerWidth / 1920;
const scaleY = window.innerHeight / 1080;
const scale = Math.min(scaleX, scaleY);
stage.style.transform = `scale(${scale})`;
}
window.addEventListener('resize', scaleStage);
scaleStage();
// Slide navigation
let current = parseInt(localStorage.getItem('slideIndex') || '0');
const slides = document.querySelectorAll('.slide');
function showSlide(n) {
current = Math.max(0, Math.min(n, slides.length - 1));
slides.forEach((s, i) => s.classList.toggle('active', i === current));
localStorage.setItem('slideIndex', current);
// Display 1-indexed to user, store 0-indexed internally
document.querySelector('.slide-counter').textContent = `${current + 1} / ${slides.length}`;
}
document.addEventListener('keydown', e => {
if (e.key === 'ArrowRight' || e.key === ' ') showSlide(current + 1);
if (e.key === 'ArrowLeft') showSlide(current - 1);
});
showSlide(current);
</script>
```
---
## Device Simulation Frames
### iPhone Frame
```jsx
const IPhoneFrame = ({ children, title = "App" }) => (
<div style={{
width: 390,
height: 844,
borderRadius: 48,
border: '12px solid #1a1a1a',
overflow: 'hidden',
position: 'relative',
boxShadow: '0 25px 50px -12px rgba(0,0,0,0.25)',
background: '#fff'
}}>
{/* Status bar */}
<div style={{
height: 54,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '0 24px',
fontSize: 14,
fontWeight: 600
}}>
<span>9:41</span>
<div style={{
width: 126,
height: 34,
background: '#1a1a1a',
borderRadius: 20,
position: 'absolute',
left: '50%',
transform: 'translateX(-50%)',
top: 8
}} />
<span> 📶</span>
</div>
{/* Content */}
<div style={{ height: 'calc(100% - 54px)', overflow: 'auto' }}>
{children}
</div>
{/* Home indicator */}
<div style={{
position: 'absolute',
bottom: 8,
left: '50%',
transform: 'translateX(-50%)',
width: 134,
height: 5,
background: '#1a1a1a',
borderRadius: 3
}} />
</div>
);
```
### Browser Window Frame
```jsx
const BrowserFrame = ({ children, url = "https://example.com", title = "Page" }) => (
<div style={{
borderRadius: 12,
overflow: 'hidden',
boxShadow: '0 25px 50px -12px rgba(0,0,0,0.25)',
border: '1px solid #e5e5e5'
}}>
{/* Title bar */}
<div style={{
background: '#f5f5f5',
padding: '12px 16px',
display: 'flex',
alignItems: 'center',
gap: 12,
borderBottom: '1px solid #e5e5e5'
}}>
<div style={{ display: 'flex', gap: 8 }}>
<div style={{ width: 12, height: 12, borderRadius: '50%', background: '#ff5f57' }} />
<div style={{ width: 12, height: 12, borderRadius: '50%', background: '#febc2e' }} />
<div style={{ width: 12, height: 12, borderRadius: '50%', background: '#28c840' }} />
</div>
<div style={{
flex: 1,
background: '#fff',
borderRadius: 6,
padding: '6px 12px',
fontSize: 13,
color: '#666',
border: '1px solid #e0e0e0'
}}>
{url}
</div>
</div>
{/* Content */}
<div style={{ background: '#fff' }}>
{children}
</div>
</div>
);
```
---
## Tweaks Panel Implementation
```jsx
const TweaksPanel = ({ config, onChange, visible }) => {
if (!visible) return null;
return (
<div style={{
position: 'fixed',
bottom: 20,
right: 20,
width: 280,
background: 'rgba(24, 24, 27, 0.95)',
backdropFilter: 'blur(12px)',
borderRadius: 12,
padding: 16,
color: '#fff',
fontSize: 13,
zIndex: 9999,
boxShadow: '0 20px 60px rgba(0,0,0,0.3)',
border: '1px solid rgba(255,255,255,0.1)'
}}>
<div style={{ fontWeight: 600, marginBottom: 12, fontSize: 14 }}>Tweaks</div>
{Object.entries(config).map(([key, value]) => (
<div key={key} style={{ marginBottom: 12 }}>
<label style={{ display: 'block', marginBottom: 4, opacity: 0.7 }}>
{key}
</label>
{typeof value === 'boolean' ? (
<input
type="checkbox"
checked={value}
onChange={e => onChange({ ...config, [key]: e.target.checked })}
/>
) : typeof value === 'number' ? (
<input
type="range"
min="0"
max="100"
value={value}
onChange={e => onChange({ ...config, [key]: Number(e.target.value) })}
style={{ width: '100%' }}
/>
) : value.startsWith('#') ? (
<input
type="color"
value={value}
onChange={e => onChange({ ...config, [key]: e.target.value })}
/>
) : (
<input
type="text"
value={value}
onChange={e => onChange({ ...config, [key]: e.target.value })}
style={{
width: '100%',
background: 'rgba(255,255,255,0.1)',
border: '1px solid rgba(255,255,255,0.2)',
borderRadius: 4,
padding: '4px 8px',
color: '#fff'
}}
/>
)}
</div>
))}
</div>
);
};
```
---
## Animation Timeline Engine
```jsx
const useTime = (duration = 5000) => {
const [time, setTime] = React.useState(0);
const [playing, setPlaying] = React.useState(true);
const frameRef = React.useRef();
const startRef = React.useRef();
React.useEffect(() => {
if (!playing) return;
const animate = (timestamp) => {
if (!startRef.current) startRef.current = timestamp;
const elapsed = (timestamp - startRef.current) % duration;
setTime(elapsed / duration); // 0 to 1
frameRef.current = requestAnimationFrame(animate);
};
frameRef.current = requestAnimationFrame(animate);
return () => cancelAnimationFrame(frameRef.current);
}, [playing, duration]);
return { time, playing, setPlaying };
};
const Easing = {
linear: t => t,
easeInOut: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
easeOut: t => 1 - Math.pow(1 - t, 3),
easeIn: t => t * t * t,
spring: t => 1 - Math.pow(Math.E, -6 * t) * Math.cos(8 * t)
};
const interpolate = (t, from, to, easing = Easing.easeInOut) => {
const progress = easing(Math.max(0, Math.min(1, t)));
return from + (to - from) * progress;
};
// Usage example:
// const { time } = useTime(3000);
// const opacity = interpolate(time, 0, 1);
// const x = interpolate(time, -100, 0, Easing.spring);
```
---
## Design Canvas
For displaying multiple design options side by side for comparison:
```jsx
const DesignCanvas = ({ options, columns = 3 }) => (
<div style={{
display: 'grid',
gridTemplateColumns: `repeat(${columns}, 1fr)`,
gap: 24,
padding: 40,
background: '#f8f9fa',
minHeight: '100vh'
}}>
{options.map((option, i) => (
<div key={i} style={{
background: '#fff',
borderRadius: 12,
overflow: 'hidden',
boxShadow: '0 1px 3px rgba(0,0,0,0.1)'
}}>
<div style={{
padding: '12px 16px',
borderBottom: '1px solid #eee',
fontSize: 13,
fontWeight: 600,
color: '#666'
}}>
Option {String.fromCharCode(65 + i)}: {option.label}
</div>
<div style={{ padding: 16 }}>
{option.content}
</div>
</div>
))}
</div>
);
```
---
## Dark Mode Toggle
```jsx
const ThemeProvider = ({ children }) => {
const [dark, setDark] = React.useState(
window.matchMedia('(prefers-color-scheme: dark)').matches
);
const theme = dark ? {
bg: '#0a0a0b',
surface: '#18181b',
border: '#27272a',
text: '#fafafa',
textMuted: '#a1a1aa',
primary: '#3b82f6'
} : {
bg: '#ffffff',
surface: '#f4f4f5',
border: '#e4e4e7',
text: '#18181b',
textMuted: '#71717a',
primary: '#2563eb'
};
return (
<ThemeContext.Provider value={{ theme, dark, setDark }}>
<div style={{ background: theme.bg, color: theme.text, minHeight: '100vh' }}>
{children}
</div>
</ThemeContext.Provider>
);
};
```
---
## Data Visualization Templates
### Chart.js Quick Start
```html
<canvas id="myChart" width="800" height="400"></canvas>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const ctx = document.getElementById('myChart').getContext('2d');
new Chart(ctx, {
type: 'line', // bar, pie, doughnut, radar, etc.
data: {
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
datasets: [{
label: 'Revenue',
data: [12, 19, 3, 5, 2, 3],
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false }
},
scales: {
y: { beginAtZero: true, grid: { color: '#f0f0f0' } },
x: { grid: { display: false } }
}
}
});
</script>
```
---
## Color System Best Practices
Use oklch to define a harmonious color system:
```css
:root {
/* oklch-based color system */
--primary-h: 250; /* hue */
--primary: oklch(0.55 0.25 var(--primary-h));
--primary-light: oklch(0.75 0.15 var(--primary-h));
--primary-dark: oklch(0.35 0.2 var(--primary-h));
/* Neutrals */
--gray-50: oklch(0.98 0.002 250);
--gray-100: oklch(0.96 0.004 250);
--gray-200: oklch(0.92 0.006 250);
--gray-300: oklch(0.87 0.008 250);
--gray-400: oklch(0.71 0.01 250);
--gray-500: oklch(0.55 0.014 250);
--gray-600: oklch(0.45 0.014 250);
--gray-700: oklch(0.37 0.014 250);
--gray-800: oklch(0.27 0.014 250);
--gray-900: oklch(0.21 0.014 250);
}
```
---
## Font Recommendations (Non-default Choices)
> ⚠️ **These are experience-based suggestions, not hard rules.**
> - Always prefer fonts already specified by the brand or design system; only refer to this table when the user hasn't provided any font scheme.
> - The only hard rule: **Avoid Inter / Roboto / Arial / Fraunces / system-ui — fonts overused by AI-generated content** that instantly signal "this was assembled by AI."
> - When choosing fonts, focus on "personality fit" rather than "what's trendy." The table below lists common high-quality choices, not an exhaustive list.
| Use Case | Recommendation | Google Fonts Name |
|------|------|------------------|
| Modern headings | Plus Jakarta Sans | Plus+Jakarta+Sans |
| Elegant body text | Outfit | Outfit |
| Technical feel | Space Grotesk | Space+Grotesk |
| Premium brand | Sora | Sora |
| Editorial feel | Newsreader | Newsreader |
| Handwritten style | Caveat | Caveat |
| Monospace / code | JetBrains Mono | JetBrains+Mono |
```html
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap" rel="stylesheet">
```
---
## Color × Font Pairing Reference
> ⚠️ **These are experience-based pairing suggestions, not hard rules.** When you have **absolutely no design context**, pick one as a starting point — it's far better than starting from Inter + #3b82f6.
> Once the user provides a brand / design system / reference site, drop this table immediately and follow their materials.
For quickly establishing a visual system with personality:
| Style | Primary Color (oklch) | Font Pairing | Best For |
|---|---|---|---|
| Modern tech | `oklch(0.55 0.25 250)` blue-violet | Space Grotesk + Inter | SaaS, dev tools, AI products |
| Elegant editorial | `oklch(0.35 0.10 30)` warm brown | Newsreader + Outfit | Content platforms, blogs, editorial |
| Premium brand | `oklch(0.20 0.02 250)` near-black | Sora + Plus Jakarta Sans | Luxury, consulting, finance |
| Lively consumer | `oklch(0.70 0.20 30)` coral | Plus Jakarta Sans + Outfit | E-commerce, lifestyle, social |
| Minimal professional | `oklch(0.50 0.15 200)` teal-blue | Outfit + Space Grotesk | Data products, dashboards, B2B |
| Artisan warmth | `oklch(0.55 0.15 80)` caramel | Caveat (decorative) + Newsreader | Food & beverage, education, creative |
Avoid these combos:
- ❌ Inter + Roboto + blue buttons (peak AI aesthetic)
- ❌ Fraunces + purple-pink gradients (overused)
- ❌ More than three font families (visual chaos)