drawn-notebook-ui 是做什么的?
概述
Drawn Notebook 不是一个组件库。它是一种设计语言——一个连贯的哲学体系,其中的每个像素、每条线宽、每个间距节奏和每个动画时间曲线都服务于一个统一的目标。其指导原则是:界面应该像一个活生生的笔记本:用墨水画在奶油色的纸上,段落随着读者向下滚动而缓缓显现,导航线索像实体旅程一样贯穿页面。没有任何元素是喧闹的。也没有任何装饰为装饰本身而存在。UI 中的每个视觉元素都通过将其锚定于意义来赢得其存在的权利。
本技能是一份精确、全面的参考。实现此 UI 时,请按顺序遵循每个部分。不要跳过令牌定义——每个间距、颜色和运动值都是承重的。不要跳过深色模式部分——两种主题是同等重要的,必须始终同时保持正确。
第 1 节 — 设计哲学
1.1 核心原则
安静的自信,而非为极简而极简。 设计是简约的,但保留下来的每个元素都是有目的的。空白空间不是懒惰——它是页面在呼吸。用户的视线由构图、重量和运动引导,而非由色彩饱和度或装饰性点缀引导。
手绘美学,而非字面的拟物化。 设计参考了绘画工艺——细描边粗细、衬线编辑字体、纸质纹理、以线条画形式呈现的 SVG 插图——但它没有假装是纸。它是一个尊重模拟工艺触感的数字表面,而不笨拙地模仿它。
每个部分都是一章。 页面是连续滚动的,每个部分都像翻到笔记本的新一页。部分的背景色在暖中性色之间微妙变化。一条连续的金色线(旅程线 SVG)将它们视觉上缝合在一起。
运动是叙事,而非娱乐。 动画像手写字一样揭示内容——从底部向上出现,柔和地淡入,描摹 SVG 路径。按现代标准,持续时间较慢(显示动画为 1.2s–2s)。这是刻意的。速度意味着噪音。缓慢意味着工艺。
避免 AI 样板模式。 以下模式被明确禁止:紫色到粉红色渐变的英雄背景、高度和圆角半径完全相同的卡片网格、悬停时发光的霓虹效果、大多数元素上过大的圆角(> 14px)、带有百分比的彩色技能条、带有粗体日期和项目符号的时间线,以及任何使用 emoji 作为装饰的行为。
第 2 节 — 设计令牌
2.1 颜色调色板 — 亮色模式(日光笔记本)
亮色模式下的每种颜色都是源自纸张、奶油色和天然墨水的暖中性色。有一个单一的强调色系列:金色/赤陶色,从柔和的琥珀色到温暖的砖红色。
/* 亮色模式(应用于 :root) */
--cream: #F7F5F0; /* 页面背景 — 主要表面 */
--sage: #EEF3ED; /* “关于”部分背景 — 最浅的绿灰色 */
--parchment: #F1ECE3; /* “经历 + 简历”部分背景 */
--warm-white: #FAF8F5; /* “技能”部分背景 — 最浅的表面 */
--linen: #F3EFE8; /* “项目”部分背景 */
--sage-tint: #EFF3EE; /* “学习”部分背景 */
--charcoal: #1A1A1A; /* 主要文本、SVG 描边、边框 */
--soft-black: #2C2C2C; /* 次要文本、强标签 */
--warm-grey: #8A8580; /* 柔化标签、时间戳、字幕 */
--light-grey: #C5BFB8; /* 滚动提示、装饰性分隔符 */
--terracotta: #B87A5A; /* 悬停状态、项目符号、活动路径点 */
--indigo: #6B6EA0; /* 保留 — 不在主要模式中使用 */
--gold: #C4956A; /* 主要强调色 — 旅程线、技能图标、筹码 */
--gold-dim: rgba(196, 149, 106, 0.3); /* 用于阴影、边框的柔和金色 */
--blush: #F0EBE5; /* 悬停时筹码/徽章背景 */
--sage-dark: #8B9E8B; /* 部分边框分隔色调 */
2.2 颜色调色板 — 深色模式(夜间笔记本)
深色模式不是简单的反色。背景从温暖的奶油色转变为深暖黑色——一种几乎带有墨渍特征的质感。强调色略微变亮,以保持在深色表面上的可读性。
/* 深色模式(应用于 :root[data-theme="dark"]) */
--cream: #141210; /* 页面背景 — 深暖黑色 */
--sage: #171816; /* “关于”部分背景 */
--parchment: #1A1815; /* “经历 + 简历”背景 */
--warm-white: #1E1B18; /* “技能”部分背景 */
--linen: #181614; /* “项目”部分背景 */
--sage-tint: #161815; /* “学习”部分背景 */
--charcoal: #EDE8E1; /* 主要文本 — 暖米白 */
--soft-black: #D8D2C8; /* 次要文本 */
--warm-grey: #9E978F; /* 柔化标签 */
--light-grey: #38342E; /* 装饰性分隔符 */
--terracotta: #D98A62; /* 悬停强调色 — 比亮色模式下略亮 */
--indigo: #8C8FB8;
--gold: #D4A373; /* 主要强调色 — 比亮色模式下略暖 */
--gold-dim: rgba(212, 163, 115, 0.3);
--blush: #241E1A;
--sage-dark: #9EAF9E;
/* 表面令牌 */
--card-glass: rgba(30, 27, 24, 0.72);
--card-glass-hover: rgba(42, 38, 34, 0.95);
--card-item-bg: rgba(24, 21, 19, 0.78);
--card-item-hover: rgba(38, 34, 30, 0.96);
--card-border: rgba(212, 163, 115, 0.22);
--card-border-hover: rgba(212, 163, 115, 0.48);
--card-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
--card-shadow-hover: 0 6px 20px rgba(0, 0, 0, 0.45);
--card-inset: inset 0 1px 0 rgba(255, 255, 255, 0.05);
--card-inset-hover: inset 0 1px 0 rgba(255, 255, 255, 0.08);
--badge-bg: rgba(24, 21, 19, 0.88);
--grain-opacity: 0.022; /* 深色模式下较低,避免浑浊 */
2.3 颜色调色板 — 亮色模式表面令牌
/* 亮色模式表面令牌 */
--card-glass: rgba(255, 255, 255, 0.65);
--card-glass-hover: #ffffff;
--card-item-bg: rgba(247, 245, 240, 0.8);
--card-item-hover: rgba(255, 255, 255, 0.98);
--card-border: rgba(196, 149, 106, 0.22);
--card-border-hover: rgba(196, 149, 106, 0.45);
--card-shadow: 0 4px 16px rgba(26, 26, 26, 0.04);
--card-shadow-hover: 0 6px 20px rgba(196, 149, 106, 0.12);
--card-inset: inset 0 1px 0 rgba(255, 255, 255, 0.9);
--card-inset-hover: inset 0 1px 0 rgba(255, 255, 255, 0.95);
--badge-bg: rgba(247, 245, 240, 0.82);
--grain-opacity: 0.035;
2.4 主题切换 — 无闪烁实现
主题检测必须在页面渲染前的 <head> 中同步运行,以防止错误主题闪烁(FOUT)。这是一个关键的实现要求:
<head>
<!-- 必须是 <head> 中的第一个脚本,在所有样式表链接之前 -->
<script>
(function () {
try {
var stored = localStorage.getItem('color-scheme');
var prefersDark = window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches;
var theme = (stored === 'dark' || (!stored && prefersDark)) ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', theme);
var meta = document.querySelector('meta[name="color-scheme"]');
if (meta && (stored === 'dark' || stored === 'light')) meta.content = stored;
var themeColor = document.querySelector('meta[name="theme-color"]');
if (themeColor) themeColor.content = theme === 'dark' ? '#141210' : '#F8F4F0';
} catch (e) {}
})();
</script>
</head>
在 Flutter/Dart 中,使用 ThemeMode.system 作为默认值,并通过 SharedPreferences 持久化覆盖设置。键是 color-scheme,值是 'dark' 或 'light'。
2.5 排版令牌
--font-serif: 'Cormorant Garamond', 'Georgia', serif;
--font-sans: 'Inter', 'Helvetica Neue', sans-serif;
Cormorant Garamond 对于此设计的编辑风格是必不可少的。它加载了 300、400、500、600 四种字重,并带有 300 和 400 的斜体变体。它用于所有标题、章节标题、英雄名称、筹码、导航栏标签、路径点消息,以及任何需要“书写”质感的地方。它的字重绝不能超过 600——这不是一款粗体字体。
Inter 是功能性辅助字体。它用于字幕、技能标签、时间戳、大写字母提示以及密集信息上下文中的正文。仅加载 300、400、500 字重。
/* Google Fonts 导入 */
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,500;0,600;1,300;1,400&family=Inter:wght@300;400;500&display=swap');
2.6 间距令牌
所有间距使用 clamp() 函数保持流动性。不要对部分内边距或内容宽度限制使用硬编码像素值。
--section-pad: clamp(80px, 12vh, 160px); /* 主要部分的垂直内边距 */
--content-max: 800px; /* 窄版列 */
--content-wide: 1100px; /* 网格的宽布局 */
--side-pad: clamp(24px, 5vw, 80px); /* 水平页边距 */
2.7 动画令牌
--ease-out: cubic-bezier(0.25, 0.46, 0.45, 0.94); /* 通用缓出 */
--ease-smooth: cubic-bezier(0.4, 0, 0.2, 1); /* 类似 Material 的平滑 */
--duration-slow: 1.2s; /* 显示动画 */
--duration-med: 0.8s; /* 中等过渡 */
--duration-fast: 0.4s; /* 快速微交互 */
第 3 节 — 全局基础
3.1 CSS 重置和基础
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 16px;
scroll-behavior: smooth;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
background-color: var(--cream);
color: var(--charcoal);
font-family: var(--font-sans);
font-weight: 300;
line-height: 1.7;
overflow-x: hidden;
position: relative;
}
3.2 文本选择
选择高亮是柔和的金色洗色——它反映了强调色系,但不具侵略性:
::selection {
background: rgba(196, 149, 106, 0.2);
color: var(--charcoal);
}
::-moz-selection {
background: rgba(196, 149, 106, 0.2);
color: var(--charcoal);
}
3.3 SVG 描边适配
所有内联 SVG 使用亮色模式下的硬编码十六进制描边(#1A1A1A、#2C2C2C)。为了在不修改 SVG 标记的情况下适配深色模式,请使用 CSS 属性选择器覆盖:
svg [stroke="#1A1A1A"], svg [stroke="#1a1a1a"] { stroke: var(--charcoal); }
svg [fill="#1A1A1A"], svg [fill="#1a1a1a"] { fill: var(--charcoal); }
svg [stroke="#2C2C2C"], svg [stroke="#2c2c2c"] { stroke: var(--soft-black); }
svg [fill="#2C2C2C"], svg [fill="#2c2c2c"] { fill: var(--soft-black); }
svg text[fill="#1A1A1A"] { fill: var(--charcoal); }
svg text[fill="#8A8580"] { fill: var(--warm-grey); }
SVG 中的金色强调色(#C4956A)有意不覆盖。 金色强调色是两种主题中的固定参考色;在深色模式下,令牌 --gold 变亮为 #D4A373,但内联 SVG 金色不需要重新映射,因为与深色背景的对比度仍然可以接受。
3.4 平滑主题过渡
对主题切换时发生变化的每个表面应用协调的过渡。这可以防止突兀的颜色跳跃:
body, section,
.about, .experience, .resume-section, .skills, .projects, .learning, .closing,
.exp-card, .exp-item, .skill-cell, .compact-card, .compact-card__icon,
.about__point, .about__visual-card, .about__chip, .closing__link,
.journey-waypoint__message, .rail-node__label, .resume-modal__panel,
.resume-modal__header {
transition:
background-color 0.35s var(--ease-out),
color 0.35s var(--ease-out),
border-color 0.35s var(--ease-out);
}
/* 始终尊重用户的运动偏好 */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { transition: none !important; }
}
第 4 节 — 纸张纹理覆盖层
一个微妙的纸张纹理作为全视口固定覆盖层应用。它位于 z-index: 9999 — 在所有内容之上 — 但 pointer-events: none 确保它永远不会拦截交互。
.grain-overlay {
position: fixed;
inset: 0;
pointer-events: none;
z-index: 9999;
opacity: var(--grain-opacity, 0.035);
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E");
background-repeat: repeat;
background-size: 200px 200px;
transform: translateZ(0);
will-change: transform;
transition: opacity 0.35s var(--ease-out);
}
/* 在深色模式下,屏幕混合使纹理更自然地显现 */
:root[data-theme="dark"] .grain-overlay {
mix-blend-mode: screen;
}
在 Flutter 中: 使用位于小部件树顶部的 CustomPaint 小部件(带有基于 shader 的噪声模式)来重现此效果,或者将纹理烘焙为透明度约为 3% 的 PNG 资源,并在深色模式下使用 BlendMode.screen。
第 5 节 — 主题切换(天体开关)
主题切换是一个小的药丸状按钮,固定定位在右上角。它
…
[为翻译而截断]