feat: 新增 design-lab——Vite MPA + Tailwind v4,设计系统可运行版(五页:目录/颜色/符号/组件/样例)
This commit is contained in:
1 parent
f3f9b94208
commit
5f5c805fad
29 files changed
+1106
No files matched your search
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
dist
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>design-lab · colors</title>
|
||||
</head>
|
||||
<body class="bg-paper text-ink">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/pages/colors/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>design-lab · components</title>
|
||||
</head>
|
||||
<body class="bg-paper text-ink">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/pages/components/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>design-lab · index</title>
|
||||
</head>
|
||||
<body class="bg-paper text-ink">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/pages/index/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@stack/design-lab",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.1.0",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"tailwindcss": "^4.1.0",
|
||||
"typescript": "~5.6.2",
|
||||
"vite": "^6.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>design-lab · shelf</title>
|
||||
</head>
|
||||
<body class="bg-paper text-ink">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/pages/shelf/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,35 @@
|
||||
/** 入口梳理 —— 读者画像推荐路径:是推荐不是强制(无锁) */
|
||||
import { CHAPTERS, PROFILES } from '../lib/chapters'
|
||||
|
||||
export function EntryGuide() {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-5">
|
||||
{PROFILES.map((p) => (
|
||||
<div key={p.name} className="w-[252px] rounded-xl border-2 border-ink bg-paper p-3 shadow-[5px_5px_0_var(--color-ink)]">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="flex h-[26px] w-[26px] items-center justify-center rounded-lg border-2 border-ink text-[14px]"
|
||||
style={{ background: CHAPTERS[p.entry].color }}
|
||||
>
|
||||
{p.icon}
|
||||
</span>
|
||||
<b className="text-[13px]">{p.name}</b>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap items-center gap-1.5">
|
||||
{p.path.map((id, i) => (
|
||||
<span key={id} className="flex items-center gap-1.5">
|
||||
{i > 0 && <span className="text-[10px] text-mute">→</span>}
|
||||
<span
|
||||
className={`rounded-full border-[1.5px] border-ink px-2.5 py-0.5 text-[10.5px] ${id === p.entry ? 'bg-clash2a font-extrabold' : 'bg-white'}`}
|
||||
>
|
||||
{id === p.entry && ['①', '②', '③'][PROFILES.indexOf(p)]} {CHAPTERS[id].title}
|
||||
</span>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<p className="m-0 mt-2 text-[10px] leading-relaxed text-mute">{p.note}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/** RouteLine —— 关系线:线形即含义(实=先后 虚=相关 点=同族)
|
||||
* 拓扑:横平竖直 + R8 直角步进;并行不合并(4px);站点 r6 半嵌卡边
|
||||
* (左贴描边、右骑硬投影外缘);同口多线自动聚合胶囊(线止于胶囊边界)。 */
|
||||
import type { ChapterId, Edge } from '../lib/types'
|
||||
|
||||
const STATION_COLORS = ['var(--color-clash2a)', 'var(--color-clash1b)', 'var(--color-clash3a)', 'var(--color-clash1a)']
|
||||
|
||||
export interface LaneNode {
|
||||
id: ChapterId
|
||||
borderLeft: number // 左描边 x(无影侧,入口贴这里)
|
||||
right: number // 右侧黑影外缘 x(描边 + 7,出口从这里出发)
|
||||
y: number // 卡中线 y
|
||||
}
|
||||
|
||||
/** 水平出发 → R8 双弯直角步进 → 水平到达;同 y 时直线 */
|
||||
function stepPath(x0: number, y0: number, x1: number, y1: number, r = 8): string {
|
||||
if (Math.abs(y1 - y0) < 1) return `M ${x0} ${y0} H ${x1}`
|
||||
const down = y1 > y0
|
||||
const sweep1 = down ? 1 : 0
|
||||
const sweep2 = down ? 0 : 1
|
||||
const cornerX = Math.max(x0 + r, x1 - 2 * r)
|
||||
return `M ${x0} ${y0} H ${cornerX} A ${r} ${r} 0 0 ${sweep1} ${cornerX + r} ${y0 + (down ? r : -r)} V ${y1 - (down ? r : -r)} A ${r} ${r} 0 0 ${sweep2} ${cornerX + 2 * r} ${y1} H ${x1}`
|
||||
}
|
||||
|
||||
export function RouteLine({
|
||||
edges,
|
||||
nodes,
|
||||
width,
|
||||
height,
|
||||
}: {
|
||||
edges: Edge[]
|
||||
nodes: Partial<Record<ChapterId, LaneNode>>
|
||||
width: number
|
||||
height: number
|
||||
}) {
|
||||
const stationColor = (i: number) => STATION_COLORS[i % STATION_COLORS.length]
|
||||
|
||||
const seqEdges = edges.filter((e) => e.kind === 'seq')
|
||||
const insByTo = new Map<ChapterId, Edge[]>()
|
||||
seqEdges.forEach((e) => insByTo.set(e.to, [...(insByTo.get(e.to) ?? []), e]))
|
||||
const firstIn = new Set<ChapterId>()
|
||||
insByTo.forEach((list) => firstIn.add(list[0].to))
|
||||
|
||||
let si = 0
|
||||
return (
|
||||
<svg
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox={`0 0 ${width} ${height}`}
|
||||
className="pointer-events-none absolute left-0 top-0"
|
||||
aria-hidden
|
||||
>
|
||||
{edges.map((e, i) => {
|
||||
const a = nodes[e.from]
|
||||
const b = nodes[e.to]
|
||||
if (!a || !b) return null
|
||||
const d = stepPath(a.right, a.y, b.borderLeft, b.y)
|
||||
if (e.kind === 'rel') return <path key={i} d={d} stroke="var(--color-ink)" strokeWidth={2} fill="none" strokeDasharray="4 2" />
|
||||
if (e.kind === 'family') return <path key={i} d={d} stroke="var(--color-ink)" strokeWidth={2.4} fill="none" strokeDasharray="0.1 6" strokeLinecap="round" />
|
||||
return <path key={i} d={d} stroke="var(--color-ink)" strokeWidth={2.5} fill="none" />
|
||||
})}
|
||||
{seqEdges.map((e) => {
|
||||
const a = nodes[e.from]
|
||||
const b = nodes[e.to]
|
||||
if (!a || !b) return null
|
||||
const ins = insByTo.get(e.to)!
|
||||
const capsule = ins.length > 1 && firstIn.has(e.to)
|
||||
// 源站点骑出边卡黑影外缘;目标:单线=圆点贴描边,多线=胶囊(只画一次)
|
||||
return (
|
||||
<g key={`st-${e.from}-${e.to}`}>
|
||||
<circle cx={a.right} cy={a.y} r={6} fill={stationColor(si++)} stroke="var(--color-ink)" strokeWidth={2} />
|
||||
{capsule ? (
|
||||
<rect x={b.borderLeft - 3} y={b.y - 10} width={6} height={20} rx={3} fill="var(--color-clash1a)" stroke="var(--color-ink)" strokeWidth={2} />
|
||||
) : ins.length === 1 ? (
|
||||
<circle cx={b.borderLeft} cy={b.y} r={6} fill={stationColor(si++)} stroke="var(--color-ink)" strokeWidth={2} />
|
||||
) : null}
|
||||
</g>
|
||||
)
|
||||
})}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/** 票券 —— 只表达完成状态:完成=沿撕线打孔(半圆缺口),未开始=实线无孔。
|
||||
* 裁切式实现:头部色块无独立描边,由票体 overflow-hidden 裁出单一描边圆角。 */
|
||||
import type { Chapter } from '../lib/types'
|
||||
|
||||
export function Ticket({ chapter, done }: { chapter: Chapter; done: boolean }) {
|
||||
return (
|
||||
<div
|
||||
className={`relative flex w-[216px] items-stretch overflow-hidden rounded-xl border-2 border-ink bg-paper shadow-[5px_5px_0_var(--color-ink)] ${done ? 'ticket-pk' : ''}`}
|
||||
style={{ ['--tk-color' as string]: chapter.color }}
|
||||
>
|
||||
{/* 头部媒体块:章节色纯填充,无独立边框(由票体裁切) */}
|
||||
<div className="flex w-14 shrink-0 items-center justify-center text-[22px]" style={{ background: chapter.color }}>{chapter.emoji}</div>
|
||||
<div className="flex-1 py-2 pl-2.5 pr-2">
|
||||
<b className="text-[12.5px]">{chapter.title}</b>
|
||||
<div className="mt-0.5 font-mono text-[8.5px] text-mute">
|
||||
No. {String(chapter.id).padStart(3, '0')} · {done ? '已完成(打孔)' : '未开始(无孔)'}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={`flex w-[42px] items-center justify-center font-mono text-[9.5px] text-mute ${done ? 'border-l-[1.5px] border-dashed border-ink' : 'border-l-[1.5px] border-solid border-ink'}`}
|
||||
>
|
||||
№{String(chapter.id).padStart(2, '0')}
|
||||
</div>
|
||||
<style>{`
|
||||
.ticket-pk::before, .ticket-pk::after {
|
||||
content: ''; position: absolute; left: 160px; width: 14px; height: 14px;
|
||||
border-radius: 50%; background: var(--color-paper); border: 2px solid var(--color-ink);
|
||||
}
|
||||
.ticket-pk::before { top: -10px; }
|
||||
.ticket-pk::after { bottom: -10px; }
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** 指派学习块 —— 普通内容块(非票据):+ 位置/预期/场景三样 + 脱靶标记 */
|
||||
export function AssignBlock({
|
||||
title,
|
||||
pos,
|
||||
expect,
|
||||
scenes,
|
||||
miss,
|
||||
}: {
|
||||
title: string
|
||||
pos: string
|
||||
expect: string
|
||||
scenes: string[]
|
||||
miss: { how: string; looks: string }
|
||||
}) {
|
||||
return (
|
||||
<div className="w-[260px] rounded-xl border-2 border-ink bg-paper p-3 shadow-[5px_5px_0_var(--color-ink)]">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg width="16" height="16" viewBox="0 0 18 18" aria-hidden>
|
||||
<path d="M9 1 v16 M1 9 h16" stroke="var(--color-clash2b)" strokeWidth="3.5" strokeLinecap="round" />
|
||||
<path d="M9 1 v16 M1 9 h16" stroke="var(--color-ink)" strokeWidth="1.2" />
|
||||
</svg>
|
||||
<b className="text-[13px]">{title}</b>
|
||||
</div>
|
||||
<div className="mt-1.5 space-y-0.5 text-[11px] leading-relaxed text-body">
|
||||
<div>
|
||||
<b>位置</b>:{pos}
|
||||
</div>
|
||||
<div>
|
||||
<b>预期</b>:{expect}
|
||||
</div>
|
||||
<div>
|
||||
<b>场景</b>:{scenes.join(' / ')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 border-t border-dashed border-ink/20 pt-2">
|
||||
<div className="flex items-start gap-2.5">
|
||||
<svg width="20" height="28" viewBox="0 0 24 34" className="mt-0.5 shrink-0" aria-hidden>
|
||||
<path d="M 12 32 L 8 27 L 16 22 L 8 17 L 16 12 L 12 7 L 14 2" fill="none" stroke="var(--color-clash1a)" strokeWidth="3" strokeLinejoin="round" strokeLinecap="round" />
|
||||
</svg>
|
||||
<div>
|
||||
<b className="text-[11.5px]">脱靶 · {miss.how}</b>
|
||||
<p className="m-0 mt-0.5 text-[10.5px] leading-relaxed text-body">{miss.looks}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/** 七个语义形状——装饰即语义(取自 ai-era-tutorial 词汇表) */
|
||||
import type { CSSProperties } from 'react'
|
||||
|
||||
interface ShapeProps {
|
||||
size?: number
|
||||
className?: string
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
/** ▲ 靶子:目标,卡头打头 */
|
||||
export const Tri = ({ size = 24, className, style }: ShapeProps) => (
|
||||
<svg width={size} height={size * 0.92} viewBox="0 0 26 24" className={className} style={style} aria-hidden>
|
||||
<path d="M13 2 L25 22 L1 22 Z" fill="var(--color-clash1a)" stroke="var(--color-ink)" strokeWidth="2" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
/** ● 积木:清单点列,实心=已学 */
|
||||
export const BrickDot = ({ size = 14, on = false, className, style }: ShapeProps & { on?: boolean }) => (
|
||||
<svg width={size} height={size} viewBox="0 0 14 14" className={className} style={style} aria-hidden>
|
||||
<circle cx="7" cy="7" r="5" fill={on ? 'var(--color-clash2a)' : 'var(--color-paper)'} stroke="var(--color-ink)" strokeWidth="2" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
/** ○ 预期:可核对的结果 */
|
||||
export const Ring = ({ size = 24, className, style }: ShapeProps) => (
|
||||
<svg width={size} height={size} viewBox="0 0 26 24" className={className} style={style} aria-hidden>
|
||||
<circle cx="13" cy="12" r="8" fill="none" stroke="var(--color-clash1b)" strokeWidth="4" />
|
||||
<circle cx="13" cy="12" r="8" fill="none" stroke="var(--color-ink)" strokeWidth="1.5" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
/** ~ 代理区:横波,交给 agent */
|
||||
export const Wave = ({ size = 40, className, style }: ShapeProps) => (
|
||||
<svg width={size} height={size * 0.5} viewBox="0 0 40 20" className={className} style={style} aria-hidden>
|
||||
<path d="M2 12 q5 -16 10 0 t10 0 t10 0" fill="none" stroke="var(--color-clash3b)" strokeWidth="3" strokeLinecap="round" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
/** ▬ 价值区:实心条,过程就地写清 */
|
||||
export const Bar = ({ size = 34, className, style }: ShapeProps) => (
|
||||
<svg width={size} height={size * 0.58} viewBox="0 0 34 20" className={className} style={style} aria-hidden>
|
||||
<rect x="2" y="6" width="30" height="9" fill="var(--color-clash3a)" stroke="var(--color-ink)" strokeWidth="2" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
/** ⌇ 脱靶:竖立颤抖——图形入口,读者追随图形进入阅读 */
|
||||
export const Tremble = ({ size = 24, className, style }: ShapeProps) => (
|
||||
<svg width={size} height={size * 1.4} viewBox="0 0 24 34" className={className} style={style} aria-hidden>
|
||||
<path
|
||||
d="M 12 32 L 8 27 L 16 22 L 8 17 L 16 12 L 12 7 L 14 2"
|
||||
fill="none"
|
||||
stroke="var(--color-clash1a)"
|
||||
strokeWidth="3.5"
|
||||
strokeLinejoin="round"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
|
||||
/** + 指派学习:位置/预期/场景三样 */
|
||||
export const Cross = ({ size = 18, className, style }: ShapeProps) => (
|
||||
<svg width={size} height={size} viewBox="0 0 18 18" className={className} style={style} aria-hidden>
|
||||
<path d="M9 1 v16 M1 9 h16" stroke="var(--color-clash2b)" strokeWidth="4" strokeLinecap="round" />
|
||||
<path d="M9 1 v16 M1 9 h16" stroke="var(--color-ink)" strokeWidth="1.2" />
|
||||
</svg>
|
||||
)
|
||||
|
||||
/** 脱靶标记:图形 + 文字,无容器(图形是第一视觉) */
|
||||
export function MissMark({ how, looks }: { how: string; looks: string }) {
|
||||
return (
|
||||
<div className="flex items-start gap-3">
|
||||
<Tremble size={22} className="mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<b className="text-[13px]">脱靶 · {how}</b>
|
||||
<p className="m-0 mt-0.5 text-[12px] leading-relaxed text-body">{looks}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import type { Assignment, Chapter, ChapterId, Edge, Profile } from './types'
|
||||
|
||||
export const CHAPTERS: Record<ChapterId, Chapter> = {
|
||||
1: {
|
||||
id: 1,
|
||||
title: '快速开始',
|
||||
emoji: '🌐',
|
||||
color: 'var(--color-ch1)',
|
||||
target: {
|
||||
goal: '本地跑起一个可交互的页面',
|
||||
expectation: '改一处代码,浏览器里立刻看到变化',
|
||||
bricks: [
|
||||
{ name: '页面骨架', learned: true },
|
||||
{ name: '请求与渲染', learned: true },
|
||||
{ name: '环境咬合', learned: true },
|
||||
{ name: '部署预览', learned: false },
|
||||
],
|
||||
},
|
||||
},
|
||||
2: {
|
||||
id: 2,
|
||||
title: 'React',
|
||||
emoji: '⚛️',
|
||||
color: 'var(--color-ch2)',
|
||||
target: {
|
||||
goal: '用组件拼出一个可维护的界面',
|
||||
expectation: '新增一种卡片类型不需要改动旧卡片',
|
||||
bricks: [
|
||||
{ name: '组件拼接', learned: true },
|
||||
{ name: '状态与副作用', learned: true },
|
||||
{ name: '受控数据流', learned: false },
|
||||
{ name: '性能边界', learned: false },
|
||||
],
|
||||
},
|
||||
},
|
||||
3: {
|
||||
id: 3,
|
||||
title: '更多示例',
|
||||
emoji: '🧱',
|
||||
color: 'var(--color-ch3)',
|
||||
target: {
|
||||
goal: '照着积木清单拼出三个小作品',
|
||||
expectation: '每个作品都能对照预期结果自检',
|
||||
bricks: [
|
||||
{ name: '表单页', learned: false },
|
||||
{ name: '列表页', learned: false },
|
||||
],
|
||||
},
|
||||
},
|
||||
4: {
|
||||
id: 4,
|
||||
title: '更多',
|
||||
emoji: '📦',
|
||||
color: 'var(--color-ch4)',
|
||||
target: {
|
||||
goal: '把示例扩展成自己的第一个完整小站',
|
||||
expectation: '有导航、有数据、能部署',
|
||||
bricks: [{ name: '路由', learned: false }],
|
||||
},
|
||||
},
|
||||
5: {
|
||||
id: 5,
|
||||
title: '全栈基础',
|
||||
emoji: '🔗',
|
||||
color: 'var(--color-ch5)',
|
||||
target: {
|
||||
goal: '把前端连上真实服务并部署上线',
|
||||
expectation: '别人能通过公网地址访问你的页面',
|
||||
bricks: [{ name: 'HTTP 与接口', learned: false }],
|
||||
},
|
||||
},
|
||||
6: {
|
||||
id: 6,
|
||||
title: '高级教程',
|
||||
emoji: '🧠',
|
||||
color: 'var(--color-ch6)',
|
||||
target: {
|
||||
goal: '指挥 agent 完成整栈项目',
|
||||
expectation: '你负责靶子与验收,agent 负责过程',
|
||||
bricks: [{ name: '识别力', learned: false }],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
/** 关系边:seq 实线先后 / rel 虚线相关 / family 点线同族 */
|
||||
export const EDGES: Edge[] = [
|
||||
{ from: 1, to: 2, kind: 'seq' },
|
||||
{ from: 2, to: 5, kind: 'seq' },
|
||||
{ from: 2, to: 5, kind: 'family' },
|
||||
]
|
||||
|
||||
export const PROFILES: Profile[] = [
|
||||
{
|
||||
icon: '🌱',
|
||||
name: '零基础入门',
|
||||
entry: 1,
|
||||
path: [1, 2, 3, 5],
|
||||
note: '完整链按序走;每步先核对预期再前进。',
|
||||
},
|
||||
{
|
||||
icon: '⚡',
|
||||
name: '写过页面的人',
|
||||
entry: 2,
|
||||
path: [2, 3, 6],
|
||||
note: '跳过装环境;从组件拼接进入。',
|
||||
},
|
||||
{
|
||||
icon: '🧠',
|
||||
name: '重度 AI 依赖',
|
||||
entry: 6,
|
||||
path: [6, 5],
|
||||
note: '从指挥 agent 做整件事切入,回头补认知基础。',
|
||||
},
|
||||
]
|
||||
|
||||
export const ASSIGNMENT: Assignment = {
|
||||
title: '指派学习 · npm',
|
||||
pos: '包管理家族——npm 的坐标与邻居',
|
||||
expect: '能说清 lockfile 锁住了什么',
|
||||
scenes: ['加一个依赖', '换源重装'],
|
||||
miss: {
|
||||
how: '装了但没跑',
|
||||
looks: '命令成功、页面空白——缺「真机验收」这块积木',
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/** 设计系统数据类型——对齐 ai-era-tutorial 词汇表 */
|
||||
|
||||
/** 章程序号(票号=章程序号) */
|
||||
export type ChapterId = 1 | 2 | 3 | 4 | 5 | 6
|
||||
|
||||
/** 卡头三件套之一:靶子 = ▲目标 + ○预期结果 + ●积木清单 */
|
||||
export interface Target {
|
||||
/** ▲ 目标:这一章做成什么(一句话) */
|
||||
goal: string
|
||||
/** ○ 预期结果:做成长什么样、怎么核对(GIF 的 alt) */
|
||||
expectation: string
|
||||
/** ● 积木清单:要用到的零件——只列用到的;实心数 = 已学 */
|
||||
bricks: { name: string; learned: boolean }[]
|
||||
}
|
||||
|
||||
/** 章节数据(MemphisCard / Ticket 共用) */
|
||||
export interface Chapter {
|
||||
id: ChapterId
|
||||
title: string
|
||||
emoji: string
|
||||
color: string // 章节色(只进 16:9 相框与图标瓦)
|
||||
target: Target
|
||||
/** 预期结果 GIF(16:9 无声循环);缺省用静态占位 */
|
||||
gifUrl?: string
|
||||
}
|
||||
|
||||
/** 关系边——线形即含义 */
|
||||
export type EdgeKind = 'seq' | 'rel' | 'family'
|
||||
/** seq 实线=先后(唯一带站点);rel 虚线=相关;family 点线=同族 */
|
||||
|
||||
export interface Edge {
|
||||
from: ChapterId
|
||||
to: ChapterId
|
||||
kind: EdgeKind
|
||||
}
|
||||
|
||||
/** 入口画像(推荐路径,非强制——无锁) */
|
||||
export interface Profile {
|
||||
icon: string
|
||||
name: string
|
||||
entry: ChapterId // ①②③ 推荐起点
|
||||
path: ChapterId[]
|
||||
note: string
|
||||
}
|
||||
|
||||
/** 指派学习块三样:位置给坐标、预期当尺子、场景管巩固 */
|
||||
export interface Assignment {
|
||||
title: string
|
||||
pos: string
|
||||
expect: string
|
||||
scenes: string[]
|
||||
/** 脱靶标记:可能性 + 长相(撞上时认出缺哪块) */
|
||||
miss: { how: string; looks: string }
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/** 页面公共骨架 */
|
||||
import type { ReactNode } from 'react'
|
||||
import '../styles/tokens.css'
|
||||
|
||||
export function Page({ tag, title, desc, children }: { tag: string; title: string; desc: string; children: ReactNode }) {
|
||||
return (
|
||||
<main className="mx-auto max-w-[1200px] px-6 py-10">
|
||||
<header>
|
||||
<span className="inline-block rounded-lg bg-[#efe6fb] px-3 py-1 font-mono text-[15px] font-bold text-[#6d28d9]">{tag}</span>
|
||||
<h1 className="mt-2 mb-1 text-[28px] font-extrabold text-ink">{title}</h1>
|
||||
<p className="m-0 text-[14.5px] text-[#6b6b6b]">{desc}</p>
|
||||
</header>
|
||||
<div className="mt-8">{children}</div>
|
||||
<footer className="mt-12 border-t-2 border-dashed border-ink/15 pt-3 text-[12px] text-[#8a8a8a]">
|
||||
design-lab · 孟菲斯 × 拟物卡 · stack idealjs ·{' '}
|
||||
<a className="underline" href="/index.html">
|
||||
返回目录
|
||||
</a>
|
||||
</footer>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export function Section({ label, note, children }: { label: string; note?: string; children: ReactNode }) {
|
||||
return (
|
||||
<section className="mb-8">
|
||||
<h2 className="mb-2 text-[13.5px] font-bold text-ink">
|
||||
{label}
|
||||
{note && <i className="ml-2 text-[11.5px] font-normal not-italic text-[#6b6b6b]">{note}</i>}
|
||||
</h2>
|
||||
{children}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Page, Section } from '../../lib/ui'
|
||||
|
||||
const PAIRS = [
|
||||
{ a: '#FF6B6B', b: '#4ECDC4', name: '对 1 · coral × teal', use: '主用对:课程相框与主图形' },
|
||||
{ a: '#FFD93D', b: '#A78BFA', name: '对 2 · mustard × violet', use: '辅助对:指派与脱靶等警示类' },
|
||||
{ a: '#FF8FAB', b: '#5B8DEF', name: '对 3 · pink × blue', use: '点缀对:代理区与散落形状' },
|
||||
]
|
||||
const NEUTRALS = [
|
||||
{ c: '#111111', n: 'ink', h: '#111111 · 描边/字/硬投影' },
|
||||
{ c: '#FFF9F0', n: 'paper', h: '#FFF9F0 · 卡面/大底' },
|
||||
{ c: '#FFFFFF', n: 'stack-1', h: '#FFFFFF · 纸叠亮边' },
|
||||
{ c: '#EADFC4', n: 'stack-2', h: '#EADFC4 · 纸叠暗层' },
|
||||
]
|
||||
const CHAPTERS6 = [
|
||||
{ c: '#FF6B6B', n: 'ch1 快速开始' }, { c: '#FF8FAB', n: 'ch2 React' }, { c: '#FFD93D', n: 'ch3 更多示例' },
|
||||
{ c: '#4ECDC4', n: 'ch4 更多' }, { c: '#5B8DEF', n: 'ch5 全栈基础' }, { c: '#A78BFA', n: 'ch6 高级教程' },
|
||||
]
|
||||
|
||||
function Chip({ c, n, h }: { c: string; n: string; h: string }) {
|
||||
return (
|
||||
<div>
|
||||
<div className="h-12 rounded-[10px] border-2 border-ink" style={{ background: c }} />
|
||||
<div className="mt-1 whitespace-nowrap text-[10.5px] font-bold">{n}</div>
|
||||
<div className="whitespace-nowrap font-mono text-[9px] text-[#6b6b6b]">{h}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DoDont({ bad, children, text }: { bad?: boolean; children: React.ReactNode; text: string }) {
|
||||
return (
|
||||
<div className="flex flex-1 items-center gap-3">
|
||||
<span className={`flex h-5 w-5 shrink-0 items-center justify-center rounded-full border-2 border-ink text-[11px] font-extrabold ${bad ? 'bg-clash1a text-paper' : 'bg-clash1b'}`}>
|
||||
{bad ? '✗' : '✓'}
|
||||
</span>
|
||||
{children}
|
||||
<p className="m-0 flex-1 text-[11.5px] leading-snug text-body">{text}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<Page tag="<Colors />" title="颜色 · 鲜艳与碰撞" desc="三组撞色对轮转,黑描边收束一切鲜艳,大底永远是纸色。章节色只进相框,状态不用颜色——用形状。">
|
||||
<Section label="撞色对" note="相邻的鲜艳必须来自不同对">
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
{PAIRS.map((p) => (
|
||||
<div key={p.name}>
|
||||
<div className="flex overflow-hidden rounded-xl border-2 border-ink">
|
||||
<div className="h-11 w-[88px]" style={{ background: p.a }} />
|
||||
<div className="h-11 w-[88px]" style={{ background: p.b }} />
|
||||
</div>
|
||||
<div className="mt-1.5 text-[11px] font-bold">{p.name}</div>
|
||||
<div className="text-[10px] text-[#555]">{p.use}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
<Section label="中性阶" note="纸的世界——字、边、投影、底">
|
||||
<div className="grid grid-cols-4 gap-3">{NEUTRALS.map((x) => <Chip key={x.n} {...x} />)}</div>
|
||||
</Section>
|
||||
<Section label="章节色" note="六章六色,只进 16:9 相框与图标瓦">
|
||||
<div className="grid grid-cols-6 gap-3">{CHAPTERS6.map((x) => <Chip key={x.n} {...x} h={x.c} />)}</div>
|
||||
</Section>
|
||||
<Section label="规则与反例" note="✗ 禁止 vs ✓ 正确">
|
||||
<div className="space-y-4">
|
||||
<DoDont bad text="同色相邻失去碰撞">
|
||||
<div className="flex overflow-hidden rounded-lg border-2 border-ink"><div className="h-10 w-20 bg-clash1a" /><div className="h-10 w-20 bg-clash1a" /></div>
|
||||
</DoDont>
|
||||
<DoDont text="相邻必撞色(跨对取色)">
|
||||
<div className="flex overflow-hidden rounded-lg border-2 border-ink"><div className="h-10 w-20 bg-clash1a" /><div className="h-10 w-20 bg-clash1b" /></div>
|
||||
</DoDont>
|
||||
<DoDont bad text="无描边的鲜艳漂浮刺眼">
|
||||
<div className="h-10 w-40 rounded-lg bg-clash2a" />
|
||||
</DoDont>
|
||||
<DoDont text="一切鲜艳以 2px ink 描边为界">
|
||||
<div className="h-10 w-40 rounded-lg border-2 border-ink bg-clash2a shadow-[4px_4px_0_var(--color-ink)]" />
|
||||
</DoDont>
|
||||
<DoDont bad text="大底鲜艳,字被淹没">
|
||||
<div className="flex h-10 w-40 items-center justify-center rounded-lg border-2 border-ink bg-clash1b text-[11px] font-bold">大底鲜艳</div>
|
||||
</DoDont>
|
||||
<DoDont text="大底纸色,鲜艳只进局部">
|
||||
<div className="flex h-10 w-40 items-center justify-center rounded-lg border-2 border-ink bg-paper text-[11px] font-bold">纸底安放正文</div>
|
||||
</DoDont>
|
||||
</div>
|
||||
</Section>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import '../../styles/tokens.css'
|
||||
import { App } from './App'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(<App />)
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Page, Section } from '../../lib/ui'
|
||||
import { ASSIGNMENT, CHAPTERS } from '../../lib/chapters'
|
||||
import { MemphisCard } from '../../components/MemphisCard'
|
||||
import { RouteLine } from '../../components/RouteLine'
|
||||
import { AssignBlock, Ticket } from '../../components/Ticket'
|
||||
import { EntryGuide } from '../../components/EntryGuide'
|
||||
|
||||
const nodes = {
|
||||
1: { id: 1, borderLeft: 40, right: 299, y: 180 },
|
||||
2: { id: 2, borderLeft: 378, right: 639, y: 108 },
|
||||
5: { id: 5, borderLeft: 699, right: 848, y: 108 },
|
||||
} as const
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<Page tag="<Components />" title="组件 · 入口载体与关系" desc="CourseCard(入口)· RouteLine(关系)· 票券(状态)· 指派块+脱靶标记(任务与警示)· 入口画像。">
|
||||
<Section label="CourseCard" note="卡头三件套=靶子;hover 拿起播放 GIF,8000ms 翻转">
|
||||
<div className="flex flex-wrap items-start gap-8">
|
||||
<div className="w-[280px]"><MemphisCard chapter={CHAPTERS[1]} entry={1} /></div>
|
||||
<div className="w-[280px]"><MemphisCard chapter={CHAPTERS[2]} entry={2} /></div>
|
||||
<div className="w-[160px]"><MemphisCard chapter={CHAPTERS[5]} compact /></div>
|
||||
</div>
|
||||
</Section>
|
||||
<Section label="RouteLine" note="实=先后 虚=相关 点=同族;站点半嵌(左贴描边、右骑黑影外缘)">
|
||||
<div className="relative h-[240px] rounded-2xl border-2 border-dashed border-[#cfcfcd] bg-[#fbfbfa] p-0">
|
||||
<RouteLine edges={[{ from: 1, to: 2, kind: 'seq' }, { from: 2, to: 5, kind: 'seq' }, { from: 2, to: 5, kind: 'family' }]} nodes={nodes} width={880} height={240} />
|
||||
<div className="absolute left-[40px] top-[72px] w-[250px]"><MemphisCard chapter={CHAPTERS[1]} /></div>
|
||||
<div className="absolute left-[380px] top-0 w-[250px]"><MemphisCard chapter={CHAPTERS[2]} /></div>
|
||||
<div className="absolute left-[700px] top-0 w-[150px]"><MemphisCard chapter={CHAPTERS[5]} compact /></div>
|
||||
</div>
|
||||
</Section>
|
||||
<Section label="票券 & 指派学习块" note="票=完成状态;块=位置/预期/场景+脱靶标记">
|
||||
<div className="flex flex-wrap items-start gap-8">
|
||||
<Ticket chapter={CHAPTERS[1]} done />
|
||||
<Ticket chapter={CHAPTERS[5]} done={false} />
|
||||
<AssignBlock {...ASSIGNMENT} />
|
||||
</div>
|
||||
</Section>
|
||||
<Section label="入口梳理" note="推荐不是强制——无锁">
|
||||
<EntryGuide />
|
||||
</Section>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import '../../styles/tokens.css'
|
||||
import { App } from './App'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(<App />)
|
||||
@@ -0,0 +1,30 @@
|
||||
const PAGES = [
|
||||
{ href: '/colors.html', name: '01 颜色', desc: '撞色对 · 中性阶 · 章节色 · 反例' },
|
||||
{ href: '/symbols.html', name: '02 符号形状', desc: '语义形状 · 词汇表 · 线形 · 票券' },
|
||||
{ href: '/components.html', name: '03 组件', desc: 'CourseCard · RouteLine · 票券 · 指派块 · 入口' },
|
||||
{ href: '/shelf.html', name: '04 样例', desc: '组合场景——实现验收对照图' },
|
||||
]
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<main className="mx-auto max-w-[880px] px-6 py-14">
|
||||
<span className="inline-block rounded-lg bg-[#efe6fb] px-3 py-1 font-mono text-[15px] font-bold text-[#6d28d9]">
|
||||
<DesignLab />
|
||||
</span>
|
||||
<h1 className="mt-2 mb-1 text-[30px] font-extrabold">design-lab · 孟菲斯 × 拟物卡</h1>
|
||||
<p className="mt-0 text-[14.5px] text-[#6b6b6b]">Vite MPA:一页一入口(src/pages/xxx/main.tsx)。设计系统四册的可运行版。</p>
|
||||
<div className="mt-8 grid grid-cols-2 gap-4">
|
||||
{PAGES.map((p) => (
|
||||
<a
|
||||
key={p.href}
|
||||
href={p.href}
|
||||
className="rounded-2xl border-2 border-ink bg-paper p-5 no-underline shadow-[7px_8px_0_var(--color-ink)] transition-transform hover:-translate-y-[3px]"
|
||||
>
|
||||
<b className="text-[17px] text-ink">{p.name}</b>
|
||||
<p className="m-0 mt-1 text-[12.5px] text-[#6b6b6b]">{p.desc}</p>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import '../../styles/tokens.css'
|
||||
import { App } from './App'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(<App />)
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Page } from '../../lib/ui'
|
||||
import { ASSIGNMENT, CHAPTERS, EDGES } from '../../lib/chapters'
|
||||
import { MemphisCard } from '../../components/MemphisCard'
|
||||
import { RouteLine } from '../../components/RouteLine'
|
||||
import { AssignBlock } from '../../components/Ticket'
|
||||
import { Bar, Cross, MissMark, Ring, Tremble, Tri, Wave } from '../../components/shapes'
|
||||
|
||||
/* 场景坐标(复刻已验收组合):卡1(40,110,250) 卡2(380,44,250,rot) 卡3(700,44,140) */
|
||||
const nodes = {
|
||||
1: { id: 1, borderLeft: 40, right: 299, y: 180 },
|
||||
2: { id: 2, borderLeft: 378, right: 639, y: 108 },
|
||||
4: { id: 4, borderLeft: 698, right: 848, y: 108 },
|
||||
} as const
|
||||
|
||||
const SCATTER = [
|
||||
{ el: <Tri size={26} />, x: 470, y: 14 },
|
||||
{ el: <Ring size={22} />, x: 830, y: 150 },
|
||||
{ el: <Cross size={20} />, x: 640, y: 440 },
|
||||
{ el: <Cross size={18} />, x: 828, y: 218 },
|
||||
{ el: <Wave size={44} />, x: 40, y: 316 },
|
||||
{ el: <Bar size={30} />, x: 262, y: 548 },
|
||||
{ el: <Tremble size={20} />, x: 96, y: 496 },
|
||||
]
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<Page tag="<Examples />" title="样例 · 组合场景" desc="全系统同台:拟物卡+三种关系线+指派块与脱靶标记+散落语义形状——实现验收的对照图。">
|
||||
<div className="relative h-[620px] overflow-hidden rounded-2xl bg-paper">
|
||||
<RouteLine edges={EDGES} nodes={nodes} width={1160} height={620} />
|
||||
{SCATTER.map((s, i) => (
|
||||
<div key={i} className="absolute" style={{ left: s.x, top: s.y }}>{s.el}</div>
|
||||
))}
|
||||
<div className="absolute left-[40px] top-[110px] w-[250px]"><MemphisCard chapter={CHAPTERS[1]} entry={1} /></div>
|
||||
<div className="absolute left-[380px] top-[44px] w-[250px] -rotate-1"><MemphisCard chapter={CHAPTERS[2]} entry={2} /></div>
|
||||
<div className="absolute left-[700px] top-[44px] w-[140px]"><MemphisCard chapter={CHAPTERS[5]} compact /></div>
|
||||
<div className="absolute left-[380px] top-[300px]"><AssignBlock {...ASSIGNMENT} /></div>
|
||||
<div className="absolute left-[60px] top-[420px] w-[280px]">
|
||||
<div className="mb-2"><Wave size={40} /></div>
|
||||
<p className="m-0 text-[11px] text-body"><b>代理区</b>:安装交给 agent,只验收产物</p>
|
||||
<div className="mt-3 flex items-center gap-2"><Bar size={30} /><span className="text-[11px] text-body"><b>价值区</b>:过程就地写清</span></div>
|
||||
<div className="mt-6"><MissMark how="装了但没跑" looks={ASSIGNMENT.miss.looks} /></div>
|
||||
</div>
|
||||
<p className="absolute bottom-4 left-[40px] m-0 text-[10.5px] text-mute">
|
||||
形状散落于空白,每个都有含义:▲ 靶子 · ● 积木 · ○ 预期 · ~ 代理区 · ▬ 价值区 · ⌇ 脱靶 · + 指派;线:实=先后 · 虚=相关 · 点=同族
|
||||
</p>
|
||||
</div>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import '../../styles/tokens.css'
|
||||
import { App } from './App'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(<App />)
|
||||
@@ -0,0 +1,102 @@
|
||||
import { Page, Section } from '../../lib/ui'
|
||||
import { Bar, BrickDot, Cross, Ring, Tremble, Tri, Wave } from '../../components/shapes'
|
||||
import { CHAPTERS } from '../../lib/chapters'
|
||||
import { Ticket } from '../../components/Ticket'
|
||||
|
||||
const SHAPES = [
|
||||
{ el: <Tri size={28} />, n: '▲ 靶子', m: '目标,卡头打头', s: '靶子先行' },
|
||||
{ el: <span className="flex"><BrickDot on /><BrickDot /></span>, n: '● 积木', m: '清单点列,实=已学', s: '积木清单' },
|
||||
{ el: <Ring size={28} />, n: '○ 预期', m: '可核对的结果', s: '预期结果' },
|
||||
{ el: <Wave size={36} />, n: '~ 代理区', m: '交给 agent', s: '每步先分类' },
|
||||
{ el: <Bar size={30} />, n: '▬ 价值区', m: '就地写过程', s: '每步先分类' },
|
||||
{ el: <Tremble size={22} />, n: '⌇ 脱靶', m: '图形入口:追随图形进入阅读', s: '脱靶提醒' },
|
||||
{ el: <Cross size={22} />, n: '+ 指派', m: '位置/预期/场景', s: '指派学习' },
|
||||
]
|
||||
|
||||
const VOCAB: { p: string; rows: [string, string][] }[] = [
|
||||
{ p: '靶子', rows: [['▲ 目标', '做什么'], ['○ 预期结果', '做成长什么样、怎么核对'], ['● 积木清单', '要用到的零件——只列用到的;说零件不说知识']] },
|
||||
{ p: '步骤分类', rows: [['~ 代理区', '无学习价值——交给 agent'], ['▬ 价值区', '有学习价值——就地写清']] },
|
||||
{ p: '指派学习', rows: [['+ 三样(图形入口)', '教材只留三样,讲解交给 agent'], ['└ 位置', '知识体系中的坐标'], ['└ 预期', '实践结果应长什么样——当尺子'], ['└ 场景', '带回 agent 再做的练习——管巩固']] },
|
||||
{ p: '脱靶提醒', rows: [['⌇ 图形标记', '不人为挖坑;跟在指派后'], ['└ 可能性 / 长相', '怎么脱靶+脱靶什么样——撞上认出缺哪块']] },
|
||||
{ p: '认知基础', rows: [['底座纹·真机验收章', 'env/库/运行环境——脚下的地,不是积木']] },
|
||||
]
|
||||
|
||||
const LINES = [
|
||||
{ d: 'M 8 34 H 60 A 8 8 0 0 0 68 26 V 18 A 8 8 0 0 1 76 10 H 180', w: 2.5, dash: '', cap: 'butt' as const, st: true, n: '实线 = 先后', m: '推荐顺序 · 主线,唯一带站点' },
|
||||
{ d: 'M 8 23 H 180', w: 2, dash: '4 2', cap: 'butt' as const, st: false, n: '虚线 = 相关', m: '内容互参 · 无顺序,无站点' },
|
||||
{ d: 'M 8 23 H 180', w: 2.4, dash: '0.1 6', cap: 'round' as const, st: false, n: '点线 = 同族', m: '同一积木家族(教一块带一族)' },
|
||||
]
|
||||
|
||||
export function App() {
|
||||
return (
|
||||
<Page tag="<Symbols />" title="符号、形状 · 装饰即语义" desc="形状对内容下定义、散落于空白;线条形式承载关系含义;票券只表达完成状态。">
|
||||
<Section label="语义形状 ×7" note="散落只在空白,密度 ≤ 8/屏">
|
||||
<div className="grid grid-cols-4 gap-3.5 md:grid-cols-7">
|
||||
{SHAPES.map((s) => (
|
||||
<div key={s.n} className="text-center">
|
||||
<div className="mx-auto flex h-[46px] w-[46px] items-center justify-center rounded-xl border-2 border-ink bg-white">{s.el}</div>
|
||||
<div className="mt-1.5 text-[11px] font-bold">{s.n}</div>
|
||||
<div className="text-[9.5px] leading-tight text-[#6b6b6b]">{s.m}</div>
|
||||
<div className="mt-0.5 text-[9px] text-[#8a8a8a]">{s.s}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
<Section label="词汇表 · 父子层级" note="与 ai-era-tutorial 词汇表对齐">
|
||||
<table className="w-full border-collapse text-[11.5px] text-body">
|
||||
<thead>
|
||||
<tr className="border-b-2 border-[#d5d5d3] text-left text-ink">
|
||||
<th className="w-24 py-1 pr-2">父词</th>
|
||||
<th className="w-44 py-1 pr-2">图形 · 子词</th>
|
||||
<th className="py-1 pr-2">释义(skill 口径)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{VOCAB.map((g) =>
|
||||
g.rows.map(([sub, def], i) => (
|
||||
<tr key={g.p + i} className="border-b border-[#ececea]">
|
||||
{i === 0 && <td rowSpan={g.rows.length} className="py-1 pr-2 align-top font-extrabold text-ink">{g.p}</td>}
|
||||
<td className="py-1 pr-2">{sub}</td>
|
||||
<td className="py-1 pr-2">{def}</td>
|
||||
</tr>
|
||||
)),
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</Section>
|
||||
<Section label="线条形式 ×4" note="形式即含义;横平竖直+R8 全形式通用">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{LINES.map((l) => (
|
||||
<div key={l.n} className="rounded-xl border-2 border-ink bg-white p-3">
|
||||
<svg width="188" height="44" viewBox="0 0 188 44">
|
||||
<path d={l.d} stroke="var(--color-ink)" strokeWidth={l.w} fill="none" strokeDasharray={l.dash} strokeLinecap={l.cap || 'butt'} />
|
||||
{l.st && <>
|
||||
<circle cx="8" cy="34" r="5.5" fill="var(--color-clash2a)" stroke="var(--color-ink)" strokeWidth="2" />
|
||||
<circle cx="181" cy="10" r="5.5" fill="var(--color-clash1b)" stroke="var(--color-ink)" strokeWidth="2" />
|
||||
</>}
|
||||
</svg>
|
||||
<div className="mt-1 text-[13px] font-extrabold">{l.n}</div>
|
||||
<div className="text-[11px] text-[#555]">{l.m}</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="rounded-xl border-2 border-ink bg-white p-3">
|
||||
<svg width="188" height="44" viewBox="0 0 188 44">
|
||||
<path d="M12 22 q10 -22 20 0 t20 0 t20 0 t20 0 t20 0 t20 0 t20 0 t20 0 t20 0" stroke="var(--color-clash3b)" strokeWidth="3" fill="none" strokeLinecap="round" />
|
||||
</svg>
|
||||
<div className="mt-1 text-[13px] font-extrabold">波浪带 = 代理交接</div>
|
||||
<div className="text-[11px] text-[#555]">agent 边界 · 区域标记,不连线</div>
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
<Section label="票券" note="状态的载体——完成即打孔(裁切式实现)">
|
||||
<div className="flex flex-wrap items-start gap-6">
|
||||
<Ticket chapter={CHAPTERS[1]} done />
|
||||
<Ticket chapter={CHAPTERS[5]} done={false} />
|
||||
<p className="m-0 max-w-[280px] flex-1 text-[11.5px] leading-relaxed text-body">
|
||||
<b>完成=沿撕线打双孔</b>(半圆缺口);未开始撕线为实线、无孔。头部色块无独立描边,由票体 overflow-hidden 裁出单一描边圆角。票号=章程序号。
|
||||
</p>
|
||||
</div>
|
||||
</Section>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import '../../styles/tokens.css'
|
||||
import { App } from './App'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(<App />)
|
||||
@@ -0,0 +1,33 @@
|
||||
@import 'tailwindcss';
|
||||
|
||||
/* ===== 设计系统 tokens(孟菲斯 × 拟物卡)===== */
|
||||
@theme {
|
||||
/* 撞色对(鲜艳的全部来源,相邻必撞) */
|
||||
--color-clash1a: #ff6b6b; /* coral · ch1 */
|
||||
--color-clash1b: #4ecdc4; /* teal · ch4 */
|
||||
--color-clash2a: #ffd93d; /* mustard · ch3 */
|
||||
--color-clash2b: #a78bfa; /* violet · ch6 */
|
||||
--color-clash3a: #ff8fab; /* pink · ch2 */
|
||||
--color-clash3b: #5b8def; /* blue · ch5 */
|
||||
|
||||
/* 中性阶(纸的世界) */
|
||||
--color-ink: #111111;
|
||||
--color-paper: #fff9f0;
|
||||
--color-stack1: #ffffff;
|
||||
--color-stack2: #eadfc4;
|
||||
--color-mute: #8a7a5c;
|
||||
--color-body: #3d3d3d;
|
||||
|
||||
/* 章节色别名 */
|
||||
--color-ch1: #ff6b6b;
|
||||
--color-ch2: #ff8fab;
|
||||
--color-ch3: #ffd93d;
|
||||
--color-ch4: #4ecdc4;
|
||||
--color-ch5: #5b8def;
|
||||
--color-ch6: #a78bfa;
|
||||
}
|
||||
|
||||
/* 语义形状通用 */
|
||||
.sem-shadow {
|
||||
filter: drop-shadow(0 1px 0 rgba(255, 255, 255, 0.9));
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>design-lab · symbols</title>
|
||||
</head>
|
||||
<body class="bg-paper text-ink">
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/pages/symbols/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src", "vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"root":["./src/components/EntryGuide.tsx","./src/components/MemphisCard.tsx","./src/components/RouteLine.tsx","./src/components/Ticket.tsx","./src/components/shapes.tsx","./src/lib/chapters.ts","./src/lib/types.ts","./src/lib/ui.tsx","./src/pages/colors/App.tsx","./src/pages/colors/main.tsx","./src/pages/components/App.tsx","./src/pages/components/main.tsx","./src/pages/index/App.tsx","./src/pages/index/main.tsx","./src/pages/shelf/App.tsx","./src/pages/shelf/main.tsx","./src/pages/symbols/App.tsx","./src/pages/symbols/main.tsx","./vite.config.ts"],"version":"5.6.3"}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { readdirSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
// MPA:根目录每个 *.html 是一个入口,对应 src/pages/<name>/main.tsx
|
||||
const pages = readdirSync(__dirname).filter((f) => f.endsWith('.html'))
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
build: {
|
||||
rollupOptions: {
|
||||
input: Object.fromEntries(pages.map((f) => [f.replace(/\.html$/, ''), resolve(__dirname, f)])),
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in new issue
Block a user