feat: 全栈教程站(AI 时代版)——OUTLINE 大纲(skill 配方:靶子先行/步骤分类/积木三句话/指派三样/脱靶/认知基础)+ Vite MPA 七页(书架+六章),跨文档 View Transitions 防闪白(书架相框↔章头共享元素 morph),组件复用 design-lab 孟菲斯×拟物卡
This commit is contained in:
1 parent
9aebbbbde3
commit
690ec5eaf5
101 files changed
+118478
-1
No files matched your search
@@ -0,0 +1,139 @@
|
||||
/** 章节页——一章的完整教学结构(对齐 ai-era-tutorial):
|
||||
* 章头靶子卡(▲目标 ○预期 ●积木,view-transition-name 与书架相框同名 morph)
|
||||
* → 步骤列(价值区荧光标记 / 代理区波浪,每步=目标+验收产物)
|
||||
* → 积木三句话(是什么/与靶子的关系/带一族)
|
||||
* → 认知基础(脚下的地,就地露出)→ 指派学习三样+脱靶提醒 */
|
||||
import type { CSSProperties } from 'react'
|
||||
import { Page, Section } from '../lib/ui'
|
||||
import { CHAPTERS, CONTENT } from '../lib/chapters'
|
||||
import type { ChapterId } from '../lib/types'
|
||||
import { AssignBlock } from './Ticket'
|
||||
import { BrickDots } from './MemphisCard'
|
||||
import { Mark, Ring, Tri, Wave } from './shapes'
|
||||
|
||||
const ALL: ChapterId[] = [1, 2, 3, 4, 5, 6]
|
||||
|
||||
export function ChapterPage({ id }: { id: ChapterId }) {
|
||||
const ch = CHAPTERS[id]
|
||||
const c = CONTENT[id]
|
||||
const prev = ALL[id - 2]
|
||||
const next = ALL[id]
|
||||
|
||||
return (
|
||||
<Page active={id}>
|
||||
{/* 章头:与书架相框共享 view-transition-name —— 点卡片进来时相框飞进来变成章头 */}
|
||||
<div
|
||||
className="flex items-center gap-4 rounded-2xl border-2 border-ink p-5 shadow-hard"
|
||||
style={{ background: ch.color, viewTransitionName: `ch-${id}` } as CSSProperties}
|
||||
>
|
||||
<span className="text-[44px] leading-none">{ch.emoji}</span>
|
||||
<div className="flex-1">
|
||||
<h1 className="m-0 text-[24px] font-extrabold text-ink">
|
||||
第 {id} 章 · {ch.title}
|
||||
</h1>
|
||||
<div className="mt-1.5 flex flex-wrap gap-x-6 gap-y-1 text-[12px] font-semibold text-ink">
|
||||
<span>▲ 目标:{ch.target.goal}</span>
|
||||
<span>○ 预期:{ch.target.expectation}</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
● 积木 <BrickDots bricks={ch.target.bricks} />
|
||||
{ch.target.bricks.length} 块
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-8">
|
||||
<Section label="步骤" note="每步先分类:这一步的过程里有没有学习价值">
|
||||
<ol className="m-0 list-none space-y-3 p-0">
|
||||
{c.steps.map((s, i) => (
|
||||
<li key={s.title} className="rounded-xl border-2 border-ink bg-paper p-4 shadow-hard">
|
||||
<div className="flex flex-wrap items-center gap-2.5">
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded-full border-2 border-ink bg-white text-[12px] font-extrabold">
|
||||
{i + 1}
|
||||
</span>
|
||||
{s.kind === 'value' ? (
|
||||
<Mark className="text-[13.5px]">{s.title}</Mark>
|
||||
) : (
|
||||
<span className="flex items-center gap-1.5 text-[13.5px] font-bold text-ink">
|
||||
<Wave size={30} />
|
||||
{s.title}
|
||||
<span className="rounded-full bg-clash3b/15 px-2 py-0.5 text-[10.5px]">代理区 · 交给 agent</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="m-0 mt-2 text-[12.5px] leading-relaxed text-body">{s.what}</p>
|
||||
<p className="m-0 mt-1.5 text-[12px] text-ink">
|
||||
<b>验收</b>:{s.accept}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</Section>
|
||||
|
||||
<Section label="积木清单" note="从靶子倒推、每块三句话、教一块带一族">
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||
{c.bricks.map((b) => (
|
||||
<div key={b.name} className="rounded-xl border-2 border-ink bg-paper p-3.5 shadow-hard">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tri size={13} />
|
||||
<b className="text-[13px]">{b.name}</b>
|
||||
</div>
|
||||
<p className="m-0 mt-1.5 text-[11.5px] leading-relaxed text-body">
|
||||
<b>是什么</b>:{b.what}
|
||||
<br />
|
||||
<b>为何在这</b>:{b.why}
|
||||
<br />
|
||||
<b>带一族</b>:{b.family}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
{c.base && (
|
||||
<Section label="认知基础" note="脚下的地(env/库/运行环境)——第一次用到就地露出">
|
||||
<div className="flex items-start gap-3 rounded-xl border-2 border-dashed border-ink/40 bg-clash3b/15 p-4">
|
||||
<Ring size={18} className="mt-0.5 shrink-0" />
|
||||
<p className="m-0 text-[12.5px] leading-relaxed text-body">{c.base}</p>
|
||||
</div>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{c.assign && (
|
||||
<Section label="指派学习" note="位置给坐标、预期当尺子、场景管巩固;讲解交给你的 agent">
|
||||
<AssignBlock {...c.assign} />
|
||||
</Section>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
{prev ? (
|
||||
<a
|
||||
href={`/ch${prev}.html`}
|
||||
className="rounded-xl border-2 border-ink bg-paper px-4 py-2 text-[12.5px] font-bold text-ink no-underline shadow-hard"
|
||||
>
|
||||
← 第 {prev} 章 · {CHAPTERS[prev].title}
|
||||
</a>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
<a
|
||||
href="/index.html"
|
||||
className="rounded-xl border-2 border-ink bg-ink px-4 py-2 text-[12.5px] font-bold text-paper no-underline"
|
||||
>
|
||||
返回书架
|
||||
</a>
|
||||
{next ? (
|
||||
<a
|
||||
href={`/ch${next}.html`}
|
||||
className="rounded-xl border-2 border-ink bg-paper px-4 py-2 text-[12.5px] font-bold text-ink no-underline shadow-hard"
|
||||
>
|
||||
第 {next} 章 · {CHAPTERS[next].title} →
|
||||
</a>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/** 入口梳理 —— 读者画像推荐路径:是推荐不是强制(无锁) */
|
||||
import { CHAPTERS, PROFILES } from '../lib/chapters'
|
||||
|
||||
export function EntryGuide() {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-5">
|
||||
{PROFILES.map((p) => (
|
||||
<div key={p.name} data-comp="entry-card" className="w-[252px] rounded-xl border-2 border-ink bg-paper p-3 shadow-hard">
|
||||
<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>}
|
||||
<a
|
||||
href={`/ch${id}.html`}
|
||||
className={`rounded-full border-[1.5px] border-ink px-2.5 py-0.5 text-[10.5px] no-underline ${id === p.entry ? 'bg-clash2a font-extrabold' : 'bg-white'}`}
|
||||
>
|
||||
{id === p.entry && ['①', '②', '③'][PROFILES.indexOf(p)]} {CHAPTERS[id].title}
|
||||
</a>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<p className="m-0 mt-2 text-[10px] leading-relaxed text-mute">{p.note}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/** CourseCard —— 教程的入口载体(孟菲斯骨架 × 拟物纸感)
|
||||
* 卡头三件套=靶子:▲目标 + ○预期结果(16:9 相框) + ●积木点列
|
||||
* 交互:hover/聚焦拿起(浮起+投影加深)并播放 GIF;desc ⇄ GIF 每 8000ms 翻转;
|
||||
* hover/Tab 锁定;触屏与 prefers-reduced-motion 不翻转。 */
|
||||
import { useEffect, useRef, useState, type ReactNode } 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,
|
||||
zoneText,
|
||||
}: {
|
||||
chapter: Chapter
|
||||
entry?: 1 | 2 | 3
|
||||
compact?: boolean
|
||||
/** 静置态相框文字(可含 <Mark> 荧光标记):教程卡片的价值区用法示例 */
|
||||
zoneText?: ReactNode
|
||||
}) {
|
||||
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}.html`}
|
||||
data-comp="course-card"
|
||||
className="group relative block rounded-[14px] border-2 border-ink bg-paper p-[11px] no-underline
|
||||
shadow-hard transition-transform duration-150 focus-visible:outline-none
|
||||
hover:-translate-y-[3px] hover:shadow-hard-lift
|
||||
focus-visible:-translate-y-[3px] focus-visible:shadow-hard-lift"
|
||||
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)',
|
||||
viewTransitionName: `ch-${chapter.id}`,
|
||||
}}
|
||||
>
|
||||
{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>
|
||||
)
|
||||
) : flip ? (
|
||||
<span className="px-3 text-center text-[11px] leading-snug text-ink/80 mix-blend-multiply">
|
||||
{chapter.target.goal}
|
||||
</span>
|
||||
) : zoneText ? (
|
||||
/* 静置态(用法示例):过程文字骑在相框里,价值区短语带荧光标记 */
|
||||
<span className="px-3 text-center text-[11.5px] font-semibold leading-snug text-ink">{zoneText}</span>
|
||||
) : (
|
||||
/* 静置态:章节 emoji 是相框的视觉主体(对齐设计稿解剖卡) */
|
||||
<span style={{ fontSize: compact ? '28px' : '40px' }}>{chapter.emoji}</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)]">
|
||||
{chapter.title}
|
||||
</b>
|
||||
{!compact && <BrickDots bricks={chapter.target.bricks} />}
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/** RouteLine —— 关系线:线形即含义(实=先后 虚=相关 点=同族)
|
||||
* 拓扑:横平竖直 + R8 直角步进;并行不合并(4px);站点 r6 半嵌卡边
|
||||
* (圆心骑左右描边,各半在卡上);线层 z-10 置于卡之上,站点不被卡遮挡;
|
||||
* 同口多线自动聚合胶囊(线止于胶囊边界)。 */
|
||||
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(出口圆心贴这里,点半嵌——不接阴影)
|
||||
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 z-10"
|
||||
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,84 @@
|
||||
/** 票券 —— 只表达完成状态:完成=沿撕线打孔(半圆缺口),未开始=实线无孔。
|
||||
* 外层 wrapper 出阴影(drop-shadow 跟随挖空后的轮廓,box-shadow 会被 mask 裁掉);
|
||||
* 内层票体裁切式:头部色块无独立描边,由票体 overflow-hidden 裁出单一描边圆角。 */
|
||||
import type { Chapter } from '../lib/types'
|
||||
|
||||
export function Ticket({ chapter, done }: { chapter: Chapter; done: boolean }) {
|
||||
return (
|
||||
<div data-comp="ticket" className={`w-fit drop-shadow-hard ${done ? 'tk-pk' : ''}`}>
|
||||
<div
|
||||
className={`relative flex w-[216px] items-stretch overflow-hidden rounded-xl border-2 border-ink bg-paper ${done ? 'ticket-pk' : ''}`}
|
||||
>
|
||||
{/* 头部媒体块:章节色纯填充,无独立边框(由票体裁切) */}
|
||||
<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>
|
||||
</div>
|
||||
{/* 孔缘弧线:mask 只减形不补线,弧线补上挖空边缘的 ink 描边 */}
|
||||
{done && (
|
||||
<>
|
||||
<i className="pk-arc pk-arc-t" aria-hidden />
|
||||
<i className="pk-arc pk-arc-b" aria-hidden />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/** 指派学习块 —— 普通内容块(非票据):+ 位置/预期/场景三样 + 脱靶标记 */
|
||||
export function AssignBlock({
|
||||
title,
|
||||
pos,
|
||||
expect,
|
||||
scenes,
|
||||
miss,
|
||||
}: {
|
||||
title: string
|
||||
pos: string
|
||||
expect: string
|
||||
scenes: string[]
|
||||
miss: { how: string; looks: string }
|
||||
}) {
|
||||
return (
|
||||
<div data-comp="assign" className="w-[260px] rounded-xl border-2 border-ink bg-paper p-3 shadow-hard">
|
||||
<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,82 @@
|
||||
/** 七个语义形状——装饰即语义(取自 ai-era-tutorial 词汇表) */
|
||||
import type { CSSProperties, ReactNode } 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 Mark = ({ children, className }: { children: ReactNode; className?: string }) => (
|
||||
<mark
|
||||
className={`-rotate-1 rounded-[3px] bg-clash2a px-[4px] py-[1px] font-bold text-ink [box-decoration-break:clone] ${className ?? ''}`}
|
||||
>
|
||||
{children}
|
||||
</mark>
|
||||
)
|
||||
|
||||
/** ⌇ 脱靶:竖立颤抖——图形入口,读者追随图形进入阅读 */
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user