feat: 新增 design-lab——Vite MPA + Tailwind v4,设计系统可运行版(五页:目录/颜色/符号/组件/样例)

This commit is contained in:
cqh963852 committed 2026-10-05 17:48:00 +08:00
1 parent f3f9b94208
commit 5f5c805fad
29 files changed
+1106

No files matched your search

@@ -0,0 +1,119 @@
/** CourseCard —— 教程的入口载体(孟菲斯骨架 × 拟物纸感)
* 卡头三件套=靶子:▲目标 + ○预期结果(16:9 相框) + ●积木点列
* 交互:hover/聚焦拿起(浮起+投影加深)并播放 GIF;desc ⇄ GIF 每 8000ms 翻转;
* hover/Tab 锁定;触屏与 prefers-reduced-motion 不翻转。 */
import { useEffect, useRef, useState } from 'react'
import type { Chapter } from '../lib/types'
import { Ring, Tri } from './shapes'
const FLIP_MS = 8000
export function BrickDots({ bricks }: { bricks: Chapter['target']['bricks'] }) {
return (
<span className="flex items-center gap-1" aria-label={`积木 ${bricks.filter((b) => b.learned).length}/${bricks.length}`}>
{bricks.map((b, i) => (
<i
key={i}
className="inline-block h-[7px] w-[7px] rounded-full border-[1.5px] border-ink"
style={{ background: b.learned ? 'var(--color-ink)' : '#fff' }}
/>
))}
</span>
)
}
export function EntryBadge({ n }: { n: 1 | 2 | 3 }) {
return (
<span
className="absolute -left-[11px] -top-[11px] z-10 flex h-6 w-6 items-center justify-center rounded-full border-2 border-paper bg-ink text-[12px] font-extrabold text-clash2a"
aria-label={`推荐入口 ${n}`}
>
{'①②③'[n - 1]}
</span>
)
}
export function MemphisCard({ chapter, entry, compact = false }: { chapter: Chapter; entry?: 1 | 2 | 3; compact?: boolean }) {
const [gifOn, setGifOn] = useState(false)
const [locked, setLocked] = useState(false)
const boxRef = useRef<HTMLAnchorElement>(null)
useEffect(() => {
const coarse = matchMedia('(pointer: coarse)').matches
const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches
if (coarse || reduced) return
const io = new IntersectionObserver(
([e]) => {
if (e.isIntersecting) setGifOn(true)
else setGifOn(false)
},
{ threshold: 0.3 },
)
if (boxRef.current) io.observe(boxRef.current)
return () => io.disconnect()
}, [])
useEffect(() => {
if (!gifOn) return
const t = setInterval(() => {
setFlip((f) => !f)
}, FLIP_MS)
return () => clearInterval(t)
}, [gifOn])
const [flip, setFlip] = useState(false)
const showGif = gifOn && (locked ? true : flip)
return (
<a
ref={boxRef}
href={`#ch${chapter.id}`}
className="group relative block rounded-[14px] border-2 border-ink bg-paper p-[11px] no-underline
shadow-[0_3px_0_var(--color-stack1),0_5px_0_var(--color-stack2),7px_8px_0_var(--color-ink)]
transition-transform duration-150 focus-visible:outline-none
hover:-translate-y-[3px] hover:shadow-[0_3px_0_var(--color-stack1),0_5px_0_var(--color-stack2),10px_11px_0_var(--color-ink)]
focus-visible:-translate-y-[3px] focus-visible:shadow-[0_3px_0_var(--color-stack1),0_5px_0_var(--color-stack2),10px_11px_0_var(--color-ink)]"
onMouseEnter={() => setLocked(true)}
onMouseLeave={() => setLocked(false)}
onFocus={() => setLocked(true)}
onBlur={() => setLocked(false)}
aria-label={`${chapter.title}:${chapter.target.goal}`}
>
{entry && <EntryBadge n={entry} />}
{/* ○ 预期结果:16:9 相框(章节撞色只进这里) */}
<div
className="relative flex aspect-video items-center justify-center overflow-hidden rounded-lg border-[3px] border-ink"
style={{
background: chapter.color,
boxShadow: 'inset 0 3px 8px rgba(26,26,26,0.30), inset 0 -1px 0 rgba(255,255,255,0.55)',
}}
>
{showGif ? (
chapter.gifUrl ? (
<img src={chapter.gifUrl} alt={chapter.target.expectation} className="h-full w-full object-cover" />
) : (
<span className="px-3 text-center text-[11px] font-bold leading-snug text-paper">
▶ {chapter.target.expectation}
</span>
)
) : (
<span className="px-3 text-center text-[11px] leading-snug text-ink/80 mix-blend-multiply">
{chapter.target.goal}
</span>
)}
<span className="absolute left-1.5 top-1 text-[9px] font-bold tracking-wide text-paper">
预期结果{showGif ? ' · GIF' : ''}
</span>
<Ring size={16} className="absolute bottom-1.5 right-1.5" />
</div>
{/* 卡头:▲ 目标 + 标题 + ● 积木点列 */}
<div className="mt-2 flex items-center gap-2">
<Tri size={14} />
<b className="flex-1 text-[14px] text-[#2b1d10] [text-shadow:0_1px_1px_rgba(255,255,255,0.9)]">
{compact ? chapter.title : `${chapter.emoji} ${chapter.title}`}
</b>
{!compact && <BrickDots bricks={chapter.target.bricks} />}
</div>
</a>
)
}