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:
parent
2e214c5b76
commit
32a23cdc3e
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
/node_modules
|
||||
24
web/.gitignore
vendored
Normal file
24
web/.gitignore
vendored
Normal 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
1
web/.npmrc
Normal file
@ -0,0 +1 @@
|
||||
registry=https://registry.npmjs.org/
|
||||
73
web/README.md
Normal file
73
web/README.md
Normal 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
23
web/eslint.config.js
Normal 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
13
web/index.html
Normal 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
3013
web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
30
web/package.json
Normal file
30
web/package.json
Normal 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
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
BIN
web/public/video.mp4
Normal file
Binary file not shown.
32
web/src/App.tsx
Normal file
32
web/src/App.tsx
Normal 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;
|
||||
705
web/src/chapters/01-opening/Opening.css
Normal file
705
web/src/chapters/01-opening/Opening.css
Normal 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;
|
||||
}
|
||||
383
web/src/chapters/01-opening/index.tsx
Normal file
383
web/src/chapters/01-opening/index.tsx
Normal 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;
|
||||
183
web/src/chapters/02-video/Video.css
Normal file
183
web/src/chapters/02-video/Video.css
Normal 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);
|
||||
}
|
||||
90
web/src/chapters/02-video/index.tsx
Normal file
90
web/src/chapters/02-video/index.tsx
Normal 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;
|
||||
480
web/src/chapters/03-core-point/CorePoint.css
Normal file
480
web/src/chapters/03-core-point/CorePoint.css
Normal 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; }
|
||||
}
|
||||
204
web/src/chapters/03-core-point/index.tsx
Normal file
204
web/src/chapters/03-core-point/index.tsx
Normal 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 HERO(step 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 SPLIT(step 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"><</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;
|
||||
641
web/src/chapters/04-role/Role.css
Normal file
641
web/src/chapters/04-role/Role.css
Normal 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);
|
||||
}
|
||||
297
web/src/chapters/04-role/index.tsx
Normal file
297
web/src/chapters/04-role/index.tsx
Normal 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 QUOTE(step 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 FLIP(step 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">></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 ALL(step 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;
|
||||
633
web/src/chapters/05-workflow/Workflow.css
Normal file
633
web/src/chapters/05-workflow/Workflow.css
Normal 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); }
|
||||
}
|
||||
266
web/src/chapters/05-workflow/index.tsx
Normal file
266
web/src/chapters/05-workflow/index.tsx
Normal 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 PIPELINE(step 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 DECIDE(step 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 Hands,10 分钟</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 SUMMARY(step 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">
|
||||
“Summarize EXTREMELY BRIEFLY — caveats and next steps only.”
|
||||
</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;
|
||||
564
web/src/chapters/06-anti-ai/AntiAi.css
Normal file
564
web/src/chapters/06-anti-ai/AntiAi.css
Normal 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);
|
||||
}
|
||||
|
||||
/* —— 反面教材 2:Emoji 滥用 —— */
|
||||
.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; }
|
||||
|
||||
/* —— 反面教材 5:data 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;
|
||||
}
|
||||
290
web/src/chapters/06-anti-ai/index.tsx
Normal file
290
web/src/chapters/06-anti-ai/index.tsx
Normal 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 HERO(step 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">></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 GRID(step 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 FONTS(step 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;
|
||||
539
web/src/chapters/07-oklch/Oklch.css
Normal file
539
web/src/chapters/07-oklch/Oklch.css
Normal 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;
|
||||
}
|
||||
306
web/src/chapters/07-oklch/index.tsx
Normal file
306
web/src/chapters/07-oklch/index.tsx
Normal 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 原文 prompt(L41-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 SOURCE(step 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 RULES(step 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 PIVOT(step 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>
|
||||
有个大问题 ——
|
||||
</Reveal>
|
||||
|
||||
<Reveal kind="tight" duration={1100} delay={1200} className="ok__pivot-issue" as="h2">
|
||||
感知<em>不均匀</em>
|
||||
</Reveal>
|
||||
</div>
|
||||
</SceneFade>
|
||||
|
||||
{/* ════════ Scene COMPARE(step 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 CLOSE(step 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;
|
||||
726
web/src/chapters/08-restraint/Restraint.css
Normal file
726
web/src/chapters/08-restraint/Restraint.css
Normal 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;
|
||||
}
|
||||
303
web/src/chapters/08-restraint/index.tsx
Normal file
303
web/src/chapters/08-restraint/index.tsx
Normal 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 JOBS(step 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 PAGE(step 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 PRINCIPLE(step 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">></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 CLOSE(step 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;
|
||||
636
web/src/chapters/09-verification/Verification.css
Normal file
636
web/src/chapters/09-verification/Verification.css
Normal 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);
|
||||
}
|
||||
277
web/src/chapters/09-verification/index.tsx
Normal file
277
web/src/chapters/09-verification/index.tsx
Normal 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 INTRO(step 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 AGENT(step 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 FORK(step 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 CHECK(step 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 CLOSE(step 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;
|
||||
631
web/src/chapters/10-to-skill/ToSkill.css
Normal file
631
web/src/chapters/10-to-skill/ToSkill.css
Normal 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);
|
||||
}
|
||||
248
web/src/chapters/10-to-skill/index.tsx
Normal file
248
web/src/chapters/10-to-skill/index.tsx
Normal 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 RECAP(step 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 PROBLEM(step 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 BANNED(step 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 PIVOT(step 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 SKILL(step 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 CLOSE(step 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;
|
||||
1141
web/src/chapters/11-skill-changes/SkillChanges.css
Normal file
1141
web/src/chapters/11-skill-changes/SkillChanges.css
Normal file
File diff suppressed because it is too large
Load Diff
663
web/src/chapters/11-skill-changes/index.tsx
Normal file
663
web/src/chapters/11-skill-changes/index.tsx
Normal 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 TREE(step 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 SCROLL(step 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 STRIP(step 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 01(step 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 02(step 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 03(step 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 04(step 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 CLOSE(step 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;
|
||||
438
web/src/chapters/12-references/References.css
Normal file
438
web/src/chapters/12-references/References.css
Normal 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);
|
||||
}
|
||||
197
web/src/chapters/12-references/index.tsx
Normal file
197
web/src/chapters/12-references/index.tsx
Normal 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 HERO(step 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 LIST(step 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 ORIGIN(step 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 CLOSE(step 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">
|
||||
—— 比让 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;
|
||||
464
web/src/chapters/13-closing/Closing.css
Normal file
464
web/src/chapters/13-closing/Closing.css
Normal 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;
|
||||
}
|
||||
240
web/src/chapters/13-closing/index.tsx
Normal file
240
web/src/chapters/13-closing/index.tsx
Normal 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 FAIR(step 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 JUMP(step 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 TRIO(step 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 RULES(step 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 CLOSE(step 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;
|
||||
591
web/src/chapters/14-outro/Outro.css
Normal file
591
web/src/chapters/14-outro/Outro.css
Normal 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);
|
||||
}
|
||||
279
web/src/chapters/14-outro/index.tsx
Normal file
279
web/src/chapters/14-outro/index.tsx
Normal 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 OPEN(step 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 EASY(step 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 GOAL(step 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 TRIPLE(step 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 BYE(step 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
36
web/src/chapters/index.ts
Normal 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
26
web/src/chapters/types.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import type { ComponentType } from 'react';
|
||||
|
||||
/** 单个章节的运行时上下文:传入到章节组件 props 中 */
|
||||
export interface ChapterContext {
|
||||
/** 当前章节内的局部 step(0..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
19
web/src/design/motion.ts
Normal 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
111
web/src/design/tokens.css
Normal 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
46
web/src/index.css
Normal 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
10
web/src/main.tsx
Normal 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>,
|
||||
)
|
||||
49
web/src/shared/LiveTicker.tsx
Normal file
49
web/src/shared/LiveTicker.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
78
web/src/shared/NumberTicker.tsx
Normal file
78
web/src/shared/NumberTicker.tsx
Normal 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-up,default 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
42
web/src/shared/Reveal.css
Normal 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
50
web/src/shared/Reveal.tsx
Normal 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,
|
||||
);
|
||||
}
|
||||
67
web/src/shared/SceneFade.tsx
Normal file
67
web/src/shared/SceneFade.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
54
web/src/stage/ChapterHost.tsx
Normal file
54
web/src/stage/ChapterHost.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
105
web/src/stage/ProgressBar.css
Normal file
105
web/src/stage/ProgressBar.css
Normal 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);
|
||||
}
|
||||
111
web/src/stage/ProgressBar.tsx
Normal file
111
web/src/stage/ProgressBar.tsx
Normal 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
21
web/src/stage/Stage.css
Normal 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
49
web/src/stage/Stage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
76
web/src/stage/useHotKeys.ts
Normal file
76
web/src/stage/useHotKeys.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { useEffect } from 'react';
|
||||
import { stepStore } from '../store/useStep';
|
||||
|
||||
/**
|
||||
* 全局快捷键 + 点击驱动 step。
|
||||
* - 点击 stage 任意空白处:next
|
||||
* - Space / →:next
|
||||
* - ←:prev
|
||||
* - Backspace:归零(debug)
|
||||
* - 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
99
web/src/store/useStep.ts
Normal 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-9:goToChapter(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
25
web/tsconfig.app.json
Normal 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
7
web/tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
24
web/tsconfig.node.json
Normal file
24
web/tsconfig.node.json
Normal 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
7
web/vite.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user