Initialize web application with React, TypeScript, and Vite setup

- Added essential project files including package.json, tsconfig, and Vite configuration.
- Created initial components and chapters for the application structure.
- Implemented ESLint configuration for code quality and consistency.
- Included .gitignore files to exclude build artifacts and dependencies.
- Added README for project documentation and setup instructions.
- Integrated a sample video and favicon for the application.
This commit is contained in:
lishiqi.conard 2026-04-21 21:47:16 +08:00
parent 2e214c5b76
commit 32a23cdc3e
61 changed files with 16738 additions and 0 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
*.swp *.swp
*.swo *.swo
*~ *~
/node_modules

24
web/.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

1
web/.npmrc Normal file
View File

@ -0,0 +1 @@
registry=https://registry.npmjs.org/

73
web/README.md Normal file
View File

@ -0,0 +1,73 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
## React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
```js
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
```

23
web/eslint.config.js Normal file
View File

@ -0,0 +1,23 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])

13
web/index.html Normal file
View File

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Claude Design Skill · 视频演示</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

3013
web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

30
web/package.json Normal file
View File

@ -0,0 +1,30 @@
{
"name": "my-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"react": "^19.2.5",
"react-dom": "^19.2.5"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@types/node": "^24.12.2",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"eslint": "^9.39.4",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.5.0",
"typescript": "~6.0.2",
"typescript-eslint": "^8.58.2",
"vite": "^8.0.9"
}
}

1
web/public/favicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
web/public/video.mp4 Normal file

Binary file not shown.

32
web/src/App.tsx Normal file
View File

@ -0,0 +1,32 @@
import { Stage } from './stage/Stage';
import { ChapterHost } from './stage/ChapterHost';
import { ProgressBar } from './stage/ProgressBar';
import { useHotKeys } from './stage/useHotKeys';
import { stepStore, useStep } from './store/useStep';
import { chapters } from './chapters';
function App() {
useHotKeys();
const { chapterIndex } = useStep();
const theme = chapters[chapterIndex]?.theme ?? 'light';
return (
<div
onClick={(e) => {
// 任何带 data-no-step 的祖先都不触发推进
const target = e.target as HTMLElement;
if (target.closest('[data-no-step]')) return;
stepStore.next();
}}
>
<Stage theme={theme}>
<ChapterHost />
</Stage>
<div data-no-step>
<ProgressBar />
</div>
</div>
);
}
export default App;

View File

@ -0,0 +1,705 @@
/* =========================================================
Chapter 01 · Opening 一则崩盘
- ink 主题深墨底
- 4 幕全部由 SceneFade 包裹独立铺满 stage互不重叠
- 没有任何"网页 chrome"无顶部章节标记 / 底部 footer
- 真实数据LegalZoom -20% / CRCL -20% / CrowdStrike -7% / Figma -7%
========================================================= */
.opening {
position: absolute;
inset: 0;
font-family: var(--f-sans);
color: var(--fg);
background: var(--bg);
overflow: hidden;
}
/* 每幕都是覆盖 stage 的全屏容器 */
.opening__sceneA,
.opening__sceneB,
.opening__sceneC,
.opening__sceneD {
position: absolute;
inset: 0;
padding: 110px 120px;
}
.opening__sceneC {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
}
.opening__sceneD {
display: flex;
flex-direction: column;
justify-content: center;
}
/* =========================================================
SCENE A · Crash
========================================================= */
/* LIVE 报价条(仅 Scene A 出现,作为"市场环境") */
.opening__live {
position: absolute;
top: 90px;
right: 120px;
display: flex;
align-items: center;
gap: 18px;
padding: 10px 20px;
border: 1px solid var(--line-mid);
background: oklch(0.965 0.018 78 / 0.025);
backdrop-filter: blur(6px);
font-family: var(--f-mono);
font-size: 16px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--fg-mute);
z-index: 6;
animation: opLiveIn 700ms cubic-bezier(.2,.8,.2,1) both;
}
@keyframes opLiveIn {
from { opacity: 0; transform: translateY(-12px); }
to { opacity: 1; transform: translateY(0); }
}
.opening__live-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--crimson);
animation: opPulse 1.6s ease-in-out infinite;
}
@keyframes opPulse {
0%, 100% { opacity: 0.4; transform: scale(0.85); }
50% { opacity: 1; transform: scale(1.15); }
}
.opening__live-label {
color: var(--fg-soft);
font-weight: 500;
}
.opening__live-clock {
color: var(--paper);
font-size: 18px;
letter-spacing: 0.08em;
}
.opening__live-sep {
color: var(--fg-faint);
}
.opening__live-quote {
display: flex;
align-items: baseline;
gap: 10px;
}
.opening__live-quote-tag {
color: var(--fg-mute);
font-size: 13px;
padding: 2px 6px;
border: 1px solid var(--line-mid);
}
.opening__live-quote-val {
color: var(--paper);
font-size: 18px;
}
.opening__live-quote-d {
color: var(--crimson);
font-size: 14px;
}
/* "FIGMA" 巨字背景水印 */
.opening__crash-mega {
position: absolute;
left: 100px;
bottom: 120px;
font-family: var(--f-serif);
font-style: italic;
font-size: 360px;
line-height: 0.85;
color: oklch(0.965 0.018 78 / 0.05);
letter-spacing: -0.04em;
user-select: none;
pointer-events: none;
white-space: nowrap;
z-index: 1;
}
/* 引子小字 */
.opening__crash-intro {
position: absolute;
top: 230px;
left: 120px;
display: flex;
align-items: center;
gap: 18px;
font-family: var(--f-mono);
font-size: 16px;
letter-spacing: 0.18em;
color: var(--fg-mute);
z-index: 4;
}
.opening__crash-intro-tag {
padding: 5px 12px;
border: 1px solid var(--line-mid);
color: var(--fg-soft);
text-transform: uppercase;
}
.opening__crash-intro-text {
font-family: var(--f-sans);
font-size: 22px;
letter-spacing: 0;
color: var(--fg-soft);
}
/* 折线图 */
.opening__chart {
position: absolute;
inset: 320px 80px 280px;
width: calc(100% - 160px);
height: calc(100% - 600px);
z-index: 2;
}
.opening__chart-grid line {
stroke: var(--line);
}
.opening__chart-history {
stroke: var(--fg-faint);
opacity: 0.65;
stroke-dasharray: 1400;
stroke-dashoffset: 1400;
animation: opPathDraw 1500ms cubic-bezier(.2,.8,.2,1) forwards,
opHistoryHum 6s ease-in-out 1500ms infinite alternate;
}
@keyframes opHistoryHum {
0% { transform: translateY(0); }
50% { transform: translateY(-3px); }
100% { transform: translateY(2px); }
}
.opening__chart-crash {
stroke: var(--crimson);
stroke-linecap: round;
stroke-dasharray: 800;
stroke-dashoffset: 800;
animation: opPathDraw 720ms cubic-bezier(.4,0,1,1) 80ms forwards;
filter: drop-shadow(0 0 24px oklch(0.560 0.200 22 / 0.5));
}
@keyframes opPathDraw {
to { stroke-dashoffset: 0; }
}
.opening__chart-fill {
opacity: 0;
animation: opFadeIn 600ms cubic-bezier(.2,.8,.2,1) 700ms forwards;
}
.opening__chart-end {
opacity: 0;
animation: opFadeIn 500ms cubic-bezier(.2,.8,.2,1) 900ms forwards;
}
.opening__chart-end circle {
fill: var(--crimson);
filter: drop-shadow(0 0 10px oklch(0.560 0.200 22 / 0.7));
animation: opEndPulse 1.8s ease-in-out infinite;
transform-origin: 1760px 510px;
}
@keyframes opEndPulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.4); }
}
.opening__chart-end line {
stroke: var(--fg-faint);
}
.opening__chart-end text {
font-family: var(--f-mono);
font-size: 13px;
letter-spacing: 0.15em;
fill: var(--fg-mute);
text-transform: uppercase;
}
@keyframes opFadeIn {
to { opacity: 1; }
}
/* 中文 headline */
.opening__crash-headline {
position: absolute;
bottom: 140px;
left: 120px;
right: 600px;
display: flex;
flex-wrap: wrap;
align-items: baseline;
gap: 22px;
z-index: 4;
}
.opening__crash-line {
font-size: 42px;
color: var(--fg-soft);
font-weight: 300;
letter-spacing: 0.02em;
}
.opening__crash-emph {
font-family: var(--f-serif);
font-style: italic;
font-size: 130px;
line-height: 0.95;
color: var(--paper);
letter-spacing: -0.02em;
font-weight: 400;
}
/* 跌幅大数字 */
.opening__crash-stat {
position: absolute;
right: 120px;
top: 230px;
text-align: right;
z-index: 4;
}
.opening__crash-stat-num {
font-family: var(--f-serif);
font-size: 240px;
line-height: 0.9;
color: var(--crimson);
letter-spacing: -0.04em;
font-feature-settings: "tnum";
display: flex;
align-items: baseline;
justify-content: flex-end;
filter: drop-shadow(0 0 40px oklch(0.560 0.200 22 / 0.35));
}
.opening__crash-stat-sign {
margin-right: 6px;
}
.opening__crash-stat-pct {
font-size: 130px;
margin-left: 4px;
}
.opening__crash-stat-label {
margin-top: 18px;
font-family: var(--f-mono);
font-size: 16px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--fg-mute);
}
/* =========================================================
SCENE B · Victims
========================================================= */
.opening__vics-eyebrow {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 18px;
margin-bottom: 56px;
}
.opening__vics-eyebrow-inner {
display: flex;
align-items: baseline;
gap: 18px;
font-size: 38px;
color: var(--fg-soft);
font-weight: 300;
}
.opening__vics-em {
font-family: var(--f-serif);
font-style: italic;
font-size: 92px;
color: var(--accent);
font-weight: 400;
line-height: 1;
position: relative;
}
.opening__vics-em::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 8px;
height: 8px;
background: oklch(0.700 0.170 42 / 0.25);
z-index: -1;
animation: opUnderlineGrow 700ms cubic-bezier(.2,.8,.2,1) 320ms both;
transform-origin: 0 50%;
}
@keyframes opUnderlineGrow {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
.opening__vics-by {
display: flex;
align-items: baseline;
gap: 14px;
font-family: var(--f-mono);
text-transform: uppercase;
letter-spacing: 0.22em;
}
.opening__vics-by-tag {
font-size: 14px;
color: var(--fg-faint);
}
.opening__vics-by-name {
font-family: var(--f-serif);
font-style: italic;
font-size: 60px;
color: var(--accent);
text-transform: none;
letter-spacing: -0.01em;
}
.opening__vics-table {
display: flex;
flex-direction: column;
border-top: 1px solid var(--line-mid);
}
.opening__vics-thead {
display: grid;
grid-template-columns: 1.4fr 1.2fr 0.5fr 0.7fr 0.5fr;
gap: 32px;
padding: 14px 0;
font-family: var(--f-mono);
font-size: 13px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--fg-faint);
border-bottom: 1px solid var(--line);
}
.opening__vic {
display: grid;
grid-template-columns: 1.4fr 1.2fr 0.5fr 0.7fr 0.5fr;
gap: 32px;
align-items: baseline;
padding: 30px 0;
border-bottom: 1px solid var(--line);
position: relative;
}
.opening__vic-product {
font-family: var(--f-mono);
font-size: 22px;
font-weight: 500;
color: var(--accent);
letter-spacing: -0.01em;
display: flex;
align-items: baseline;
gap: 12px;
}
.opening__vic-product::before {
content: "▸";
color: var(--accent);
font-size: 16px;
}
.opening__vic-company {
font-family: var(--f-serif);
font-size: 60px;
letter-spacing: -0.01em;
color: var(--fg);
line-height: 1;
}
.opening__vic-ticker {
font-family: var(--f-mono);
font-size: 16px;
color: var(--fg-mute);
text-transform: uppercase;
letter-spacing: 0.1em;
border: 1px solid var(--line-mid);
padding: 4px 10px;
display: inline-block;
width: fit-content;
}
.opening__vic-field {
font-size: 18px;
color: var(--fg-mute);
}
.opening__vic-drop {
font-family: var(--f-mono);
font-size: 44px;
font-weight: 500;
color: var(--fg-mute);
text-align: right;
font-feature-settings: "tnum";
letter-spacing: -0.01em;
}
.opening__vic.is-current {
background: linear-gradient(
to right,
transparent 0%,
oklch(0.700 0.170 42 / 0.05) 30%,
oklch(0.700 0.170 42 / 0.10) 70%,
transparent 100%
);
}
.opening__vic.is-current .opening__vic-company {
color: var(--paper);
}
.opening__vic.is-current .opening__vic-drop {
color: var(--crimson);
}
.opening__vic-flag {
position: absolute;
right: 0;
top: -4px;
font-family: var(--f-mono);
font-size: 11px;
letter-spacing: 0.3em;
text-transform: uppercase;
color: var(--accent);
padding: 3px 8px;
border: 1px solid var(--accent);
background: var(--bg);
animation: opFlagPulse 1.6s ease-in-out infinite;
}
@keyframes opFlagPulse {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
/* =========================================================
SCENE C · Claude Design 揭幕
========================================================= */
.opening__reveal-meta {
display: flex;
align-items: center;
gap: 22px;
font-family: var(--f-mono);
font-size: 16px;
letter-spacing: 0.3em;
text-transform: uppercase;
color: var(--fg-mute);
margin-bottom: 56px;
}
.opening__reveal-meta .dot {
width: 4px;
height: 4px;
background: var(--fg-mute);
border-radius: 50%;
}
.opening__reveal-title {
display: flex;
align-items: baseline;
gap: 32px;
margin: 0;
font-size: 220px;
line-height: 0.9;
font-family: var(--f-serif);
font-weight: 400;
letter-spacing: -0.03em;
}
.opening__reveal-word {
display: inline-block;
}
.opening__reveal-word--em {
font-style: italic;
color: var(--accent);
}
.opening__reveal-sub {
margin-top: 50px;
display: flex;
align-items: center;
gap: 22px;
font-size: 52px;
font-weight: 300;
color: var(--fg-soft);
}
.opening__reveal-tilde {
color: var(--accent);
font-family: var(--f-serif);
font-style: italic;
}
.opening__reveal-tags {
margin-top: 56px;
display: flex;
gap: 14px;
}
.opening__reveal-tags span {
padding: 10px 20px;
border: 1px solid var(--line-mid);
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--fg-mute);
background: oklch(0.965 0.018 78 / 0.025);
}
/* =========================================================
SCENE D · Skill pivot
========================================================= */
.opening__pivot-eyebrow {
display: flex;
align-items: center;
gap: 16px;
font-family: var(--f-mono);
font-size: 18px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--accent);
margin-bottom: 36px;
}
.opening__pivot-eyebrow-bar {
width: 56px;
height: 1px;
background: var(--accent);
}
.opening__pivot-title {
font-family: var(--f-serif);
font-weight: 400;
font-size: 124px;
line-height: 1.05;
letter-spacing: -0.025em;
color: var(--paper);
max-width: 1500px;
margin: 0 0 64px 0;
display: flex;
flex-wrap: wrap;
gap: 0 18px;
}
.opening__pivot-em {
color: var(--accent);
font-style: italic;
position: relative;
white-space: nowrap;
}
.opening__pivot-em::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 6px;
height: 8px;
background: oklch(0.700 0.170 42 / 0.25);
z-index: -1;
animation: opUnderlineGrow 800ms cubic-bezier(.2,.8,.2,1) 1100ms both;
transform-origin: 0 50%;
}
.opening__pivot-row {
display: grid;
grid-template-columns: 1.1fr 1fr;
gap: 80px;
align-items: center;
}
.opening__skill-card {
position: relative;
border: 1px solid var(--line-mid);
background: oklch(0.965 0.018 78 / 0.025);
padding: 36px 44px;
backdrop-filter: blur(4px);
}
.opening__skill-card-deco::before,
.opening__skill-card-deco::after {
content: "";
position: absolute;
width: 18px;
height: 18px;
}
.opening__skill-card-deco::before {
top: -1px;
left: -1px;
border-top: 2px solid var(--accent);
border-left: 2px solid var(--accent);
}
.opening__skill-card-deco::after {
bottom: -1px;
right: -1px;
border-bottom: 2px solid var(--accent);
border-right: 2px solid var(--accent);
}
.opening__skill-card-head {
display: flex;
align-items: baseline;
gap: 18px;
padding-bottom: 22px;
border-bottom: 1px solid var(--line);
margin-bottom: 24px;
}
.opening__skill-card-tag {
font-family: var(--f-mono);
font-size: 13px;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--accent);
padding: 4px 10px;
border: 1px solid var(--accent);
}
.opening__skill-card-name {
font-family: var(--f-mono);
font-size: 32px;
font-weight: 500;
color: var(--paper);
letter-spacing: -0.01em;
}
.opening__skill-card-body {
display: flex;
flex-direction: column;
gap: 12px;
}
.opening__skill-card-row {
display: grid;
grid-template-columns: 130px 1fr;
gap: 24px;
align-items: baseline;
font-size: 22px;
}
.opening__skill-card-k {
font-family: var(--f-mono);
text-transform: uppercase;
letter-spacing: 0.18em;
font-size: 13px;
color: var(--fg-mute);
}
.opening__skill-card-v {
color: var(--paper);
}
.opening__skill-card-foot {
margin-top: 24px;
padding-top: 18px;
border-top: 1px solid var(--line);
display: flex;
align-items: center;
gap: 12px;
font-family: var(--f-mono);
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.22em;
color: var(--fg-mute);
}
.opening__skill-card-pulse {
width: 8px;
height: 8px;
border-radius: 50%;
background: oklch(0.78 0.18 145);
animation: opPulse 1.6s ease-in-out infinite;
}
.opening__pivot-aside {
font-family: var(--f-serif);
font-size: 56px;
line-height: 1.2;
color: var(--fg-soft);
position: relative;
}
.opening__pivot-aside-q {
position: absolute;
left: -56px;
top: -36px;
font-size: 200px;
color: var(--accent);
opacity: 0.4;
font-style: italic;
line-height: 1;
}
.opening__pivot-aside em {
font-style: italic;
color: var(--paper);
display: block;
margin-top: 12px;
}

View File

@ -0,0 +1,383 @@
import type { ChapterContext, ChapterDef } from '../types';
import { Reveal } from '../../shared/Reveal';
import { NumberTicker } from '../../shared/NumberTicker';
import { LiveClock, FlickerNumber } from '../../shared/LiveTicker';
import { SceneFade } from '../../shared/SceneFade';
import './Opening.css';
/**
* Chapter 01 · Opening
*
* 4 × 14 step
* Scene A · Crash (0..3) Figma
* Scene B · Victims (4..8) Anthropic
* Scene C · Reveal (9..11) Claude Design
* Scene D · Skill (12..13) Skill
*
* LegalZoom -20% / CRCL -20% / CrowdStrike -7% / Figma -7%
*
*
* - "网页 chrome" / footer / hint
* - SceneFade bug
* - step 1
*/
const VICTIMS = [
{ product: 'Claude Cowork', company: 'LegalZoom', ticker: 'LZ', drop: -20.0, field: '法律服务' },
{ product: 'Claude Code Security', company: 'Circle Internet', ticker: 'CRCL', drop: -20.0, field: '云安全' },
{ product: 'Claude Mythos', company: 'CrowdStrike', ticker: 'CRWD', drop: -7.0, field: '终端安全' },
{ product: 'Claude Design', company: 'Figma', ticker: 'FIG', drop: -7.0, field: '设计协作', current: true },
];
function Opening({ localStep }: ChapterContext) {
const at = (n: number) => localStep >= n;
const between = (a: number, b: number) => localStep >= a && localStep <= b;
const sceneCrash = localStep <= 3;
const sceneVictims = between(4, 8);
const sceneReveal = between(9, 11);
const sceneSkill = at(12);
return (
<section className="opening">
{/* ============== SCENE A · Crash (0..3) ============== */}
<SceneFade active={sceneCrash}>
<div className="opening__sceneA">
{/* LIVE 报价条 —— 仅在崩盘场景中作为"市场环境"出现 */}
<div className="opening__live">
<span className="opening__live-dot" />
<span className="opening__live-label">NASDAQ · LIVE</span>
<LiveClock className="opening__live-clock" />
<span className="opening__live-sep">|</span>
<span className="opening__live-quote">
<span className="opening__live-quote-tag">FIG</span>
<FlickerNumber base={48.32} amplitude={0.18} className="opening__live-quote-val" />
<span className="opening__live-quote-d">7.0%</span>
</span>
</div>
{/* "FIGMA" 巨字背景 —— step 2 才显形 */}
{at(2) && (
<Reveal kind="blur" duration={1200} delay={120}>
<div className="opening__crash-mega">FIGMA</div>
</Reveal>
)}
{/* 引子小字 —— step 1 */}
{at(1) && (
<Reveal kind="rise" duration={620} className="opening__crash-intro">
<span className="opening__crash-intro-tag">2026 · 04 · 17 · NASDAQ CLOSE</span>
<span className="opening__crash-intro-text"> </span>
</Reveal>
)}
{/* 折线图 —— step 1 开始绘制历史 / step 2 崩盘 */}
<CrashChart phase={localStep} />
{/* 中文 headline —— step 2 */}
{at(2) && (
<Reveal kind="rise" duration={780} delay={620} className="opening__crash-headline">
<span className="opening__crash-line">Figma</span>
<em className="opening__crash-emph"></em>
</Reveal>
)}
{/* 7.0% 大数字 —— step 3 */}
{at(3) && (
<Reveal kind="rise" duration={520} className="opening__crash-stat">
<div className="opening__crash-stat-num">
<span className="opening__crash-stat-sign"></span>
<NumberTicker
to={7.0}
from={0}
duration={1200}
decimals={1}
delay={120}
/>
<span className="opening__crash-stat-pct">%</span>
</div>
<div className="opening__crash-stat-label"> · </div>
</Reveal>
)}
</div>
</SceneFade>
{/* ============== SCENE B · Victims (4..8) ============== */}
<SceneFade active={sceneVictims}>
<div className="opening__sceneB">
<div className="opening__vics-eyebrow">
<Reveal kind="rise" duration={680} className="opening__vics-eyebrow-inner">
<span></span>
<em className="opening__vics-em"> N </em>
<span> </span>
</Reveal>
<Reveal kind="fade" duration={520} delay={520} className="opening__vics-by">
<span className="opening__vics-by-tag">CRASHED · BY</span>
<span className="opening__vics-by-name">ANTHROPIC</span>
</Reveal>
</div>
<div className="opening__vics-table">
<div className="opening__vics-thead">
<span>ANTHROPIC PRODUCT</span>
<span>AFFECTED COMPANY</span>
<span>TICKER</span>
<span>SECTOR</span>
<span style={{ textAlign: 'right' }}>DAY DROP</span>
</div>
{VICTIMS.map((v, i) => {
const showAt = 5 + i;
if (!at(showAt)) return null;
return (
<Reveal
key={v.company}
kind="wipe-r"
duration={780}
delay={i === 0 ? 80 : 0}
>
<VictimRow v={v} animate={localStep === showAt} />
</Reveal>
);
})}
</div>
</div>
</SceneFade>
{/* ============== SCENE C · Claude Design 揭幕 (9..11) ============== */}
<SceneFade active={sceneReveal}>
<div className="opening__sceneC">
<Reveal kind="fall" duration={700} className="opening__reveal-meta">
<span>ANTHROPIC</span>
<span className="dot" />
<span>NEW PRODUCT</span>
<span className="dot" />
<span>2026 · 04 · 17</span>
</Reveal>
<h1 className="opening__reveal-title">
<Reveal
kind="blur"
duration={1100}
delay={120}
className="opening__reveal-word"
>
Claude
</Reveal>
{at(10) && (
<Reveal
kind="blur"
duration={1100}
className="opening__reveal-word opening__reveal-word--em"
>
Design
</Reveal>
)}
</h1>
{at(10) && (
<Reveal kind="rise" duration={720} delay={680} className="opening__reveal-sub">
<span className="opening__reveal-tilde"></span>
<span> Claude Code</span>
</Reveal>
)}
{at(11) && (
<div className="opening__reveal-tags">
<Reveal kind="rise" duration={520}>
<span>Powered by Opus 4.7</span>
</Reveal>
<Reveal kind="rise" duration={520} delay={120}>
<span>Pro · Max · Team · Enterprise</span>
</Reveal>
<Reveal kind="rise" duration={520} delay={240}>
<span> · </span>
</Reveal>
</div>
)}
</div>
</SceneFade>
{/* ============== SCENE D · Skill (12..13) ============== */}
<SceneFade active={sceneSkill}>
<div className="opening__sceneD">
<Reveal kind="rise" duration={620} className="opening__pivot-eyebrow">
<span className="opening__pivot-eyebrow-bar" />
<span> </span>
</Reveal>
<h2 className="opening__pivot-title">
<Reveal kind="blur" duration={900} delay={120}>
</Reveal>
<Reveal
kind="blur"
duration={900}
delay={520}
className="opening__pivot-em"
>
Skill
</Reveal>
</h2>
{at(13) && (
<div className="opening__pivot-row">
<Reveal kind="wipe-r" duration={900} delay={80}>
<SkillCard />
</Reveal>
<Reveal kind="rise" duration={720} delay={520} className="opening__pivot-aside">
<span className="opening__pivot-aside-q">"</span>
<span></span>
<em></em>
</Reveal>
</div>
)}
</div>
</SceneFade>
</section>
);
}
/* ────────────── 子组件 ────────────── */
function CrashChart({ phase }: { phase: number }) {
const showHistory = phase >= 1;
const showCrash = phase >= 2;
const showFill = phase >= 2;
const showEnd = phase >= 2;
return (
<svg
className="opening__chart"
viewBox="0 0 1760 540"
preserveAspectRatio="none"
aria-hidden
>
<defs>
<linearGradient id="crashGrad" x1="0" x2="0" y1="0" y2="1">
<stop offset="0%" stopColor="oklch(0.560 0.200 22)" stopOpacity="0.5" />
<stop offset="100%" stopColor="oklch(0.560 0.200 22)" stopOpacity="0" />
</linearGradient>
</defs>
<g className="opening__chart-grid">
{[0, 1, 2, 3].map((i) => (
<line key={i} x1="0" x2="1760" y1={120 + i * 110} y2={120 + i * 110} />
))}
</g>
{showHistory && (
<path
className="opening__chart-history"
d="M 0 290 L 110 285 L 220 296 L 330 282 L 440 290 L 550 276 L 660 286 L 770 270 L 880 280 L 990 264 L 1100 272 L 1210 258"
fill="none"
strokeWidth="2.5"
/>
)}
{showCrash && (
<path
className="opening__chart-crash"
d="M 1210 258 L 1290 320 L 1380 400 L 1500 470 L 1760 510"
fill="none"
strokeWidth="3"
/>
)}
{showFill && (
<path
className="opening__chart-fill"
d="M 1210 258 L 1290 320 L 1380 400 L 1500 470 L 1760 510 L 1760 540 L 1210 540 Z"
fill="url(#crashGrad)"
/>
)}
{showEnd && (
<g className="opening__chart-end">
<line x1="0" y1="258" x2="1760" y2="258" strokeDasharray="2 8" strokeWidth="1" />
<circle cx="1760" cy="510" r="6" />
<text x="1740" y="245" textAnchor="end"> 51.95</text>
<text x="1740" y="495" textAnchor="end"> 48.32</text>
</g>
)}
</svg>
);
}
interface VictimRowProps {
v: typeof VICTIMS[number];
animate: boolean;
}
function VictimRow({ v, animate }: VictimRowProps) {
return (
<div className={`opening__vic ${v.current ? 'is-current' : ''}`}>
<span className="opening__vic-product">{v.product}</span>
<span className="opening__vic-company">{v.company}</span>
<span className="opening__vic-ticker">${v.ticker}</span>
<span className="opening__vic-field">{v.field}</span>
<span className="opening__vic-drop">
{animate ? (
<NumberTicker
to={Math.abs(v.drop)}
decimals={1}
duration={900}
delay={220}
prefix=""
suffix="%"
/>
) : (
<span style={{ fontVariantNumeric: 'tabular-nums' }}>
{Math.abs(v.drop).toFixed(1)}%
</span>
)}
</span>
{v.current && <span className="opening__vic-flag">CURRENT</span>}
</div>
);
}
function SkillCard() {
return (
<div className="opening__skill-card">
<div className="opening__skill-card-deco" />
<div className="opening__skill-card-head">
<span className="opening__skill-card-tag">SKILL · v1</span>
<span className="opening__skill-card-name">web-design-engineer</span>
</div>
<div className="opening__skill-card-body">
<div className="opening__skill-card-row">
<span className="opening__skill-card-k"></span>
<span className="opening__skill-card-v">Cursor · Claude Code · Codex</span>
</div>
<div className="opening__skill-card-row">
<span className="opening__skill-card-k"></span>
<span className="opening__skill-card-v"> 400 · 520 </span>
</div>
<div className="opening__skill-card-row">
<span className="opening__skill-card-k"></span>
<span className="opening__skill-card-v">Claude Design · + </span>
</div>
<div className="opening__skill-card-row">
<span className="opening__skill-card-k"></span>
<span className="opening__skill-card-v">85 95 </span>
</div>
</div>
<div className="opening__skill-card-foot">
<span className="opening__skill-card-pulse" />
<span>READY · open-source</span>
</div>
</div>
);
}
const def: ChapterDef = {
id: 'opening',
title: '开场 · 一则崩盘',
eyebrow: '01',
steps: 14,
theme: 'ink',
Component: Opening,
};
export default def;

View File

@ -0,0 +1,183 @@
/* =========================================================
Chapter 02 · Anthropic 官方宣传片
light 主题 · 米色纸感底 · 中央虚拟显示器外框
========================================================= */
.vid {
position: absolute;
inset: 0;
background: var(--bg);
color: var(--fg);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 100px 80px;
font-family: var(--f-sans);
overflow: hidden;
}
/* TV 外壳 */
.vid__tv-wrap {
display: flex;
flex-direction: column;
align-items: center;
}
.vid__tv {
position: relative;
width: 1280px;
background: linear-gradient(
180deg,
oklch(0.245 0.015 60) 0%,
oklch(0.190 0.014 60) 100%
);
border-radius: 18px;
padding: 18px 22px 22px;
box-shadow:
0 60px 80px -40px oklch(0.180 0.014 60 / 0.35),
0 24px 36px -20px oklch(0.180 0.014 60 / 0.20),
inset 0 1px 0 oklch(0.965 0.018 78 / 0.08),
inset 0 -1px 0 oklch(0.180 0.014 60 / 0.6);
}
/* 角落螺丝 */
.vid__screw {
position: absolute;
width: 8px;
height: 8px;
border-radius: 50%;
background: radial-gradient(
circle at 35% 35%,
oklch(0.700 0.012 60) 0%,
oklch(0.380 0.012 60) 70%,
oklch(0.180 0.012 60) 100%
);
box-shadow: inset 0 0 1px oklch(0.180 0.012 60);
}
.vid__screw--tl { top: 12px; left: 12px; }
.vid__screw--tr { top: 12px; right: 12px; }
.vid__screw--bl { bottom: 14px; left: 12px; }
.vid__screw--br { bottom: 14px; right: 12px; }
/* 顶部状态条 */
.vid__topstrip {
display: flex;
align-items: center;
gap: 12px;
padding: 0 18px 12px;
font-family: var(--f-mono);
font-size: 11px;
letter-spacing: 0.28em;
color: oklch(0.620 0.020 60);
text-transform: uppercase;
}
.vid__led {
width: 7px;
height: 7px;
border-radius: 50%;
background: oklch(0.78 0.18 145);
box-shadow: 0 0 8px oklch(0.78 0.18 145 / 0.7);
animation: vidLedPulse 1.4s ease-in-out infinite;
}
@keyframes vidLedPulse {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
.vid__topstrip-spacer {
flex: 1;
}
/* 屏幕(视频容器) */
.vid__screen {
position: relative;
aspect-ratio: 16 / 9;
background: #000;
border-radius: 6px;
overflow: hidden;
box-shadow:
inset 0 0 0 1px oklch(0.180 0.014 60),
inset 0 0 24px oklch(0.180 0.014 60 / 0.6);
}
.vid__video {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
background: #000;
}
/* 微弱扫描线 */
.vid__scanlines {
position: absolute;
inset: 0;
pointer-events: none;
z-index: 2;
background-image: repeating-linear-gradient(
180deg,
oklch(0.965 0.018 78 / 0) 0px,
oklch(0.965 0.018 78 / 0) 2px,
oklch(0.965 0.018 78 / 0.025) 3px,
oklch(0.965 0.018 78 / 0) 4px
);
mix-blend-mode: overlay;
opacity: 0.6;
}
/* 底部品牌条 */
.vid__brandstrip {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 14px 18px 4px;
font-family: var(--f-mono);
color: oklch(0.620 0.020 60);
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.18em;
}
.vid__brand-mark {
width: 14px;
height: 14px;
background: var(--accent);
clip-path: polygon(50% 0, 100% 100%, 0 100%);
}
.vid__brand-name {
font-weight: 600;
color: oklch(0.880 0.020 78);
letter-spacing: 0.22em;
}
/* 底座 */
.vid__stand {
display: flex;
flex-direction: column;
align-items: center;
margin-top: -2px;
}
.vid__stand-neck {
width: 200px;
height: 24px;
background: linear-gradient(
180deg,
oklch(0.220 0.015 60) 0%,
oklch(0.180 0.014 60) 100%
);
clip-path: polygon(8% 0, 92% 0, 100% 100%, 0 100%);
box-shadow: 0 4px 8px -2px oklch(0.180 0.014 60 / 0.3);
}
.vid__stand-base {
width: 340px;
height: 8px;
background: linear-gradient(
180deg,
oklch(0.230 0.015 60) 0%,
oklch(0.170 0.014 60) 100%
);
border-radius: 4px;
box-shadow:
0 12px 24px -8px oklch(0.180 0.014 60 / 0.5),
0 4px 6px -1px oklch(0.180 0.014 60 / 0.3);
}

View File

@ -0,0 +1,90 @@
import { useEffect, useRef } from 'react';
import type { ChapterContext, ChapterDef } from '../types';
import { Reveal } from '../../shared/Reveal';
import './Video.css';
/**
* Chapter 02 · Anthropic
*
* + + 16:9 /video.mp4
*
* / eyebrow / caption
* - controls /
* - TV + data-no-step controls
* - TV Ch03
*/
function VideoChapter(_: ChapterContext) {
const videoRef = useRef<HTMLVideoElement>(null);
useEffect(() => {
const v = videoRef.current;
if (!v) return;
v.currentTime = 0;
const play = v.play();
if (play && typeof play.catch === 'function') {
play.catch(() => {/* 用户必须自行点击播放 */});
}
return () => {
v.pause();
};
}, []);
return (
<section className="vid">
<Reveal kind="rise" duration={900} delay={120} className="vid__tv-wrap">
<div className="vid__tv" data-no-step>
{/* 角注 4 颗螺丝 */}
<span className="vid__screw vid__screw--tl" />
<span className="vid__screw vid__screw--tr" />
<span className="vid__screw vid__screw--bl" />
<span className="vid__screw vid__screw--br" />
{/* 顶部状态条 */}
<div className="vid__topstrip">
<span className="vid__led" />
<span>ON · CH · 02</span>
<span className="vid__topstrip-spacer" />
<span>SIGNAL · STABLE</span>
</div>
{/* 屏幕 */}
<div className="vid__screen">
<div className="vid__scanlines" aria-hidden />
<video
ref={videoRef}
src="/video.mp4"
className="vid__video"
controls
playsInline
muted
autoPlay
/>
</div>
{/* 底部品牌条 */}
<div className="vid__brandstrip">
<span className="vid__brand-mark" />
<span className="vid__brand-name">ANTHROPIC</span>
</div>
</div>
{/* 底座 */}
<div className="vid__stand" data-no-step>
<span className="vid__stand-neck" />
<span className="vid__stand-base" />
</div>
</Reveal>
</section>
);
}
const def: ChapterDef = {
id: 'video',
title: '官方宣传片',
eyebrow: '02',
steps: 1,
theme: 'light',
Component: VideoChapter,
};
export default def;

View File

@ -0,0 +1,480 @@
/* =========================================================
Chapter 03 · 核心观点
ink 主题 · hero 50/50 split 提示词倾泻 leaked pivot
========================================================= */
.cp {
position: absolute;
inset: 0;
background: var(--bg);
color: var(--fg);
font-family: var(--f-sans);
overflow: hidden;
}
/* 背景网格氛围 */
.cp__grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(to right, var(--line) 0, var(--line) 1px, transparent 1px),
linear-gradient(to bottom, var(--line) 0, var(--line) 1px, transparent 1px);
background-size: 80px 80px;
mask-image: radial-gradient(circle at 50% 50%, #000 0%, #000 60%, transparent 95%);
pointer-events: none;
opacity: 0.5;
}
/* 角落坐标十字 */
.cp__cornerTL,
.cp__cornerBR {
position: absolute;
width: 28px;
height: 28px;
pointer-events: none;
}
.cp__cornerTL { top: 56px; left: 64px; }
.cp__cornerBR { bottom: 56px; right: 64px; }
.cp__cornerTL span,
.cp__cornerBR span {
position: absolute;
background: var(--fg-faint);
}
.cp__cornerTL span:nth-child(1),
.cp__cornerBR span:nth-child(1) { top: 0; left: 50%; width: 1px; height: 100%; transform: translateX(-50%); }
.cp__cornerTL span:nth-child(2),
.cp__cornerBR span:nth-child(2) { left: 0; top: 50%; width: 100%; height: 1px; transform: translateY(-50%); }
/* ===================== Scene HERO ===================== */
.cp__hero {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120px 140px;
text-align: center;
}
.cp__hero-eyebrow {
display: flex;
align-items: center;
gap: 22px;
font-family: var(--f-mono);
font-size: 18px;
letter-spacing: 0.4em;
text-transform: uppercase;
color: var(--fg-mute);
margin-bottom: 64px;
}
.cp__hero-eyebrow-bar {
width: 64px;
height: 1px;
background: var(--line-mid);
}
.cp__hero-title {
font-family: var(--f-serif);
font-weight: 400;
font-size: 168px;
line-height: 1.02;
letter-spacing: -0.02em;
color: var(--fg);
margin: 0;
max-width: 1500px;
}
.cp__hero-em {
font-style: italic;
color: var(--accent);
position: relative;
}
.cp__hero-em::after {
content: '';
position: absolute;
left: 4%;
right: 4%;
bottom: 8px;
height: 4px;
background: var(--accent);
opacity: 0.25;
transform-origin: left;
animation: cpUnderline 1100ms 320ms cubic-bezier(.2,.8,.2,1) backwards;
}
@keyframes cpUnderline {
from { transform: scaleX(0); opacity: 0; }
to { transform: scaleX(1); opacity: 0.25; }
}
/* ===================== Scene SPLIT ===================== */
.cp__split {
position: absolute;
inset: 0;
padding: 110px 100px 100px;
display: flex;
flex-direction: column;
align-items: center;
}
.cp__split-eyebrow {
display: flex;
align-items: center;
gap: 18px;
font-family: var(--f-mono);
font-size: 18px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--fg-mute);
margin-bottom: 28px;
}
.cp__split-eyebrow-arrow {
color: var(--accent);
font-family: var(--f-sans);
letter-spacing: 0;
}
/* 分隔线(中央) */
.cp__split-divider {
position: absolute;
top: 220px;
bottom: 200px;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
pointer-events: none;
}
.cp__split-divider-line {
width: 1px;
flex: 1;
background: linear-gradient(
to bottom,
transparent,
var(--line-mid) 20%,
var(--line-mid) 80%,
transparent
);
}
.cp__split-divider-knob {
width: 36px;
height: 36px;
border: 1px solid var(--line-strong);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--f-serif);
font-size: 28px;
color: var(--accent);
background: var(--bg);
margin: 14px 0;
}
/* 两栏 */
.cp__columns {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 120px;
width: 100%;
max-width: 1640px;
margin-top: 24px;
flex: 1;
}
.cp__col {
display: flex;
flex-direction: column;
padding: 8px 8px 0;
}
.cp__col--left { padding-right: 80px; }
.cp__col--right { padding-left: 80px; }
.cp__col-pct {
display: flex;
align-items: baseline;
gap: 6px;
font-family: var(--f-serif);
font-weight: 400;
font-size: 200px;
line-height: 1;
color: var(--fg);
letter-spacing: -0.04em;
margin-bottom: 12px;
}
.cp__col--right .cp__col-pct {
color: var(--accent);
}
.cp__col-pct-sign {
font-family: var(--f-serif);
font-size: 100px;
color: var(--fg-mute);
}
.cp__col--right .cp__col-pct-sign {
color: var(--accent);
opacity: 0.7;
}
.cp__col-kicker {
font-family: var(--f-mono);
font-size: 16px;
letter-spacing: 0.34em;
text-transform: uppercase;
color: var(--fg-mute);
margin-bottom: 14px;
}
.cp__col-title {
font-family: var(--f-serif);
font-weight: 400;
font-size: 86px;
line-height: 1;
margin: 0 0 22px;
color: var(--fg);
}
.cp__col-desc {
font-family: var(--f-sans);
font-size: 26px;
line-height: 1.5;
color: var(--fg-soft);
margin: 0 0 28px;
max-width: 540px;
}
/* 左:进度计 */
.cp__col-meter {
position: relative;
height: 32px;
margin-bottom: 28px;
max-width: 540px;
}
.cp__col-meter-bar {
position: absolute;
inset: 0;
background: linear-gradient(
to right,
var(--accent),
var(--accent-deep)
);
height: 4px;
top: 14px;
border-radius: 1px;
transition: width 1200ms cubic-bezier(.2,.8,.2,1) 600ms;
}
.cp__col-meter-ticks {
position: absolute;
inset: 0;
display: flex;
justify-content: space-between;
align-items: center;
}
.cp__col-meter-ticks span {
width: 1px;
height: 12px;
background: var(--line-mid);
}
.cp__col-tags {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.cp__col-tags span {
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--fg-mute);
border: 1px solid var(--line-mid);
padding: 6px 12px;
border-radius: var(--r-pill);
}
/* 右:文档预览 */
.cp__doc {
margin-top: 4px;
max-width: 620px;
background: oklch(0.330 0.014 60);
border: 1px solid var(--line-mid);
border-radius: 4px;
overflow: hidden;
box-shadow:
0 32px 60px -32px oklch(0 0 0 / 0.4),
inset 0 1px 0 oklch(0.965 0.018 78 / 0.04);
}
.cp__doc-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
background: oklch(0.380 0.014 60);
border-bottom: 1px solid var(--line-mid);
font-family: var(--f-mono);
font-size: 13px;
letter-spacing: 0.06em;
color: var(--fg-mute);
}
.cp__doc-bar-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: oklch(0.420 0.020 60);
}
.cp__doc-bar-name {
margin-left: 8px;
}
.cp__doc-body {
padding: 14px 18px 22px;
min-height: 280px;
font-family: var(--f-mono);
font-size: 15px;
line-height: 1.65;
color: var(--fg-soft);
position: relative;
}
.cp__doc-line {
display: flex;
gap: 16px;
white-space: pre;
align-items: baseline;
}
.cp__doc-line-no {
width: 22px;
flex: none;
color: var(--fg-faint);
text-align: right;
}
.cp__doc-line-text {
color: var(--fg-soft);
}
.cp__doc-cursor {
display: inline-block;
margin-top: 6px;
margin-left: 38px;
color: var(--accent);
animation: cpCursor 1.1s steps(1) infinite;
}
@keyframes cpCursor {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0; }
}
/* leaked 徽章 */
.cp__leaked {
position: absolute;
bottom: 130px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 28px;
padding: 20px 32px;
background: oklch(0.340 0.014 60);
border: 1px solid var(--line-strong);
border-radius: 4px;
box-shadow: 0 24px 48px -20px oklch(0 0 0 / 0.45);
max-width: 920px;
}
.cp__leaked-stamp {
display: flex;
align-items: center;
gap: 10px;
font-family: var(--f-mono);
font-size: 22px;
letter-spacing: 0.32em;
color: var(--crimson);
font-weight: 600;
border: 2px solid var(--crimson);
padding: 10px 16px 8px;
border-radius: 2px;
transform: rotate(-3deg);
}
.cp__leaked-stamp-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--crimson);
box-shadow: 0 0 8px var(--crimson);
animation: cpStampPulse 1.2s ease-in-out infinite;
}
@keyframes cpStampPulse {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
.cp__leaked-meta {
display: flex;
align-items: center;
gap: 28px;
}
.cp__leaked-meta-time {
display: flex;
align-items: baseline;
gap: 6px;
font-family: var(--f-serif);
font-size: 72px;
line-height: 1;
color: var(--fg);
}
.cp__leaked-meta-lt {
font-family: var(--f-serif);
font-size: 56px;
color: var(--fg-mute);
margin-right: 4px;
}
.cp__leaked-meta-unit {
font-family: var(--f-mono);
font-size: 18px;
letter-spacing: 0.28em;
color: var(--fg-mute);
margin-left: 6px;
}
.cp__leaked-meta-text {
font-family: var(--f-sans);
font-size: 22px;
line-height: 1.45;
color: var(--fg-soft);
}
/* pivot 转场指引 */
.cp__pivot {
position: absolute;
bottom: 56px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 14px;
font-family: var(--f-mono);
font-size: 18px;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--accent);
}
.cp__pivot-arrow {
width: 56px;
height: 1px;
background: var(--accent);
position: relative;
}
.cp__pivot-arrow::after {
content: '';
position: absolute;
right: -2px;
top: 50%;
width: 8px;
height: 8px;
border-right: 1px solid var(--accent);
border-bottom: 1px solid var(--accent);
transform: translateY(-50%) rotate(-45deg);
}
.cp__pivot-text {
animation: cpPivotBlink 2.4s ease-in-out infinite;
}
@keyframes cpPivotBlink {
0%, 100% { opacity: 0.65; }
50% { opacity: 1; }
}

View File

@ -0,0 +1,204 @@
import type { ChapterContext, ChapterDef } from '../types';
import { Reveal } from '../../shared/Reveal';
import { SceneFade } from '../../shared/SceneFade';
import { NumberTicker } from '../../shared/NumberTicker';
import './CorePoint.css';
/**
* Chapter 03 ·
*
*
* Claude Design Opus 4.7
* 线 24
*
* 6 / step 0..5
* 0 + eyebrow
* 1 hero "Claude Design · 为什么这么强?"
* 2 50/50 OPUS 4.7 / SYSTEM PROMPT
* 3 "提示词从天而降"
* 4 leaked "< 24 HOURS · LEAKED"
* 5 "下面,我们逐条拆解 ↓"
*/
const PROMPT_LINES: string[] = [
'You are an expert designer working with the user as a manager.',
'You produce design artifacts on behalf of the user using HTML.',
'HTML is your tool, but your medium and output format vary.',
'You must embody an expert in that domain:',
' animator, UX designer, slide designer, prototyper, etc.',
'Avoid web design tropes and conventions',
' unless you are making a web page.',
'## Your workflow',
'1. Understand user needs. Ask clarifying questions ...',
'2. Explore provided resources. Read the design system ...',
];
function CorePoint({ localStep }: ChapterContext) {
const at = (n: number) => localStep >= n;
// 场景1+ 进入分析2 之前是单独 hero 居中
const sceneHero = localStep <= 1;
const sceneSplit = localStep >= 2;
return (
<section className="cp">
{/* 装饰性背景网格 + 角落坐标 */}
<div className="cp__grid" aria-hidden />
<div className="cp__cornerTL" aria-hidden>
<span /><span />
</div>
<div className="cp__cornerBR" aria-hidden>
<span /><span />
</div>
{/* ───────── Scene HEROstep 0..1)───────── */}
<SceneFade active={sceneHero} exitMs={420} enterDelayMs={120}>
<div className="cp__hero">
<Reveal kind="fade" duration={700} delay={120} className="cp__hero-eyebrow">
<span className="cp__hero-eyebrow-bar" />
<span>03 · </span>
<span className="cp__hero-eyebrow-bar" />
</Reveal>
{at(1) && (
<Reveal kind="rise" duration={1100} delay={80} className="cp__hero-title" as="h1">
Claude Design<br />
<em className="cp__hero-em"></em>
</Reveal>
)}
</div>
</SceneFade>
{/* ───────── Scene SPLITstep 2..5)───────── */}
<SceneFade active={sceneSplit} exitMs={420} enterDelayMs={420}>
<div className="cp__split">
{/* 顶部留下问句的小回响(不再是大字) */}
<Reveal kind="fade" duration={620} delay={120} className="cp__split-eyebrow">
<span></span>
<span className="cp__split-eyebrow-arrow"></span>
<span></span>
</Reveal>
{/* 中央分隔线 */}
<Reveal kind="fade" duration={900} delay={300} className="cp__split-divider">
<span className="cp__split-divider-line" />
<span className="cp__split-divider-knob">+</span>
<span className="cp__split-divider-line" />
</Reveal>
<div className="cp__columns">
{/* —— 左OPUS 4.7 —— */}
<Reveal kind="rise" duration={900} delay={420} className="cp__col cp__col--left">
<div className="cp__col-pct">
<NumberTicker to={50} duration={1100} decimals={0} />
<span className="cp__col-pct-sign">%</span>
</div>
<div className="cp__col-kicker">MODEL</div>
<h2 className="cp__col-title">Opus 4.7</h2>
<p className="cp__col-desc">
Anthropic <br />
</p>
<div className="cp__col-meter">
<div className="cp__col-meter-bar" style={{ width: at(2) ? '50%' : '0%' }} />
<div className="cp__col-meter-ticks">
<span /><span /><span /><span /><span />
</div>
</div>
<div className="cp__col-tags">
<span>reasoning</span>
<span>taste</span>
<span>code</span>
</div>
</Reveal>
{/* —— 右SYSTEM PROMPT —— */}
<Reveal kind="rise" duration={900} delay={560} className="cp__col cp__col--right">
<div className="cp__col-pct">
<NumberTicker to={50} duration={1100} delay={140} decimals={0} />
<span className="cp__col-pct-sign">%</span>
</div>
<div className="cp__col-kicker">SYSTEM PROMPT</div>
<h2 className="cp__col-title"></h2>
<p className="cp__col-desc">
~420 system prompt <br />
"角色 / 流程 / 边界 / 品味"
</p>
{/* 文档预览 */}
<div className="cp__doc">
<div className="cp__doc-bar">
<span className="cp__doc-bar-dot" />
<span className="cp__doc-bar-dot" />
<span className="cp__doc-bar-dot" />
<span className="cp__doc-bar-name">claude-design.system.md</span>
</div>
<div className="cp__doc-body">
{at(3) && PROMPT_LINES.map((line, i) => (
<Reveal
key={`pl-${i}-${localStep}`}
kind="fall"
duration={520}
delay={i * 90}
className="cp__doc-line"
>
<span className="cp__doc-line-no">{String(i + 1).padStart(2, '0')}</span>
<span className="cp__doc-line-text">{line}</span>
</Reveal>
))}
{at(3) && (
<Reveal kind="fade" duration={400} delay={PROMPT_LINES.length * 90 + 200}>
<span className="cp__doc-cursor"></span>
</Reveal>
)}
</div>
</div>
</Reveal>
</div>
{/* leaked 徽章 */}
{at(4) && (
<Reveal kind="rise" duration={780} className="cp__leaked">
<div className="cp__leaked-stamp">
<span className="cp__leaked-stamp-dot" />
LEAKED
</div>
<div className="cp__leaked-meta">
<div className="cp__leaked-meta-time">
<span className="cp__leaked-meta-lt">&lt;</span>
<NumberTicker to={24} duration={900} decimals={0} />
<span className="cp__leaked-meta-unit">HOURS</span>
</div>
<div className="cp__leaked-meta-text">
线 24 <br />
/ 广
</div>
</div>
</Reveal>
)}
{/* 转场指引 */}
{at(5) && (
<Reveal kind="rise" duration={760} delay={120} className="cp__pivot">
<span className="cp__pivot-arrow" />
<span className="cp__pivot-text"></span>
</Reveal>
)}
</div>
</SceneFade>
</section>
);
}
const def: ChapterDef = {
id: 'core-point',
title: '核心观点',
eyebrow: '03',
steps: 6,
theme: 'ink',
Component: CorePoint,
};
export default def;

View File

@ -0,0 +1,641 @@
/* =========================================================
Chapter 04 · 角色定位
light 主题 · 引文 高亮 benefit 名片翻 全卡 + 收尾
========================================================= */
.role {
position: absolute;
inset: 0;
background: var(--bg);
color: var(--fg);
font-family: var(--f-sans);
overflow: hidden;
}
/* 顶部 eyebrow 贯穿全章节 */
.role__eyebrow {
position: absolute;
top: 56px;
left: 96px;
display: flex;
align-items: center;
gap: 18px;
font-family: var(--f-mono);
font-size: 16px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--fg-mute);
z-index: 4;
}
.role__eyebrow-num {
font-family: var(--f-serif);
font-size: 28px;
letter-spacing: 0;
color: var(--accent);
font-style: italic;
}
.role__eyebrow-bar {
width: 36px;
height: 1px;
background: var(--line-mid);
}
.role__eyebrow-bar--long { width: 96px; }
.role__eyebrow-text { color: var(--fg); letter-spacing: 0.18em; }
.role__eyebrow-mute { color: var(--fg-faint); font-size: 13px; }
/* ===================== Scene QUOTE ===================== */
.role__quote-scene {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 140px 140px 120px;
text-align: center;
}
/* ===== prompt 源标签mono 小字) ===== */
.role__src {
display: inline-flex;
align-items: center;
gap: 8px;
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--fg-mute);
margin-bottom: 28px;
padding: 6px 12px;
border: 1px solid var(--line-mid);
border-radius: 2px;
background: var(--bg-2);
}
.role__src-bracket {
color: var(--accent);
font-weight: 600;
}
.role__src-label {
color: var(--fg);
font-weight: 600;
letter-spacing: 0.22em;
}
.role__src-sep {
color: var(--fg-faint);
}
.role__src-line {
color: var(--accent);
font-weight: 600;
}
.role__src-mute {
color: var(--fg-mute);
}
.role__quote-en {
font-family: var(--f-serif);
font-style: italic;
font-weight: 400;
font-size: 96px;
line-height: 1.18;
color: var(--fg);
margin: 0 0 48px;
max-width: 1500px;
letter-spacing: -0.01em;
}
.role__quote-marks {
font-family: var(--f-serif);
font-style: italic;
color: var(--fg-faint);
font-size: 110px;
line-height: 0;
vertical-align: -8px;
margin: 0 4px;
}
.role__quote-keyword {
position: relative;
font-style: italic;
color: var(--fg);
display: inline-block;
padding: 0 4px;
transition: color 360ms var(--ease-enter);
}
.role__quote-keyword.is-marked {
color: var(--accent);
}
.role__quote-keyword .role__quote-mark {
position: absolute;
left: -10%;
right: -10%;
bottom: 18px;
height: 3px;
background: var(--accent);
transform: scaleX(0);
transform-origin: left;
opacity: 0;
}
.role__quote-keyword.is-marked .role__quote-mark {
animation: roleUnderline 720ms cubic-bezier(.2,.8,.2,1) forwards;
}
.role__quote-keyword--alt.is-marked .role__quote-mark {
background: var(--accent-deep);
animation-delay: 220ms;
}
@keyframes roleUnderline {
from { transform: scaleX(0); opacity: 0; }
to { transform: scaleX(1); opacity: 0.7; }
}
.role__quote-cn {
font-family: var(--f-serif);
font-weight: 400;
font-size: 38px;
line-height: 1.6;
color: var(--fg-soft);
margin: 0 0 36px;
letter-spacing: 0.04em;
}
.role__quote-cn-key {
position: relative;
padding: 0 6px;
transition: color 360ms;
}
.role__quote-cn-key.is-marked {
color: var(--accent);
background: linear-gradient(180deg, transparent 60%, oklch(0.860 0.060 60 / 0.6) 60%);
}
.role__quote-cn-key--alt.is-marked {
color: var(--accent-deep);
background: linear-gradient(180deg, transparent 60%, oklch(0.860 0.060 60 / 0.6) 60%);
}
.role__quote-note {
font-family: var(--f-mono);
font-size: 18px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--fg-mute);
margin-bottom: 56px;
}
.role__quote-note u {
color: var(--crimson);
text-decoration-thickness: 2px;
text-underline-offset: 4px;
}
.role__benefits {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 48px;
width: 100%;
max-width: 1280px;
margin-top: 8px;
}
.role__benefit {
position: relative;
padding: 28px 32px 32px;
background: var(--bg-2);
border: 1px solid var(--line-mid);
border-radius: 6px;
text-align: left;
box-shadow: 0 24px 40px -28px oklch(0.180 0.014 60 / 0.4);
}
.role__benefit-num {
position: absolute;
top: -22px;
left: 28px;
width: 44px;
height: 44px;
background: var(--accent);
color: var(--paper);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--f-serif);
font-size: 28px;
font-style: italic;
}
.role__benefit-title {
font-family: var(--f-serif);
font-size: 44px;
line-height: 1.1;
color: var(--fg);
margin: 8px 0 14px;
}
.role__benefit-desc {
font-family: var(--f-sans);
font-size: 22px;
line-height: 1.55;
color: var(--fg-soft);
}
/* ===================== Scene FLIP ===================== */
.role__flip-scene {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 140px 140px 80px;
gap: 32px;
}
/* ===== 原文引用块prompt 风) ===== */
.role__excerpt {
width: 100%;
max-width: 1280px;
background: var(--bg-2);
border: 1px solid var(--line-mid);
border-left: 3px solid var(--accent);
border-radius: 2px;
padding: 22px 32px 26px;
text-align: left;
box-shadow: 0 18px 32px -22px oklch(0.180 0.014 60 / 0.30);
}
.role__excerpt-head {
display: flex;
align-items: center;
gap: 8px;
font-family: var(--f-mono);
font-size: 13px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--fg-mute);
margin-bottom: 14px;
padding-bottom: 12px;
border-bottom: 1px dashed var(--line-mid);
}
.role__excerpt-body {
display: flex;
align-items: flex-start;
gap: 18px;
}
.role__excerpt-gt {
font-family: var(--f-mono);
font-size: 38px;
line-height: 1.2;
color: var(--accent);
flex: none;
}
.role__excerpt-text {
font-family: var(--f-mono);
font-size: 26px;
line-height: 1.55;
color: var(--fg);
flex: 1;
}
.role__excerpt-em {
font-style: normal;
color: var(--accent);
background: linear-gradient(180deg, transparent 70%, oklch(0.860 0.060 60 / 0.5) 70%);
padding: 0 2px;
}
.role__excerpt-list {
font-style: italic;
color: var(--accent-deep);
}
.role__flip-eyebrow {
display: flex;
align-items: baseline;
gap: 16px;
font-family: var(--f-serif);
font-size: 32px;
color: var(--fg-soft);
margin: 4px 0 0;
}
.role__flip-eyebrow-arrow {
color: var(--fg-mute);
font-family: var(--f-sans);
font-size: 28px;
}
.role__flip-eyebrow-em {
font-style: italic;
color: var(--accent);
font-size: 42px;
position: relative;
}
.role__flip-eyebrow-em::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 4px;
height: 3px;
background: var(--accent);
opacity: 0.3;
transform-origin: left;
animation: roleUnderline 800ms 220ms cubic-bezier(.2,.8,.2,1) backwards;
}
/* 翻牌窗3 张卡片层叠translateY 控制可见状态 */
.role__flip-window {
position: relative;
width: 720px;
height: 380px;
perspective: 1600px;
}
.role__flip-slot {
position: absolute;
inset: 0;
transition:
transform 760ms cubic-bezier(.2,.8,.2,1),
opacity 600ms cubic-bezier(.2,.8,.2,1),
filter 600ms;
transform-origin: 50% 50% -200px;
will-change: transform, opacity;
}
.role__flip-slot[data-state="prev"] {
transform: translateY(-30%) rotateX(50deg);
opacity: 0;
pointer-events: none;
filter: blur(4px);
}
.role__flip-slot[data-state="current"] {
transform: translateY(0) rotateX(0deg);
opacity: 1;
filter: blur(0);
}
.role__flip-slot[data-state="next"] {
transform: translateY(30%) rotateX(-50deg);
opacity: 0;
pointer-events: none;
filter: blur(4px);
}
/* 进度刻度(步进) */
.role__flip-ticks {
display: flex;
gap: 64px;
margin-top: 8px;
}
.role__flip-tick {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
opacity: 0.32;
transition: opacity 380ms, transform 380ms;
}
.role__flip-tick.is-active {
opacity: 1;
transform: translateY(-2px);
}
.role__flip-tick-num {
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.32em;
color: var(--fg-mute);
}
.role__flip-tick.is-active .role__flip-tick-num {
color: var(--accent);
}
.role__flip-tick-name {
font-family: var(--f-serif);
font-size: 22px;
color: var(--fg);
}
.role__flip-tick.is-active .role__flip-tick-name {
color: var(--accent);
font-style: italic;
}
/* ===================== Card 视觉 ===================== */
.role__card {
position: relative;
width: 100%;
height: 100%;
background: var(--paper);
border: 1px solid var(--line-mid);
box-shadow:
0 30px 50px -30px oklch(0.180 0.014 60 / 0.35),
0 12px 18px -10px oklch(0.180 0.014 60 / 0.18),
inset 0 1px 0 oklch(0.965 0.018 78 / 1);
display: flex;
flex-direction: column;
overflow: hidden;
}
.role__card::before {
/* 纸感纹理 */
content: '';
position: absolute;
inset: 0;
background-image:
radial-gradient(circle at 20% 20%, oklch(0.180 0.014 60 / 0.04) 0, transparent 50%),
radial-gradient(circle at 80% 80%, oklch(0.180 0.014 60 / 0.04) 0, transparent 50%);
pointer-events: none;
}
.role__card--lg { padding: 32px 44px; }
.role__card--sm { padding: 22px 26px; }
.role__card-strip {
display: flex;
align-items: center;
gap: 12px;
font-family: var(--f-mono);
font-size: 13px;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--fg-mute);
padding-bottom: 18px;
border-bottom: 1px dashed var(--line-mid);
}
.role__card--sm .role__card-strip {
font-size: 11px;
padding-bottom: 12px;
}
.role__card-strip-dot {
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--fg-faint);
}
.role__card-body {
flex: 1;
display: grid;
grid-template-columns: auto 1fr;
gap: 28px;
align-items: center;
padding: 22px 0;
}
.role__card-meta {
width: 110px;
height: 110px;
border-radius: 6px;
background: oklch(0.928 0.024 78);
display: flex;
align-items: center;
justify-content: center;
color: var(--accent);
}
.role__card--sm .role__card-meta {
width: 64px;
height: 64px;
}
.role__icon {
width: 60px;
height: 60px;
}
.role__card--sm .role__icon {
width: 36px;
height: 36px;
}
.role__card-text {
display: flex;
flex-direction: column;
gap: 8px;
}
.role__card-en {
font-family: var(--f-serif);
font-style: italic;
font-size: 36px;
color: var(--fg-mute);
line-height: 1;
}
.role__card--sm .role__card-en {
font-size: 22px;
}
.role__card-cn {
font-family: var(--f-serif);
font-size: 64px;
line-height: 1;
color: var(--fg);
letter-spacing: 0.02em;
}
.role__card--sm .role__card-cn {
font-size: 36px;
}
.role__card-foot {
display: flex;
align-items: baseline;
gap: 12px;
padding-top: 18px;
border-top: 1px solid var(--line-mid);
}
.role__card--sm .role__card-foot {
padding-top: 12px;
}
.role__card-ctx {
font-family: var(--f-serif);
font-style: italic;
font-size: 28px;
color: var(--accent);
}
.role__card--sm .role__card-ctx {
font-size: 18px;
}
.role__card-ctx-en {
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--fg-faint);
}
.role__card--sm .role__card-ctx-en {
font-size: 11px;
}
/* 角注 */
.role__card-corner {
position: absolute;
width: 12px;
height: 12px;
border: 1px solid var(--fg-faint);
}
.role__card-corner--tl { top: 8px; left: 8px; border-right: 0; border-bottom: 0; }
.role__card-corner--tr { top: 8px; right: 8px; border-left: 0; border-bottom: 0; }
.role__card-corner--bl { bottom: 8px; left: 8px; border-right: 0; border-top: 0; }
.role__card-corner--br { bottom: 8px; right: 8px; border-left: 0; border-top: 0; }
/* ===================== Scene ALL ===================== */
.role__all-scene {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 140px 100px 120px;
}
.role__all-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 36px;
width: 100%;
max-width: 1480px;
margin-bottom: 64px;
}
.role__all-slot {
height: 280px;
animation: roleAllRise 720ms cubic-bezier(.2,.8,.2,1) backwards;
}
.role__all-slot:nth-child(1) { transform: rotate(-1.5deg); }
.role__all-slot:nth-child(2) { transform: rotate(0.5deg); }
.role__all-slot:nth-child(3) { transform: rotate(-0.8deg); }
@keyframes roleAllRise {
from { opacity: 0; transform: translateY(28px) rotate(-3deg); }
to { opacity: 1; }
}
.role__all-takeaway {
font-family: var(--f-serif);
font-weight: 400;
font-size: 88px;
line-height: 1.15;
text-align: center;
margin: 0 0 28px;
color: var(--fg);
max-width: 1400px;
}
.role__all-takeaway em {
font-style: italic;
color: var(--fg-soft);
}
.role__all-em {
color: var(--accent);
position: relative;
}
.role__all-em::after {
content: '';
position: absolute;
left: -4%;
right: -4%;
bottom: 6px;
height: 8px;
background: var(--accent);
opacity: 0.22;
}
.role__all-foot {
display: flex;
align-items: center;
gap: 16px;
font-family: var(--f-mono);
font-size: 16px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--fg-mute);
}
.role__all-foot-dot {
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--fg-faint);
}

View File

@ -0,0 +1,297 @@
import type { ChapterContext, ChapterDef } from '../types';
import { Reveal } from '../../shared/Reveal';
import { SceneFade } from '../../shared/SceneFade';
import './Role.css';
/**
* Chapter 04 ·
*
*
* "你是一个专家设计师,用户是你的产品经理。"
* - "AI 助手" designer / manager +
* - = Motion Designer = UX Designer = Deck Designer
* - =
*
* 8 / step 0..7
* 0 eyebrow
* 1 hero italic +
* 2 designer / manager
* 3 benefit /
* 4 Motion Designer
* 5 UX Designer
* 6 Deck Designer
* 7 + "好的角色定位 · 是动态的"
*/
interface Role {
id: string;
en: string;
cn: string;
ctx: string;
ctxEn: string;
icon: 'motion' | 'ux' | 'deck';
}
const ROLES: Role[] = [
{ id: 'motion', en: 'Motion Designer', cn: '动效设计师', ctx: '做动画时', ctxEn: 'when animating', icon: 'motion' },
{ id: 'ux', en: 'UX Designer', cn: 'UX 设计师', ctx: '做原型时', ctxEn: 'when prototyping', icon: 'ux' },
{ id: 'deck', en: 'Deck Designer', cn: 'Deck 设计师', ctx: '做幻灯片时', ctxEn: 'when decking', icon: 'deck' },
];
function RoleIcon({ kind }: { kind: Role['icon'] }) {
switch (kind) {
case 'motion':
return (
<svg viewBox="0 0 60 60" className="role__icon">
<circle cx="14" cy="30" r="6" fill="currentColor" opacity="0.25" />
<circle cx="30" cy="30" r="6" fill="currentColor" opacity="0.55" />
<circle cx="46" cy="30" r="6" fill="currentColor" opacity="1" />
<path d="M6 44 Q30 4 54 44" stroke="currentColor" fill="none" strokeWidth="1.2" opacity="0.6" />
</svg>
);
case 'ux':
return (
<svg viewBox="0 0 60 60" className="role__icon">
<rect x="6" y="10" width="22" height="14" stroke="currentColor" fill="none" strokeWidth="1.2" />
<rect x="32" y="10" width="22" height="34" stroke="currentColor" fill="none" strokeWidth="1.2" />
<rect x="6" y="28" width="22" height="22" stroke="currentColor" fill="none" strokeWidth="1.2" />
<path d="M16 18 H22 M40 18 H48 M16 36 H22" stroke="currentColor" strokeWidth="1.2" />
</svg>
);
case 'deck':
return (
<svg viewBox="0 0 60 60" className="role__icon">
<rect x="6" y="14" width="48" height="32" stroke="currentColor" fill="none" strokeWidth="1.2" />
<path d="M14 24 H40 M14 32 H32 M14 40 H26" stroke="currentColor" strokeWidth="1.2" />
<circle cx="46" cy="38" r="3" fill="currentColor" />
</svg>
);
}
}
function RoleCard({ role, size = 'lg' }: { role: Role; size?: 'lg' | 'sm' }) {
return (
<div className={`role__card role__card--${size}`}>
<div className="role__card-strip">
<span>ROLE / {role.id.toUpperCase()}</span>
<span className="role__card-strip-dot" />
<span>WHEN ACTIVE</span>
</div>
<div className="role__card-body">
<div className="role__card-meta">
<RoleIcon kind={role.icon} />
</div>
<div className="role__card-text">
<div className="role__card-en">{role.en}</div>
<div className="role__card-cn">{role.cn}</div>
</div>
</div>
<div className="role__card-foot">
<span className="role__card-ctx">{role.ctx}</span>
<span className="role__card-ctx-en">{role.ctxEn}</span>
</div>
<span className="role__card-corner role__card-corner--tl" />
<span className="role__card-corner role__card-corner--tr" />
<span className="role__card-corner role__card-corner--bl" />
<span className="role__card-corner role__card-corner--br" />
</div>
);
}
function Role({ localStep }: ChapterContext) {
const at = (n: number) => localStep >= n;
// 三幕:引文 / 单卡片切换 / 收尾全卡
const sceneQuote = localStep <= 3;
const sceneFlip = localStep >= 4 && localStep <= 6;
const sceneAll = localStep >= 7;
const roleIndex = Math.min(2, Math.max(0, localStep - 4));
return (
<section className="role">
{/* ═════════ Scene QUOTEstep 0..3)═════════ */}
<SceneFade active={sceneQuote} exitMs={420} enterDelayMs={120}>
<div className="role__quote-scene">
{at(1) && (
<Reveal kind="fade" duration={620} delay={80} className="role__src">
<span className="role__src-bracket">[</span>
<span className="role__src-label">SYSTEM PROMPT</span>
<span className="role__src-sep">·</span>
<span className="role__src-line">L01</span>
<span className="role__src-sep">/</span>
<span className="role__src-mute"></span>
<span className="role__src-bracket">]</span>
</Reveal>
)}
{at(1) && (
<Reveal kind="rise" duration={1100} delay={180} className="role__quote-en" as="p">
<span className="role__quote-marks">"</span>
You are an expert{' '}
<em className={`role__quote-keyword ${at(2) ? 'is-marked' : ''}`}>
designer
<span className="role__quote-mark" />
</em>
, working with the user as a{' '}
<em className={`role__quote-keyword role__quote-keyword--alt ${at(2) ? 'is-marked' : ''}`}>
manager
<span className="role__quote-mark" />
</em>
.
<span className="role__quote-marks">"</span>
</Reveal>
)}
{at(1) && (
<Reveal kind="rise" duration={900} delay={420} className="role__quote-cn" as="p">
<span></span>
<span className={`role__quote-cn-key ${at(2) ? 'is-marked' : ''}`}></span>
<span> </span>
<span className={`role__quote-cn-key role__quote-cn-key--alt ${at(2) ? 'is-marked' : ''}`}></span>
<span></span>
</Reveal>
)}
{at(2) && (
<Reveal kind="fade" duration={620} delay={120} className="role__quote-note">
<u></u> "你是一个 AI 助手"
</Reveal>
)}
{at(3) && (
<div className="role__benefits">
<Reveal kind="rise" duration={780} delay={80} className="role__benefit">
<div className="role__benefit-num">A</div>
<div className="role__benefit-title"></div>
<div className="role__benefit-desc">
AI <br />
</div>
</Reveal>
<Reveal kind="rise" duration={780} delay={220} className="role__benefit">
<div className="role__benefit-num">B</div>
<div className="role__benefit-title"> · </div>
<div className="role__benefit-desc">
PM / / <br />
</div>
</Reveal>
</div>
)}
</div>
</SceneFade>
{/* ═════════ Scene FLIPstep 4..6)═════════ */}
<SceneFade active={sceneFlip} exitMs={420} enterDelayMs={420}>
<div className="role__flip-scene">
{/* 原文参考块 —— 来自系统提示词 L04 */}
<Reveal kind="rise" duration={780} delay={80} className="role__excerpt">
<div className="role__excerpt-head">
<span className="role__src-bracket">[</span>
<span className="role__src-label">SYSTEM PROMPT</span>
<span className="role__src-sep">·</span>
<span className="role__src-line">L04</span>
<span className="role__src-sep">/</span>
<span className="role__src-mute"></span>
<span className="role__src-bracket">]</span>
</div>
<div className="role__excerpt-body">
<span className="role__excerpt-gt">&gt;</span>
<span className="role__excerpt-text">
HTML is your tool, but your{' '}
<em className="role__excerpt-em">medium and output format vary</em>.<br />
You must <em className="role__excerpt-em">embody an expert</em> in that domain:{' '}
<span className="role__excerpt-list">
animator, UX designer, slide designer, prototyper, etc.
</span>
</span>
</div>
</Reveal>
<Reveal kind="fade" duration={520} delay={520} className="role__flip-eyebrow">
<span className="role__flip-eyebrow-arrow"></span>
<span> </span>
<span className="role__flip-eyebrow-em"></span>
</Reveal>
{/* 名片"翻牌"窗 */}
<div className="role__flip-window">
{ROLES.map((r, i) => {
const state = i === roleIndex
? 'current'
: i < roleIndex ? 'prev' : 'next';
return (
<div
key={r.id}
className="role__flip-slot"
data-state={state}
>
<RoleCard role={r} size="lg" />
</div>
);
})}
</div>
{/* 步进刻度 */}
<div className="role__flip-ticks">
{ROLES.map((r, i) => (
<div
key={r.id}
className={`role__flip-tick ${i === roleIndex ? 'is-active' : ''}`}
>
<span className="role__flip-tick-num">0{i + 1}</span>
<span className="role__flip-tick-name">{r.cn}</span>
</div>
))}
</div>
</div>
</SceneFade>
{/* ═════════ Scene ALLstep 7═════════ */}
<SceneFade active={sceneAll} exitMs={420} enterDelayMs={420}>
<div className="role__all-scene">
<Reveal kind="rise" duration={780} delay={120} className="role__all-row">
{ROLES.map((r, i) => (
<div
key={r.id}
className="role__all-slot"
style={{ animationDelay: `${i * 120}ms` }}
>
<RoleCard role={r} size="sm" />
</div>
))}
</Reveal>
<Reveal kind="rise" duration={900} delay={520} className="role__all-takeaway" as="h2">
<br />
<em> <span className="role__all-em"></span> </em>
</Reveal>
<Reveal kind="fade" duration={620} delay={900} className="role__all-foot">
<span> PPT</span>
<span className="role__all-foot-dot" />
<span></span>
<span className="role__all-foot-dot" />
<span></span>
</Reveal>
</div>
</SceneFade>
</section>
);
}
const def: ChapterDef = {
id: 'role',
title: '第一部分 · 角色定位',
eyebrow: '04',
steps: 8,
theme: 'light',
Component: Role,
};
export default def;

View File

@ -0,0 +1,633 @@
/* =========================================================
Chapter 05 · 第二部分 · 工作流
ink 主题 · 流水线 vs 极简总结
========================================================= */
.wf {
position: absolute;
inset: 0;
background: var(--bg);
color: var(--fg);
font-family: var(--f-sans);
overflow: hidden;
}
/* —— 顶部 eyebrow —— */
.wf__eyebrow {
position: absolute;
top: 56px;
left: 96px;
display: flex;
align-items: center;
gap: 18px;
font-family: var(--f-mono);
font-size: 16px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--fg-mute);
z-index: 4;
}
.wf__eyebrow-num {
font-family: var(--f-serif);
font-size: 28px;
letter-spacing: 0;
color: var(--accent);
font-style: italic;
}
.wf__eyebrow-bar {
width: 36px;
height: 1px;
background: var(--line-mid);
}
.wf__eyebrow-bar--long { width: 96px; }
.wf__eyebrow-text { color: var(--fg); letter-spacing: 0.18em; }
.wf__eyebrow-mute { color: var(--fg-faint); font-size: 13px; }
/* —— 通用 prompt 源标签子部件 —— */
.wf__src-bracket { color: var(--accent); font-weight: 600; }
.wf__src-label { color: var(--fg); font-weight: 600; letter-spacing: 0.22em; }
.wf__src-sep { color: var(--fg-faint); }
.wf__src-line { color: var(--accent); font-weight: 600; }
.wf__src-mute { color: var(--fg-mute); }
/* ===================== Scene PIPELINE ===================== */
.wf__pipe-scene {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 140px 100px 100px;
gap: 80px;
}
/* 原文引用块(与 ch04 同款) */
.wf__excerpt {
width: 100%;
max-width: 1280px;
background: var(--bg-2);
border: 1px solid var(--line-mid);
border-left: 3px solid var(--accent);
border-radius: 2px;
padding: 22px 32px 26px;
text-align: left;
box-shadow: 0 18px 32px -22px oklch(0 0 0 / 0.5);
}
.wf__excerpt-head {
display: flex;
align-items: center;
gap: 8px;
font-family: var(--f-mono);
font-size: 13px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--fg-mute);
margin-bottom: 14px;
padding-bottom: 12px;
border-bottom: 1px dashed var(--line-mid);
}
.wf__excerpt-body {
display: flex;
flex-direction: column;
gap: 8px;
}
.wf__excerpt-title {
font-family: var(--f-mono);
font-size: 22px;
color: var(--accent);
margin-bottom: 6px;
}
.wf__excerpt-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px 36px;
font-family: var(--f-mono);
font-size: 19px;
line-height: 1.45;
color: var(--fg-soft);
}
.wf__excerpt-list b {
color: var(--accent);
font-weight: 600;
margin-right: 6px;
}
.wf__excerpt-list code {
background: oklch(0.380 0.014 60);
padding: 0 6px;
border-radius: 2px;
color: var(--accent);
}
.wf__excerpt-list em {
font-style: italic;
color: var(--accent);
background: linear-gradient(180deg, transparent 70%, oklch(0.700 0.170 42 / 0.25) 70%);
padding: 0 2px;
}
/* —— 流水线 —— */
.wf__pipeline {
position: relative;
width: 100%;
max-width: 1480px;
padding: 0 60px;
}
.wf__line {
position: absolute;
top: 50%;
left: 60px;
right: 60px;
height: 2px;
background: var(--line-mid);
border-radius: 1px;
transform: translateY(-50%);
overflow: hidden;
}
.wf__line-fill {
height: 100%;
background: linear-gradient(to right, var(--accent), var(--accent-deep));
transition: width 1100ms cubic-bezier(.2,.8,.2,1);
box-shadow: 0 0 12px var(--accent);
}
.wf__stations {
position: relative;
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 24px;
}
.wf__station {
display: flex;
flex-direction: column;
align-items: center;
gap: 22px;
position: relative;
transition: filter 540ms;
filter: grayscale(0.6) brightness(0.6);
}
.wf__station.is-lit {
filter: grayscale(0) brightness(1);
}
.wf__station-cn {
font-family: var(--f-serif);
font-size: 30px;
color: var(--fg);
height: 36px;
}
.wf__station-en {
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--fg-mute);
}
.wf__station.is-lit .wf__station-en {
color: var(--accent);
}
.wf__station-node {
position: relative;
width: 56px;
height: 56px;
border-radius: 50%;
background: var(--bg);
border: 2px solid var(--line-mid);
display: flex;
align-items: center;
justify-content: center;
font-family: var(--f-serif);
font-size: 28px;
font-style: italic;
color: var(--fg-mute);
z-index: 1;
transition: border-color 540ms, background 540ms, color 540ms, transform 540ms;
}
.wf__station.is-lit .wf__station-node {
background: var(--accent);
border-color: var(--accent);
color: var(--paper);
transform: scale(1.08);
box-shadow: 0 0 0 6px oklch(0.700 0.170 42 / 0.15);
}
.wf__station-no {
z-index: 2;
}
.wf__station-pulse {
position: absolute;
inset: -6px;
border-radius: 50%;
border: 1px solid var(--accent);
opacity: 0;
pointer-events: none;
}
.wf__station.is-lit .wf__station-pulse {
animation: wfPulse 1.6s ease-out infinite;
}
@keyframes wfPulse {
0% { transform: scale(0.9); opacity: 0.7; }
100% { transform: scale(1.6); opacity: 0; }
}
/* ===================== Scene DECIDE ===================== */
.wf__decide-scene {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 140px 100px 80px;
}
.wf__decide-head {
display: flex;
flex-direction: column;
align-items: center;
gap: 14px;
margin-bottom: 56px;
text-align: center;
}
.wf__decide-num {
font-family: var(--f-mono);
font-size: 16px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--accent);
}
.wf__decide-title {
font-family: var(--f-serif);
font-weight: 400;
font-size: 76px;
line-height: 1.15;
color: var(--fg);
margin: 0;
}
.wf__decide-title em {
font-style: italic;
color: var(--accent);
}
.wf__decide-do {
position: relative;
}
.wf__decide-do::after {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 4px;
height: 4px;
background: var(--accent);
opacity: 0.3;
transform-origin: left;
animation: wfUnderline 800ms 320ms cubic-bezier(.2,.8,.2,1) backwards;
}
@keyframes wfUnderline {
from { transform: scaleX(0); opacity: 0; }
to { transform: scaleX(1); opacity: 0.3; }
}
.wf__decide-rule {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 6px 14px;
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.24em;
text-transform: uppercase;
color: var(--fg-mute);
border: 1px solid var(--line-mid);
border-radius: 2px;
background: var(--bg-2);
}
.wf__decide-rule-text {
color: var(--fg);
letter-spacing: 0.16em;
}
/* —— 决策对比(两栏 + 中央 vs —— */
.wf__decide-grid {
position: relative;
display: grid;
grid-template-columns: 1fr 80px 1fr;
gap: 24px;
width: 100%;
max-width: 1500px;
align-items: stretch;
}
.wf__chat {
display: flex;
flex-direction: column;
gap: 16px;
padding: 28px 28px 20px;
background: var(--bg-2);
border: 1px solid var(--line-mid);
border-radius: 4px;
position: relative;
}
.wf__chat--ask { border-top: 3px solid var(--crimson); }
.wf__chat--do { border-top: 3px solid var(--accent); }
.wf__chat-tag {
display: flex;
align-items: center;
gap: 10px;
font-family: var(--f-mono);
font-size: 13px;
letter-spacing: 0.24em;
text-transform: uppercase;
color: var(--fg-mute);
margin-bottom: 4px;
}
.wf__chat-tag-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--crimson);
box-shadow: 0 0 6px var(--crimson);
animation: wfPulse 1.6s ease-out infinite;
}
.wf__chat-tag-dot--do {
background: var(--accent);
box-shadow: 0 0 6px var(--accent);
}
.wf__bubble {
padding: 16px 20px;
border-radius: 12px;
position: relative;
font-family: var(--f-sans);
font-size: 22px;
line-height: 1.45;
}
.wf__bubble p { margin: 0; }
.wf__bubble-meta {
display: block;
font-family: var(--f-mono);
font-size: 11px;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--fg-faint);
margin-bottom: 6px;
}
.wf__bubble--user {
background: oklch(0.380 0.014 60);
border: 1px solid var(--line-mid);
border-bottom-left-radius: 2px;
align-self: flex-start;
max-width: 84%;
}
.wf__bubble--ai {
background: oklch(0.355 0.018 60);
border: 1px solid oklch(0.700 0.170 42 / 0.4);
border-bottom-right-radius: 2px;
align-self: flex-end;
max-width: 92%;
box-shadow: 0 8px 16px -8px oklch(0.700 0.170 42 / 0.25);
}
.wf__qmarks {
position: absolute;
top: -22px;
right: 20px;
display: flex;
gap: 8px;
font-family: var(--f-serif);
font-style: italic;
font-size: 36px;
color: var(--crimson);
pointer-events: none;
}
.wf__qmarks span {
animation: wfQ 2.4s ease-in-out infinite;
opacity: 0;
}
@keyframes wfQ {
0%, 100% { opacity: 0; transform: translateY(8px); }
20%, 70% { opacity: 0.9; transform: translateY(0); }
}
.wf__action {
margin-top: 14px;
display: flex;
flex-direction: column;
gap: 6px;
}
.wf__action-bar {
height: 6px;
background: linear-gradient(to right, var(--accent) 0%, var(--accent-deep) 100%);
border-radius: 1px;
transform-origin: left;
animation: wfBar 1.2s cubic-bezier(.2,.8,.2,1) infinite;
}
.wf__action-bar:nth-child(1) { width: 70%; animation-delay: 0ms; }
.wf__action-bar:nth-child(2) { width: 90%; animation-delay: 200ms; }
.wf__action-bar:nth-child(3) { width: 50%; animation-delay: 400ms; }
@keyframes wfBar {
0% { transform: scaleX(0); opacity: 0.4; }
60% { transform: scaleX(1); opacity: 1; }
100% { transform: scaleX(1); opacity: 0.4; }
}
.wf__chat-verdict {
margin-top: auto;
padding: 12px 14px;
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.22em;
text-transform: uppercase;
text-align: center;
border: 1px dashed var(--line-mid);
border-radius: 2px;
}
.wf__chat-verdict--ask { color: var(--crimson); border-color: var(--crimson); }
.wf__chat-verdict--do { color: var(--accent); border-color: var(--accent); }
.wf__decide-vs {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
pointer-events: none;
}
.wf__decide-vs-line {
width: 1px;
flex: 1;
background: var(--line-mid);
}
.wf__decide-vs-knob {
width: 44px;
height: 44px;
border: 1px solid var(--line-strong);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--f-serif);
font-style: italic;
font-size: 22px;
color: var(--fg-mute);
background: var(--bg);
}
/* ===================== Scene SUMMARY ===================== */
.wf__sum-scene {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 140px 100px 80px;
gap: 28px;
}
.wf__sum-num {
font-family: var(--f-mono);
font-size: 16px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--accent);
}
.wf__sum-hero {
font-family: var(--f-serif);
font-weight: 400;
font-size: 88px;
line-height: 1.15;
text-align: center;
margin: 0;
color: var(--fg);
display: flex;
flex-direction: column;
gap: 6px;
}
.wf__sum-hero-en em {
font-style: italic;
color: var(--accent);
}
.wf__sum-hero-cn {
font-size: 56px;
color: var(--fg-soft);
}
.wf__sum-hero-cn em {
font-style: italic;
color: var(--accent);
}
.wf__sum-source {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 6px 14px;
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--fg-mute);
border: 1px solid var(--line-mid);
border-radius: 2px;
background: var(--bg-2);
}
.wf__sum-source-quote {
font-family: var(--f-mono);
letter-spacing: 0.04em;
text-transform: none;
color: var(--fg);
margin-left: 6px;
}
.wf__sum-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 36px;
width: 100%;
max-width: 1400px;
margin-top: 18px;
}
.wf__sum-card {
position: relative;
padding: 22px 26px 24px;
background: var(--bg-2);
border: 1px solid var(--line-mid);
border-radius: 4px;
overflow: hidden;
}
.wf__sum-card--bad {
border-top: 3px solid var(--crimson);
}
.wf__sum-card--good {
border-top: 3px solid var(--accent);
box-shadow: 0 16px 28px -18px oklch(0.700 0.170 42 / 0.35);
}
.wf__sum-card-tag {
display: flex;
align-items: center;
gap: 8px;
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--fg-mute);
margin-bottom: 14px;
padding-bottom: 12px;
border-bottom: 1px dashed var(--line-mid);
}
.wf__sum-x {
font-family: var(--f-serif);
font-style: italic;
font-size: 20px;
color: var(--crimson);
}
.wf__sum-check {
font-family: var(--f-serif);
font-style: italic;
font-size: 20px;
color: var(--accent);
}
.wf__sum-card-body {
font-family: var(--f-sans);
font-size: 22px;
line-height: 1.5;
color: var(--fg-soft);
}
.wf__sum-card-body p { margin: 0 0 8px; }
.wf__sum-card-body p:last-child { margin: 0; }
.wf__sum-card-body b {
font-family: var(--f-mono);
font-size: 16px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--accent);
margin-right: 10px;
}
.wf__sum-card-body s {
text-decoration-color: var(--crimson);
text-decoration-thickness: 2px;
color: var(--fg-mute);
}
.wf__sum-strike {
position: absolute;
left: -10%;
right: -10%;
top: 50%;
height: 2px;
background: var(--crimson);
transform: translateY(-50%) rotate(-3deg);
opacity: 0;
animation: wfStrike 700ms 480ms cubic-bezier(.2,.8,.2,1) forwards;
transform-origin: left;
}
@keyframes wfStrike {
from { opacity: 0; transform: translateY(-50%) rotate(-3deg) scaleX(0); }
to { opacity: 0.7; transform: translateY(-50%) rotate(-3deg) scaleX(1); }
}

View File

@ -0,0 +1,266 @@
import type { ChapterContext, ChapterDef } from '../types';
import { Reveal } from '../../shared/Reveal';
import { SceneFade } from '../../shared/SceneFade';
import './Workflow.css';
/**
* Chapter 05 ·
*
*
* -
* - /
* : "做个 PPT" AI
* : "做个 PPT, 工程全员 All Hands, 10 min" AI
* - "Summarize EXTREMELY BRIEFLY"
*
* 7 / step 0..6
* 0 eyebrow
* 1 prompt block + 线
* 2 1-3 line
* 3 4-6
* 4 pivot "何时问 vs 何时干"
* 5
* 6 "Summarize EXTREMELY BRIEFLY" + /
*/
interface Station {
no: string;
en: string;
cn: string;
}
const STATIONS: Station[] = [
{ no: '1', en: 'Understand', cn: '理解需求' },
{ no: '2', en: 'Explore', cn: '探索资源' },
{ no: '3', en: 'Plan', cn: '制定计划' },
{ no: '4', en: 'Build', cn: '搭建结构' },
{ no: '5', en: 'Verify', cn: '完成验证' },
{ no: '6', en: 'Brief', cn: '极简总结' },
];
function Workflow({ localStep }: ChapterContext) {
const at = (n: number) => localStep >= n;
// —— 三幕 ——
const scenePipe = localStep <= 3;
const sceneDecide = localStep === 4 || localStep === 5;
const sceneSummary = localStep >= 6;
// 流水线点亮进度 0..6
const litCount = (() => {
if (localStep < 1) return 0;
if (localStep === 1) return 0;
if (localStep === 2) return 3;
return 6;
})();
const linePct = (litCount / 6) * 100;
return (
<section className="wf">
{/* ════════════ Scene PIPELINEstep 0..3)════════════ */}
<SceneFade active={scenePipe} exitMs={420} enterDelayMs={120}>
<div className="wf__pipe-scene">
{at(1) && (
<Reveal kind="rise" duration={780} delay={80} className="wf__excerpt">
<div className="wf__excerpt-head">
<span className="wf__src-bracket">[</span>
<span className="wf__src-label">SYSTEM PROMPT</span>
<span className="wf__src-sep">·</span>
<span className="wf__src-line">L17-23</span>
<span className="wf__src-sep">/</span>
<span className="wf__src-mute"></span>
<span className="wf__src-bracket">]</span>
</div>
<div className="wf__excerpt-body">
<div className="wf__excerpt-title">## Your workflow</div>
<div className="wf__excerpt-list">
<span><b>1.</b> Understand user needs ...</span>
<span><b>2.</b> Explore provided resources ...</span>
<span><b>3.</b> Plan and/or make a todo list.</span>
<span><b>4.</b> Build folder structure ...</span>
<span><b>5.</b> Finish: call <code>done</code> ...</span>
<span><b>6.</b> Summarize <em>EXTREMELY BRIEFLY</em> caveats and next steps only.</span>
</div>
</div>
</Reveal>
)}
{at(1) && (
<Reveal kind="rise" duration={900} delay={520} className="wf__pipeline">
{/* 底部基线 */}
<div className="wf__line">
<div className="wf__line-fill" style={{ width: `${linePct}%` }} />
</div>
{/* 6 站 */}
<div className="wf__stations">
{STATIONS.map((s, i) => {
const lit = i < litCount;
return (
<div
key={s.no}
className={`wf__station ${lit ? 'is-lit' : ''}`}
style={{ transitionDelay: `${i * 90}ms` }}
>
<div className="wf__station-cn">{s.cn}</div>
<div className="wf__station-node">
<span className="wf__station-no">{s.no}</span>
<span className="wf__station-pulse" />
</div>
<div className="wf__station-en">{s.en}</div>
</div>
);
})}
</div>
</Reveal>
)}
</div>
</SceneFade>
{/* ════════════ Scene DECIDEstep 4..5)════════════ */}
<SceneFade active={sceneDecide} exitMs={420} enterDelayMs={420}>
<div className="wf__decide-scene">
<Reveal kind="rise" duration={780} delay={80} className="wf__decide-head">
<span className="wf__decide-num"></span>
<h2 className="wf__decide-title">
<em></em><em className="wf__decide-do"></em>
</h2>
<div className="wf__decide-rule">
<span className="wf__src-bracket">[</span>
<span className="wf__src-label">RULE</span>
<span className="wf__src-bracket">]</span>
<span className="wf__decide-rule-text"> · </span>
</div>
</Reveal>
<div className="wf__decide-grid">
{/* —— 左:模糊请求 → 反复问 —— */}
<Reveal kind="rise" duration={780} delay={260} className="wf__chat wf__chat--ask">
<div className="wf__chat-tag">
<span className="wf__chat-tag-dot" />
AMBIGUOUS ·
</div>
<div className="wf__bubble wf__bubble--user">
<span className="wf__bubble-meta">USER</span>
<p> PPT</p>
</div>
<div className="wf__bubble wf__bubble--ai">
<span className="wf__bubble-meta">CLAUDE</span>
<p>...</p>
<div className="wf__qmarks">
<span style={{ animationDelay: '0ms' }}>?</span>
<span style={{ animationDelay: '180ms' }}>?</span>
<span style={{ animationDelay: '360ms' }}>?</span>
<span style={{ animationDelay: '540ms' }}>?</span>
</div>
</div>
<div className="wf__chat-verdict wf__chat-verdict--ask">
ASK QUESTIONS
</div>
</Reveal>
{/* —— 中央分隔 —— */}
<div className="wf__decide-vs">
<span className="wf__decide-vs-line" />
<span className="wf__decide-vs-knob">vs</span>
<span className="wf__decide-vs-line" />
</div>
{/* —— 右:详细请求 → 直接动手 —— */}
{at(5) && (
<Reveal kind="rise" duration={780} delay={120} className="wf__chat wf__chat--do">
<div className="wf__chat-tag">
<span className="wf__chat-tag-dot wf__chat-tag-dot--do" />
ENOUGH INFO ·
</div>
<div className="wf__bubble wf__bubble--user">
<span className="wf__bubble-meta">USER</span>
<p> PPT All Hands10&nbsp;</p>
</div>
<div className="wf__bubble wf__bubble--ai">
<span className="wf__bubble-meta">CLAUDE</span>
<p> </p>
<div className="wf__action">
<span className="wf__action-bar" />
<span className="wf__action-bar" />
<span className="wf__action-bar" />
</div>
</div>
<div className="wf__chat-verdict wf__chat-verdict--do">
NO QUESTIONS · GO BUILD
</div>
</Reveal>
)}
</div>
</div>
</SceneFade>
{/* ════════════ Scene SUMMARYstep 6════════════ */}
<SceneFade active={sceneSummary} exitMs={420} enterDelayMs={420}>
<div className="wf__sum-scene">
<Reveal kind="fade" duration={620} delay={80} className="wf__sum-num">
</Reveal>
<Reveal kind="rise" duration={1100} delay={180} className="wf__sum-hero" as="h1">
<span className="wf__sum-hero-en">Summarize <em>EXTREMELY BRIEFLY</em></span>
<span className="wf__sum-hero-cn"> <em></em> <em></em></span>
</Reveal>
<Reveal kind="rise" duration={780} delay={520} className="wf__sum-source">
<span className="wf__src-bracket">[</span>
<span className="wf__src-label">SYSTEM PROMPT</span>
<span className="wf__src-sep">·</span>
<span className="wf__src-line">L23</span>
<span className="wf__src-bracket">]</span>
<span className="wf__sum-source-quote">
&ldquo;Summarize EXTREMELY BRIEFLY caveats and next steps only.&rdquo;
</span>
</Reveal>
<Reveal kind="rise" duration={780} delay={760} className="wf__sum-grid">
{/* 反例 */}
<div className="wf__sum-card wf__sum-card--bad">
<div className="wf__sum-card-tag">
<span className="wf__sum-x">×</span>
</div>
<div className="wf__sum-card-body">
<s>Header.tsx</s> <s>Hero.tsx</s>
<s>theme.ts</s> hover ...
</div>
<div className="wf__sum-strike" />
</div>
{/* 正例 */}
<div className="wf__sum-card wf__sum-card--good">
<div className="wf__sum-card-tag">
<span className="wf__sum-check"></span> +
</div>
<div className="wf__sum-card-body">
<p><b>caveats</b> / </p>
<p><b>next</b> hover / </p>
</div>
</div>
</Reveal>
</div>
</SceneFade>
</section>
);
}
const def: ChapterDef = {
id: 'workflow',
title: '第二部分 · 工作流',
eyebrow: '05',
steps: 7,
theme: 'ink',
Component: Workflow,
};
export default def;

View File

@ -0,0 +1,564 @@
/* =========================================================
Chapter 06 · 第三部分 · AI
light 主题 · hero 反面教材 grid + 红斜线 字体黑名单 / 替代
========================================================= */
.aa {
position: absolute;
inset: 0;
background: var(--bg);
color: var(--fg);
font-family: var(--f-sans);
overflow: hidden;
}
/* —— 顶部 eyebrow —— */
.aa__eyebrow {
position: absolute;
top: 56px;
left: 96px;
display: flex;
align-items: center;
gap: 18px;
font-family: var(--f-mono);
font-size: 16px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--fg-mute);
z-index: 4;
}
.aa__eyebrow-num {
font-family: var(--f-serif);
font-size: 28px;
letter-spacing: 0;
color: var(--accent);
font-style: italic;
}
.aa__eyebrow-bar {
width: 36px;
height: 1px;
background: var(--line-mid);
}
.aa__eyebrow-bar--long { width: 96px; }
.aa__eyebrow-text { color: var(--fg); letter-spacing: 0.18em; }
.aa__eyebrow-mute { color: var(--fg-faint); font-size: 13px; }
/* —— 通用 prompt 源标签子部件 —— */
.aa__src-bracket { color: var(--accent); font-weight: 600; }
.aa__src-label { color: var(--fg); font-weight: 600; letter-spacing: 0.22em; }
.aa__src-sep { color: var(--fg-faint); }
.aa__src-line { color: var(--accent); font-weight: 600; }
.aa__src-mute { color: var(--fg-mute); }
/* ===================== Scene HERO ===================== */
.aa__hero {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 140px 140px 120px;
text-align: center;
gap: 32px;
}
.aa__hero-title {
font-family: var(--f-serif);
font-weight: 400;
font-size: 168px;
line-height: 1.05;
margin: 0;
}
.aa__hero-em {
font-style: italic;
color: var(--accent);
position: relative;
}
.aa__hero-em::after {
content: '';
position: absolute;
left: 4%;
right: 4%;
bottom: 8px;
height: 6px;
background: var(--accent);
opacity: 0.22;
transform-origin: left;
animation: aaUnderline 1100ms 360ms cubic-bezier(.2,.8,.2,1) backwards;
}
@keyframes aaUnderline {
from { transform: scaleX(0); opacity: 0; }
to { transform: scaleX(1); opacity: 0.22; }
}
.aa__hero-sub {
font-family: var(--f-serif);
font-size: 36px;
line-height: 1.4;
color: var(--fg-soft);
margin: 0;
max-width: 1100px;
}
.aa__hero-sub em {
font-style: italic;
color: var(--accent);
}
.aa__excerpt {
width: 100%;
max-width: 1100px;
background: var(--bg-2);
border: 1px solid var(--line-mid);
border-left: 3px solid var(--accent);
border-radius: 2px;
padding: 18px 28px 22px;
text-align: left;
margin-top: 12px;
}
.aa__excerpt-head {
display: flex;
align-items: center;
gap: 8px;
font-family: var(--f-mono);
font-size: 13px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--fg-mute);
margin-bottom: 10px;
}
.aa__excerpt-body {
display: flex;
align-items: flex-start;
gap: 14px;
}
.aa__excerpt-gt {
font-family: var(--f-mono);
font-size: 28px;
color: var(--accent);
}
.aa__excerpt-text {
font-family: var(--f-mono);
font-size: 22px;
line-height: 1.5;
color: var(--fg);
}
.aa__excerpt-text em {
font-style: normal;
color: var(--accent);
background: linear-gradient(180deg, transparent 70%, oklch(0.860 0.060 60 / 0.6) 70%);
padding: 0 2px;
}
/* ===================== Scene GRID ===================== */
.aa__grid-scene {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 140px 100px 90px;
gap: 32px;
}
.aa__grid-cap {
font-family: var(--f-serif);
font-size: 32px;
font-style: italic;
color: var(--fg-soft);
text-align: center;
}
.aa__grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 28px;
width: 100%;
max-width: 1480px;
}
.aa__grid-slot {
animation: aaGridIn 720ms cubic-bezier(.2,.8,.2,1) backwards;
}
@keyframes aaGridIn {
from { opacity: 0; transform: translateY(28px) scale(0.94); }
to { opacity: 1; transform: none; }
}
.aa__bad {
display: flex;
flex-direction: column;
background: var(--bg-2);
border: 1px solid var(--line-mid);
border-radius: 4px;
overflow: hidden;
transition: filter 600ms, opacity 600ms;
}
.aa__bad.is-slashed {
filter: grayscale(0.55) brightness(0.92);
opacity: 0.78;
}
.aa__bad-canvas {
position: relative;
height: 220px;
display: flex;
align-items: center;
justify-content: center;
background:
repeating-linear-gradient(45deg,
oklch(0.180 0.014 60 / 0.025) 0 6px,
transparent 6px 12px);
overflow: hidden;
}
/* —— 反面教材 1紫粉蓝渐变 —— */
.aa__bad-gradient {
position: absolute;
inset: 14px;
border-radius: 12px;
background: linear-gradient(135deg, #a78bfa 0%, #f0abfc 35%, #67e8f9 70%, #fda4af 100%);
box-shadow: 0 12px 36px -10px oklch(0.6 0.18 320 / 0.4);
}
/* —— 反面教材 2Emoji 滥用 —— */
.aa__bad-emoji {
display: flex;
gap: 12px;
font-size: 44px;
line-height: 1;
filter: drop-shadow(0 2px 4px oklch(0 0 0 / 0.15));
}
/* —— 反面教材 3左侧色条卡 —— */
.aa__bad-leftbar {
position: relative;
width: 220px;
height: 130px;
background: white;
border-radius: 14px;
display: flex;
align-items: stretch;
overflow: hidden;
box-shadow: 0 16px 28px -10px oklch(0.180 0.014 60 / 0.18);
}
.aa__bad-leftbar-stripe {
width: 8px;
background: linear-gradient(to bottom, #6366f1, #a855f7);
}
.aa__bad-leftbar-body {
flex: 1;
padding: 18px 16px;
display: flex;
flex-direction: column;
gap: 8px;
}
.aa__bad-leftbar-t {
width: 60%;
height: 14px;
background: oklch(0.4 0.04 280);
border-radius: 4px;
}
.aa__bad-leftbar-l {
width: 90%;
height: 8px;
background: oklch(0.85 0.02 280);
border-radius: 4px;
}
.aa__bad-leftbar-l--short { width: 60%; }
/* —— 反面教材 4烂大街字体 —— */
.aa__bad-font {
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
font-size: 22px;
color: var(--fg);
}
.aa__bad-font span:first-child { font-weight: 600; font-size: 26px; }
/* —— 反面教材 5data slop —— */
.aa__bad-data {
display: grid;
grid-template-columns: repeat(2, auto);
gap: 18px 32px;
font-family: var(--f-mono);
font-size: 22px;
color: var(--fg);
}
.aa__bad-data > div {
display: flex;
align-items: center;
gap: 8px;
}
.aa__bad-data span {
color: oklch(0.6 0.18 320);
font-size: 20px;
}
.aa__bad-data b {
font-family: var(--f-serif);
font-style: italic;
font-weight: 400;
font-size: 28px;
color: var(--fg);
}
/* —— 反面教材 6复杂 SVG —— */
.aa__bad-svg {
width: 130px;
height: 130px;
filter: drop-shadow(0 6px 12px oklch(0.6 0.18 320 / 0.35));
}
/* —— 红斜杠 —— */
.aa__bad-slash {
position: absolute;
inset: 0;
pointer-events: none;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 360ms;
}
.aa__bad.is-slashed .aa__bad-slash {
opacity: 1;
}
.aa__bad-slash-line {
display: block;
width: 130%;
height: 5px;
background: var(--crimson);
transform: rotate(-18deg) scaleX(0);
transform-origin: left;
border-radius: 1px;
box-shadow:
0 0 12px oklch(0.560 0.200 22 / 0.6),
0 0 24px oklch(0.560 0.200 22 / 0.25);
animation-fill-mode: forwards;
}
.aa__bad.is-slashed .aa__bad-slash-line {
animation: aaSlash 520ms cubic-bezier(.6,-0.05,.2,1.2) forwards;
}
@keyframes aaSlash {
from { transform: rotate(-18deg) scaleX(0); }
to { transform: rotate(-18deg) scaleX(1); }
}
/* 错开每张卡的斜线触发节奏 */
.aa__grid-slot:nth-child(1) .aa__bad-slash-line { animation-delay: 80ms; }
.aa__grid-slot:nth-child(2) .aa__bad-slash-line { animation-delay: 180ms; }
.aa__grid-slot:nth-child(3) .aa__bad-slash-line { animation-delay: 280ms; }
.aa__grid-slot:nth-child(4) .aa__bad-slash-line { animation-delay: 380ms; }
.aa__grid-slot:nth-child(5) .aa__bad-slash-line { animation-delay: 480ms; }
.aa__grid-slot:nth-child(6) .aa__bad-slash-line { animation-delay: 580ms; }
.aa__bad-foot {
display: flex;
flex-direction: column;
gap: 4px;
padding: 14px 18px 16px;
border-top: 1px solid var(--line-mid);
background: var(--bg);
}
.aa__bad-en {
font-family: var(--f-mono);
font-size: 13px;
letter-spacing: 0.28em;
text-transform: uppercase;
color: var(--fg-faint);
}
.aa__bad-cn {
font-family: var(--f-serif);
font-size: 26px;
color: var(--fg);
}
.aa__grid-verdict {
font-family: var(--f-serif);
font-size: 32px;
color: var(--fg-soft);
text-align: center;
display: flex;
align-items: center;
gap: 16px;
}
.aa__grid-verdict em {
font-style: italic;
color: var(--accent);
}
.aa__grid-verdict-mark {
font-family: var(--f-serif);
font-style: italic;
font-size: 36px;
color: var(--crimson);
}
/* ===================== Scene FONTS ===================== */
.aa__fonts-scene {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 130px 100px 90px;
gap: 32px;
}
.aa__fonts-head {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
text-align: center;
}
.aa__fonts-num {
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--accent);
}
.aa__fonts-title {
font-family: var(--f-serif);
font-weight: 400;
font-size: 64px;
line-height: 1.15;
color: var(--fg);
margin: 0;
}
.aa__fonts-title em {
font-style: italic;
color: var(--accent);
}
.aa__fonts-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 36px;
width: 100%;
max-width: 1480px;
}
.aa__fonts-col {
display: flex;
flex-direction: column;
background: var(--bg-2);
border: 1px solid var(--line-mid);
border-radius: 4px;
padding: 22px 28px 26px;
}
.aa__fonts-col--ban {
border-top: 3px solid var(--crimson);
}
.aa__fonts-col--alt {
border-top: 3px solid var(--accent);
box-shadow: 0 16px 28px -18px oklch(0.700 0.170 42 / 0.35);
}
.aa__fonts-col-tag {
display: flex;
align-items: center;
gap: 8px;
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.24em;
text-transform: uppercase;
color: var(--fg-mute);
padding-bottom: 14px;
margin-bottom: 14px;
border-bottom: 1px dashed var(--line-mid);
}
.aa__fonts-col-mark {
font-family: var(--f-serif);
font-style: italic;
font-size: 22px;
color: var(--crimson);
}
.aa__fonts-col-mark--ok {
color: var(--accent);
}
.aa__fonts-list {
display: flex;
flex-direction: column;
gap: 14px;
}
.aa__fonts-row {
position: relative;
display: grid;
grid-template-columns: 1fr 1.2fr auto;
gap: 16px;
align-items: center;
padding: 8px 4px;
animation: aaFontIn 540ms cubic-bezier(.2,.8,.2,1) backwards;
}
@keyframes aaFontIn {
from { opacity: 0; transform: translateX(-12px); }
to { opacity: 1; transform: none; }
}
.aa__fonts-row-name {
font-size: 28px;
color: var(--fg);
}
.aa__fonts-row-sample {
font-size: 24px;
color: var(--fg-soft);
}
.aa__fonts-row-tag {
font-family: var(--f-mono);
font-size: 12px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--fg-faint);
}
.aa__fonts-row-strike {
position: absolute;
left: -4px;
right: -4px;
top: 50%;
height: 2px;
background: var(--crimson);
transform-origin: left;
transform: translateY(-50%) scaleX(0);
animation: aaRowStrike 520ms cubic-bezier(.6,-0.05,.2,1.2) forwards;
opacity: 0.85;
}
.aa__fonts-row--ban:nth-child(1) .aa__fonts-row-strike { animation-delay: 480ms; }
.aa__fonts-row--ban:nth-child(2) .aa__fonts-row-strike { animation-delay: 580ms; }
.aa__fonts-row--ban:nth-child(3) .aa__fonts-row-strike { animation-delay: 680ms; }
.aa__fonts-row--ban:nth-child(4) .aa__fonts-row-strike { animation-delay: 780ms; }
.aa__fonts-row--ban:nth-child(5) .aa__fonts-row-strike { animation-delay: 880ms; }
@keyframes aaRowStrike {
to { transform: translateY(-50%) scaleX(1); }
}
.aa__fonts-foot {
display: flex;
align-items: center;
gap: 16px;
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.24em;
text-transform: uppercase;
color: var(--fg-mute);
}
.aa__fonts-foot span:last-child {
color: var(--accent);
font-weight: 600;
}
.aa__fonts-foot-dot {
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--fg-faint);
flex: none;
}

View File

@ -0,0 +1,290 @@
import type { ChapterContext, ChapterDef } from '../types';
import { Reveal } from '../../shared/Reveal';
import { SceneFade } from '../../shared/SceneFade';
import './AntiAi.css';
/**
* Chapter 06 · AI
*
*
* - "AI 味" / Emoji / / / data slop / SVG
* - "味道"
* - (Inter / Roboto / Arial / Fraunces / system-ui)
* - (Plus Jakarta Sans / Space Grotesk / Sora / Newsreader)
*
* 6 / step 0..5
* 0 eyebrow
* 1 hero "什么是 AI 味?" + prompt block
* 2 6 stagger
* 3 "叉掉"
* 4
* 5 + takeaway
*/
interface AntiPattern {
id: string;
cn: string;
en: string;
}
const PATTERNS: AntiPattern[] = [
{ id: 'gradient', cn: '紫粉蓝渐变背景', en: 'PASTEL GRADIENT' },
{ id: 'emoji', cn: 'Emoji 当图标', en: 'EMOJI ICONS' },
{ id: 'leftbar', cn: '左侧彩色色条卡', en: 'LEFT COLOR BAR' },
{ id: 'font', cn: '烂大街字体', en: 'STOCK FONTS' },
{ id: 'data', cn: '堆砌假数据', en: 'DATA SLOP' },
{ id: 'svg', cn: '复杂硬画 SVG', en: 'OVER-DRAWN SVG' },
];
const BANNED_FONTS = [
{ name: 'Inter', family: 'Inter, sans-serif', sample: 'AaBbCc 123' },
{ name: 'Roboto', family: 'Roboto, sans-serif', sample: 'AaBbCc 123' },
{ name: 'Arial', family: 'Arial, sans-serif', sample: 'AaBbCc 123' },
{ name: 'Fraunces', family: '"Fraunces", serif', sample: 'AaBbCc 123' },
{ name: 'system-ui', family: 'system-ui, sans-serif', sample: 'AaBbCc 123' },
];
const BETTER_FONTS = [
{ name: 'Plus Jakarta Sans', family: '"Plus Jakarta Sans", sans-serif', sample: 'AaBbCc 123', tag: 'sans · workhorse' },
{ name: 'Space Grotesk', family: '"Space Grotesk", sans-serif', sample: 'AaBbCc 123', tag: 'sans · 工程感' },
{ name: 'Sora', family: '"Sora", sans-serif', sample: 'AaBbCc 123', tag: 'sans · 现代克制' },
{ name: 'Newsreader', family: '"Newsreader", serif', sample: 'AaBbCc 123', tag: 'serif · 编辑感' },
];
/** 单个反面教材小卡片 —— 视觉示意 + 名称 + (后期) 红斜线 */
function BadCard({ p, slashed }: { p: AntiPattern; slashed: boolean }) {
return (
<div className={`aa__bad ${slashed ? 'is-slashed' : ''}`} data-id={p.id}>
<div className="aa__bad-canvas">
{p.id === 'gradient' && (
<div className="aa__bad-gradient" />
)}
{p.id === 'emoji' && (
<div className="aa__bad-emoji">
<span>🚀</span><span>🎯</span><span>💡</span><span></span><span>🔥</span>
</div>
)}
{p.id === 'leftbar' && (
<div className="aa__bad-leftbar">
<span className="aa__bad-leftbar-stripe" />
<div className="aa__bad-leftbar-body">
<div className="aa__bad-leftbar-t" />
<div className="aa__bad-leftbar-l" />
<div className="aa__bad-leftbar-l aa__bad-leftbar-l--short" />
</div>
</div>
)}
{p.id === 'font' && (
<div className="aa__bad-font">
<span style={{ fontFamily: 'Inter, sans-serif' }}>Welcome to Our Platform</span>
<span style={{ fontFamily: 'Roboto, sans-serif' }}>Get Started in Seconds</span>
</div>
)}
{p.id === 'data' && (
<div className="aa__bad-data">
<div><span></span><b>4.9</b></div>
<div><span></span><b>+42%</b></div>
<div><span></span><b>12k</b></div>
<div><span></span><b>99.9%</b></div>
</div>
)}
{p.id === 'svg' && (
<svg viewBox="0 0 80 80" className="aa__bad-svg">
<defs>
<linearGradient id={`g-${p.id}`} x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stopColor="#a78bfa" />
<stop offset="50%" stopColor="#f0abfc" />
<stop offset="100%" stopColor="#67e8f9" />
</linearGradient>
</defs>
<path
d="M40 8 L60 24 L72 48 L60 70 L40 72 L20 70 L8 48 L20 24 Z"
fill={`url(#g-${p.id})`}
stroke="#7c3aed"
strokeWidth="1.5"
/>
<circle cx="40" cy="40" r="14" fill="white" opacity="0.6" />
<path d="M30 40 L50 40 M40 30 L40 50" stroke="#7c3aed" strokeWidth="1.5" />
</svg>
)}
{/* 红色斜杠覆盖层 */}
<div className="aa__bad-slash" aria-hidden>
<span className="aa__bad-slash-line" />
</div>
</div>
<div className="aa__bad-foot">
<span className="aa__bad-en">{p.en}</span>
<span className="aa__bad-cn">{p.cn}</span>
</div>
</div>
);
}
function AntiAi({ localStep }: ChapterContext) {
const at = (n: number) => localStep >= n;
const sceneHero = localStep <= 1;
const sceneGrid = localStep === 2 || localStep === 3;
const sceneFonts = localStep >= 4;
const slashed = localStep >= 3;
const showAlt = localStep >= 5;
return (
<section className="aa">
{/* ════════ Scene HEROstep 0..1)════════ */}
<SceneFade active={sceneHero} exitMs={420} enterDelayMs={120}>
<div className="aa__hero">
{at(1) && (
<>
<Reveal kind="rise" duration={1100} delay={120} className="aa__hero-title" as="h1">
<em className="aa__hero-em">AI </em>
</Reveal>
<Reveal kind="rise" duration={780} delay={520} className="aa__hero-sub" as="p">
<br />
<em></em>
</Reveal>
<Reveal kind="rise" duration={780} delay={820} className="aa__excerpt">
<div className="aa__excerpt-head">
<span className="aa__src-bracket">[</span>
<span className="aa__src-label">SYSTEM PROMPT</span>
<span className="aa__src-sep">·</span>
<span className="aa__src-line">L04</span>
<span className="aa__src-bracket">]</span>
</div>
<div className="aa__excerpt-body">
<span className="aa__excerpt-gt">&gt;</span>
<span className="aa__excerpt-text">
Avoid <em>web design tropes and conventions</em> unless you are
making a web page.
</span>
</div>
</Reveal>
</>
)}
</div>
</SceneFade>
{/* ════════ Scene GRIDstep 2..3)════════ */}
<SceneFade active={sceneGrid} exitMs={420} enterDelayMs={420}>
<div className="aa__grid-scene">
<Reveal kind="fade" duration={620} delay={80} className="aa__grid-cap">
AI "老套路"
</Reveal>
<div className="aa__grid">
{PATTERNS.map((p, i) => (
<div
key={p.id}
className="aa__grid-slot"
style={{ animationDelay: `${i * 110}ms` }}
>
<BadCard p={p} slashed={slashed} />
</div>
))}
</div>
{at(3) && (
<Reveal kind="fade" duration={620} delay={620} className="aa__grid-verdict">
<span className="aa__grid-verdict-mark">×</span>
Claude Design <em></em> AI
</Reveal>
)}
</div>
</SceneFade>
{/* ════════ Scene FONTSstep 4..5)════════ */}
<SceneFade active={sceneFonts} exitMs={420} enterDelayMs={420}>
<div className="aa__fonts-scene">
<Reveal kind="rise" duration={780} delay={80} className="aa__fonts-head">
<span className="aa__fonts-num"></span>
<h2 className="aa__fonts-title">
<em></em>
</h2>
</Reveal>
<div className="aa__fonts-grid">
{/* 黑名单 */}
<Reveal kind="rise" duration={780} delay={260} className="aa__fonts-col aa__fonts-col--ban">
<div className="aa__fonts-col-tag">
<span className="aa__fonts-col-mark">×</span>
BLACKLIST ·
</div>
<div className="aa__fonts-list">
{BANNED_FONTS.map((f, i) => (
<div
key={f.name}
className="aa__fonts-row aa__fonts-row--ban"
style={{ animationDelay: `${i * 80}ms` }}
>
<div className="aa__fonts-row-name" style={{ fontFamily: f.family }}>
{f.name}
</div>
<div className="aa__fonts-row-sample" style={{ fontFamily: f.family }}>
{f.sample}
</div>
<span className="aa__fonts-row-strike" />
</div>
))}
</div>
</Reveal>
{/* 推荐替代 */}
{showAlt && (
<Reveal kind="rise" duration={900} delay={120} className="aa__fonts-col aa__fonts-col--alt">
<div className="aa__fonts-col-tag">
<span className="aa__fonts-col-mark aa__fonts-col-mark--ok"></span>
ALTERNATIVES ·
</div>
<div className="aa__fonts-list">
{BETTER_FONTS.map((f, i) => (
<div
key={f.name}
className="aa__fonts-row aa__fonts-row--alt"
style={{ animationDelay: `${i * 110}ms` }}
>
<div className="aa__fonts-row-name" style={{ fontFamily: f.family }}>
{f.name}
</div>
<div className="aa__fonts-row-sample" style={{ fontFamily: f.family }}>
{f.sample}
</div>
<span className="aa__fonts-row-tag">{f.tag}</span>
</div>
))}
</div>
</Reveal>
)}
</div>
{showAlt && (
<Reveal kind="fade" duration={620} delay={620} className="aa__fonts-foot">
<span></span>
<span className="aa__fonts-foot-dot" />
<span></span>
<span className="aa__fonts-foot-dot" />
<span></span>
<span className="aa__fonts-foot-dot" />
<span> AI </span>
</Reveal>
)}
</div>
</SceneFade>
</section>
);
}
const def: ChapterDef = {
id: 'anti-ai',
title: '第三部分 · 去 AI 味',
eyebrow: '06',
steps: 6,
theme: 'light',
Component: AntiAi,
};
export default def;

View File

@ -0,0 +1,539 @@
/* =========================================================
Chapter 07 · 第四部分 · oklch 配色
light 主题 · 顺序对齐口播稿:
原文 prompt 三层策略 大字提问 双色相条对比 收尾
========================================================= */
.ok {
position: absolute;
inset: 0;
background: var(--bg);
color: var(--fg);
font-family: var(--f-sans);
overflow: hidden;
}
/* —— 通用 prompt 源标签 —— */
.ok__src-bracket { color: var(--accent); font-weight: 600; }
.ok__src-label { color: var(--fg); font-weight: 600; letter-spacing: 0.22em; }
.ok__src-sep { color: var(--fg-faint); }
.ok__src-line { color: var(--accent); font-weight: 600; }
.ok__src-mute { color: var(--fg-mute); }
/* ===================== Scene SOURCE (step 0) ===================== */
.ok__src-scene {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 140px 140px 120px;
gap: 36px;
text-align: center;
}
.ok__src-eyebrow {
display: flex;
align-items: center;
gap: 10px;
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--fg-mute);
}
.ok__src-block {
width: 100%;
max-width: 1400px;
background: var(--bg-2);
border: 1px solid var(--line-mid);
border-left: 3px solid var(--accent);
border-radius: 2px;
padding: 36px 44px 40px;
text-align: left;
display: flex;
flex-direction: column;
gap: 20px;
}
.ok__src-line-row {
display: grid;
grid-template-columns: 56px 1fr;
align-items: baseline;
gap: 24px;
font-family: var(--f-mono);
font-size: 32px;
line-height: 1.45;
color: var(--fg);
opacity: 0;
transform: translateY(10px);
animation: okSrcLineIn 720ms cubic-bezier(.2,.8,.2,1) forwards;
}
.ok__src-line-row--1 { animation-delay: 380ms; }
.ok__src-line-row--2 { animation-delay: 980ms; }
.ok__src-line-row--3 { animation-delay: 1580ms; }
@keyframes okSrcLineIn {
to { opacity: 1; transform: none; }
}
.ok__src-num {
font-family: var(--f-mono);
font-size: 18px;
letter-spacing: 0.08em;
color: var(--fg-faint);
text-align: right;
}
.ok__src-text em.ok__src-h {
font-style: normal;
color: var(--accent);
background: linear-gradient(180deg, transparent 65%, oklch(0.860 0.060 60 / 0.7) 65%);
padding: 0 4px;
font-weight: 600;
}
.ok__src-h--1 { animation: okSrcHi 700ms 700ms cubic-bezier(.2,.8,.2,1) backwards; }
.ok__src-h--2 { animation: okSrcHi 700ms 1300ms cubic-bezier(.2,.8,.2,1) backwards; }
.ok__src-h--3 { animation: okSrcHi 700ms 1900ms cubic-bezier(.2,.8,.2,1) backwards; }
@keyframes okSrcHi {
from {
background: linear-gradient(180deg, transparent 65%, oklch(0.860 0.060 60 / 0) 65%);
color: var(--fg);
}
to {
background: linear-gradient(180deg, transparent 65%, oklch(0.860 0.060 60 / 0.7) 65%);
color: var(--accent);
}
}
.ok__src-foot {
font-family: var(--f-serif);
font-style: italic;
font-size: 28px;
color: var(--fg-mute);
}
.ok__src-foot em {
font-style: italic;
color: var(--accent);
}
/* ===================== Scene RULES (step 1) ===================== */
.ok__rules-scene {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 130px 140px 100px;
gap: 40px;
}
.ok__rules-head {
font-family: var(--f-serif);
font-weight: 400;
font-size: 72px;
line-height: 1.1;
margin: 0;
text-align: center;
}
.ok__rules-head em {
font-style: italic;
color: var(--accent);
}
.ok__rules {
width: 100%;
max-width: 1400px;
display: flex;
flex-direction: column;
gap: 18px;
}
.ok__rule {
display: grid;
grid-template-columns: 90px 1fr auto;
align-items: center;
gap: 28px;
padding: 22px 32px;
background: var(--bg-2);
border: 1px solid var(--line-mid);
border-left: 4px solid var(--line-mid);
border-radius: 3px;
transition: border-color 360ms;
}
.ok__rule--good { border-left-color: var(--accent); }
.ok__rule--ok { border-left-color: var(--accent); }
.ok__rule--bad { border-left-color: var(--crimson); opacity: 0.94; }
.ok__rule-num {
font-family: var(--f-mono);
font-size: 56px;
letter-spacing: -0.02em;
color: var(--fg-faint);
font-weight: 500;
}
.ok__rule-body {
display: flex;
flex-direction: column;
gap: 8px;
}
.ok__rule-title {
font-family: var(--f-serif);
font-size: 36px;
color: var(--fg);
line-height: 1.2;
}
.ok__rule-title em {
font-style: italic;
color: var(--accent);
}
.ok__rule-desc {
font-family: var(--f-sans);
font-size: 19px;
line-height: 1.5;
color: var(--fg-mute);
}
.ok__rule-swatches {
display: flex;
gap: 6px;
margin-top: 4px;
}
.ok__rule-swatches span {
width: 32px;
height: 32px;
border-radius: 2px;
box-shadow: inset 0 0 0 1px oklch(0 0 0 / 0.08);
animation: okSwatchIn 520ms cubic-bezier(.2,.8,.2,1) backwards;
}
.ok__rule-swatches span:nth-child(2) { animation-delay: 60ms; }
.ok__rule-swatches span:nth-child(3) { animation-delay: 120ms; }
.ok__rule-swatches span:nth-child(4) { animation-delay: 180ms; }
.ok__rule-swatches span:nth-child(5) { animation-delay: 240ms; }
.ok__rule-swatches span:nth-child(6) { animation-delay: 300ms; }
@keyframes okSwatchIn {
from { opacity: 0; transform: translateY(14px) scale(0.92); }
to { opacity: 1; transform: none; }
}
.ok__rule-mark {
font-family: var(--f-serif);
font-style: italic;
font-size: 56px;
line-height: 1;
width: 56px;
text-align: center;
}
.ok__rule-mark--good { color: var(--accent); }
.ok__rule-mark--bad { color: var(--crimson); }
/* ===================== Scene PIVOT (step 2) ===================== */
.ok__pivot {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 24px;
padding: 140px;
}
.ok__pivot-q {
font-family: var(--f-serif);
font-weight: 400;
font-size: 168px;
line-height: 1.05;
margin: 0;
color: var(--fg);
}
.ok__pivot-em {
font-family: var(--f-mono);
font-style: italic;
color: var(--accent);
font-size: 0.78em;
letter-spacing: -0.01em;
margin: 0 0.06em;
position: relative;
}
.ok__pivot-em::after {
content: '';
position: absolute;
left: 6%;
right: 6%;
bottom: 6px;
height: 8px;
background: var(--accent);
opacity: 0.22;
transform-origin: left;
animation: okPivotUnderline 1100ms 380ms cubic-bezier(.2,.8,.2,1) backwards;
}
@keyframes okPivotUnderline {
from { transform: scaleX(0); opacity: 0; }
to { transform: scaleX(1); opacity: 0.22; }
}
.ok__pivot-sub {
font-family: var(--f-serif);
font-size: 36px;
line-height: 1.4;
color: var(--fg-soft);
margin: 12px 0 0;
}
.ok__pivot-strike {
position: relative;
color: var(--fg-mute);
font-family: var(--f-mono);
font-style: italic;
padding: 0 6px;
}
.ok__pivot-strike::after {
content: '';
position: absolute;
left: 0;
right: 0;
top: 55%;
height: 3px;
background: var(--crimson);
transform-origin: left;
transform: translateY(-50%) scaleX(0);
animation: okPivotStrike 520ms 980ms cubic-bezier(.6,-0.05,.2,1.2) forwards;
opacity: 0.75;
}
@keyframes okPivotStrike {
to { transform: translateY(-50%) scaleX(1); }
}
.ok__pivot-issue {
font-family: var(--f-serif);
font-weight: 400;
font-size: 96px;
line-height: 1;
margin: 0;
color: var(--fg);
letter-spacing: -0.01em;
}
.ok__pivot-issue em {
font-style: italic;
color: var(--crimson);
}
/* ===================== Scene COMPARE (step 3) ===================== */
.ok__cmp-scene {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 130px 100px 100px;
gap: 36px;
}
.ok__cmp-cap {
font-family: var(--f-serif);
font-size: 30px;
font-style: italic;
color: var(--fg-soft);
text-align: center;
}
.ok__cmp-cap em {
font-style: italic;
color: var(--accent);
}
.ok__cmp-grid {
display: flex;
flex-direction: column;
gap: 64px;
width: 100%;
max-width: 1480px;
}
.ok__cmp-row {
position: relative;
display: flex;
flex-direction: column;
gap: 14px;
padding-bottom: 88px;
}
.ok__cmp-row-head {
display: flex;
align-items: baseline;
gap: 18px;
}
.ok__cmp-row-tag {
font-family: var(--f-mono);
font-size: 18px;
letter-spacing: 0.24em;
text-transform: uppercase;
padding: 4px 10px;
border-radius: 2px;
}
.ok__cmp-row-tag--bad {
color: var(--crimson);
background: oklch(0.560 0.200 22 / 0.10);
border: 1px solid oklch(0.560 0.200 22 / 0.45);
}
.ok__cmp-row-tag--good {
color: var(--accent-deep);
background: oklch(0.700 0.170 42 / 0.12);
border: 1px solid oklch(0.700 0.170 42 / 0.45);
}
.ok__cmp-row-formula {
font-family: var(--f-mono);
font-size: 18px;
color: var(--fg-mute);
letter-spacing: 0.04em;
}
.ok__cmp-strip {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 8px;
height: 110px;
}
.ok__cmp-swatch {
position: relative;
border-radius: 2px;
box-shadow: inset 0 0 0 1px oklch(0 0 0 / 0.06);
animation: okSwatchIn 620ms cubic-bezier(.2,.8,.2,1) backwards;
}
/* HSL 行的"黄色"格子单独脉冲一下 */
.ok__cmp-swatch.is-spot {
animation: okSwatchIn 620ms cubic-bezier(.2,.8,.2,1) backwards,
okSpotPulse 1600ms 1400ms ease-in-out infinite;
box-shadow:
inset 0 0 0 1px oklch(0 0 0 / 0.06),
0 0 0 0 oklch(0.560 0.200 22 / 0.5);
}
@keyframes okSpotPulse {
0%, 100% {
box-shadow:
inset 0 0 0 1px oklch(0 0 0 / 0.06),
0 0 0 0 oklch(0.560 0.200 22 / 0.45);
transform: translateY(0);
}
50% {
box-shadow:
inset 0 0 0 1px oklch(0 0 0 / 0.06),
0 0 0 8px oklch(0.560 0.200 22 / 0);
transform: translateY(-4px);
}
}
.ok__cmp-swatch-tick {
position: absolute;
bottom: -22px;
left: 50%;
transform: translateX(-50%);
font-family: var(--f-mono);
font-size: 11px;
letter-spacing: 0.04em;
color: var(--fg-faint);
}
/* —— 感知亮度曲线 —— */
.ok__cmp-curve {
position: absolute;
bottom: 28px;
left: 0;
right: 0;
width: 100%;
height: 60px;
pointer-events: none;
animation: okCurveIn 720ms cubic-bezier(.2,.8,.2,1) backwards;
}
.ok__cmp-curve--bad { animation-delay: 1100ms; }
.ok__cmp-curve--good { animation-delay: 1280ms; }
@keyframes okCurveIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: none; }
}
.ok__cmp-callout {
position: absolute;
bottom: -30px;
left: 0;
display: flex;
align-items: center;
gap: 10px;
font-family: var(--f-serif);
font-size: 22px;
font-style: italic;
opacity: 0;
animation: okCalloutIn 620ms cubic-bezier(.2,.8,.2,1) forwards;
}
.ok__cmp-callout--bad { color: var(--crimson); animation-delay: 1700ms; }
.ok__cmp-callout--good { color: var(--accent-deep); animation-delay: 1900ms; }
@keyframes okCalloutIn {
from { opacity: 0; transform: translateX(-8px); }
to { opacity: 1; transform: none; }
}
.ok__cmp-callout-arrow {
font-family: var(--f-mono);
font-style: normal;
}
/* ===================== Scene CLOSE (step 4) ===================== */
.ok__close {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 20px;
padding: 140px;
}
.ok__close-eyebrow {
font-family: var(--f-serif);
font-style: italic;
font-size: 36px;
color: var(--fg-mute);
}
.ok__close-line {
font-family: var(--f-serif);
font-weight: 400;
font-size: 200px;
line-height: 1;
margin: 0;
color: var(--fg);
}
.ok__close-line em {
font-style: italic;
color: var(--accent);
}
.ok__close-arrow {
font-family: var(--f-mono);
font-size: 96px;
color: var(--accent);
line-height: 1;
display: inline-block;
animation: okCloseArrow 1200ms 1100ms cubic-bezier(.6,-0.05,.2,1.2) backwards;
}
@keyframes okCloseArrow {
from { transform: translateY(40px); opacity: 0; }
to { transform: none; opacity: 1; }
}
.ok__close-caption {
font-family: var(--f-serif);
font-style: italic;
font-size: 30px;
color: var(--fg-mute);
margin-top: 16px;
}

View File

@ -0,0 +1,306 @@
import type { ChapterContext, ChapterDef } from '../types';
import { Reveal } from '../../shared/Reveal';
import { SceneFade } from '../../shared/SceneFade';
import './Oklch.css';
/**
* Chapter 07 · oklch
*
*
* 1. "他的配色策略分成了:优先用品牌色;不够就用 oklch 派生衍生色;绝对不要凭空编新色。"
* 2. "为什么是 oklch 呢?"
* 3. "传统的 HSL色彩空间有个大问题 — 感知不均匀。"
* "同样的亮度值,黄色看着比蓝色亮一大截。"
* 4. "oklch 是感知均匀的色彩空间。保持亮度和色度不变,只转色相角,出来的颜色自然就和谐。"
* 5. "这个细节看着小,但网页端高级感一下就上来了。"
*
* 5 / step 0..4
* 0 promptL41-43"点亮"
* 1 oklch
* 2 pivot "为什么是 oklch " + "HSL 有个大问题 —— 感知不均匀"
* 3 HSL vs OKLCH + 线 + spotlight
* 4 "网页端 高级感 ↑"
*/
const HUES = [0, 30, 60, 90, 120, 150, 180, 210, 240, 270, 300, 330];
/** HSL 感知亮度(粗略 sRGB BT.709 相对亮度) */
function hslPerceived(h: number): number {
const s = 0.7;
const l = 0.6;
const c = (1 - Math.abs(2 * l - 1)) * s;
const hh = h / 60;
const x = c * (1 - Math.abs((hh % 2) - 1));
let r = 0, g = 0, b = 0;
if (hh < 1) { r = c; g = x; }
else if (hh < 2) { r = x; g = c; }
else if (hh < 3) { g = c; b = x; }
else if (hh < 4) { g = x; b = c; }
else if (hh < 5) { r = x; b = c; }
else { r = c; b = x; }
const m = l - c / 2;
r += m; g += m; b += m;
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
function Oklch({ localStep }: ChapterContext) {
const at = (n: number) => localStep >= n;
const sceneSrc = localStep <= 0;
const sceneRules = localStep === 1;
const scenePivot = localStep === 2;
const sceneCompare = localStep === 3;
const sceneClose = localStep >= 4;
return (
<section className="ok">
{/* ════════ Scene SOURCEstep 0—— 原文先出 ════════ */}
<SceneFade active={sceneSrc} exitMs={420} enterDelayMs={120}>
<div className="ok__src-scene">
<Reveal kind="fade" duration={620} delay={80} className="ok__src-eyebrow">
<span className="ok__src-bracket">[</span>
<span className="ok__src-label">SYSTEM PROMPT</span>
<span className="ok__src-sep">·</span>
<span className="ok__src-line">L41-43</span>
<span className="ok__src-sep">/</span>
<span className="ok__src-mute">Color Strategy </span>
<span className="ok__src-bracket">]</span>
</Reveal>
<Reveal kind="rise" duration={1100} delay={280} className="ok__src-block">
<div className="ok__src-line-row ok__src-line-row--1">
<span className="ok__src-num">L41</span>
<span className="ok__src-text">
Color usage: try to use colors from{' '}
<em className="ok__src-h ok__src-h--1">brand / design system</em>.
</span>
</div>
<div className="ok__src-line-row ok__src-line-row--2">
<span className="ok__src-num">L42</span>
<span className="ok__src-text">
If too restrictive, use{' '}
<em className="ok__src-h ok__src-h--2">oklch</em>{' '}
to define harmonious colors that match.
</span>
</div>
<div className="ok__src-line-row ok__src-line-row--3">
<span className="ok__src-num">L43</span>
<span className="ok__src-text">
<em className="ok__src-h ok__src-h--3">Avoid inventing</em>{' '}
new colors from scratch.
</span>
</div>
</Reveal>
<Reveal kind="fade" duration={780} delay={2200} className="ok__src-foot">
<em></em>
</Reveal>
</div>
</SceneFade>
{/* ════════ Scene RULESstep 1—— 三层策略 ════════ */}
<SceneFade active={sceneRules} exitMs={420} enterDelayMs={420}>
<div className="ok__rules-scene">
<Reveal kind="rise" duration={780} delay={80} className="ok__rules-head" as="h2">
<em>线</em>
</Reveal>
<div className="ok__rules">
{/* 第 1 层 */}
<Reveal kind="rise" duration={780} delay={220} className="ok__rule ok__rule--good">
<div className="ok__rule-num">01</div>
<div className="ok__rule-body">
<div className="ok__rule-title"></div>
<div className="ok__rule-desc"> design system "再创造"</div>
<div className="ok__rule-swatches">
<span style={{ background: 'oklch(0.965 0.018 78)' }} />
<span style={{ background: 'oklch(0.700 0.170 42)' }} />
<span style={{ background: 'oklch(0.275 0.012 60)' }} />
</div>
</div>
<div className="ok__rule-mark ok__rule-mark--good"></div>
</Reveal>
{/* 第 2 层 */}
<Reveal kind="rise" duration={780} delay={420} className="ok__rule ok__rule--ok">
<div className="ok__rule-num">02</div>
<div className="ok__rule-body">
<div className="ok__rule-title"><em>oklch </em></div>
<div className="ok__rule-desc">L / C h </div>
<div className="ok__rule-swatches">
{[42, 90, 150, 200, 260, 320].map((h) => (
<span key={h} style={{ background: `oklch(0.70 0.15 ${h})` }} />
))}
</div>
</div>
<div className="ok__rule-mark ok__rule-mark--good"></div>
</Reveal>
{/* 第 3 层 */}
<Reveal kind="rise" duration={780} delay={620} className="ok__rule ok__rule--bad">
<div className="ok__rule-num">03</div>
<div className="ok__rule-body">
<div className="ok__rule-title"></div>
<div className="ok__rule-desc">"我觉得这个紫色挺好看" AI </div>
<div className="ok__rule-swatches">
<span style={{ background: '#a78bfa' }} />
<span style={{ background: '#f0abfc' }} />
<span style={{ background: '#67e8f9' }} />
<span style={{ background: '#fda4af' }} />
</div>
</div>
<div className="ok__rule-mark ok__rule-mark--bad">×</div>
</Reveal>
</div>
</div>
</SceneFade>
{/* ════════ Scene PIVOTstep 2—— 大字提问 ════════ */}
<SceneFade active={scenePivot} exitMs={420} enterDelayMs={420}>
<div className="ok__pivot">
<Reveal kind="rise" duration={1100} delay={120} className="ok__pivot-q" as="h1">
<em className="ok__pivot-em">oklch</em>
</Reveal>
<Reveal kind="rise" duration={780} delay={780} className="ok__pivot-sub" as="p">
<span className="ok__pivot-strike">HSL</span>
&nbsp;
</Reveal>
<Reveal kind="tight" duration={1100} delay={1200} className="ok__pivot-issue" as="h2">
<em></em>
</Reveal>
</div>
</SceneFade>
{/* ════════ Scene COMPAREstep 3—— 双色相条 + 曲线 ════════ */}
<SceneFade active={sceneCompare} exitMs={420} enterDelayMs={420}>
<div className="ok__cmp-scene">
<Reveal kind="fade" duration={620} delay={80} className="ok__cmp-cap">
/ · 12 <em></em>
</Reveal>
<div className="ok__cmp-grid">
{/* HSL 行 */}
<div className="ok__cmp-row">
<div className="ok__cmp-row-head">
<span className="ok__cmp-row-tag ok__cmp-row-tag--bad">HSL · </span>
<span className="ok__cmp-row-formula">hsl(h, 70%, 60%)</span>
</div>
<div className="ok__cmp-strip">
{HUES.map((h, i) => (
<div
key={`hsl-${h}`}
className={`ok__cmp-swatch ${h === 60 ? 'is-spot' : ''}`}
style={{
background: `hsl(${h} 70% 60%)`,
animationDelay: `${i * 60}ms`,
}}
>
<span className="ok__cmp-swatch-tick">{h}°</span>
</div>
))}
</div>
<svg
className="ok__cmp-curve ok__cmp-curve--bad"
viewBox="0 0 1200 80"
preserveAspectRatio="none"
>
<path
d={
'M0 80 ' +
HUES.map((h, i) => {
const x = (i / (HUES.length - 1)) * 1200;
const y = 78 - hslPerceived(h) * 70;
return `L${x.toFixed(1)} ${y.toFixed(1)}`;
}).join(' ') +
' L1200 80 Z'
}
fill="var(--crimson)"
fillOpacity="0.12"
stroke="var(--crimson)"
strokeWidth="2"
/>
</svg>
<div className="ok__cmp-callout ok__cmp-callout--bad">
<span className="ok__cmp-callout-arrow"></span>
<span> 60% </span>
</div>
</div>
{/* OKLCH 行 */}
<div className="ok__cmp-row">
<div className="ok__cmp-row-head">
<span className="ok__cmp-row-tag ok__cmp-row-tag--good">OKLCH · </span>
<span className="ok__cmp-row-formula">oklch(0.70 0.15 h)</span>
</div>
<div className="ok__cmp-strip">
{HUES.map((h, i) => (
<div
key={`ok-${h}`}
className="ok__cmp-swatch"
style={{
background: `oklch(0.70 0.15 ${h})`,
animationDelay: `${i * 60 + 220}ms`,
}}
>
<span className="ok__cmp-swatch-tick">{h}°</span>
</div>
))}
</div>
<svg
className="ok__cmp-curve ok__cmp-curve--good"
viewBox="0 0 1200 80"
preserveAspectRatio="none"
>
<path
d={'M0 80 L0 30 L1200 30 L1200 80 Z'}
fill="var(--accent)"
fillOpacity="0.10"
stroke="var(--accent)"
strokeWidth="2"
/>
</svg>
<div className="ok__cmp-callout ok__cmp-callout--good">
<span className="ok__cmp-callout-arrow"></span>
<span>L / C h </span>
</div>
</div>
</div>
</div>
</SceneFade>
{/* ════════ Scene CLOSEstep 4════════ */}
<SceneFade active={sceneClose} exitMs={420} enterDelayMs={420}>
<div className="ok__close">
<Reveal kind="fade" duration={780} delay={120} className="ok__close-eyebrow">
</Reveal>
<Reveal kind="rise" duration={1300} delay={460} className="ok__close-line" as="h1">
<em></em>
</Reveal>
<Reveal kind="rise" duration={1100} delay={1080} className="ok__close-arrow" as="span">
</Reveal>
<Reveal kind="fade" duration={780} delay={1500} className="ok__close-caption" as="p">
</Reveal>
</div>
</SceneFade>
</section>
);
}
const def: ChapterDef = {
id: 'oklch',
title: '第四部分 · oklch 配色',
eyebrow: '07',
steps: 5,
theme: 'light',
Component: Oklch,
};
export default def;

View File

@ -0,0 +1,726 @@
/* =========================================================
Chapter 08 · 第五部分 · 内容克制
light 主题 · 顺序对齐口播稿:
乔布斯 1000No/1Yes 开场 落地页 塞满 立原则 收尾
========================================================= */
.re {
position: absolute;
inset: 0;
background: var(--bg);
color: var(--fg);
font-family: var(--f-sans);
overflow: hidden;
}
/* —— 通用 prompt 源标签 —— */
.re__src-bracket { color: var(--accent); font-weight: 600; }
.re__src-label { color: var(--fg); font-weight: 600; letter-spacing: 0.22em; }
.re__src-sep { color: var(--fg-faint); }
.re__src-line { color: var(--accent); font-weight: 600; }
/* ===================== Scene JOBS (step 0) ===================== */
.re__jobs {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 24px;
padding: 120px;
}
.re__jobs-by {
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--fg-faint);
margin-bottom: 16px;
}
.re__jobs-row {
display: flex;
align-items: center;
gap: 64px;
}
.re__jobs-num {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
}
.re__jobs-num-figure {
font-family: var(--f-serif);
font-weight: 400;
font-size: 320px;
line-height: 0.9;
letter-spacing: -0.04em;
}
.re__jobs-num-label {
font-family: var(--f-mono);
font-size: 22px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--fg-mute);
}
.re__jobs-num--no .re__jobs-num-figure {
color: var(--fg-mute);
font-style: italic;
}
.re__jobs-num--yes .re__jobs-num-figure {
color: var(--accent);
font-style: italic;
position: relative;
}
.re__jobs-num--yes .re__jobs-num-figure::after {
content: '';
position: absolute;
left: -10%;
right: -10%;
bottom: 28px;
height: 14px;
background: var(--accent);
opacity: 0.18;
transform-origin: left;
animation: reJobsUnderline 1100ms 1100ms cubic-bezier(.2,.8,.2,1) backwards;
}
@keyframes reJobsUnderline {
from { transform: scaleX(0); opacity: 0; }
to { transform: scaleX(1); opacity: 0.18; }
}
.re__jobs-arrow {
font-family: var(--f-serif);
font-style: italic;
font-size: 64px;
color: var(--fg-faint);
}
.re__jobs-quote {
margin-top: 24px;
font-family: var(--f-serif);
font-style: italic;
font-size: 36px;
color: var(--fg-soft);
letter-spacing: 0.01em;
}
.re__jobs-quote em {
color: var(--accent);
font-style: italic;
font-size: 1.1em;
}
.re__jobs-src {
display: flex;
align-items: center;
gap: 8px;
margin-top: 4px;
font-family: var(--f-mono);
font-size: 13px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--fg-mute);
}
/* ===================== Scene PAGE (step 1..3) ===================== */
.re__page-scene {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
padding: 120px 120px 90px;
gap: 24px;
}
.re__page-cap {
display: flex;
align-items: baseline;
gap: 14px;
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.24em;
text-transform: uppercase;
color: var(--fg-mute);
}
.re__page-cap-tag {
color: var(--accent);
font-weight: 600;
}
.re__page-cap-sep {
color: var(--fg-faint);
}
.re__page-cap-text {
font-family: var(--f-serif);
font-style: italic;
letter-spacing: 0.02em;
text-transform: none;
font-size: 22px;
color: var(--fg-soft);
}
/* —— 浏览器外框 —— */
.re__browser {
width: 100%;
max-width: 1500px;
background: var(--bg-2);
border: 1px solid var(--line-mid);
border-radius: 4px;
overflow: hidden;
box-shadow: 0 24px 48px -28px oklch(0 0 0 / 0.18);
}
.re__browser-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
background: oklch(0.910 0.020 78);
border-bottom: 1px solid var(--line-mid);
}
.re__browser-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: oklch(0.820 0.030 78);
}
.re__browser-dot:nth-child(1) { background: oklch(0.730 0.180 28); }
.re__browser-dot:nth-child(2) { background: oklch(0.830 0.150 80); }
.re__browser-dot:nth-child(3) { background: oklch(0.770 0.140 145); }
.re__browser-url {
margin-left: 14px;
font-family: var(--f-mono);
font-size: 13px;
color: var(--fg-mute);
letter-spacing: 0.04em;
}
.re__browser-body {
background: var(--bg);
padding: 18px 22px;
height: 600px;
overflow: hidden;
}
/* —— 6 段 section —— */
.re__page {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-auto-rows: 1fr;
gap: 14px;
width: 100%;
height: 100%;
}
.re__sec {
position: relative;
display: flex;
flex-direction: column;
background: var(--bg);
border: 1px solid var(--line-mid);
border-radius: 3px;
padding: 14px 18px;
gap: 10px;
overflow: hidden;
animation: reSecIn 720ms cubic-bezier(.2,.8,.2,1) backwards;
transition:
opacity 600ms cubic-bezier(.4,0,1,1),
filter 600ms cubic-bezier(.4,0,1,1),
transform 600ms cubic-bezier(.4,0,1,1);
}
@keyframes reSecIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: none; }
}
.re__sec.is-pruned {
opacity: 0.32;
filter: grayscale(0.8);
transform: scale(0.985);
}
.re__sec-head {
display: flex;
align-items: baseline;
gap: 10px;
font-family: var(--f-mono);
font-size: 12px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--fg-mute);
}
.re__sec-num { color: var(--accent); font-weight: 600; }
.re__sec-label { color: var(--fg); font-weight: 600; }
.re__sec-dot { width: 4px; height: 4px; border-radius: 50%; background: var(--fg-faint); }
.re__sec-cn {
font-family: var(--f-serif);
font-style: italic;
letter-spacing: 0.02em;
text-transform: none;
font-size: 16px;
color: var(--fg-soft);
}
.re__sec-body {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
align-items: stretch;
justify-content: center;
}
.re__sec-bar {
height: 8px;
background: var(--line-mid);
border-radius: 2px;
}
.re__sec-bar--w70 { width: 70%; }
.re__sec-bar--w50 { width: 50%; }
/* —— Filler 内容(视觉上"塞满")—— */
.re__filler {
display: flex;
width: 100%;
height: 100%;
animation: reFillerIn 520ms cubic-bezier(.2,.8,.2,1) backwards;
}
@keyframes reFillerIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: none; }
}
.re__filler--hero {
flex-direction: column;
align-items: flex-start;
gap: 6px;
padding: 4px 0;
}
.re__filler-h {
font-family: var(--f-serif);
font-size: 24px;
font-weight: 500;
line-height: 1.1;
color: var(--fg);
}
.re__filler-sub {
font-family: var(--f-sans);
font-size: 13px;
line-height: 1.4;
color: var(--fg-mute);
}
.re__filler-cta {
margin-top: 4px;
padding: 4px 10px;
background: var(--accent);
color: var(--paper);
font-family: var(--f-mono);
font-size: 11px;
letter-spacing: 0.18em;
text-transform: uppercase;
border-radius: 2px;
}
.re__filler--feat {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(2, 1fr);
gap: 6px;
width: 100%;
height: 100%;
}
.re__filler-card {
display: flex;
flex-direction: column;
gap: 4px;
padding: 6px 8px;
background: var(--bg-2);
border: 1px solid var(--line);
border-radius: 2px;
}
.re__filler-card-icon {
font-size: 14px;
color: var(--accent);
}
.re__filler-card-t {
height: 6px;
width: 60%;
background: var(--line-mid);
border-radius: 1px;
}
.re__filler-card-l {
height: 4px;
width: 90%;
background: var(--line);
border-radius: 1px;
}
.re__filler--social {
flex-direction: row;
gap: 8px;
}
.re__filler-quote {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
padding: 6px 8px;
background: var(--bg-2);
border: 1px solid var(--line);
border-radius: 2px;
font-family: var(--f-serif);
font-style: italic;
font-size: 11px;
line-height: 1.35;
color: var(--fg-soft);
}
.re__filler-quote-mark {
font-family: var(--f-serif);
font-size: 18px;
line-height: 1;
color: var(--accent);
}
.re__filler-quote-by {
font-family: var(--f-mono);
font-size: 9px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--fg-faint);
}
.re__filler--data {
display: grid;
grid-template-columns: repeat(4, 1fr);
align-items: center;
gap: 4px;
width: 100%;
font-family: var(--f-mono);
font-size: 10px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--fg-mute);
text-align: center;
}
.re__filler--data span {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.re__filler--data b {
font-family: var(--f-serif);
font-style: italic;
font-weight: 400;
font-size: 22px;
color: var(--fg);
letter-spacing: 0;
}
.re__filler--faq {
flex-direction: column;
gap: 4px;
font-family: var(--f-sans);
font-size: 12px;
color: var(--fg-soft);
}
.re__filler--faq span {
padding: 4px 8px;
background: var(--bg-2);
border: 1px solid var(--line);
border-radius: 2px;
}
.re__filler--contact {
flex-direction: column;
gap: 4px;
}
.re__filler-input {
display: block;
width: 100%;
height: 14px;
background: var(--bg-2);
border: 1px solid var(--line-mid);
border-radius: 2px;
}
.re__filler-btn {
align-self: flex-start;
margin-top: 2px;
padding: 4px 10px;
background: var(--accent);
color: var(--paper);
font-family: var(--f-mono);
font-size: 10px;
letter-spacing: 0.18em;
text-transform: uppercase;
border-radius: 2px;
}
/* —— 红 × 修剪覆盖层 —— */
.re__sec-prune {
position: absolute;
inset: 0;
pointer-events: none;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 360ms;
}
.re__sec.is-pruned .re__sec-prune {
opacity: 1;
}
.re__sec-prune-mark {
font-family: var(--f-serif);
font-style: italic;
font-size: 96px;
line-height: 1;
color: var(--crimson);
transform: scale(0);
animation: rePruneMarkIn 520ms cubic-bezier(.6,-0.05,.2,1.2) forwards;
}
.re__sec-prune-line {
position: absolute;
left: 0;
right: 0;
top: 50%;
height: 3px;
background: var(--crimson);
transform: translateY(-50%) scaleX(0);
transform-origin: left;
animation: rePruneLine 520ms cubic-bezier(.6,-0.05,.2,1.2) forwards;
opacity: 0.55;
}
@keyframes rePruneMarkIn {
from { transform: scale(0) rotate(-12deg); opacity: 0; }
to { transform: scale(1) rotate(-12deg); opacity: 0.92; }
}
@keyframes rePruneLine {
to { transform: translateY(-50%) scaleX(1); }
}
/* 错开 6 段被砍的节奏 */
.re__sec:nth-child(1).is-pruned .re__sec-prune-mark,
.re__sec:nth-child(1).is-pruned .re__sec-prune-line { animation-delay: 80ms; }
.re__sec:nth-child(2).is-pruned .re__sec-prune-mark,
.re__sec:nth-child(2).is-pruned .re__sec-prune-line { animation-delay: 200ms; }
.re__sec:nth-child(3).is-pruned .re__sec-prune-mark,
.re__sec:nth-child(3).is-pruned .re__sec-prune-line { animation-delay: 320ms; }
.re__sec:nth-child(4).is-pruned .re__sec-prune-mark,
.re__sec:nth-child(4).is-pruned .re__sec-prune-line { animation-delay: 440ms; }
.re__sec:nth-child(5).is-pruned .re__sec-prune-mark,
.re__sec:nth-child(5).is-pruned .re__sec-prune-line { animation-delay: 560ms; }
.re__sec:nth-child(6).is-pruned .re__sec-prune-mark,
.re__sec:nth-child(6).is-pruned .re__sec-prune-line { animation-delay: 680ms; }
.re__page-verdict {
font-family: var(--f-serif);
font-size: 32px;
font-style: italic;
color: var(--fg-soft);
text-align: center;
display: flex;
align-items: center;
gap: 14px;
}
.re__page-verdict em {
font-style: italic;
color: var(--accent);
}
.re__page-verdict-mark {
font-family: var(--f-serif);
font-style: italic;
font-size: 36px;
color: var(--crimson);
}
/* ===================== Scene PRINCIPLE (step 4) ===================== */
.re__princ {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 130px 140px 100px;
gap: 32px;
text-align: center;
}
.re__princ-head {
font-family: var(--f-mono);
font-size: 16px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--accent);
margin: 0;
}
.re__princ-line {
font-family: var(--f-serif);
font-weight: 400;
font-size: 88px;
line-height: 1.15;
margin: 0;
color: var(--fg);
max-width: 1500px;
}
.re__princ-line em {
font-style: italic;
color: var(--accent);
position: relative;
}
.re__princ-line em::after {
content: '';
position: absolute;
left: 4%;
right: 4%;
bottom: 8px;
height: 8px;
background: var(--accent);
opacity: 0.22;
transform-origin: left;
animation: rePrincUnderline 1100ms 800ms cubic-bezier(.2,.8,.2,1) backwards;
}
@keyframes rePrincUnderline {
from { transform: scaleX(0); opacity: 0; }
to { transform: scaleX(1); opacity: 0.22; }
}
.re__princ-rules {
display: flex;
flex-direction: column;
gap: 18px;
margin-top: 16px;
align-items: center;
}
.re__princ-rule {
display: flex;
align-items: baseline;
gap: 18px;
padding: 14px 32px;
background: var(--bg-2);
border: 1px solid var(--line-mid);
border-radius: 999px;
font-family: var(--f-serif);
font-size: 32px;
color: var(--fg);
box-shadow: 0 12px 24px -16px oklch(0 0 0 / 0.18);
}
.re__princ-q {
color: var(--fg-soft);
font-style: italic;
}
.re__princ-arrow {
font-family: var(--f-mono);
font-style: normal;
font-size: 24px;
color: var(--fg-faint);
}
.re__princ-a {
color: var(--fg);
font-style: italic;
font-weight: 500;
}
.re__princ-a em {
font-style: italic;
color: var(--accent);
}
.re__princ-excerpt {
width: 100%;
max-width: 1100px;
margin-top: 12px;
background: var(--bg-2);
border: 1px solid var(--line-mid);
border-left: 3px solid var(--accent);
border-radius: 2px;
padding: 18px 28px 22px;
text-align: left;
}
.re__princ-excerpt-head {
display: flex;
align-items: center;
gap: 8px;
font-family: var(--f-mono);
font-size: 13px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--fg-mute);
margin-bottom: 10px;
}
.re__princ-excerpt-body {
display: flex;
align-items: flex-start;
gap: 14px;
}
.re__princ-excerpt-gt {
font-family: var(--f-mono);
font-size: 28px;
color: var(--accent);
}
.re__princ-excerpt-text {
font-family: var(--f-mono);
font-size: 20px;
line-height: 1.55;
color: var(--fg);
}
.re__princ-excerpt-text em {
font-style: normal;
color: var(--accent);
background: linear-gradient(180deg, transparent 70%, oklch(0.860 0.060 60 / 0.6) 70%);
padding: 0 2px;
}
/* ===================== Scene CLOSE (step 5) ===================== */
.re__close {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
padding: 140px;
text-align: center;
}
.re__close-line {
font-family: var(--f-serif);
font-weight: 400;
font-size: 110px;
line-height: 1.15;
margin: 0;
color: var(--fg);
max-width: 1500px;
}
.re__close-line--alt {
color: var(--fg);
}
.re__close-line em {
font-style: italic;
color: var(--accent);
}
.re__close-foot {
margin-top: 48px;
display: flex;
align-items: center;
gap: 24px;
font-family: var(--f-mono);
font-size: 22px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--fg-mute);
}
.re__close-foot-eq {
color: var(--accent);
font-family: var(--f-serif);
font-style: italic;
font-size: 36px;
letter-spacing: 0;
}

View File

@ -0,0 +1,303 @@
import type { ChapterContext, ChapterDef } from '../types';
import { Reveal } from '../../shared/Reveal';
import { SceneFade } from '../../shared/SceneFade';
import './Restraint.css';
/**
* Chapter 08 ·
*
*
* 1. "提示词里引了乔布斯一句经典的话:'一千个 No 换一个 Yes'。"
* 2. "AI 做设计有个毛病——恨不得把空间塞满。"
* "Hero、特性、评价、数据、FAQ、联系方式…一股脑全上了但每块都很平庸。"
* 3. "Claude Design 的态度很明确:每个元素得证明自己为什么该在那。"
* 4. "想加东西?先问用户。页面看着空?那是排版的问题,用留白来解决,别靠塞东西。"
* 5. "一个大胆的留白,比十个凑数的板块有表现力得多。"
*
* 6 / step 0..5
* 0 hero "1000 No · 1 Yes" + Steve Jobs + prompt(L75)
* 1 AI 6 section 线
* 2 filler "恨不得把空间塞满"
* 3 × "每个元素得证明自己"
* 4 : "想加?先问。空?用留白" + earn-its-place
* 5 : "一个大胆的留白 > 十个凑数的板块"
*/
interface Section {
id: string;
label: string;
cn: string;
}
const SECTIONS: Section[] = [
{ id: 'hero', label: 'HERO', cn: '主视觉' },
{ id: 'feat', label: 'FEATURES', cn: '6 大特性' },
{ id: 'social', label: 'TESTIMONIALS', cn: '客户评价' },
{ id: 'data', label: 'DATA', cn: '数据展示' },
{ id: 'faq', label: 'FAQ', cn: '常见问题' },
{ id: 'contact', label: 'CONTACT', cn: '联系方式' },
];
function SectionBlock({
s,
index,
filled,
pruned,
}: {
s: Section;
index: number;
filled: boolean;
pruned: boolean;
}) {
return (
<div
className={`re__sec ${filled ? 'is-filled' : ''} ${pruned ? 'is-pruned' : ''}`}
style={{ animationDelay: `${index * 90}ms` }}
>
<div className="re__sec-head">
<span className="re__sec-num">{String(index + 1).padStart(2, '0')}</span>
<span className="re__sec-label">{s.label}</span>
<span className="re__sec-dot" />
<span className="re__sec-cn">{s.cn}</span>
</div>
<div className="re__sec-body">
{!filled && (
<>
<span className="re__sec-bar re__sec-bar--w70" />
<span className="re__sec-bar re__sec-bar--w50" />
</>
)}
{filled && s.id === 'hero' && (
<div className="re__filler re__filler--hero">
<span className="re__filler-h">Build the Future. Today.</span>
<span className="re__filler-sub">The all-in-one platform for the modern team fast, simple, powerful.</span>
<span className="re__filler-cta">Get Started </span>
</div>
)}
{filled && s.id === 'feat' && (
<div className="re__filler re__filler--feat">
{[0, 1, 2, 3, 4, 5].map((i) => (
<span key={i} className="re__filler-card">
<span className="re__filler-card-icon"></span>
<span className="re__filler-card-t" />
<span className="re__filler-card-l" />
</span>
))}
</div>
)}
{filled && s.id === 'social' && (
<div className="re__filler re__filler--social">
{[0, 1, 2].map((i) => (
<span key={i} className="re__filler-quote">
<span className="re__filler-quote-mark">"</span>
Best product I've ever used. 10/10.
<span className="re__filler-quote-by"> User #{i + 1}</span>
</span>
))}
</div>
)}
{filled && s.id === 'data' && (
<div className="re__filler re__filler--data">
<span><b>10k+</b> users</span>
<span><b>99.9%</b> uptime</span>
<span><b>4.9</b> rating</span>
<span><b>+42%</b> growth</span>
</div>
)}
{filled && s.id === 'faq' && (
<div className="re__filler re__filler--faq">
<span> How does it work?</span>
<span> Is there a free trial?</span>
<span> Can I cancel anytime?</span>
</div>
)}
{filled && s.id === 'contact' && (
<div className="re__filler re__filler--contact">
<span className="re__filler-input" />
<span className="re__filler-input" />
<span className="re__filler-btn">Send Message</span>
</div>
)}
</div>
<div className="re__sec-prune" aria-hidden>
<span className="re__sec-prune-mark">×</span>
<span className="re__sec-prune-line" />
</div>
</div>
);
}
function Restraint({ localStep }: ChapterContext) {
const at = (n: number) => localStep >= n;
void at;
const sceneJobs = localStep <= 0;
const scenePage = localStep >= 1 && localStep <= 3;
const scenePrinc = localStep === 4;
const sceneClose = localStep >= 5;
const filled = localStep >= 2;
const pruned = localStep >= 3;
return (
<section className="re">
{/* ════════ Scene JOBSstep 0—— 乔布斯名言开场 ════════ */}
<SceneFade active={sceneJobs} exitMs={420} enterDelayMs={120}>
<div className="re__jobs">
<Reveal kind="fade" duration={780} delay={120} className="re__jobs-by">
STEVE JOBS ·
</Reveal>
<div className="re__jobs-row">
<Reveal kind="rise" duration={1100} delay={300} className="re__jobs-num re__jobs-num--no">
<span className="re__jobs-num-figure">1000</span>
<span className="re__jobs-num-label">No</span>
</Reveal>
<Reveal kind="fade" duration={780} delay={760} className="re__jobs-arrow" as="span">
</Reveal>
<Reveal kind="rise" duration={1100} delay={1000} className="re__jobs-num re__jobs-num--yes">
<span className="re__jobs-num-figure">1</span>
<span className="re__jobs-num-label">Yes</span>
</Reveal>
</div>
<Reveal kind="fade" duration={780} delay={1500} className="re__jobs-quote" as="p">
<em>"</em>
One thousand no's for every yes.
<em>"</em>
</Reveal>
<Reveal kind="rise" duration={780} delay={1900} className="re__jobs-src">
<span className="re__src-bracket">[</span>
<span className="re__src-label">SYSTEM PROMPT</span>
<span className="re__src-sep">·</span>
<span className="re__src-line">L77</span>
<span className="re__src-bracket">]</span>
</Reveal>
</div>
</SceneFade>
{/* ════════ Scene PAGEstep 1..3)—— AI 把 6 段塞满 → 砍 ════════ */}
<SceneFade active={scenePage} exitMs={420} enterDelayMs={420}>
<div className="re__page-scene">
<Reveal kind="fade" duration={620} delay={80} className="re__page-cap">
<span className="re__page-cap-tag">A TYPICAL "AI" LANDING PAGE</span>
<span className="re__page-cap-sep">/</span>
<span className="re__page-cap-text">
{!filled && 'AI 一上来就把 6 段全摆好了 ——'}
{filled && !pruned && '然后把每一格都塞满 ——'}
{pruned && '一一拷问:你为什么在这?'}
</span>
</Reveal>
<div className="re__browser">
<div className="re__browser-bar">
<span className="re__browser-dot" />
<span className="re__browser-dot" />
<span className="re__browser-dot" />
<span className="re__browser-url">claude-design.demo / fake-landing-page</span>
</div>
<div className="re__browser-body">
<div className="re__page">
{SECTIONS.map((s, i) => (
<SectionBlock
key={s.id}
s={s}
index={i}
filled={filled}
pruned={pruned}
/>
))}
</div>
</div>
</div>
{pruned && (
<Reveal kind="fade" duration={620} delay={620} className="re__page-verdict">
<span className="re__page-verdict-mark">×</span>
"还行" <em></em>
</Reveal>
)}
</div>
</SceneFade>
{/* ════════ Scene PRINCIPLEstep 4—— 立原则 ════════ */}
<SceneFade active={scenePrinc} exitMs={420} enterDelayMs={420}>
<div className="re__princ">
<Reveal kind="rise" duration={780} delay={80} className="re__princ-head" as="h2">
Claude Design
</Reveal>
<Reveal kind="rise" duration={1100} delay={360} className="re__princ-line" as="p">
<em></em>
</Reveal>
<div className="re__princ-rules">
<Reveal kind="rise" duration={720} delay={760} className="re__princ-rule">
<span className="re__princ-q">西</span>
<span className="re__princ-arrow"></span>
<span className="re__princ-a"></span>
</Reveal>
<Reveal kind="rise" duration={720} delay={1000} className="re__princ-rule">
<span className="re__princ-q"></span>
<span className="re__princ-arrow"></span>
<span className="re__princ-a"><em></em></span>
</Reveal>
</div>
<Reveal kind="rise" duration={780} delay={1400} className="re__princ-excerpt">
<div className="re__princ-excerpt-head">
<span className="re__src-bracket">[</span>
<span className="re__src-label">SYSTEM PROMPT</span>
<span className="re__src-sep">·</span>
<span className="re__src-line">L75</span>
<span className="re__src-bracket">]</span>
</div>
<div className="re__princ-excerpt-body">
<span className="re__princ-excerpt-gt">&gt;</span>
<span className="re__princ-excerpt-text">
Never pad a design with{' '}
<em>placeholder text, dummy sections</em>{' '}
just to fill space. <em>Every element should earn its place.</em>
</span>
</div>
</Reveal>
</div>
</SceneFade>
{/* ════════ Scene CLOSEstep 5—— 大胆留白 ════════ */}
<SceneFade active={sceneClose} exitMs={420} enterDelayMs={420}>
<div className="re__close">
<Reveal kind="rise" duration={1300} delay={120} className="re__close-line" as="h1">
<em></em>
</Reveal>
<Reveal kind="rise" duration={1300} delay={780} className="re__close-line re__close-line--alt" as="h1">
<em></em>
</Reveal>
<Reveal kind="fade" duration={780} delay={1700} className="re__close-foot">
<span></span>
<span className="re__close-foot-eq">=</span>
<span></span>
</Reveal>
</div>
</SceneFade>
</section>
);
}
const def: ChapterDef = {
id: 'restraint',
title: '第五部分 · 内容克制',
eyebrow: '08',
steps: 6,
theme: 'light',
Component: Restraint,
};
export default def;

View File

@ -0,0 +1,636 @@
/* =========================================================
Chapter 09 · 第六部分 · 验证闭环
ink 主题 · prompt 原文 Agent 自循环 fork 检查 收尾
========================================================= */
.vf {
position: absolute;
inset: 0;
background: var(--bg);
color: var(--fg);
font-family: var(--f-sans);
overflow: hidden;
}
.vf__src-bracket { color: var(--accent); font-weight: 600; }
.vf__src-label { color: var(--fg); font-weight: 600; letter-spacing: 0.22em; }
.vf__src-sep { color: var(--fg-faint); }
.vf__src-line { color: var(--accent); font-weight: 600; }
/* ===================== Scene INTRO (step 0) ===================== */
.vf__intro {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 140px;
gap: 32px;
text-align: center;
}
.vf__intro-tag {
display: flex;
align-items: center;
gap: 8px;
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--fg-mute);
}
.vf__intro-code {
display: flex;
align-items: flex-start;
gap: 28px;
padding: 32px 44px;
background: var(--bg-2);
border: 1px solid var(--line-mid);
border-left: 3px solid var(--accent);
border-radius: 2px;
font-family: var(--f-mono);
font-size: 30px;
line-height: 1.55;
text-align: left;
max-width: 1400px;
color: var(--fg-soft);
box-shadow: 0 24px 48px -28px oklch(0 0 0 / 0.45);
}
.vf__intro-code-num {
font-size: 36px;
color: var(--accent);
font-weight: 500;
line-height: 1.4;
}
.vf__intro-code-text em {
font-style: normal;
color: var(--fg);
}
.vf__intro-fn {
font-style: normal;
color: var(--accent) !important;
background: oklch(0.700 0.170 42 / 0.14);
padding: 0 8px;
border-radius: 2px;
font-weight: 500;
}
.vf__intro-title {
font-family: var(--f-serif);
font-weight: 400;
font-size: 96px;
line-height: 1.1;
margin: 0;
color: var(--fg);
max-width: 1500px;
}
.vf__intro-title em {
font-style: italic;
color: var(--accent);
}
/* ===================== Scene AGENT (step 1) ===================== */
.vf__agent-scene {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 130px 140px 100px;
gap: 40px;
}
.vf__agent-cap {
font-family: var(--f-serif);
font-size: 32px;
font-style: italic;
color: var(--fg-soft);
}
.vf__agent-cap em {
color: var(--accent);
}
.vf__board {
position: relative;
width: 100%;
max-width: 1400px;
height: 460px;
display: flex;
align-items: center;
justify-content: center;
}
.vf__board--fork {
justify-content: space-between;
padding: 0 60px;
}
/* —— Agent 节点 —— */
.vf__node {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 28px 40px 24px;
background: var(--bg-2);
border: 2px solid var(--accent);
border-radius: 4px;
min-width: 320px;
box-shadow: 0 24px 48px -22px oklch(0 0 0 / 0.5);
animation: vfNodeIn 720ms cubic-bezier(.2,.8,.2,1) backwards;
}
@keyframes vfNodeIn {
from { opacity: 0; transform: translateY(20px) scale(0.92); }
to { opacity: 1; transform: none; }
}
.vf__node--small {
min-width: 280px;
padding: 20px 30px 18px;
border-color: var(--line-strong);
box-shadow: 0 18px 32px -22px oklch(0 0 0 / 0.4);
}
.vf__node--verifier {
border: 2px dashed var(--accent);
animation: vfNodeIn 720ms 600ms cubic-bezier(.6,-0.05,.2,1.2) backwards;
box-shadow: 0 24px 48px -22px oklch(0.700 0.170 42 / 0.4);
}
.vf__node-tag {
font-family: var(--f-mono);
font-size: 12px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--accent);
font-weight: 600;
}
.vf__node-tag--alt {
color: var(--accent);
}
.vf__node-title {
font-family: var(--f-serif);
font-style: italic;
font-size: 36px;
line-height: 1.05;
color: var(--fg);
margin: 4px 0;
}
.vf__node-meta {
font-family: var(--f-mono);
font-size: 13px;
color: var(--fg-mute);
letter-spacing: 0.04em;
}
.vf__node-fresh {
color: oklch(0.770 0.140 145);
font-weight: 600;
letter-spacing: 0.06em;
}
/* —— 自循环 SVG —— */
.vf__loop {
position: absolute;
inset: -60px;
width: calc(100% + 120px);
height: calc(100% + 120px);
pointer-events: none;
}
.vf__loop-ring {
transform-origin: 100px 100px;
animation: vfLoopSpin 5s linear infinite;
}
@keyframes vfLoopSpin {
from { transform: rotate(0); }
to { transform: rotate(360deg); }
}
/* —— 自言自语气泡 —— */
.vf__bubble {
position: absolute;
top: -68px;
left: 50%;
transform: translateX(-50%);
padding: 10px 18px;
background: var(--bg);
border: 1px solid var(--line-mid);
border-radius: 999px;
font-family: var(--f-serif);
font-style: italic;
font-size: 22px;
color: var(--fg-soft);
white-space: nowrap;
animation: vfBubbleIn 620ms 800ms cubic-bezier(.2,.8,.2,1) backwards;
}
.vf__bubble::after {
content: '';
position: absolute;
bottom: -7px;
left: 50%;
transform: translateX(-50%) rotate(45deg);
width: 10px;
height: 10px;
background: var(--bg);
border-right: 1px solid var(--line-mid);
border-bottom: 1px solid var(--line-mid);
}
@keyframes vfBubbleIn {
from { opacity: 0; transform: translate(-50%, -8px); }
to { opacity: 1; transform: translate(-50%, 0); }
}
.vf__agent-verdict {
display: flex;
align-items: center;
gap: 14px;
font-family: var(--f-serif);
font-size: 30px;
font-style: italic;
color: var(--fg-soft);
}
.vf__agent-verdict em {
font-style: italic;
color: var(--crimson);
}
.vf__agent-verdict-x {
font-family: var(--f-serif);
font-style: italic;
font-size: 36px;
color: var(--crimson);
}
/* ===================== Scene FORK (step 2) ===================== */
.vf__fork-scene {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 130px 140px 100px;
gap: 32px;
}
.vf__fork-cap {
display: flex;
align-items: baseline;
gap: 18px;
font-family: var(--f-serif);
font-size: 30px;
font-style: italic;
color: var(--fg-soft);
}
.vf__fork-cap em {
color: var(--accent);
}
.vf__fork-cap-fn {
font-family: var(--f-mono);
font-style: normal;
font-size: 26px;
color: var(--accent);
background: oklch(0.700 0.170 42 / 0.14);
padding: 4px 10px;
border-radius: 2px;
}
.vf__fork-cap-arrow {
font-family: var(--f-mono);
font-style: normal;
color: var(--fg-faint);
}
.vf__fork-link {
position: absolute;
left: 50%;
top: 50%;
width: 600px;
height: 220px;
transform: translate(-50%, -50%);
pointer-events: none;
}
.vf__fork-link-branch {
stroke-dashoffset: 200;
animation: vfForkDraw 1100ms 380ms cubic-bezier(.2,.8,.2,1) forwards;
}
@keyframes vfForkDraw {
to { stroke-dashoffset: 0; }
}
/* —— 子 Agent 模拟 iframe —— */
.vf__node-iframe {
margin-top: 10px;
width: 240px;
background: var(--bg);
border: 1px solid var(--line-mid);
border-radius: 2px;
overflow: hidden;
}
.vf__node-iframe-bar {
display: flex;
align-items: center;
gap: 5px;
padding: 6px 10px;
background: oklch(0.355 0.014 60);
border-bottom: 1px solid var(--line-mid);
}
.vf__node-iframe-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--fg-faint);
}
.vf__node-iframe-url {
margin-left: 8px;
font-family: var(--f-mono);
font-size: 10px;
color: var(--fg-mute);
letter-spacing: 0.04em;
}
.vf__node-iframe-body {
display: block;
position: relative;
height: 80px;
background:
repeating-linear-gradient(
45deg,
oklch(0.965 0.018 78 / 0.04) 0 6px,
transparent 6px 12px);
overflow: hidden;
}
.vf__node-iframe-flash {
position: absolute;
inset: 0;
background: linear-gradient(
90deg,
transparent 0%,
oklch(0.700 0.170 42 / 0.45) 50%,
transparent 100%);
transform: translateX(-100%);
animation: vfFlash 1800ms 1100ms ease-in-out infinite;
}
@keyframes vfFlash {
0% { transform: translateX(-100%); }
60% { transform: translateX(100%); }
100% { transform: translateX(100%); }
}
.vf__fork-verdict {
font-family: var(--f-serif);
font-style: italic;
font-size: 26px;
color: var(--fg-mute);
}
.vf__fork-verdict em {
font-style: italic;
color: var(--accent);
}
/* ===================== Scene CHECK (step 3) ===================== */
.vf__check-scene {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 130px 140px 100px;
gap: 28px;
}
.vf__check-cap {
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--accent);
}
.vf__check-panel {
width: 100%;
max-width: 1280px;
background: var(--bg-2);
border: 1px solid var(--line-mid);
border-radius: 4px;
overflow: hidden;
box-shadow: 0 32px 60px -32px oklch(0 0 0 / 0.5);
}
.vf__check-panel-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 14px 22px;
background: oklch(0.355 0.014 60);
border-bottom: 1px solid var(--line-mid);
}
.vf__check-panel-dot {
width: 9px;
height: 9px;
border-radius: 50%;
background: oklch(0.500 0.020 60);
}
.vf__check-panel-name {
margin-left: 10px;
font-family: var(--f-mono);
font-size: 14px;
color: var(--fg-mute);
letter-spacing: 0.06em;
}
.vf__check-panel-status {
margin-left: auto;
font-family: var(--f-mono);
font-size: 12px;
letter-spacing: 0.32em;
color: var(--accent);
animation: vfStatusBlink 1.4s ease-in-out infinite;
}
@keyframes vfStatusBlink {
0%, 100% { opacity: 0.4; }
50% { opacity: 1; }
}
.vf__check-list {
display: flex;
flex-direction: column;
}
.vf__check-row {
display: grid;
grid-template-columns: 56px 240px auto 1fr 60px;
align-items: center;
gap: 18px;
padding: 18px 28px;
border-bottom: 1px dashed var(--line);
font-family: var(--f-mono);
font-size: 22px;
color: var(--fg);
opacity: 0;
animation: vfCheckIn 520ms cubic-bezier(.2,.8,.2,1) forwards;
}
.vf__check-row:last-child { border-bottom: none; }
@keyframes vfCheckIn {
from { opacity: 0; transform: translateX(-12px); }
to { opacity: 1; transform: none; }
}
.vf__check-num {
color: var(--fg-faint);
letter-spacing: 0.08em;
}
.vf__check-label {
color: var(--fg);
letter-spacing: 0.18em;
font-weight: 600;
}
.vf__check-cn {
font-family: var(--f-serif);
font-style: italic;
font-size: 20px;
color: var(--fg-mute);
letter-spacing: 0;
}
.vf__check-bar {
position: relative;
height: 4px;
background: var(--line);
border-radius: 2px;
overflow: hidden;
}
.vf__check-bar::after {
content: '';
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 0;
background: var(--accent);
animation: vfCheckBar 700ms cubic-bezier(.2,.8,.2,1) forwards;
}
.vf__check-row:nth-child(1) .vf__check-bar::after { animation-delay: 320ms; }
.vf__check-row:nth-child(2) .vf__check-bar::after { animation-delay: 540ms; }
.vf__check-row:nth-child(3) .vf__check-bar::after { animation-delay: 760ms; }
.vf__check-row:nth-child(4) .vf__check-bar::after { animation-delay: 980ms; }
@keyframes vfCheckBar {
to { width: 100%; }
}
.vf__check-mark {
position: relative;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.vf__check-mark-spin {
position: absolute;
inset: 0;
border: 2px solid var(--line-mid);
border-top-color: var(--accent);
border-radius: 50%;
animation: vfSpin 700ms linear infinite;
}
.vf__check-mark-tick {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-family: var(--f-serif);
font-style: italic;
font-size: 28px;
color: oklch(0.770 0.140 145);
opacity: 0;
animation: vfTickIn 320ms cubic-bezier(.6,-0.05,.2,1.2) forwards;
}
@keyframes vfSpin {
to { transform: rotate(360deg); }
}
@keyframes vfTickIn {
from { opacity: 0; transform: scale(0.4); }
to { opacity: 1; transform: scale(1); }
}
/* 错峰spinner 跑完才显示对勾 */
.vf__check-row:nth-child(1) .vf__check-mark-spin { animation: vfSpin 700ms linear 1; }
.vf__check-row:nth-child(2) .vf__check-mark-spin { animation: vfSpin 700ms 220ms linear 1; }
.vf__check-row:nth-child(3) .vf__check-mark-spin { animation: vfSpin 700ms 440ms linear 1; }
.vf__check-row:nth-child(4) .vf__check-mark-spin { animation: vfSpin 700ms 660ms linear 1; }
.vf__check-row:nth-child(1) .vf__check-mark-spin { animation-fill-mode: forwards; }
.vf__check-row .vf__check-mark-spin {
animation-fill-mode: forwards;
}
.vf__check-row:nth-child(1) .vf__check-mark-tick { animation-delay: 1020ms; }
.vf__check-row:nth-child(2) .vf__check-mark-tick { animation-delay: 1240ms; }
.vf__check-row:nth-child(3) .vf__check-mark-tick { animation-delay: 1460ms; }
.vf__check-row:nth-child(4) .vf__check-mark-tick { animation-delay: 1680ms; }
/* spinner 在对勾出现时淡出 */
.vf__check-row:nth-child(1) .vf__check-mark-spin { animation: vfSpin 700ms linear 1, vfSpinOut 240ms 1020ms forwards; }
.vf__check-row:nth-child(2) .vf__check-mark-spin { animation: vfSpin 700ms 220ms linear 1, vfSpinOut 240ms 1240ms forwards; }
.vf__check-row:nth-child(3) .vf__check-mark-spin { animation: vfSpin 700ms 440ms linear 1, vfSpinOut 240ms 1460ms forwards; }
.vf__check-row:nth-child(4) .vf__check-mark-spin { animation: vfSpin 700ms 660ms linear 1, vfSpinOut 240ms 1680ms forwards; }
@keyframes vfSpinOut {
to { opacity: 0; transform: scale(0.6); }
}
.vf__check-foot {
display: flex;
align-items: center;
gap: 14px;
padding: 16px 28px;
background: oklch(0.355 0.014 60);
border-top: 1px solid var(--line-mid);
font-family: var(--f-mono);
font-size: 14px;
color: var(--fg-mute);
letter-spacing: 0.04em;
}
.vf__check-foot-tag {
padding: 4px 10px;
border-radius: 2px;
letter-spacing: 0.32em;
font-weight: 600;
}
.vf__check-foot-tag--pass {
color: oklch(0.770 0.140 145);
background: oklch(0.770 0.140 145 / 0.14);
border: 1px solid oklch(0.770 0.140 145 / 0.45);
}
/* ===================== Scene CLOSE (step 4) ===================== */
.vf__close {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 12px;
padding: 140px;
}
.vf__close-l1,
.vf__close-l2 {
font-family: var(--f-serif);
font-weight: 400;
font-size: 128px;
line-height: 1.05;
margin: 0;
color: var(--fg);
}
.vf__close-l1 em,
.vf__close-l2 em {
font-style: italic;
color: var(--accent);
}
.vf__close-cap {
margin-top: 32px;
font-family: var(--f-serif);
font-style: italic;
font-size: 30px;
color: var(--fg-mute);
}

View File

@ -0,0 +1,277 @@
import type { ChapterContext, ChapterDef } from '../types';
import { Reveal } from '../../shared/Reveal';
import { SceneFade } from '../../shared/SceneFade';
import './Verification.css';
/**
* Chapter 09 ·
*
*
* 1. "第六个点,验证。"
* 2. "在开发完成后,它会 Fork 出一个独立的子 Agent然后对当前完成的网页做全面检查。"
* 3. "同一个 Agent 检查自己的输出的时候,天然会倾向于觉得没问题。"
* 4. "换一个全新的上下文,这种'自我感觉良好'就容易被打破。"
*
* L22
* "Finish: call `done` to surface the file to the user and check it loads cleanly.
* If errors, fix and `done` again. If clean, call `fork_verifier_agent`."
*
* 5 / step 0..4
* 0 hero · (L22) + "验证 —— 不信任自己的输出"
* 1 Agent self-loop"我做的没问题吧?" "确认偏误"
* 2 fork Agent "fresh context"
* 3 Agent 4 SCREENSHOT / CONSOLE / LAYOUT / JS PROBE
* 4 "换个新脑子 / 才能跳出 自我感觉良好"
*/
interface Check {
id: string;
label: string;
cn: string;
}
const CHECKS: Check[] = [
{ id: 'shot', label: 'SCREENSHOT', cn: '截图比对' },
{ id: 'cons', label: 'CONSOLE LOGS', cn: '控制台错误' },
{ id: 'lay', label: 'LAYOUT', cn: '布局偏移' },
{ id: 'js', label: 'JS PROBE', cn: 'DOM 探测' },
];
function Verification({ localStep }: ChapterContext) {
const at = (n: number) => localStep >= n;
const sceneIntro = localStep <= 0;
const sceneAgent = localStep === 1;
const sceneFork = localStep === 2;
const sceneCheck = localStep === 3;
const sceneClose = localStep >= 4;
return (
<section className="vf">
{/* ════════ Scene INTROstep 0════════ */}
<SceneFade active={sceneIntro} exitMs={420} enterDelayMs={120}>
<div className="vf__intro">
<Reveal kind="fade" duration={620} delay={80} className="vf__intro-tag">
<span className="vf__src-bracket">[</span>
<span className="vf__src-label">SYSTEM PROMPT</span>
<span className="vf__src-sep">·</span>
<span className="vf__src-line">L22</span>
<span className="vf__src-bracket">]</span>
</Reveal>
<Reveal kind="rise" duration={1100} delay={300} className="vf__intro-code">
<span className="vf__intro-code-num">5.</span>
<span className="vf__intro-code-text">
Finish: call <em className="vf__intro-fn">done</em>.<br />
If errors, <em>fix</em> and <em className="vf__intro-fn">done</em> again.<br />
If clean, call <em className="vf__intro-fn">fork_verifier_agent()</em>.
</span>
</Reveal>
<Reveal kind="rise" duration={1100} delay={1700} className="vf__intro-title" as="h1">
<em></em>
</Reveal>
</div>
</SceneFade>
{/* ════════ Scene AGENTstep 1—— 主 Agent + 自循环 ════════ */}
<SceneFade active={sceneAgent} exitMs={420} enterDelayMs={420}>
<div className="vf__agent-scene">
<Reveal kind="fade" duration={620} delay={80} className="vf__agent-cap">
Agent <em></em>
</Reveal>
<div className="vf__board">
{/* 主 Agent 节点 */}
<div className="vf__node vf__node--main">
<div className="vf__node-tag">MAIN AGENT</div>
<div className="vf__node-title">opus 4.7</div>
<div className="vf__node-meta">role · designer · L01</div>
{/* 自循环箭头SVG */}
<svg className="vf__loop" viewBox="0 0 200 200" aria-hidden>
<defs>
<marker
id="vf-loop-head"
viewBox="0 0 10 10"
refX="6" refY="5"
markerWidth="8" markerHeight="8"
orient="auto-start-reverse"
>
<path d="M0 0 L10 5 L0 10 Z" fill="var(--accent)" />
</marker>
</defs>
<circle
cx="100" cy="100" r="86"
fill="none"
stroke="var(--accent)"
strokeWidth="2"
strokeDasharray="6 8"
className="vf__loop-ring"
markerEnd="url(#vf-loop-head)"
pathLength="100"
strokeDashoffset="0"
/>
</svg>
{/* 自言自语气泡 */}
<div className="vf__bubble vf__bubble--self">
"我做的应该没问题吧?"
</div>
</div>
</div>
<Reveal kind="rise" duration={780} delay={1500} className="vf__agent-verdict">
<span className="vf__agent-verdict-x">×</span>
<em></em> <em></em>
</Reveal>
</div>
</SceneFade>
{/* ════════ Scene FORKstep 2—— fork 出子 Agent ════════ */}
<SceneFade active={sceneFork} exitMs={420} enterDelayMs={420}>
<div className="vf__fork-scene">
<Reveal kind="fade" duration={620} delay={80} className="vf__fork-cap">
<span className="vf__fork-cap-fn">fork_verifier_agent()</span>
<span className="vf__fork-cap-arrow"></span>
<span> <em></em></span>
</Reveal>
<div className="vf__board vf__board--fork">
{/* 主 Agent左侧 */}
<div className="vf__node vf__node--main vf__node--small">
<div className="vf__node-tag">MAIN AGENT</div>
<div className="vf__node-title">opus 4.7</div>
<div className="vf__node-meta"> · 稿</div>
</div>
{/* fork 连线 */}
<svg className="vf__fork-link" viewBox="0 0 600 220" preserveAspectRatio="none">
<defs>
<marker
id="vf-fork-head"
viewBox="0 0 10 10"
refX="9" refY="5"
markerWidth="10" markerHeight="10"
orient="auto"
>
<path d="M0 0 L10 5 L0 10 Z" fill="var(--accent)" />
</marker>
</defs>
{/* 主线 */}
<path
d="M0 110 L240 110"
stroke="var(--line-strong)"
strokeWidth="2"
fill="none"
/>
{/* 分叉线(弹性曲线) */}
<path
className="vf__fork-link-branch"
d="M240 110 C 320 110, 360 40, 580 40"
stroke="var(--accent)"
strokeWidth="2.5"
strokeDasharray="8 6"
fill="none"
markerEnd="url(#vf-fork-head)"
/>
{/* 节点圆点 */}
<circle cx="240" cy="110" r="6" fill="var(--accent)" />
</svg>
{/* 子 Agent右上 */}
<div className="vf__node vf__node--verifier">
<div className="vf__node-tag vf__node-tag--alt">VERIFIER AGENT</div>
<div className="vf__node-title">subagent · 0x9c</div>
<div className="vf__node-meta">
<span className="vf__node-fresh"> fresh context</span>
</div>
<div className="vf__node-iframe">
<span className="vf__node-iframe-bar">
<span className="vf__node-iframe-dot" />
<span className="vf__node-iframe-dot" />
<span className="vf__node-iframe-dot" />
<span className="vf__node-iframe-url">about:blank</span>
</span>
<span className="vf__node-iframe-body">
<span className="vf__node-iframe-flash" />
</span>
</div>
</div>
</div>
<Reveal kind="rise" duration={780} delay={1100} className="vf__fork-verdict">
<em> iframe</em> · <em></em> ·
</Reveal>
</div>
</SceneFade>
{/* ════════ Scene CHECKstep 3—— 子 Agent 跑检查清单 ════════ */}
<SceneFade active={sceneCheck} exitMs={420} enterDelayMs={420}>
<div className="vf__check-scene">
<Reveal kind="fade" duration={620} delay={80} className="vf__check-cap">
Agent ·
</Reveal>
<div className="vf__check-panel">
<div className="vf__check-panel-bar">
<span className="vf__check-panel-dot" />
<span className="vf__check-panel-dot" />
<span className="vf__check-panel-dot" />
<span className="vf__check-panel-name">verifier · subagent · 0x9c</span>
<span className="vf__check-panel-status">RUNNING</span>
</div>
<div className="vf__check-list">
{CHECKS.map((c, i) => (
<div
key={c.id}
className={`vf__check-row vf__check-row--${c.id}`}
style={{ animationDelay: `${i * 220}ms` }}
>
<span className="vf__check-num">0{i + 1}</span>
<span className="vf__check-label">{c.label}</span>
<span className="vf__check-cn">/ {c.cn}</span>
<span className="vf__check-bar" />
<span className="vf__check-mark">
<span className="vf__check-mark-spin" />
<span className="vf__check-mark-tick"></span>
</span>
</div>
))}
</div>
<div className="vf__check-foot">
<span className="vf__check-foot-tag vf__check-foot-tag--pass">PASS</span>
<span>silent on pass · Agent</span>
</div>
</div>
</div>
</SceneFade>
{/* ════════ Scene CLOSEstep 4════════ */}
<SceneFade active={sceneClose} exitMs={420} enterDelayMs={420}>
<div className="vf__close">
<Reveal kind="rise" duration={1100} delay={120} className="vf__close-l1" as="h1">
<em></em>
</Reveal>
<Reveal kind="rise" duration={1100} delay={680} className="vf__close-l2" as="h1">
<em></em>
</Reveal>
<Reveal kind="fade" duration={780} delay={1500} className="vf__close-cap" as="p">
AI 稿
</Reveal>
</div>
</SceneFade>
</section>
);
}
const def: ChapterDef = {
id: 'verification',
title: '第六部分 · 验证闭环',
eyebrow: '09',
steps: 5,
theme: 'ink',
Component: Verification,
};
export default def;

View File

@ -0,0 +1,631 @@
/* =========================================================
Chapter 10 · 过渡到 Skill
light 主题 · 回顾 三号被封 好消息 Skill 收尾
========================================================= */
.ts {
position: absolute;
inset: 0;
background: var(--bg);
color: var(--fg);
font-family: var(--f-sans);
overflow: hidden;
}
.ts__src-bracket { color: var(--accent); font-weight: 600; }
.ts__src-label { color: var(--fg); font-weight: 600; letter-spacing: 0.22em; }
/* ===================== Scene RECAP (step 0) ===================== */
.ts__recap {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 130px 140px 100px;
gap: 56px;
text-align: center;
}
.ts__recap-title {
font-family: var(--f-serif);
font-weight: 400;
font-size: 112px;
line-height: 1.1;
margin: 0;
color: var(--fg);
}
.ts__recap-title em {
font-style: italic;
color: var(--accent);
}
.ts__recap-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 14px 28px;
width: 100%;
max-width: 1100px;
}
.ts__recap-item {
display: flex;
align-items: center;
gap: 14px;
padding: 14px 22px;
background: var(--bg-2);
border: 1px solid var(--line-mid);
border-left: 3px solid var(--accent);
border-radius: 2px;
font-family: var(--f-serif);
font-size: 26px;
color: var(--fg);
opacity: 0;
animation: tsRecapIn 620ms cubic-bezier(.2,.8,.2,1) forwards;
}
@keyframes tsRecapIn {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: none; }
}
.ts__recap-num {
font-family: var(--f-mono);
font-size: 18px;
letter-spacing: 0.12em;
color: var(--fg-faint);
}
.ts__recap-name {
flex: 1;
font-style: italic;
text-align: left;
}
.ts__recap-tick {
font-family: var(--f-serif);
font-style: italic;
font-size: 28px;
color: var(--accent);
}
/* ===================== Scene PROBLEM (step 1) ===================== */
.ts__prob {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 24px;
padding: 140px;
}
.ts__prob-but {
font-family: var(--f-serif);
font-weight: 400;
font-size: 200px;
line-height: 0.95;
margin: 0;
color: var(--fg);
font-style: italic;
}
.ts__prob-line {
font-family: var(--f-serif);
font-weight: 400;
font-size: 88px;
line-height: 1.15;
margin: 0;
color: var(--fg);
}
.ts__prob-line em {
font-style: italic;
color: var(--crimson);
position: relative;
}
.ts__prob-line em::after {
content: '';
position: absolute;
left: 4%;
right: 4%;
bottom: 8px;
height: 8px;
background: var(--crimson);
opacity: 0.18;
transform-origin: left;
animation: tsProbUnderline 1100ms 1100ms cubic-bezier(.2,.8,.2,1) backwards;
}
@keyframes tsProbUnderline {
from { transform: scaleX(0); opacity: 0; }
to { transform: scaleX(1); opacity: 0.18; }
}
.ts__prob-meta {
margin-top: 32px;
display: flex;
align-items: center;
gap: 18px;
font-family: var(--f-mono);
font-size: 18px;
letter-spacing: 0.24em;
text-transform: uppercase;
color: var(--fg-mute);
}
.ts__prob-meta-dot {
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--fg-faint);
}
/* ===================== Scene BANNED (step 2) ===================== */
.ts__banned-scene {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120px 100px 90px;
gap: 40px;
}
.ts__banned-cap {
font-family: var(--f-serif);
font-style: italic;
font-size: 32px;
color: var(--fg-soft);
text-align: center;
}
.ts__banned-cap em {
color: var(--crimson);
font-style: italic;
font-weight: 500;
}
.ts__banned-row {
display: flex;
gap: 36px;
align-items: flex-start;
justify-content: center;
}
.ts__card {
position: relative;
width: 380px;
background: var(--bg-2);
border: 1px solid var(--line-mid);
border-radius: 4px;
overflow: hidden;
box-shadow: 0 24px 48px -28px oklch(0 0 0 / 0.25);
opacity: 0;
transform: translateY(0);
animation: tsCardFall 1100ms cubic-bezier(.6,-0.05,.2,1.2) forwards;
}
.ts__card--n1 { transform-origin: 70% 100%; }
.ts__card--n2 { transform-origin: 50% 100%; margin-top: 32px; }
.ts__card--n3 { transform-origin: 30% 100%; margin-top: 64px; }
@keyframes tsCardFall {
0% { opacity: 0; transform: translateY(-50px) rotate(-2deg); }
20% { opacity: 1; transform: translateY(0) rotate(0); }
60% { opacity: 1; transform: translateY(0) rotate(0); }
100% { opacity: 0.7; transform: translateY(28px) rotate(var(--fall-rot, 4deg)); }
}
.ts__card--n1 { --fall-rot: -6deg; }
.ts__card--n2 { --fall-rot: 3deg; }
.ts__card--n3 { --fall-rot: -2deg; }
.ts__card-bar {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: oklch(0.910 0.020 78);
border-bottom: 1px solid var(--line-mid);
}
.ts__card-bar-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: oklch(0.820 0.030 78);
}
.ts__card-bar-dot:nth-child(1) { background: oklch(0.730 0.180 28); }
.ts__card-bar-dot:nth-child(2) { background: oklch(0.830 0.150 80); }
.ts__card-bar-dot:nth-child(3) { background: oklch(0.770 0.140 145); }
.ts__card-bar-name {
margin-left: 10px;
font-family: var(--f-mono);
font-size: 12px;
color: var(--fg-mute);
letter-spacing: 0.04em;
}
.ts__card-body {
display: flex;
align-items: center;
gap: 18px;
padding: 22px 24px 26px;
}
.ts__card-avatar {
width: 60px;
height: 60px;
border-radius: 999px;
background: oklch(0.700 0.170 42 / 0.18);
color: var(--accent);
display: flex;
align-items: center;
justify-content: center;
font-family: var(--f-serif);
font-style: italic;
font-size: 32px;
}
.ts__card-info {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 0;
}
.ts__card-name {
font-family: var(--f-serif);
font-size: 24px;
color: var(--fg);
}
.ts__card-mail {
font-family: var(--f-mono);
font-size: 13px;
color: var(--fg-mute);
letter-spacing: 0.04em;
}
.ts__card-plan {
display: flex;
align-items: center;
gap: 8px;
font-family: var(--f-mono);
font-size: 12px;
color: var(--fg-faint);
letter-spacing: 0.06em;
margin-top: 4px;
}
.ts__card-plan-tag {
padding: 2px 8px;
background: var(--accent);
color: var(--paper);
letter-spacing: 0.18em;
border-radius: 2px;
font-weight: 600;
}
/* —— BANNED 印章 —— */
.ts__stamp {
position: absolute;
top: 50%;
left: 50%;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 16px 32px 12px;
border: 4px solid var(--crimson);
border-radius: 4px;
color: var(--crimson);
font-family: var(--f-mono);
letter-spacing: 0.08em;
text-transform: uppercase;
background: oklch(0.560 0.200 22 / 0.06);
transform: translate(-50%, -50%) rotate(-12deg) scale(0);
opacity: 0;
animation: tsStampIn 480ms cubic-bezier(.6,-0.05,.2,1.2) forwards;
}
.ts__card--n1 .ts__stamp { animation-delay: 600ms; }
.ts__card--n2 .ts__stamp { animation-delay: 920ms; }
.ts__card--n3 .ts__stamp { animation-delay: 1240ms; }
@keyframes tsStampIn {
from { opacity: 0; transform: translate(-50%, -50%) rotate(-12deg) scale(1.6); }
to { opacity: 1; transform: translate(-50%, -50%) rotate(-12deg) scale(1); }
}
.ts__stamp-text {
font-family: var(--f-mono);
font-size: 36px;
font-weight: 700;
letter-spacing: 0.16em;
}
.ts__stamp-sub {
font-family: var(--f-mono);
font-size: 11px;
letter-spacing: 0.18em;
opacity: 0.85;
}
.ts__banned-foot {
display: flex;
align-items: center;
gap: 14px;
font-family: var(--f-serif);
font-style: italic;
font-size: 28px;
color: var(--fg-mute);
}
.ts__banned-foot em {
color: var(--crimson);
font-style: italic;
font-weight: 500;
}
.ts__banned-foot-x {
font-family: var(--f-serif);
font-style: italic;
font-size: 32px;
color: var(--crimson);
}
/* ===================== Scene PIVOT (step 3) ===================== */
.ts__pivot {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 16px;
padding: 140px;
}
.ts__pivot-eyebrow {
font-family: var(--f-serif);
font-style: italic;
font-size: 36px;
color: var(--fg-mute);
}
.ts__pivot-good {
font-family: var(--f-serif);
font-weight: 400;
font-size: 168px;
line-height: 1;
margin: 0;
color: var(--fg);
}
.ts__pivot-good em {
font-style: italic;
color: var(--accent);
position: relative;
}
.ts__pivot-good em::after {
content: '';
position: absolute;
left: 4%;
right: 4%;
bottom: 12px;
height: 12px;
background: var(--accent);
opacity: 0.22;
transform-origin: left;
animation: tsPivotUnderline 1100ms 600ms cubic-bezier(.2,.8,.2,1) backwards;
}
@keyframes tsPivotUnderline {
from { transform: scaleX(0); opacity: 0; }
to { transform: scaleX(1); opacity: 0.22; }
}
.ts__pivot-line {
font-family: var(--f-serif);
font-weight: 400;
font-size: 80px;
line-height: 1.15;
margin: 0;
color: var(--fg);
}
.ts__pivot-line em {
font-style: italic;
color: var(--accent);
}
.ts__pivot-cap {
margin-top: 16px;
font-family: var(--f-serif);
font-style: italic;
font-size: 28px;
color: var(--fg-mute);
}
.ts__pivot-cap em {
color: var(--accent);
}
/* ===================== Scene SKILL (step 4) ===================== */
.ts__skill-scene {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 110px 140px 90px;
gap: 28px;
text-align: center;
}
.ts__skill-eyebrow {
display: flex;
align-items: center;
gap: 8px;
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--fg-mute);
}
.ts__skill-card {
width: 100%;
max-width: 1280px;
background: var(--bg-2);
border: 1px solid var(--line-mid);
border-radius: 4px;
overflow: hidden;
box-shadow: 0 32px 60px -28px oklch(0 0 0 / 0.25);
text-align: left;
}
.ts__skill-bar {
display: flex;
align-items: center;
gap: 6px;
padding: 10px 18px;
background: oklch(0.910 0.020 78);
border-bottom: 1px solid var(--line-mid);
}
.ts__skill-bar-dot {
width: 9px;
height: 9px;
border-radius: 50%;
background: oklch(0.820 0.030 78);
}
.ts__skill-bar-dot:nth-child(1) { background: oklch(0.730 0.180 28); }
.ts__skill-bar-dot:nth-child(2) { background: oklch(0.830 0.150 80); }
.ts__skill-bar-dot:nth-child(3) { background: oklch(0.770 0.140 145); }
.ts__skill-bar-path {
margin-left: 14px;
font-family: var(--f-mono);
font-size: 14px;
color: var(--fg-mute);
letter-spacing: 0.04em;
}
.ts__skill-body {
padding: 40px 56px 44px;
display: flex;
flex-direction: column;
gap: 12px;
}
.ts__skill-tag {
font-family: var(--f-mono);
font-size: 13px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--accent);
font-weight: 600;
}
.ts__skill-name {
font-family: var(--f-mono);
font-weight: 500;
font-size: 88px;
line-height: 1;
margin: 4px 0 8px;
color: var(--fg);
letter-spacing: -0.02em;
}
.ts__skill-desc {
font-family: var(--f-serif);
font-size: 32px;
line-height: 1.4;
color: var(--fg-soft);
margin: 0;
}
.ts__skill-desc em {
font-style: italic;
color: var(--accent);
}
.ts__skill-meta {
margin-top: 14px;
display: flex;
align-items: center;
gap: 14px;
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.24em;
text-transform: uppercase;
color: var(--fg-mute);
}
.ts__skill-meta-dot {
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--fg-faint);
}
.ts__tools-cap {
font-family: var(--f-serif);
font-style: italic;
font-size: 24px;
color: var(--fg-mute);
}
.ts__tools-row {
display: flex;
gap: 24px;
align-items: stretch;
}
.ts__tool {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 18px 36px 22px;
background: var(--bg-2);
border: 1px solid var(--line-mid);
border-top: 3px solid var(--accent);
border-radius: 3px;
min-width: 200px;
opacity: 0;
animation: tsToolIn 720ms cubic-bezier(.2,.8,.2,1) forwards;
}
@keyframes tsToolIn {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: none; }
}
.ts__tool-glyph {
font-family: var(--f-mono);
font-size: 12px;
letter-spacing: 0.32em;
color: var(--accent);
}
.ts__tool-name {
font-family: var(--f-serif);
font-size: 32px;
color: var(--fg);
}
.ts__tool-mono {
font-family: var(--f-mono);
font-size: 12px;
color: var(--fg-mute);
letter-spacing: 0.06em;
}
/* ===================== Scene CLOSE (step 5) ===================== */
.ts__close {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 16px;
padding: 140px;
}
.ts__close-l1 {
font-family: var(--f-serif);
font-weight: 400;
font-size: 110px;
line-height: 1;
margin: 0;
color: var(--fg-soft);
font-style: italic;
}
.ts__close-l2 {
font-family: var(--f-serif);
font-weight: 400;
font-size: 168px;
line-height: 1;
margin: 0;
color: var(--fg);
}
.ts__close-l2 em {
font-style: italic;
color: var(--accent);
}

View File

@ -0,0 +1,248 @@
import type { ChapterContext, ChapterDef } from '../types';
import { Reveal } from '../../shared/Reveal';
import { SceneFade } from '../../shared/SceneFade';
import './ToSkill.css';
/**
* Chapter 10 · Skill
*
*
* 1. "以上就是 Claude Design 提示词里最核心的东西。"
* 2. "但有个现实问题 — Anthropic 的产品在国内用起来都非常的难。"
* "我自己被封了三个号,彻底放弃官方渠道了。"
* "而且没有 API没法接到自己的工作流里。"
* 3. "
* Claude Design "
* 4. " Skill web-design-engineer
* "
* 5. "Claude Code、Cursor、Codex 都能直接用,人人都能成为顶级网页设计师。"
*
* 6 / step 0..5
* 0 "以上 —— 提示词原文最核心的东西" +
* 1 "但..." + "Anthropic 在国内 —— 难"
* 2 + BANNED + "没 API"
* 3 Pivot"好消息 —— 提示词 已经泄出来了"
* 4 Skill web-design-engineer +
* 5 "人人都能成为 顶级网页设计师"
*/
const RECAP_POINTS = [
'角色定位',
'工作流',
'去 AI 味',
'oklch 配色',
'内容克制',
'验证闭环',
];
const TOOLS = [
{ id: 'cc', name: 'Claude Code', mono: 'claude.code' },
{ id: 'cu', name: 'Cursor', mono: 'cursor.sh' },
{ id: 'cx', name: 'Codex', mono: 'codex.cli' },
];
function ToSkill({ localStep }: ChapterContext) {
const at = (n: number) => localStep >= n;
void at;
const sceneRecap = localStep <= 0;
const sceneProb = localStep === 1;
const sceneBanned = localStep === 2;
const scenePivot = localStep === 3;
const sceneSkill = localStep === 4;
const sceneClose = localStep >= 5;
return (
<section className="ts">
{/* ════════ Scene RECAPstep 0════════ */}
<SceneFade active={sceneRecap} exitMs={420} enterDelayMs={120}>
<div className="ts__recap">
<Reveal kind="rise" duration={1100} delay={120} className="ts__recap-title" as="h1">
<br />
<em></em>西
</Reveal>
<div className="ts__recap-list">
{RECAP_POINTS.map((p, i) => (
<div
key={p}
className="ts__recap-item"
style={{ animationDelay: `${640 + i * 120}ms` }}
>
<span className="ts__recap-num">0{i + 1}</span>
<span className="ts__recap-name">{p}</span>
<span className="ts__recap-tick"></span>
</div>
))}
</div>
</div>
</SceneFade>
{/* ════════ Scene PROBLEMstep 1—— "但..." ════════ */}
<SceneFade active={sceneProb} exitMs={420} enterDelayMs={420}>
<div className="ts__prob">
<Reveal kind="rise" duration={1100} delay={120} className="ts__prob-but" as="h1">
</Reveal>
<Reveal kind="rise" duration={1100} delay={680} className="ts__prob-line" as="h2">
Anthropic <br />
<em></em>
</Reveal>
<Reveal kind="fade" duration={780} delay={1500} className="ts__prob-meta">
<span></span>
<span className="ts__prob-meta-dot" />
<span> API</span>
<span className="ts__prob-meta-dot" />
<span></span>
</Reveal>
</div>
</SceneFade>
{/* ════════ Scene BANNEDstep 2—— 三张账号倒下 ════════ */}
<SceneFade active={sceneBanned} exitMs={420} enterDelayMs={420}>
<div className="ts__banned-scene">
<Reveal kind="fade" duration={620} delay={80} className="ts__banned-cap">
"我自己被封了 <em>三个号</em>,彻底放弃官方渠道了。"
</Reveal>
<div className="ts__banned-row">
{[1, 2, 3].map((n, i) => (
<div
key={n}
className={`ts__card ts__card--n${n}`}
style={{ animationDelay: `${i * 320 + 380}ms` }}
>
<div className="ts__card-bar">
<span className="ts__card-bar-dot" />
<span className="ts__card-bar-dot" />
<span className="ts__card-bar-dot" />
<span className="ts__card-bar-name">claude.ai / account</span>
</div>
<div className="ts__card-body">
<div className="ts__card-avatar">{['F', 'G', 'H'][i]}</div>
<div className="ts__card-info">
<div className="ts__card-name"> #{n}</div>
<div className="ts__card-mail">flower-{i + 1}@anthropic.user</div>
<div className="ts__card-plan">
<span className="ts__card-plan-tag">Pro</span>
<span>activated · 2026.0{i + 1}</span>
</div>
</div>
</div>
{/* BANNED 印章 */}
<div className="ts__stamp" aria-hidden>
<span className="ts__stamp-text">BANNED</span>
<span className="ts__stamp-sub">violation · #{n}</span>
</div>
</div>
))}
</div>
<Reveal kind="fade" duration={780} delay={1700} className="ts__banned-foot">
<span className="ts__banned-foot-x">×</span>
<em> API</em>
</Reveal>
</div>
</SceneFade>
{/* ════════ Scene PIVOTstep 3—— "好消息" ════════ */}
<SceneFade active={scenePivot} exitMs={420} enterDelayMs={420}>
<div className="ts__pivot">
<Reveal kind="fade" duration={620} delay={120} className="ts__pivot-eyebrow">
</Reveal>
<Reveal kind="rise" duration={1100} delay={320} className="ts__pivot-good" as="h1">
<em></em>
</Reveal>
<Reveal kind="rise" duration={1100} delay={920} className="ts__pivot-line" as="h2">
<em></em>
</Reveal>
<Reveal kind="fade" duration={780} delay={1700} className="ts__pivot-cap">
"Claude Design 厉害的另一半,主要就<em>这套提示词</em>里。"
</Reveal>
</div>
</SceneFade>
{/* ════════ Scene SKILLstep 4—— web-design-engineer ════════ */}
<SceneFade active={sceneSkill} exitMs={420} enterDelayMs={420}>
<div className="ts__skill-scene">
<Reveal kind="fade" duration={620} delay={80} className="ts__skill-eyebrow">
<span className="ts__src-bracket">[</span>
<span className="ts__src-label">SKILL · OPEN SOURCE</span>
<span className="ts__src-bracket">]</span>
</Reveal>
<Reveal kind="rise" duration={1100} delay={300} className="ts__skill-card">
<div className="ts__skill-bar">
<span className="ts__skill-bar-dot" />
<span className="ts__skill-bar-dot" />
<span className="ts__skill-bar-dot" />
<span className="ts__skill-bar-path">.claude / skills / web-design-engineer / SKILL.md</span>
</div>
<div className="ts__skill-body">
<div className="ts__skill-tag">SKILL.md</div>
<h2 className="ts__skill-name">web-design-engineer</h2>
<p className="ts__skill-desc">
Claude Design <br />
<em></em> Skill
</p>
<div className="ts__skill-meta">
<span> 400 </span>
<span className="ts__skill-meta-dot" />
<span></span>
<span className="ts__skill-meta-dot" />
<span></span>
</div>
</div>
</Reveal>
<Reveal kind="fade" duration={620} delay={1100} className="ts__tools-cap">
<span> </span>
</Reveal>
<div className="ts__tools-row">
{TOOLS.map((t, i) => (
<div
key={t.id}
className="ts__tool"
style={{ animationDelay: `${1300 + i * 180}ms` }}
>
<div className="ts__tool-glyph">[ {t.id} ]</div>
<div className="ts__tool-name">{t.name}</div>
<div className="ts__tool-mono">{t.mono}</div>
</div>
))}
</div>
</div>
</SceneFade>
{/* ════════ Scene CLOSEstep 5════════ */}
<SceneFade active={sceneClose} exitMs={420} enterDelayMs={420}>
<div className="ts__close">
<Reveal kind="rise" duration={1100} delay={120} className="ts__close-l1" as="h1">
</Reveal>
<Reveal kind="rise" duration={1300} delay={760} className="ts__close-l2" as="h1">
<em></em>
</Reveal>
</div>
</SceneFade>
</section>
);
}
const def: ChapterDef = {
id: 'to-skill',
title: '过渡 · Skill 是怎么来的',
eyebrow: '10',
steps: 6,
theme: 'light',
Component: ToSkill,
};
export default def;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,663 @@
import type { ReactNode } from 'react';
import type { ChapterContext, ChapterDef } from '../types';
import { Reveal } from '../../shared/Reveal';
import { SceneFade } from '../../shared/SceneFade';
import './SkillChanges.css';
/**
* Chapter 11 · Skill
*
* article/稿.md
* " Skill 400 Claude Design
* "
*
* v0
* Claude Design AI
* × AI
*
* 8 / step 0..7
* 0 Skill SKILL.md 400 / references 520
* 1 SKILL.md scroll
* 2 / vs
* 3 + 01 · + SKILL.md L79-91 /
* 4 + 02 · v0 + SKILL.md L93-101
* 5 + 03 · AI + SKILL.md L200-219
* 6 + 04 · × + advanced-patterns.md L505-516
* 7 "给 AI 一个靠谱的起点 —— 比让它自由发挥稳定"
*/
/*
*
* */
interface CodeLine {
/** 真实行号(用于左侧 gutter */
n: number;
/** 行内容 */
text: ReactNode;
/** 是否高亮(不模糊、不弱化) */
hi?: boolean;
/** 行视觉缩进(每级 16px */
indent?: number;
}
interface ExcerptProps {
filePath: string;
range: string;
caption?: ReactNode;
lines: CodeLine[];
/** delay before highlight pulse */
pulseDelay?: number;
}
function Excerpt({ filePath, range, caption, lines, pulseDelay = 600 }: ExcerptProps) {
return (
<div className="sk__ex">
<div className="sk__ex-bar">
<span className="sk__ex-bar-dot" />
<span className="sk__ex-bar-dot" />
<span className="sk__ex-bar-dot" />
<span className="sk__ex-bar-path">{filePath}</span>
<span className="sk__ex-bar-range">{range}</span>
</div>
<div className="sk__ex-body">
{lines.map((l, i) => (
<div
key={`${l.n}-${i}`}
className={`sk__ex-line ${l.hi ? 'is-hi' : ''}`}
style={{
['--ex-indent' as string]: `${(l.indent ?? 0) * 16}px`,
animationDelay: l.hi ? `${pulseDelay + i * 40}ms` : undefined,
}}
>
<span className="sk__ex-num">{l.n}</span>
<span className="sk__ex-text">{l.text}</span>
</div>
))}
</div>
{caption && <div className="sk__ex-caption">{caption}</div>}
</div>
);
}
/*
* SKILL.md
* */
const SCROLL_LINES: { t: ReactNode; hi?: boolean; mute?: boolean }[] = [
{ t: '---', mute: true },
{ t: 'name: web-design-engineer', hi: true },
{ t: 'description: |' },
{ t: ' Build high-quality visual Web artifacts using HTML / CSS / JS / React —' },
{ t: ' web pages, dashboards, prototypes, slide decks, animated demos, …' },
{ t: ' Use this skill whenever the request involves a visual deliverable.' },
{ t: '---', mute: true },
{ t: '' },
{ t: '# Web Design Engineer', hi: true },
{ t: '' },
{ t: 'Core philosophy: the bar is "stunning," not "functional".' },
{ t: 'Every pixel is intentional. Every interaction is deliberate.' },
{ t: '' },
{ t: '## Workflow', hi: true },
{ t: '' },
{ t: '### Step 1 · Understand the Requirements' },
{ t: '### Step 2 · Gather Design Context' },
{ t: '### Step 3 · Declare the Design System Before Writing Code', hi: true },
{ t: '### Step 4 · Show a v0 Draft Early', hi: true },
{ t: '### Step 5 · Full Build' },
{ t: '### Step 6 · Verification' },
{ t: '' },
{ t: '## Technical Specifications' },
{ t: '' },
{ t: '#### Three Non-negotiable Hard Rules', hi: true },
{ t: '1. Never use `const styles = {...}`' },
{ t: '2. Separate <script type="text/babel"> blocks do not share scope' },
{ t: '3. Do not use `scrollIntoView`' },
{ t: '' },
{ t: '## Design Principles' },
{ t: '' },
{ t: '### Avoid AI-Style Clichés', hi: true },
{ t: '- Overuse of gradient backgrounds' },
{ t: '- Cookie-cutter gradient buttons + large-radius cards' },
{ t: '- Overreliance on Inter / Roboto / Arial / Fraunces' },
{ t: '- Meaningless stats / numbers / icon spam' },
{ t: '- Fabricated customer logo walls' },
{ t: '' },
{ t: '### Emoji Rules' },
{ t: '**No emoji by default.** Only when the brand itself uses them.' },
{ t: '' },
{ t: '### Placeholder Philosophy' },
{ t: 'A placeholder signals "real material needed here."' },
{ t: 'A fake signals "I cut corners."', hi: true },
{ t: '' },
{ t: '### Content Principles' },
{ t: '- No filler content — every element must earn its place', hi: true },
{ t: '- Less is more — "1,000 no\'s for every yes"' },
{ t: '- Whitespace is design' },
{ t: '' },
{ t: '## Pre-delivery Checklist', hi: true },
{ t: '- [ ] Browser console shows no errors, no warnings' },
{ t: '- [ ] All colors come from the declared design system' },
{ t: '- [ ] No `scrollIntoView`' },
{ t: '- [ ] No AI clichés (purple-pink gradients, Inter/Roboto, …)' },
{ t: '- [ ] No filler content, no fabricated data' },
{ t: '- [ ] Visual quality at Dribbble / Behance showcase level', hi: true },
{ t: '' },
{ t: '## Further Reference', hi: true },
{ t: '- references/advanced-patterns.md → full code template library' },
{ t: '' },
];
function SkillChanges({ localStep }: ChapterContext) {
const sceneTree = localStep <= 0;
const sceneScroll = localStep === 1;
const sceneStrip = localStep === 2;
const sceneA = localStep === 3;
const sceneB = localStep === 4;
const sceneC = localStep === 5;
const sceneD = localStep === 6;
const sceneClose = localStep >= 7;
return (
<section className="sk">
{/* ════════ Scene TREEstep 0—— 文件目录全貌 ════════ */}
<SceneFade active={sceneTree} exitMs={420} enterDelayMs={120}>
<div className="sk__tree-scene">
<Reveal kind="fade" duration={620} delay={80} className="sk__tree-eyebrow">
<span className="sk__src-bracket">[</span>
<span className="sk__src-label">SKILL · OPEN SOURCE</span>
<span className="sk__src-bracket">]</span>
</Reveal>
<Reveal kind="rise" duration={1100} delay={260} className="sk__tree-title" as="h1">
Skill
</Reveal>
<Reveal kind="rise" duration={780} delay={620} className="sk__tree-card">
<div className="sk__tree-bar">
<span className="sk__tree-bar-dot" />
<span className="sk__tree-bar-dot" />
<span className="sk__tree-bar-dot" />
<span className="sk__tree-bar-path">~ / .claude / skills / web-design-engineer</span>
</div>
<div className="sk__tree-body">
<div className="sk__tree-row sk__tree-row--dir" style={{ animationDelay: '900ms' }}>
<span className="sk__tree-glyph"></span>
<span className="sk__tree-name">web-design-engineer<span className="sk__tree-slash">/</span></span>
<span className="sk__tree-meta">root</span>
</div>
<div className="sk__tree-row sk__tree-row--file sk__tree-row--main" style={{ animationDelay: '1080ms' }}>
<span className="sk__tree-pipe"></span>
<span className="sk__tree-name sk__tree-name--md">SKILL.md</span>
<span className="sk__tree-tag"></span>
<span className="sk__tree-meta sk__tree-meta--em"> 400 </span>
</div>
<div className="sk__tree-row sk__tree-row--dir sk__tree-row--sub" style={{ animationDelay: '1260ms' }}>
<span className="sk__tree-pipe"></span>
<span className="sk__tree-glyph"></span>
<span className="sk__tree-name">references<span className="sk__tree-slash">/</span></span>
<span className="sk__tree-tag"></span>
</div>
<div className="sk__tree-row sk__tree-row--file sk__tree-row--child" style={{ animationDelay: '1440ms' }}>
<span className="sk__tree-pipe sk__tree-pipe--child"></span>
<span className="sk__tree-name sk__tree-name--md">advanced-patterns.md</span>
<span className="sk__tree-tag"></span>
<span className="sk__tree-meta sk__tree-meta--em"> 520 </span>
</div>
</div>
</Reveal>
<Reveal kind="fade" duration={780} delay={1700} className="sk__tree-foot">
<em> 920 </em> AI <em></em>
</Reveal>
</div>
</SceneFade>
{/* ════════ Scene SCROLLstep 1—— SKILL.md 假滚动 ════════ */}
<SceneFade active={sceneScroll} exitMs={420} enterDelayMs={420}>
<div className="sk__scroll-scene">
<Reveal kind="fade" duration={620} delay={80} className="sk__scroll-eyebrow">
<span className="sk__src-bracket">[</span>
<span className="sk__src-label">FILE · SKILL.md</span>
<span className="sk__src-sep">·</span>
<span className="sk__src-line"> 400 </span>
<span className="sk__src-bracket">]</span>
</Reveal>
<Reveal kind="fade" duration={620} delay={260} className="sk__scroll-cap">
/ / AI / <em></em>
</Reveal>
<Reveal kind="rise" duration={780} delay={460} className="sk__scroll-card">
<div className="sk__scroll-bar">
<span className="sk__scroll-bar-dot" />
<span className="sk__scroll-bar-dot" />
<span className="sk__scroll-bar-dot" />
<span className="sk__scroll-bar-path">SKILL.md</span>
<span className="sk__scroll-bar-meta">utf-8 · markdown · readonly</span>
</div>
<div className="sk__scroll-frame">
<div className="sk__scroll-stream">
{/* 重复两遍以做无缝循环 */}
{[0, 1].map((loop) => (
<div key={loop} className="sk__scroll-block">
{SCROLL_LINES.map((l, i) => (
<div
key={`${loop}-${i}`}
className={`sk__scroll-line ${l.hi ? 'is-hi' : ''} ${l.mute ? 'is-mute' : ''}`}
>
<span className="sk__scroll-num">{String(i + 1).padStart(3, '0')}</span>
<span className="sk__scroll-text">{l.t || '\u00A0'}</span>
</div>
))}
</div>
))}
</div>
{/* 顶/底渐隐遮罩、扫描线由 CSS 提供 */}
<div className="sk__scroll-cursor" aria-hidden />
</div>
</Reveal>
<Reveal kind="fade" duration={780} delay={1100} className="sk__scroll-foot">
<span> </span>
<em> · 4 </em>
<span> </span>
</Reveal>
</div>
</SceneFade>
{/* ════════ Scene STRIPstep 2—— 第一刀:剥离 ════════ */}
<SceneFade active={sceneStrip} exitMs={420} enterDelayMs={420}>
<div className="sk__strip-scene">
<Reveal kind="fade" duration={620} delay={80} className="sk__chg-num">
</Reveal>
<Reveal kind="rise" duration={1100} delay={240} className="sk__chg-title" as="h2">
<em></em> Claude Design /
</Reveal>
<div className="sk__strip">
{/* 左:留下 */}
<Reveal kind="rise" duration={780} delay={500} className="sk__strip-col sk__strip-col--keep">
<div className="sk__strip-head">
<span className="sk__strip-mark sk__strip-mark--keep"></span>
<span>KEEP · </span>
</div>
<ul className="sk__strip-list">
{[
'动态角色 · 设计师身份切换',
'六步工作流',
'反 AI 味清单',
'oklch 配色',
'内容克制原则',
'验证闭环',
].map((t, i) => (
<li
key={t}
className="sk__strip-row sk__strip-row--keep"
style={{ animationDelay: `${700 + i * 80}ms` }}
>
<span className="sk__strip-row-glyph">+</span>
{t}
</li>
))}
</ul>
</Reveal>
{/* 中间 X */}
<div className="sk__strip-sep" aria-hidden>
<span className="sk__strip-sep-line" />
<span className="sk__strip-sep-knob">×</span>
<span className="sk__strip-sep-line" />
</div>
{/* 右:摘掉 */}
<Reveal kind="rise" duration={780} delay={680} className="sk__strip-col sk__strip-col--drop">
<div className="sk__strip-head">
<span className="sk__strip-mark sk__strip-mark--drop">×</span>
<span>DROP · / </span>
</div>
<ul className="sk__strip-list">
{[
'show_html()',
'show_to_user()',
'fork_verifier_agent()',
'iframe sandbox',
'tweaks panel · runtime',
'pptx export',
'GitHub 集成',
'snip 工具',
].map((t, i) => (
<li
key={t}
className="sk__strip-row sk__strip-row--drop"
style={{ animationDelay: `${900 + i * 70}ms` }}
>
<span className="sk__strip-row-glyph"></span>
<code>{t}</code>
</li>
))}
</ul>
</Reveal>
</div>
</div>
</SceneFade>
{/* ════════ Scene CHANGE 01step 3—— 先宣告设计系统 ════════ */}
<SceneFade active={sceneA} exitMs={420} enterDelayMs={420}>
<div className="sk__chg-scene">
<Reveal kind="fade" duration={620} delay={80} className="sk__chg-num">+ 01</Reveal>
<Reveal kind="rise" duration={1100} delay={240} className="sk__chg-title" as="h2">
<em></em>
</Reveal>
<div className="sk__split">
{/* 左:流程对比 */}
<Reveal kind="rise" duration={720} delay={500} className="sk__split-left">
<div className="sk__chg-flow">
<div className="sk__chg-flow-col sk__chg-flow-col--bad">
<div className="sk__chg-flow-tag">
<span className="sk__chg-flow-mark sk__chg-flow-mark--bad">×</span>
</div>
<div className="sk__chg-flow-step"></div>
<span className="sk__chg-flow-arrow"></span>
<div className="sk__chg-flow-step sk__chg-flow-step--code"> </div>
<span className="sk__chg-flow-arrow"></span>
<div className="sk__chg-flow-step sk__chg-flow-step--out"></div>
<div className="sk__chg-flow-tip"> <em></em></div>
</div>
<div className="sk__chg-flow-col sk__chg-flow-col--good">
<div className="sk__chg-flow-tag">
<span className="sk__chg-flow-mark sk__chg-flow-mark--good"></span>
</div>
<div className="sk__chg-flow-step"></div>
<span className="sk__chg-flow-arrow"></span>
<div className="sk__chg-flow-step sk__chg-flow-step--system">
<div className="sk__chg-system">
<div className="sk__chg-system-row"><span>palette</span><b>oklch · </b></div>
<div className="sk__chg-system-row"><span>fonts</span><b>Newsreader + Sora</b></div>
<div className="sk__chg-system-row"><span>spacing</span><b>4 / 8 / 16 / 32</b></div>
<div className="sk__chg-system-row"><span>radius</span><b>0 / 2 / 4</b></div>
</div>
</div>
<span className="sk__chg-flow-arrow"></span>
<div className="sk__chg-flow-step sk__chg-flow-step--code"></div>
<div className="sk__chg-flow-tip"> <em></em></div>
</div>
</div>
</Reveal>
{/* 右:原文 excerpt */}
<Reveal kind="rise" duration={780} delay={760} className="sk__split-right">
<Excerpt
filePath="SKILL.md"
range="L79 — L91"
pulseDelay={1000}
lines={[
{ n: 77, text: '', },
{ n: 78, text: '---' },
{ n: 79, text: <><b>### Step 3:</b> Declare the Design System <em>Before</em> Writing Code</>, hi: true },
{ n: 80, text: '' },
{ n: 81, text: <><b>Before writing the first line of code</b>, articulate the design system in</>, hi: true },
{ n: 82, text: 'Markdown and let the user confirm before proceeding:' , hi: true },
{ n: 83, text: '' },
{ n: 84, text: '```markdown' },
{ n: 85, text: 'Design Decisions:' },
{ n: 86, text: '- Color palette: [primary / secondary / neutral / accent]', hi: true, indent: 0 },
{ n: 87, text: '- Typography: [heading font / body font / code font]', hi: true, indent: 0 },
{ n: 88, text: '- Spacing system: [base unit and multiples]', hi: true, indent: 0 },
{ n: 89, text: '- Border-radius strategy / Shadow / Motion …' },
{ n: 90, text: '```' },
{ n: 91, text: '' },
{ n: 92, text: '### Step 4: Show a v0 Draft Early' },
{ n: 93, text: '' },
]}
caption={<>"在写第一行代码之前 —— 先<em>说清楚</em>"</>}
/>
</Reveal>
</div>
</div>
</SceneFade>
{/* ════════ Scene CHANGE 02step 4—— v0 半成品 ════════ */}
<SceneFade active={sceneB} exitMs={420} enterDelayMs={420}>
<div className="sk__chg-scene">
<Reveal kind="fade" duration={620} delay={80} className="sk__chg-num">+ 02</Reveal>
<Reveal kind="rise" duration={1100} delay={240} className="sk__chg-title" as="h2">
<em>v0 </em>
</Reveal>
<div className="sk__split">
<Reveal kind="rise" duration={720} delay={500} className="sk__split-left">
<div className="sk__v">
<div className="sk__v-card sk__v-card--v0">
<div className="sk__v-tag">
<span className="sk__v-mark sk__v-mark--good"></span>
v0 · <em></em>
</div>
<div className="sk__v-mock">
<span className="sk__v-mock-bar sk__v-mock-bar--w70" />
<span className="sk__v-mock-bar sk__v-mock-bar--w50" />
<div className="sk__v-mock-grid">
<span /><span /><span />
</div>
<span className="sk__v-mock-bar sk__v-mock-bar--w40" />
</div>
<div className="sk__v-foot"> + <em></em></div>
</div>
<div className="sk__v-vs" aria-hidden>vs</div>
<div className="sk__v-card sk__v-card--v1">
<div className="sk__v-tag">
<span className="sk__v-mark sk__v-mark--bad">×</span>
v1
</div>
<div className="sk__v-mock sk__v-mock--full">
<span className="sk__v-mock-h">Build the Future. Today.</span>
<span className="sk__v-mock-sub">Modern. Fast. Powerful.</span>
<div className="sk__v-mock-grid sk__v-mock-grid--full">
<span><b></b></span><span><b></b></span><span><b></b></span>
</div>
<span className="sk__v-mock-cta">Get Started </span>
</div>
<div className="sk__v-foot"> 3 <em></em></div>
<div className="sk__v-strike" aria-hidden>
<span className="sk__v-strike-line" />
</div>
</div>
</div>
</Reveal>
<Reveal kind="rise" duration={780} delay={760} className="sk__split-right">
<Excerpt
filePath="SKILL.md"
range="L93 — L101"
pulseDelay={1000}
lines={[
{ n: 91, text: '' },
{ n: 92, text: '---' },
{ n: 93, text: <><b>### Step 4:</b> Show a <em>v0 Draft</em> Early</>, hi: true },
{ n: 94, text: '' },
{ n: 95, text: <><b>Don\'t hold back a big reveal.</b> Before writing full components, put</>, hi: true },
{ n: 96, text: 'together a "viewable v0" using placeholders + key layout +', hi: true },
{ n: 97, text: 'the declared design system:' },
{ n: 98, text: '' },
{ n: 99, text: '- The goal of v0: let the user course-correct early' },
{ n: 100, text: '- Includes: core structure + tokens + key placeholders' },
{ n: 101, text: '- Does NOT include: content details, complete components' },
{ n: 102, text: '' },
{ n: 103, text: <>A v0 with placeholders is more valuable than a "perfect v1"</>, hi: true },
{ n: 104, text: <>that took <em>3x the time</em> if direction is wrong, scrapped.</>, hi: true },
{ n: 105, text: '' },
]}
caption={<>"粗糙的 v0 → 用户能立刻看见<em>方向</em>"</>}
/>
</Reveal>
</div>
</div>
</SceneFade>
{/* ════════ Scene CHANGE 03step 5—— 反 AI 味扩展 ════════ */}
<SceneFade active={sceneC} exitMs={420} enterDelayMs={420}>
<div className="sk__chg-scene">
<Reveal kind="fade" duration={620} delay={80} className="sk__chg-num">+ 03</Reveal>
<Reveal kind="rise" duration={1100} delay={240} className="sk__chg-title" as="h2">
<em></em> AI
</Reveal>
<div className="sk__split">
<Reveal kind="rise" duration={720} delay={500} className="sk__split-left">
<div className="sk__cd-col">
<div className="sk__cd-tag">
<span className="sk__cd-mark">+ </span>
AI ·
</div>
<ul className="sk__cd-anti">
{[
'紫粉蓝渐变背景',
'渐变按钮 + 大圆角卡片组合',
'凭空 logo 墙 / 假好评 / 假数据',
'无意义 stats / 数字 / 图标堆砌',
'emoji 当 icon 替身',
'老掉牙字体 Inter / Roboto / Arial',
].map((t, i) => (
<li
key={t}
className="sk__cd-anti-row"
style={{ animationDelay: `${700 + i * 100}ms` }}
>
<span className="sk__cd-anti-x">×</span>
{t}
</li>
))}
</ul>
</div>
</Reveal>
<Reveal kind="rise" duration={780} delay={760} className="sk__split-right">
<Excerpt
filePath="SKILL.md"
range="L200 — L219"
pulseDelay={1000}
lines={[
{ n: 198, text: '---' },
{ n: 199, text: '' },
{ n: 200, text: <><b>### Avoid AI-Style Clichés</b></>, hi: true },
{ n: 201, text: '' },
{ n: 202, text: 'Actively avoid these telltale "obviously AI" patterns:' },
{ n: 203, text: '' },
{ n: 204, text: '- Overuse of gradient backgrounds (purple-pink-blue)', hi: true },
{ n: 205, text: '- Rounded cards with a colored left-border accent' },
{ n: 206, text: '- Cookie-cutter gradient buttons + large-radius cards', hi: true },
{ n: 207, text: '- Overreliance on Inter / Roboto / Arial / Fraunces', hi: true },
{ n: 208, text: '- Meaningless stats / numbers / icon spam ("data slop")', hi: true },
{ n: 209, text: '- Fabricated customer logo walls / fake testimonial counts', hi: true },
{ n: 210, text: '' },
{ n: 211, text: '### Emoji Rules' },
{ n: 212, text: '' },
{ n: 213, text: <><b>No emoji by default.</b> Only when the brand uses them.</>, hi: true },
{ n: 214, text: '' },
{ n: 215, text: '- × Using emoji as icon substitutes' },
{ n: 216, text: '- × Using emoji as decorative filler' },
]}
caption={<> Claude Design "AI 味"<em></em></>}
/>
</Reveal>
</div>
</div>
</SceneFade>
{/* ════════ Scene CHANGE 04step 6—— 配色 × 字体配对 ════════ */}
<SceneFade active={sceneD} exitMs={420} enterDelayMs={420}>
<div className="sk__chg-scene">
<Reveal kind="fade" duration={620} delay={80} className="sk__chg-num">+ 04</Reveal>
<Reveal kind="rise" duration={1100} delay={240} className="sk__chg-title" as="h2">
<em> × </em>
</Reveal>
<div className="sk__split">
<Reveal kind="rise" duration={720} delay={500} className="sk__split-left">
<div className="sk__cd-col">
<div className="sk__cd-tag">
<span className="sk__cd-mark">+ </span>
· 5
</div>
<div className="sk__pairs">
{[
{ tag: '优雅杂志风', color: 'oklch 暖棕', font: 'Newsreader + Outfit' },
{ tag: '高端品牌', color: 'oklch 近黑', font: 'Sora + Plus Jakarta Sans' },
{ tag: '极简专业', color: 'oklch 青蓝', font: 'Outfit + Space Grotesk' },
{ tag: '活泼消费', color: 'oklch 珊瑚', font: 'Plus Jakarta Sans + Outfit' },
{ tag: '手作温度', color: 'oklch 焦糖', font: 'Caveat + Newsreader' },
].map((p, i) => (
<div
key={p.tag}
className="sk__pair"
style={{ animationDelay: `${700 + i * 110}ms` }}
>
<span className="sk__pair-tag">{p.tag}</span>
<span className="sk__pair-color">{p.color}</span>
<span className="sk__pair-x">×</span>
<span className="sk__pair-font">{p.font}</span>
</div>
))}
</div>
</div>
</Reveal>
<Reveal kind="rise" duration={780} delay={760} className="sk__split-right">
<Excerpt
filePath="references / advanced-patterns.md"
range="L505 — L516"
pulseDelay={1000}
lines={[
{ n: 503, text: '## Color × Font Pairing Reference', },
{ n: 504, text: '' },
{ n: 505, text: <>| Style | Primary (oklch) | Font Pairing | Best For |</>, hi: true },
{ n: 506, text: '|---|---|---|---|' },
{ n: 507, text: <>| Modern tech | <em>oklch(0.55 0.25 250)</em> | Space Grotesk + Inter | SaaS, AI |</> },
{ n: 508, text: <>| <b>Elegant editorial</b> | oklch(0.35 0.10 30) warm brown | <em>Newsreader + Outfit</em> | Content, blogs |</>, hi: true },
{ n: 509, text: <>| <b>Premium brand</b> | oklch(0.20 0.02 250) near-black | <em>Sora + Plus Jakarta Sans</em> | Luxury, finance |</>, hi: true },
{ n: 510, text: <>| Lively consumer | oklch(0.70 0.20 30) coral | Plus Jakarta Sans + Outfit | E-commerce |</>, hi: true },
{ n: 511, text: <>| <b>Minimal pro</b> | oklch(0.50 0.15 200) teal-blue | <em>Outfit + Space Grotesk</em> | Data, B2B |</>, hi: true },
{ n: 512, text: <>| <b>Artisan warmth</b> | oklch(0.55 0.15 80) caramel | <em>Caveat + Newsreader</em> | Food, education |</>, hi: true },
{ n: 513, text: '' },
{ n: 514, text: '> 这些配对的核心:给 AI 一个有品位的<em>起点</em>。', hi: true, },
{ n: 515, text: '' },
]}
caption={<> AI <em></em> </>}
/>
</Reveal>
</div>
</div>
</SceneFade>
{/* ════════ Scene CLOSEstep 7════════ */}
<SceneFade active={sceneClose} exitMs={420} enterDelayMs={420}>
<div className="sk__close">
<Reveal kind="rise" duration={1100} delay={120} className="sk__close-l1" as="h1">
AI <em></em>
</Reveal>
<Reveal kind="rise" duration={1100} delay={780} className="sk__close-l2" as="h2">
<em></em>
</Reveal>
</div>
</SceneFade>
</section>
);
}
const def: ChapterDef = {
id: 'skill-changes',
title: 'Skill 的关键改动',
eyebrow: '11',
steps: 8,
theme: 'light',
Component: SkillChanges,
};
export default def;

View File

@ -0,0 +1,438 @@
/* =========================================================
Chapter 12 · References / advanced-patterns.md
light 主题 · Hero 7 大模板瀑布 灵感来源 收尾
========================================================= */
.rf {
position: absolute;
inset: 0;
background: var(--bg);
color: var(--fg);
font-family: var(--f-sans);
overflow: hidden;
}
.rf__src-bracket { color: var(--accent); font-weight: 600; }
.rf__src-label { color: var(--fg); font-weight: 600; letter-spacing: 0.22em; }
/* ===================== Scene HERO (step 0) ===================== */
.rf__hero {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 24px;
padding: 140px;
}
.rf__hero-eyebrow {
display: flex;
align-items: center;
gap: 8px;
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--fg-mute);
}
.rf__hero-pre {
font-family: var(--f-serif);
font-style: italic;
font-size: 44px;
color: var(--fg-soft);
margin: 0;
}
.rf__hero-name {
font-family: var(--f-mono);
font-weight: 500;
font-size: 120px;
line-height: 1;
letter-spacing: -0.02em;
color: var(--fg);
margin: 8px 0 0;
}
.rf__hero-name em {
font-style: italic;
color: var(--accent);
position: relative;
}
.rf__hero-name em::after {
content: '';
position: absolute;
left: 1%;
right: 1%;
bottom: 8px;
height: 10px;
background: var(--accent);
opacity: 0.18;
transform-origin: left;
animation: rfHeroUnderline 1300ms 1100ms cubic-bezier(.2,.8,.2,1) backwards;
}
@keyframes rfHeroUnderline {
from { transform: scaleX(0); opacity: 0; }
to { transform: scaleX(1); opacity: 0.18; }
}
.rf__hero-dim {
color: var(--fg-faint);
font-style: italic;
font-weight: 400;
}
.rf__hero-meta {
margin-top: 16px;
display: flex;
align-items: center;
gap: 18px;
font-family: var(--f-mono);
font-size: 16px;
letter-spacing: 0.24em;
text-transform: uppercase;
color: var(--fg-mute);
}
.rf__hero-meta-dot {
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--fg-faint);
}
/* ===================== Scene LIST (step 1) ===================== */
.rf__list-scene {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 110px 100px 90px;
gap: 32px;
}
.rf__list-cap {
font-family: var(--f-serif);
font-style: italic;
font-size: 32px;
color: var(--fg-soft);
text-align: center;
}
.rf__list-cap em {
color: var(--accent);
font-style: italic;
font-weight: 500;
}
.rf__grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 18px;
width: 100%;
max-width: 1640px;
}
.rf__card {
position: relative;
display: flex;
flex-direction: column;
gap: 10px;
padding: 22px 24px 18px;
background: var(--bg-2);
border: 1px solid var(--line-mid);
border-top: 3px solid var(--accent);
border-radius: 3px;
min-height: 240px;
opacity: 0;
animation: rfCardIn 720ms cubic-bezier(.2,.8,.2,1) forwards;
transition: transform 360ms, box-shadow 360ms;
}
@keyframes rfCardIn {
from { opacity: 0; transform: translateY(28px); }
to { opacity: 1; transform: none; }
}
.rf__card-num {
display: flex;
align-items: baseline;
justify-content: space-between;
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--accent);
font-weight: 600;
}
.rf__card-glyph {
font-family: var(--f-serif);
font-style: italic;
font-size: 28px;
color: var(--fg-faint);
letter-spacing: 0;
line-height: 1;
}
.rf__card-name {
font-family: var(--f-mono);
font-size: 17px;
color: var(--fg);
letter-spacing: 0.02em;
font-weight: 500;
margin-top: 4px;
}
.rf__card-cn {
font-family: var(--f-serif);
font-style: italic;
font-size: 26px;
color: var(--fg);
line-height: 1.15;
}
.rf__card-desc {
flex: 1;
font-family: var(--f-sans);
font-size: 14px;
line-height: 1.55;
color: var(--fg-mute);
}
.rf__card-foot {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 8px;
padding-top: 10px;
border-top: 1px dashed var(--line);
font-family: var(--f-mono);
font-size: 11px;
color: var(--fg-faint);
letter-spacing: 0.04em;
}
.rf__card-foot-arrow {
color: var(--accent);
font-size: 14px;
}
/* —— 第 8 格 hint —— */
.rf__card--hint {
background: oklch(0.700 0.170 42 / 0.06);
border: 1px dashed var(--accent);
border-top: 3px solid var(--accent);
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 14px;
justify-content: center;
}
.rf__card-hint-eyebrow {
font-family: var(--f-mono);
font-size: 13px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--accent);
font-weight: 600;
}
.rf__card-hint-line {
font-family: var(--f-serif);
font-style: italic;
font-size: 22px;
line-height: 1.4;
color: var(--fg-soft);
}
.rf__card-hint-line em {
color: var(--accent);
font-style: italic;
}
/* ===================== Scene ORIGIN (step 2) ===================== */
.rf__origin {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 130px 140px 90px;
gap: 32px;
text-align: center;
}
.rf__origin-eyebrow {
font-family: var(--f-serif);
font-style: italic;
font-size: 36px;
color: var(--fg-mute);
}
.rf__origin-row {
display: grid;
grid-template-columns: 1fr 200px 1fr;
align-items: stretch;
gap: 0;
width: 100%;
max-width: 1500px;
}
.rf__origin-card {
display: flex;
flex-direction: column;
gap: 16px;
padding: 32px 36px 36px;
background: var(--bg-2);
border: 1px solid var(--line-mid);
border-radius: 4px;
text-align: left;
box-shadow: 0 24px 48px -28px oklch(0 0 0 / 0.22);
}
.rf__origin-card--src {
border-left: 3px solid var(--fg-faint);
opacity: 0.85;
}
.rf__origin-card--dst {
border-left: 3px solid var(--accent);
}
.rf__origin-card-tag {
display: flex;
align-items: center;
gap: 8px;
font-family: var(--f-mono);
font-size: 12px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--fg-mute);
}
.rf__origin-card-tag--dst .rf__src-label {
color: var(--accent);
}
.rf__origin-card-fn {
font-family: var(--f-mono);
font-size: 36px;
color: var(--fg);
letter-spacing: -0.01em;
line-height: 1.2;
}
.rf__origin-card--src .rf__origin-card-fn-name {
color: var(--fg);
}
.rf__origin-card--dst .rf__origin-card-fn-name {
color: var(--accent);
}
.rf__origin-card-fn-paren {
color: var(--fg-faint);
}
.rf__origin-card-desc {
font-family: var(--f-serif);
font-style: italic;
font-size: 22px;
line-height: 1.45;
color: var(--fg-soft);
}
.rf__origin-card-desc em {
color: var(--accent);
font-style: italic;
}
/* —— 中间提炼箭头 —— */
.rf__origin-arrow {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
}
.rf__origin-arrow-line {
position: absolute;
top: 50%;
left: 8px;
right: 8px;
height: 2px;
background: linear-gradient(90deg,
var(--fg-faint) 0%,
var(--accent) 100%);
transform: translateY(-50%) scaleX(0);
transform-origin: left;
animation: rfOriginLine 720ms 200ms cubic-bezier(.2,.8,.2,1) forwards;
}
@keyframes rfOriginLine {
to { transform: translateY(-50%) scaleX(1); }
}
.rf__origin-arrow-text {
position: relative;
z-index: 2;
padding: 4px 12px;
background: var(--bg);
border: 1px solid var(--accent);
border-radius: 2px;
font-family: var(--f-mono);
font-size: 13px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--accent);
font-weight: 600;
}
.rf__origin-arrow-head {
position: absolute;
top: 50%;
right: 0;
transform: translateY(-50%);
font-family: var(--f-mono);
font-size: 28px;
color: var(--accent);
z-index: 2;
background: var(--bg);
padding: 0 4px;
}
.rf__origin-foot {
font-family: var(--f-serif);
font-style: italic;
font-size: 28px;
color: var(--fg-soft);
}
.rf__origin-foot em {
color: var(--accent);
font-style: italic;
font-weight: 500;
}
/* ===================== Scene CLOSE (step 3) ===================== */
.rf__close {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 18px;
padding: 140px;
}
.rf__close-l1 {
font-family: var(--f-serif);
font-weight: 400;
font-size: 128px;
line-height: 1;
margin: 0;
color: var(--fg);
}
.rf__close-l2 {
font-family: var(--f-serif);
font-weight: 400;
font-size: 64px;
line-height: 1.2;
margin: 0;
color: var(--fg-soft);
}
.rf__close-l1 em,
.rf__close-l2 em {
font-style: italic;
color: var(--accent);
}

View File

@ -0,0 +1,197 @@
import type { ChapterContext, ChapterDef } from '../types';
import { Reveal } from '../../shared/Reveal';
import { SceneFade } from '../../shared/SceneFade';
import './References.css';
/**
* Chapter 12 · References ·
*
* article/稿.md L218
* "Skill references
* Claude Design `copy_starter_component` "
*
* 4 / step 0..3
* 0 hero · references / advanced-patterns.md +
* 1 7 ·
* 2 · Claude Design copy_starter_component
* 3 "给 AI 提供高质量的起点脚手架 —— 而非从零硬画"
*/
interface Tpl {
id: string;
num: string;
name: string;
cn: string;
desc: string;
glyph: string;
}
const TEMPLATES: Tpl[] = [
{ id: 'slide', num: '01', name: 'Responsive Slide Engine', cn: '响应式幻灯片引擎', desc: '1920×1080 自适应缩放 / 1-indexed 标号 / localStorage 续播', glyph: '▭' },
{ id: 'frame', num: '02', name: 'Device Simulation Frames', cn: '设备模拟外框', desc: 'iPhone / Android / 浏览器窗口 —— 让原型像在真机里', glyph: '▢' },
{ id: 'tweak', num: '03', name: 'Tweaks Panel', cn: '运行时参数面板', desc: '右下角浮动面板:主题 / 字号 / 暗色 / 间距 一键切', glyph: '⚙' },
{ id: 'time', num: '04', name: 'Animation Timeline Engine', cn: '动画时间线引擎', desc: 'useTime + Easing + interpolate —— 时间轴可拖拽', glyph: '⌁' },
{ id: 'canvas', num: '05', name: 'Design Canvas', cn: '多方案对比画布', desc: '把 N 个变体并排铺开,让用户一眼挑出来', glyph: '◫' },
{ id: 'dark', num: '06', name: 'Dark Mode Toggle', cn: '暗色模式切换', desc: 'prefers-color-scheme + 手动覆盖token 一键翻面', glyph: '◐' },
{ id: 'data', num: '07', name: 'Data Visualization', cn: '数据可视化模板', desc: 'Chart.js / D3 / oklch palette —— data-ink 比优先', glyph: '◢' },
];
function References({ localStep }: ChapterContext) {
const sceneHero = localStep <= 0;
const sceneList = localStep === 1;
const sceneOrigin = localStep === 2;
const sceneClose = localStep >= 3;
return (
<section className="rf">
{/* ════════ Scene HEROstep 0════════ */}
<SceneFade active={sceneHero} exitMs={420} enterDelayMs={120}>
<div className="rf__hero">
<Reveal kind="fade" duration={620} delay={80} className="rf__hero-eyebrow">
<span className="rf__src-bracket">[</span>
<span className="rf__src-label">SKILL · </span>
<span className="rf__src-bracket">]</span>
</Reveal>
<Reveal kind="rise" duration={1100} delay={260} className="rf__hero-pre" as="p">
Skill
</Reveal>
<Reveal kind="rise" duration={1300} delay={620} className="rf__hero-name" as="h1">
<span className="rf__hero-dim">references / </span>
<em>advanced-patterns.md</em>
</Reveal>
<Reveal kind="rise" duration={780} delay={1300} className="rf__hero-meta">
<span> 520 </span>
<span className="rf__hero-meta-dot" />
<span>7 </span>
<span className="rf__hero-meta-dot" />
<span></span>
</Reveal>
</div>
</SceneFade>
{/* ════════ Scene LISTstep 1—— 7 大模板瀑布 ════════ */}
<SceneFade active={sceneList} exitMs={420} enterDelayMs={420}>
<div className="rf__list-scene">
<Reveal kind="fade" duration={620} delay={80} className="rf__list-cap">
7 <em></em> · AI
</Reveal>
<div className="rf__grid">
{TEMPLATES.map((t, i) => (
<div
key={t.id}
className={`rf__card rf__card--${t.id}`}
style={{ animationDelay: `${260 + i * 110}ms` }}
>
<div className="rf__card-num">
<span>{t.num}</span>
<span className="rf__card-glyph">{t.glyph}</span>
</div>
<div className="rf__card-name">{t.name}</div>
<div className="rf__card-cn">{t.cn}</div>
<div className="rf__card-desc">{t.desc}</div>
<div className="rf__card-foot">
<span className="rf__card-foot-mono">references/advanced-patterns.md</span>
<span className="rf__card-foot-arrow"></span>
</div>
</div>
))}
{/* 第 8 格补一个"摘自原始 Skill"的小标签 */}
<div
className="rf__card rf__card--hint"
style={{ animationDelay: `${260 + 7 * 110}ms` }}
>
<div className="rf__card-hint-eyebrow">+ </div>
<div className="rf__card-hint-line">
<em></em><br />
"AI 自由发挥"
</div>
</div>
</div>
</div>
</SceneFade>
{/* ════════ Scene ORIGINstep 2—— 灵感来源 ════════ */}
<SceneFade active={sceneOrigin} exitMs={420} enterDelayMs={420}>
<div className="rf__origin">
<Reveal kind="fade" duration={620} delay={80} className="rf__origin-eyebrow">
</Reveal>
<div className="rf__origin-row">
{/* 左Claude Design 原始函数 */}
<Reveal kind="rise" duration={780} delay={300} className="rf__origin-card rf__origin-card--src">
<div className="rf__origin-card-tag">
<span className="rf__src-bracket">[</span>
<span className="rf__src-label">CLAUDE DESIGN · TOOL</span>
<span className="rf__src-bracket">]</span>
</div>
<div className="rf__origin-card-fn">
<span className="rf__origin-card-fn-name">copy_starter_component</span>
<span className="rf__origin-card-fn-paren">()</span>
</div>
<div className="rf__origin-card-desc">
Agent <em></em> <br />
<em>"自由发挥"</em>
</div>
</Reveal>
{/* 中:箭头 */}
<Reveal kind="fade" duration={780} delay={780} className="rf__origin-arrow" as="span">
<span className="rf__origin-arrow-line" />
<span className="rf__origin-arrow-text"></span>
<span className="rf__origin-arrow-head"></span>
</Reveal>
{/* 右references */}
<Reveal kind="rise" duration={780} delay={900} className="rf__origin-card rf__origin-card--dst">
<div className="rf__origin-card-tag rf__origin-card-tag--dst">
<span className="rf__src-bracket">[</span>
<span className="rf__src-label">SKILL · references/</span>
<span className="rf__src-bracket">]</span>
</div>
<div className="rf__origin-card-fn">
<span className="rf__origin-card-fn-name">advanced-patterns.md</span>
</div>
<div className="rf__origin-card-desc">
7 <em></em> <br />
Claude Code · Cursor · Codex import
</div>
</Reveal>
</div>
<Reveal kind="fade" duration={780} delay={1500} className="rf__origin-foot">
Anthropic <em></em>
</Reveal>
</div>
</SceneFade>
{/* ════════ Scene CLOSEstep 3════════ */}
<SceneFade active={sceneClose} exitMs={420} enterDelayMs={420}>
<div className="rf__close">
<Reveal kind="rise" duration={1100} delay={120} className="rf__close-l1" as="h1">
<em></em>
</Reveal>
<Reveal kind="rise" duration={1100} delay={780} className="rf__close-l2" as="h2">
&nbsp; AI <em></em>
</Reveal>
</div>
</SceneFade>
</section>
);
}
const def: ChapterDef = {
id: 'references',
title: 'references · 高级模板库',
eyebrow: '12',
steps: 4,
theme: 'light',
Component: References,
};
export default def;

View File

@ -0,0 +1,464 @@
/* =========================================================
Chapter 13 · 收尾 · 85 95
light 主题 · 公允声明 大字数字 三组词 琐碎规则 质变
========================================================= */
.cl {
position: absolute;
inset: 0;
background: var(--bg);
color: var(--fg);
font-family: var(--f-sans);
overflow: hidden;
}
/* ===================== Scene FAIR (step 0) ===================== */
.cl__fair {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 32px;
padding: 140px;
}
.cl__fair-eyebrow {
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--fg-mute);
}
.cl__fair-line {
font-family: var(--f-serif);
font-weight: 400;
font-size: 96px;
line-height: 1.1;
margin: 0;
color: var(--fg);
max-width: 1500px;
}
.cl__fair-line em {
font-style: italic;
color: var(--accent);
position: relative;
}
.cl__fair-line em::after {
content: '';
position: absolute;
left: 4%;
right: 4%;
bottom: 8px;
height: 8px;
background: var(--accent);
opacity: 0.2;
transform-origin: left;
animation: clFairUnderline 1100ms 800ms cubic-bezier(.2,.8,.2,1) backwards;
}
@keyframes clFairUnderline {
from { transform: scaleX(0); opacity: 0; }
to { transform: scaleX(1); opacity: 0.2; }
}
.cl__fair-cap {
font-family: var(--f-serif);
font-style: italic;
font-size: 36px;
line-height: 1.5;
color: var(--fg-mute);
max-width: 1300px;
margin: 0;
}
.cl__fair-cap em {
color: var(--accent);
font-style: italic;
}
/* ===================== Scene JUMP (step 1) ===================== */
.cl__jump {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 32px;
padding: 100px;
}
.cl__jump-eyebrow {
font-family: var(--f-serif);
font-style: italic;
font-size: 36px;
color: var(--fg-mute);
}
.cl__jump-row {
display: flex;
align-items: center;
gap: 56px;
}
.cl__jump-num {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.cl__jump-num-figure {
font-family: var(--f-serif);
font-weight: 400;
font-size: 280px;
line-height: 0.9;
letter-spacing: -0.04em;
color: var(--fg-mute);
font-style: italic;
font-variant-numeric: tabular-nums;
}
.cl__jump-num-figure--big {
color: var(--accent);
font-size: 360px;
position: relative;
}
.cl__jump-num-figure--big::after {
content: '';
position: absolute;
left: -8%;
right: -8%;
bottom: 30px;
height: 14px;
background: var(--accent);
opacity: 0.18;
transform-origin: left;
animation: clJumpUnder 1100ms 2400ms cubic-bezier(.2,.8,.2,1) backwards;
z-index: -1;
}
@keyframes clJumpUnder {
from { transform: scaleX(0); opacity: 0; }
to { transform: scaleX(1); opacity: 0.18; }
}
.cl__jump-num-tag {
font-family: var(--f-mono);
font-size: 18px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--fg-mute);
}
.cl__jump-num-tag--alt {
color: var(--accent);
font-weight: 600;
}
.cl__jump-arrow {
font-family: var(--f-serif);
font-style: italic;
font-size: 96px;
color: var(--fg-faint);
line-height: 1;
align-self: center;
animation: clJumpArrow 800ms 1500ms cubic-bezier(.6,-0.05,.2,1.2) backwards;
}
@keyframes clJumpArrow {
from { transform: translateX(-12px); opacity: 0; }
to { transform: none; opacity: 1; }
}
.cl__jump-meta {
display: flex;
align-items: center;
gap: 18px;
font-family: var(--f-mono);
font-size: 22px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--fg-mute);
}
.cl__jump-meta-plus {
font-family: var(--f-serif);
font-style: italic;
font-size: 36px;
color: var(--accent);
letter-spacing: 0;
}
.cl__jump-meta-text {
letter-spacing: 0.04em;
}
.cl__jump-meta-dot {
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--fg-faint);
}
/* ===================== Scene TRIO (step 2) ===================== */
.cl__trio {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 56px;
padding: 120px;
}
.cl__trio-eyebrow {
font-family: var(--f-serif);
font-style: italic;
font-size: 36px;
color: var(--fg-mute);
}
.cl__trio-eyebrow em {
color: var(--accent);
font-style: italic;
}
.cl__trio-rows {
display: flex;
flex-direction: column;
gap: 24px;
align-items: stretch;
}
.cl__trio-row {
display: grid;
grid-template-columns: 1fr 80px 1fr;
align-items: baseline;
gap: 24px;
padding: 14px 0;
font-family: var(--f-serif);
font-size: 88px;
line-height: 1.05;
opacity: 0;
animation: clTrioIn 900ms cubic-bezier(.2,.8,.2,1) forwards;
animation-delay: var(--d, 0ms);
}
@keyframes clTrioIn {
from { opacity: 0; transform: translateY(28px); }
to { opacity: 1; transform: none; }
}
.cl__trio-from {
text-align: right;
color: var(--fg-mute);
font-style: italic;
font-weight: 400;
text-decoration: line-through;
text-decoration-thickness: 2px;
text-decoration-color: var(--fg-faint);
}
.cl__trio-arrow {
text-align: center;
font-family: var(--f-mono);
font-style: normal;
font-size: 56px;
color: var(--fg-faint);
align-self: center;
}
.cl__trio-to {
text-align: left;
color: var(--fg);
font-style: italic;
font-weight: 500;
position: relative;
}
.cl__trio-to::after {
content: '';
position: absolute;
left: 0;
right: 6%;
bottom: 8px;
height: 10px;
background: var(--accent);
opacity: 0.2;
transform-origin: left;
animation: clTrioUnderline 900ms cubic-bezier(.2,.8,.2,1) backwards;
animation-delay: calc(var(--d, 0ms) + 600ms);
z-index: -1;
}
@keyframes clTrioUnderline {
from { transform: scaleX(0); opacity: 0; }
to { transform: scaleX(1); opacity: 0.2; }
}
/* ===================== Scene RULES (step 3) ===================== */
.cl__rules {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 26px;
padding: 110px 100px 90px;
}
.cl__rules-eyebrow {
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--fg-mute);
}
.cl__rules-eyebrow em {
color: var(--accent);
font-style: italic;
font-family: var(--f-serif);
letter-spacing: 0;
text-transform: none;
font-size: 18px;
}
.cl__rules-title {
font-family: var(--f-serif);
font-weight: 400;
font-size: 72px;
line-height: 1.1;
margin: 0;
color: var(--fg);
}
.cl__rules-title em {
font-style: italic;
color: var(--accent);
}
.cl__rules-cloud {
display: flex;
flex-wrap: wrap;
gap: 14px 16px;
justify-content: center;
align-items: center;
max-width: 1400px;
padding: 24px 0;
}
.cl__rules-chip {
display: inline-flex;
align-items: center;
gap: 10px;
padding: 12px 22px;
background: var(--bg-2);
border: 1px solid var(--line-mid);
border-radius: 999px;
font-family: var(--f-serif);
font-size: 28px;
font-style: italic;
color: var(--fg);
white-space: nowrap;
opacity: 0;
transform: translateY(28px) translateX(var(--shift, 0)) rotate(var(--rot, 0));
animation: clRulesChipIn 720ms cubic-bezier(.6,-0.05,.2,1.2) forwards;
box-shadow: 0 12px 24px -16px oklch(0 0 0 / 0.18);
}
@keyframes clRulesChipIn {
to {
opacity: 1;
transform: translateY(0) translateX(0) rotate(var(--rot, 0));
}
}
.cl__rules-chip--0 { border-left: 3px solid var(--accent); }
.cl__rules-chip--1 { border-left: 3px solid var(--accent); }
.cl__rules-chip--2 { border-left: 3px solid var(--accent); opacity: 0.92; }
.cl__rules-chip--3 { border-left: 3px solid var(--accent); }
.cl__rules-chip-mark {
font-family: var(--f-mono);
font-style: normal;
font-size: 22px;
color: var(--accent);
font-weight: 600;
}
.cl__rules-foot {
font-family: var(--f-serif);
font-style: italic;
font-size: 28px;
color: var(--fg-mute);
}
/* ===================== Scene CLOSE (step 4) ===================== */
.cl__close {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
gap: 18px;
padding: 140px;
}
.cl__close-l1 {
font-family: var(--f-serif);
font-weight: 400;
font-size: 72px;
line-height: 1.1;
margin: 0;
color: var(--fg-soft);
font-style: italic;
}
.cl__close-l2 {
font-family: var(--f-serif);
font-weight: 400;
font-size: 200px;
line-height: 1;
margin: 0;
color: var(--fg);
display: flex;
align-items: baseline;
gap: 24px;
}
.cl__close-l2 em {
font-style: italic;
color: var(--accent);
position: relative;
}
.cl__close-l2 em:nth-of-type(1)::after,
.cl__close-l2 em:nth-of-type(2)::after {
content: '';
position: absolute;
left: 4%;
right: 4%;
bottom: 16px;
height: 14px;
background: var(--accent);
opacity: 0.18;
transform-origin: left;
animation: clCloseUnderline 1100ms cubic-bezier(.2,.8,.2,1) backwards;
z-index: -1;
}
.cl__close-l2 em:nth-of-type(1)::after { animation-delay: 1500ms; }
.cl__close-l2 em:nth-of-type(2)::after { animation-delay: 1900ms; }
@keyframes clCloseUnderline {
from { transform: scaleX(0); opacity: 0; }
to { transform: scaleX(1); opacity: 0.18; }
}
.cl__close-cap {
margin-top: 32px;
font-family: var(--f-serif);
font-style: italic;
font-size: 36px;
color: var(--fg-mute);
}
.cl__close-cap em {
color: var(--accent);
font-style: italic;
}

View File

@ -0,0 +1,240 @@
import { useEffect, useRef, useState } from 'react';
import type { ChapterContext, ChapterDef } from '../types';
import { Reveal } from '../../shared/Reveal';
import { SceneFade } from '../../shared/SceneFade';
import './Closing.css';
/**
* Chapter 13 · · 85 95
*
* article/稿.md L274-281
* 1. " Skill
* Opus 4.7 西"
* 2. "Skill 带来的提升,大概就是 85 分到 95 分的差距。"
* 3. "从能用到好看,从完整到精致,从合格到有风格。"
* 4. "这 10 的差距?就是 Skill 里那些看起来很琐碎的规则。"
* 5. "每一条效果不大,但加在一起,就会产生质变了。"
*
* 5 / step 0..4
* 0 · "没 Skill 的版本 —— 本身已经不错"
* 1 hero · 85 95 ticker
* 2 · / /
* 3 +10 · 10+ "琐碎规则"chip
* 4 ·
*/
/*
* NumberTicker: 数字从 from to "跳分"
* /active
* */
interface TickerProps {
from: number;
to: number;
duration?: number;
delay?: number;
/** 是否处于活动场景;切换时 reset */
active: boolean;
}
function NumberTicker({ from, to, duration = 2200, delay = 0, active }: TickerProps) {
const [val, setVal] = useState(from);
const rafRef = useRef<number | null>(null);
useEffect(() => {
if (!active) {
setVal(from);
return;
}
let start = 0;
const startTimer = window.setTimeout(() => {
const step = (ts: number) => {
if (!start) start = ts;
const t = Math.min((ts - start) / duration, 1);
// easeOutCubic
const eased = 1 - Math.pow(1 - t, 3);
setVal(from + (to - from) * eased);
if (t < 1) {
rafRef.current = requestAnimationFrame(step);
}
};
rafRef.current = requestAnimationFrame(step);
}, delay);
return () => {
window.clearTimeout(startTimer);
if (rafRef.current) cancelAnimationFrame(rafRef.current);
};
}, [from, to, duration, delay, active]);
return <span>{Math.round(val)}</span>;
}
/*
* 3 10 "琐碎规则" chip
* */
const SMALL_RULES = [
{ id: 'inter', text: '不用 Inter / Roboto' },
{ id: 'oklch', text: 'oklch 配色' },
{ id: 'system', text: '先宣告设计系统' },
{ id: 'v0', text: 'v0 半成品先出' },
{ id: 'restraint', text: '内容克制' },
{ id: 'placeholder', text: '占位符 > 假图' },
{ id: 'whitespace', text: '留白 = 设计' },
{ id: 'nograd', text: '禁紫粉蓝渐变' },
{ id: 'noemoji', text: '禁 emoji 当 icon' },
{ id: 'verify', text: 'fork 子 Agent 验证' },
];
function Closing({ localStep }: ChapterContext) {
const sceneFair = localStep <= 0;
const sceneJump = localStep === 1;
const sceneTrio = localStep === 2;
const sceneRules = localStep === 3;
const sceneClose = localStep >= 4;
return (
<section className="cl">
{/* ════════ Scene FAIRstep 0—— 公允声明 ════════ */}
<SceneFade active={sceneFair} exitMs={420} enterDelayMs={120}>
<div className="cl__fair">
<Reveal kind="fade" duration={620} delay={80} className="cl__fair-eyebrow">
</Reveal>
<Reveal kind="rise" duration={1100} delay={300} className="cl__fair-line" as="h1">
Skill <em></em>
</Reveal>
<Reveal kind="fade" duration={780} delay={1300} className="cl__fair-cap" as="p">
Opus 4.7 西<br />
<em></em>
</Reveal>
</div>
</SceneFade>
{/* ════════ Scene JUMPstep 1—— 85 → 95 大字 ════════ */}
<SceneFade active={sceneJump} exitMs={420} enterDelayMs={420}>
<div className="cl__jump">
<Reveal kind="fade" duration={620} delay={80} className="cl__jump-eyebrow">
Skill
</Reveal>
<div className="cl__jump-row">
<Reveal kind="rise" duration={1100} delay={300} className="cl__jump-num cl__jump-num--from">
<span className="cl__jump-num-figure">
<NumberTicker from={70} to={85} duration={1100} delay={400} active={sceneJump} />
</span>
<span className="cl__jump-num-tag"> Skill</span>
</Reveal>
<Reveal kind="fade" duration={780} delay={1500} className="cl__jump-arrow" as="span">
</Reveal>
<Reveal kind="rise" duration={1100} delay={1700} className="cl__jump-num cl__jump-num--to">
<span className="cl__jump-num-figure cl__jump-num-figure--big">
<NumberTicker from={85} to={95} duration={1500} delay={1900} active={sceneJump} />
</span>
<span className="cl__jump-num-tag cl__jump-num-tag--alt"> Skill</span>
</Reveal>
</div>
<Reveal kind="fade" duration={780} delay={3100} className="cl__jump-meta">
<span className="cl__jump-meta-plus">+ 10</span>
<span className="cl__jump-meta-text"></span>
<span className="cl__jump-meta-dot" />
<span className="cl__jump-meta-text"></span>
</Reveal>
</div>
</SceneFade>
{/* ════════ Scene TRIOstep 2—— 三组词对比 ════════ */}
<SceneFade active={sceneTrio} exitMs={420} enterDelayMs={420}>
<div className="cl__trio">
<Reveal kind="fade" duration={620} delay={80} className="cl__trio-eyebrow">
10
</Reveal>
<div className="cl__trio-rows">
{[
{ from: '能用', to: '好看', delay: 260 },
{ from: '完整', to: '精致', delay: 720 },
{ from: '合格', to: '有风格', delay: 1180 },
].map((r) => (
<div
key={r.from}
className="cl__trio-row"
style={{ ['--d' as string]: `${r.delay}ms` }}
>
<span className="cl__trio-from">{r.from}</span>
<span className="cl__trio-arrow"></span>
<span className="cl__trio-to">{r.to}</span>
</div>
))}
</div>
</div>
</SceneFade>
{/* ════════ Scene RULESstep 3—— 10 条琐碎规则飞入 ════════ */}
<SceneFade active={sceneRules} exitMs={420} enterDelayMs={420}>
<div className="cl__rules">
<Reveal kind="fade" duration={620} delay={80} className="cl__rules-eyebrow">
10 <em></em>
</Reveal>
<Reveal kind="rise" duration={1100} delay={260} className="cl__rules-title" as="h2">
<em></em>
</Reveal>
<div className="cl__rules-cloud">
{SMALL_RULES.map((r, i) => (
<span
key={r.id}
className={`cl__rules-chip cl__rules-chip--${i % 4}`}
style={{
animationDelay: `${500 + i * 110}ms`,
['--rot' as string]: `${(i % 5 - 2) * 1.6}deg`,
['--shift' as string]: `${(i % 3 - 1) * 18}px`,
}}
>
<span className="cl__rules-chip-mark">+</span>
{r.text}
</span>
))}
</div>
<Reveal kind="fade" duration={780} delay={2100} className="cl__rules-foot">
</Reveal>
</div>
</SceneFade>
{/* ════════ Scene CLOSEstep 4—— 量变 → 质变 ════════ */}
<SceneFade active={sceneClose} exitMs={420} enterDelayMs={420}>
<div className="cl__close">
<Reveal kind="rise" duration={1100} delay={120} className="cl__close-l1" as="h1">
</Reveal>
<Reveal kind="rise" duration={1300} delay={780} className="cl__close-l2" as="h1">
<em></em> <em></em>
</Reveal>
<Reveal kind="fade" duration={780} delay={1700} className="cl__close-cap" as="p">
<em></em><em></em>
</Reveal>
</div>
</SceneFade>
</section>
);
}
const def: ChapterDef = {
id: 'closing',
title: '收尾 · 85 → 95 分',
eyebrow: '13',
steps: 5,
theme: 'light',
Component: Closing,
};
export default def;

View File

@ -0,0 +1,591 @@
/* =========================================================
Chapter 14 · Outro · 项目预告 + 三连
light 主题 · 开源资源 Easy Agent 项目目标 三连 下期见
========================================================= */
.ot {
position: absolute;
inset: 0;
background: var(--bg);
color: var(--fg);
font-family: var(--f-sans);
overflow: hidden;
}
/* ===================== Scene OPEN (step 0) ===================== */
.ot__open {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 36px;
padding: 100px 140px;
text-align: center;
}
.ot__open-eyebrow {
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--fg-mute);
}
.ot__open-title {
font-family: var(--f-serif);
font-weight: 400;
font-size: 68px;
line-height: 1.15;
margin: 0;
color: var(--fg);
}
.ot__open-title em {
font-style: italic;
color: var(--accent);
position: relative;
}
.ot__open-title em::after {
content: '';
position: absolute;
left: 4%;
right: 4%;
bottom: 6px;
height: 8px;
background: var(--accent);
opacity: 0.18;
transform-origin: left;
animation: otOpenUnder 1100ms 800ms cubic-bezier(.2,.8,.2,1) backwards;
}
@keyframes otOpenUnder {
from { transform: scaleX(0); opacity: 0; }
to { transform: scaleX(1); opacity: 0.18; }
}
.ot__open-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 28px;
width: 100%;
max-width: 1500px;
margin-top: 24px;
}
.ot__open-card {
position: relative;
background: var(--bg-2);
border: 1px solid var(--line-mid);
border-left: 3px solid var(--accent);
padding: 32px 32px 28px;
text-align: left;
display: flex;
flex-direction: column;
gap: 14px;
min-height: 280px;
opacity: 0;
transform: translateY(28px);
animation: otOpenCardIn 720ms cubic-bezier(.2,.8,.2,1) forwards;
box-shadow: 0 24px 48px -36px oklch(0 0 0 / 0.18);
}
@keyframes otOpenCardIn {
to { opacity: 1; transform: none; }
}
.ot__open-card-num {
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.18em;
color: var(--fg-faint);
}
.ot__open-card-name {
font-family: var(--f-mono);
font-size: 24px;
color: var(--fg);
line-height: 1.25;
word-break: break-word;
}
.ot__open-card-cn {
font-family: var(--f-serif);
font-style: italic;
font-size: 30px;
line-height: 1.2;
color: var(--accent);
margin-top: 6px;
}
.ot__open-card-desc {
font-family: var(--f-sans);
font-size: 18px;
line-height: 1.5;
color: var(--fg-mute);
margin-top: auto;
}
.ot__open-card-foot {
display: flex;
align-items: baseline;
gap: 10px;
padding-top: 14px;
border-top: 1px dashed var(--line);
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.24em;
text-transform: uppercase;
color: var(--fg-faint);
}
.ot__open-card-foot-arrow {
font-family: var(--f-serif);
font-style: italic;
font-size: 22px;
letter-spacing: 0;
color: var(--accent);
line-height: 1;
}
.ot__open-foot {
font-family: var(--f-serif);
font-style: italic;
font-size: 28px;
color: var(--fg-mute);
}
/* ===================== Scene EASY (step 1) ===================== */
.ot__easy {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 24px;
padding: 120px;
text-align: center;
}
.ot__easy-eyebrow {
display: inline-flex;
align-items: baseline;
gap: 12px;
font-family: var(--f-mono);
font-size: 16px;
letter-spacing: 0.24em;
text-transform: uppercase;
color: var(--fg-mute);
}
.ot__src-bracket { color: var(--fg-faint); font-family: var(--f-serif); font-style: italic; font-size: 26px; letter-spacing: 0; }
.ot__src-label { letter-spacing: 0.18em; }
.ot__easy-pre {
font-family: var(--f-serif);
font-style: italic;
font-size: 36px;
color: var(--fg-mute);
margin: 0;
}
.ot__easy-name {
font-family: var(--f-serif);
font-weight: 400;
font-size: 240px;
line-height: 1;
letter-spacing: -0.02em;
margin: 0;
color: var(--fg);
position: relative;
}
.ot__easy-name em {
font-style: italic;
color: var(--accent);
position: relative;
display: inline-block;
}
.ot__easy-name em::after {
content: '';
position: absolute;
left: 2%;
right: 2%;
bottom: 14px;
height: 16px;
background: var(--accent);
opacity: 0.16;
transform-origin: left;
animation: otEasyUnder 1100ms 1100ms cubic-bezier(.2,.8,.2,1) backwards;
z-index: -1;
}
@keyframes otEasyUnder {
from { transform: scaleX(0); opacity: 0; }
to { transform: scaleX(1); opacity: 0.16; }
}
.ot__easy-sub {
font-family: var(--f-serif);
font-weight: 400;
font-size: 56px;
line-height: 1.15;
margin: 0;
color: var(--fg-soft);
}
.ot__easy-sub em {
font-style: italic;
color: var(--accent);
}
.ot__easy-meta {
margin-top: 12px;
display: inline-flex;
align-items: center;
gap: 16px;
font-family: var(--f-mono);
font-size: 16px;
letter-spacing: 0.24em;
text-transform: uppercase;
color: var(--fg-mute);
}
.ot__easy-meta-dot {
width: 4px;
height: 4px;
border-radius: 50%;
background: var(--fg-faint);
}
/* ===================== Scene GOAL (step 2) ===================== */
.ot__goal {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 56px;
padding: 120px 140px;
text-align: center;
}
.ot__goal-eyebrow {
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--fg-mute);
}
.ot__goal-flow {
display: grid;
grid-template-columns: 1fr 280px 1fr;
align-items: center;
gap: 24px;
width: 100%;
max-width: 1600px;
}
.ot__goal-step {
display: flex;
flex-direction: column;
gap: 14px;
padding: 36px 44px;
background: var(--bg-2);
border: 1px solid var(--line-mid);
text-align: left;
min-height: 200px;
justify-content: center;
}
.ot__goal-step--from {
border-left: 3px solid var(--fg-faint);
opacity: 0.85;
}
.ot__goal-step--to {
border-left: 3px solid var(--accent);
background: oklch(0.965 0.030 38);
}
.ot__goal-step-tag {
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.24em;
text-transform: uppercase;
color: var(--fg-faint);
}
.ot__goal-step-tag--alt { color: var(--accent); font-weight: 600; }
.ot__goal-step-line {
font-family: var(--f-serif);
font-style: italic;
font-size: 44px;
line-height: 1.2;
color: var(--fg);
}
.ot__goal-step-line em {
font-style: italic;
color: var(--accent);
font-weight: 500;
}
.ot__goal-arrow {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.ot__goal-arrow-line {
width: 100%;
height: 1px;
background: var(--line-mid);
}
.ot__goal-arrow-text {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--bg);
padding: 0 16px;
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.24em;
text-transform: uppercase;
color: var(--accent);
}
.ot__goal-arrow-head {
position: absolute;
right: -8px;
top: 50%;
transform: translateY(-50%);
font-family: var(--f-serif);
font-style: italic;
font-size: 28px;
color: var(--accent);
background: var(--bg);
padding-left: 4px;
line-height: 1;
}
.ot__goal-foot {
font-family: var(--f-serif);
font-style: italic;
font-size: 32px;
color: var(--fg-mute);
}
.ot__goal-foot em {
color: var(--accent);
font-style: italic;
}
/* ===================== Scene TRIPLE (step 3) ===================== */
.ot__triple {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 36px;
padding: 120px;
text-align: center;
}
.ot__triple-eyebrow {
font-family: var(--f-serif);
font-style: italic;
font-size: 36px;
color: var(--fg-mute);
}
.ot__triple-title {
font-family: var(--f-serif);
font-weight: 400;
font-size: 80px;
line-height: 1.1;
margin: 0;
color: var(--fg);
}
.ot__triple-title em {
font-style: italic;
color: var(--accent);
position: relative;
}
.ot__triple-title em::after {
content: '';
position: absolute;
left: 4%;
right: 4%;
bottom: 8px;
height: 10px;
background: var(--accent);
opacity: 0.18;
transform-origin: left;
animation: otTripleUnder 1100ms 800ms cubic-bezier(.2,.8,.2,1) backwards;
z-index: -1;
}
@keyframes otTripleUnder {
from { transform: scaleX(0); opacity: 0; }
to { transform: scaleX(1); opacity: 0.18; }
}
.ot__triple-row {
display: grid;
grid-template-columns: repeat(3, 240px);
gap: 60px;
margin-top: 24px;
}
.ot__triple-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 36px 24px 28px;
background: var(--bg-2);
border: 1px solid var(--line-mid);
opacity: 0;
transform: translateY(28px) scale(0.94);
animation: otTripleCardIn 720ms cubic-bezier(.6,-0.05,.2,1.2) forwards;
}
@keyframes otTripleCardIn {
to { opacity: 1; transform: none; }
}
.ot__triple-card:hover .ot__icon { color: var(--accent); }
.ot__triple-icon-wrap {
width: 96px;
height: 96px;
display: flex;
align-items: center;
justify-content: center;
color: var(--fg);
position: relative;
}
.ot__icon {
width: 100%;
height: 100%;
transition: color 280ms cubic-bezier(.2,.8,.2,1), transform 320ms cubic-bezier(.6,-0.05,.2,1.2);
}
/* hover-like 自动节奏脉冲 —— icon 颜色随节奏从 fg → accent → fg */
.ot__triple-card:nth-child(1) .ot__icon { animation: otIconPulse 2400ms 1100ms cubic-bezier(.2,.8,.2,1) infinite; }
.ot__triple-card:nth-child(2) .ot__icon { animation: otIconPulse 2400ms 1500ms cubic-bezier(.2,.8,.2,1) infinite; }
.ot__triple-card:nth-child(3) .ot__icon { animation: otIconPulse 2400ms 1900ms cubic-bezier(.2,.8,.2,1) infinite; }
@keyframes otIconPulse {
0%, 100% { color: var(--fg); transform: scale(1); }
20% { color: var(--accent); transform: scale(1.06); }
60% { color: var(--fg); transform: scale(1); }
}
.ot__triple-label {
font-family: var(--f-serif);
font-style: italic;
font-size: 36px;
color: var(--fg);
}
.ot__triple-mono {
font-family: var(--f-mono);
font-size: 13px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--fg-faint);
}
.ot__triple-foot {
margin-top: 16px;
font-family: var(--f-serif);
font-style: italic;
font-size: 28px;
color: var(--fg-mute);
}
.ot__triple-foot em {
color: var(--accent);
font-style: italic;
}
/* ===================== Scene BYE (step 4) ===================== */
.ot__bye {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 28px;
padding: 140px;
text-align: center;
}
.ot__bye-eyebrow {
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--fg-mute);
}
.ot__bye-line {
font-family: var(--f-serif);
font-weight: 400;
font-size: 220px;
line-height: 1;
margin: 0;
color: var(--fg);
display: flex;
align-items: baseline;
gap: 24px;
}
.ot__bye-line em {
font-style: italic;
color: var(--accent);
position: relative;
}
.ot__bye-line em::after {
content: '';
position: absolute;
left: 2%;
right: 2%;
bottom: 18px;
height: 16px;
background: var(--accent);
opacity: 0.18;
transform-origin: left;
animation: otByeUnder 1100ms 1300ms cubic-bezier(.2,.8,.2,1) backwards;
z-index: -1;
}
@keyframes otByeUnder {
from { transform: scaleX(0); opacity: 0; }
to { transform: scaleX(1); opacity: 0.18; }
}
.ot__bye-arrow {
font-family: var(--f-serif);
font-style: italic;
font-size: 100px;
color: var(--fg-faint);
line-height: 1;
align-self: center;
animation: otByeArrow 1100ms 1700ms cubic-bezier(.6,-0.05,.2,1.2) backwards;
}
@keyframes otByeArrow {
from { transform: translate(-12px, 12px); opacity: 0; }
to { transform: none; opacity: 1; }
}
.ot__bye-sig {
margin-top: 16px;
display: flex;
align-items: center;
gap: 18px;
font-family: var(--f-mono);
font-size: 14px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--fg-faint);
}
.ot__bye-sig-bar {
width: 80px;
height: 1px;
background: var(--line-mid);
}

View File

@ -0,0 +1,279 @@
import type { ChapterContext, ChapterDef } from '../types';
import { Reveal } from '../../shared/Reveal';
import { SceneFade } from '../../shared/SceneFade';
import './Outro.css';
/**
* Chapter 14 · Outro · +
*
* article/稿.md L284-295
* 1. "Skill Claude Design DEMO
* "
* 2. "最后推荐下我最近在做的 Easy Agent 开源项目。"
* 3. " Claude Code Harness
* Agent "
* 4. "如果本期教程对你有所帮助,希望得到一个免费的三连 ——"
* 5. "我们下期见。"
*
* 5 / step 0..4
* 0 · "已打包开源" + Skill / Prompt / DEMOs
* 1 Easy Agent · hero + "从零复刻 Claude Code · Harness"
* 2 · "完整跟下来 → 企业级 Agent 开发能力"
* 3 CTA · like / star / follow emoji
* 4 ·
*/
interface Resource {
id: string;
num: string;
name: string;
cn: string;
desc: string;
}
const RESOURCES: Resource[] = [
{ id: 'skill', num: '01', name: 'web-design-engineer', cn: 'Skill 完整代码', desc: '本期主角 · SKILL.md + references' },
{ id: 'prompt', num: '02', name: 'claude-design / system.md', cn: '原始参考 Prompt', desc: 'Claude Design 系统提示词原文 · ≈ 420 行' },
{ id: 'demo', num: '03', name: 'demos /', cn: '几个 DEMO 网站', desc: '本期演示用到的所有产物站点' },
];
/*
* line-art · emoji
* - like: 拇指 +
* - star: 五角星
* - follow: + +
* */
function IconLike() {
return (
<svg className="ot__icon" viewBox="0 0 64 64" aria-hidden>
<path
d="M22 28 L22 56 L46 56 C50 56 52 53 52 50 L54 36 C54.5 33 52.5 31 50 31 L40 31 L42 22 C43 18 41 14 37 14 C35 14 34 15 33 17 L26 28 Z"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinejoin="round"
strokeLinecap="round"
/>
<rect
x="10" y="28" width="10" height="28"
rx="1.5"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
/>
</svg>
);
}
function IconStar() {
return (
<svg className="ot__icon" viewBox="0 0 64 64" aria-hidden>
<path
d="M32 8 L40 24 L58 26.5 L45 39 L48 56 L32 47.5 L16 56 L19 39 L6 26.5 L24 24 Z"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
strokeLinejoin="round"
strokeLinecap="round"
/>
</svg>
);
}
function IconFollow() {
return (
<svg className="ot__icon" viewBox="0 0 64 64" aria-hidden>
<circle
cx="32" cy="32" r="22"
fill="none"
stroke="currentColor"
strokeWidth="2.5"
/>
<path
d="M32 21 L32 43 M21 32 L43 32"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
/>
</svg>
);
}
const TRIPLE = [
{ id: 'like', icon: <IconLike />, label: '点赞', mono: 'LIKE' },
{ id: 'star', icon: <IconStar />, label: '收藏', mono: 'COLLECT' },
{ id: 'follow', icon: <IconFollow />, label: '关注', mono: 'FOLLOW' },
];
function Outro({ localStep }: ChapterContext) {
const sceneOpen = localStep <= 0;
const sceneEasy = localStep === 1;
const sceneGoal = localStep === 2;
const sceneTriple = localStep === 3;
const sceneBye = localStep >= 4;
return (
<section className="ot">
{/* ════════ Scene OPENstep 0—— 开源资源卡 ════════ */}
<SceneFade active={sceneOpen} exitMs={420} enterDelayMs={120}>
<div className="ot__open">
<Reveal kind="fade" duration={620} delay={80} className="ot__open-eyebrow">
</Reveal>
<Reveal kind="rise" duration={1100} delay={300} className="ot__open-title" as="h1">
<em></em> · /
</Reveal>
<div className="ot__open-grid">
{RESOURCES.map((r, i) => (
<div
key={r.id}
className="ot__open-card"
style={{ animationDelay: `${700 + i * 160}ms` }}
>
<div className="ot__open-card-num">{r.num}</div>
<div className="ot__open-card-name">{r.name}</div>
<div className="ot__open-card-cn">{r.cn}</div>
<div className="ot__open-card-desc">{r.desc}</div>
<div className="ot__open-card-foot">
<span className="ot__open-card-foot-arrow"></span>
<span className="ot__open-card-foot-text">open</span>
</div>
</div>
))}
</div>
<Reveal kind="fade" duration={780} delay={1500} className="ot__open-foot">
</Reveal>
</div>
</SceneFade>
{/* ════════ Scene EASYstep 1—— Easy Agent 项目预告 ════════ */}
<SceneFade active={sceneEasy} exitMs={420} enterDelayMs={420}>
<div className="ot__easy">
<Reveal kind="fade" duration={620} delay={80} className="ot__easy-eyebrow">
<span className="ot__src-bracket">[</span>
<span className="ot__src-label"> · </span>
<span className="ot__src-bracket">]</span>
</Reveal>
<Reveal kind="rise" duration={1100} delay={260} className="ot__easy-pre" as="p">
</Reveal>
<Reveal kind="rise" duration={1300} delay={620} className="ot__easy-name" as="h1">
<em>Easy Agent</em>
</Reveal>
<Reveal kind="rise" duration={1100} delay={1300} className="ot__easy-sub" as="h2">
Claude Code <em>Harness</em>
</Reveal>
<Reveal kind="fade" duration={780} delay={1900} className="ot__easy-meta">
<span>open source</span>
<span className="ot__easy-meta-dot" />
<span>step-by-step</span>
<span className="ot__easy-meta-dot" />
<span> Agent</span>
</Reveal>
</div>
</SceneFade>
{/* ════════ Scene GOALstep 2—— 项目目标 ════════ */}
<SceneFade active={sceneGoal} exitMs={420} enterDelayMs={420}>
<div className="ot__goal">
<Reveal kind="fade" duration={620} delay={80} className="ot__goal-eyebrow">
</Reveal>
<div className="ot__goal-flow">
<Reveal kind="rise" duration={780} delay={260} className="ot__goal-step ot__goal-step--from">
<div className="ot__goal-step-tag">YOU · </div>
<div className="ot__goal-step-line"> AI Agent </div>
</Reveal>
<Reveal kind="fade" duration={780} delay={620} className="ot__goal-arrow" as="span">
<span className="ot__goal-arrow-line" />
<span className="ot__goal-arrow-text">Easy Agent</span>
<span className="ot__goal-arrow-head"></span>
</Reveal>
<Reveal kind="rise" duration={780} delay={900} className="ot__goal-step ot__goal-step--to">
<div className="ot__goal-step-tag ot__goal-step-tag--alt">YOU · </div>
<div className="ot__goal-step-line"><em></em> Agent </div>
</Reveal>
</div>
<Reveal kind="fade" duration={780} delay={1500} className="ot__goal-foot">
"AI 转型 —— <em>不容错过</em>"
</Reveal>
</div>
</SceneFade>
{/* ════════ Scene TRIPLEstep 3—— 自绘三连 ════════ */}
<SceneFade active={sceneTriple} exitMs={420} enterDelayMs={420}>
<div className="ot__triple">
<Reveal kind="fade" duration={620} delay={80} className="ot__triple-eyebrow">
</Reveal>
<Reveal kind="rise" duration={1100} delay={260} className="ot__triple-title" as="h1">
<em></em>
</Reveal>
<div className="ot__triple-row">
{TRIPLE.map((t, i) => (
<div
key={t.id}
className="ot__triple-card"
style={{ animationDelay: `${600 + i * 200}ms` }}
>
<div className="ot__triple-icon-wrap">
{t.icon}
</div>
<div className="ot__triple-label">{t.label}</div>
<div className="ot__triple-mono">{t.mono}</div>
</div>
))}
</div>
<Reveal kind="fade" duration={780} delay={1700} className="ot__triple-foot">
<em> AI </em>
</Reveal>
</div>
</SceneFade>
{/* ════════ Scene BYEstep 4—— 下期见 ════════ */}
<SceneFade active={sceneBye} exitMs={420} enterDelayMs={420}>
<div className="ot__bye">
<Reveal kind="fade" duration={780} delay={120} className="ot__bye-eyebrow">
</Reveal>
<Reveal kind="rise" duration={1300} delay={500} className="ot__bye-line" as="h1">
<em></em>
<span className="ot__bye-arrow"></span>
</Reveal>
<Reveal kind="fade" duration={780} delay={1500} className="ot__bye-sig">
<span className="ot__bye-sig-bar" />
<span className="ot__bye-sig-text">claude-design / web-design-engineer</span>
<span className="ot__bye-sig-bar" />
</Reveal>
</div>
</SceneFade>
</section>
);
}
const def: ChapterDef = {
id: 'outro',
title: 'Outro · 项目预告 + 三连',
eyebrow: '14',
steps: 5,
theme: 'light',
Component: Outro,
};
export default def;

36
web/src/chapters/index.ts Normal file
View File

@ -0,0 +1,36 @@
import type { ChapterDef } from './types';
import Opening from './01-opening';
import Video from './02-video';
import CorePoint from './03-core-point';
import Role from './04-role';
import Workflow from './05-workflow';
import AntiAi from './06-anti-ai';
import Oklch from './07-oklch';
import Restraint from './08-restraint';
import Verification from './09-verification';
import ToSkill from './10-to-skill';
import SkillChanges from './11-skill-changes';
import References from './12-references';
import Closing from './13-closing';
import Outro from './14-outro';
/**
*
* append
*/
export const chapters: ChapterDef[] = [
Opening,
Video,
CorePoint,
Role,
Workflow,
AntiAi,
Oklch,
Restraint,
Verification,
ToSkill,
SkillChanges,
References,
Closing,
Outro,
];

26
web/src/chapters/types.ts Normal file
View File

@ -0,0 +1,26 @@
import type { ComponentType } from 'react';
/** 单个章节的运行时上下文:传入到章节组件 props 中 */
export interface ChapterContext {
/** 当前章节内的局部 step0..steps-1 */
localStep: number;
/** 当前章节的总 step 数 */
steps: number;
/** 进入 / 离开方向(用于转场,正向 1 / 反向 -1 */
direction: 1 | -1;
}
export interface ChapterDef {
/** 唯一标识(用于 URL hash / 调试) */
id: string;
/** 中文标题(用于进度条 tooltip */
title: string;
/** 英文 / 编号 eyebrow视觉用 */
eyebrow?: string;
/** 章节内 step 数量(必须 >= 1 */
steps: number;
/** 主题light = 米底ink = 深墨底 */
theme?: 'light' | 'ink';
/** 章节组件 */
Component: ComponentType<ChapterContext>;
}

19
web/src/design/motion.ts Normal file
View File

@ -0,0 +1,19 @@
/**
* tokens.css --ease-* / --d-*
*/
export const ease = {
enter: 'cubic-bezier(.2, .8, .2, 1)',
exit: 'cubic-bezier(.4, 0, 1, 1)',
emphasized: 'cubic-bezier(.6, -0.05, .2, 1.2)',
} as const;
export const dur = {
micro: 200,
base: 480,
hero: 900,
epic: 1600,
} as const;
export type EaseName = keyof typeof ease;
export type DurName = keyof typeof dur;

111
web/src/design/tokens.css Normal file
View File

@ -0,0 +1,111 @@
/* =========================================================
Design Tokens Claude Design Skill 视频站点
暖调Anthropic 米色 + 暖橘+ 半反转节奏
========================================================= */
:root {
/* —— 色彩OKLCH 派生,禁止再造新色相) —— */
/* paper 系:默认浅底(米色纸感) */
--paper: oklch(0.965 0.018 78);
--paper-deep: oklch(0.928 0.024 78);
--paper-edge: oklch(0.880 0.028 78);
/* ink 系:深墨(暗章节用作底色,浅章节作文本) */
--ink: oklch(0.180 0.014 60);
--ink-soft: oklch(0.300 0.014 60);
--ink-mute: oklch(0.520 0.012 60);
--ink-faint: oklch(0.760 0.010 60);
/* accent 系暖橘Claude 调) */
--ember: oklch(0.700 0.170 42);
--ember-deep: oklch(0.560 0.180 38);
--ember-soft: oklch(0.860 0.060 60);
/* 极少量功能色:仅用于"股价崩盘 / 错误"等明确语义 */
--crimson: oklch(0.560 0.200 22);
--cyan: oklch(0.700 0.090 220);
/* 中性线条(基于 ink 的透明派生) */
--line-1: oklch(0.180 0.014 60 / 0.10);
--line-2: oklch(0.180 0.014 60 / 0.22);
--line-3: oklch(0.180 0.014 60 / 0.45);
/* 反色场景下的线条ink 底) */
--line-on-ink-1: oklch(0.965 0.018 78 / 0.10);
--line-on-ink-2: oklch(0.965 0.018 78 / 0.22);
--line-on-ink-3: oklch(0.965 0.018 78 / 0.45);
/* —— 间距8px 基准) —— */
--s-1: 8px;
--s-2: 16px;
--s-3: 24px;
--s-4: 32px;
--s-5: 48px;
--s-6: 64px;
--s-7: 96px;
--s-8: 128px;
--s-9: 192px;
/* —— 字号(基于 1920×1080 stage 的 px 绝对单位) —— */
--t-eyebrow: 18px; /* 章节编号 / eyebrow */
--t-caption: 22px;
--t-body: 28px; /* 正文最小 */
--t-body-lg: 34px;
--t-lead: 42px;
--t-h3: 56px;
--t-h2: 84px;
--t-h1: 128px;
--t-display: 200px;
--t-mega: 320px;
/* —— 字体 —— */
--f-serif: "Instrument Serif", "Songti SC", "Noto Serif SC", Georgia, serif;
--f-sans: "Geist", "PingFang SC", "HarmonyOS Sans SC", system-ui, -apple-system, sans-serif;
--f-mono: "Geist Mono", ui-monospace, "JetBrains Mono", Menlo, monospace;
/* —— 圆角(保持极克制,几乎都是 0 —— */
--r-0: 0px;
--r-1: 2px;
--r-2: 4px;
--r-pill: 999px;
/* —— 缓动 / 时长(与 motion.ts 同步) —— */
--ease-enter: cubic-bezier(.2, .8, .2, 1);
--ease-exit: cubic-bezier(.4, 0, 1, 1);
--ease-emphasized: cubic-bezier(.6, -0.05, .2, 1.2);
--d-micro: 200ms;
--d-base: 480ms;
--d-hero: 900ms;
--d-epic: 1600ms;
/* —— 主题角色(默认浅底) —— */
--bg: var(--paper);
--bg-2: var(--paper-deep);
--fg: var(--ink);
--fg-soft: var(--ink-soft);
--fg-mute: var(--ink-mute);
--fg-faint: var(--ink-faint);
--line: var(--line-1);
--line-mid: var(--line-2);
--line-strong:var(--line-3);
--accent: var(--ember);
--accent-deep:var(--ember-deep);
}
/* 深底章节通过给祖先元素加 .theme-ink 翻转角色 —— 仅角色变token 不变 */
/* 暗色不再用纯墨黑 调到 oklch 0.27 左右的"软炭灰"
让浅深章节切换时眼睛不被刺一下 */
.theme-ink {
--bg: oklch(0.275 0.012 60);
--bg-2: oklch(0.320 0.014 60);
--fg: var(--paper);
--fg-soft: oklch(0.890 0.020 78);
--fg-mute: oklch(0.700 0.018 78);
--fg-faint: oklch(0.560 0.014 60);
--line: var(--line-on-ink-1);
--line-mid: var(--line-on-ink-2);
--line-strong:var(--line-on-ink-3);
--accent: var(--ember);
--accent-deep:var(--ember-deep);
}

46
web/src/index.css Normal file
View File

@ -0,0 +1,46 @@
/* —— 字体加载 —— */
@import url("https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Geist:wght@300;400;500;600;700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;500;600&display=swap");
/* Ch06 用 —— 黑名单字体 */
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=Roboto:wght@400;500&family=Fraunces:ital,wght@0,400;1,400&display=swap");
/* Ch06 用 —— 推荐替代字体 */
@import url("https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;600&family=Space+Grotesk:wght@400;500&family=Sora:wght@400;600&family=Newsreader:ital,wght@0,400;1,400&display=swap");
@import "./design/tokens.css";
/* —— Reset —— */
*, *::before, *::after { box-sizing: border-box; }
html, body, #root { margin: 0; padding: 0; height: 100%; }
body {
font-family: var(--f-sans);
color: var(--fg);
background: #000; /* letterbox 黑边 */
overflow: hidden;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
font-synthesis: none;
}
button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
::selection { background: var(--accent); color: var(--paper); }
/* —— 全局排版基线 —— */
h1, h2, h3, p { margin: 0; }
h1, h2, h3 {
font-family: var(--f-serif);
font-weight: 400;
letter-spacing: -0.02em;
line-height: 1.02;
text-wrap: balance;
}
p { line-height: 1.5; text-wrap: pretty; }
/* —— Reduced motion —— */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 1ms !important;
transition-duration: 1ms !important;
}
}

10
web/src/main.tsx Normal file
View File

@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

View File

@ -0,0 +1,49 @@
import { useEffect, useState } from 'react';
/** 不停跳动的实时时间HH:MM:SS */
export function LiveClock({ className }: { className?: string }) {
const [now, setNow] = useState(() => new Date());
useEffect(() => {
const id = window.setInterval(() => setNow(new Date()), 1000);
return () => clearInterval(id);
}, []);
const pad = (n: number) => String(n).padStart(2, '0');
return (
<span className={className} style={{ fontVariantNumeric: 'tabular-nums' }}>
{pad(now.getHours())}:{pad(now.getMinutes())}:{pad(now.getSeconds())}
</span>
);
}
/** 持续微抖的小数(模拟实时报价末位变化) */
export function FlickerNumber({
base,
amplitude = 0.05,
decimals = 2,
className,
prefix = '',
suffix = '',
intervalMs = 1100,
}: {
base: number;
amplitude?: number;
decimals?: number;
className?: string;
prefix?: string;
suffix?: string;
intervalMs?: number;
}) {
const [v, setV] = useState(base);
useEffect(() => {
const id = window.setInterval(() => {
const noise = (Math.random() * 2 - 1) * amplitude;
setV(base + noise);
}, intervalMs);
return () => clearInterval(id);
}, [base, amplitude, intervalMs]);
return (
<span className={className} style={{ fontVariantNumeric: 'tabular-nums' }}>
{prefix}{v.toFixed(decimals)}{suffix}
</span>
);
}

View File

@ -0,0 +1,78 @@
import { useEffect, useRef, useState } from 'react';
interface Props {
to: number;
from?: number;
duration?: number; // ms
decimals?: number;
prefix?: string;
suffix?: string;
/** 出场延迟 */
delay?: number;
className?: string;
/** 是否显示符号位(如 + / - */
signed?: boolean;
}
const easeOutQuart = (t: number) => 1 - Math.pow(1 - t, 4);
/**
* RAF count-updefault easeOutQuart
* 使 tabular-nums
*/
export function NumberTicker({
to,
from = 0,
duration = 1400,
decimals = 1,
prefix = '',
suffix = '',
delay = 0,
className,
signed = false,
}: Props) {
const [val, setVal] = useState(from);
const startedRef = useRef(false);
useEffect(() => {
let raf = 0;
let timer = 0;
const start = () => {
const t0 = performance.now();
const tick = (now: number) => {
const t = Math.min(1, (now - t0) / duration);
const eased = easeOutQuart(t);
setVal(from + (to - from) * eased);
if (t < 1) raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
};
if (delay > 0) {
timer = window.setTimeout(start, delay);
} else {
start();
}
startedRef.current = true;
return () => {
cancelAnimationFrame(raf);
clearTimeout(timer);
};
}, [to, from, duration, delay]);
const display = (() => {
const abs = Math.abs(val).toFixed(decimals);
if (!signed) return abs;
if (val < 0) return `${abs}`;
if (val > 0) return `+${abs}`;
return abs;
})();
return (
<span
className={className}
style={{ fontVariantNumeric: 'tabular-nums', display: 'inline-block' }}
>
{prefix}{display}{suffix}
</span>
);
}

42
web/src/shared/Reveal.css Normal file
View File

@ -0,0 +1,42 @@
.reveal {
animation-timing-function: cubic-bezier(.2, .8, .2, 1);
animation-fill-mode: both;
animation-iteration-count: 1;
will-change: opacity, transform, filter;
}
/* span / em / strong 自动转 inline-block确保 transform 生效 */
span.reveal, em.reveal, strong.reveal {
display: inline-block;
}
.reveal--rise { animation-name: revealRise; }
.reveal--fall { animation-name: revealFall; }
.reveal--fade { animation-name: revealFade; }
.reveal--blur { animation-name: revealBlur; }
.reveal--wipe-r{ animation-name: revealWipeR; }
.reveal--tight { animation-name: revealTight; }
@keyframes revealRise {
from { opacity: 0; transform: translateY(28px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes revealFall {
from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes revealFade {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes revealBlur {
from { opacity: 0; filter: blur(18px); transform: translateY(40px); }
to { opacity: 1; filter: blur(0); transform: translateY(0); }
}
@keyframes revealWipeR {
from { opacity: 0; clip-path: inset(0 100% 0 0); }
to { opacity: 1; clip-path: inset(0 0 0 0); }
}
@keyframes revealTight {
from { opacity: 0; letter-spacing: 0.4em; filter: blur(8px); }
to { opacity: 1; letter-spacing: -0.02em; filter: blur(0); }
}

50
web/src/shared/Reveal.tsx Normal file
View File

@ -0,0 +1,50 @@
import { createElement, type CSSProperties, type ReactNode } from 'react';
import './Reveal.css';
type RevealKind =
| 'rise' // 默认:下方 24px + opacity
| 'fall' // 顶部 -24px + opacity
| 'fade' // 仅 opacity
| 'blur' // blur 16px → 0
| 'wipe-r' // 自左 wipe
| 'tight' // letter-spacing 0.4em → 0
;
type AsTag = 'div' | 'span' | 'h1' | 'h2' | 'h3' | 'p' | 'em' | 'strong';
interface Props {
children: ReactNode;
delay?: number; // ms
duration?: number; // ms默认 720
kind?: RevealKind;
className?: string;
style?: CSSProperties;
as?: AsTag;
}
/**
* React
* `localStep >= n && <Reveal>...</Reveal>`
*/
export function Reveal({
children,
delay = 0,
duration = 720,
kind = 'rise',
className = '',
style,
as = 'div',
}: Props) {
return createElement(
as,
{
className: `reveal reveal--${kind} ${className}`.trim(),
style: {
animationDelay: `${delay}ms`,
animationDuration: `${duration}ms`,
...style,
},
},
children,
);
}

View File

@ -0,0 +1,67 @@
import { useEffect, useRef, useState, type ReactNode } from 'react';
interface Props {
active: boolean;
/** 退出渐隐时长ms */
exitMs?: number;
/** 进入前的等待时长留给上一幕退出ms */
enterDelayMs?: number;
children: ReactNode;
className?: string;
}
/**
* +
*
* - active true enterDelayMs fade out
* - active false exitMs
*
* /
*/
export function SceneFade({
active,
exitMs = 360,
enterDelayMs = 220,
children,
className = '',
}: Props) {
const [mounted, setMounted] = useState(active);
const [shown, setShown] = useState(false);
const timerRef = useRef<number | null>(null);
useEffect(() => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
if (active) {
setMounted(true);
// 让上一幕先开始 fade out再淡入本幕
timerRef.current = window.setTimeout(() => setShown(true), enterDelayMs);
} else {
setShown(false);
timerRef.current = window.setTimeout(() => setMounted(false), exitMs);
}
return () => {
if (timerRef.current) clearTimeout(timerRef.current);
};
}, [active, exitMs, enterDelayMs]);
if (!mounted) return null;
return (
<div
className={`scene-fade ${className}`}
data-shown={shown}
style={{
position: 'absolute',
inset: 0,
opacity: shown ? 1 : 0,
transition: `opacity ${shown ? enterDelayMs + 80 : exitMs}ms cubic-bezier(.4,0,1,1)`,
pointerEvents: shown ? 'auto' : 'none',
}}
>
{children}
</div>
);
}

View File

@ -0,0 +1,54 @@
import { useEffect, useRef, useState } from 'react';
import { chapters } from '../chapters';
import { useStep } from '../store/useStep';
/**
*
* chapterIndex step
*/
export function ChapterHost() {
const { chapterIndex, localStep, direction } = useStep();
const Current = chapters[chapterIndex];
const [renderedIdx, setRenderedIdx] = useState(chapterIndex);
const [phase, setPhase] = useState<'in' | 'out'>('in');
const pendingRef = useRef<number | null>(null);
useEffect(() => {
if (chapterIndex === renderedIdx) return;
pendingRef.current = chapterIndex;
setPhase('out');
const t = setTimeout(() => {
setRenderedIdx(pendingRef.current!);
setPhase('in');
}, 220);
return () => clearTimeout(t);
}, [chapterIndex, renderedIdx]);
const Active = chapters[renderedIdx] ?? Current;
const themeClass = Active.theme === 'ink' ? 'theme-ink' : '';
const Component = Active.Component;
return (
<div
className={`chapter-host ${themeClass}`}
data-phase={phase}
data-chapter-id={Active.id}
style={{
position: 'absolute',
inset: 0,
background: 'var(--bg)',
color: 'var(--fg)',
opacity: phase === 'in' ? 1 : 0,
transform: phase === 'in' ? 'translateY(0)' : 'translateY(8px)',
transition: 'opacity 220ms var(--ease-exit), transform 220ms var(--ease-exit), background 480ms var(--ease-enter), color 480ms var(--ease-enter)',
}}
>
<Component
localStep={renderedIdx === chapterIndex ? localStep : Active.steps - 1}
steps={Active.steps}
direction={direction}
/>
</div>
);
}

View File

@ -0,0 +1,105 @@
/* —— 进度条热区:固定在 viewport 底部 —— */
.progress-zone {
position: fixed;
left: 0;
right: 0;
bottom: 0;
height: 14px; /* 默认热区高度(不可见) */
z-index: 50;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 0 36px 10px;
pointer-events: auto;
opacity: 0;
transform: translateY(8px);
transition: opacity 320ms cubic-bezier(.4,0,1,1),
transform 320ms cubic-bezier(.4,0,1,1),
height 320ms cubic-bezier(.4,0,1,1),
padding 320ms cubic-bezier(.4,0,1,1);
}
.progress-zone.is-visible {
height: 64px;
opacity: 1;
transform: translateY(0);
background: linear-gradient(
to top,
rgba(0, 0, 0, 0.55) 0%,
rgba(0, 0, 0, 0) 100%
);
}
/* —— 上方文字标识 —— */
.progress-meta {
display: flex;
align-items: baseline;
gap: 14px;
font-family: "Geist Mono", ui-monospace, monospace;
font-size: 12px;
color: rgba(245, 240, 230, 0.85);
text-transform: uppercase;
letter-spacing: 0.12em;
margin-bottom: 8px;
user-select: none;
}
.progress-meta__num {
font-weight: 600;
color: oklch(0.78 0.13 50); /* ember 提亮版 */
}
.progress-meta__title {
font-family: "Geist", system-ui, sans-serif;
font-size: 13px;
letter-spacing: 0;
text-transform: none;
color: rgba(255, 252, 245, 0.95);
flex: 1;
}
.progress-meta__count {
font-variant-numeric: tabular-nums;
}
/* —— 实际进度条 —— */
.progress-bar {
position: relative;
height: 8px;
cursor: pointer;
display: flex;
align-items: center;
}
.progress-bar__track {
position: absolute;
left: 0; right: 0;
height: 1px;
background: rgba(255, 255, 255, 0.18);
}
.progress-bar__fill {
position: absolute;
left: 0; right: 0;
height: 1px;
background: oklch(0.78 0.13 50);
transform-origin: 0 50%;
transition: transform 280ms cubic-bezier(.2,.8,.2,1);
}
.progress-bar__tick {
position: absolute;
top: 50%;
width: 12px;
height: 12px;
margin-left: -6px;
margin-top: -6px;
border: 1px solid rgba(255, 255, 255, 0.45);
background: rgba(0, 0, 0, 0.4);
border-radius: 0;
transform: rotate(45deg);
transition: background 200ms, border-color 200ms, transform 200ms;
}
.progress-bar__tick:hover {
background: oklch(0.78 0.13 50);
border-color: oklch(0.78 0.13 50);
}
.progress-bar__tick.is-current {
background: oklch(0.78 0.13 50);
border-color: oklch(0.78 0.13 50);
transform: rotate(45deg) scale(1.15);
}

View File

@ -0,0 +1,111 @@
import { useEffect, useRef, useState } from 'react';
import { chapters } from '../chapters';
import { stepStore, useStep, chapterStartGlobal } from '../store/useStep';
import './ProgressBar.css';
/**
*
* - 12px
* - 1.2s
* - / globalStep
* - 线 + + tooltip
*
*
*/
export function ProgressBar() {
const { globalStep, totalSteps, chapterIndex } = useStep();
const [visible, setVisible] = useState(false);
const draggingRef = useRef(false);
const hideTimerRef = useRef<number | null>(null);
const barRef = useRef<HTMLDivElement>(null);
const show = () => {
if (hideTimerRef.current) {
clearTimeout(hideTimerRef.current);
hideTimerRef.current = null;
}
setVisible(true);
};
const scheduleHide = () => {
if (draggingRef.current) return;
if (hideTimerRef.current) clearTimeout(hideTimerRef.current);
hideTimerRef.current = window.setTimeout(() => setVisible(false), 1200);
};
const seekFromEvent = (clientX: number) => {
const el = barRef.current;
if (!el) return;
const rect = el.getBoundingClientRect();
const ratio = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
const target = Math.round(ratio * (totalSteps - 1));
stepStore.goToGlobal(target);
};
useEffect(() => {
const onMove = (e: MouseEvent) => {
if (draggingRef.current) seekFromEvent(e.clientX);
};
const onUp = () => {
draggingRef.current = false;
scheduleHide();
};
window.addEventListener('mousemove', onMove);
window.addEventListener('mouseup', onUp);
return () => {
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseup', onUp);
};
}, [totalSteps]);
// 章节刻度(每章起始 globalStep 的归一化位置)
const ticks = chapters.map((_, i) => chapterStartGlobal(i) / Math.max(1, totalSteps - 1));
const progress = totalSteps > 1 ? globalStep / (totalSteps - 1) : 0;
const currentChapter = chapters[chapterIndex];
return (
<div
className={`progress-zone ${visible ? 'is-visible' : ''}`}
onMouseEnter={show}
onMouseLeave={scheduleHide}
onClick={(e) => e.stopPropagation()}
>
<div className="progress-meta">
<span className="progress-meta__num">
{String(chapterIndex + 1).padStart(2, '0')}
</span>
<span className="progress-meta__title">{currentChapter.title}</span>
<span className="progress-meta__count">
{globalStep + 1} / {totalSteps}
</span>
</div>
<div
className="progress-bar"
ref={barRef}
onMouseDown={(e) => {
draggingRef.current = true;
seekFromEvent(e.clientX);
}}
>
<div className="progress-bar__track" />
<div
className="progress-bar__fill"
style={{ transform: `scaleX(${progress})` }}
/>
{ticks.map((r, i) => (
<button
key={i}
className={`progress-bar__tick ${i === chapterIndex ? 'is-current' : ''}`}
style={{ left: `${r * 100}%` }}
title={chapters[i].title}
onClick={(e) => {
e.stopPropagation();
stepStore.goToChapter(i);
}}
/>
))}
</div>
</div>
);
}

21
web/src/stage/Stage.css Normal file
View File

@ -0,0 +1,21 @@
.stage-letterbox {
position: fixed;
inset: 0;
background: #000;
overflow: hidden;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.stage {
position: relative;
flex: none;
transform-origin: center center;
background: var(--bg);
color: var(--fg);
overflow: hidden;
transition: background var(--d-base) var(--ease-enter),
color var(--d-base) var(--ease-enter);
}

49
web/src/stage/Stage.tsx Normal file
View File

@ -0,0 +1,49 @@
import { useEffect, useRef, useState, type ReactNode } from 'react';
import './Stage.css';
const STAGE_W = 1920;
const STAGE_H = 1080;
/** 舞台四周保留的最小留白(视口像素)—— 防止贴边 */
const SAFE_PAD = 24;
interface Props {
children: ReactNode;
/** 主题light = 米底ink = 深墨底 */
theme?: 'light' | 'ink';
}
/**
* 16:9 1920×1080 CSS transform
* flex + origin translate% × scale
*/
export function Stage({ children, theme = 'light' }: Props) {
const wrapRef = useRef<HTMLDivElement>(null);
const [scale, setScale] = useState(1);
useEffect(() => {
const compute = () => {
const vw = window.innerWidth - SAFE_PAD * 2;
const vh = window.innerHeight - SAFE_PAD * 2;
setScale(Math.min(vw / STAGE_W, vh / STAGE_H));
};
compute();
window.addEventListener('resize', compute);
return () => window.removeEventListener('resize', compute);
}, []);
return (
<div className="stage-letterbox" ref={wrapRef}>
<div
className={`stage ${theme === 'ink' ? 'theme-ink' : ''}`}
style={{
width: STAGE_W,
height: STAGE_H,
transform: `scale(${scale})`,
}}
>
{children}
</div>
</div>
);
}

View File

@ -0,0 +1,76 @@
import { useEffect } from 'react';
import { stepStore } from '../store/useStep';
/**
* + step
* - stage next
* - Space / next
* - prev
* - Backspacedebug
* - 1..9 1-indexed
*
* / [data-no-step] click stopPropagation
*
* 使 capture + stopImmediatePropagation
* <video> / <audio>
* Space / /
*/
export function useHotKeys() {
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
// 真正的输入框 / 可编辑区放过
const tgt = e.target as HTMLElement | null;
if (
tgt instanceof HTMLInputElement ||
tgt instanceof HTMLTextAreaElement ||
(tgt && (tgt as HTMLElement).isContentEditable)
) {
return;
}
let handled = false;
switch (e.key) {
case ' ':
case 'Spacebar':
case 'Enter':
case 'ArrowRight':
stepStore.next();
handled = true;
break;
case 'ArrowLeft':
stepStore.prev();
handled = true;
break;
case 'Backspace':
stepStore.goToGlobal(0);
handled = true;
break;
default: {
if (/^[1-9]$/.test(e.key)) {
stepStore.goToChapter(parseInt(e.key, 10) - 1);
handled = true;
}
}
}
if (handled) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
// 关键:把焦点从 <video>/<audio> 等媒体元素夺回来,
// 否则下一次按键又会被原生控件吃掉。
if (
tgt instanceof HTMLVideoElement ||
tgt instanceof HTMLAudioElement ||
tgt instanceof HTMLButtonElement
) {
tgt.blur();
}
}
};
// capture 阶段:抢在 video / audio 原生快捷键之前
window.addEventListener('keydown', onKey, { capture: true });
return () => window.removeEventListener('keydown', onKey, { capture: true } as EventListenerOptions);
}, []);
}

99
web/src/store/useStep.ts Normal file
View File

@ -0,0 +1,99 @@
import { useSyncExternalStore } from 'react';
import { chapters } from '../chapters';
/**
* step store
*
* - globalStep: 跨章节累加索引0 = 1 step 0
* - / Space / next() globalStep + 1
* - prev() globalStep - 1
* - 1-9goToChapter(i)
* - goToGlobal(n)
*/
type Listener = () => void;
interface Snapshot {
globalStep: number;
totalSteps: number;
chapterIndex: number;
localStep: number;
direction: 1 | -1;
}
let listeners = new Set<Listener>();
const totalSteps = () =>
chapters.reduce((acc, c) => acc + c.steps, 0);
/** 给定 globalStep求章节 index 与 localStep */
function locate(global: number): { chapterIndex: number; localStep: number } {
let acc = 0;
for (let i = 0; i < chapters.length; i++) {
const next = acc + chapters[i].steps;
if (global < next) return { chapterIndex: i, localStep: global - acc };
acc = next;
}
// 越界 → 最后一章最后一步
const last = chapters.length - 1;
return { chapterIndex: last, localStep: chapters[last].steps - 1 };
}
/** 求章节起始 globalStep */
export function chapterStartGlobal(chapterIndex: number): number {
let acc = 0;
for (let i = 0; i < chapterIndex; i++) acc += chapters[i].steps;
return acc;
}
let snapshot: Snapshot = {
globalStep: 0,
totalSteps: totalSteps(),
chapterIndex: 0,
localStep: 0,
direction: 1,
};
function emit() {
listeners.forEach((l) => l());
}
function set(globalStep: number, direction: 1 | -1) {
const total = totalSteps();
const clamped = Math.max(0, Math.min(total - 1, globalStep));
if (clamped === snapshot.globalStep) return;
const loc = locate(clamped);
snapshot = {
globalStep: clamped,
totalSteps: total,
chapterIndex: loc.chapterIndex,
localStep: loc.localStep,
direction,
};
emit();
}
export const stepStore = {
subscribe(l: Listener) {
listeners.add(l);
return () => listeners.delete(l);
},
getSnapshot() {
return snapshot;
},
next() { set(snapshot.globalStep + 1, 1); },
prev() { set(snapshot.globalStep - 1, -1); },
goToGlobal(n: number) {
const dir: 1 | -1 = n >= snapshot.globalStep ? 1 : -1;
set(n, dir);
},
goToChapter(chapterIndex: number) {
const target = chapterStartGlobal(chapterIndex);
const dir: 1 | -1 = target >= snapshot.globalStep ? 1 : -1;
set(target, dir);
},
};
export function useStep(): Snapshot {
return useSyncExternalStore(stepStore.subscribe, stepStore.getSnapshot, stepStore.getSnapshot);
}

25
web/tsconfig.app.json Normal file
View File

@ -0,0 +1,25 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023", "DOM"],
"module": "esnext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

7
web/tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

24
web/tsconfig.node.json Normal file
View File

@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "es2023",
"lib": ["ES2023"],
"module": "esnext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

7
web/vite.config.ts Normal file
View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vite.dev/config/
export default defineConfig({
plugins: [react()],
})