diff --git a/apps/tutorial/design-colors.html b/apps/tutorial/design-colors.html new file mode 100644 index 0000000..72b7986 --- /dev/null +++ b/apps/tutorial/design-colors.html @@ -0,0 +1,12 @@ + + + + + + 设计系统 · 颜色 · 鲜艳与碰撞 + + +
+ + + diff --git a/apps/tutorial/design-components.html b/apps/tutorial/design-components.html new file mode 100644 index 0000000..557c97c --- /dev/null +++ b/apps/tutorial/design-components.html @@ -0,0 +1,12 @@ + + + + + + 设计系统 · 组件 · 入口载体与状态 + + +
+ + + diff --git a/apps/tutorial/design-examples.html b/apps/tutorial/design-examples.html new file mode 100644 index 0000000..7f67edf --- /dev/null +++ b/apps/tutorial/design-examples.html @@ -0,0 +1,12 @@ + + + + + + 设计系统 · 样例 · 组合场景 + + +
+ + + diff --git a/apps/tutorial/design-symbols.html b/apps/tutorial/design-symbols.html new file mode 100644 index 0000000..5bad762 --- /dev/null +++ b/apps/tutorial/design-symbols.html @@ -0,0 +1,12 @@ + + + + + + 设计系统 · 符号、形状 · 装饰即语义 + + +
+ + + diff --git a/apps/tutorial/src/components/Scatter.tsx b/apps/tutorial/src/components/Scatter.tsx new file mode 100644 index 0000000..bde9a9e --- /dev/null +++ b/apps/tutorial/src/components/Scatter.tsx @@ -0,0 +1,90 @@ +/** 散落形状摆放——「卡片附近空当 + 随机」: + * 网格采样容器空白 → 过滤出距内容盒([data-comp])≤ near 的空当(不胡乱撒)→ + * 种子随机挑位(彼此保距)+空当内随机偏移+随机旋转。 + * 种子固定 → 每次渲染一致(视觉基线可测);容器重排(ResizeObserver)自动重算。 */ +import { useLayoutEffect, useRef, useState, type ReactNode } from 'react' + +function mulberry32(seed: number) { + let t = seed >>> 0 + return () => { + t = (t + 0x6d2b79f5) >>> 0 + let x = Math.imul(t ^ (t >>> 15), 1 | t) + x = (x + Math.imul(x ^ (x >>> 7), 61 | x)) ^ x + return ((x ^ (x >>> 14)) >>> 0) / 4294967296 + } +} + +type Place = { x: number; y: number; rot: number } + +export function Scatter({ + shapes, + seed = 7, + near = 150, // 空当须距某内容盒 ≤ near——只散在卡片附近 + margin = 34, // 与内容盒保持的最小间隙(≥ 最大形状半对角) + step = 30, // 采样网格密度 + minGap = 80, // 形状彼此最小间距 +}: { + shapes: ReactNode[] + seed?: number + near?: number + margin?: number + step?: number + minGap?: number +}) { + const layerRef = useRef(null) + const [places, setPlaces] = useState([]) + + useLayoutEffect(() => { + const layer = layerRef.current + const host = layer?.parentElement + if (!layer || !host) return + + const place = () => { + const hb = host.getBoundingClientRect() + const rects = [...host.querySelectorAll('[data-comp]')].map((el) => el.getBoundingClientRect()) + if (!rects.length || !hb.width) return + const dist = (x: number, y: number, r: DOMRect) => + Math.hypot(Math.max(r.left - x, 0, x - r.right), Math.max(r.top - y, 0, y - r.bottom)) + const cand: { x: number; y: number }[] = [] + for (let x = 36; x < hb.width - 36; x += step) + for (let y = 36; y < hb.height - 36; y += step) { + const px = hb.left + x + const py = hb.top + y + if (rects.every((r) => dist(px, py, r) >= margin) && rects.some((r) => dist(px, py, r) <= near)) + cand.push({ x, y }) + } + const rnd = mulberry32(seed) + for (let i = cand.length - 1; i > 0; i--) { + const j = Math.floor(rnd() * (i + 1)) + ;[cand[i], cand[j]] = [cand[j], cand[i]] + } + const taken: Place[] = [] + for (const c of cand) { + if (taken.length >= shapes.length) break + if (taken.some((t) => Math.hypot(t.x - c.x, t.y - c.y) < minGap)) continue + taken.push({ x: c.x + (rnd() - 0.5) * 14, y: c.y + (rnd() - 0.5) * 14, rot: Math.round((rnd() - 0.5) * 52) }) + } + setPlaces(taken) + } + + place() + const ro = new ResizeObserver(place) + ro.observe(host) + return () => ro.disconnect() + }, [seed, near, margin, step, minGap, shapes.length]) + + return ( +
+ {places.map((p, i) => ( +
+ {shapes[i]} +
+ ))} +
+ ) +} diff --git a/apps/tutorial/src/lib/chapters.ts b/apps/tutorial/src/lib/chapters.ts index f928768..217bb08 100644 --- a/apps/tutorial/src/lib/chapters.ts +++ b/apps/tutorial/src/lib/chapters.ts @@ -430,3 +430,15 @@ export const CONTENT: Record = { }, }, } + +/** 指派学习块示例数据(设计系统·组件/样例页演示用) */ +export const ASSIGNMENT: import('./types').Assignment = { + title: '指派学习 · HTTP 协议', + pos: 'Web 通信的地基,旁边是 DNS(找机器)与 TCP(铺线路)', + expect: '能向 agent 讲清一次请求的往返:URL → DNS → 连接 → 请求行/头/体 → 响应 → 渲染', + scenes: ['看 Network 面板复述一次页面加载', '用 curl 拿一个网页的原始响应'], + miss: { + how: '装了但没跑', + looks: '命令成功、页面空白——缺「真机验收」这块积木', + }, +} diff --git a/apps/tutorial/src/lib/ui.tsx b/apps/tutorial/src/lib/ui.tsx index a6a5b22..035a2fb 100644 --- a/apps/tutorial/src/lib/ui.tsx +++ b/apps/tutorial/src/lib/ui.tsx @@ -1,10 +1,20 @@ /** 教程站页面骨架:站头导航跨页常驻(view-transition-name 共享元素,页间不闪烁)。 - * 选中项背景色=该章色(与章头 title 横幅统一);书架选中为 ink。 */ + * 选中项背景色=该章色(与章头 title 横幅统一);书架/设计册选中为 ink。 */ import type { ReactNode } from 'react' import type { ChapterId } from './types' import { CHAPTERS } from './chapters' import '../styles/tokens.css' +/** 设计系统四册(自 design-lab 迁入) */ +export type DesignKey = 'design-colors' | 'design-symbols' | 'design-components' | 'design-examples' + +const DESIGN: { href: string; label: string; key: DesignKey }[] = [ + { href: '/design-colors.html', label: '颜色', key: 'design-colors' }, + { href: '/design-symbols.html', label: '符号形状', key: 'design-symbols' }, + { href: '/design-components.html', label: '组件', key: 'design-components' }, + { href: '/design-examples.html', label: '样例', key: 'design-examples' }, +] + const NAV: { href: string; label: string; id?: ChapterId }[] = [ { href: '/index.html', label: '书架' }, ...([1, 2, 3, 4, 5, 6] as ChapterId[]).map((id) => ({ @@ -23,8 +33,8 @@ export function Page({ /** 章节页标题在章头横幅里,可省 */ title?: string desc?: string - /** 站头高亮:章节 id 或书架 'index' */ - active?: ChapterId | 'index' + /** 站头高亮:章节 id / 书架 / 设计系统册 */ + active?: ChapterId | 'index' | DesignKey children: ReactNode }) { return ( @@ -51,6 +61,21 @@ export function Page({ ) })} + + {DESIGN.map((n) => { + const on = n.key === active + return ( + + 设计·{n.label} + + ) + })} {(title || desc) && (
diff --git a/apps/tutorial/src/pages/design-colors/App.tsx b/apps/tutorial/src/pages/design-colors/App.tsx new file mode 100644 index 0000000..2a363a2 --- /dev/null +++ b/apps/tutorial/src/pages/design-colors/App.tsx @@ -0,0 +1,109 @@ +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 · 卡面/大底' }, +] +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 ( +
+
+
{n}
+
{h}
+
+ ) +} + +/* 三条规则的「为什么」:鲜艳如何成立、如何被收束、注意力如何分配 */ +const RULES = [ + { + name: '相邻必撞色(跨对取色)', + why: '孟菲斯的鲜艳靠碰撞成立:两个高饱和色块相邻时,只有色相拉开距离,视觉才读得出「两个声音」。同色相邻只是一个声音变大——画面发平发闷,鲜艳互相抵消;跨对取色(coral 挨 teal、mustard 挨 violet)反而把彼此衬得更亮。拿到相邻色块时问一句「它俩是不是同一对」,是,就换掉一个。', + badCap: '同色相邻失去碰撞', + goodCap: '相邻必撞色(跨对取色)', + badDemo:
, + goodDemo:
, + }, + { + name: '黑描边收束鲜艳', + why: '高饱和色放在浅纸上会「振动」:边缘发虚、像悬浮在页面上。2px ink 描边给每块鲜艳一道明确的边界,把颜色关进形状里——这也是拟物卡世界的基础:卡片、形状、站点共用同一粗细的墨线,颜色再艳也是「零件」而不是「光」,缩放与低分屏上轮廓依然清晰。去掉描边,鲜艳就是没裁过的布料。', + badCap: '无描边的鲜艳漂浮刺眼', + goodCap: '一切鲜艳以 2px ink 描边为界', + badDemo:
, + goodDemo:
, + }, + { + name: '大底纸色,鲜艳只进局部', + why: '鲜艳是注意力,铺满屏幕等于没有注意力。大底永远是 paper,鲜艳只进相框、站点、散落形状这些局部(≤10%/屏),视线才有落点、正文才永远可读;大底一旦鲜艳,字被淹没,卡片也失去「纸上的物」的载体感。记住层级:大底是世界,卡片是物,鲜艳是物上的标记。', + badCap: '大底鲜艳,字被淹没', + goodCap: '大底纸色,鲜艳只进局部', + badDemo:
大底鲜艳
, + goodDemo:
纸底安放正文
, + }, +] + +function DoDont({ bad, children, text }: { bad?: boolean; children: React.ReactNode; text: string }) { + return ( +
+ + {bad ? '✗' : '✓'} + + {children} +

{text}

+
+ ) +} + +export function App() { + return ( + +
+
+ {PAIRS.map((p) => ( +
+
+
+
+
+
{p.name}
+
{p.use}
+
+ ))} +
+
+
+
{NEUTRALS.map((x) => )}
+
+
+
{CHAPTERS6.map((x) => )}
+
+
+
+ {RULES.map((r, i) => ( +
+
+ {i + 1} + {r.name} +
+

{r.why}

+
+ {r.badDemo} + {r.goodDemo} +
+
+ ))} +
+
+
+ ) +} diff --git a/apps/tutorial/src/pages/design-colors/main.tsx b/apps/tutorial/src/pages/design-colors/main.tsx new file mode 100644 index 0000000..a3fd25e --- /dev/null +++ b/apps/tutorial/src/pages/design-colors/main.tsx @@ -0,0 +1,4 @@ +import { createRoot } from 'react-dom/client' +import { App } from './App' + +createRoot(document.getElementById('root')!).render() diff --git a/apps/tutorial/src/pages/design-components/App.tsx b/apps/tutorial/src/pages/design-components/App.tsx new file mode 100644 index 0000000..fc0cfbe --- /dev/null +++ b/apps/tutorial/src/pages/design-components/App.tsx @@ -0,0 +1,36 @@ +import { Page, Section } from '../../lib/ui' +import { ASSIGNMENT, CHAPTERS } from '../../lib/chapters' +import { MemphisCard } from '../../components/MemphisCard' +import { AssignBlock, Ticket } from '../../components/Ticket' +import { EntryGuide } from '../../components/EntryGuide' +import { Mark } from '../../components/shapes' + +export function App() { + return ( + +
+
+
+
+
+
+ 这一步过程就地写清:环境与依赖如何咬合} + /> +
+
+
+
+
+ + + +
+
+
+ +
+
+ ) +} diff --git a/apps/tutorial/src/pages/design-components/main.tsx b/apps/tutorial/src/pages/design-components/main.tsx new file mode 100644 index 0000000..a3fd25e --- /dev/null +++ b/apps/tutorial/src/pages/design-components/main.tsx @@ -0,0 +1,4 @@ +import { createRoot } from 'react-dom/client' +import { App } from './App' + +createRoot(document.getElementById('root')!).render() diff --git a/apps/tutorial/src/pages/design-examples/App.tsx b/apps/tutorial/src/pages/design-examples/App.tsx new file mode 100644 index 0000000..5c04bea --- /dev/null +++ b/apps/tutorial/src/pages/design-examples/App.tsx @@ -0,0 +1,46 @@ +/** 组合场景=大部分组件拿出来的真实使用,不做解释性标注: + * 入口画像 → 课程卡与关系线 → 进度票券 → 指派块;形状散落于卡片附近空当。 */ +import { Page } from '../../lib/ui' +import { ASSIGNMENT, CHAPTERS, EDGES } from '../../lib/chapters' +import { MemphisCard } from '../../components/MemphisCard' +import { RouteLine } from '../../components/RouteLine' +import { AssignBlock, Ticket } from '../../components/Ticket' +import { EntryGuide } from '../../components/EntryGuide' +import { Scatter } from '../../components/Scatter' +import { Cross, 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: 290, y: 180 }, + 2: { id: 2, borderLeft: 380, right: 630, y: 108 }, + 5: { id: 5, borderLeft: 700, right: 840, y: 108 }, +} as const + +export function App() { + return ( + + +
+ , + , + , + , + , + , + ]} + /> + +
+
+
+
+
+ + +
+
+
+ ) +} diff --git a/apps/tutorial/src/pages/design-examples/main.tsx b/apps/tutorial/src/pages/design-examples/main.tsx new file mode 100644 index 0000000..a3fd25e --- /dev/null +++ b/apps/tutorial/src/pages/design-examples/main.tsx @@ -0,0 +1,4 @@ +import { createRoot } from 'react-dom/client' +import { App } from './App' + +createRoot(document.getElementById('root')!).render() diff --git a/apps/tutorial/src/pages/design-symbols/App.tsx b/apps/tutorial/src/pages/design-symbols/App.tsx new file mode 100644 index 0000000..5d642b6 --- /dev/null +++ b/apps/tutorial/src/pages/design-symbols/App.tsx @@ -0,0 +1,103 @@ +import { Page, Section } from '../../lib/ui' +import { BrickDot, Cross, Mark, Ring, Tremble, Tri, Wave } from '../../components/shapes' +import { CHAPTERS } from '../../lib/chapters' +import { Ticket } from '../../components/Ticket' +import type { ReactNode } from 'react' + +const SHAPES = [ + { el: , n: '▲ 靶子', m: '目标,卡头打头', s: '靶子先行' }, + { el: , n: '● 积木', m: '清单点列,实=已学', s: '积木清单' }, + { el: , n: '○ 预期', m: '可核对的结果', s: '预期结果' }, + { el: , n: '~ 代理区', m: '交给 agent', s: '每步先分类' }, + { el: 过程, n: '荧光标记 价值区', m: '划在文字上,不是散落图形', s: '每步先分类' }, + { el: , n: '⌇ 脱靶', m: '图形入口:追随图形进入阅读', s: '脱靶提醒' }, + { el: , n: '+ 指派', m: '位置/预期/场景', s: '指派学习' }, +] + +const VOCAB: { p: string; rows: [ReactNode, 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 ( + +
+
+ {SHAPES.map((s) => ( +
+
{s.el}
+
{s.n}
+
{s.m}
+
{s.s}
+
+ ))} +
+
+
+ + + + + + + + + + {VOCAB.map((g) => + g.rows.map(([sub, def], i) => ( + + {i === 0 && } + + + + )), + )} + +
父词图形 · 子词释义(skill 口径)
{g.p}{sub}{def}
+
+
+
+ {LINES.map((l) => ( +
+ + + {l.st && <> + + + } + +
{l.n}
+
{l.m}
+
+ ))} +
+ + + +
波浪带 = 代理交接
+
agent 边界 · 区域标记,不连线
+
+
+
+
+
+ + +

+ 完成=沿撕线打双孔(半圆缺口);未开始撕线为实线、无孔。头部色块无独立描边,由票体 overflow-hidden 裁出单一描边圆角。票号=章程序号。 +

+
+
+
+ ) +} diff --git a/apps/tutorial/src/pages/design-symbols/main.tsx b/apps/tutorial/src/pages/design-symbols/main.tsx new file mode 100644 index 0000000..a3fd25e --- /dev/null +++ b/apps/tutorial/src/pages/design-symbols/main.tsx @@ -0,0 +1,4 @@ +import { createRoot } from 'react-dom/client' +import { App } from './App' + +createRoot(document.getElementById('root')!).render()