refactor: 设计系统完整提取为 @stack/design-system 共享包(组件/tokens/类型唯一定义处,双端引用不再拷贝)
- 组件参数化适配双端:MemphisCard href/morph,EntryGuide 数据注入+linkChapter - RouteLine hover 跟随(站点 translateY(-3)、连线 CSS d 平滑插值、直线变斜线)随包回到 design-lab,shelf 场景接线演示 - tokens.css 显式 @source 包内源码——经 node_modules 解析时自动扫描漏类(text-mute 等静默丢失致 4% 画面偏差,已修复并验证) - E2E 新增 hover 跟随契约(静置 L 水平、hover 变斜线、双站点 -3px、卡片同步),28/28 全绿
This commit is contained in:
1 parent
c943a9e94d
commit
65df0ef1d7
40 files changed
+190
-735
No files matched your search
@@ -154,6 +154,27 @@ test.describe('RouteLine 规格契约', () => {
|
|||||||
expect(z).toBe('10')
|
expect(z).toBe('10')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('hover 卡片:线与站点跟随浮起(-3px 同步,直线变斜线)', async ({ page }) => {
|
||||||
|
const get = () =>
|
||||||
|
page.evaluate(() => {
|
||||||
|
const svg = [...document.querySelectorAll('svg')].find((s) => s.getAttribute('width') === '1160')
|
||||||
|
return {
|
||||||
|
stations: [...svg.querySelectorAll('g[style]')].map((g) => (g as HTMLElement).style.transform),
|
||||||
|
d25: [...svg.querySelectorAll('path')].map((el) => el.getAttribute('d') || '').find((d) => d.startsWith('M 630')),
|
||||||
|
cardTranslate: getComputedStyle(document.querySelector('a[data-comp="course-card"][href="#ch2"]')).translate,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const before = await get()
|
||||||
|
await page.locator('div.absolute.left-\\[380px\\]').hover()
|
||||||
|
await page.waitForTimeout(250)
|
||||||
|
const after = await get()
|
||||||
|
expect(before.d25).toBe('M 630 108 L 700 108') // 静置:几何水平(L 结构可平滑插值)
|
||||||
|
expect(after.d25).toBe('M 630 105 L 700 108') // hover 卡2:源端随站点抬升,直线变斜线
|
||||||
|
expect(before.stations.every((t) => t === 'translateY(0px)')).toBeTruthy()
|
||||||
|
expect(after.stations.filter((t) => t === 'translateY(-3px)').length).toBe(2) // 源+目标站点跟随
|
||||||
|
expect(after.cardTranslate).toBe('0px -3px') // 卡片本体同步浮起
|
||||||
|
})
|
||||||
|
|
||||||
test('实线端点落在水平端口(M x y H 起、H x 止)', async ({ page }) => {
|
test('实线端点落在水平端口(M x y H 起、H x 止)', async ({ page }) => {
|
||||||
const d = await page.locator('svg path[stroke-width="2.5"]').first().getAttribute('d')
|
const d = await page.locator('svg path[stroke-width="2.5"]').first().getAttribute('d')
|
||||||
expect(d).toMatch(/^M [\d.]+ [\d.]+ H/)
|
expect(d).toMatch(/^M [\d.]+ [\d.]+ H/)
|
||||||
|
|||||||
@@ -12,7 +12,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1"
|
"react-dom": "^18.3.1",
|
||||||
|
"@stack/design-system": "workspace:*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "1.63.0",
|
"@playwright/test": "1.63.0",
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
/** 入口梳理 —— 读者画像推荐路径:是推荐不是强制(无锁) */
|
|
||||||
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>}
|
|
||||||
<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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
/** 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}`}
|
|
||||||
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)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
/** 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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Assignment, Chapter, ChapterId, Edge, Profile } from './types'
|
import type { Assignment, Chapter, ChapterId, Edge, Profile } from '@stack/design-system'
|
||||||
|
|
||||||
export const CHAPTERS: Record<ChapterId, Chapter> = {
|
export const CHAPTERS: Record<ChapterId, Chapter> = {
|
||||||
1: {
|
1: {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
/** 页面公共骨架 */
|
/** 页面公共骨架 */
|
||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import '../styles/tokens.css'
|
|
||||||
|
|
||||||
export function Page({ tag, title, desc, children }: { tag: string; title: string; desc: string; children: ReactNode }) {
|
export function Page({ tag, title, desc, children }: { tag: string; title: string; desc: string; children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import '@stack/design-system/styles/tokens.css'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import '../../styles/tokens.css'
|
|
||||||
import { App } from './App'
|
import { App } from './App'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(<App />)
|
createRoot(document.getElementById('root')!).render(<App />)
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Page, Section } from '../../lib/ui'
|
import { Page, Section } from '../../lib/ui'
|
||||||
import { ASSIGNMENT, CHAPTERS } from '../../lib/chapters'
|
import { ASSIGNMENT, CHAPTERS, PROFILES } from '../../lib/chapters'
|
||||||
import { MemphisCard } from '../../components/MemphisCard'
|
import { MemphisCard } from '@stack/design-system'
|
||||||
import { AssignBlock, Ticket } from '../../components/Ticket'
|
import { AssignBlock, Ticket } from '@stack/design-system'
|
||||||
import { EntryGuide } from '../../components/EntryGuide'
|
import { EntryGuide } from '@stack/design-system'
|
||||||
import { Mark } from '../../components/shapes'
|
import { Mark } from '@stack/design-system'
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
@@ -33,7 +33,7 @@ export function App() {
|
|||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
<Section label="入口梳理" note="推荐不是强制——无锁">
|
<Section label="入口梳理" note="推荐不是强制——无锁">
|
||||||
<EntryGuide />
|
<EntryGuide chapters={CHAPTERS} profiles={PROFILES} />
|
||||||
</Section>
|
</Section>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import '@stack/design-system/styles/tokens.css'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import '../../styles/tokens.css'
|
|
||||||
import { App } from './App'
|
import { App } from './App'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(<App />)
|
createRoot(document.getElementById('root')!).render(<App />)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import '@stack/design-system/styles/tokens.css'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import '../../styles/tokens.css'
|
|
||||||
import { App } from './App'
|
import { App } from './App'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(<App />)
|
createRoot(document.getElementById('root')!).render(<App />)
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
/** 组合场景=大部分组件拿出来的真实使用,不做解释性标注:
|
/** 组合场景=大部分组件拿出来的真实使用,不做解释性标注:
|
||||||
* 入口画像 → 课程卡与关系线 → 进度票券 → 指派块;形状散落于卡片附近空当。 */
|
* 入口画像 → 课程卡与关系线 → 进度票券 → 指派块;形状散落于卡片附近空当。 */
|
||||||
import { Page } from '../../lib/ui'
|
import { Page } from '../../lib/ui'
|
||||||
import { ASSIGNMENT, CHAPTERS, EDGES } from '../../lib/chapters'
|
import { ASSIGNMENT, CHAPTERS, EDGES, PROFILES } from '../../lib/chapters'
|
||||||
import { MemphisCard } from '../../components/MemphisCard'
|
import { useState } from 'react'
|
||||||
import { RouteLine } from '../../components/RouteLine'
|
import type { ChapterId } from '@stack/design-system'
|
||||||
import { AssignBlock, Ticket } from '../../components/Ticket'
|
import { MemphisCard } from '@stack/design-system'
|
||||||
import { EntryGuide } from '../../components/EntryGuide'
|
import { RouteLine } from '@stack/design-system'
|
||||||
import { Scatter } from '../../components/Scatter'
|
import { AssignBlock, Ticket } from '@stack/design-system'
|
||||||
import { Cross, Ring, Tremble, Tri, Wave } from '../../components/shapes'
|
import { EntryGuide } from '@stack/design-system'
|
||||||
|
import { Scatter } from '@stack/design-system'
|
||||||
|
import { Cross, Ring, Tremble, Tri, Wave } from '@stack/design-system'
|
||||||
|
|
||||||
/* 场景坐标(节点=卡描边真实边缘):卡1(40,110,250) 卡2(380,44,250,rot) 卡3(700,44,140) */
|
/* 场景坐标(节点=卡描边真实边缘):卡1(40,110,250) 卡2(380,44,250,rot) 卡3(700,44,140) */
|
||||||
const nodes = {
|
const nodes = {
|
||||||
@@ -17,13 +19,14 @@ const nodes = {
|
|||||||
} as const
|
} as const
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
|
const [hovered, setHovered] = useState<ChapterId | null>(null)
|
||||||
return (
|
return (
|
||||||
<Page
|
<Page
|
||||||
tag="<Examples />"
|
tag="<Examples />"
|
||||||
title="样例 · 组合场景"
|
title="样例 · 组合场景"
|
||||||
desc="大部分组件同台真实使用:入口画像 → 课程卡与关系线 → 进度票券 → 指派块;形状散落于卡片附近空当。"
|
desc="大部分组件同台真实使用:入口画像 → 课程卡与关系线 → 进度票券 → 指派块;hover 卡片,线与站点跟随浮起;形状散落于卡片附近空当。"
|
||||||
>
|
>
|
||||||
<EntryGuide />
|
<EntryGuide chapters={CHAPTERS} profiles={PROFILES} />
|
||||||
<div className="relative mt-8 h-[600px] overflow-hidden rounded-2xl bg-paper">
|
<div className="relative mt-8 h-[600px] overflow-hidden rounded-2xl bg-paper">
|
||||||
<Scatter
|
<Scatter
|
||||||
shapes={[
|
shapes={[
|
||||||
@@ -35,10 +38,28 @@ export function App() {
|
|||||||
<Tremble size={20} key="tremble" />,
|
<Tremble size={20} key="tremble" />,
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<RouteLine edges={EDGES} nodes={nodes} width={1160} height={600} />
|
<RouteLine edges={EDGES} nodes={nodes} width={1160} height={600} hovered={hovered} />
|
||||||
<div className="absolute left-[40px] top-[110px] w-[250px]"><MemphisCard chapter={CHAPTERS[1]} entry={1} /></div>
|
<div
|
||||||
<div className="absolute left-[380px] top-[44px] w-[250px] -rotate-1"><MemphisCard chapter={CHAPTERS[2]} entry={2} /></div>
|
className="absolute left-[40px] top-[110px] w-[250px]"
|
||||||
<div className="absolute left-[700px] top-[44px] w-[140px]"><MemphisCard chapter={CHAPTERS[5]} compact /></div>
|
onMouseEnter={() => setHovered(1)}
|
||||||
|
onMouseLeave={() => setHovered(null)}
|
||||||
|
>
|
||||||
|
<MemphisCard chapter={CHAPTERS[1]} entry={1} />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="absolute left-[380px] top-[44px] w-[250px] -rotate-1"
|
||||||
|
onMouseEnter={() => setHovered(2)}
|
||||||
|
onMouseLeave={() => setHovered(null)}
|
||||||
|
>
|
||||||
|
<MemphisCard chapter={CHAPTERS[2]} entry={2} />
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="absolute left-[700px] top-[44px] w-[140px]"
|
||||||
|
onMouseEnter={() => setHovered(5)}
|
||||||
|
onMouseLeave={() => setHovered(null)}
|
||||||
|
>
|
||||||
|
<MemphisCard chapter={CHAPTERS[5]} compact />
|
||||||
|
</div>
|
||||||
<div className="absolute left-[860px] top-[320px]"><AssignBlock {...ASSIGNMENT} /></div>
|
<div className="absolute left-[860px] top-[320px]"><AssignBlock {...ASSIGNMENT} /></div>
|
||||||
<div className="absolute left-[60px] top-[440px] flex gap-4">
|
<div className="absolute left-[60px] top-[440px] flex gap-4">
|
||||||
<Ticket chapter={CHAPTERS[2]} done />
|
<Ticket chapter={CHAPTERS[2]} done />
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import '@stack/design-system/styles/tokens.css'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import '../../styles/tokens.css'
|
|
||||||
import { App } from './App'
|
import { App } from './App'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(<App />)
|
createRoot(document.getElementById('root')!).render(<App />)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Page, Section } from '../../lib/ui'
|
import { Page, Section } from '../../lib/ui'
|
||||||
import { BrickDot, Cross, Mark, Ring, Tremble, Tri, Wave } from '../../components/shapes'
|
import { BrickDot, Cross, Mark, Ring, Tremble, Tri, Wave } from '@stack/design-system'
|
||||||
import { CHAPTERS } from '../../lib/chapters'
|
import { CHAPTERS } from '../../lib/chapters'
|
||||||
import { Ticket } from '../../components/Ticket'
|
import { Ticket } from '@stack/design-system'
|
||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
|
|
||||||
const SHAPES = [
|
const SHAPES = [
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import '@stack/design-system/styles/tokens.css'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import '../../styles/tokens.css'
|
|
||||||
import { App } from './App'
|
import { App } from './App'
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(<App />)
|
createRoot(document.getElementById('root')!).render(<App />)
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
@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-mute: #8a7a5c;
|
|
||||||
--color-body: #3d3d3d;
|
|
||||||
|
|
||||||
/* 统一硬投影——唯一投影配方,所有基础组件复用(rest / 拿起) */
|
|
||||||
--shadow-hard: 5px 5px 0 var(--color-ink);
|
|
||||||
--shadow-hard-lift: 8px 8px 0 var(--color-ink);
|
|
||||||
/* 同配方的 drop-shadow 形态:给挖空轮廓用(票券)——box-shadow 会被 mask 裁掉 */
|
|
||||||
--drop-shadow-hard: 5px 5px 0 var(--color-ink);
|
|
||||||
|
|
||||||
/* 章节色别名 */
|
|
||||||
--color-ch1: #ff6b6b;
|
|
||||||
--color-ch2: #ff8fab;
|
|
||||||
--color-ch3: #ffd93d;
|
|
||||||
--color-ch4: #4ecdc4;
|
|
||||||
--color-ch5: #5b8def;
|
|
||||||
--color-ch6: #a78bfa;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 票券打孔(完成票专用),三件分工:
|
|
||||||
* 挖空 = 票体 mask 差集(两端 1/4 圆硬停剪切,孔穿透露出页面底色);
|
|
||||||
* 阴影 = 外层 wrapper drop-shadow-hard——drop-shadow 跟随 alpha 轮廓,
|
|
||||||
* 孔挖到哪阴影咬到哪(box-shadow 会被 mask 的 border-box 裁切整体吃掉,不可用);
|
|
||||||
* 孔缘 = .pk-arc 半圆环补 ink 边线(mask 只减形不补线),圆心骑撕线两端。 */
|
|
||||||
.tk-pk {
|
|
||||||
--hole-x: 172.75px; /* 撕线中心:216 − 右描边 2 − stub 42 + 撕线半宽 */
|
|
||||||
}
|
|
||||||
.ticket-pk {
|
|
||||||
-webkit-mask-image:
|
|
||||||
radial-gradient(circle 7px at var(--hole-x) 0, transparent 6px, #000 6.5px),
|
|
||||||
radial-gradient(circle 7px at var(--hole-x) 100%, transparent 6px, #000 6.5px);
|
|
||||||
-webkit-mask-composite: source-in;
|
|
||||||
mask-image:
|
|
||||||
radial-gradient(circle 7px at var(--hole-x) 0, transparent 6px, #000 6.5px),
|
|
||||||
radial-gradient(circle 7px at var(--hole-x) 100%, transparent 6px, #000 6.5px);
|
|
||||||
mask-composite: intersect;
|
|
||||||
}
|
|
||||||
/* 孔缘弧线:16px 圆环(描边带 r6–8),clip-path 只留票内一半,不进阴影轮廓 */
|
|
||||||
.pk-arc {
|
|
||||||
position: absolute;
|
|
||||||
left: calc(var(--hole-x) - 8px);
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
border: 2px solid var(--color-ink);
|
|
||||||
border-radius: 9999px;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.pk-arc-t {
|
|
||||||
top: -8px;
|
|
||||||
clip-path: inset(50% 0 0 0);
|
|
||||||
}
|
|
||||||
.pk-arc-b {
|
|
||||||
bottom: -8px;
|
|
||||||
clip-path: inset(0 0 50% 0);
|
|
||||||
}
|
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@stack/design-system": "workspace:*",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1"
|
"react-dom": "^18.3.1"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,10 +6,8 @@
|
|||||||
import type { CSSProperties } from 'react'
|
import type { CSSProperties } from 'react'
|
||||||
import { Page, Section } from '../lib/ui'
|
import { Page, Section } from '../lib/ui'
|
||||||
import { CHAPTERS, CONTENT } from '../lib/chapters'
|
import { CHAPTERS, CONTENT } from '../lib/chapters'
|
||||||
import type { ChapterId } from '../lib/types'
|
import type { ChapterId } from '@stack/design-system'
|
||||||
import { AssignBlock } from './Ticket'
|
import { AssignBlock, BrickDots, Mark, Ring, Tri, Wave } from '@stack/design-system'
|
||||||
import { BrickDots } from './MemphisCard'
|
|
||||||
import { Mark, Ring, Tri, Wave } from './shapes'
|
|
||||||
|
|
||||||
const ALL: ChapterId[] = [1, 2, 3, 4, 5, 6]
|
const ALL: ChapterId[] = [1, 2, 3, 4, 5, 6]
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
/** 入口梳理 —— 读者画像推荐路径:是推荐不是强制(无锁) */
|
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
/** 散落形状摆放——「卡片附近空当 + 随机」:
|
|
||||||
* 网格采样容器空白 → 过滤出距内容盒([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<HTMLDivElement>(null)
|
|
||||||
const [places, setPlaces] = useState<Place[]>([])
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<div ref={layerRef} data-scatter-layer aria-hidden className="pointer-events-none absolute inset-0">
|
|
||||||
{places.map((p, i) => (
|
|
||||||
<div
|
|
||||||
key={i}
|
|
||||||
data-scatter
|
|
||||||
className="absolute"
|
|
||||||
style={{ left: p.x, top: p.y, transform: `translate(-50%, -50%) rotate(${p.rot}deg)` }}
|
|
||||||
>
|
|
||||||
{shapes[i]}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
/** 票券 —— 只表达完成状态:完成=沿撕线打孔(半圆缺口),未开始=实线无孔。
|
|
||||||
* 外层 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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
/** 七个语义形状——装饰即语义(取自 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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/** 教程内容数据——大纲见 OUTLINE.md,词汇与结构对齐 ai-era-tutorial skill */
|
/** 教程内容数据——大纲见 OUTLINE.md,词汇与结构对齐 ai-era-tutorial skill */
|
||||||
import type { Assignment, Chapter, ChapterId, Edge, Profile } from './types'
|
import type { Assignment, Chapter, ChapterId, Edge, Profile } from '@stack/design-system'
|
||||||
|
|
||||||
export const CHAPTERS: Record<ChapterId, Chapter> = {
|
export const CHAPTERS: Record<ChapterId, Chapter> = {
|
||||||
1: {
|
1: {
|
||||||
@@ -432,7 +432,7 @@ export const CONTENT: Record<ChapterId, ChapterContent> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 指派学习块示例数据(设计系统·组件/样例页演示用) */
|
/** 指派学习块示例数据(设计系统·组件/样例页演示用) */
|
||||||
export const ASSIGNMENT: import('./types').Assignment = {
|
export const ASSIGNMENT: import('@stack/design-system').Assignment = {
|
||||||
title: '指派学习 · HTTP 协议',
|
title: '指派学习 · HTTP 协议',
|
||||||
pos: 'Web 通信的地基,旁边是 DNS(找机器)与 TCP(铺线路)',
|
pos: 'Web 通信的地基,旁边是 DNS(找机器)与 TCP(铺线路)',
|
||||||
expect: '能向 agent 讲清一次请求的往返:URL → DNS → 连接 → 请求行/头/体 → 响应 → 渲染',
|
expect: '能向 agent 讲清一次请求的往返:URL → DNS → 连接 → 请求行/头/体 → 响应 → 渲染',
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
/** 设计系统数据类型——对齐 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 }
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
/** 教程站页面骨架:站头导航跨页常驻(view-transition-name 共享元素,页间不闪烁)。
|
/** 教程站页面骨架:站头导航跨页常驻(view-transition-name 共享元素,页间不闪烁)。
|
||||||
* 选中项背景色=该章色(与章头 title 横幅统一);书架/设计册选中为 ink。 */
|
* 选中项背景色=该章色(与章头 title 横幅统一);书架/设计册选中为 ink。 */
|
||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
import type { ChapterId } from './types'
|
import type { ChapterId } from '@stack/design-system'
|
||||||
import { CHAPTERS } from './chapters'
|
import { CHAPTERS } from './chapters'
|
||||||
import '../styles/tokens.css'
|
|
||||||
|
|
||||||
/** 设计系统四册(自 design-lab 迁入) */
|
/** 设计系统四册(自 design-lab 迁入) */
|
||||||
export type DesignKey = 'design-colors' | 'design-typography' | 'design-symbols' | 'design-components' | 'design-examples'
|
export type DesignKey = 'design-colors' | 'design-typography' | 'design-symbols' | 'design-components' | 'design-examples'
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Page, Section } from '../../lib/ui'
|
import { Page, Section } from '../../lib/ui'
|
||||||
import { ASSIGNMENT, CHAPTERS } from '../../lib/chapters'
|
import { ASSIGNMENT, CHAPTERS, PROFILES } from '../../lib/chapters'
|
||||||
import { MemphisCard } from '../../components/MemphisCard'
|
import { MemphisCard } from '@stack/design-system'
|
||||||
import { AssignBlock, Ticket } from '../../components/Ticket'
|
import { AssignBlock, Ticket } from '@stack/design-system'
|
||||||
import { EntryGuide } from '../../components/EntryGuide'
|
import { EntryGuide } from '@stack/design-system'
|
||||||
import { Mark } from '../../components/shapes'
|
import { Mark } from '@stack/design-system'
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
@@ -29,7 +29,7 @@ export function App() {
|
|||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
<Section label="入口梳理" note="推荐不是强制——无锁">
|
<Section label="入口梳理" note="推荐不是强制——无锁">
|
||||||
<EntryGuide />
|
<EntryGuide chapters={CHAPTERS} profiles={PROFILES} />
|
||||||
</Section>
|
</Section>
|
||||||
</Page>
|
</Page>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
/** 组合场景=大部分组件拿出来的真实使用,不做解释性标注:
|
/** 组合场景=大部分组件拿出来的真实使用,不做解释性标注:
|
||||||
* 入口画像 → 课程卡与关系线 → 进度票券 → 指派块;形状散落于卡片附近空当。 */
|
* 入口画像 → 课程卡与关系线 → 进度票券 → 指派块;形状散落于卡片附近空当。 */
|
||||||
import { Page } from '../../lib/ui'
|
import { Page } from '../../lib/ui'
|
||||||
import { ASSIGNMENT, CHAPTERS, EDGES } from '../../lib/chapters'
|
import { ASSIGNMENT, CHAPTERS, EDGES, PROFILES } from '../../lib/chapters'
|
||||||
import { MemphisCard } from '../../components/MemphisCard'
|
import { AssignBlock, EntryGuide, MemphisCard, RouteLine, Scatter, Ticket } from '@stack/design-system'
|
||||||
import { RouteLine } from '../../components/RouteLine'
|
import { Cross, Ring, Tremble, Tri, Wave } from '@stack/design-system'
|
||||||
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) */
|
/* 场景坐标(节点=卡描边真实边缘):卡1(40,110,250) 卡2(380,44,250,rot) 卡3(700,44,140) */
|
||||||
const nodes = {
|
const nodes = {
|
||||||
@@ -19,7 +15,7 @@ const nodes = {
|
|||||||
export function App() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
<Page active="design-examples" title="样例 · 组合场景" desc="大部分组件同台真实使用:入口画像 → 课程卡与关系线 → 进度票券 → 指派块;形状散落于卡片附近空当。">
|
<Page active="design-examples" title="样例 · 组合场景" desc="大部分组件同台真实使用:入口画像 → 课程卡与关系线 → 进度票券 → 指派块;形状散落于卡片附近空当。">
|
||||||
<EntryGuide />
|
<EntryGuide chapters={CHAPTERS} profiles={PROFILES} />
|
||||||
<div className="relative mt-8 h-[600px] overflow-hidden rounded-2xl bg-paper">
|
<div className="relative mt-8 h-[600px] overflow-hidden rounded-2xl bg-paper">
|
||||||
<Scatter
|
<Scatter
|
||||||
shapes={[
|
shapes={[
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Page, Section } from '../../lib/ui'
|
import { Page, Section } from '../../lib/ui'
|
||||||
import { BrickDot, Cross, Mark, Ring, Tremble, Tri, Wave } from '../../components/shapes'
|
import { BrickDot, Cross, Mark, Ring, Tremble, Tri, Wave } from '@stack/design-system'
|
||||||
import { CHAPTERS } from '../../lib/chapters'
|
import { CHAPTERS } from '../../lib/chapters'
|
||||||
import { Ticket } from '../../components/Ticket'
|
import { Ticket } from '@stack/design-system'
|
||||||
import type { ReactNode } from 'react'
|
import type { ReactNode } from 'react'
|
||||||
|
|
||||||
const SHAPES = [
|
const SHAPES = [
|
||||||
|
|||||||
@@ -2,12 +2,9 @@
|
|||||||
* 主线场景:hover 卡片时线与站点跟随浮起(状态提升到场景层) */
|
* 主线场景:hover 卡片时线与站点跟随浮起(状态提升到场景层) */
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Page, Section } from '../../lib/ui'
|
import { Page, Section } from '../../lib/ui'
|
||||||
import { CHAPTERS } from '../../lib/chapters'
|
import { MemphisCard, EntryGuide, RouteLine, Ticket } from '@stack/design-system'
|
||||||
import { MemphisCard } from '../../components/MemphisCard'
|
import type { ChapterId } from '@stack/design-system'
|
||||||
import { EntryGuide } from '../../components/EntryGuide'
|
import { CHAPTERS, PROFILES } from '../../lib/chapters'
|
||||||
import { RouteLine } from '../../components/RouteLine'
|
|
||||||
import { Ticket } from '../../components/Ticket'
|
|
||||||
import type { ChapterId } from '../../lib/types'
|
|
||||||
|
|
||||||
/* 场景坐标(节点=卡描边真实边缘):卡1(40,110,250) 卡2(380,44,250,rot) 卡5(700,44,140) */
|
/* 场景坐标(节点=卡描边真实边缘):卡1(40,110,250) 卡2(380,44,250,rot) 卡5(700,44,140) */
|
||||||
const nodes = {
|
const nodes = {
|
||||||
@@ -34,12 +31,12 @@ export function App() {
|
|||||||
desc="靶子先行:每章先给目标、预期结果、积木清单,步骤只写「产出什么+核对什么」;过程有没有学习价值,每步先分类。"
|
desc="靶子先行:每章先给目标、预期结果、积木清单,步骤只写「产出什么+核对什么」;过程有没有学习价值,每步先分类。"
|
||||||
>
|
>
|
||||||
<Section label="入口画像" note="推荐不是强制——无锁">
|
<Section label="入口画像" note="推荐不是强制——无锁">
|
||||||
<EntryGuide />
|
<EntryGuide chapters={CHAPTERS} profiles={PROFILES} linkChapter />
|
||||||
</Section>
|
</Section>
|
||||||
<Section label="六章" note="卡片是入口;点相框进入章节">
|
<Section label="六章" note="卡片是入口;点相框进入章节">
|
||||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{GRID.map(({ id, entry }) => (
|
{GRID.map(({ id, entry }) => (
|
||||||
<MemphisCard key={id} chapter={CHAPTERS[id]} entry={entry} />
|
<MemphisCard key={id} chapter={CHAPTERS[id]} entry={entry} href={`/ch${id}.html`} morph />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
@@ -61,21 +58,21 @@ export function App() {
|
|||||||
onMouseEnter={() => setHovered(1)}
|
onMouseEnter={() => setHovered(1)}
|
||||||
onMouseLeave={() => setHovered(null)}
|
onMouseLeave={() => setHovered(null)}
|
||||||
>
|
>
|
||||||
<MemphisCard chapter={CHAPTERS[1]} />
|
<MemphisCard chapter={CHAPTERS[1]} href="/ch1.html" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="absolute left-[380px] top-[44px] w-[250px] -rotate-1"
|
className="absolute left-[380px] top-[44px] w-[250px] -rotate-1"
|
||||||
onMouseEnter={() => setHovered(2)}
|
onMouseEnter={() => setHovered(2)}
|
||||||
onMouseLeave={() => setHovered(null)}
|
onMouseLeave={() => setHovered(null)}
|
||||||
>
|
>
|
||||||
<MemphisCard chapter={CHAPTERS[2]} />
|
<MemphisCard chapter={CHAPTERS[2]} href="/ch2.html" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="absolute left-[700px] top-[44px] w-[140px]"
|
className="absolute left-[700px] top-[44px] w-[140px]"
|
||||||
onMouseEnter={() => setHovered(5)}
|
onMouseEnter={() => setHovered(5)}
|
||||||
onMouseLeave={() => setHovered(null)}
|
onMouseLeave={() => setHovered(null)}
|
||||||
>
|
>
|
||||||
<MemphisCard chapter={CHAPTERS[5]} compact />
|
<MemphisCard chapter={CHAPTERS[5]} compact href="/ch5.html" />
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute left-[60px] top-[380px] flex gap-4">
|
<div className="absolute left-[60px] top-[380px] flex gap-4">
|
||||||
<Ticket chapter={CHAPTERS[1]} done />
|
<Ticket chapter={CHAPTERS[1]} done />
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# @stack/design-system
|
||||||
|
|
||||||
|
孟菲斯 × 拟物卡设计系统的唯一定义处:组件(MemphisCard/RouteLine/Ticket/AssignBlock/EntryGuide/Scatter/shapes)、tokens(颜色/字体/投影/票券打孔)、类型。
|
||||||
|
|
||||||
|
`apps/design-lab`(规格实验场)与 `apps/tutorial`(教程站)共同引用本包——特性只改这里,双端同步获得。消费方式:workspace 依赖 + 源码直出(vite 编译 TS,无需构建步骤)。
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "@stack/design-system",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"main": "./src/index.ts",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts",
|
||||||
|
"./styles/tokens.css": "./src/styles/tokens.css"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/** 入口梳理 —— 读者画像推荐路径:是推荐不是强制(无锁)。
|
||||||
|
* 章节数据与画像由宿主注入(design-lab 演示数据 / tutorial 真实数据)。 */
|
||||||
|
import type { Chapter, ChapterId, Profile } from '../lib/types'
|
||||||
|
|
||||||
|
export function EntryGuide({
|
||||||
|
chapters,
|
||||||
|
profiles,
|
||||||
|
linkChapter = false,
|
||||||
|
}: {
|
||||||
|
chapters: Record<ChapterId, Chapter>
|
||||||
|
profiles: Profile[]
|
||||||
|
/** 路径 chip 是否链接章节页(教程站 true) */
|
||||||
|
linkChapter?: boolean
|
||||||
|
}) {
|
||||||
|
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>}
|
||||||
|
{linkChapter ? (
|
||||||
|
<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
|
||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
+8
-2
@@ -38,12 +38,18 @@ export function MemphisCard({
|
|||||||
entry,
|
entry,
|
||||||
compact = false,
|
compact = false,
|
||||||
zoneText,
|
zoneText,
|
||||||
|
href,
|
||||||
|
morph = false,
|
||||||
}: {
|
}: {
|
||||||
chapter: Chapter
|
chapter: Chapter
|
||||||
entry?: 1 | 2 | 3
|
entry?: 1 | 2 | 3
|
||||||
compact?: boolean
|
compact?: boolean
|
||||||
/** 静置态相框文字(可含 <Mark> 荧光标记):教程卡片的价值区用法示例 */
|
/** 静置态相框文字(可含 <Mark> 荧光标记):教程卡片的价值区用法示例 */
|
||||||
zoneText?: ReactNode
|
zoneText?: ReactNode
|
||||||
|
/** 卡片跳转目标:缺省锚点 #ch<id>(单页演示);整页跳转传 /ch<id>.html */
|
||||||
|
href?: string
|
||||||
|
/** 挂 view-transition-name(跨文档共享元素变形)。同页同章多卡时勿开(重名失效) */
|
||||||
|
morph?: boolean
|
||||||
}) {
|
}) {
|
||||||
const [gifOn, setGifOn] = useState(false)
|
const [gifOn, setGifOn] = useState(false)
|
||||||
const [locked, setLocked] = useState(false)
|
const [locked, setLocked] = useState(false)
|
||||||
@@ -78,7 +84,7 @@ export function MemphisCard({
|
|||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
ref={boxRef}
|
ref={boxRef}
|
||||||
href={`/ch${chapter.id}.html`}
|
href={href ?? `#ch${chapter.id}`}
|
||||||
data-comp="course-card"
|
data-comp="course-card"
|
||||||
className="group relative block rounded-[14px] border-2 border-ink bg-paper p-[11px] no-underline
|
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
|
shadow-hard transition-transform duration-150 focus-visible:outline-none
|
||||||
@@ -97,7 +103,7 @@ export function MemphisCard({
|
|||||||
style={{
|
style={{
|
||||||
background: chapter.color,
|
background: chapter.color,
|
||||||
boxShadow: 'inset 0 3px 8px rgba(26,26,26,0.30), inset 0 -1px 0 rgba(255,255,255,0.55)',
|
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}`,
|
...(morph ? { viewTransitionName: `ch-${chapter.id}` } : {}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{showGif ? (
|
{showGif ? (
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,12 @@
|
|||||||
|
/** @stack/design-system —— 孟菲斯 × 拟物卡设计系统(唯一定义处)
|
||||||
|
* 组件、tokens、类型从此导出;design-lab 与 tutorial 共同引用,禁止再拷贝。 */
|
||||||
|
import './styles/tokens.css'
|
||||||
|
|
||||||
|
export * from './lib/types'
|
||||||
|
export { MemphisCard, BrickDots, EntryBadge } from './components/MemphisCard'
|
||||||
|
export { RouteLine } from './components/RouteLine'
|
||||||
|
export type { LaneNode } from './components/RouteLine'
|
||||||
|
export { Ticket, AssignBlock } from './components/Ticket'
|
||||||
|
export { EntryGuide } from './components/EntryGuide'
|
||||||
|
export { Scatter } from './components/Scatter'
|
||||||
|
export { Tri, BrickDot, Ring, Wave, Mark, Tremble, Cross, MissMark } from './components/shapes'
|
||||||
File renamed without changes.
@@ -1,4 +1,7 @@
|
|||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
|
/* 显式声明包内组件源码为扫描源——经 node_modules 符号链接解析时,
|
||||||
|
* 自动内容检测不会覆盖 workspace 包,工具类会静默丢失 */
|
||||||
|
@source '../';
|
||||||
|
|
||||||
/* ===== 设计系统 tokens(孟菲斯 × 拟物卡)===== */
|
/* ===== 设计系统 tokens(孟菲斯 × 拟物卡)===== */
|
||||||
@theme {
|
@theme {
|
||||||
Reference in new issue
Block a user